Compare commits

..

No commits in common. "master" and "wifimanager" have entirely different histories.

50 changed files with 348 additions and 16063 deletions

View File

@ -1,34 +0,0 @@
FROM python:3.14.5-slim-bookworm
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=1000
ENV PLATFORMIO_CORE_DIR=/home/${USERNAME}/.platformio
ENV PATH="${PLATFORMIO_CORE_DIR}/penv/bin:${PATH}"
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
build-essential \
ca-certificates \
udev \
openssh-client \
sudo \
&& rm -rf /var/lib/apt/lists/*
RUN python -m pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir platformio
RUN groupadd --gid ${USER_GID} ${USERNAME} \
&& useradd --uid ${USER_UID} --gid ${USER_GID} --create-home --shell /bin/bash ${USERNAME} \
&& usermod --append --groups dialout ${USERNAME} \
&& mkdir -p /workspace ${PLATFORMIO_CORE_DIR} \
&& chown -R ${USERNAME}:${USERNAME} /workspace /home/${USERNAME}
RUN echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} && \
chmod 0440 /etc/sudoers.d/${USERNAME}
USER ${USERNAME}
WORKDIR /workspace
CMD ["pio", "run"]

View File

@ -1,19 +0,0 @@
{
"name": "Water Temp Dev Container",
"build": {
"dockerfile": "./Dockerfile"
},
"remoteUser": "vscode",
"updateRemoteUserUID": true,
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly"
],
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"platformio.platformio-ide"
]
}
}
}

1
.gitignore vendored
View File

@ -36,4 +36,3 @@
*.exe
*.out
*.app
schematic/water_temp/water_temp-backups/*

View File

@ -1,20 +0,0 @@
#ifndef LORA_CONFIG_H
#define LORA_CONFIG_H
struct LoraConfig {
float frequency;
int spreadingFactor;
float signalBandwidth;
int codingRate4;
int power;
};
namespace LoraDefaults {
constexpr float frequency = 433.775f;
constexpr int spreadingFactor = 12;
constexpr float signalBandwidth = 125.0f;
constexpr int codingRate4 = 5;
constexpr int power = 20;
}
#endif /* LORA_CONFIG_H */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 KiB

5
reciver/.gitignore vendored
View File

@ -1,5 +0,0 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

Binary file not shown.

Before

Width:  |  Height:  |  Size: 717 B

