diff --git a/.gitignore b/.gitignore
index 0a00e2c..75bcb86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+config.custom.h
+settings.custom.cpp
+
 Reports/*.pdf
 
 # Created by https://www.gitignore.io/api/libreoffice
diff --git a/.gitmodules b/.gitmodules
index 29a9c01..c8dfb1d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,9 +4,18 @@
 [submodule "iot/libraries/Entropy"]
 	path = iot/libraries/Entropy
 	url = https://github.com/taoyuan/Entropy.git
-[submodule "iot/libraries/arduino-lmic"]
-	path = iot/libraries/arduino-lmic
-	url = https://github.com/mcci-catena/arduino-lmic.git
 [submodule "iot/libraries/TinyGPSPlus"]
 	path = iot/libraries/TinyGPSPlus
 	url = https://github.com/mikalhart/TinyGPSPlus.git
+[submodule "iot/libraries/arduino-lmic"]
+	path = iot/libraries/arduino-lmic
+	url = https://github.com/matthijskooijman/arduino-lmic.git
+[submodule "iot/libraries/Arduino-MemoryFree"]
+	path = iot/libraries/Arduino-MemoryFree
+	url = https://github.com/mpflaga/Arduino-MemoryFree.git
+[submodule "iot/libraries/SdFat"]
+	path = iot/libraries/SdFat
+	url = https://github.com/greiman/SdFat.git
+[submodule "iot/libraries/TinyGPS"]
+	path = iot/libraries/TinyGPS
+	url = https://github.com/mikalhart/TinyGPS.git
diff --git a/README.md b/README.md
index 52b0f53..b0bc453 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,13 @@
  - [Project box](https://smile.amazon.co.uk/waterproof-plastic-Enclosure-Power-junction-Grey/dp/B01JHBHNMA/) - if we can't 3D print one
  - [TPL-5111](https://www.adafruit.com/product/3573) - also available from Pimoroni
 
+## Getting Started
+Run `bash ./build setup` from the command line at the root of this repository.
+
+### IoT Device
+ - Copy `settings.custom.cpp.example` to `settings.custom.cpp` and fill in the fields there
+ - Review `settings.h` to make sure it matches your setup
+ - Copy the folders in `iot/libraries` to your Arduino IDE libraries folder
 
 ## Useful Links
  - Entropy extraction from the watchdog timer vs the internal clock: https://github.com/taoyuan/Entropy
diff --git a/build b/build
index 958cdf0..38c5ef4 100755
--- a/build
+++ b/build
@@ -43,18 +43,60 @@ fi
 
 ###############################################################################
 
-task_setup() {
-	task_begin "Setting up";
+# Toggles commenting and uncommenting lines in a file that contain a specific 
+# substring. Checks for word boundaries either side of the substring.
+# From https://stackoverflow.com/a/24901636/1460422
+# $1 - Filename
+# $2 - Search string
+comment_toggle() {
+	filename="${1}";
+	search_string="${2}";
 	
+	awk -v commentId='//' -v word="${search_string}" '
+	  $0 ~ "(^|[[:punct:][:space:]])" word "($|[[:punct:][:space:]])" { 
+	    if (match($0, "^[[:space:]]*" commentId))
+	      $0 = substr($0, RSTART + RLENGTH)
+	    else
+	      $0 = commentId $0
+	  } 
+	  { print }' "${filename}" > tmpfile.$$ && mv tmpfile.$$ "${filename}"
+}
+
+task_setup() {
+	stage_begin "Setting up";
+	
+	task_begin "Checking Environment";
 	check_command git true;
+	check_command awk true;
 	check_command pdflatex true;
 	check_command bibtex true;
+	task_end $?;
 	
-	subtask_begin "Initialising submodules";
+	task_begin "Initialising submodules";
 	git submodule update --init;
-	subtask_end $?;
+	task_end $?;
 	
-	task_end 0;
+	task_begin "Preconfiguring libraries";
+	config_file_directory="./iot/libraries/arduino-lmic/src/lmic/";
+	config_file_name="config.h";
+	cd "${config_file_directory}";
+	git reset --hard;
+	# Disable OTAA
+	comment_toggle "${config_file_name}" "#define DISABLE_JOIN";
+	# Disable class b stuff
+	comment_toggle "${config_file_name}" "#define DISABLE_PING";
+	comment_toggle "${config_file_name}" "#define DISABLE_BEACONS";
+	# Disable other misc. stuff we're not likely to use
+	comment_toggle "${config_file_name}" "#define DISABLE_MCMD_DCAP_REQ"; # Duty cycle cap - won't  work anyway 'cause we're shutting down in between
+	comment_toggle "${config_file_name}" "#define DISABLE_MCMD_DN2P_SET"; # Receiving stuff
+	
+	# echo "#define DISABLE_JOIN" >>"${config_file_name}";
+	# echo "#define DISABLE_PING" >>"${config_file_name}";
+	# echo "#define DISABLE_BEACONS" >>"${config_file_name}";
+	cd -;
+	task_end $?;
+	
+	stage_end 0;
 }
 
 task_render() {
diff --git a/iot/TTNTest/TTNTest.ino b/iot/TTNTest/TTNTest.ino
new file mode 100644
index 0000000..a1585ca
--- /dev/null
+++ b/iot/TTNTest/TTNTest.ino
@@ -0,0 +1,195 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
+ *
+ * Permission is hereby granted, free of charge, to anyone
+ * obtaining a copy of this document and accompanying files,
+ * to do whatever they want with them without any restriction,
+ * including, but not limited to, copying, modification and redistribution.
+ * NO WARRANTY OF ANY KIND IS PROVIDED.
+ *
+ * This example sends a valid LoRaWAN packet with payload "Hello,
+ * world!", using frequency and encryption settings matching those of
+ * the (early prototype version of) The Things Network.
+ *
+ * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in g1,
+ *  0.1% in g2).
+ *
+ * Change DEVADDR to a unique address!
+ * See http://thethingsnetwork.org/wiki/AddressSpace
+ *
+ * Do not forget to define the radio type correctly in config.h.
+ *
+ *******************************************************************************/
+
+#include <lmic.h>
+#include <hal/hal.h>
+#include <SPI.h>
+
+#include "config.custom.h"
+
+// These callbacks are only used in over-the-air activation, so they are
+// left empty here (we cannot leave them out completely unless
+// DISABLE_JOIN is set in config.h, otherwise the linker will complain).
+void os_getArtEui (u1_t* buf) { }
+void os_getDevEui (u1_t* buf) { }
+void os_getDevKey (u1_t* buf) { }
+
+static uint8_t mydata[] = "Hello, world!";
+static osjob_t sendjob;
+
+// Schedule TX every this many seconds (might become longer due to duty
+// cycle limitations).
+const unsigned TX_INTERVAL = 60;
+
+// Pin mapping
+const lmic_pinmap lmic_pins = {
+	.nss = 10,
+	.rxtx = LMIC_UNUSED_PIN,
+	.rst = 9,
+	.dio = {2, 6, 7},
+};
+
+void onEvent (ev_t ev) {
+	Serial.print(os_getTime());
+	Serial.print(": ");
+	switch(ev) {
+		case EV_SCAN_TIMEOUT:
+			Serial.println(F("EV_SCAN_TIMEOUT"));
+			break;
+		case EV_BEACON_FOUND:
+			Serial.println(F("EV_BEACON_FOUND"));
+			break;
+		case EV_BEACON_MISSED:
+			Serial.println(F("EV_BEACON_MISSED"));
+			break;
+		case EV_BEACON_TRACKED:
+			Serial.println(F("EV_BEACON_TRACKED"));
+			break;
+		case EV_JOINING:
+			Serial.println(F("EV_JOINING"));
+			break;
+		case EV_JOINED:
+			Serial.println(F("EV_JOINED"));
+			break;
+		case EV_RFU1:
+			Serial.println(F("EV_RFU1"));
+			break;
+		case EV_JOIN_FAILED:
+			Serial.println(F("EV_JOIN_FAILED"));
+			break;
+		case EV_REJOIN_FAILED:
+			Serial.println(F("EV_REJOIN_FAILED"));
+			break;
+			break;
+		case EV_TXCOMPLETE:
+			Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
+			if(LMIC.dataLen) {
+				// data received in rx slot after tx
+				Serial.print(F("Data Received: "));
+				Serial.write(LMIC.frame+LMIC.dataBeg, LMIC.dataLen);
+				Serial.println();
+			}
+			// Schedule next transmission
+			os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
+			break;
+		case EV_LOST_TSYNC:
+			Serial.println(F("EV_LOST_TSYNC"));
+			break;
+		case EV_RESET:
+			Serial.println(F("EV_RESET"));
+			break;
+		case EV_RXCOMPLETE:
+			// data received in ping slot
+			Serial.println(F("EV_RXCOMPLETE"));
+			break;
+		case EV_LINK_DEAD:
+			Serial.println(F("EV_LINK_DEAD"));
+			break;
+		case EV_LINK_ALIVE:
+			Serial.println(F("EV_LINK_ALIVE"));
+			break;
+		 default:
+			Serial.println(F("Unknown event"));
+			break;
+	}
+}
+
+void do_send(osjob_t* j){
+	// Check if there is not a current TX/RX job running
+	if (LMIC.opmode & OP_TXRXPEND) {
+		Serial.println(F("OP_TXRXPEND, not sending"));
+	} else {
+		// Prepare upstream data transmission at the next possible time.
+		LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
+		Serial.println(F("Packet queued"));
+	}
+	// Next TX is scheduled after TX_COMPLETE event.
+}
+
+void setup() {
+	Serial.begin(115200);
+	Serial.println(F("Starting"));
+
+	#ifdef VCC_ENABLE
+	// For Pinoccio Scout boards
+	pinMode(VCC_ENABLE, OUTPUT);
+	digitalWrite(VCC_ENABLE, HIGH);
+	delay(1000);
+	#endif
+
+	// LMIC init
+	os_init();
+	// Reset the MAC state. Session and pending data transfers will be discarded.
+	LMIC_reset();
+
+	// Set static session parameters. Instead of dynamically establishing a session
+	// by joining the network, precomputed session parameters are be provided.
+	#ifdef PROGMEM
+	// On AVR, these values are stored in flash and only copied to RAM
+	// once. Copy them to a temporary buffer here, LMIC_setSession will
+	// copy them into a buffer of its own again.
+	uint8_t appskey[sizeof(APPSKEY)];
+	uint8_t nwkskey[sizeof(NWKSKEY)];
+	memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
+	memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
+	LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
+	#else
+	// If not running an AVR with PROGMEM, just use the arrays directly 
+	LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
+	#endif
+
+	// Set up the channels used by the Things Network, which corresponds
+	// to the defaults of most gateways. Without this, only three base
+	// channels from the LoRaWAN specification are used, which certainly
+	// works, so it is good for debugging, but can overload those
+	// frequencies, so be sure to configure the full frequency range of
+	// your network here (unless your network autoconfigures them).
+	// Setting up channels should happen after LMIC_setSession, as that
+	// configures the minimal channel set.
+	LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
+	LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
+	// TTN defines an additional channel at 869.525Mhz using SF9 for class B
+	// devices' ping slots. LMIC does not have an easy way to define set this
+	// frequency and support for class B is spotty and untested, so this
+	// frequency is not configured here.
+
+	// Disable link check validation
+	LMIC_setLinkCheckMode(0);
+
+	// Set data rate and transmit power (note: txpow seems to be ignored by the library)
+	LMIC_setDrTxpow(DR_SF7,14);
+
+	// Start job
+	do_send(&sendjob);
+}
+
+void loop() {
+	os_runloop_once();
+}
diff --git a/iot/TTNTest/config.custom.h.example b/iot/TTNTest/config.custom.h.example
new file mode 100644
index 0000000..65a82ea
--- /dev/null
+++ b/iot/TTNTest/config.custom.h.example
@@ -0,0 +1,15 @@
+// Copy this file to have the name "config.custom.h", and fill in the 3 sections below
+
+// LoRaWAN NwkSKey, network session key
+// Big endian (i.e. msb)
+static const PROGMEM u1_t NWKSKEY[16] = { .... };
+
+// LoRaWAN AppSKey, application session key
+// Big endian (i.e. msb)
+static const u1_t PROGMEM APPSKEY[16] = { .... };
+
+// LoRaWAN end-device address (DevAddr)
+// See http://thethingsnetwork.org/wiki/AddressSpace
+// The library converts the address to network byte order as needed.
+// Big endian (i.e. msb)
+static const u4_t DEVADDR = 0x....; // <-- Change this address for every node!
diff --git a/iot/TTNTestABPNew/TTNTestABPNew.ino b/iot/TTNTestABPNew/TTNTestABPNew.ino
new file mode 100644
index 0000000..408a713
--- /dev/null
+++ b/iot/TTNTestABPNew/TTNTestABPNew.ino
@@ -0,0 +1,256 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
+ * Copyright (c) 2018 Terry Moore, MCCI
+ *
+ * Permission is hereby granted, free of charge, to anyone
+ * obtaining a copy of this document and accompanying files,
+ * to do whatever they want with them without any restriction,
+ * including, but not limited to, copying, modification and redistribution.
+ * NO WARRANTY OF ANY KIND IS PROVIDED.
+ *
+ * This example sends a valid LoRaWAN packet with payload "Hello,
+ * world!", using frequency and encryption settings matching those of
+ * the The Things Network.
+ *
+ * This uses ABP (Activation-by-personalisation), where a DevAddr and
+ * Session keys are preconfigured (unlike OTAA, where a DevEUI and
+ * application key is configured, while the DevAddr and session keys are
+ * assigned/generated in the over-the-air-activation procedure).
+ *
+ * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
+ * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
+ * violated by this sketch when left running for longer)!
+ *
+ * To use this sketch, first register your application and device with
+ * the things network, to set or generate a DevAddr, NwkSKey and
+ * AppSKey. Each device should have their own unique values for these
+ * fields.
+ *
+ * Do not forget to define the radio type correctly in
+ * arduino-lmic/project_config/lmic_project_config.h or from your BOARDS.txt.
+ *
+ *******************************************************************************/
+
+ // References:
+ // [feather] adafruit-feather-m0-radio-with-lora-module.pdf
+
+#include <lmic.h>
+#include <hal/hal.h>
+#include <SPI.h>
+
+#include "config.custom.h"
+
+// These callbacks are only used in over-the-air activation, so they are
+// left empty here (we cannot leave them out completely unless
+// DISABLE_JOIN is set in arduino-lmic/project_config/lmic_project_config.h,
+// otherwise the linker will complain).
+void os_getArtEui (u1_t* buf) { }
+void os_getDevEui (u1_t* buf) { }
+void os_getDevKey (u1_t* buf) { }
+
+static uint8_t mydata[] = "Hello, world!";
+static osjob_t sendjob;
+
+// Schedule TX every this many seconds (might become longer due to duty
+// cycle limitations).
+const unsigned TX_INTERVAL = 60;
+
+// Pin mapping
+const lmic_pinmap lmic_pins = {
+	.nss = 10,
+	.rxtx = LMIC_UNUSED_PIN,
+	.rst = 9,
+	.dio = {2, 6, 7},
+};
+
+void onEvent (ev_t ev) {
+	Serial.print(os_getTime());
+	Serial.print(": ");
+	switch(ev) {
+		case EV_SCAN_TIMEOUT:
+			Serial.println(F("EV_SCAN_TIMEOUT"));
+			break;
+		case EV_BEACON_FOUND:
+			Serial.println(F("EV_BEACON_FOUND"));
+			break;
+		case EV_BEACON_MISSED:
+			Serial.println(F("EV_BEACON_MISSED"));
+			break;
+		case EV_BEACON_TRACKED:
+			Serial.println(F("EV_BEACON_TRACKED"));
+			break;
+		case EV_JOINING:
+			Serial.println(F("EV_JOINING"));
+			break;
+		case EV_JOINED:
+			Serial.println(F("EV_JOINED"));
+			break;
+		/*
+		|| This event is defined but not used in the code. No
+		|| point in wasting codespace on it.
+		||
+		|| case EV_RFU1:
+		||     Serial.println(F("EV_RFU1"));
+		||     break;
+		*/
+		case EV_JOIN_FAILED:
+			Serial.println(F("EV_JOIN_FAILED"));
+			break;
+		case EV_REJOIN_FAILED:
+			Serial.println(F("EV_REJOIN_FAILED"));
+			break;
+		case EV_TXCOMPLETE:
+			Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
+			if (LMIC.txrxFlags & TXRX_ACK)
+			  Serial.println(F("Received ack"));
+			if (LMIC.dataLen) {
+			  Serial.println(F("Received "));
+			  Serial.println(LMIC.dataLen);
+			  Serial.println(F(" bytes of payload"));
+			}
+			// Schedule next transmission
+			os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
+			break;
+		case EV_LOST_TSYNC:
+			Serial.println(F("EV_LOST_TSYNC"));
+			break;
+		case EV_RESET:
+			Serial.println(F("EV_RESET"));
+			break;
+		case EV_RXCOMPLETE:
+			// data received in ping slot
+			Serial.println(F("EV_RXCOMPLETE"));
+			break;
+		case EV_LINK_DEAD:
+			Serial.println(F("EV_LINK_DEAD"));
+			break;
+		case EV_LINK_ALIVE:
+			Serial.println(F("EV_LINK_ALIVE"));
+			break;
+		/*
+		|| This event is defined but not used in the code. No
+		|| point in wasting codespace on it.
+		||
+		|| case EV_SCAN_FOUND:
+		||    Serial.println(F("EV_SCAN_FOUND"));
+		||    break;
+		*/
+		case EV_TXSTART:
+			Serial.println(F("EV_TXSTART"));
+			break;
+		default:
+			Serial.print(F("Unknown event: "));
+			Serial.println((unsigned) ev);
+			break;
+	}
+}
+
+void do_send(osjob_t* j){
+	// Check if there is not a current TX/RX job running
+	if (LMIC.opmode & OP_TXRXPEND) {
+		Serial.println(F("OP_TXRXPEND, not sending"));
+	} else {
+		// Prepare upstream data transmission at the next possible time.
+		LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
+		Serial.println(F("Packet queued"));
+	}
+	// Next TX is scheduled after TX_COMPLETE event.
+}
+
+void setup() {
+//    pinMode(13, OUTPUT); 
+	while (!Serial); // wait for Serial to be initialized
+	Serial.begin(115200);
+	delay(100);     // per sample code on RF_95 test
+	Serial.println(F("Starting"));
+	
+	// Activate the right SPI device
+	pinMode(10, OUTPUT);
+	pinMode(3, OUTPUT);
+	digitalWrite(10, LOW);
+	digitalWrite(3, HIGH);
+
+	#ifdef VCC_ENABLE
+	// For Pinoccio Scout boards
+	pinMode(VCC_ENABLE, OUTPUT);
+	digitalWrite(VCC_ENABLE, HIGH);
+	delay(1000);
+	#endif
+
+	// LMIC init
+	os_init();
+	// Reset the MAC state. Session and pending data transfers will be discarded.
+	LMIC_reset();
+
+	// Set static session parameters. Instead of dynamically establishing a session
+	// by joining the network, precomputed session parameters are be provided.
+	#ifdef PROGMEM
+	// On AVR, these values are stored in flash and only copied to RAM
+	// once. Copy them to a temporary buffer here, LMIC_setSession will
+	// copy them into a buffer of its own again.
+	uint8_t appskey[sizeof(APPSKEY)];
+	uint8_t nwkskey[sizeof(NWKSKEY)];
+	memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
+	memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
+	LMIC_setSession (0x13, DEVADDR, nwkskey, appskey);
+	#else
+	// If not running an AVR with PROGMEM, just use the arrays directly
+	LMIC_setSession (0x13, DEVADDR, NWKSKEY, APPSKEY);
+	#endif
+
+	#if defined(CFG_eu868)
+	// Set up the channels used by the Things Network, which corresponds
+	// to the defaults of most gateways. Without this, only three base
+	// channels from the LoRaWAN specification are used, which certainly
+	// works, so it is good for debugging, but can overload those
+	// frequencies, so be sure to configure the full frequency range of
+	// your network here (unless your network autoconfigures them).
+	// Setting up channels should happen after LMIC_setSession, as that
+	// configures the minimal channel set.
+	LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
+	LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
+	// TTN defines an additional channel at 869.525Mhz using SF9 for class B
+	// devices' ping slots. LMIC does not have an easy way to define set this
+	// frequency and support for class B is spotty and untested, so this
+	// frequency is not configured here.
+	#elif defined(CFG_us915)
+	// NA-US channels 0-71 are configured automatically
+	// but only one group of 8 should (a subband) should be active
+	// TTN recommends the second sub band, 1 in a zero based count.
+	// https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json
+	LMIC_selectSubBand(1);
+	#endif
+
+	// Disable link check validation
+	LMIC_setLinkCheckMode(0);
+
+	// TTN uses SF9 for its RX2 window.
+	LMIC.dn2Dr = DR_SF9;
+
+	// Set data rate and transmit power for uplink
+	LMIC_setDrTxpow(DR_SF7,14);
+
+	// Start job
+	do_send(&sendjob);
+}
+
+void loop() {
+	unsigned long now;
+	now = millis();
+	if ((now & 512) != 0) {
+	  digitalWrite(13, HIGH);
+	}
+	else {
+	  digitalWrite(13, LOW);
+	}
+	  
+	os_runloop_once();
+	
+}
diff --git a/iot/TTNTestABPNew/config.custom.h.example b/iot/TTNTestABPNew/config.custom.h.example
new file mode 100644
index 0000000..65a82ea
--- /dev/null
+++ b/iot/TTNTestABPNew/config.custom.h.example
@@ -0,0 +1,15 @@
+// Copy this file to have the name "config.custom.h", and fill in the 3 sections below
+
+// LoRaWAN NwkSKey, network session key
+// Big endian (i.e. msb)
+static const PROGMEM u1_t NWKSKEY[16] = { .... };
+
+// LoRaWAN AppSKey, application session key
+// Big endian (i.e. msb)
+static const u1_t PROGMEM APPSKEY[16] = { .... };
+
+// LoRaWAN end-device address (DevAddr)
+// See http://thethingsnetwork.org/wiki/AddressSpace
+// The library converts the address to network byte order as needed.
+// Big endian (i.e. msb)
+static const u4_t DEVADDR = 0x....; // <-- Change this address for every node!
diff --git a/iot/TTNTestOTAA/TTNTestOTAA.ino b/iot/TTNTestOTAA/TTNTestOTAA.ino
new file mode 100644
index 0000000..db59ca6
--- /dev/null
+++ b/iot/TTNTestOTAA/TTNTestOTAA.ino
@@ -0,0 +1,159 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
+ *
+ * Permission is hereby granted, free of charge, to anyone
+ * obtaining a copy of this document and accompanying files,
+ * to do whatever they want with them without any restriction,
+ * including, but not limited to, copying, modification and redistribution.
+ * NO WARRANTY OF ANY KIND IS PROVIDED.
+ *
+ * This example sends a valid LoRaWAN packet with payload "Hello,
+ * world!", using frequency and encryption settings matching those of
+ * the The Things Network.
+ *
+ * This uses OTAA (Over-the-air activation), where where a DevEUI and
+ * application key is configured, which are used in an over-the-air
+ * activation procedure where a DevAddr and session keys are
+ * assigned/generated for use with all further communication.
+ *
+ * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
+ * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
+ * violated by this sketch when left running for longer)!
+
+ * To use this sketch, first register your application and device with
+ * the things network, to set or generate an AppEUI, DevEUI and AppKey.
+ * Multiple devices can use the same AppEUI, but each device has its own
+ * DevEUI and AppKey.
+ *
+ * Do not forget to define the radio type correctly in config.h.
+ *
+ *******************************************************************************/
+#define DISABLE_PING
+#define DISABLE_BEACONS
+
+#include <lmic.h>
+#include <hal/hal.h>
+#include <SPI.h>
+
+#include "config.custom.h"
+
+static uint8_t mydata[] = "Hello, world!";
+static osjob_t sendjob;
+
+// Schedule TX every this many seconds (might become longer due to duty
+// cycle limitations).
+const unsigned TX_INTERVAL = 60;
+
+
+void onEvent (ev_t ev) {
+    Serial.print(os_getTime());
+    Serial.print(": ");
+    switch(ev) {
+        case EV_SCAN_TIMEOUT:
+            Serial.println(F("EV_SCAN_TIMEOUT"));
+            break;
+        case EV_BEACON_FOUND:
+            Serial.println(F("EV_BEACON_FOUND"));
+            break;
+        case EV_BEACON_MISSED:
+            Serial.println(F("EV_BEACON_MISSED"));
+            break;
+        case EV_BEACON_TRACKED:
+            Serial.println(F("EV_BEACON_TRACKED"));
+            break;
+        case EV_JOINING:
+            Serial.println(F("EV_JOINING"));
+            break;
+        case EV_JOINED:
+            Serial.println(F("EV_JOINED"));
+
+            // Disable link check validation (automatically enabled
+            // during join, but not supported by TTN at this time).
+            LMIC_setLinkCheckMode(0);
+            break;
+        case EV_RFU1:
+            Serial.println(F("EV_RFU1"));
+            break;
+        case EV_JOIN_FAILED:
+            Serial.println(F("EV_JOIN_FAILED"));
+            break;
+        case EV_REJOIN_FAILED:
+            Serial.println(F("EV_REJOIN_FAILED"));
+            break;
+            break;
+        case EV_TXCOMPLETE:
+            Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
+            if (LMIC.txrxFlags & TXRX_ACK)
+              Serial.println(F("Received ack"));
+            if (LMIC.dataLen) {
+              Serial.println(F("Received "));
+              Serial.println(LMIC.dataLen);
+              Serial.println(F(" bytes of payload"));
+            }
+            // Schedule next transmission
+            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
+            break;
+        case EV_LOST_TSYNC:
+            Serial.println(F("EV_LOST_TSYNC"));
+            break;
+        case EV_RESET:
+            Serial.println(F("EV_RESET"));
+            break;
+        case EV_RXCOMPLETE:
+            // data received in ping slot
+            Serial.println(F("EV_RXCOMPLETE"));
+            break;
+        case EV_LINK_DEAD:
+            Serial.println(F("EV_LINK_DEAD"));
+            break;
+        case EV_LINK_ALIVE:
+            Serial.println(F("EV_LINK_ALIVE"));
+            break;
+         default:
+            Serial.println(F("Unknown event"));
+            break;
+    }
+}
+
+void do_send(osjob_t* j){
+    // Check if there is not a current TX/RX job running
+    if (LMIC.opmode & OP_TXRXPEND) {
+        Serial.println(F("OP_TXRXPEND, not sending"));
+    } else {
+        // Prepare upstream data transmission at the next possible time.
+        LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
+        Serial.println(F("Packet queued"));
+    }
+    // Next TX is scheduled after TX_COMPLETE event.
+}
+
+void setup() {
+    Serial.begin(115200);
+    Serial.println(F("Starting"));
+	
+	// Activate the right SPI device
+	pinMode(10, OUTPUT);
+	pinMode(3, OUTPUT);
+	digitalWrite(10, LOW);
+	digitalWrite(3, HIGH);
+
+    #ifdef VCC_ENABLE
+    // For Pinoccio Scout boards
+    pinMode(VCC_ENABLE, OUTPUT);
+    digitalWrite(VCC_ENABLE, HIGH);
+    delay(1000);
+    #endif
+
+    // LMIC init
+    os_init();
+    // Reset the MAC state. Session and pending data transfers will be discarded.
+    LMIC_reset();
+	LMIC_setClockError(5 * MAX_CLOCK_ERROR / 100);
+
+    // Start job (sending automatically starts OTAA too)
+    do_send(&sendjob);
+}
+
+void loop() {
+    os_runloop_once();
+}
diff --git a/iot/TTNTestOTAA/config.custom.h.example b/iot/TTNTestOTAA/config.custom.h.example
new file mode 100644
index 0000000..43e3409
--- /dev/null
+++ b/iot/TTNTestOTAA/config.custom.h.example
@@ -0,0 +1,26 @@
+
+// This EUI must be in little-endian format, so least-significant-byte
+// first. When copying an EUI from ttnctl output, this means to reverse
+// the bytes. For TTN issued EUIs the last bytes should be 0xD5, 0xB3,
+// 0x70.
+static const u1_t PROGMEM APPEUI[8]={ ..... };
+void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8);}
+
+// This should also be in little endian format, see above.
+static const u1_t PROGMEM DEVEUI[8]={ ...... };
+void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8);}
+
+// This key should be in big endian format (or, since it is not really a
+// number but a block of memory, endianness does not really apply). In
+// practice, a key taken from ttnctl can be copied as-is.
+// The key shown here is the semtech default key.
+static const u1_t PROGMEM APPKEY[16] = { ...... };
+void os_getDevKey (u1_t* buf) {  memcpy_P(buf, APPKEY, 16);}
+
+
+const lmic_pinmap lmic_pins = {
+	.nss = 10,
+	.rxtx = LMIC_UNUSED_PIN,
+	.rst = 9,
+	.dio = {2, 6, 7},
+};
diff --git a/iot/libraries/Arduino-MemoryFree b/iot/libraries/Arduino-MemoryFree
new file mode 160000
index 0000000..aed5cc5
--- /dev/null
+++ b/iot/libraries/Arduino-MemoryFree
@@ -0,0 +1 @@
+Subproject commit aed5cc5e28f8f01afc41a625151c184a8e94e32e
diff --git a/iot/libraries/SdFat b/iot/libraries/SdFat
new file mode 160000
index 0000000..3b79f38
--- /dev/null
+++ b/iot/libraries/SdFat
@@ -0,0 +1 @@
+Subproject commit 3b79f38cb554809a45a0ccd6d9d6752b6ad948c2
diff --git a/iot/libraries/TinyGPS b/iot/libraries/TinyGPS
new file mode 160000
index 0000000..db4ef9c
--- /dev/null
+++ b/iot/libraries/TinyGPS
@@ -0,0 +1 @@
+Subproject commit db4ef9c97af767e7345f5ccb277ac3bd1a2eb81f
diff --git a/iot/libraries/arduino-lmic b/iot/libraries/arduino-lmic
index 9f01615..ba1265d 160000
--- a/iot/libraries/arduino-lmic
+++ b/iot/libraries/arduino-lmic
@@ -1 +1 @@
-Subproject commit 9f016150100afcde4d3def8abae234c6efc46e0c
+Subproject commit ba1265d5d2f775177cdc7c82186724e4f0bdc3a8
diff --git a/iot/main/gps.cpp b/iot/main/gps.cpp
index bbfe2dd..1189565 100644
--- a/iot/main/gps.cpp
+++ b/iot/main/gps.cpp
@@ -1,54 +1,93 @@
 #include "gps.h"
 #include "settings.h"
 
