diff --git a/doc/ESP32_eink.webp b/doc/ESP32_eink.webp new file mode 100644 index 0000000..5b39b31 Binary files /dev/null and b/doc/ESP32_eink.webp differ diff --git a/doc/board_pins.webp b/doc/board_pins.webp new file mode 100644 index 0000000..8f503df Binary files /dev/null and b/doc/board_pins.webp differ diff --git a/sender/platformio.ini b/sender/platformio.ini index bcee878..0f50a70 100644 --- a/sender/platformio.ini +++ b/sender/platformio.ini @@ -13,6 +13,11 @@ platform = espressif32 board = ttgo-lora32-v1 framework = arduino monitor_speed = 115200 +build_flags = + -D DEBUG lib_deps = - thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays@^4.5.0 + zinggjm/GxEPD2@^1.5.6 + bblanchon/ArduinoJson@^7.0.4 + peterus/esp-logger@^1.0.0 sandeepmistry/LoRa@^0.8.0 + olikraus/U8g2_for_Adafruit_GFX@^1.8.0 diff --git a/sender/src/config.cpp b/sender/src/config.cpp new file mode 100644 index 0000000..4d96cb2 --- /dev/null +++ b/sender/src/config.cpp @@ -0,0 +1,113 @@ +#include +#include +#include "config.hpp" +#include "logger.h" + +extern logging::Logger logger; + +Config::Config() +{ + _filePath = "/config.json"; + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "CONFIG", "Init config"); + if (!SPIFFS.begin(false)) + { + logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "CONFIG", "SPIFFS Mount Failed"); + return; + } + readFile(SPIFFS, _filePath.c_str()); +} + +void Config::readFile(fs::FS &fs, const char *fileName) +{ + JsonDocument data; + File configFile = fs.open(fileName, "r"); + DeserializationError error = deserializeJson(data, configFile); + if (error) + { + logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "CONFIG", "Failed to read file, using default configuration"); + } + + LoraConfig lora; + lora.frequency = data["lora"]["frequency"].as(); + lora.spreadingFactor = data["lora"]["spreadingFactor"].as(); + lora.signalBandwidth = data["lora"]["signalBandwidth"].as(); + lora.codingRate4 = data["lora"]["codingRate4"].as(); + lora.power = data["lora"]["power"].as(); + logConfigInfo(data); + configFile.close(); + + loraConfig = lora; + + isConfigLoaded = true; + +} + +void Config::writeData() +{ + JsonDocument doc; + + JsonObject lora = doc["lora"].to(); + lora["frequency"] = 433775000; + lora["spreadingFactor"] = 12; + lora["signalBandwidth"] = 125000; + lora["codingRate4"] = 5; + lora["power"] = 20; + + // Delete existing file, otherwise the configuration is appended to the file + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "CONFIG", "Write DATA to file"); + + SPIFFS.remove(_filePath); + File configFile = SPIFFS.open(_filePath.c_str(), "w"); + if (!configFile) + { + logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "CONFIG", "Failed to open file for writing"); + return; + } + + // Serialize the JSON document to the file + if (serializeJson(doc, configFile) == 0) + { + logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "CONFIG", "Failed to write to file"); + } + + logConfigInfo(doc); + // Close the file + configFile.close(); +} + +void Config::logConfigInfo(JsonDocument& doc) { + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "CONFIG", "Logging configuration data:"); + + JsonObject rootJson = doc.as(); + + // Iterate over each key-value pair at the root level + for (auto kvp : rootJson) { + String key = kvp.key().c_str(); + String value; + + // Get the value associated with the key + const JsonVariant& jsonValue = kvp.value(); + + // Check the type of the JSON value and convert it to a string accordingly + if (jsonValue.is()) { + value = jsonValue.as(); + } else if (jsonValue.is()) { + value = String(jsonValue.as()); + } else if (jsonValue.is()) { + value = String(jsonValue.as()); + } else if (jsonValue.is()) { + value = jsonValue.as() ? "true" : "false"; + } else if (jsonValue.is() || jsonValue.is()) { + JsonDocument tempJson; + tempJson.set(jsonValue); + serializeJson(tempJson, value); + } else if (jsonValue.isNull()) { + value = "null"; + } else { + value = "Unsupported data type"; + } + + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "CONFIG", (key + ": " + value).c_str()); + } +} + diff --git a/sender/src/config.hpp b/sender/src/config.hpp new file mode 100644 index 0000000..67fd770 --- /dev/null +++ b/sender/src/config.hpp @@ -0,0 +1,31 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include + + +class LoraConfig { +public: + long frequency; + int spreadingFactor; + long signalBandwidth; + int codingRate4; + int power; +}; + +class Config { + public: + bool isConfigLoaded = false; + LoraConfig loraConfig; + Config(); + void writeData(); + + void logConfigInfo(JsonDocument& configJson); + private: + void readFile(fs::FS &fs, const char *fileName); + String _filePath; +}; + +#endif /* CONFIG_H */ \ No newline at end of file diff --git a/sender/src/eink.cpp b/sender/src/eink.cpp new file mode 100644 index 0000000..71e4301 --- /dev/null +++ b/sender/src/eink.cpp @@ -0,0 +1,144 @@ +#include +#include +#include "eink.hpp" +#include "logger.h" + +extern logging::Logger logger; + +Eink::Eink(int csPin, int dcPin, int rstPin, int busyPin) + : eink(GxEPD2_213_BN(csPin, dcPin, rstPin, busyPin)), u8g2Fonts() +{ +} + +void Eink::setup_eink() { + + SPI.begin(EPD_SCLK, EPD_MISO, EPD_MOSI); + eink.init(115200, true, 2, false); + eink.setRotation(1); + eink.fillScreen(GxEPD_WHITE); + eink.setTextColor(GxEPD_BLACK); + eink.setFullWindow(); + u8g2Fonts.begin(eink); + u8g2Fonts.setFont(u8g2_font_7x13_tf); + u8g2Fonts.setForegroundColor(GxEPD_BLACK); // apply Adafruit GFX color + u8g2Fonts.setBackgroundColor(GxEPD_WHITE); + + + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "Display init done!"); +} + +void Eink::show_display(String header, int wait){ + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "One line: %s", header.c_str()); + eink.setFont(&FreeMonoBold9pt7b); + eink.setCursor(0,10); + eink.println(header); + eink.println("Line 2"); + eink.println("Line 3"); + eink.println("Line 4"); + eink.display(); + delay(wait); + + int16_t x1, y1; + uint16_t w, h; + eink.getTextBounds(header, 0, 0, &x1, &y1, &w, &h); +} + + +void Eink::show_temp(float temperature){ + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "Show temp: %f", temperature); + + int textX = 40; + u8g2Fonts.setFont(u8g2_font_fur42_tf ); + + drawString(textX, 70, String(temperature, 1) + "°", LEFT); + drawMercury(temperature); +} +void Eink::drawSignalBars(int x, int y, int percentage) { + // Define stapler properties + const int staplerWidth = 4; + const int staplerHeight = 14; + const int staplerSpacing = 2; + int numBars = percentage / 20; + + // Limit numBars to the maximum (5 staplers) + numBars = min(numBars, 5); // Ensure no more than 5 staplers are drawn + int staplerX; + int currentHeight; + int staplerY; + // Loop to draw staplers with variable heights and bottom alignment + for (int i = 0; i < numBars; i++) { + // Calculate x-position for each stapler + staplerX = x + i * (staplerWidth + staplerSpacing); + + // Calculate stapler height based on the number of bars (1 to 5) + currentHeight = staplerHeight * (i + 1) / 6; // Scales from 1/5 to full height + + // Calculate y-position for bottom alignment + staplerY = y - currentHeight; // Subtract height for bottom placement + // Draw the stapler body (rectangle) + eink.fillRect(staplerX, staplerY, staplerWidth, currentHeight, GxEPD_BLACK); + } + + drawString(staplerX +staplerWidth +2, staplerY , String(percentage) + "%", LEFT); +} + +void Eink::display(bool partialupgrade) +{ + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "Update display"); + + eink.display(partialupgrade); +} + +void Eink::drawMercury(float temperature){ + int x = 20; + int y = 30; + int height = 60; + int width = 10; + int mercuryLevel = map(temperature, 0.0, 100.0, 20, height); + int radius = 14; + eink.drawRect(x, y - radius + 2, width, height,GxEPD_BLACK ); + + eink.drawCircle(x + (width/2) , y + height, radius, GxEPD_BLACK); + eink.fillCircle(x + (width/2), y + height, radius - 2, GxEPD_BLACK); + + eink.fillRect( + x + 2, + y + height - mercuryLevel - radius + 2, + width - 4, + mercuryLevel, + GxEPD_BLACK + ); + +} + +void Eink::drawBattery(int x, int y) { + + uint8_t percentage = 100; + float voltage = analogRead(35) / 4096.0 * 7.46; + if (voltage > 1 ) { // Only display if there is a valid reading + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "Voltage: %d", voltage); + percentage = 2836.9625 * pow(voltage, 4) - 43987.4889 * pow(voltage, 3) + 255233.8134 * pow(voltage, 2) - 656689.7123 * voltage + 632041.7303; + if (voltage >= 4.20) percentage = 100; + if (voltage <= 3.50) percentage = 0; + eink.drawRect(x + 15, y - 12, 19, 10, GxEPD_BLACK); + eink.fillRect(x + 34, y - 10, 2, 5, GxEPD_BLACK); + eink.fillRect(x + 17, y - 10, 15 * percentage / 100.0, 6, GxEPD_BLACK); + drawString(x + 60, y - 11, String(percentage) + "%", RIGHT); + } +} + +void Eink::drawString(int x, int y, String text, alignmentType alignment) { + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK drawstring", "x: %d", y); + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK drawstring", "y: %d", x); + + + int16_t x1, y1; //the bounds of x,y and w and h of the variable 'text' in pixels. + uint16_t w, h; + eink.setTextWrap(false); + eink.getTextBounds(text, x, y, &x1, &y1, &w, &h); + if (alignment == RIGHT) x = x - w; + if (alignment == CENTER) x = x - w / 2; + u8g2Fonts.setCursor(x+2, y + h); + u8g2Fonts.print(text); + +} \ No newline at end of file diff --git a/sender/src/eink.hpp b/sender/src/eink.hpp new file mode 100644 index 0000000..d391fab --- /dev/null +++ b/sender/src/eink.hpp @@ -0,0 +1,44 @@ +#ifndef EINK_H +#define EINK_H +#include +#include +#include "epd/GxEPD2_213.h" +#include + + + + +#define EPD_MOSI (23) +#define EPD_MISO (-1) //elink no use +#define EPD_SCLK (18) + +#define EPD_BUSY (4) +#define EPD_RSET (16) +#define EPD_DC (17) +#define EPD_CS (5) + +class Eink { +public: + Eink(int csPin = EPD_CS, int dcPin = EPD_DC, int rstPin = EPD_RSET, int busyPin = EPD_BUSY); + + void setup_eink(); + void show_temp(float temperature); + void show_display(String header, int wait=100); + void drawBattery(int x, int y); + void drawSignalBars(int x, int y, int numBars); + void display(bool partialupgrade = false); + +private: + U8G2_FOR_ADAFRUIT_GFX u8g2Fonts; + enum alignmentType {LEFT, RIGHT, CENTER}; + + void drawMercury(float temperature); + void drawString(int x, int y, String text, alignmentType alignment); + void drawCircleSegment(int x0, int y0, int radius, int startAngle, int endAngle); + // GxEPD2_213_BN epd; + GxEPD2_BW eink; + +}; + + +#endif /* EINK_H */ \ No newline at end of file diff --git a/sender/src/lorahandler.cpp b/sender/src/lorahandler.cpp new file mode 100644 index 0000000..476b5b9 --- /dev/null +++ b/sender/src/lorahandler.cpp @@ -0,0 +1,54 @@ +#include "config.hpp" +#include "lorahandler.hpp" +#include "logger.h" + +extern logging::Logger logger; +extern Config config; + +void LoraHandler::setup() +{ + + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", "Set SPI pins!"); + SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ); + + long freq = config.loraConfig.frequency; + if (!LoRa.begin(freq)) + { + logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "Starting LoRa failed!"); + // show_display("ERROR", "Starting LoRa failed!"); + while (true) + { + delay(1000); + } + } + LoRa.setSpreadingFactor(config.loraConfig.spreadingFactor); + LoRa.setSignalBandwidth(config.loraConfig.signalBandwidth); + LoRa.setCodingRate4(config.loraConfig.codingRate4); + LoRa.enableCrc(); + LoRa.setTxPower(config.loraConfig.power); + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", "LoRa init done!"); + String currentLoRainfo = "LoRa Freq: " + String(config.loraConfig.frequency) + " / SF:" + String(config.loraConfig.spreadingFactor) + " / CR: " + String(config.loraConfig.codingRate4); + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", currentLoRainfo.c_str()); +} + +ReceivedLoRaPacket LoraHandler::receivePacket() +{ + ReceivedLoRaPacket receivedLoraPacket; + String packet = ""; + int packetSize = LoRa.parsePacket(); + if (packetSize) + { + while (LoRa.available()) + { + int inChar = LoRa.read(); + packet += (char)inChar; + } + receivedLoraPacket.text = packet; + receivedLoraPacket.rssi = LoRa.packetRssi(); + receivedLoraPacket.snr = LoRa.packetSnr(); + receivedLoraPacket.freqError = LoRa.packetFrequencyError(); + logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa Rx", "---> %s", packet.c_str()); + } + return receivedLoraPacket; +} \ No newline at end of file diff --git a/sender/src/lorahandler.hpp b/sender/src/lorahandler.hpp new file mode 100644 index 0000000..8b5a1a3 --- /dev/null +++ b/sender/src/lorahandler.hpp @@ -0,0 +1,29 @@ +#ifndef LORAHANDLER_H +#define LORAHANDLER_H + +#include "config.hpp" +#include +#include + +#define LORA_SCK 5 +#define LORA_MISO 19 +#define LORA_MOSI 27 +#define LORA_CS 18 // CS --> NSS +#define LORA_RST 14 +#define LORA_IRQ 26 // IRQ --> DIO0 + +struct ReceivedLoRaPacket { + String text; + int rssi; + float snr; + int freqError; +}; + +class LoraHandler { + public: + void setup(); + ReceivedLoRaPacket receivePacket(); + private: +}; + +#endif /* LORAHANDLER_H */ \ No newline at end of file diff --git a/sender/src/main.cpp b/sender/src/main.cpp index 4a8f8d5..53a2b20 100644 --- a/sender/src/main.cpp +++ b/sender/src/main.cpp @@ -1,129 +1,30 @@ -#include "SSD1306.h" // alias for `#include "SSD1306Wire.h"` -#include -#include -#include -// #include "SSD1306.h" -#include +#include +#include "config.hpp" +#include "eink.hpp" + +logging::Logger logger; +Eink eink; -//OLED pins to ESP32 GPIOs via this connecthin: -//OLED_SDA GPIO4 -//OLED_SCL GPIO15 -//OLED_RST GPIO16 +void setup() +{ + Serial.begin(115200); -SSD1306 display(0x3c, 4, 15); + #ifndef DEBUG + logger.setDebugLevel(logging::LoggerLevel::LOGGER_LEVEL_INFO); + #endif + delay(100); - // WIFI_LoRa_32 ports -// GPIO5 SX1278 SCK -// GPIO19 SX1278 MISO -// GPIO27 SX1278 MOSI -// GPIO18 SX1278 CS -// GPIO14 SX1278 RESET -// GPIO26 SX1278 IRQ(Interrupt Request) - -#define SS 18 -#define RST 14 -#define DI0 26 -// #define BAND 429E6 //915E6 - -// #define BAND 434500000.00 -#define BAND 434500000.00 - -#define spreadingFactor 9 -// #define SignalBandwidth 62.5E3 -#define SignalBandwidth 31.25E3 -#define preambleLength 8 -#define codingRateDenominator 8 - - - - - -int counter = 0; - -void setup() { - pinMode(25,OUTPUT); //Send success, LED will bright 1 second - - pinMode(16,OUTPUT); - digitalWrite(16, LOW); // set GPIO16 low to reset OLED - delay(50); - digitalWrite(16, HIGH); - - Serial.begin(115200); - while (!Serial); //If just the the basic function, must connect to a computer - -// Initialising the UI will init the display too. - display.init(); - display.flipScreenVertically(); - display.setFont(ArialMT_Plain_10); - display.setTextAlignment(TEXT_ALIGN_LEFT); - display.drawString(5,5,"LoRa Sender"); - display.display(); - - SPI.begin(5,19,27,18); - LoRa.setPins(SS,RST,DI0); - Serial.println("LoRa Sender"); - if (!LoRa.begin(BAND)) { - Serial.println("Starting LoRa failed!"); - while (1); - } - - Serial.print("LoRa Spreading Factor: "); - Serial.println(spreadingFactor); - LoRa.setSpreadingFactor(spreadingFactor); - - Serial.print("LoRa Signal Bandwidth: "); - Serial.println(SignalBandwidth); - LoRa.setSignalBandwidth(SignalBandwidth); - - LoRa.setCodingRate4(codingRateDenominator); - - LoRa.setPreambleLength(preambleLength); - - Serial.println("LoRa Initial OK!"); - display.drawString(5,20,"LoRa Initializing OK!"); - display.display(); - delay(2000); + eink.setup_eink(); + delay(100); + + eink.drawBattery(186, 14); // eink width 250 - drawbattery width + eink.drawSignalBars(180 - 24 - 4, 12, 84); // drawbattery width - drawsignal + eink.show_temp(30.0); + eink.display(); } -void loop() { - Serial.print("Sending packet: "); - Serial.println(counter); - - display.clear(); - display.setFont(ArialMT_Plain_16); - display.drawString(3, 5, "Sending packet "); - display.drawString(50, 30, String(counter)); - display.display(); - - // send packet - LoRa.beginPacket(); - LoRa.print("Hello.."); - LoRa.print(counter); - LoRa.endPacket(); - - counter++; - digitalWrite(25, HIGH); // turn the LED on (HIGH is the voltage level) - delay(1000); // wait for a second - digitalWrite(25, LOW); // turn the LED off by making the voltage LOW - delay(1000); // wait for a second - -// delay(3000); +void loop() +{ } - -/* Calc battery: - float voltage = analogRead(35) / 4096.0 * 7.46; - uint8_t percentage = 100; - if (voltage > 1) { - // Only display if there is a valid reading - Serial.println("Voltage = " + String(voltage)); - percentage = 2836.9625 * pow(voltage, 4) - 43987.4889 * pow(voltage, 3) + 255233.8134 * pow(voltage, 2) - 656689.7123 * voltage + 632041.7303; - if (voltage >= 4.20) percentage = 100; - if (voltage <= 3.50) percentage = 0; - Serial.println("Percentage = " + String(percentage)); - } - - - -*/ \ No newline at end of file