View File

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>ESP WEB SERVER</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
</head>
<body>
<div class="topnav">
<h1>ESP WEB SERVER</h1>
</div>
<div class="content">
<div class="card-grid">
<div class="card">
<p class="card-title"><i class="fas fa-lightbulb"></i> GPIO 2</p>
<p>
<a href="on"><button class="button-on">ON</button></a>
<a href="off"><button class="button-off">OFF</button></a>
</p>
<p class="state">State: %STATE%</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,109 +0,0 @@
html {
font-family: Arial, Helvetica, sans-serif;
display: inline-block;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
p {
font-size: 1.4rem;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
}
body {
margin: 0;
}
.content {
padding: 5%;
}
.card-grid {
max-width: 800px;
margin: 0 auto;
display: grid;
grid-gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.card {
background-color: white;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
font-size: 1.2rem;
font-weight: bold;
color: #034078
}
input[type=submit] {
border: none;
color: #FEFCFB;
background-color: #034078;
padding: 15px 15px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
width: 100px;
margin-right: 10px;
border-radius: 4px;
transition-duration: 0.4s;
}
input[type=submit]:hover {
background-color: #1282A2;
}
input[type=text], input[type=number], select {
width: 50%;
padding: 12px 20px;
margin: 18px;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
label {
font-size: 1.2rem;
}
.value{
font-size: 1.2rem;
color: #1282A2;
}
.state {
font-size: 1.2rem;
color: #1282A2;
}
button {
border: none;
color: #FEFCFB;
padding: 15px 32px;
text-align: center;
font-size: 16px;
width: 100px;
border-radius: 4px;
transition-duration: 0.4s;
}
.button-on {
background-color: #034078;
}
.button-on:hover {
background-color: #1282A2;
}
.button-off {
background-color: #858585;
}
.button-off:hover {
background-color: #252524;
}

View File

@ -15,13 +15,10 @@ framework = arduino
monitor_speed = 115200
build_flags =
-D DEBUG
-I../common/include
-D MOCK_LORA
-D MQTT_MAX_PACKET_SIZE=1024
lib_deps =
https://github.com/tzapu/WiFiManager.git
bblanchon/ArduinoJson@^7.0.4
peterus/esp-logger@^1.0.0
knolleary/PubSubClient@^2.8
jgromes/RadioLib@^6.0.0
sandeepmistry/LoRa@^0.8.0
thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays@^4.5.0

View File

@ -35,9 +35,9 @@ void Config::readFile(fs::FS &fs, const char *fileName)
mqtt.id = data["mqtt"]["id"].as<String>();
LoraConfig lora;
lora.frequency = data["lora"]["frequency"].as<float>();
lora.frequency = data["lora"]["frequency"].as<long>();
lora.spreadingFactor = data["lora"]["spreadingFactor"].as<int>();
lora.signalBandwidth = data["lora"]["signalBandwidth"].as<float>();
lora.signalBandwidth = data["lora"]["signalBandwidth"].as<long>();
lora.codingRate4 = data["lora"]["codingRate4"].as<int>();
lora.power = data["lora"]["power"].as<int>();
logConfigInfo(data);
@ -63,11 +63,11 @@ void Config::writeData()
mqttJson["topic"] = this->mqttConfig.topic;
JsonObject lora = doc["lora"].to<JsonObject>();
lora["frequency"] = LoraDefaults::frequency;
lora["spreadingFactor"] = LoraDefaults::spreadingFactor;
lora["signalBandwidth"] = LoraDefaults::signalBandwidth;
lora["codingRate4"] = LoraDefaults::codingRate4;
lora["power"] = LoraDefaults::power;
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");
@ -118,8 +118,6 @@ void Config::logConfigInfo(JsonDocument& doc) {
value = String(jsonValue.as<int>());
} else if (jsonValue.is<long>()) {
value = String(jsonValue.as<long>());
} else if (jsonValue.is<float>()) {
value = String(jsonValue.as<float>(), 3);
} else if (jsonValue.is<bool>()) {
value = jsonValue.as<bool>() ? "true" : "false";
} else if (jsonValue.is<JsonArray>() || jsonValue.is<JsonObject>()) {

View File

@ -4,7 +4,6 @@
#include <Arduino.h>
#include <FS.h>
#include <ArduinoJson.h>
#include "lora_config.hpp"
class MqttConfig {
public:
@ -18,6 +17,14 @@ class MqttConfig {
void setMqtt(const String& server, const int& port, const String& username, const String& password, const String& topic);
};
class LoraConfig {
public:
long frequency;
int spreadingFactor;
long signalBandwidth;
int codingRate4;
int power;
};
class Config {
public:

View File

@ -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;
}

View File

@ -2,7 +2,8 @@
#define LORAHANDLER_H
#include "config.hpp"
#include <RadioLib.h>
#include <SPI.h>
#include <LoRa.h>
#define LORA_SCK 5
#define LORA_MISO 19
@ -20,13 +21,9 @@ struct ReceivedLoRaPacket {
class LoraHandler {
public:
LoraHandler();
void setup();
ReceivedLoRaPacket receivePacket();
private:
SX1278* radio;
SPIClass* spi;
int mockCounter = 0;
};
#endif /* LORAHANDLER_H */

View File

@ -6,7 +6,6 @@
#include "lorahandler.hpp"
#include "display.hpp"
#include <WiFi.h>
#include <ArduinoJson.h>
Config config;
WiFiServer server(80);
@ -51,55 +50,41 @@ void setup()
void loop()
{
ReceivedLoRaPacket packet = lora.receivePacket();
if (!packet.text.isEmpty())
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Processing LoRa packet: %s", packet.text.c_str());
JsonDocument doc;
DeserializationError error = deserializeJson(doc, packet.text);
if (!error) {
float temp = doc["temp"] | 0.0;
int battery = doc["battery"] | 0;
int boot = doc["boot"] | 0;
int update = doc["update"] | 0;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Parsed - Temp: %.2f, Battery: %d, Boot: %d, Update: %d", temp, battery, boot, update);
if (WiFi.status() == WL_CONNECTED) {
mqtt.publishSensorData(temp, packet.rssi, battery);
}
ReceivedLoRaPacket packet = lora.receivePacket();
if (packet.text.isEmpty())
{
packet.rssi = 69;
packet.snr = 3.3;
}
if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP();
String line0 = "IP: " + ip.toString();
display.show_message(line0.c_str(), 0);
} else {
display.show_message("WiFi not connected", 0);
}
String line1 = "LoRa RSSI: " + String(packet.rssi) + " dBm";
display.show_message(line1.c_str(), 1);
String line2 = "SNR: " + String(packet.snr, 1) + " dB";
display.show_message(line2.c_str(), 2);
display.show_message("Data received OK", 3);
} else {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "MAIN", "Failed to parse JSON: %s", error.c_str());
}
if (WiFi.status() == WL_CONNECTED) { // Check WiFi connection
IPAddress ip = WiFi.localIP();
String line0 = "IP: " + ip.toString();
display.show_message(line0.c_str(), 0);
} else {
// Optional: Display message indicating WiFi not connected
display.show_message("WiFi not connected", 0);
}
// Kör MQTT-klientens loop
mqtt.mqttRun();
int mqtt_pkt = 123455;
String line1 = "Mqtt pkt: " + String(mqtt_pkt);
display.show_message(line1.c_str(), 1);
// FIX: Ändra från 100ms till 1000ms (1 sekund).
// Detta förhindrar att ESP32:an kraschar/tappar WiFi av logg-spamming,
// men är tillräckligt snabbt för att inte missa LoRa-paket.
delay(1000);
String line2 = "Last lora, rssi: " + String(packet.rssi ) ;
display.show_message(line2.c_str(), 2);
display.show_message("Fourth", 3);
display.show_message("Fifth", 4);
sleep(5);
/*
mqtt.mqttRun();
if (portalRunning)
{
wm.process();
}
mqtt.mqttPublish("test/sensor", "123");
checkButton();
sleep(10);*/
}
void checkButton()
{
// check for button press

View File

@ -0,0 +1,123 @@
#include "mqtt.hpp"
#include "config.hpp"
#include <PubSubClient.h>
#include "logger.h"
extern logging::Logger logger;
extern Config config;
extern Config config;
Mqtt::Mqtt(WiFiClient& wifiClient) : mqttClient(wifiClient) {}
void Mqtt::mqttSetup()
{
String message = "MqttServer: " + config.mqttConfig.server + ", MqttPort: " + config.mqttConfig.port;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", message.c_str());
if (config.mqttConfig.server.length() > 0)
{
// strcpy(mqttHost, config.mqtt.server.c_str());
String message = "MqttServer: " + config.mqttConfig.server + ", MqttPort: " + config.mqttConfig.port;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", message.c_str());
IPAddress mqttServerIP;
if(mqttServerIP.fromString(config.mqttConfig.server)){
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", mqttServerIP.toString().c_str());
mqttClient.setServer(mqttServerIP, config.mqttConfig.port);
}else{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "MQTT Invalid IP address format %s", config.mqttConfig.server);
}
mqttClient.setCallback(callback);
}
else
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "MQTT config is not set");
}
}
void Mqtt::mqttRun()
{
delay(100);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "MQTT RUN");
if (WiFi.status() == WL_CONNECTED && !mqttClient.loop()) {
mqttReconnect();
}
}
void Mqtt::mqttReconnect()
{
int retryCount = 0;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Attempting MQTT connection...");
// Loop until we're reconnected
while (!mqttClient.connected() && retryCount < 10)
{
// Attempt to connect
String mqttId = config.mqttConfig.id;
if (mqttId.isEmpty())
{
mqttId = "ESP32-";
mqttId += String(random(0xffff), HEX);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Set ID: %s", mqttId );
config.setMqttId(mqttId);
config.writeData();
}
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "Server: %s", config.mqttConfig.server.c_str());
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "Port: %d", config.mqttConfig.port);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "Username: %s", config.mqttConfig.username.c_str());
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "Password: %s", config.mqttConfig.password.c_str());
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "ID: %s", config.mqttConfig.id.c_str());
if (mqttClient.connect(mqttId.c_str(), "simon", "bajsa123"))
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "connected");
String topic = config.mqttConfig.topic;
if (topic.isEmpty())
{
topic = config.mqttConfig.id;
}
mqttClient.subscribe(topic.c_str());
String message = "Subscribe to: " + topic;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", message.c_str());
}
else
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "MQTT", "Not connected to mqtt, try again in 5 s.");
// Wait 5 seconds before retrying
delay(5000);
}
}
// Check if we're connected after the loop
if (!mqttClient.connected()) {
// Connection failed after maximum retries
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to connect to MQTT broker after 10 retries");
}
}
void Mqtt::callback(char *topic, uint8_t *payload, unsigned int length)
// Not implemented yet. Just for debug
{
// Create spot in memory for message
char message[length + 1];
// Write payload to String
strncpy(message, (char *)payload, length);
// Nullify last character to eliminate garbage at end
message[length] = '\0';
// Create correct object
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Received: %s", message);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "From: %s", topic);
}
void Mqtt::mqttPublish(const char* topic, const char* payload){
if (mqttClient.connected()) {
mqttClient.publish(topic, payload);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Published message to %s: %s", topic, payload);
} else {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to publish message. MQTT client not connected.");
}
}

