This commit is contained in:
E. S 2025-06-22 18:28:29 +03:00
commit 66672c7808
10 changed files with 632 additions and 0 deletions

6
.gitignore vendored Normal file
View File

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

28
include/common.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef JANITZA104_ESP32_COMMON_H_
#define JANITZA104_ESP32_COMMON_H_
#include <stdlib.h>
#include <Arduino.h>
#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_

35
include/janitza.h Normal file
View File

@ -0,0 +1,35 @@
#ifndef JANITZA104_ESP32_JANITZA_H_
#define JANITZA104_ESP32_JANITZA_H_
#include <ModbusMaster.h>
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_

26
include/led.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef JANITZA104_ESP32_LED_H_
#define JANITZA104_ESP32_LED_H_
#include <Arduino.h>
#include <stdint.h>
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_

71
mqtt_monitor.sh Executable file
View File

@ -0,0 +1,71 @@
#!/bin/sh
set -e
usage() {
echo "usage: $0 [-h|--help] [-t <topic>]" >&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

35
platformio.ini Normal file
View File

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

22
src/common.cpp Normal file
View File

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

91
src/janitza.cpp Normal file
View File

@ -0,0 +1,91 @@
#include <cstdint>
#include <cstring>
#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;
}

18
src/led.cpp Normal file
View File

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

300
src/main.cpp Normal file
View File

@ -0,0 +1,300 @@
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
#include <cstring>
#include <time.h>
#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<uint32_t>(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;
}