From 66672c7808417dad465e1f989c8d863bcbe1bc33 Mon Sep 17 00:00:00 2001 From: "E. S" Date: Sun, 22 Jun 2025 18:28:29 +0300 Subject: [PATCH] initial --- .gitignore | 6 + include/common.h | 28 +++++ include/janitza.h | 35 ++++++ include/led.h | 26 ++++ mqtt_monitor.sh | 71 +++++++++++ platformio.ini | 35 ++++++ src/common.cpp | 22 ++++ src/janitza.cpp | 91 ++++++++++++++ src/led.cpp | 18 +++ src/main.cpp | 300 ++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 632 insertions(+) create mode 100644 .gitignore create mode 100644 include/common.h create mode 100644 include/janitza.h create mode 100644 include/led.h create mode 100755 mqtt_monitor.sh create mode 100644 platformio.ini create mode 100644 src/common.cpp create mode 100644 src/janitza.cpp create mode 100644 src/led.cpp create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07c5eeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +/mqtt_ca.crt \ No newline at end of file diff --git a/include/common.h b/include/common.h new file mode 100644 index 0000000..6890dd1 --- /dev/null +++ b/include/common.h @@ -0,0 +1,28 @@ +#ifndef JANITZA104_ESP32_COMMON_H_ +#define JANITZA104_ESP32_COMMON_H_ + +#include +#include + +#ifdef DEBUG + +#define PRINTLN(s) Serial.println(s) +#define PRINT(s) Serial.print(s) +#define PRINTF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__) + +static void printWithPadding(String str, int totalLength); + +#else + +#define PRINTLN(s) +#define PRINT(s) +#define PRINTF(...) +#define printWithPadding(s, l) + +#endif + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(*(a))) + +bool startsWith(const char* source, const char* prefix); + +#endif //JANITZA104_ESP32_COMMON_H_ \ No newline at end of file diff --git a/include/janitza.h b/include/janitza.h new file mode 100644 index 0000000..6e4bf06 --- /dev/null +++ b/include/janitza.h @@ -0,0 +1,35 @@ +#ifndef JANITZA104_ESP32_JANITZA_H_ +#define JANITZA104_ESP32_JANITZA_H_ + +#include + +enum class JanitzaType { FLOAT, LONG64, INT, SHORT }; +enum class JanitzaUnit { V, A, W, VA, Wh }; + +struct JanitzaRegister { + uint16_t addr; + JanitzaType type; + JanitzaUnit unit; + const char* name; + const char* desc; +}; + + +class JanitzaReader { +private: + ModbusMaster node; + uint8_t lastErrorCode = 0; + + static void modbusPreTransmissionCallback(); + static void modbusPostTransmissionCallback(); + +public: + void configure(); + bool readFloat(const JanitzaRegister& reg, float* val); + uint8_t getLastErrorCode() const; +}; + + +const char* JanitzaUnitStr(JanitzaUnit unit); + +#endif //JANITZA104_ESP32_JANITZA_H_ \ No newline at end of file diff --git a/include/led.h b/include/led.h new file mode 100644 index 0000000..c2bbf68 --- /dev/null +++ b/include/led.h @@ -0,0 +1,26 @@ +#ifndef JANITZA104_ESP32_LED_H_ +#define JANITZA104_ESP32_LED_H_ + +#include +#include + +class Led { +private: + uint8_t _pin; + +public: + explicit Led(uint8_t pin) : _pin(pin) { + pinMode(_pin, OUTPUT); + off(); + } + + inline void off() const { digitalWrite(_pin, HIGH); } + inline void on() const { digitalWrite(_pin, LOW); } + + void on_off(uint16_t delay_ms, bool last_delay = false) const; + void blink(uint8_t count, uint16_t delay_ms) const; +}; + +extern const Led* mcu_led; + +#endif //JANITZA104_ESP32_LED_H_ diff --git a/mqtt_monitor.sh b/mqtt_monitor.sh new file mode 100755 index 0000000..a24943b --- /dev/null +++ b/mqtt_monitor.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +set -e + +usage() { + echo "usage: $0 [-h|--help] [-t ]" >&2 + exit +} + +die() { + echo "error: $1" >&2 + exit 1 +} + +check_command() { + if ! command -v "$1" >/dev/null; then + die "$1 is not installed. Please install $1 to proceed." + fi +} + +parse_yaml() { + if ! yaml_output=$(yq -r "$1" "$2" 2>/dev/null); then + die "YAML file is malformed or does not exist." + fi + echo "$yaml_output" +} + +for c in yq mosquitto_sub tput; do check_command $c; done + +[ -z "$1" ] && usage + +bold=$(tput bold) +rst=$(tput sgr0) +topic= +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + -t|--topic) + topic="$2" + shift + ;; + *) ;; + esac + shift +done + +[ -z "$topic" ] && die "Topic not provided. Use -t or --topic to specify the topic." + +remote_host="mqtt.example.org" +remote_port=8883 +username="admin" +password="password" + +mydir=$(dirname "$0") +cafile="$mydir/mqtt_ca.crt" +topic_regex="${topic%#}" +topic_regex="${topic_regex//\//\\/}" + +echo mosquitto_sub -d -h "$remote_host" -p "$remote_port" --cafile "$cafile" -t "$topic" -u "$username" -P "$password" -v + +mosquitto_sub -d -h "$remote_host" -p "$remote_port" --cafile "$cafile" -t "$topic" -u "$username" -P "$password" -v | while IFS= read -r line; do + binary_data="$(echo "$line" | sed "s/^${topic_regex}[^ ]* //")" + echo -n "${bold}$(echo "$line" | awk '{print $1}')${rst}" + echo -n " " + echo -n "$binary_data" | xxd -p | tr -d '\n' | sed 's/../& /g' + echo +# echo +done \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..727143a --- /dev/null +++ b/platformio.ini @@ -0,0 +1,35 @@ +; 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:denky32] +platform = espressif32 +board = esp32dev +framework = arduino +lib_deps = + 4-20ma/ModbusMaster@^2.0.1 + knolleary/PubSubClient@^2.8 +monitor_speed = 115200 +build_flags = + -DCONFIG_FW_VERSION=1 + -DCONFIG_MCU_LED_GPIO=2 + -DCONFIG_JANITZA_EMULATE=1 + -DCONFIG_JANITZA_EMULATE_ERROR=0 + -DCONFIG_MODBUS_DIR_PIN=4 + -DCONFIG_MODBUS_RX_PIN=18 + -DCONFIG_MODBUS_TX_PIN=19 + -DCONFIG_MODBUS_SLAVE_ID=1 + -DCONFIG_MODBUS_SERIAL_BAUD=9600 + -DCONFIG_MQTT_SERVER="\"mqtt.example.org\"" + -DCONFIG_MQTT_PORT=8883 + -DCONFIG_MQTT_USERNAME="\"username\"" + -DCONFIG_MQTT_PASSWORD="\"password\"" + -DCONFIG_WIFI_HOSTNAME="\"ESP_J104R_0001\"" + -DCONFIG_NODE_ID=0 +build_type = release \ No newline at end of file diff --git a/src/common.cpp b/src/common.cpp new file mode 100644 index 0000000..1ada866 --- /dev/null +++ b/src/common.cpp @@ -0,0 +1,22 @@ +#include "common.h" + +#ifdef DEBUG + +void printWithPadding(String str, int totalLength) { + int paddingLength = totalLength - str.length(); + Serial.print(str); + for (int i = 0; i < paddingLength; i++) { + Serial.print(" "); + } +} + +#endif + +bool startsWith(const char* source, const char* prefix) { + while (*prefix) { + if (*source != *prefix) return false; + source++; + prefix++; + } + return true; +} diff --git a/src/janitza.cpp b/src/janitza.cpp new file mode 100644 index 0000000..0330257 --- /dev/null +++ b/src/janitza.cpp @@ -0,0 +1,91 @@ +#include +#include +#include "janitza.h" +#include "common.h" + +#define MODBUS_TRANSMISSION_DELAY 2 + +const char* JanitzaUnitStr(JanitzaUnit unit) { + switch (unit) { + case JanitzaUnit::V: return "V"; + case JanitzaUnit::A: return "A"; + case JanitzaUnit::W: return "W"; + case JanitzaUnit::VA: return "VA"; + case JanitzaUnit::Wh: return "Wh"; + default: return "[?]"; + } +} + +void JanitzaReader::configure() { +#ifndef CONFIG_JANITZA_EMULATE + pinMode(CONFIG_MODBUS_DIR_PIN, OUTPUT); + digitalWrite(CONFIG_MODBUS_DIR_PIN, LOW); + + Serial2.begin(CONFIG_MODBUS_SERIAL_BAUD, SERIAL_8N1, CONFIG_MODBUS_RX_PIN, CONFIG_MODBUS_TX_PIN); + Serial2.setTimeout(200); + node.begin(CONFIG_MODBUS_SLAVE_ID, Serial2); + + node.preTransmission(JanitzaReader::modbusPreTransmissionCallback); + node.postTransmission(JanitzaReader::modbusPostTransmissionCallback); +#endif +} + +void JanitzaReader::modbusPreTransmissionCallback() { +#ifndef CONFIG_JANITZA_EMULATE + delay(MODBUS_TRANSMISSION_DELAY); + digitalWrite(CONFIG_MODBUS_DIR_PIN, HIGH); +#endif +} + +void JanitzaReader::modbusPostTransmissionCallback() { +#ifndef CONFIG_JANITZA_EMULATE + digitalWrite(CONFIG_MODBUS_DIR_PIN, LOW); + delay(MODBUS_TRANSMISSION_DELAY); +#endif +} + +bool JanitzaReader::readFloat(const JanitzaRegister& reg, float* val) { +#ifndef CONFIG_JANITZA_EMULATE + uint8_t result; + uint16_t buf[2]; + + result = node.readInputRegisters(reg.addr, 2); + if (result == node.ku8MBSuccess) { + buf[1] = node.getResponseBuffer(0x00); + buf[0] = node.getResponseBuffer(0x01); + *val = *((float*)buf); + PRINTF("JanitzaReader: %s = %.2f\n", reg->name, val); + return true; + } else { + lastErrorCode = result; + PRINTF("JanitzaReader: failed to read %d, device response: %d\n", reg.addr, result); + return false; + } +#else +#if CONFIG_JANITZA_EMULATE_ERROR == 1 + PRINTLN("JanitzaReader: emulation read error"); + return false; +#endif + + if (startsWith(reg.name, "_ULN") || startsWith(reg.name, "_ULN")) { + *val = 220.0f; + } else if (startsWith(reg.name, "_ILN")) { + *val = 5.0f; + } else if (startsWith(reg.name, "_I_SUM3")) { + *val = 15.0f; + } else if (startsWith(reg.name, "_PLN")) { + *val = 100.0f; + } else if (!strcmp(reg.name, "_P_SUM3")) { + *val = 250.0f; + } else if (startsWith(reg.name, "_SLN")) { + *val = 400.0f; + } else if (startsWith(reg.name, "_WH")) { + *val = 450.0f; + } + return true; +#endif +} + +uint8_t JanitzaReader::getLastErrorCode() const { + return lastErrorCode; +} \ No newline at end of file diff --git a/src/led.cpp b/src/led.cpp new file mode 100644 index 0000000..8f33f80 --- /dev/null +++ b/src/led.cpp @@ -0,0 +1,18 @@ +#include "led.h" + +void Led::on_off(uint16_t delay_ms, bool last_delay) const { + on(); + delay(delay_ms); + + off(); + if (last_delay) + delay(delay_ms); +} + +void Led::blink(uint8_t count, uint16_t delay_ms) const { + for (uint8_t i = 0; i < count; i++) { + on_off(delay_ms, i < count-1); + } +} + +const Led* mcu_led = new Led(CONFIG_MCU_LED_GPIO); diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c2241d7 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,300 @@ +#include +#include +#include +#include +#include +#include +#include "janitza.h" +#include "common.h" +#include "led.h" + +#define MQTT_RECONNECT_SECONDS 3 +#define LOOP_DELAY 500 +#define READ_FREQ 5000 + +#define CONFIG_WIFI_SSID "ssid" +#define CONFIG_WIFI_PSK "11112222" + +// Certificate +const char* mqttRootCA = \ +"-----BEGIN CERTIFICATE-----\n" \ +... +"-----END CERTIFICATE-----"; + +const char* mqttServer = CONFIG_MQTT_SERVER; +const char* mqttUserName = CONFIG_MQTT_USERNAME; +const char* mqttPassword = CONFIG_MQTT_PASSWORD; + +static JanitzaRegister umg104_registers[] = { + {19000, JanitzaType::FLOAT, JanitzaUnit::V, "_ULN[0]", "Voltage L1-N"}, + {19002, JanitzaType::FLOAT, JanitzaUnit::V, "_ULN[1]", "Voltage L2-N"}, + {19004, JanitzaType::FLOAT, JanitzaUnit::V, "_ULN[2]", "Voltage L3-N"}, + + {19006, JanitzaType::FLOAT, JanitzaUnit::V, "_ULL[0]", "Voltage L1-L2"}, + {19008, JanitzaType::FLOAT, JanitzaUnit::V, "_ULL[1]", "Voltage L2-L3"}, + {19010, JanitzaType::FLOAT, JanitzaUnit::V, "_ULL[2]", "Voltage L3-L1"}, + + {19012, JanitzaType::FLOAT, JanitzaUnit::A, "_ILN[0]", "Apparent current L1"}, + {19014, JanitzaType::FLOAT, JanitzaUnit::A, "_ILN[1]", "Apparent current L2"}, + {19016, JanitzaType::FLOAT, JanitzaUnit::A, "_ILN[2]", "Apparent current L3"}, + + {19018, JanitzaType::FLOAT, JanitzaUnit::A, "_I_SUM3", "Vector sum; IN=I1+I2+I3"}, + {19026, JanitzaType::FLOAT, JanitzaUnit::W, "_P_SUM3", "Sum; Psum3=P1+P2+P3"}, + + {19020, JanitzaType::FLOAT, JanitzaUnit::W, "_PLN[0]", "Real power L1"}, + {19022, JanitzaType::FLOAT, JanitzaUnit::W, "_PLN[1]", "Real power L2"}, + {19024, JanitzaType::FLOAT, JanitzaUnit::W, "_PLN[2]", "Real power L3"}, + + {19028, JanitzaType::FLOAT, JanitzaUnit::VA, "_SLN[0]", "Apparent power L1"}, + {19030, JanitzaType::FLOAT, JanitzaUnit::VA, "_SLN[1]", "Apparent power L2"}, + {19032, JanitzaType::FLOAT, JanitzaUnit::VA, "_SLN[2]", "Apparent power L3"}, + + {19054, JanitzaType::FLOAT, JanitzaUnit::Wh, "_WH[0]", "Real energy L1"}, + {19056, JanitzaType::FLOAT, JanitzaUnit::Wh, "_WH[1]", "Real energy L2"}, + {19058, JanitzaType::FLOAT, JanitzaUnit::Wh, "_WH[2]", "Real energy L3"} +}; + +static WiFiClientSecure espClient; +static PubSubClient mqtt(espClient); + +static JanitzaReader jr; + +static const char* MqttHelloTopic = "slrmn/%d/hello"; +static const char* MqttOtaTopic = "slrmn/%d/ota"; +static const char* MqttEnergyTopic = "slrmn/%d/energy"; +static const char* MqttErrorTopic = "slrmn/%d/error"; + +struct __attribute__((__packed__)) MqttEnergyDataPayload { + float uln0; + float uln1; + float uln2; + + float ull0; + float ull1; + float ull2; + + float iln0; + float iln1; + float iln2; + + float i_sum3; + float p_sum3; + + float pln0; + float pln1; + float pln2; + + float sln0; + float sln1; + float sln2; + + float wh0; + float wh1; + float wh2; + + uint32_t timestamp; +}; + +struct __attribute__((__packed__)) MqttErrorPayload { + uint32_t timestamp; + uint16_t addr; + uint8_t code; +}; + +struct __attribute__((__packed__)) MqttHelloPayload { + uint32_t timestamp; + uint16_t fw_version; +}; + +static volatile bool mqttNeedsReconnect = true; +static bool timeConfigured = false; +static bool wifiConnectionCalled = false; +// static bool wifiOnceConnected = false; + +static long lastNTPCheck = 0; +static long lastJanitraRead = 0; +static long lastMqttReconnectAttempt = 0; +static long wifiReconnectStarted = 0; + +static void WiFiStationConnected(WiFiEvent_t event, WiFiEventInfo_t info); +static void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); + +static void mqttReconnect(); +static void mqttCallback(char* topic, uint8_t* payload, size_t length); +static void getTopicName(char* buf, size_t size, const char* fmt); +static String strgen(int length); + +void setup() { +#ifdef DEBUG + Serial.begin(115200); +#endif + +#ifndef CONFIG_JANITZA_EMULATE + jr.configure(); +#endif + + + WiFi.begin(CONFIG_WIFI_SSID, CONFIG_WIFI_PSK); + WiFi.setAutoReconnect(true); + WiFi.hostname(CONFIG_WIFI_HOSTNAME); + + espClient.setCACert(mqttRootCA); + // espClient.setInsecure(); + + wifiConnectionCalled = true; +} + +void loop() { + auto ws = WiFi.status(); + if (ws != WL_CONNECTED) { + if (wifiReconnectStarted > 0 && millis() - wifiReconnectStarted > 60000) { + PRINTF("\nfailed to reconnect, restarting...\n"); + ESP.restart(); + return; + } + if (!wifiConnectionCalled) { + PRINT("Connecting to wifi.."); + // WiFi.setAutoReconnect(true); + WiFi.disconnect(); + WiFi.reconnect(); + wifiConnectionCalled = true; + wifiReconnectStarted = millis(); + } + PRINT("."); + mcu_led->blink(2, 50); + delay(LOOP_DELAY); + mqttNeedsReconnect = true; + return; + } + + wifiConnectionCalled = false; + + if (!timeConfigured) { + randomSeed(micros()); + + configTime(3600*3, 0, "pool.ntp.org", "ntp0.ntp-servers.net", "ntp4.ntp-servers.net"); + timeConfigured = true; + + PRINTLN("Waiting for time"); + while (time(nullptr) < 24 * 3600) { + PRINT("."); + delay(LOOP_DELAY); + } + PRINTLN(""); + } + + time_t timestamp = time(nullptr); + + long now = millis(); + long delaytime = LOOP_DELAY; + bool firstTimeFetch = lastNTPCheck == 0; + + int mqttState = mqtt.state(); + if (!mqtt.connected() && (mqttNeedsReconnect && (!lastMqttReconnectAttempt || now-lastMqttReconnectAttempt > MQTT_RECONNECT_SECONDS*1000)) || mqttState < 0) + mqttReconnect(); + + if (mqtt.connected()) { + mqtt.loop(); + + if (!lastJanitraRead || now-lastJanitraRead > READ_FREQ) { + uint16_t buf[2]; + char topic[32]; + float val; + + uint16_t failedAddr = 0; + uint8_t failedCode = 0; + uint32_t ts = timestamp; + + MqttEnergyDataPayload energyPayload; + energyPayload.timestamp = ts; + + JanitzaRegister* reg; + for (int i = 0; i < ARRAY_SIZE(umg104_registers); i++) { + reg = &umg104_registers[i]; + if (!jr.readFloat(*reg, &val)) { + failedCode = jr.getLastErrorCode(); + failedAddr = reg->addr; + break; + } + (*(float*)((uint8_t*)&energyPayload + sizeof(float)*i)) = val; + } + + if (failedAddr != 0) { + MqttErrorPayload errorPayload = { + .timestamp = ts, + .addr = failedAddr, + .code = failedCode + }; + + getTopicName(topic, sizeof(topic), MqttErrorTopic); + mqtt.publish(topic, (uint8_t*)&errorPayload, sizeof(errorPayload)); + } else { + getTopicName(topic, sizeof(topic), MqttEnergyTopic); + mqtt.publish(topic, (uint8_t*)&energyPayload, sizeof(energyPayload)); + } + + lastJanitraRead = now; + } else { + long timedelta = READ_FREQ - (now - lastJanitraRead); + delaytime = timedelta > LOOP_DELAY ? LOOP_DELAY : timedelta; + } + } + + // PRINTLN(timestamp); + delay(delaytime); +} + +static void mqttCallback(char* topic, uint8_t* payload, size_t length) { + +} + +static void mqttReconnect() { + char buf[32]; + long now = millis(); + mqttNeedsReconnect = false; + lastMqttReconnectAttempt = now; + mqtt.setBufferSize(1024); + mqtt.setServer(mqttServer, CONFIG_MQTT_PORT); + String clientId = String(CONFIG_WIFI_HOSTNAME) + "_" + strgen(4); + PRINTF("mqtt client id will be %s\n", clientId.c_str()); + if (mqtt.connect(clientId.c_str(), mqttUserName, mqttPassword)) { + PRINTLN("mqtt: connected [1]"); + mqtt.loop(); + mqtt.setCallback(mqttCallback); + + // subscribe to the OTA topic + getTopicName(buf, sizeof(buf), MqttOtaTopic); + mqtt.subscribe(buf); + + // send hello payl + getTopicName(buf, sizeof(buf), MqttHelloTopic); + auto payload = MqttHelloPayload { + .timestamp = static_cast(time(nullptr)), + .fw_version = CONFIG_FW_VERSION + }; + mqtt.publish(buf, (uint8_t*)&payload, sizeof(payload)); + + PRINTLN("mqtt: connected [2]"); + lastMqttReconnectAttempt = 0; + } else { + PRINTF("mqtt: failed to connect, rc=%d, retrying in %d seconds\n", + mqtt.state(), MQTT_RECONNECT_SECONDS); + mqttNeedsReconnect = true; + } +} + +static void getTopicName(char* buf, size_t size, const char* fmt) { + memset(buf, 0, size); + snprintf(buf, size-1, fmt, CONFIG_NODE_ID); +} + +static String strgen(int length) { + String randomString = ""; + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (int i = 0; i < length; i++) { + int index = random(characters.length()); + char nextChar = characters.charAt(index); + randomString += nextChar; + } + return randomString; +} \ No newline at end of file