View File

@ -4,7 +4,6 @@
#include "wifi.hpp"
#include "config.hpp"
#include <WiFi.h>
#define MQTT_MAX_PACKET_SIZE 1024
#include <PubSubClient.h>
@ -14,18 +13,10 @@ class Mqtt {
void mqttSetup();
void mqttRun();
void mqttPublish(const char* topic, const char* payload);
void mqttPublishRetained(const char* topic, const char* payload);
void publishTemperature(float temp);
void publishRSSI(int rssi);
void publishSensorData(float temp, int rssi, int battery);
void publishDiscoveryConfig();
private:
void mqttReconnect();
void setupHomeAssistantDiscovery();
static void callback(char *topic, uint8_t *payload, unsigned int length);
PubSubClient mqttClient;
String deviceId;
String baseTopic;
};
#endif /* MQTT_H */

View File

@ -1,147 +0,0 @@
#include "config.hpp"
#include "lorahandler.hpp"
#include "logger.h"
#include <ArduinoJson.h>
extern logging::Logger logger;
extern Config config;
LoraHandler::LoraHandler() {
// Initialize with SX1278 radio
}
void LoraHandler::setup()
{
#ifdef MOCK_LORA
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", "Running in MOCK MODE - simulating LoRa reception!");
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", "Mock data will be generated every 10 calls");
return;
#endif
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", "Initializing SX1278 with RadioLib!");
// Create new SPI instance for LoRa
spi = new SPIClass(VSPI);
spi->begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
// --- FIX APPLIED HERE ---
Module* mod = new Module(LORA_CS, LORA_IRQ, LORA_RST, RADIOLIB_NC, *spi);
radio = new SX1278(mod);
// Initialize LoRa module
int state = radio->begin();
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "LoRa init failed: %i", state);
while (true) {
delay(1000);
}
}
// Configure LoRa parameters
const float freq = config.loraConfig.frequency;
const float bandwidth = config.loraConfig.signalBandwidth;
state = radio->setFrequency(freq);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setFrequency failed: %i", state);
}
state = radio->setSpreadingFactor(config.loraConfig.spreadingFactor);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setSpreadingFactor failed: %i", state);
}
state = radio->setBandwidth(bandwidth);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setBandwidth failed: %i", state);
}
state = radio->setCodingRate(config.loraConfig.codingRate4);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setCodingRate failed: %i", state);
}
state = radio->setOutputPower(config.loraConfig.power);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setOutputPower failed: %i", state);
}
// Enable CRC
radio->setCRC(true);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", "SX1278 initialized successfully!");
String currentLoRainfo = "LoRa Freq: " + String(freq, 3) + " MHz / BW: " + String(bandwidth, 1) + " kHz / 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;
#ifdef MOCK_LORA
// Mock mode - simulate LoRa reception every 10 calls
mockCounter++;
if (mockCounter >= 10)
{
mockCounter = 0;
// Create mock sensor data from sender
float temp = 22.5 + (random(0, 50) / 100.0); // 22.5 - 23.0°C
int battery = 75 + random(0, 20); // 75-95%
int boot = 5;
int update = 1;
// Build JSON payload like sender would send
JsonDocument doc;
doc["temp"] = temp;
doc["battery"] = battery;
doc["boot"] = boot;
doc["update"] = update;
serializeJson(doc, receivedLoraPacket.text);
// Simulate signal parameters
receivedLoraPacket.rssi = -85 + random(0, 30); // -85 to -55 dBm
receivedLoraPacket.snr = 5.0 + (random(0, 100) / 100.0); // 5.0 to 6.0 dB
receivedLoraPacket.freqError = random(-1000, 1000);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa Rx [MOCK]", "---> %s (RSSI: %d, SNR: %.1f)",
receivedLoraPacket.text.c_str(), receivedLoraPacket.rssi, receivedLoraPacket.snr);
}
return receivedLoraPacket;
#else
// Real hardware mode
// Enable RX mode
digitalWrite(LORA_RXEN, HIGH);
digitalWrite(LORA_TXEN, LOW);
int state = radio->receive(0); // 0 = non-blocking
if (state == RADIOLIB_ERR_RX_ONGOING)
{
// No packet received yet
return receivedLoraPacket;
}
else if (state == RADIOLIB_ERR_NONE)
{
// Packet received!
receivedLoraPacket.text = radio->readData();
receivedLoraPacket.rssi = radio->getRSSI();
receivedLoraPacket.snr = radio->getSNR();
receivedLoraPacket.freqError = radio->getFrequencyError();
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa Rx", "---> %s (RSSI: %d, SNR: %.1f)",
receivedLoraPacket.text.c_str(), receivedLoraPacket.rssi, receivedLoraPacket.snr);
}
else
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa Rx", "Receive error: %i", state);
}
return receivedLoraPacket;
#endif
}

View File

