From ea34c399d7d868fa81091c35c1cfb696a593880b Mon Sep 17 00:00:00 2001 From: bionickatana Date: Sat, 8 Aug 2026 20:58:06 -0600 Subject: [PATCH] Initial project commit --- .gitignore | 3 + design.md | 506 +++++++++++++++++++++++++++++++ include/README | 37 +++ lib/README | 46 +++ platformio.ini | 20 ++ readme.md | 10 + requirements.txt | 2 + src/main.cpp | 36 +++ src/services/ota_service.cpp | 25 ++ src/services/ota_service.h | 16 + src/services/service.h | 46 +++ src/services/service_manager.cpp | 27 ++ src/services/service_manager.h | 20 ++ src/services/web_service.cpp | 161 ++++++++++ src/services/web_service.h | 19 ++ src/services/wifi_service.cpp | 39 +++ src/services/wifi_service.h | 15 + test/README | 11 + 18 files changed, 1039 insertions(+) create mode 100644 .gitignore create mode 100644 design.md create mode 100644 include/README create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 readme.md create mode 100644 requirements.txt create mode 100644 src/main.cpp create mode 100644 src/services/ota_service.cpp create mode 100644 src/services/ota_service.h create mode 100644 src/services/service.h create mode 100644 src/services/service_manager.cpp create mode 100644 src/services/service_manager.h create mode 100644 src/services/web_service.cpp create mode 100644 src/services/web_service.h create mode 100644 src/services/wifi_service.cpp create mode 100644 src/services/wifi_service.h create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f8bafd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.pio +.clang_complete +.ccls diff --git a/design.md b/design.md new file mode 100644 index 0000000..a3a23f2 --- /dev/null +++ b/design.md @@ -0,0 +1,506 @@ +# High level overview + +This application is less like an Arduino sketch and more like an **embeded application**. It should include the patterns of seperation of concerns, services, interfaces, event-driven communication, configuration management and dependency boundries. Here are the design requirements: + + +This project is the **Hub** in a Hub and spoke architechure. The Hub is responsible for creating an access point that all of the spokes (Nodes) can connect to. The Hub will use a round-robbin scheduling to collect sensor data from the spokes. The spokes are collecting audio data and storing it in a buffer that is read by the Hub. This means that we have to be on time and can not fall behind schedule. This means that we should either use or lean heavily toward a real time system. + +The Hub is also responsible for storing the audio data it is collecting onto an SD card. This **must** be done using 4 bit SDIO to be able to achieve the write speeds required to save all of the audio data. We estimate to support up to 10 Nodes each with 4 microphones. The raw audio should be saved to the SD card for post processing. + +The Hub also will act as the interface with the users. It will contain a web dashboard that users can use to view the status of the Hub and the Nodes. + + +## Define Hub responsibilities + + +My Hub is going to have several independent responsibilities: + +1. **Network Management** + + * Creates WiFi network + * Handles connected devices + * Manages IP addresses + * Handles OTA + * Handles Node authentication + +2. **Device Management** + + * Knows what sensor nodes exist + * Tracks online/offline state + * Stores metadata: + + * device ID + * last seen time + * firmware version + * sensor capabilities + * signal strength + +3. **Sensor Data Collection** + + * Polls other ESP32 nodes + * Parses responses + * Validates data + * Converts into a common format + +4. **Data Storage** + + * SD card management + * Logging + * File rotation + * Handling SD card failures + +5. **Web Interface** + + * Dashboard + * API endpoints + * Configuration pages + * OTA interface + +6. **System Monitoring** + + * Free heap + * Uptime + * CPU usage + * Connected clients + * Errors + +7. **Configuration** + + * WiFi settings + * Sensor definitions + * Logging intervals + * Device names + + +# Proposed High-Level Structure + +I would lean toward something like this: + +``` +src/ +│ +├── main.cpp +│ +├── core/ +│ ├── system_manager.cpp +│ ├── event_bus.cpp +│ └── config.cpp +│ +├── network/ +│ ├── wifi_manager.cpp +│ ├── ota_manager.cpp +│ └── api_server.cpp +│ +├── sensors/ +│ ├── sensor_manager.cpp +│ ├── sensor_node.cpp +│ └── sensor_protocol.cpp +│ +├── storage/ +│ ├── sd_manager.cpp +│ └── data_logger.cpp +│ +├── web/ +│ ├── web_server.cpp +│ ├── dashboard.cpp +│ └── web_assets/ +│ +└── models/ + ├── sensor_data.h + ├── device_info.h + └── system_status.h +``` + +The goal is that `main.cpp` becomes boring. + +Ideally: + +```text +initialize system +start services +run loop +``` + + +# How Modules Communicate + + +``` + +----------------+ + | Web Interface | + +-------+--------+ + | + | + +-------v--------+ + | System State | + +-------+--------+ + | + +-------------+-------------+ + | | | + v v v + + Sensor Manager SD Logger Network Manager +``` + +The web interface does not know how sensors work. + +It asks: + +> "Give me current system state." + +Not: + +> "Go ask the sensor manager, which talks to WiFi, which parses packets..." + +--- + +# Introduce a Data Model Layer + +Instead of passing random variables around: + +```cpp +temperature +humidity +battery +deviceName +``` + +Create common structures. + +Conceptually: + +``` +SensorReading + +{ + deviceId, + timestamp, + temperature, + humidity, + batteryVoltage +} +``` + +Then everything speaks this language: + +``` +Sensor Node + | + | + v +SensorReading + | + +------> SD Logger + | + +------> Dashboard + | + +------> API +``` + +Now if I change from ESP-NOW to WiFi HTTP to MQTT later, the rest of the system does not care. + +--- + +# Consider a Service-Oriented Architecture + +I would like a service oriented architechure like this: + +``` +WifiService +SensorService +StorageService +WebService +OTAService +``` + +Each service has: + +``` +begin() +loop() +status() +``` + +Conceptually: + +``` +setup() +{ + wifi.begin(); + storage.begin(); + sensors.begin(); + web.begin(); +} + + +loop() +{ + wifi.update(); + sensors.update(); + storage.update(); + web.update(); +} +``` + +This is very ESP32-friendly. + +--- + +# Use Interfaces Where Things Might Change + +For example, how do sensors communicate? + +Today: + +``` +Hub ---- HTTP ----> Sensor ESP32 +``` + +Tomorrow: + +``` +Hub ---- ESP-NOW ----> Sensor ESP32 +``` + +Later: + +``` +Hub ---- MQTT ----> Sensor ESP32 +``` + +If we build our sensor system around: + +``` +SensorTransport +``` + +we can swap the communication method without rewriting our sensor manager. + +Example concept: + +``` + SensorManager + + | + | + + SensorTransport Interface + + / | \ + + HTTP ESP-NOW MQTT +``` + +This is a very powerful pattern. + +--- + +# Event-Driven vs Polling + +My system naturally has events: + +Examples: + +* Sensor came online +* Sensor stopped responding +* New reading received +* SD card full +* OTA started + +Instead of: + +```cpp +if(sensorOffline) +{ + updateWebPage(); +} +``` + +you could have: + +``` +SensorManager + | + | + emits: + SensorOfflineEvent + + | + | + +------+------+ + | | + Web Interface Logger +``` + +This prevents modules from knowing about each other. + +For an ESP32 project, I would not over-engineer this with a full framework, but a simple event queue could be very clean. + +--- + +# Configuration Management + +Avoid hardcoding: + +```cpp +const char* wifiName = "MyNetwork"; +``` + +Instead: + +``` +/config.json +``` + +Example: + +``` +{ + "Hub_name": "greenhouse-Hub", + "logging_interval": 60, + "sensors": [ + { + "id": "sensor01", + "location": "north bed" + } + ] +} +``` + +Then your web dashboard could eventually modify configuration. + +--- + +# Think About Failure Modes Early + +Embedded systems fail differently than servers. + +Plan for: + +## SD card removed + +If the SD card is removed, it is acceptable to crash/halt (of course with a +nice error message) We do not have enough RAM to store the data so it is not +feasable to buffer until the SD card comes back. + +## Sensor disappears + +If one of the Nodes dissappear, we need to alert the user. This should be done +with a warning light on the device however I do not think it is a critical +failure. As we are recording audio, we can just insert silence in that section +if the Node drops off. + +## Power loss + +With power loss I want the stored audio capture to not be corrupted. That is +about the only requirement. If the system dies mid recording I want to be able +to still use whatever was recorded before it died. + +## Corrupt configuration + +I want to have the device configurable through USB. So I can give it a default +configuration however then end users can use a yaml file to reconfigure the +device. This should then reboot the device and start it up again. + +# Logging Architecture + +I would avoid: + +``` +sensor_manager.cpp + +writeFile() +``` + +Instead: + +``` +SensorManager + +produces: + +SensorReading + + | + v + +DataLogger + + | + v + +SDManager +``` + +Then the logger can do it's own thing and the sensor system doesn't care. + +--- + +# Mental Model + +Think of the Hub as a small operating system: + +``` + HUB + + +----------------+ + | Web Dashboard | + +----------------+ + + +----------------+ + | System State | + +----------------+ + + +----------+ +----------+ +----------+ + | Sensors | | Storage | | Network | + +----------+ +----------+ +----------+ + + +----------------+ + | Hardware Layer | + +----------------+ +``` + +--- + +# Node Hub communication + +This will be done using UDP packets. The Hub will send out a packet to ask for +data and the Node will reply with a packet containnig the binary audio data. + +--- + +# Node count + +I estimate having 10 nodes sampling 4 microphones each. Doing the math this +ends up with about 6 MB/sec which is doable if I use 4 bit SDIO and wifi is +totally capable of handling those speeds. + + +--- + +## Data frequency + +The nodes do not have much RAM so I want to collect frequently. I am thinking +that when the Nodes connect to the Hub they are assigned a "time slot" so that +they can dump their data during that time. + +--- + +## Power + +The Hub is batery powered however it will be a large power bank. The nodes +however are not and will be kept light weight. + +--- + + +My initial recommendation would be: + +* **Service-oriented architecture** +* **Strong data models** +* **Central system state** +* **Event-based communication where useful** +* **Hardware abstraction boundaries** +* **Separate firmware projects with shared libraries** + +The next thing I would design is the **communication protocol between the Hub and sensor nodes**, because that decision will influence almost every other piece. diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..374bf22 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,20 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +;lib_deps = + + +; upload via OTA +upload_protocol = espota +upload_port = 192.168.4.1 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..1ec7b70 --- /dev/null +++ b/readme.md @@ -0,0 +1,10 @@ +# Hub board + +Responsibilities: + + - Host the network + - Round Robin collection of audio data from nodes + - Simple user interface via web + - Fast writing to an SD card using SDIO + +board: esp32dev diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..27915f1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pio +platformio diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..20af32b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,36 @@ +// Nathan Hinton +// Main file for the hub node. Starting with the WIFI initialization. + + +#include + +#include "services/service_manager.h" +#include "services/wifi_service.h" +#include "services/web_service.h" +#include "services/ota_service.h" + + +ServiceManager services; + + +WiFiService wifi; +WebService web; +OTAService ota; + + +void setup() +{ + Serial.begin(115200); + + services.add(&wifi); + services.add(&ota); + services.add(&web); + + services.begin(); +} + + +void loop() +{ + services.update(); +} diff --git a/src/services/ota_service.cpp b/src/services/ota_service.cpp new file mode 100644 index 0000000..fab11d4 --- /dev/null +++ b/src/services/ota_service.cpp @@ -0,0 +1,25 @@ +#include "ota_service.h" + + +void OTAService::begin() +{ + ArduinoOTA.setHostname("ESP32-Dashboard"); + + ArduinoOTA.onStart([](){ + Serial.println("OTA started"); + }); + + ArduinoOTA.onEnd([](){ + Serial.println("OTA finished"); + }); + + ArduinoOTA.begin(); + + Serial.println("OTA ready"); +} + + +void OTAService::update() +{ + ArduinoOTA.handle(); +} diff --git a/src/services/ota_service.h b/src/services/ota_service.h new file mode 100644 index 0000000..79bb15e --- /dev/null +++ b/src/services/ota_service.h @@ -0,0 +1,16 @@ +#pragma once + +#include "service.h" +#include + +class OTAService : public Service { + +public: + + OTAService() + : Service("OTA", 10) + {} + + void begin() override; + void update() override; +}; diff --git a/src/services/service.h b/src/services/service.h new file mode 100644 index 0000000..3bc5107 --- /dev/null +++ b/src/services/service.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +class Service { +public: + Service(const char* name, uint32_t intervalMs) + : name(name), + intervalMs(intervalMs), + lastRun(0), + lastExecutionUs(0) + {} + + virtual void begin() = 0; + virtual void update() = 0; + + void run() + { + uint32_t now = millis(); + + if (now - lastRun < intervalMs) + return; + + lastRun = now; + + uint32_t start = micros(); + + update(); + + lastExecutionUs = micros() - start; + } + + const char* getName() { + return name; + } + + uint32_t getExecutionTimeUs() { + return lastExecutionUs; + } + +protected: + const char* name; + uint32_t intervalMs; + uint32_t lastRun; + uint32_t lastExecutionUs; +}; diff --git a/src/services/service_manager.cpp b/src/services/service_manager.cpp new file mode 100644 index 0000000..4c68951 --- /dev/null +++ b/src/services/service_manager.cpp @@ -0,0 +1,27 @@ +#include "service_manager.h" + +void ServiceManager::add(Service* service) +{ + if(count < MAX_SERVICES) + { + services[count++] = service; + } +} + + +void ServiceManager::begin() +{ + for(uint8_t i = 0; i < count; i++) + { + services[i]->begin(); + } +} + + +void ServiceManager::update() +{ + for(uint8_t i = 0; i < count; i++) + { + services[i]->run(); + } +} diff --git a/src/services/service_manager.h b/src/services/service_manager.h new file mode 100644 index 0000000..24409ff --- /dev/null +++ b/src/services/service_manager.h @@ -0,0 +1,20 @@ +#pragma once + +#include "service.h" + +#define MAX_SERVICES 10 + +class ServiceManager { +public: + + void add(Service* service); + + void begin(); + + void update(); + +private: + + Service* services[MAX_SERVICES]; + uint8_t count = 0; +}; diff --git a/src/services/web_service.cpp b/src/services/web_service.cpp new file mode 100644 index 0000000..493d397 --- /dev/null +++ b/src/services/web_service.cpp @@ -0,0 +1,161 @@ +#include "web_service.h" + +#define FIRMWARE_VERSION "1.0.0" + +WebService::WebService() + : + Service("Web", 10), + server(80) +{ +} + + +String formatUptime() { + unsigned long seconds = millis() / 1000; + + unsigned long days = seconds / 86400; + seconds %= 86400; + + unsigned long hours = seconds / 3600; + seconds %= 3600; + + unsigned long minutes = seconds / 60; + seconds %= 60; + + char buffer[64]; + snprintf(buffer, sizeof(buffer), + "%lu days %02lu:%02lu:%02lu", + days, hours, minutes, seconds); + + return String(buffer); +} + +String ROOT_HTML = R"rawliteral( + + + + +ESP32 Dashboard + + + + + + + + + + + +
+

