NodeMCU WiFi setup

NodeMCU devices have WiFi built in, and it’s easy to configure using Lua.

One of the easiest examples of setting this up was on Limpkin’s blog. I took this and tweaked it slightly so it works with the more recent Lua wifi module.

station_cfg={}
station_cfg.ssid="Rob's iPhone"
station_cfg.pwd="secret password"
wifi.setmode(wifi.STATION)
wifi.sta.config(station_cfg)
wifi.sta.connect()
tmr.alarm(1, 1000, 1, function()
    if wifi.sta.getip() == nil then
        print("IP unavailable, Waiting...")
    else
        tmr.stop(1)
        print("ESP8266 mode is: " .. wifi.getmode())
        print("The module MAC address is: " .. wifi.ap.getmac())
        print("Config done, IP is "..wifi.sta.getip())
    end
end)

Upload this using luatool or similar as the init.lua file that runs when the NodeMCU powers up.

In this example, the script tries to connect to my iPhone hotspot. We use tmr.alarm to check once a second if we have an IP address. If we don’t, we let the user know. If we do, we stop the timer from continually executing, and print out some of our wifi configuration details.

It’s at this point we could start using the connection to talk to the internet if we needed to.

Using an i2c 128×64 OLED display with Lua on a NodeMCU

Small 128×64 OLED i2c displays can picked up for just over £3 on Amazon, and they are great for interfacing with a NodeMCU IoT device.

Wiring is simple…

  • NodeMCU 3.3v – OLED VCC
  • NodeMCU GND – OLED GND
  • NodeMCU D1 – OLED SDA
  • NodeMCU D2 – OLED SCL or SCK

Wiring an i2c OLED display to a NodeMCU

Your NodeMCU firmware must have been compiled with the i2c and u8g modules included. If you need to add these, see my previous post – NodeMCU Lua setup using a Mac. It also explains how to upload the example Lua code below.

The u8g module comes with some great examples. I’ve taken some of the relevant parts for a simple two page Lua script that prints out two alternating screens of text.

sda = 1 -- SDA Pin
scl = 2 -- SCL Pin

function init_display(sda,scl) --Set up the u8glib lib
     sla = 0x3C
     i2c.setup(0, sda, scl, i2c.SLOW)
     disp = u8g.ssd1306_128x64_i2c(sla)
     disp:setFont(u8g.font_6x10)
     disp:setFontRefHeightExtendedText()
     disp:setDefaultForegroundColor()
     disp:setFontPosTop()
end

function updateDisplay(func)
  -- Draws one page and schedules the next page, if there is one
  local function drawPages()
    func()
    if (disp:nextPage() == true) then
      node.task.post(drawPages)
    end
  end
  -- Restart the draw loop and start drawing pages
  disp:firstPage()
  node.task.post(drawPages)
end

function drawScreen1()
     disp:drawFrame(2,2,126,62)
     disp:drawFrame(5,5,121,57)
     disp:drawStr(5, 20, "  RobertPrice.co.uk")
     disp:drawStr(5, 35, "    @robertprice")
end

function drawScreen2()
     disp:drawFrame(2,2,126,62)
     disp:drawFrame(5,5,121,57)
     disp:drawStr(5, 20, "  NodeMCU OLED Test")
     disp:drawStr(5, 35, "   12th July 2017")
end

local drawDemo = { drawScreen1, drawScreen2 }

function demoLoop()
  -- Start the draw loop with one of the demo functions
  local f = table.remove(drawDemo,1)
  updateDisplay(f)
  table.insert(drawDemo,f)
end

init_display(sda,scl)
demoLoop()
tmr.alarm(4, 5000, 1, demoLoop)

This may look a bit complex for just displaying text, but it aims to be reusable. We use the tmr.alarm to avoid bogging the CPU down in a loop.

NodeMCU driving an i2c OLED display

I’ve tried this with two different i2c OLED displays, and they both work.

NodeMCU Lua setup using a Mac

I recently bought a NodeMCU. This is a small ESP8266 based card with built in WiFi, MicroUSB, and a Lua interpreter that can be used for developing IoT (Internet of Things) devices.

I had trouble getting it working initially, so I wanted to share how I fixed this on a Mac.

Install Mac drivers

If you’re not using a Mac, you can skip this part.