@ -1,255 +0,0 @@
#include "mqtt.hpp"
#include "config.hpp"
#include <PubSubClient.h>
#include "logger.h"
#include <ArduinoJson.h>
extern logging::Logger logger;
extern Config config;
Mqtt::Mqtt(WiFiClient& wifiClient) : mqttClient(wifiClient) {
deviceId = "water_temp_receiver";
// FIX 1: Låt bas-topic ligga helt fritt på roten, UTAN "homeassistant/".
// Det är hit din data (temperatur, rssi, batteri) kommer att skickas.
baseTopic = "water_temp_receiver";
}
void Mqtt::mqttSetup()
{
if (config.mqttConfig.server.length() > 0)
{
String message = "MqttServer: " + config.mqttConfig.server + ", MqttPort: " + config.mqttConfig.port;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", message.c_str());
IPAddress mqttServerIP;
if(mqttServerIP.fromString(config.mqttConfig.server)){
mqttClient.setServer(mqttServerIP, config.mqttConfig.port);
}else{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "MQTT Invalid IP address format %s", config.mqttConfig.server);
}
mqttClient.setCallback(callback);
}
else
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "MQTT config is not set");
}
}
void Mqtt::mqttRun()
{
delay(100);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "MQTT RUN");
if (WiFi.status() == WL_CONNECTED && !mqttClient.loop()) {
mqttReconnect();
}
}
void Mqtt::mqttReconnect()
{
int retryCount = 0;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Attempting MQTT connection...");
while (!mqttClient.connected() && retryCount < 10)
{
String mqttId = config.mqttConfig.id;
if (mqttId.isEmpty())
{
mqttId = "ESP32-";
mqttId += String(random(0xffff), HEX);
config.setMqttId(mqttId);
config.writeData();
}
if (mqttClient.connect(mqttId.c_str(), config.mqttConfig.username.c_str(), config.mqttConfig.password.c_str()))
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "connected");
setupHomeAssistantDiscovery();
String topic = config.mqttConfig.topic;
if (topic.isEmpty())
{
topic = config.mqttConfig.id;
}
mqttClient.subscribe(topic.c_str());
}
else
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "MQTT", "Not connected to mqtt, try again in 5 s.");
delay(5000);
}
retryCount++;
}
if (!mqttClient.connected()) {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to connect to MQTT broker after 10 retries");
}
}
void Mqtt::setupHomeAssistantDiscovery()
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Setting up Home Assistant MQTT Discovery using char buffers");
// Skapa fasta teckenbuffertar för ämnen (topics) och JSON-data
char topicBuffer[128];
char jsonBuffer[512]; // 512 bytes räcker gott och väl för denna JSON
// Vi låter alla sensorer lyssna på den gemensamma JSON-strängen på /state
// Bygg upp strängarna med snabba snprintf istället för dynamiska String-objekt
char sharedStateTopic[64];
char availTopic[64];
snprintf(sharedStateTopic, sizeof(sharedStateTopic), "%s/state", baseTopic.c_str());
snprintf(availTopic, sizeof(availTopic), "%s/availability", baseTopic.c_str());
// 1. Temperature sensor discovery
{
JsonDocument doc; // I v7 använder man bara JsonDocument, inte DynamicJsonDocument
doc["name"] = "Water Temperature";
// Bygg unikt ID i en temporär buffert
char uniqueId[64];
snprintf(uniqueId, sizeof(uniqueId), "%s_temperature", deviceId.c_str());
doc["unique_id"] = uniqueId;
doc["unit_of_measurement"] = "°C";
doc["device_class"] = "temperature";
doc["state_class"] = "measurement";
doc["state_topic"] = sharedStateTopic;
doc["value_template"] = "{{ value_json.temperature }}";
doc["availability_topic"] = availTopic;
// I ArduinoJson v7 skapar man nästlade objekt direkt så här:
JsonObject device = doc["device"].to<JsonObject>();
device["identifiers"][0] = deviceId;
device["name"] = "Water Temperature Sensor";
device["model"] = "Water Temp v1";
device["manufacturer"] = "DIY";
// Serialisera direkt till vår fasta char-buffert
serializeJson(doc, jsonBuffer, sizeof(jsonBuffer));
// Bygg config-topic och publicera
snprintf(topicBuffer, sizeof(topicBuffer), "homeassistant/sensor/%s/temperature/config", deviceId.c_str());
mqttPublishRetained(topicBuffer, jsonBuffer);
}
// 2. RSSI sensor discovery
{
JsonDocument doc;
doc["name"] = "LoRa RSSI";
char uniqueId[64];
snprintf(uniqueId, sizeof(uniqueId), "%s_rssi", deviceId.c_str());
doc["unique_id"] = uniqueId;
doc["unit_of_measurement"] = "dBm";
doc["device_class"] = "signal_strength";
doc["state_class"] = "measurement";
doc["state_topic"] = sharedStateTopic;
doc["value_template"] = "{{ value_json.rssi }}";
doc["availability_topic"] = availTopic;
JsonObject device = doc["device"].to<JsonObject>();
device["identifiers"][0] = deviceId;
device["name"] = "Water Temperature Sensor";
serializeJson(doc, jsonBuffer, sizeof(jsonBuffer));
snprintf(topicBuffer, sizeof(topicBuffer), "homeassistant/sensor/%s/rssi/config", deviceId.c_str());
mqttPublishRetained(topicBuffer, jsonBuffer);
}
// 3. Battery level discovery
{
JsonDocument doc;
doc["name"] = "Sender Battery";
char uniqueId[64];
snprintf(uniqueId, sizeof(uniqueId), "%s_battery", deviceId.c_str());
doc["unique_id"] = uniqueId;
doc["unit_of_measurement"] = "%";
doc["device_class"] = "battery";
doc["state_class"] = "measurement";
doc["state_topic"] = sharedStateTopic;
doc["value_template"] = "{{ value_json.battery }}";
doc["availability_topic"] = availTopic;
JsonObject device = doc["device"].to<JsonObject>();
device["identifiers"][0] = deviceId;
device["name"] = "Water Temperature Sensor";
serializeJson(doc, jsonBuffer, sizeof(jsonBuffer));
snprintf(topicBuffer, sizeof(topicBuffer), "homeassistant/sensor/%s/battery/config", deviceId.c_str());
mqttPublishRetained(topicBuffer, jsonBuffer);
}
// Sätt enheten till "online" med retain
mqttPublishRetained(availTopic, "online");
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Home Assistant Discovery setup complete");
}
void Mqtt::publishTemperature(float temp)
{
String topic = baseTopic + "/temperature";
String payload = String(temp, 2);
mqttPublish(topic.c_str(), payload.c_str());
}
void Mqtt::publishRSSI(int rssi)
{
String topic = baseTopic + "/rssi";
String payload = String(rssi);
mqttPublish(topic.c_str(), payload.c_str());
}
void Mqtt::publishSensorData(float temp, int rssi, int battery)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Publishing sensor data - Temp: %.2f, RSSI: %d, Battery: %d", temp, rssi, battery);
publishTemperature(temp);
publishRSSI(rssi);
String topic = baseTopic + "/battery";
String payload = String(battery);
mqttPublish(topic.c_str(), payload.c_str());
// Kombinerad JSON-statustopic
{
JsonDocument doc;
doc["temperature"] = serialized(String(temp, 2));
doc["rssi"] = rssi;
doc["battery"] = battery;
doc["timestamp"] = millis();
String jsonPayload;
serializeJson(doc, jsonPayload);
String jsonTopic = baseTopic + "/state";
mqttPublish(jsonTopic.c_str(), jsonPayload.c_str());
}
}
void Mqtt::callback(char *topic, uint8_t *payload, unsigned int length)
{
char message[length + 1];
strncpy(message, (char *)payload, length);
message[length] = '\0';
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Received: %s From: %s", message, topic);
}
void Mqtt::mqttPublish(const char* topic, const char* payload){
if (mqttClient.connected()) {
mqttClient.publish(topic, payload);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Published message to %s: %s", topic, payload);
} else {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to publish message. MQTT client not connected.");
}
}
void Mqtt::mqttPublishRetained(const char* topic, const char* payload) {
if (mqttClient.connected()) {
mqttClient.publish(topic, payload, true);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Published RETAINED message to %s", topic);
} else {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to publish retained message. Not connected.");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,315 +0,0 @@
(footprint "LilyGO_T5_2.13_V2.3"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(descr "Footprint for LilyGO T5 2.13 V2.3 e-paper module")
(tags "LilyGO T5 e-paper")
(property "Reference" "REF**"
(at 1.27 -37.6 0)
(layer "F.SilkS")
(uuid "2eb68159-2f43-4e2b-a754-310864418cce")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Value" "LilyGO_T5_2.13"
(at 10 -10 0)
(layer "F.Fab")
(uuid "6c908391-4387-4b76-ae85-321cce059780")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 1.27 -16.51 0)
(layer "F.Fab")
(hide yes)
(uuid "4a68447e-20bc-4125-b639-8f458329ed17")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 1.27 -16.51 0)
(layer "F.Fab")
(hide yes)
(uuid "8043e483-8f19-4aba-a4d9-5939616e0de9")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(attr through_hole)
(fp_line
(start 0 -66.29)
(end 36.35 -66.29)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "e5210833-f708-411c-84db-98902b353473")
)
(fp_line
(start 0 0)
(end 0 -66.29)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "f5975bc1-2665-4b16-8fec-43baaf338d14")
)
(fp_line
(start 36.35 -66.29)
(end 36.35 0)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "a043b2d7-a9f5-43b8-ae4f-df7bdc84d005")
)
(fp_line
(start 36.35 0)
(end 0 0)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "0b56b776-f073-4bf6-9ad5-854b39e4778a")
)
(fp_rect
(start 0 -66.29)
(end 36.35 0)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "F.CrtYd")
(uuid "a2044ea7-61aa-4787-80be-82b519e322ee")
)
(pad "1" thru_hole rect
(at 1.27 -32.6)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "e6845e72-5998-4306-a024-9276fd5f1ad5")
)
(pad "2" thru_hole rect
(at 1.27 -35.14)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "37207e8a-a869-4bf7-9477-6f1d87a3b321")
)
(pad "3" thru_hole rect
(at 1.27 -37.68)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "0af65940-aa93-4c60-a852-bb45e9d5e76c")
)
(pad "4" thru_hole rect
(at 1.27 -40.22)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "caacd5f6-6871-491f-aa32-3e2b4a778b34")
)
(pad "5" thru_hole rect
(at 1.27 -42.76)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "11e76bf1-e9d5-4d91-a623-0bbfd21c48f7")
)
(pad "6" thru_hole rect
(at 1.27 -45.3)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "883035b8-b5b0-4e13-8af9-3d9f4a5e4d2c")
)
(pad "7" thru_hole rect
(at 1.27 -47.84)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "ea2c5f3c-f803-4907-b690-3fb78abd768e")
)
(pad "8" thru_hole rect
(at 1.27 -50.38)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "db6ecdec-fb70-4e5f-9d44-e4530501f83d")
)
(pad "9" thru_hole rect
(at 1.27 -52.92)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "52df1fa0-6332-451a-b6ae-745b6323a1a3")
)
(pad "10" thru_hole rect
(at 1.27 -55.46)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "f7c3f3c9-c6c5-4377-9f06-1a7ce0f65690")
)
(pad "11" thru_hole rect
(at 1.27 -58)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "861104ac-78a7-47df-95cc-4b026abe2fd7")
)
(pad "12" thru_hole rect
(at 1.27 -60.54)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "8799d0d2-4f41-4353-b01c-cd4c7c414144")
)
(pad "13" thru_hole rect
(at 1.27 -63.08)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "2532904b-396d-4fe4-9350-768ca406f153")
)
(pad "14" thru_hole circle
(at 33.5 -32.68)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "1fe1915d-aeca-47c0-a8bc-b6aa57a1192c")
)
(pad "15" thru_hole circle
(at 33.5 -35.22)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "b1eeaad9-1cf0-43a8-b301-6d125ed7766f")
)
(pad "16" thru_hole circle
(at 33.5 -37.76)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "b5772389-9314-4992-a362-5e19a3d5573a")
)
(pad "17" thru_hole circle
(at 33.5 -40.3)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "186630c9-c5d1-4a64-8359-8b0e3bbdc45d")
)
(pad "18" thru_hole circle
(at 33.5 -42.84)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "380413e1-fbbc-453d-bcc5-7632b249fe7f")
)
(pad "19" thru_hole circle
(at 33.5 -45.38)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "d76f48db-a08d-4003-9c61-4a5e38fbff73")
)
(pad "20" thru_hole circle
(at 33.5 -47.92)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "6dee520c-4d6b-4e0b-b2e9-c34f433653f1")
)
(pad "21" thru_hole circle
(at 33.5 -50.46)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "76b9ca8d-a3eb-49ea-9c09-e59d5a04bf27")
)
(pad "22" thru_hole circle
(at 33.5 -53)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "d5fb5aaf-a7a7-4b30-bc70-c77b5eb47f97")
)
(pad "23" thru_hole circle
(at 33.5 -55.54)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "34924216-c3b4-4231-ba37-1e5648dfd6a0")
)
(pad "24" thru_hole circle
(at 33.5 -58.08)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "7e7f50b0-0a7f-49f4-b82f-c5d736f416e5")
)
(pad "25" thru_hole circle
(at 33.5 -60.62)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "d9db3d97-2265-4b8c-8b68-62232c907e68")
)
(pad "26" thru_hole circle
(at 33.5 -63.16)
(size 1.5 1.5)
(drill 0.8)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "4ac0d3d2-1c70-4fe4-a676-0c4c7cd4a072")
)
(embedded_fonts no)
)

