commit 50085550dd0bb3c24e2930644e4d976f67852fe7 Author: noerw Date: Wed Aug 31 19:21:24 2016 +0200 initial version diff --git a/TelnetPrint.h b/TelnetPrint.h new file mode 100644 index 0000000..6c2ea53 --- /dev/null +++ b/TelnetPrint.h @@ -0,0 +1,70 @@ +/** + * sets up a telnet server, which can then be used for debug logging purposes + * provides all Print methods. + * begin() must be called in the setup! + * pollClients() must be called in the loop! + * to disable the routines, just skip the begin() call in the setup + */ + +# pragma once +#include +#define MAX_TELNET_CLIENTS 3 + +class TelnetPrint : public Print { + protected: + WiFiServer server; + WiFiClient clients[MAX_TELNET_CLIENTS]; + + public: + TelnetPrint(uint16_t port=23) : server(port) {} + + void begin() { + server.begin(); + server.setNoDelay(true); + } + + void pollClients() { + //check clients for data + for(uint16_t i = 0; i < MAX_TELNET_CLIENTS; i++){ + if (clients[i] && clients[i].connected()){ + while(clients[i].available()) clients[i].read(); + } + } + + // try to establish pending connections + if (server.hasClient()) { + for(uint8_t i = 0; i < MAX_TELNET_CLIENTS; i++){ + //find free/disconnected spot + if (!clients[i] || !clients[i].connected()){ + if(clients[i]) clients[i].stop(); + clients[i] = server.available(); + return; + } + } + //no free/disconnected spot so reject + WiFiClient serverClient = server.available(); + serverClient.stop(); + } + } + + // TODO: find more efficient method than sending byte by byte + virtual size_t write(uint8_t s) { + for(uint8_t i = 0; i < MAX_TELNET_CLIENTS; i++){ + if (clients[i] && clients[i].connected()){ + clients[i].write(s); + } + } + return 1; + } + + // Is only called for Strings, not char arrays + virtual size_t write(const char *buffer, size_t size) { + for(uint8_t i = 0; i < MAX_TELNET_CLIENTS; i++){ + if (clients[i] && clients[i].connected()){ + clients[i].write(buffer, size); + } + } + return size; + } +}; + diff --git a/api.h b/api.h new file mode 100644 index 0000000..b6ee1f4 --- /dev/null +++ b/api.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include "config.h" + +WiFiClientSecure api; + +bool postMeasurement(String m, String sensorID) { + //telnet.print("Connecting to API.. "); + if (!api.connect(API_ENDPOINT, 443)) { + //telnet.println("connection failed"); + return false; + } + if (api.verify(API_FINGERPRINT, API_ENDPOINT)) { + //telnet.println("certificate matches"); + } else { + //telnet.println("certificate doesn't match"); + return false; + } + + String url = "/boxes/" + String(ID_BOX) + "/" + sensorID; + api.print(String("POST ") + url + " HTTP/1.1\r\n" + + "Host: " + API_ENDPOINT + "\r\n" + + "User-Agent: mobile-sensebox-esp8266\r\n" + + "Connection: close\r\n\r\n"); + + return true; +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..6da7db2 --- /dev/null +++ b/config.h @@ -0,0 +1,19 @@ +/* WiFi (ESP8266) */ +#pragma once + +static const char* WIFI_SSID = "Elmo"; +static const char* WIFI_PASS = "FreiBier123"; + +/* GPS reciever (uBloc NEO-7M) */ +static const int GPS_RX_PIN = 4; +static const int GPS_TX_PIN = 3; +static const int GPS_BAUD = 9600; +static const int GPS_INTERVAL = 1000; // update interval of the gps device + +/* API (openSenseMap) */ +static const char* API_ENDPOINT = "api.opensensemap.org"; +// SHA1 of the API SSL cert +static const char* API_FINGERPRINT = "0F B0 0C E0 FD 18 C2 0B 07 1C 21 AB A0 FF EF CC 09 62 57 A9"; +static const char* API_KEY = "..."; +static const char* ID_BOX = "57308b2c566b8d3c11114a9f"; +static const char* ID_SENSOR_WIFI = "..."; diff --git a/gps.h b/gps.h new file mode 100644 index 0000000..76fc5af --- /dev/null +++ b/gps.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include +#include "config.h" + +/* GPS */ +TinyGPSPlus gps; +// allocate the maximum buffer size, so we can reduce the Serial polling interval to a minimum +//SoftwareSerial gpsSerial(GPS_RX_PIN, GPS_TX_PIN, false, 27*49); + +void pollGPS() { + while (Serial.available() > 0) + gps.encode(Serial.read()); +} + +bool updateLocation() { + // abort if the GPS device does not push valid data within one update cycle + static const unsigned int timeout = 2 * GPS_INTERVAL; + unsigned long start = millis(); + + do { + pollGPS(); + if (millis() - start > timeout) return false; + } while (!gps.location.isUpdated()); + return true; +} + +bool updateTime() { + // abort if the GPS device does not push valid data within one update cycle + static const unsigned int timeout = 2 * GPS_INTERVAL; + unsigned long start = millis(); + + do { + pollGPS(); + if (millis() - start > timeout) return false; + } while ( !(gps.date.isUpdated() && gps.time.isUpdated()) ); + + // in case we didnt timeout, resync the local clock + setTime(gps.time.hour(), gps.time.minute(), gps.time.second(), + gps.date.day(), gps.date.month(), gps.date.year()); + + return true; +} + + +/* UTILS */ +String getISODate() { + char result[16] = { 0 }; + sprintf(result, "%04d-%02d-%02d-T%02d:%02d:%02dZ", + year(), month(), day(), hour(), minute(), second()); + return (String)result; +} diff --git a/mobile-sensebox.ino b/mobile-sensebox.ino new file mode 100644 index 0000000..3e33129 --- /dev/null +++ b/mobile-sensebox.ino @@ -0,0 +1,95 @@ +#include "config.h" +#include "gps.h" +#include "wifi.h" +#include "api.h" +#include "storage.h" +#include "TelnetPrint.h" + +#define DEBUG_OUT Serial + +//TelnetPrint telnet = TelnetPrint(); +Storage storage = Storage(); + +/* UTILS */ +void printState(WifiState wifi) { + DEBUG_OUT.print("homeAvailable: "); + DEBUG_OUT.println(wifi.homeAvailable); + DEBUG_OUT.print("numNetworks: "); + DEBUG_OUT.println(wifi.numNetworks); + DEBUG_OUT.print("numUnencrypted: "); + DEBUG_OUT.println(wifi.numUnencrypted); + + DEBUG_OUT.print("lat: "); + //DEBUG_OUT.print(gps.location.lat(), 6); + DEBUG_OUT.print(" lng: "); + //DEBUG_OUT.println(gps.location.lng(), 6); + + DEBUG_OUT.println(getISODate()); + DEBUG_OUT.println(""); +} + + +/* MAIN ENTRY POINTS */ +void setup() { + //Serial.begin(9600); // GPS reciever + DEBUG_OUT.begin(115200); + + WiFi.mode(WIFI_STA); + connectWifi(WIFI_SSID, WIFI_PASS); + + delay(5000); + + // wait until we got a first fix from GPS, and thus an initial time + /*DEBUG_OUT.print("Getting GPS fix.."); + while (!updateLocation()) { DEBUG_OUT.print("."); } + DEBUG_OUT.print(" done! ");*/ + DEBUG_OUT.println(getISODate()); + + DEBUG_OUT.println("Setup done!\n"); + DEBUG_OUT.println("WiFi MAC WiFi IP"); + DEBUG_OUT.print(WiFi.macAddress()); + DEBUG_OUT.print(" "); + DEBUG_OUT.println(WiFi.localIP()); + + DEBUG_OUT.print("SPIFF bytes free: "); + DEBUG_OUT.println(storage.begin()); + + digitalWrite(D9, HIGH); +} + +bool firstLoop = true; +void loop() { + //pollGPS(); + //DEBUG_OUT.pollClients(); + WifiState wifi = scanWifi(WIFI_SSID); + + // TODO: take other measurements (average them?) +/* + if(!updateLocation()) DEBUG_OUT.println("GPS timed out (location)"); + if(!updateTime()) DEBUG_OUT.println("GPS timed out (time)"); +*/ + // TODO: write measurements to file + + // TODO: connect to wifi, if available & not connected yet + // then upload local data, clean up + + printState(wifi); + + //delay(20000); + if (firstLoop) { + Measurement testMeasure; + testMeasure.lat = 51.2; + testMeasure.lng = 7.89; + testMeasure.value = 66.6; + strcpy(testMeasure.timeStamp, "2016-08-21T09:07:22Z"); + strcpy(testMeasure.sensorID, "123457812345678123456781234567"); + + if (storage.add(testMeasure)) { + DEBUG_OUT.println("measurement stored! storage size: "); + } else { + DEBUG_OUT.println("measurement store failed! storage size: "); + } + DEBUG_OUT.println(storage.size()); + firstLoop = false; + } +} diff --git a/storage.h b/storage.h new file mode 100644 index 0000000..aef3bdb --- /dev/null +++ b/storage.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include "api.h" + +#define MEASUREMENT_JSON_SIZE (JSON_OBJECT_SIZE(5)) + +struct Measurement { + char timeStamp[21]; + float lat; + float lng; + float value; + char sensorID[32]; +}; + +// TODO: TEST THIS THING!! +class Storage { + protected: + // not needed, as we post the data as json string? + /*Measurement deserializeMeasurement(char* s) { + Measurement m; + StaticJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.parseObject(s); + m.timeStamp = (const char[sizeof Measurement.timeStamp])jsonBuffer.strdup(root["date"]); + m.lat = root["lat"]; + m.lng = root["lng"]; + m.value = root["value"]; + m.sensorID = root["sensorId"]; + return m; + }*/ + + bool serializeMeasurement(Measurement& m, Print& f) { + StaticJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + // TODO: fix data model + root["date"] = m.timeStamp; + root["lat"] = m.lat; + root["lng"] = m.lng; + root["value"] = m.value; + root["sensorId"] = m.sensorID; + root.printTo(f); + f.print("\n"); + return root.success(); + } + + public: + Storage() {} + + size_t begin() { + SPIFFS.begin(); + FSInfo fs; + SPIFFS.info(fs); + return fs.totalBytes - fs.usedBytes; + } + + bool add(Measurement& m, const char* directory = "/measurements/") { + byte uuid[16]; + ESP8266TrueRandom.uuid(uuid); + // we need to shorten the uuid, as long filenames are not supported it seems..? + String fileName = directory + ESP8266TrueRandom.uuidToString(uuid).substring(26); + + if (File f = SPIFFS.open(fileName, "w") ) { + bool success = serializeMeasurement(m, f); + f.close(); + return success; + } + return false; + } + + String pop(const char* directory = "/measurements/") { + Dir dir = SPIFFS.openDir(directory); + String measurement = ""; + if (!dir.next()) return measurement; // abort if storage is empty + String fileName = dir.fileName(); + File f = dir.openFile("r"); + measurement = f.readStringUntil('\n'); // assumes that the data does not contain any \n! + f.close(); + SPIFFS.remove(fileName); + return measurement; + } + + /*const Measurement pop(const char* directory = "/measurements/") { + Dir dir = SPIFFS.openDir(directory); + Measurement m; + if (!dir.next()) return m; // abort if storage is empty + String fileName = dir.fileName(); + File f = dir.openFile("r"); + m = deserializeMeasurement(f); + f.close(); + SPIFFS.remove(fileName); + return m; + }*/ + + uint16_t size(const char* directory = "/measurements/") { + Dir dir = SPIFFS.openDir(directory); + uint16_t i = 0; + while(dir.next()) i++; + return i; + } + +}; + diff --git a/wifi.h b/wifi.h new file mode 100644 index 0000000..3809689 --- /dev/null +++ b/wifi.h @@ -0,0 +1,47 @@ +#pragma once +#include +#include "config.h" + +/* WIFI */ +struct WifiState { + bool homeAvailable; + unsigned int numNetworks; + unsigned int numUnencrypted; +}; + +// TODO: filter duplicate SSIDs! +WifiState scanWifi(String homeSSID) { + WifiState state; + state.homeAvailable = false; + state.numNetworks = WiFi.scanNetworks(false, false); + state.numUnencrypted = 0; + + for (unsigned int i = 0; i < state.numNetworks; i++) { + if (WiFi.encryptionType(i) == ENC_TYPE_NONE) + ++state.numUnencrypted; + + if (WiFi.SSID(i) == homeSSID) + state.homeAvailable = true; + } + + return state; +} + +bool connectWifi(const char* ssid, const char* pass) { + static const unsigned int timeout = 10000; // abort after 10 secs + unsigned long start = millis(); + + WiFi.disconnect(); + WiFi.begin(ssid, pass); + //telnet.print("Connecting to WiFi."); + while (WiFi.status() != WL_CONNECTED && millis() - start < timeout) { + delay(200); + //telnet.print("."); + } + if (WiFi.status() == WL_CONNECTED) { + //telnet.println("connected!"); + return true; + } + //telnet.println(" timeout"); + return false; +}