-// The GPS message decoder
-TinyGPSPlus	gps;
-// The serial connection to the GPS device
-SoftwareSerial serial_gps(PIN_GPS_RX, PIN_GPS_TX);
+#include <TinyGPS.h>
 
-void gps_begin() {
+SoftwareSerial gps_begin() {
+	// FUTURE: If this doesn't work as expected, we may want to find a more reliable SoftwareSerial library
+	SoftwareSerial serial_gps(PIN_GPS_RX, PIN_GPS_TX);
 	// Initialise the software-based serial connection to the GPS device
 	serial_gps.begin(BAUD_GPS);
+	return serial_gps;
 }
 
-void gps_fetch() {
-	Serial.print("[gps] Getting location: ");
-	uint32_t ms_last_update = millis();
+GPSLocation gps_fetch() {
+	// The serial connection to the GPS module
+	SoftwareSerial serial_gps = gps_begin();
+	// The GPS message decoder
+	TinyGPS	gps;
+	// The struct that will hold the result
+	GPSLocation result;
+	
+	Serial.print(F("[gps] Working: "));
+	uint32_t start = millis(), ms_last_update = millis();
+	uint8_t ticks = 0;
+	unsigned long fix_age = TinyGPS::GPS_INVALID_AGE;
+	unsigned long chars;
+	unsigned short sentences, failed_checksum;
+	
+	
+	
 	// We WILL discover our location - if it's the last thing we do!
 	while(true) {
-		if(millis() > 5000 && gps.charsProcessed() < 10) {
-			Serial.println(F("\n[error] Failed to initialise GPS device."));
+		gps.stats(&chars, &sentences, &failed_checksum);
+		
+		if(millis() - start > 5000 && chars < 10) {
+			Serial.println(F("\n[error] GPS device init failed"));
 			while(true) { delay(1); }
 		}
 		
 		// Make sure there's something to read
-		if(serial_gps.available() <= 0) {
+		if(serial_gps.available() <= 0)
 			continue;
-		}
 		
 		// If it failed, go around again
 		if(!gps.encode(serial_gps.read()))
 			continue;
 		
+		gps.f_get_position(&(result.lat), &(result.lng), &fix_age);
+		
 		// If we haven't acquired a lock yet, go around again
-		// Also go around again if the location is the same hasn't yet been updated (GPS devices are strange)
-		if(!gps.location.isValid()) {
-			// NOTE: It can really take a rather long time to get a lock. Just wait patiently :-)
-			if(millis() - ms_last_update > 500) Serial.print(".");
+		if(fix_age == TinyGPS::GPS_INVALID_AGE) {
+			// NOTE: It can take a rather long time to get a lock. Just wait patiently :-)
+			if(millis() - ms_last_update > 500) {
+				ticks++;
+				Serial.print('.');
+				if(ticks > 80) {
+					Serial.println();
+					ticks = 0;
+				}
+				// Serial.println(gps.location.lat());
+				// Serial.println(gps.location.lng());
+				// Serial.println(gps.date.year());
+				// Serial.println(gps.time.hour());
+				// Serial.println(gps.time.minute());
+			}
 			ms_last_update = millis();
 			continue;
 		}
 		
 		// Hooray!
-		Serial.println("ok");
-		return;
+		Serial.println(F("ok"));
+		
+		// gps.f_get_position pushes the values into result directly
+		
+		#ifdef SD_DEBUG_ENABLED
+		int year;
+		byte month, day, hour, minute, second, _hundredths;
+		gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &_hundredths, &fix_age);
+		
+		snprintf(result.time, 19, "%04d-%02d-%02d %02d:%02d:%02d",
+			year, month, day,
+			hour, minute, second
+		);
+		#endif
+		return result;
 	}
+	
+	gps_end(serial_gps);
 }
 
-TinyGPSPlus gps_info() {
-	return gps;
-}
-
-void gps_end() {
-	Serial.println(F("[warning] Putting the GPS device to sleep isn't implemented yet."));
+void gps_end(SoftwareSerial serial_gps) {
+	serial_gps.end();
 }
diff --git a/iot/main/gps.h b/iot/main/gps.h
index 40d2ee4..da2cac5 100644
--- a/iot/main/gps.h
+++ b/iot/main/gps.h
@@ -2,27 +2,29 @@
 
 #include <SoftwareSerial.h>
 
-#include <TinyGPS++.h>
-
 #include "settings.h"
 
+// A lightweight struct to hold location information.
+// TinyGPS++ actually uses a *ton* of RAM so we can't keep the instance around as a global variable
+// FUTURE: Rewrite this to use TinyGPS's integer system instead?
+struct GPSLocation {
+	float lat;
+	float lng;
+	#ifdef SD_DEBUG_ENABLED
+	char time[20];
+	#endif
+};
+
 /**
  * Initialises the connection to the GPS device.
  */
-void gps_begin();
+SoftwareSerial gps_begin();
 
 /**
  * Fetches new data from the GPS module.
  * May take a moment, as the GPS device needs time to acquire a satellite fix.
  */
-void gps_fetch();
-
-/**
- * Fetches the latest information from the GPS device.
- * Call gps_fetch() first.
- * @return TinyGPSLocation	The current location.
- */
-TinyGPSLocation gps_info();
+GPSLocation gps_fetch();
 
 /**
  * Ends the connection to the GPS device and puts it to sleep in order to save 
@@ -31,5 +33,6 @@ TinyGPSLocation gps_info();
  * system and so doesn't get turned off after each measurement, as it takes 
  * ~30s to reacquire a lock when it first starts up - hence why we put it to 
  * sleep instead.
+ * TODO: Connect the gps module to the timed power rail instead, as its; got an onboard battery.
  */
-void gps_end();
+void gps_end(SoftwareSerial gps);
diff --git a/iot/main/main.ino b/iot/main/main.ino
index 73b100b..9648ded 100644
--- a/iot/main/main.ino
+++ b/iot/main/main.ino
@@ -1,47 +1,78 @@
-#include <Arduino.h>
-#include <TinyGPS++.h>
-#include <SD.h>
-
 #include "settings.h"
+#include <Arduino.h>
+#ifdef ENABLE_MEMORY_DIAGNOSTICS
+// #include <MemoryFree.h>
+#define DISPLAY_FREE_MEMORY()	Serial.println(freeMemory(), DEC);
+#else
+#define DISPLAY_FREE_MEMORY()	// noop
+#endif
+
 #include "random.h"
 // BAD PRACTICE: For some extremely strange reason, the Arduino IDE doesn't pick up random.cpp like it does our other source files - so we've got to explicitly include it here. If we had control over the build process (which we don't), we've use a Makefile here that handled this better.
 #include "random.cpp"
 #include "gps.h"
+#include "peripheral.h"
+#include "storage.h"
+#include "power.h"
+#include "transmission.h"
 
 
 void setup() {
 	Serial.begin(BAUD_PC);
-	Serial.println("[main] Beginning collection sequence.");
-	
+	Serial.println(F("[main] Starting"));
 	random_begin();
 	
-	
-	gps_begin();
-	TinyGPSPlus gps_data = gps_location();
-	gps_end();
-	
-	Serial.print("[main] Location: ("); Serial.print(loc.lat()); Serial.print(", "); Serial.print(loc.lng()); Serial.println(")");
+	DISPLAY_FREE_MEMORY();
+	GPSLocation gps_data = gps_fetch();
+	DISPLAY_FREE_MEMORY();
+		
+	Serial.print(F("[main] Location ")); Serial.print(gps_data.lat); Serial.print(F(", ")); Serial.println(gps_data.lng);
 	
 	uint32_t id = random_get();
 	
-	Serial.print("[main] id: ");
+	Serial.print(F("[main] Id "));
 	Serial.println(id);
 	
-	store_init(); 
-	store_reading(id, gps_data.location);
-	char debug_message[64];
-	int chars = snprintf(debug_message, 64, "%d-%d-%d %d:%d:%d | %f %f",
-		gps_data.date.year(),
-		gps_data.date.month(),
-		gps_data.date.day(),
-		gps_data.time.hour(),
-		gps_data.time.minute(),
-		gps_data.time.second(),
-		gps_data.location.lat(),
-		gps_data.location.lng(),
-	);
-	store_debug(debug_message, chars);
-	store_close();
+	// Activate microSD card breakout board on the SPI bus
+	
+	peripheral_register(PIN_SPI_CS_RFM95);
+	peripheral_register(PIN_SPI_CS_SD);
+	
+	peripheral_unsilence(PIN_SPI_CS_SD);
+	
+	DISPLAY_FREE_MEMORY();
+	store_reading(id, gps_data);
+	DISPLAY_FREE_MEMORY();
+	#ifdef SD_DEBUG_ENABLED
+	store_debug(gps_data.time, 19 - 1); // Don't print the null byte
+	DISPLAY_FREE_MEMORY();
+	#endif
+	
+	// ------------------------------------------------------------------------
+	
+	// Activate the RFM95
+	peripheral_switch(PIN_SPI_CS_SD, PIN_SPI_CS_RFM95);
+	
+	const uint8_t message_size = sizeof(uint32_t) + sizeof(float)*2;
+	uint8_t message[message_size];
+	
+	const uint8_t* bytes_id = reinterpret_cast<uint8_t const *>(&id);
+	const uint8_t* bytes_lat = reinterpret_cast<uint8_t const *>(&(gps_data.lat));
+	const uint8_t* bytes_lng = reinterpret_cast<uint8_t const *>(&(gps_data.lat));
+	
+	for(int i = 0; i < sizeof(uint32_t); i++) {
+		message[i] = bytes_id[i];
+	}
+	for(int i = 0; i < sizeof(float); i++) {
+		message[sizeof(uint32_t) + i] = bytes_lat[i];
+		message[sizeof(uint32_t) + sizeof(float) + i] = bytes_lng[i];
+	}
+	
+	#ifdef ENABLE_RADIO
+	Serial.println(F("[main] Transmitting"));
+	transmit_init();
+	transmit_send(message, message_size);
+	#endif
 	
 	power_off(); // Doesn't return
 }
diff --git a/iot/main/peripheral.cpp b/iot/main/peripheral.cpp
new file mode 100644
index 0000000..2899b9c
--- /dev/null
+++ b/iot/main/peripheral.cpp
@@ -0,0 +1,20 @@
+#include <Arduino.h>
+
+void peripheral_register(uint8_t pin_number) {
+	pinMode(OUTPUT, pin_number);
+	// Disable the device by default to avoid issues
+	digitalWrite(pin_number, HIGH);
+}
+
+void peripheral_unsilence(uint8_t pin_number) {
+	digitalWrite(pin_number, LOW);
+}
+
+void peripheral_silence(uint8_t pin_number) {
+	digitalWrite(pin_number, HIGH);
+}
+
+void peripheral_switch(uint8_t pin_number_old, uint8_t pin_number_new) {
+	digitalWrite(pin_number_old, HIGH);
+	digitalWrite(pin_number_new, LOW);
+}
diff --git a/iot/main/peripheral.h b/iot/main/peripheral.h
new file mode 100644
index 0000000..58fb084
--- /dev/null
+++ b/iot/main/peripheral.h
@@ -0,0 +1,24 @@
+#pragma once
+
+/**
+ * Register a new SPI peripheral.
+ * @param	pin_number	The pin number of the device's chip select pin.
+ */
+void peripheral_register(uint8_t pin_number);
+
+/**
+ * Allows the device with the given chip select pin to talk on the SPI bus.
+ * @param pin_number The pin number of the chip select pin of the device to allow to talk.
+ */
+void peripheral_unsilence(uint8_t pin_number);
+/**
+ * Stops the device with the given chip select pin from talking on the SPI bus.
+ * @param pin_number The chip-select pin number of the device to stop.
+ */
+void peripheral_silence(uint8_t pin_number);
+/**
+ * Switches the active device from one to another on the SPI bus.
+ * @param pin_number_old The chip-select pin number of the old device to switch out from.
+ * @param pin_number_new The chip-select pin number of the new device to switch in to.
+ */
+void peripheral_switch(uint8_t pin_number_old, uint8_t pin_number_new);
diff --git a/iot/main/power.cpp b/iot/main/power.cpp
index 7dbf3f0..d5a281d 100644
--- a/iot/main/power.cpp
+++ b/iot/main/power.cpp
@@ -2,19 +2,10 @@
 
 #include "settings.h"
 
-
-void power_gps_wakeup() {
-	Serial.println("Warn: GPS wakeup isn't implemented yet.");
-}
-
-void power_gps_standby() {
-	Serial.println("Warn: GPS standby isn't implemented yet.");
-}
-
 void power_off() {
-	Serial.println(F("[power] Beginning shutdown sequence"));
-	Serial.println(F("[power] Switching GPS module to standby"));
-	power_gps_standby();
+	Serial.println(F("[power] Shutting down"));
+	// Serial.println(F("[power] Switching GPS module to standby"));
+	// power_gps_standby();
 	
 	Serial.println(F("[power] Signalling TPL5111"));
 	pinMode(PIN_TPL_DONE, OUTPUT);
diff --git a/iot/main/power.h b/iot/main/power.h
index 0f9da8f..94e8717 100644
--- a/iot/main/power.h
+++ b/iot/main/power.h
@@ -1,13 +1,4 @@
 
-/**
- * Wakes the GPS module up from standby.
- */
-void power_gps_wakeup();
-/**
- * Puts the GPS module into standby.
- */
-void power_gps_standby();
-
 /**
  * Signals that our work is complete and that we can turn off now to the 
  * TPL5111.
diff --git a/iot/main/settings.custom.cpp.example b/iot/main/settings.custom.cpp.example
new file mode 100644
index 0000000..11fdf42
--- /dev/null
+++ b/iot/main/settings.custom.cpp.example
@@ -0,0 +1,26 @@
+#include <lmic.h>
+#include <hal/hal.h>
+#include <SPI.h>
+
+#include "settings.h"
+
+
+// LoRaWAN NwkSKey, network session key
+// This is the default Semtech key, which is used by the early prototype TTN
+// network.
+static const PROGMEM u1_t NWKSKEY[16] = { .... };
+
+// LoRaWAN AppSKey, application session key
+// This is the default Semtech key, which is used by the early prototype TTN
+// network.
+static const u1_t PROGMEM APPSKEY[16] = { ..... };
+
+// LoRaWAN end-device address (DevAddr)
+static const u4_t DEVADDR = 0x....; // <-- Change this address for every node!
+
+const lmic_pinmap lmic_pins = {
+	.nss = PIN_SPI_CS_RFM95,
+	.rxtx = LMIC_UNUSED_PIN,
+	.rst = 9,
+	.dio = {2, 6, 7},
+};
diff --git a/iot/main/settings.custom.h b/iot/main/settings.custom.h
new file mode 100644
index 0000000..28e4fee
--- /dev/null
+++ b/iot/main/settings.custom.h
@@ -0,0 +1,14 @@
+#include <lmic.h>
+#include <hal/hal.h>
+#include <SPI.h>
+
+/**
+ * Don't edit this file - you want to make a copy of settings.custom.cpp, call 
+ * it settings.cpp, and edit that instead.
+ */
+
+extern const PROGMEM u1_t NWKSKEY[16];
+extern const u1_t PROGMEM APPSKEY[16];
+extern const u4_t DEVADDR;
+
+extern const lmic_pinmap lmic_pins;
diff --git a/iot/main/settings.h b/iot/main/settings.h
index df2f353..d245323 100644
--- a/iot/main/settings.h
+++ b/iot/main/settings.h
@@ -1,5 +1,18 @@
 #pragma once
 
+#include <lmic.h>
+#include <hal/hal.h>
+#include <SPI.h>
+
+#include "settings.custom.h"
+
+/*
+ * This is the main settings file - customise it to match your setup.
+ * Don't forget that you need to take a copy of settings.custom.cpp.example as
+ * settings.custom.cpp, and fill in the private keys the program needs to talk 
+ * over LORaWAN.
+ */
+
 //////////////////////////////////
 ////////////// Main //////////////
 //////////////////////////////////
@@ -7,23 +20,44 @@
 // The speed at which we should talk over our main hardware serial connection.
 #define BAUD_PC 		115200
 
-// Multiple devices can use the same SPI data pin AFAIKT, but some libraries *cough* SD *cough* are too stupid to figure out which pin it is on their own.
+// Multiple devices can use the same SPI data pin AFAIKT, but some libraries 
+// *cough* SD *cough* are too stupid to figure out which pin it is on their own.
 #define PIN_SPI_DATA	9
 
+// The 'done' pin to pulse to signal to the TPL5111
 #define PIN_TPL_DONE	8
 
+// Uncomment to print RAM diagnostics at regular intervals.
+//#define ENABLE_MEMORY_DIAGNOSTICS
+
+/////////////
+/// RFM95 ///
+/////////////
+
+// Whether to actually enable LMIC-based transmission or not.
+#define ENABLE_RADIO
+
+// The SPI chip-select pin for the RFM 95
+#define PIN_SPI_CS_RFM95	10
+
+// Pin mapping - see settings.custom.cpp
+
 /////////////////////////////////
 ////////////// GPS //////////////
 /////////////////////////////////
 
 // The *TX* gin of the GPS device.
-// This is swapped because we receive the GPS device's message on our side on the RX pin, and the GPS device transmits messages on the TX.
+// This is swapped because we receive the GPS device's message on our side on 
+// the RX pin, and the GPS device transmits messages on the TX.
 #define PIN_GPS_RX	5
 // The *RX* pin on the GPS device.
-// This is swapped because where the GPs device is receiving, we aresending and vice versa.
+// This is swapped because where the GPs device is receiving, we aresending and 
+// vice versa.
 // The TX / RX here are according to *our* side, not the GPS device's side.
 #define PIN_GPS_TX	4
-// The speed at which we should talk to the GPS device. Some GPS devices require a certain speed in order to use certain commands, so it's important that you check the datasheets for the device you're using.
+// The speed at which we should talk to the GPS device. Some GPS devices 
+// require a certain speed in order to use certain commands, so it's important 
+// that you check the datasheets for the device you're using.
 // 9600 is the correct speed for a NEO-6M.
 #define BAUD_GPS	9600
 
@@ -32,10 +66,13 @@
 //////////////////////////////////
 
 // The chip select pin that activates the connection to the microSD card over SPI.
-#define	PIN_SD_SPI_CHIP_SELECT	3
+#define PIN_SPI_CS_SD		3
 
 // The filename on the microSD card to store data in.
-#define SD_FILENAME	"data.tsv"
+#define SD_FILENAME	F("data.tsv")
+
+// Whether to write debug information to the filename below. This could compromise privacy, so it should be commented out for production.
+// #define SD_DEBUG_ENABLED
 
 // The filename on the microSD card to store debug information in
-#define SD_FILENAME_DEBUG "debug.log"
+#define SD_FILENAME_DEBUG F("debug.log")
diff --git a/iot/main/storage.cpp b/iot/main/storage.cpp
index 3448d1a..b636e21 100644
--- a/iot/main/storage.cpp
+++ b/iot/main/storage.cpp
@@ -1,41 +1,48 @@
 #include <Arduino.h>
-#include <SD.h>
-#include <TinyGPS++.h>
+#include <SdFat.h>
+// #include <ArduinoFiles.h>
 
 #include "settings.h"
+#include "gps.h"
 
-File _debug_handle;
+// FUTURE: We might be able to trim it down if we disable long filenames with #define 
 
-void store_init() {
-	// NOTE: If this doesn't work, then we need to use pinMode() & specify the data pin here instead.
-	if(!SD.begin(PIN_SD_SPI_CHIP_SELECT)) {
-		Serial.println(F("Error: Failed to initialise connection to microSD card"));
-		while(true) { delay(1); } // Pseudo-halt, but uses delay() to ensure we keep passing back control to allow easy re-programming
+SdFat store_init() {
+	SdFat card;
+	if(!card.begin(PIN_SPI_CS_SD)) {
+		Serial.println("Error: MicroSD card init failed");
+		while(true) delay(100);
 	}
+	return card;
 }
 
-void store_reading(uint32_t id, TinyGPSLocation location) {
+void store_reading(uint32_t id, GPSLocation location) {
+	// Port the rest of this to SdFat from SD
+	SdFat card = store_init();
+	
 	// Open the file to  write the data to. If it doesn't exist it will be created.
 	// Unlike elsewhere, FILE_WRITE opens the file with the cursor starting at the end, so it's basically actually equivalent to FILE_APPEND that we use in other environments. Confusing, I know.
-	File handle = SD.open(SD_FILENAME, FILE_WRITE);
-	
-	handle.print(id);
-	handle.print("\t");
-	
-	handle.print(location.lat());
-	handle.print("\t");
-	
-	handle.print(location.lng());
-	handle.println();
-	
-	handle.close();
+	File handle = card.open(SD_FILENAME, O_APPEND | O_CREAT | O_WRONLY);
+	uint8_t places = 6;
+	handle.printField(id, '\t');
+	handle.printField(location.lat, '\t', places);
+	handle.printField(location.lng, '\n', places);
+	handle.close(); // Syncs implicitly
 }
 
 void store_debug(char* buffer, int size) {
-	File handle = SD.open(SD_FILENAME_DEBUG, FILE_WRITE);
-	
+	SdFat card = store_init();
+	File handle = card.open(SD_FILENAME_DEBUG, O_APPEND | O_CREAT | O_WRONLY);
+	handle.write(buffer, size);
+	handle.write('\n');
+	handle.close();
 }
 
-void store_close() {
-	SD.end();
+/*
+void store_end() {
+	// Apparently we'ree fine so long as we don't have any open file handles - there's no end() method on the SdFat class
+	// card->end();
+	delete card;
+	card = nullptr;
 }
+*/
diff --git a/iot/main/storage.h b/iot/main/storage.h
index d5386c4..bb0a476 100644
--- a/iot/main/storage.h
+++ b/iot/main/storage.h
@@ -1,14 +1,14 @@
 #pragma once
 
 #include <SPI.h>
-#include <SD.h>
+#include <SdFat.h>
 
-#include <TinyGPS++.h>
+#include "gps.h"
 
 void store_init();
 
-void store_reading(uint32_t id, TinyGPSLocation location);
+void store_reading(uint32_t id, GPSLocation location);
 
 void store_debug(char* buffer, int size);
 
-void store_close();
+// void store_end();
diff --git a/iot/main/transmission.cpp b/iot/main/transmission.cpp
new file mode 100644
index 0000000..ad32f50
--- /dev/null
+++ b/iot/main/transmission.cpp
@@ -0,0 +1,150 @@
+#include <lmic.h>
+#include <hal/hal.h>
+#include <SPI.h>
+
+#include "settings.h"
+
+// Global static variable that's used to detect when LMIC has finished doing it's thing
+bool is_sending_complete = false;
+
+void transmit_init() {
+	Serial.println(F("[LMIC] Init"));
+	// LMIC init
+	os_init();
+	// Reset the MAC state. Session and pending data transfers will be discarded.
+	LMIC_reset();
+	
+	// Set static session parameters. Instead of dynamically establishing a session
+	// by joining the network, precomputed session parameters are be provided.
+	#ifdef PROGMEM
+	// On AVR, these values are stored in flash and only copied to RAM
+	// once. Copy them to a temporary buffer here, LMIC_setSession will
+	// copy them into a buffer of its own again.
+	uint8_t appskey[sizeof(APPSKEY)];
+	uint8_t nwkskey[sizeof(NWKSKEY)];
+	memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
+	memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
+	LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
+	#else
+	// If not running an AVR with PROGMEM, just use the arrays directly 
+	LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
+	#endif
+	
+	// Set up the channels used by the Things Network, which corresponds
+	// to the defaults of most gateways. Without this, only three base
+	// channels from the LoRaWAN specification are used, which certainly
+	// works, so it is good for debugging, but can overload those
+	// frequencies, so be sure to configure the full frequency range of
+	// your network here (unless your network autoconfigures them).
+	// Setting up channels should happen after LMIC_setSession, as that
+	// configures the minimal channel set.
+	LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
+	LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
+	LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
+	// TTN defines an additional channel at 869.525Mhz using SF9 for class B
+	// devices' ping slots. LMIC does not have an easy way to define set this
+	// frequency and support for class B is spotty and untested, so this
+	// frequency is not configured here.
+
+	// Disable link check validation
+	LMIC_setLinkCheckMode(0);
+
+	// Set data rate and transmit power (note: txpow seems to be ignored by the library)
+	LMIC_setDrTxpow(DR_SF7,14);
+}
+
+/**
+ * Sends a specified message via LoRaWAN.
+ * @param data   The message to send.
+ * @param length The length of the given message.
+ */
+bool transmit_send(uint8_t* data, int length) {
+	// Check if there is not a current TX/RX job running
+	if (LMIC.opmode & OP_TXRXPEND) {
+		Serial.println(F("OP_TXRXPEND: Job running, can't send"));
+		return false;
+	}
+	// Prepare upstream data transmission at the next possible time.
+	LMIC_setTxData2(1, data, length, 0);
+	Serial.println(F("Packet queued"));
+	
+	// Run the LMIC loop, but only until it's finished sending the packet
+	while (!is_sending_complete) {
+		os_runloop_once();
+	}
+	
+	// Reset it for next time (just in case)
+	is_sending_complete = false;
+	
+	return true;
+}
+
+void onEvent (ev_t ev) {
+	Serial.print(millis());
+	Serial.print(" ");
+	switch(ev) {
+		case EV_SCAN_TIMEOUT:
+			Serial.println(F("EV_SCAN_TIMEOUT"));
+			break;
+		case EV_BEACON_FOUND:
+			Serial.println(F("EV_BEACON_FOUND"));
+			break;
+		case EV_BEACON_MISSED:
+			Serial.println(F("EV_BEACON_MISSED"));
+			break;
+		case EV_BEACON_TRACKED:
+			Serial.println(F("EV_BEACON_TRACKED"));
+			break;
+		/*case EV_JOINING:
+			Serial.println(F("EV_JOINING"));
+			break;
+		case EV_JOINED:
+			Serial.println(F("EV_JOINED"));
+			break;*/
+		case EV_RFU1:
+			Serial.println(F("EV_RFU1"));
+			break;
+		/*case EV_JOIN_FAILED:
+			Serial.println(F("EV_JOIN_FAILED"));
+			break;
+		case EV_REJOIN_FAILED:
+			Serial.println(F("EV_REJOIN_FAILED"));
+			break;*/
+		case EV_TXCOMPLETE:
+			Serial.println(F("EV_TXCOMPLETE (incl. RX window wait)"));
+			if(LMIC.dataLen) {
+				// data received in rx slot after tx
+				Serial.print(F("Received: "));
+				Serial.write(LMIC.frame+LMIC.dataBeg, LMIC.dataLen);
+				Serial.println();
+			}
+			// We're done!
+			is_sending_complete = true;
+			break;
+		case EV_LOST_TSYNC:
+			Serial.println(F("EV_LOST_TSYNC"));
+			break;
+		case EV_RESET:
+			Serial.println(F("EV_RESET"));
+			break;
+		case EV_RXCOMPLETE:
+			// data received in ping slot
+			Serial.println(F("EV_RXCOMPLETE"));
+			break;
+		case EV_LINK_DEAD:
+			Serial.println(F("EV_LINK_DEAD"));
+			break;
+		case EV_LINK_ALIVE:
+			Serial.println(F("EV_LINK_ALIVE"));
+			break;
+		 default:
+			Serial.println(F("Unknown event"));
+			break;
+	}
+}
diff --git a/iot/main/transmission.h b/iot/main/transmission.h
new file mode 100644
index 0000000..38a9bc4
--- /dev/null
+++ b/iot/main/transmission.h
@@ -0,0 +1,13 @@
+#pragma once
+
+/**
+ * Initialises the RFM95 LoRa radio.
+ */
+void transmit_init();
+
+/**
+ * Sends a specified message via LoRaWAN.
+ * @param data   The message to send.
+ * @param length The length of the given message.
+ */
+bool transmit_send(byte* data, int length);