File diff suppressed because it is too large Load Diff

View File

@ -1,135 +0,0 @@
{
"board": {
"active_layer": 7,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 1,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"prototype_zone_fills": false,
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"locked_item_shadows",
"conflict_shadows",
"shapes"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 1
},
"git": {
"integration_disabled": false,
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "water_temp.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
],
"col_order": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"col_widths": [
99,
156,
75,
82,
119,
138,
129,
113,
61,
520
],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_time_domain_details": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": 1
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"hierarchy_collapsed": [],
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"ruleAreas": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

View File

@ -1,727 +0,0 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_barcodes": false,
"apply_defaults_to_fp_dimensions": false,
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.8,
"height": 1.27,
"width": 2.54
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"min_clearance": 0.5
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_field_mismatch": "warning",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"missing_tuning_profile": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "warning",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_not_centered_on_via": "ignore",
"track_on_post_machined_layer": "error",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"tuning_profile_track_geometries": "ignore",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.0,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.0,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
}
],
"zones_allow_external_fillets": false
},
"ipc2581": {
"bom_rev": "",
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": "",
"sch_revision": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"component_class_settings": {
"assignments": [],
"meta": {
"version": 0
},
"sheet_component_classes": {
"enabled": false
}
},
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"field_name_whitespace": "warning",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"ground_pin_not_ground": "warning",
"hier_label_mismatch": "error",
"isolated_pin_label": "warning",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"stacked_pin_name": "warning",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [
"Library"
],
"pinned_symbol_libs": [
"MyKicadLib"
]
},
"meta": {
"filename": "water_temp.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.25,
"tuning_profile": "",
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
},
{
"clearance": 0.3,
"diff_pair_gap": 0.25,
"diff_pair_width": 0.2,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Power",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 0,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.5,
"tuning_profile": "",
"via_diameter": 0.6,
"via_drill": 0.3
}
],
"meta": {
"version": 5
},
"net_colors": {
"3v3": "rgb(194, 0, 0)"
},
"netclass_assignments": null,
"netclass_patterns": [
{
"netclass": "Power",
"pattern": "3v3"
},
{
"netclass": "Power",
"pattern": "GND"
},
{
"netclass": "Power",
"pattern": "V"
},
{
"netclass": "Power",
"pattern": "DRV"
}
]
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "gerber/",
"pos_files": "",
"specctra_dsn": "",
"step": "",
"svg": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"annotation": {
"method": 0,
"sort_order": 0
},
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
},
{
"group_by": false,
"label": "Beskrivning",
"name": "Description",
"show": false
},
{
"group_by": false,
"label": "nr.",
"name": "${ITEM_NUMBER}",
"show": false
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "",
"sort_asc": true,
"sort_field": "Reference"
},
"bus_aliases": {},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"hop_over_size_choice": 0,
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"reuse_designators": true,
"space_save_all_events": true,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0,
"top_level_sheets": [
{
"filename": "water_temp.kicad_sch",
"name": "water_temp",
"uuid": "c1e691f1-9ee7-4a71-abd3-04b9ff725907"
}
],
"used_designators": "U5,C2",
"variants": []
},
"sheets": [
[
"c1e691f1-9ee7-4a71-abd3-04b9ff725907",
"water_temp"
]
],
"text_variables": {},
"tuning_profiles": {
"meta": {
"version": 0
},
"tuning_profiles_impedance_geometric": []
}
}

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -13,13 +13,6 @@ platform = espressif32
board = ttgo-lora32-v1
framework = arduino
monitor_speed = 115200
build_flags =
-D DEBUG
-I../common/include
lib_deps =
zinggjm/GxEPD2@^1.5.6
bblanchon/ArduinoJson@^7.0.4
peterus/esp-logger@^1.0.0
jgromes/RadioLib@^6.0.0
olikraus/U8g2_for_Adafruit_GFX@^1.8.0
milesburton/DallasTemperature@^3.11.0
thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays@^4.5.0
sandeepmistry/LoRa@^0.8.0