You plug the NodeMCU into the Mac via USB. The Mac won’t support this by default, and you need to install a driver. The driver you need to install is the Silcon Labs CP210x USB to UART Bridge.

Once installed, plug in the NodeMCU and check that the device /dev/tty.SLAB_USBtoUART exists.

To connect to the NodeMCU, you’ll need some tools. To test, download CoolTerm. In Options, for Port select SLAB_USBtoUART, and for Baudrate select 115200.

If you are lucky you’ll get a prompt, if not you may need to build and install some new firmware.

If you do get a prompt type…

print "Hello";

… and Lua should echo “Hello” back to you.

Build and install the NodeMCU firmware

If you need to build new firmware, there is a very useful online site called NodeMCU-Build.com that I used to build the firmware with the right modules I wanted for my project.

Once you’ve build the firmware, you’ll need to install the Python esptool.py to flash the firmware to the NodeMCU.

git clone https://github.com/themadinventor/esptool.git
cd esptool
sudo python ./setup.py install

Check the flash size of your NodeMCU.

To flash the firmware hold down “FLASH” and press “RST” on the NodeMCU, then use the following command (remembering to disconnect CoolTerm first if connected)…

esptool.py --port /dev/tty.SLAB_USBtoUART write_flash -fm dio 0x00000 ~/Downloads/nodemcu-master-12-modules-2017-07-07-21-24-03-float.bin

As esptool.py runs, you should see something like this…

