commit 512ec482f7eea0fec6568440cfdcc288725ac76f Author: Martin Bauer Date: Sat Aug 17 23:26:17 2019 +0200 New firmware - with host mockup for testing diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..5c98b42 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/firmware.iml b/.idea/firmware.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/firmware.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8822db8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..cb860b6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..dea4edf --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required (VERSION 2.6) +project (pooltrainer_firmware) + +add_executable(test sessiontest.cpp) \ No newline at end of file diff --git a/MockDtypes.h b/MockDtypes.h new file mode 100644 index 0000000..3b57802 --- /dev/null +++ b/MockDtypes.h @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include +#include +#include + +typedef uint32_t uint_t; + +typedef std::string String; +typedef uint8_t byte; +typedef const char * PGM_P; + +using std::min; +using std::max; + +inline uint32_t strlen_P(PGM_P str) { + return std::strlen(str); +} + +inline const char * F( const char * in) { return in; } + + +inline void _assert(const char* expression, const char* message, const char* file, int line) +{ + std::cerr << "Assert " << file << ":" << line << " '" << expression << "' failed." << std::endl; + std::cerr << message << std::endl; + + abort(); +} + +template< typename T> +inline std::string toString(const T & t) { + std::stringstream stream; + stream << t; + return stream.str(); +} + +#define assert(EXPRESSION, MSG) ((EXPRESSION) ? (void)0 : _assert(#EXPRESSION, #MSG, __FILE__, __LINE__)) \ No newline at end of file diff --git a/MockSerial.h b/MockSerial.h new file mode 100644 index 0000000..6daaccb --- /dev/null +++ b/MockSerial.h @@ -0,0 +1,18 @@ +#pragma once + +#include + + +class SerialMock { +public: + template< typename T> + static inline void print(const T & str) { + std::cout << str; + } + template< typename T> + static inline void println(const T & str) { + std::cout << str << std::endl; + } +}; + +static SerialMock Serial; \ No newline at end of file diff --git a/SessionChunkIO.h b/SessionChunkIO.h new file mode 100644 index 0000000..af6b380 --- /dev/null +++ b/SessionChunkIO.h @@ -0,0 +1,28 @@ +#include +#include + + +class ESP8266HttpMsgPackWriter { +public: + HttpWriterAdaptor(ESP8266WebServer + * o) + : + obj_(o) {} + + void write(const char *data, uint32_t size) { + obj_->sendContent_P(data, size); + } + +private: + ESP8266WebServer *obj_; +}; + + +template +void saveSessionChunkToFile(const SessionChunk_T &chunk, const String &fileName) { + String startTimeStr(chunk.getStartTime()); + File f = SPIFFS.open(fileName, "w"); + StreamingMsgPackEncoder encoder(&f); + chunk.serialize(encoder); + f.close(); +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..af74308 --- /dev/null +++ b/config.h @@ -0,0 +1,18 @@ + +// WIFI Parameters +const char* WIFI_SSID = "RepeaterWZ"; +const char* WIFI_PASSWD = "Bau3rWLAN"; +const char* HOSTNAME = "swimtrainer"; +const bool CORS_HEADER = true; + +// HX711 connection +const int LOADCELL_DOUT_PIN = D2; +const int LOADCELL_SCK_PIN = D3; +const int LED_PIN = D1; + +// Measurement parameters +const int DELAY = 100; // interval in ms between measurements +const int SESSION_SIZE = 1024*8; // how many data points are added to the session +const byte MEASUREMENT_AVG_COUNT = 1; // averages over this many consecutive AD-converter reads +const byte TARE_AVG_COUNT = 50; // number of measurements in tare-phase (to find 0 ) +const int DIVIDER = 128; // uint32 measurements are divided by this factor, before stored in uint16_t diff --git a/firmware.ino b/firmware.ino new file mode 100644 index 0000000..4357902 --- /dev/null +++ b/firmware.ino @@ -0,0 +1,138 @@ +#include "HX711.h" +#include +#include // for NTP +#include // for NTP +#include +#include + +#include "config.h" + + +int16_t compressMeasurement(int32_t value) { + return (int16_t)(measurement / DIVIDER) +} + + + +HX711 scale; +WiFiUDP ntpUDP; +NTPClient timeClient(ntpUDP, "pool.ntp.org"); +TrainingSession session; +ESP8266WebServer webServer(80); + +bool makeMeasurement(long & measurementOut) +{ + if (scale.is_ready()) + { + measurementOut = scale.get_value(MEASUREMENT_AVG_COUNT); + return true; + } + else + return false; +} + + +void setup() +{ + digitalWrite(LED_PIN, HIGH); + + // Serial + Serial.begin(115200); + while(!Serial) {} + + // wifi + WiFi.mode(WIFI_STA); + WiFi.hostname(HOSTNAME); + WiFi.begin(WIFI_SSID, WIFI_PASSWD); + + Serial.print(F("\n\n")); + Serial.println(F("Waiting for WIFI connection...")); + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + } + + Serial.print(F("Connected to WiFi. IP:")); + Serial.println(WiFi.localIP()); + + timeClient.begin(); + timeClient.update(); + + // initialize cell + scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN); + scale.tare( TARE_AVG_COUNT ); + + // NTP + session.init( &timeClient ); + Serial.print("Initialized NTP client: "); + Serial.println(timeClient.getEpochTime()); + + // webserver + webServer.on("/api/session", [] () { + session.send(&webServer); + }); + webServer.on("/api/save", [] () { + webServer.send(200, "text/plain", session.saveToFileSystem()); + }); + webServer.on("/api/tare", [] () { + scale.tare( TARE_AVG_COUNT ); + webServer.send(200, "text/plain", "OK"); + }); + webServer.on("/", HTTP_GET, [](){ + Serial.println("index.html requested"); + File file = SPIFFS.open("/index.html", "r"); + size_t sent = webServer.streamFile(file, "text/html"); + file.close(); + }); + webServer.on("/swimtrainer.webmanifest", HTTP_GET, [](){ + File file = SPIFFS.open("/swimtrainer.webmanifest", "r"); + size_t sent = webServer.streamFile(file, "application/manifest+json"); + file.close(); + }); + + + + webServer.begin(); + Serial.println("Webserver started"); + + // flash file system + if(!SPIFFS.begin()){ + Serial.println("An Error has occurred while mounting SPIFFS"); + } +} + + +void loop() +{ + const long cycleStart = millis(); + + + digitalWrite(LED_PIN, HIGH); + + + long measurement = 0; + if(makeMeasurement(measurement)) + { + session.addPoint(measurement); + } else { + Serial.println("Measurement skipped - cell not ready"); + } + + webServer.handleClient(); + + const long cycleDuration = millis() - cycleStart; + if( cycleDuration <= DELAY) + { + delay(DELAY - cycleDuration); + } + else + { + Serial.print("Skipping measurement, cycle duration was "); + Serial.println(cycleDuration); + const long skipped = (cycleDuration / DELAY); + //for(int i=0; i < skipped; ++i) + // session.addPoint(0xFFFFFFFE); + + delay(DELAY * (skipped + 1) - cycleDuration); + } + +} \ No newline at end of file diff --git a/session/MockStorage.h b/session/MockStorage.h new file mode 100644 index 0000000..dc8d38d --- /dev/null +++ b/session/MockStorage.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include + + +class Adaptor { +public: + Adaptor(const String &fileName) { + static const String baseDirectory("."); + auto fullFileName = baseDirectory + fileName; + fptr = fopen(fullFileName.c_str(), "wb"); + } + + ~Adaptor() { + fclose(fptr); + } + + void write(const char *data, uint32_t size) { + fwrite(data, size, 1, fptr); + } + +private: + FILE * fptr; +}; + + +class MockStorageWriter { +public: + MockStorageWriter(const String &fileName) { + adaptor_ = new Adaptor(fileName); + encoder_ = new StreamingMsgPackEncoder (adaptor_); + } + ~MockStorageWriter(){ + delete adaptor_; + delete encoder_; + } + MockStorageWriter(const MockStorageWriter &) = delete; + + StreamingMsgPackEncoder &encoder() { return *encoder_; } + +private: + Adaptor * adaptor_; + StreamingMsgPackEncoder * encoder_; + +}; + + +class MockStorageReader { +public: + MockStorageReader(const String &fileName) + { + static const String baseDirectory("."); + auto fullFileName = baseDirectory + fileName; + fptr = fopen(fullFileName.c_str(), "rb"); + } + + uint32_t readBytes(char *buffer, size_t length) { + return fread(buffer, length, 1, fptr); + } + + bool seek(uint32_t pos) { + auto ret = fseek(fptr, pos, SEEK_SET); + return ret == 0; + } +private: + FILE * fptr; +}; \ No newline at end of file diff --git a/session/Session.h b/session/Session.h new file mode 100644 index 0000000..8266f5b --- /dev/null +++ b/session/Session.h @@ -0,0 +1,113 @@ +#include "SessionChunk.h" + + +template +class Session { +public: + typedef SessionChunk Chunk_T; + + Session() + : currentChunk(&chunks[0]), + otherChunk(&chunks[1]) {} + + void init(uint32_t epochStartTime) { + currentChunk = &chunks[0]; + currentChunk->init(epochStartTime, 0); + } + + bool addPoint(Measurement_T measurement) { + const bool successful = currentChunk->addPoint(measurement); + if (!successful) { + rotate(); + const bool secondInsertSuccess = currentChunk->addPoint(measurement); + assert(secondInsertSuccess, "Session: insertion after rotation failed"); + //TODO check that there is place for file - remove old files + } + return true; + } + + template + void serialize(StreamingMsgPackEncoder & encoder, uint32_t startIdx) const + { + const uint32_t lastIdx = currentChunk->getStartIndex() + currentChunk->numMeasurements(); + if( lastIdx <= startIdx) { + encoder.sendArray(nullptr, 0); + return; + } + + Chunk_T::sendHeader(encoder, currentChunk->getStartTime(), startIdx); + encoder.sendArrayHeader(lastIdx - startIdx); + while(startIdx < lastIdx) + startIdx = serializeChunk(encoder, startIdx); + assert(startIdx == lastIdx, "Not all data was sent"); + } + +private: + void rotate() { + if( otherChunkFilled() ) + saveChunkToFile(otherChunk); + swapChunks(); + + currentChunk->init(otherChunk->getStartTime(), otherChunk->getStartIndex() + CHUNK_SIZE); + } + + bool otherChunkFilled() const { + return otherChunk->numMeasurements() > 0; + } + + void swapChunks() { + Chunk_T *tmp = currentChunk; + currentChunk = otherChunk; + otherChunk = tmp; + } + + void saveChunkToFile(Chunk_T *chunk) const { + const uint32_t chunkNr = chunk->getStartIndex() / CHUNK_SIZE; + const auto fileName = chunkFileName(chunkNr, chunk->getStartTime()); + Writer writer( fileName ); + chunk->serialize(writer.encoder()); + }; + + template< typename T> + uint32_t serializeChunk(StreamingMsgPackEncoder & encoder, uint32_t startIdx) { + assert( startIdx < currentChunk->getStartIndex() + currentChunk->numMeasurements(), + "serializeChunk: invalid startIdx" ); + + if( startIdx >= currentChunk->getStartIndex() ) { + encoder.sendArrayPartialContents( currentChunk->getDataPointer(), currentChunk->numMeasurements() ); + return currentChunk->getStartIndex() + currentChunk->numMeasurements(); + } else if( startIdx >= otherChunk->getStartIndex() && otherChunkFilled() ) { + encoder.sendArrayPartialContents( otherChunk->getDataPointer(), otherChunk->numMeasurements() ); + assert( otherChunk->numMeasurements(), CHUNK_SIZE ); + return otherChunk->getStartIndex() + otherChunk->numMeasurements(); + } else { + if( encoder.getSizeCountMode() ) { + encoder.sendArrayPartialContents(nullptr, CHUNK_SIZE); + } else { + const uint32_t chunkNr = startIdx / CHUNK_SIZE; + const auto chunkFileName = (chunkNr, currentChunk->getStartTime()); + Reader reader(chunkFileName); + reader.seek(Chunk_T::valueOffset()); + + const uint32_t PART_SIZE = 32; + static_assert( PART_SIZE < CHUNK_SIZE && CHUNK_SIZE % PART_SIZE == 0); + + Measurement_T buffer[PART_SIZE]; + for(uint32_t i = 0; i < CHUNK_SIZE; i += PART_SIZE) + { + reader.readBytes((char*) buffer, sizeof(Measurement_T) * PART_SIZE); + encoder.sendArrayPartialContents(buffer, PART_SIZE); + } + } + return startIdx + CHUNK_SIZE; + } + } + + static String chunkFileName(uint32_t chunkNr, uint32_t startTime) { + return("/dat/" + toString(startTime) + "_" + toString(chunkNr)); + } + + Chunk_T chunks[2]; + Chunk_T *currentChunk; + Chunk_T *otherChunk; +}; diff --git a/session/SessionChunk.h b/session/SessionChunk.h new file mode 100644 index 0000000..7718fe1 --- /dev/null +++ b/session/SessionChunk.h @@ -0,0 +1,108 @@ +#include "StreamingMsgPackEncoder.h" + + +template +class SessionChunk +{ +public: + SessionChunk() + : nextFree(0), sessionStartTime(0), startIndex(0) + {} + + void init(uint32_t epochStartTime, uint32_t startIdx) + { + nextFree = 0; + sessionStartTime = epochStartTime; + startIndex = startIdx; + } + + uint32_t getStartTime() const { + return sessionStartTime; + } + + uint32_t getStartIndex() const { + return startIndex; + } + + uint32_t numMeasurements() const { + return nextFree; + } + + bool addPoint(Measurement_T measurement) + { + if( nextFree >= SIZE) + return false; + values[nextFree] = measurement; + nextFree++; + + return true; + } + + template + void serialize(StreamingMsgPackEncoder & encoder) const + { + sendHeader(encoder, sessionStartTime, startIndex); + encoder.sendArray(values + startIndex, nextFree - startIndex); + } + + template + void serialize(StreamingMsgPackEncoder & encoder, uint32_t start, uint32_t end) const + { + if( start < this->startIndex ) + start = this->startIndex; + if( end == 0 ) { + end = start + this->nextFree; + } + + sendHeader(encoder, sessionStartTime, start); + + bool sendEmpty = + (start >= end) || + (end <= this->startIndex) || + (start >= (this->startIndex + this->nextFree)); + if( sendEmpty ) { + encoder.sendArray(nullptr, 0); + } else { + const uint32_t idxStart = (start - this->startIndex); + const uint32_t length = min(nextFree, end - start); + encoder.sendArray(values + idxStart, length); + } + } + + template + static uint32_t valueOffset() + { + StreamingMsgPackEncoder encoder(nullptr); + encoder.setSizeCountMode(true); + sendHeader(encoder, 0, 0); + return encoder.getContentLength(); + } + + Measurement_T * getDataPointer() { + return values; + } + + const Measurement_T * getDataPointer() const { + return values; + } + + template + static void sendHeader(StreamingMsgPackEncoder & encoder, uint32_t sessionStartTime, uint32_t startIndex) + { + encoder.sendMap16(3); + + encoder.sendString255("sessionStartTime"); + encoder.sendInt(sessionStartTime); + + encoder.sendString255("startIndex"); + encoder.sendInt(startIndex); + + encoder.sendString255("values"); + } + +private: + uint32_t nextFree = 0; + uint32_t sessionStartTime; + uint32_t startIndex; + Measurement_T values[SIZE]; +}; \ No newline at end of file diff --git a/session/SpiffsStorage.h b/session/SpiffsStorage.h new file mode 100644 index 0000000..d4d625a --- /dev/null +++ b/session/SpiffsStorage.h @@ -0,0 +1,36 @@ +#pragma once +#include "StreamingMsgPackEncoder.h" +#include + + +class SpiffsStorageWriter { +public: + SpiffsStorageWriter(const String &fileName) : + f_(SPIFFS.open(fileName, "w")), + encoder_(&f_) {} + + StreamingMsgPackEncoder &encoder() { return encoder_; } + +private: + File f_; + StreamingMsgPackEncoder encoder_; +}; + + +class SpiffsStorageReader +{ +public: + SpiffsBackendReader(const String &fileName) : + f_(SPIFFS.open(fileName, "w")) + {} + + uint32_t readBytes(char *buffer, size_t length) { + return f_.readBytes(buffer, length); + } + + bool seek(uint32_t pos) { + return f_.seek(pos); + } +private: + File f_; +}; \ No newline at end of file diff --git a/session/StreamingMsgPackEncoder.h b/session/StreamingMsgPackEncoder.h new file mode 100644 index 0000000..5a357ea --- /dev/null +++ b/session/StreamingMsgPackEncoder.h @@ -0,0 +1,126 @@ + +template struct TypeToMsgPackCode{}; +template<> struct TypeToMsgPackCode { static const char CODE; }; +template<> struct TypeToMsgPackCode{ static const char CODE; }; +template<> struct TypeToMsgPackCode{ static const char CODE; }; +template<> struct TypeToMsgPackCode { static const char CODE; }; +template<> struct TypeToMsgPackCode { static const char CODE; }; +template<> struct TypeToMsgPackCode { static const char CODE; }; + +const char TypeToMsgPackCode::CODE = '\xcc'; +const char TypeToMsgPackCode::CODE = '\xcd'; +const char TypeToMsgPackCode::CODE = '\xce'; +const char TypeToMsgPackCode::CODE = '\xd0'; +const char TypeToMsgPackCode::CODE = '\xd1'; +const char TypeToMsgPackCode::CODE = '\xd2'; + + +template +class StreamingMsgPackEncoder +{ +public: + StreamingMsgPackEncoder(Writer * writer_) + : writer(writer_), contentLength(0), sizeCountMode(false) + {} + + void sendMap16(byte size) + { + if( sizeCountMode ) + { + contentLength += 1; + } + else + { + size |= 0b10000000; + writer->write((const char*)(&size), 1); + } + } + + void sendString255(PGM_P s) + { + auto len = strlen_P(s); + if( len >= 255 ) { + Serial.println(F("ERROR: StreamingMsgPackEncoder::string255 - string too long")); + return; + } + byte castedLen = (byte)(len); + + if( sizeCountMode ) + { + contentLength += 2 + castedLen; + } + else + { + writer->write("\xd9", 1); + writer->write((const char*)&castedLen, 1); + writer->write(s, len); + } + } + + template + void sendInt(T value) + { + if( sizeCountMode ) + { + contentLength += 1 + sizeof(T); + } + else + { + if( sizeof(T) == 4 ) + value = htonl(value); + else if( sizeof(T) == 2) + value = htons(value); + writer->write(&TypeToMsgPackCode::CODE, 1); + writer->write((const char*)&value, sizeof(T)); + } + } + + template + void sendArray(const T * data, uint32_t length) + { + sendArrayHeader(length); + sendArrayPartialContents(data, length); + } + + template + void sendArrayHeader(uint32_t length) + { + if( sizeCountMode ) + { + contentLength += 1 + sizeof(uint32_t) + 1 + length * sizeof(T); + } + else + { + uint32_t nlength = htonl(length * sizeof(T)); + writer->write("\xc9", 1); // ext dtype since typed arrays are not supported by msgpack + writer->write((char*)(&nlength), sizeof(uint32_t) ); + writer->write(&TypeToMsgPackCode::CODE, 1); // put code for type here, this is not part of msgpack but custom + } + } + + template + void sendArrayPartialContents(T * data, uint32_t length) + { + if( !sizeCountMode ) { + writer->write((char*)(data), length * sizeof(T)); + } + } + + void setSizeCountMode(bool sizeCountMode_=true) + { + sizeCountMode = sizeCountMode_; + } + + bool getSizeCountMode() const { + return sizeCountMode; + } + + uint32_t getContentLength() const { + return contentLength; + } + +private: + Writer * writer; + uint32_t contentLength; + bool sizeCountMode; +}; diff --git a/sessiontest.cpp b/sessiontest.cpp new file mode 100644 index 0000000..ca7a261 --- /dev/null +++ b/sessiontest.cpp @@ -0,0 +1,18 @@ +#include "MockDtypes.h" +#include "MockSerial.h" +#include "session/Session.h" +#include "session/MockStorage.h" + +const uint32_t SESSION_SIZE = 128; +typedef Session MockSession; + +int main(int argc, char**argv) +{ + MockSession session; + + for( uint16_t i=0; i < SESSION_SIZE; ++i) { + session.addPoint(i); + } + + return 0; +}