View File

@ -1,115 +0,0 @@
#include <ArduinoJson.h>
#include <SPIFFS.h>
#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<float>();
lora.spreadingFactor = data["lora"]["spreadingFactor"].as<int>();
lora.signalBandwidth = data["lora"]["signalBandwidth"].as<float>();
lora.codingRate4 = data["lora"]["codingRate4"].as<int>();
lora.power = data["lora"]["power"].as<int>();
logConfigInfo(data);
configFile.close();
loraConfig = lora;
isConfigLoaded = true;
}
void Config::writeData()
{
JsonDocument doc;
JsonObject lora = doc["lora"].to<JsonObject>();
lora["frequency"] = LoraDefaults::frequency;
lora["spreadingFactor"] = LoraDefaults::spreadingFactor;
lora["signalBandwidth"] = LoraDefaults::signalBandwidth;
lora["codingRate4"] = LoraDefaults::codingRate4;
lora["power"] = LoraDefaults::power;
// 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<JsonObject>();
// 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<String>()) {
value = jsonValue.as<String>();
} else if (jsonValue.is<int>()) {
value = String(jsonValue.as<int>());
} else if (jsonValue.is<long>()) {
value = String(jsonValue.as<long>());
} else if (jsonValue.is<float>()) {
value = String(jsonValue.as<float>(), 3);
} else if (jsonValue.is<bool>()) {
value = jsonValue.as<bool>() ? "true" : "false";
} else if (jsonValue.is<JsonArray>() || jsonValue.is<JsonObject>()) {
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());
}
}

View File

@ -1,24 +0,0 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <Arduino.h>
#include <FS.h>
#include <ArduinoJson.h>
#include "lora_config.hpp"
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 */

View File

@ -1,153 +0,0 @@
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeSerifBold24pt7b.h>
#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 = 50;
//u8g2Fonts.setFont(u8g2_font_fur42_tf);
u8g2Fonts.setFont(u8g2_font_inb46_mf);
drawString(textX, 70, String(temperature, 1) + "°", LEFT);
drawMercury(temperature);
}
void Eink::show_internal_temp(int x, int y, float temperature)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "Show internal temp: %f", temperature);
u8g2Fonts.setFont(u8g2_font_7x13_tf);
drawString(x + 60, y - 11, String(temperature, 1) + "°", LEFT);
}
void Eink::show_count(int x, int y, float cnt, String unit)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "Show cnt: %f", cnt);
u8g2Fonts.setFont(u8g2_font_7x13_tf);
drawString(x + 60, y - 11, unit + String(cnt, 1), LEFT);
}
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, 5.0, 35.0, 10, height);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "mercuryLevel: %d", mercuryLevel);
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, int percentage)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "EINK", "drawBattery: %d %", percentage);
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)
{
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);
}

View File