ESP32 Dashboard OTA

+

Device uptime:

+
Loading...
+
+
+
Loading...
+
+ + +)rawliteral"; + + +// server.send(200, "text/html", html); +// } +// +// void handleUptime() { +// server.send(200, "text/plain", formatUptime()); +// } +// +// void handleVersion() { +// server.send(200, "text/plain", FIRMWARE_VERSION); +// } +// + +void WebService::begin() { + // Root path + server.on("/", [this](){ + server.send( + 200, + "text/html", + ROOT_HTML + ); + }); + + // Version path + server.on("/version", [this](){ + server.send( + 200, + "text/plain", + FIRMWARE_VERSION + ); + }); + + // Uptime path + server.on("/uptime", [this]() { + server.send( + 200, + "text/plain", + formatUptime() + ); + + }); + + + server.begin(); + + Serial.println("Web server started"); +} + + +void WebService::update() +{ + server.handleClient(); +} diff --git a/src/services/web_service.h b/src/services/web_service.h new file mode 100644 index 0000000..1516860 --- /dev/null +++ b/src/services/web_service.h @@ -0,0 +1,19 @@ +#pragma once + +#include "service.h" +#include + + +class WebService : public Service { + +public: + + WebService(); + + void begin() override; + void update() override; + +private: + + WebServer server; +}; diff --git a/src/services/wifi_service.cpp b/src/services/wifi_service.cpp new file mode 100644 index 0000000..b0e70a3 --- /dev/null +++ b/src/services/wifi_service.cpp @@ -0,0 +1,39 @@ +#include "wifi_service.h" + +#include + + +const char* ssid = "ESP32-Dashboard"; +const char* password = "esp32password"; + + +WiFiService::WiFiService() +: +Service("WiFi", 1000) +{ + +} + + +void WiFiService::begin() +{ + WiFi.mode(WIFI_AP); + + WiFi.softAP( + ssid, + password + ); + + + Serial.print("IP: "); + Serial.println(WiFi.softAPIP()); +} + + +void WiFiService::update() +{ + // Future: + // monitor connected clients + // reconnect logic + // diagnostics +} diff --git a/src/services/wifi_service.h b/src/services/wifi_service.h new file mode 100644 index 0000000..c2318ea --- /dev/null +++ b/src/services/wifi_service.h @@ -0,0 +1,15 @@ +#pragma once + +#include "service.h" + + +class WiFiService : public Service { + +public: + + WiFiService(); + + void begin() override; + void update() override; + +}; diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html