esptool.py v2.0.1
Connecting........_
Detecting chip type... ESP8266
Chip is ESP8266
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Auto-detected Flash size: 4MB
Flash params set to 0x0240
Compressed 754992 bytes to 505060...
Wrote 754992 bytes (505060 compressed) at 0x00000000 in 44.5 seconds (effective 135.6 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting...

Connect to it using CoolTerm, then press the “RST” button. You should see some messy characters, then something like following (depending on what you built into your firmware)…

NodeMCU custom build by frightanic.com
.branch: master
.commit: c8ac5cfb912ff206b03dd7c60ffbb2dafb83fe5e
.SSL: true
.modules: adc,bit,cron,crypto,encoder,file,gpio,http,i2c,net,node,ow,pcm,sjson,sntp,spi,struct,tmr,u8g,uart,websocket,wifi,tls
 build .built on: 2017-07-12 09:30
 powered by Lua 5.1.4 on SDK 2.1.0(116b762)
lua: cannot open init.lua
>

Upload Lua code to the NodeMCU

The best way to get Lua code onto the NodeMCU is to use the Python luatool.

git clone https://github.com/4refr0nt/luatool
cd luatool

To test we’ll upload a simple Lua script that will blink the NodeMCU’s onboard LED, save this as “blink.lua”.

LED_PIN = 0

gpio.mode(LED_PIN, gpio.OUTPUT)
value = true

tmr.alarm(0, 500, 1, function ()
    gpio.write(LED_PIN, value and gpio.HIGH or gpio.LOW)
    value = not value
end)

Upload it to the NodeMCU using the following command (making sure Coolterm is disconnected first)…

python luatool/luatool.py --port /dev/tty.SLAB_USBtoUART --src blink.lua --dest init.lua --dofile

You should see the following…

->file.open("init.lua", "w") -> ok
->file.close() -> ok
->file.remove("init.lua") -> ok
->file.open("init.lua", "w+") -> ok
->file.writeline([==[LED_PIN = 0]==]) -> ok
->file.writeline([==[]==]) -> ok
->file.writeline([==[gpio.mode(LED_PIN, gpio.OUTPUT)]==]) -> ok
->file.writeline([==[value = true]==]) -> ok
->file.writeline([==[]==]) -> ok
->file.writeline([==[tmr.alarm(0, 500, 1, function ()]==]) -> ok
->file.writeline([==[gpio.write(LED_PIN, value and gpio.HIGH or gpio.LOW)]==]) -> ok
->file.writeline([==[value = not value]==]) -> ok
->file.writeline([==[end)]==]) -> ok
->file.flush() -> ok
->file.close() -> ok
->dofile("init.lua") -> send without check
--->>> All done <<<--

The onboard LED on the NodeMCU should now be blinking.

The NodeMCU executes the init.lua script by default, so when we upload we tell luatool to upload our blink.lua script as init.lua.

Analysing Beat The Street data using R

Beat The Street is a game currently running in East Sussex with the aim of increasing the amount of walking and cycling people do. Players are given a free Beat The Street card that they tap on Beat Boxes to record their journeys. At least 2 different Beat Boxes must be tapped in the space of an hour for points to be recorded. Each 2nd box tapped within an hour of the last earns at least 10 points.

While there isn’t an officially documented API to the Beat The Street data, the website itself is built using JSON feeds we can use to extract data ourselves. There can be multiple schemes running at once. I’m playing in East Sussex which is scheme 47, we’ll need this ID shortly.

Firstly we’ll need to login as this sets a cookie that identifies us for subsequent calls.

library(httr)
loginurl <- "https://beatthestreet.me/api/Login?schemeId=47"
loginbody <- list(Username = "Your Username", Password = "Your Password", RememberMe = "false")
r <- POST(loginurl, body = loginbody, encode = "form", verbose())

We can use R's httr library to handle the login for us. We POST our details to the form. I'm using the verbose() option for debugging, but that's optional.

Now we are logged in, we should have a cookie set identifying us, so we can request a record of our journeys so far.

swipesurl <- "https://beatthestreet.me/api/UserCardSwipesWidget?schemeId=47"
r <- GET(swipesurl, accept_json())
cont <- content(r, as = "parsed", type="application/json")

The returned JSON data is an array of objects looking like this.

[
    {
        "SwipeDatetime":"2017-07-11T07:18:38",
        "Name":"E019",
        "Description":"E.Sussex",
        "Line1":
        "N/A",
        "Line2":"N/A",
        "Line3":"N/A",
        "Postcode":"ES1",
        "City":"E.Sussex",
        "Points":10
    },{
        "SwipeDatetime":"2017-07-11T07:14:40",
        "Name":"E024",
        "Description":"E.Sussex",
        "Line1":"N/A",
        "Line2":"N/A",
        "Line3":"N/A",
        "Postcode":"ES1",
        "City":"E.Sussex",
        "Points":0
    }
]

We can parse this by calling content().

The values are all strings apart from "Points" which is an integer. However, sometimes Beat The Street returns a null value, which we'll want to convert to an R NA. We can do this by applying a small utility function over the data to convert any nulls to NA.

nullToNA <- function(x) {
    x[sapply(x, is.null)] <- NA
    return(x)
}
cont <- lapply(cont, nullToNA)

Now we can convert our data to a data.frame, cast the "SwipeDatetime" as a DateTime, and add in an extra field called "Date" that's just the Date without the time.

library(plyr)
swipes <- ldply(cont, data.frame)
swipes$SwipeDatetime <- as.POSIXct(swipes$SwipeDatetime, format = "%Y-%m-%dT%H:%M:%S")
swipes$Date <- as.Date(swipes$SwipeDatetime)

Now we have our data in usable format, we can extract some useful information.

I can plot a graph of how many Beat Boxes I've tapped each day.

library(ggplot2)
ggplot(swipes, aes(Date)) + geom_bar(stat="count") + ggtitle("Beat Boxes Visited Per Day") + ylab("Visits") + xlab("Date") + theme(axis.text.x = element_text(angle = 90, hjust = 1))

Number of Beat Boxes visited by day

Or I can plot which Beat Boxes I visit most.

swipesFrame <- data.frame(sort(table(swipes$Name)))
ggplot(data=swipesFrame, aes(x=Var1, y=Freq)) + geom_bar(stat="identity") + ggtitle("Most Visited Beat Boxes") + ylab("Visits") + xlab("Beat Box") + theme(axis.text.x = element_text(angle = 90, hjust = 1))

Most visited Beat Boxes

I can even plot this onto a map assuming I have the latitude and longitudes of the Beat Boxes already available in a data.frame called "beatboxes", and the map in "map". The larger the frequency of visit, the larger the plotted point on the map.

library(ggmap)
swipeFrameLatLng <- merge(swipesFrame, beatboxes, by.x = "Var1", by.y="id")
ggmap(map) + geom_point(data = swipeFrameLatLng, aes(x = longitude, y = latitude, color="Red", size=Freq*10)) + ylab("") + xlab("") + guides(colour = "colorbar",size = "none")

Most visited Beat Boxes map

Using file_get_contents to PUT JSON data over HTTPS to a webservice

It’s possible to use PHP’s file_get_contents to PUT JSON data to a RESTful webservice if you don’t want to use cURL.

In this example I’m going to try to add or update a person using the NationBuilder API.

Firstly, we need the URL of the webservice, and the data you want to update as a JSON string.

$access_token = 'set access token here';
$nation = 'set nation name here';
$url = "https://$nation.nationbuilder.com/api/v1/people/push?access_token=$access_token";
// details we want to update
$data = [
    'person' => [
        'email_opt_in' => true,
        'do_not_contact' => false,
        'do_not_call' => false,
        'first_name' => 'Robert',
        'last_name' => 'Price',
        'email' => 'robert.price@email.adress',
        'home_address' => [
            'zip' => 'AB1 2CD'
        ],
        'bio' => 'Software developer - robertprice.co.uk'
    ]
];
$json_data = json_encode($data);

Now we need to setup a stream_context so file_get_contents knows how to handle the request.

Even though we’re PUTing data over HTTPS, we need to use the http context. We set method to PUTContent-type to application/json, Accept to application/json, Connection to close, and Content-length to the length of our JSON string. We’re PUTing this data over HTTP1.1 so we set the to 1.1. We’re sending the JSON in the body of the request, so we set this in the content.

As we are using HTTPS, we need to configure the ssl settings. In this case I’m not going to verify the peer or the peer name.

$context = stream_context_create([
    'http' => [
        'method' => 'PUT',
        'header' => "Content-type: application/json\r\n" .
                    "Accept: application/json\r\n" .
                    "Connection: close\r\n" .
                    "Content-length: " . strlen($json_data) . "\r\n",
        'protocol_version' => 1.1,
        'content' => $json_data
    ],
    'ssl' => [
        'verify_peer' => false,
        'verify_peer_name' => false
    ]
]);

Finally we PUT the data by calling file_get_contents. NationBuilder returns a JSON response to this call, so we can decode that and echo a confirmation message to the screen.

$rawdata = file_get_contents($url, false, $context);
if ($rawdata === false) {
    exit("Unable to update data at $url");
}
$data = json_decode($rawdata, true);
if (JSON_ERROR_NONE !== json_last_error()) {
    exit("Failed to parse json: " . json_last_error_msg());
}
echo "Updated details for {$data['person']['first_name']} {$data['person']['last_name']}\n";

The full code for this, and some other examples of querying NationBuilder using PHP, can be found in my NationBuilder People In Nation – GitHub repository.

You may also be interested in my previous article on POSTing JSON to Web Service.

Using a motion sensor on a Raspberry Pi with PHP

Adding a motion sensor to a Raspberry Pi is easy. In this article I’ll show you how to use it with PHP.

The HC-SR501 is a pyroelectric infrared PIR motion sensor module, that connects to the Raspberry Pi using just 3 wires.

Connect GND to GND on the Pi, VCC to +5V on the Pi, and OUT to GPIO 17 on the Pi. That’s all that’s needed.

Added a HC-SR501 motion detector to a Raspberry Pi

To use the sensor with PHP we use the PHPi library. This library supports event driven bindings for the Raspberry Pi GPIO. We install this using Composer.

composer require calcinai/phpi

To use the PHPi library we need to bring it in using the autoloader, and add some namespace shortcuts.

require 'vendor/autoload.php';

use Calcinai\PHPi\Pin;
use Calcinai\PHPi\Pin\PinFunction;

We then create a board

$board = \Calcinai\PHPi\Factory::create();

We now need to declare that we’re using GPIO pin 17 for input.

$pin = $board->getPin(17)
             ->setFunction(PinFunction::INPUT)
             ->setPull(Pin::PULL_UP);

As this is event driven programming, we need to listen to the pin to see when it changes. When the pin goes high, we know motion has been detected and we can do something. In this example, we’ll just write a message to the screen.

$pin->on('level.high', function() {
        echo "Motion detected\n";
});

Finally we start the start the loop running, so the script is listening and reacting to events.

$board->getLoop()->run();

Putting it all together

Here’s the final working PHP to detect motion using the HC-SR501

<?php

require 'vendor/autoload.php';

use Calcinai\PHPi\Pin;
use Calcinai\PHPi\Pin\PinFunction;

$board = \Calcinai\PHPi\Factory::create();

$pin = $board->getPin(17) //BCM pin number
             ->setFunction(PinFunction::INPUT)
             ->setPull(Pin::PULL_UP);

$pin->on('level.high', function() {
        echo "Motion detected\n";
});

$board->getLoop()->run();

Reading a temperature sensor using PHP on a Raspberry Pi

It’s easy to add a temperature sensor to a Raspberry Pi. In this example I’ll explain how to set it up and access the data using PHP.

The DS18b20 is a great digital temperature sensor. It only needs three wires and a resistor to get it working on the Raspberry Pi.

The red wire is +3.3v, the black is ground, and yellow is data.

The resistor is connected between red and yellow to pull up the voltage on the data line.

Red is connected to pin 1 on the GPIO, black to pin 6, and yellow to pin 7.

Connecting a temperature sensor to a Raspberry Pi

Reading the temperature

Now the circuit is ready, we can access the data. We need to enable the relevant modules on the Raspberry Pi to do this.

modprobe w1-gpio
modprobe w1-therm

If we now look in the /sys/bus/w1/devices/ directory, we should see a directory starting with 28. This is where we can find the temperature data. Inside this directory is a file called w1_slave. This is the file we read get the data. When we read it, it actually asks the sensor for the data and return it. This means there is a slight delay before the data returns.

pi@Nowscreen:~ $ cat /sys/bus/w1/devices/28-031683a865ff/w1_slave
95 01 4b 46 7f ff 0c 10 65 : crc=65 YES
95 01 4b 46 7f ff 0c 10 65 t=25312

The temperature is the value t=25312. We divide this by 1000 to get the temperature of 25.312 degrees celcius.

Reading the temperature with PHP

The first thing we need to do is to find the directory where the w1_slave file is. We can use globbing to help here.

$base_dir = '/sys/bus/w1/devices/';
$device_folder = glob($base_dir . '28*')[0];
$device_file = $device_folder . '/w1_slave';

Now we need to read in the data. We can use the file method as this returns each line of the file in an array.

$data = file($device_file, FILE_IGNORE_NEW_LINES);

Now we extract the temperature. We check the first line is correct by checking for the value “YES” at the end of the line. If this is present we get the value for “t=” at the end of the second line. Finally we divide the value by 1000, and return it.

$temperature = null;
if (preg_match('/YES$/', $data[0])) {
    if (preg_match('/t=(\d+)$/', $data[1], $matches, PREG_OFFSET_CAPTURE)) {
        $temperature = $matches[1][0] / 1000;
    }
}

Now we can display the temperature.

if ($temperature) {
    echo "Temperature is ${temperature}C\n";
} else {
    echo "Unable to get temperature\n";
}

Final PHP temperature sensor code

Here’s the finished code. I’ve also included two system calls to modprobe to ensure the necessary modules are loaded before reading.

<?php

exec('modprobe w1-gpio');
exec('modprobe w1-therm');

$base_dir = '/sys/bus/w1/devices/';
$device_folder = glob($base_dir . '28*')[0];
$device_file = $device_folder . '/w1_slave';

$data = file($device_file, FILE_IGNORE_NEW_LINES);

$temperature = null;
if (preg_match('/YES$/', $data[0])) {
    if (preg_match('/t=(\d+)$/', $data[1], $matches, PREG_OFFSET_CAPTURE)) {
        $temperature = $matches[1][0] / 1000;
    }
}

if ($temperature) {
    echo "Temperature is ${temperature}C\n";
} else {
    echo "Unable to get temperature\n";
}

Controlling a LED on a Raspberry Pi with PHP

I wanted to make a LED light up using PHP on a Raspberry Pi. Most of the examples I’ve seen are for Python, so I wanted to see if was possible or not.

It is actually pretty easy to do, so I thought I’d share my work.

The first thing to do is to wire up the LED to the Raspberry Pi. I used a breadboard so there is no soldering required.

Connect the LED to GPIO pin 18, along with a 330ohm resistor, to GND. We need the resistor to limit the current through the LED otherwise it could burn out.

That’s the LED attached to Raspberry Pi, now we need to control it with PHP code.

PHP LED Control Code

Before we can write any PHP, we need to enable the gpio module on the Raspberry Pi.

sudo modprobe w1-gpio

We need a third party PHP library to be able to talk to the GPIO pins. I’m using php-gpio.

We need to install this with Composer before we can use it.

get http://getcomposer.org/composer.phar
php composer.phar require ronanguilloux/php-gpio

We can now start our PHP code by using the autoloader to bring in the GPIO library.

require 'vendor/autoload.php';

use PhpGpio\Gpio;
$gpio = new GPIO();

Now to control the LED we need to make sure GPIO pin 18 is set to “out”.

$gpio->setup(18, 'out');

To turn the LED on we write the value “1” to pin 18.

$gpio->output(18, 1);

To turn the LED off we write the value “0” to pin 18.

$gpio->output(18, 0);

Finally, we need to make sure we clean up after ourselves.

$gpio->unexportAll();

The Final Code

Bringing this all together, we can write a simple PHP script to turn the LED on, wait 1 second, and turn it off again.

<?php

require 'vendor/autoload.php';

use PhpGpio\Gpio;
$gpio = new GPIO();
$gpio->setup(18, 'out');

echo "LED on\n";
$gpio->output(18, 1);

sleep(1);

echo "LED off\n";
$gpio->output(18, 0);

$gpio->unexportAll();

The final code needs to be run as root to be able to access gpio.

Sparkline Creation Using PHP

Sparkline graphs have had a bit of press lately, so I’ve been playing about with generating them on my commutes to and from work.

If you’ve not heard of them before, a Sparkline is a small graph that is often used inline with text.

I’ve been using PHP, so I’ve created a simple class called Spark, with a single static method called line. This returns a simple string as an inline graph from an array of values that have been passed in.

Sparkline Examples

Here’s an example plotting a simple staircase type graph.

require_once('lib/Spark.php');

echo Spark::line(array(1, 2, 3, 4, 5, 6, 7, 8)) . "n";

> ▁▂▃▄▅▆▇█

Here’s an example plotting a cosine wave.

$cos = array();
for($i=0; $i <= 360; $i=$i+20) {
    $cos[] = cos($i * M_PI / 180);
}
echo Spark::line($cos) . "n";

> ██▇▆▅▄▃▂▁▁▁▂▃▄▅▆▇██

Summary

The code is available on GitHub as php-spark, and is free to use. It was inspired by a simple project for Go called Spark, which was inspired from elsewhere.

Fixing The National Rail Mobile Website For iPhones

I’ve been frustrated that the National Rail Enquiries mobile website doesn’t work properly on my Apple iPhone 4s, running iOS7, so I decided to take a look and see what is actually going on.

You can turn on debugging on your iPhone in the settings, so I turned this on, and loaded Safari on my Macbook Pro to take a look at what was actually happening.

The “Go” button on the mobile website acts as a submit button so I put a breakpoint on it to see what happened.

The event handler calls a function called setRecents, and it fails when it tries to save my recent stations to localStorage.

if (hasStorage) {
    localStorage.setItem("recentStations", recVal);
} else {
    setCookie("recentStations", recVal, 365);
}

The hasStorage global variable has a true value, allowing it to try to save to local storage, so I tried setting this to false and tried again. This time to website worked as it falls back to using cookies. So the problem is with localStorage on mobile Safari, I’m not sure why.

I then took a look to see how hasStorage was set. It was being set in the following function.

function checkStorage() {
    // Feature detect + local reference
    var storage,
    fail,
    uid;
    try {
        uid = new Date;
        (storage = window.localStorage).setItem(uid, uid);
        fail = storage.getItem(uid) != uid;
        storage.removeItem(uid);
        fail && (storage = false);
    } catch (e) {}
    if (storage) {
        hasStorage = true; //if localstorage available
    }
};

I set breakpoints through the function, reloaded and single stepped what was happening in the debugger.

The uid was correctly set to a Date object with the current date and time. The next line was where the problem occured. storage was assigned as a Storage object, but the setItem method failed. This meant that the storage variable was set, but an exception was thrown. The exception handler does nothing, so the line after was executed. This sees that storage is available, but not that it didn’t work, so sets hasStorage incorrectly to true.

The fix would be to modify the exception handler to set storage to false instead of doing nothing.

If any developers of the National Rail website are reading this, please add this line so I can use your website to plan my train journeys again using my iPhone!

The National Rail mobile website on an iPhone

All information correct at time of writing and publishing. 20th April 2014.