@ -1,31 +0,0 @@
#ifndef EINK_H
#define EINK_H
#include <Arduino.h>
#include <GxEPD2_BW.h>
#include "epd/GxEPD2_213.h"
#include <U8g2_for_Adafruit_GFX.h>
#include "pins.hpp"
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_internal_temp(int x, int y, float temperature);
void show_count(int x, int y, float cnt, String unit);
void show_display(String header, int wait=100);
void drawBattery(int x, int y, int percentage=100);
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_BW<GxEPD2_213_BN, GxEPD2_213_BN::HEIGHT> eink;
};
#endif /* EINK_H */

View File

@ -1,97 +0,0 @@
#include "io.hpp"
#include "logger.h"
#include "pins.hpp"
#include "WiFi.h"
#include "driver/adc.h"
#include <esp_wifi.h>
#include <esp_bt.h>
extern logging::Logger logger;
IO::IO() : oneWire(DI_DALLAS), sensors(&oneWire) {
// Constructor body (if needed)
}
#define uS_TO_S_FACTOR 1000000 /* Conversion factor for micro seconds to seconds */
#define TIME_TO_SLEEP 600
void IO::setup_io()
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "IO", "SETUP IO");
oneWire.begin(DI_DALLAS);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "IO", "oneWire.begin");
sensors.begin();
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "IO", "sensors.begi");
sensors.setResolution(12);
}
void IO::set_low_power()
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "IO", "Set lower power attrs");
pinMode(DI_LED, OUTPUT);
digitalWrite(DI_LED, LOW); // TODO check if pin 22(ledbuiltin 2) is a led
setCpuFrequencyMhz(80);
WiFi.mode(WIFI_OFF);
btStop();
esp_wifi_stop();
esp_bt_controller_disable();
esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
adc_power_on();
}
void IO::set_deep_sleep()
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "IO", "Going to sleep");
esp_deep_sleep_start();
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "IO", "Will not reach");
}
uint8_t IO::read_battery()
{
uint8_t percentage = 100;
float voltage = analogRead(ADC_BATTERY) / 4096.0 * 7.46;
// float voltage = analogRead(35) / 4096.0 * 7.46;
if (voltage > 1)
{ // Only display if there is a valid reading
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;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "io", "Voltage: %f", voltage);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "io", "Percentage: %d", percentage);
}
return percentage;
}
float IO::read_temp()
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "IO", "read_temp");
sensors.requestTemperatures(); // Request temperature readings
delay(1000);
float tempC = sensors.getTempCByIndex(0);
if (tempC != DEVICE_DISCONNECTED_C)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "io", "Temp: %f", tempC);
}
else if(tempC == -127.00) {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "io", "Couldn't read temp");
}
else
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "io", "Couldn't read temp");
}
return tempC;
}
float IO::read_internal_temp()
{
float chipTemp = temperatureRead();
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "io", "chipTemp: %f", chipTemp);
return chipTemp;
}

View File

@ -1,23 +0,0 @@
#ifndef IO_H
#define IO_H
#include <Arduino.h>
#include <OneWire.h>
#include <DallasTemperature.h>
class IO {
public:
IO();
void setup_io();
void set_low_power();
void set_deep_sleep();
float read_temp();
float read_internal_temp();
uint8_t read_battery();
private:
OneWire oneWire;
DallasTemperature sensors;
};
#endif /* IO_H */

View File

@ -1,138 +0,0 @@
#include "config.hpp"
#include "lorahandler.hpp"
#include "logger.h"
#include "pins.hpp"
extern logging::Logger logger;
extern Config config;
LoraHandler::LoraHandler() : radio(nullptr), spi(nullptr) {}
void LoraHandler::setup()
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", "Initializing SX1268 with RadioLib!");
// Create new SPI instance for LoRa
spi = new SPIClass(VSPI);
spi->begin(RADIO_SCK, RADIO_MISO, RADIO_MOSI, RADIO_CS);
// Create SX1268 instance
radio = new SX1268(new Module(RADIO_CS, RADIO_IRQ, RADIO_RST, RADIOLIB_NC, *spi));
// Configure RXEN/TXEN pins
pinMode(RADIO_RXEN, OUTPUT);
pinMode(RADIO_TXEN, OUTPUT);
digitalWrite(RADIO_RXEN, LOW);
digitalWrite(RADIO_TXEN, LOW);
const float freq = config.loraConfig.frequency;
const float bandwidth = config.loraConfig.signalBandwidth;
// Initialize LoRa module
int state = radio->begin();
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "LoRa init failed: %i", state);
while (true) {
delay(1000);
}
}
// Configure LoRa parameters
state = radio->setFrequency(freq);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setFrequency failed: %i", state);
}
state = radio->setSpreadingFactor(config.loraConfig.spreadingFactor);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setSpreadingFactor failed: %i", state);
}
state = radio->setBandwidth(bandwidth);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setBandwidth failed: %i", state);
}
state = radio->setCodingRate(config.loraConfig.codingRate4);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setCodingRate failed: %i", state);
}
state = radio->setOutputPower(config.loraConfig.power);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setOutputPower failed: %i", state);
}
// Enable 2-byte LoRa CRC
state = radio->setCRC(2);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa", "setCRC failed: %i", state);
}
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", "SX1268 initialized successfully!");
String currentLoRainfo = "LoRa Freq: " + String(freq, 3) + " MHz / BW: " + String(bandwidth, 1) + " kHz / SF:" + String(config.loraConfig.spreadingFactor) + " / CR: " + String(config.loraConfig.codingRate4);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", currentLoRainfo.c_str());
}
void LoraHandler::sendPacket(const String& data)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa Tx", "Sending: %s", data.c_str());
// Enable TX mode
digitalWrite(RADIO_TXEN, HIGH);
digitalWrite(RADIO_RXEN, LOW);
delay(10);
String payload = data;
int state = radio->transmit(payload);
// Disable TX mode
digitalWrite(RADIO_TXEN, LOW);
if (state != RADIOLIB_ERR_NONE)
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa Tx", "Transmit failed: %i", state);
}
else
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa Tx", "Packet sent!");
}
}
ReceivedLoRaPacket LoraHandler::receivePacket()
{
ReceivedLoRaPacket receivedLoraPacket;
// Enable RX mode
digitalWrite(RADIO_RXEN, HIGH);
digitalWrite(RADIO_TXEN, LOW);
int state = radio->receive(receivedLoraPacket.text);
if (state == RADIOLIB_ERR_RX_TIMEOUT)
{
return receivedLoraPacket;
}
else if (state == RADIOLIB_ERR_NONE)
{
receivedLoraPacket.rssi = radio->getRSSI();
receivedLoraPacket.snr = radio->getSNR();
receivedLoraPacket.freqError = radio->getFrequencyError();
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa Rx", "---> %s (RSSI: %.1f, SNR: %.1f)",
receivedLoraPacket.text.c_str(), receivedLoraPacket.rssi, receivedLoraPacket.snr);
}
else
{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "LoRa Rx", "Receive error: %i", state);
}
return receivedLoraPacket;
}

View File

@ -1,25 +0,0 @@
#ifndef LORAHANDLER_H
#define LORAHANDLER_H
#include "config.hpp"
#include <RadioLib.h>
struct ReceivedLoRaPacket {
String text;
float rssi;
float snr;
float freqError;
};
class LoraHandler {
public:
LoraHandler();
void setup();
void sendPacket(const String& data);
ReceivedLoRaPacket receivePacket();
private:
SX1268* radio;
SPIClass* spi;
};
#endif /* LORAHANDLER_H */

View File

@ -1,97 +1,129 @@
#include <logger.h>
#include "config.hpp"
#include "eink.hpp"
#include "io.hpp"
#include "lorahandler.hpp"
#include "tpl5110.hpp"
#include "pins.hpp"
#include <ArduinoJson.h>
logging::Logger logger;
Config config;
Eink eink;
IO io;
LoraHandler lora;
TPL5110 tpl5110(TPL5110_DONE);
RTC_DATA_ATTR int bootCount = 0;
RTC_DATA_ATTR int updateCount = 0;
RTC_DATA_ATTR float last_temp = 20;
RTC_DATA_ATTR int last_battery = 0;
bool update = false;
#include "SSD1306.h" // alias for `#include "SSD1306Wire.h"`
#include <WiFi.h>
#include <SPI.h>
#include <LoRa.h>
// #include "SSD1306.h"
#include<Arduino.h>
void setup()
{
//OLED pins to ESP32 GPIOs via this connecthin:
//OLED_SDA GPIO4
//OLED_SCL GPIO15
//OLED_RST GPIO16
SSD1306 display(0x3c, 4, 15);
// 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);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Boot number: %i", bootCount);
update = false;
#ifndef DEBUG
logger.setDebugLevel(logging::LoggerLevel::LOGGER_LEVEL_INFO);
#endif
tpl5110.setup(); // Initialize TPL5110 timer
io.setup_io();
io.set_low_power();
uint8_t battery = io.read_battery();
if (abs(battery - last_battery) >= 2)
{
last_battery = battery;
update = true;
}
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Battery: %i", battery);
float temp = io.read_temp();
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "TEMP: %f", temp);
if (abs(temp - last_temp) >= 0.5 && temp > -120)
{
last_temp = temp;
update = true;
}
while (!Serial); //If just the the basic function, must connect to a computer
float internal_temp = io.read_internal_temp();
eink.setup_eink();
++bootCount;
if (update)
{
++updateCount;
eink.drawBattery(186, 14, battery); // eink width 250 - drawbattery width
eink.drawSignalBars(180 - 24 - 4, 12, 84); // drawbattery width - drawsignal
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "TEMP: %f", temp);
eink.show_temp(temp);
eink.show_internal_temp(180 - 24 - 4 - 100, 12, internal_temp);
eink.show_count(180 - 24 - 4 - 100 - 60, 12, (float)bootCount, "c: ");
eink.show_count(180 - 24 - 4 - 100 - 60 - 50, 12, (float)bootCount, "u: ");
eink.display(true);
// Initialize LoRa and send data
lora.setup();
// Create JSON payload
JsonDocument doc;
doc["temp"] = temp;
doc["battery"] = battery;
doc["boot"] = bootCount;
doc["update"] = updateCount;
String payload;
serializeJson(doc, payload);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Sending LoRa payload: %s", payload.c_str());
lora.sendPacket(payload);
delay(1000); // Give time for transmission
// 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);
}
// Signal TPL5110 that work is done - device will sleep
tpl5110.signalDone();
delay(100);
io.set_deep_sleep();
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);
}
void loop()
{
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);
}
/* 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));
}
*/

View File

@ -1,33 +0,0 @@
#ifndef PINS_H
#define PINS_H
// Pin assignments for EPD display
const int EPD_MOSI = 23;
const int EPD_MISO = -1; // elink no use
const int EPD_SCLK = 18;
const int EPD_BUSY = 4;
const int EPD_RSET = 16;
const int EPD_DC = 17;
const int EPD_CS = 5;
// Pin assignments for LORA module (SX1268 E22-400M22S)
const int RADIO_SCK = 5;
const int RADIO_MISO = 19;
const int RADIO_MOSI = 27;
const int RADIO_CS = 18; // CS --> NSS
const int RADIO_RST = 14;
const int RADIO_IRQ = 26; // IRQ --> DIO0
const int RADIO_RXEN = 2; // RX Enable
const int RADIO_TXEN = 13; // TX Enable
// TPL5110 timer module
const int TPL5110_DONE = 15; // DONE signal
// Battery pin
const int ADC_BATTERY = 35; //builtin = 35
const int DI_DALLAS = 32;
// Builtin LED
const int DI_LED = LED_BUILTIN;
#endif /* PINS_H */

View File

@ -1,32 +0,0 @@
#include "tpl5110.hpp"
#include "logger.h"
extern logging::Logger logger;
TPL5110::TPL5110(int donePin) : _donePin(donePin) {
}
void TPL5110::setup() {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "TPL5110", "Initializing TPL5110 timer!");
pinMode(_donePin, OUTPUT);
digitalWrite(_donePin, LOW); // DONE pin starts LOW
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "TPL5110", "TPL5110 initialized!");
}
void TPL5110::signalDone() {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "TPL5110", "Signaling DONE to TPL5110!");
digitalWrite(_donePin, HIGH);
delay(100); // Hold HIGH for 100ms
digitalWrite(_donePin, LOW);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "TPL5110", "DONE signal sent - device will sleep now");
}
void TPL5110::setActive() {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "TPL5110", "TPL5110 set to ACTIVE");
digitalWrite(_donePin, LOW);
}
void TPL5110::setInactive() {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "TPL5110", "TPL5110 set to INACTIVE");
digitalWrite(_donePin, HIGH);
}

View File

@ -1,17 +0,0 @@
#ifndef TPL5110_H
#define TPL5110_H
#include <Arduino.h>
class TPL5110 {
public:
TPL5110(int donePin);
void setup();
void signalDone();
void setActive();
void setInactive();
private:
int _donePin;
};
#endif /* TPL5110_H */