diff --git a/README.md b/README.md new file mode 100644 index 0000000..9693edf --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Janitza Power Meter + +Code for reading the Janitza UMG 96S via Modbus and pushing the values into a InfluxDB. \ No newline at end of file diff --git a/janitza-bhb-umg96s-24v-en.pdf b/janitza-bhb-umg96s-24v-en.pdf new file mode 100644 index 0000000..217466f Binary files /dev/null and b/janitza-bhb-umg96s-24v-en.pdf differ diff --git a/software/platformio.ini b/software/platformio.ini index 9b8e90b..aef32e3 100644 --- a/software/platformio.ini +++ b/software/platformio.ini @@ -17,5 +17,5 @@ lib_deps = ModbusMaster@2.0.1 Ethernet@2.0.0 build_flags = - -D ENABLE_HWSERIAL2 - ; -Wl,-u,vfprintf -lprintf_flt -lm ; enable float printf \ No newline at end of file + -D ENABLE_HWSERIAL3 + -Wl,-u_printf_float ; enable float printf \ No newline at end of file diff --git a/software/src/globals.h b/software/src/globals.h index 4d6f42d..bd655aa 100644 --- a/software/src/globals.h +++ b/software/src/globals.h @@ -8,10 +8,12 @@ #define UPDATE_INTERVAL 5000 // ms #define CT_RATIO (40/5) // calculated current transformer ratio (e.g. 40:5 -> 8) -#define MODUBS_SERIAL Serial2 +#define MODUBS_SERIAL Serial3 #define MODBUS_BAUD 38400 #define MODBUS_ADDR 1 + + #define SPI2_MOSI PB15 #define SPI2_MISO PB14 #define SPI2_SCK PB13 diff --git a/software/src/janitza.h b/software/src/janitza.h index 16a2bc7..64cf96c 100644 --- a/software/src/janitza.h +++ b/software/src/janitza.h @@ -5,6 +5,7 @@ typedef struct { uint8_t multiplier; uint8_t applyCtRatio : 1; uint8_t type : 2; + uint8_t phaseTag : 3; const char *influxStr; } registerDefinition_t; @@ -14,40 +15,317 @@ enum registerDataTypes { LONG, }; +enum phaseTag { + P_ALL, + P_L1, + P_L2, + P_L3, + P_L1L2, + P_L2L3, + P_L3L1, + P_TAG_NUM // length of enum +}; + +const char *phaseTagStr[] = { + "All", + "L1", + "L2", + "L3", + "L1-L2", + "L2-L3", + "L3-L1" +}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +#define EMPTY {0, 1, false, INT, P_ALL, NULL} +#define LONG_SPACER EMPTY + + +// TODO: fix addresses (currently not needed) registerDefinition_t registerDefinition[] = { - //addr multipl CTratio type name - { 200, 10, false, INT, "ULN1" }, - { 201, 10, false, INT, "ULN2" }, - { 202, 10, false, INT, "ULN3" }, - { 203, 10, false, INT, "ULL12" }, // L1-L2 - { 204, 10, false, INT, "ULL23" }, - { 205, 10, false, INT, "ULL31" }, - { 206, 1, true, INT, "IL1" }, - { 207, 1, true, INT, "IL2" }, - { 208, 1, true, INT, "IL3" }, - { 209, 10, true, INT, "PL1" }, // real power - { 210, 10, true, INT, "PL2" }, - { 211, 10, true, INT, "PL3" }, - { 212, 10, true, INT, "QL1" }, // reactive power - { 213, 10, true, INT, "QL2" }, - { 214, 10, true, INT, "QL3" }, - { 215, 10, true, INT, "SL1" }, // apparent power - { 216, 10, true, INT, "SL2" }, - { 217, 10, true, INT, "SL3" }, - { 218, 100, false, INT, "CosPhiL1" }, - { 219, 100, false, INT, "CosPhiL2" }, - { 220, 100, false, INT, "CosPhiL3" }, - { 221, 10, false, INT, "ULN1H1" }, - { 221, 10, false, INT, "ULN1H3" }, - { 221, 10, false, INT, "ULN1H5" }, - { 221, 10, false, INT, "ULN1H7" }, - { 221, 10, false, INT, "ULN1H9" }, - { 221, 10, false, INT, "ULN1H11" }, - { 221, 10, false, INT, "ULN1H13" }, - { 221, 10, false, INT, "ULN1H15" }, - // ... + //addr multipl CTratio type phase name + { 200, 10, false, INT, P_L1, "U_LN" }, + { 201, 10, false, INT, P_L2, "U_LN" }, + { 202, 10, false, INT, P_L3, "U_LN" }, + { 203, 10, false, INT, P_L1L2, "U_LL" }, // L1-L2 + { 204, 10, false, INT, P_L2L3, "U_LL" }, + { 205, 10, false, INT, P_L3L1, "U_LL" }, + { 206, 1, true, INT, P_L1, "I" }, + { 207, 1, true, INT, P_L2, "I" }, + { 208, 1, true, INT, P_L3, "I" }, + { 209, 10, true, INT, P_L1, "P" }, // real power + { 210, 10, true, INT, P_L2, "P" }, + { 211, 10, true, INT, P_L3, "P" }, + { 212, 10, true, INT, P_L1, "Q" }, // reactive power + { 213, 10, true, INT, P_L2, "Q" }, + { 214, 10, true, INT, P_L3, "Q" }, + { 215, 10, true, INT, P_L1, "S" }, // apparent power + { 216, 10, true, INT, P_L2, "S" }, + { 217, 10, true, INT, P_L3, "S" }, + { 218, 100, false, INT, P_L1, "CosPhi" }, + { 219, 100, false, INT, P_L2, "CosPhi" }, + { 220, 100, false, INT, P_L3, "CosPhi" }, + { 1, 10, false, INT, P_L1, "U_LN_Harm1" }, + { 1, 10, false, INT, P_L1, "U_LN_Harm3" }, + { 1, 10, false, INT, P_L1, "U_LN_Harm5" }, + { 1, 10, false, INT, P_L1, "U_LN_Harm7" }, + { 1, 10, false, INT, P_L1, "U_LN_Harm9" }, + { 1, 10, false, INT, P_L1, "U_LN_Harm11" }, + { 1, 10, false, INT, P_L1, "U_LN_Harm13" }, + { 1, 10, false, INT, P_L1, "U_LN_Harm15" }, + { 1, 10, false, INT, P_L2, "U_LN_Harm1" }, + { 1, 10, false, INT, P_L2, "U_LN_Harm3" }, + { 1, 10, false, INT, P_L2, "U_LN_Harm5" }, + { 1, 10, false, INT, P_L2, "U_LN_Harm7" }, + { 1, 10, false, INT, P_L2, "U_LN_Harm9" }, + { 1, 10, false, INT, P_L2, "U_LN_Harm11" }, + { 1, 10, false, INT, P_L2, "U_LN_Harm13" }, + { 1, 10, false, INT, P_L2, "U_LN_Harm15" }, + { 1, 10, false, INT, P_L3, "U_LN_Harm1" }, + { 1, 10, false, INT, P_L3, "U_LN_Harm3" }, + { 1, 10, false, INT, P_L3, "U_LN_Harm5" }, + { 1, 10, false, INT, P_L3, "U_LN_Harm7" }, + { 1, 10, false, INT, P_L3, "U_LN_Harm9" }, + { 1, 10, false, INT, P_L3, "U_LN_Harm11" }, + { 1, 10, false, INT, P_L3, "U_LN_Harm13" }, + { 1, 10, false, INT, P_L3, "U_LN_Harm15" }, + { 1, 1, false, INT, P_L1, "I_Harm1" }, + { 1, 1, false, INT, P_L1, "I_Harm3" }, + { 1, 1, false, INT, P_L1, "I_Harm5" }, + { 1, 1, false, INT, P_L1, "I_Harm7" }, + { 1, 1, false, INT, P_L1, "I_Harm9" }, + { 1, 1, false, INT, P_L1, "I_Harm11" }, + { 1, 1, false, INT, P_L1, "I_Harm13" }, + { 1, 1, false, INT, P_L1, "I_Harm15" }, + { 1, 1, false, INT, P_L2, "I_Harm1" }, + { 1, 1, false, INT, P_L2, "I_Harm3" }, + { 1, 1, false, INT, P_L2, "I_Harm5" }, + { 1, 1, false, INT, P_L2, "I_Harm7" }, + { 1, 1, false, INT, P_L2, "I_Harm9" }, + { 1, 1, false, INT, P_L2, "I_Harm11" }, + { 1, 1, false, INT, P_L2, "I_Harm13" }, + { 1, 1, false, INT, P_L2, "I_Harm15" }, + { 1, 1, false, INT, P_L3, "I_Harm1" }, + { 1, 1, false, INT, P_L3, "I_Harm3" }, + { 1, 1, false, INT, P_L3, "I_Harm5" }, + { 1, 1, false, INT, P_L3, "I_Harm7" }, + { 1, 1, false, INT, P_L3, "I_Harm9" }, + { 1, 1, false, INT, P_L3, "I_Harm11" }, + { 1, 1, false, INT, P_L3, "I_Harm13" }, + { 1, 1, false, INT, P_L3, "I_Harm15" }, + { 1, 10, false, INT, P_L1, "THD_U" }, + { 1, 10, false, INT, P_L2, "THD_U" }, + { 1, 10, false, INT, P_L3, "THD_U" }, + { 1, 10, false, INT, P_L1, "THD_I" }, + { 1, 10, false, INT, P_L2, "THD_I" }, + { 1, 10, false, INT, P_L3, "THD_I" }, + { 1, 100, false, INT, P_L1, "F" }, + { 1, 100, false, INT, P_ALL, "CosPhi" }, + { 1, 1, false, INT, P_ALL, "Rotation" }, + { 1, 1, false, INT, P_ALL, "I_N" }, + { 1, 1, false, INT, P_ALL, "P" }, + { 1, 1, false, INT, P_ALL, "Q" }, + { 1, 1, false, INT, P_ALL, "S" }, + EMPTY, // { 0, 1, true, INT, P_L1, "I_Mean" }, + EMPTY, // { 0, 1, true, INT, P_L2, "I_Mean" }, + EMPTY, // { 0, 1, true, INT, P_L3, "I_Mean" }, + EMPTY, // { 0, 10, true, INT, P_L1, "P_Mean" }, // real power + EMPTY, // { 0, 10, true, INT, P_L2, "P_Mean" }, + EMPTY, // { 0, 10, true, INT, P_L3, "P_Mean" }, + EMPTY, // { 0, 10, true, INT, P_L1, "Q_Mean" }, // reactive power + EMPTY, // { 0, 10, true, INT, P_L2, "Q_Mean" }, + EMPTY, // { 0, 10, true, INT, P_L3, "Q_Mean" }, + EMPTY, // { 0, 10, true, INT, P_L1, "S_Mean" }, // apparent power + EMPTY, // { 0, 10, true, INT, P_L2, "S_Mean" }, + EMPTY, // { 0, 10, true, INT, P_L3, "S_Mean" }, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + { 1, 60, false, LONG, P_ALL, "OperatingTime" }, + LONG_SPACER, + EMPTY, + LONG_SPACER, + EMPTY, + LONG_SPACER, + EMPTY, + LONG_SPACER, + EMPTY, + LONG_SPACER, + EMPTY, + LONG_SPACER, + EMPTY, + LONG_SPACER, + { 1, 1, false, INT, P_ALL, "Temperature" }, + { 1, 100, false, INT, P_ALL, "U_Operating" }, + EMPTY, + LONG_SPACER, + EMPTY, + EMPTY, + EMPTY, + EMPTY, + { 1, 1, false, LONG, P_ALL, "Wp" }, // Wh + LONG_SPACER, + { 1, 1, false, LONG, P_ALL, "Wq" }, + LONG_SPACER, + EMPTY, + EMPTY, + { 1, 1, false, LONG, P_ALL, "Wp_Import" }, // Wh + LONG_SPACER, + { 1, 1, false, LONG, P_ALL, "Wp_Supply" }, // Wh + LONG_SPACER, }; #define REG_DEF_NUM (sizeof(registerDefinition) / sizeof(registerDefinition[0])) #define REG_DEF_START_ADDR 200 + #define REG_SERIAL 911 \ No newline at end of file diff --git a/software/src/main.cpp b/software/src/main.cpp index 522ba57..8f4aa4a 100644 --- a/software/src/main.cpp +++ b/software/src/main.cpp @@ -12,30 +12,48 @@ ModbusMaster mb; uint32_t _serialNumber; EthernetClient client; -void sendInfluxRequest(char *lineProtocolCommand) { - if (client.connect(INFLUX_HOST, INFLUX_PORT)) { - client.printf("POST /write?db=%s HTTP/1.1\n", INFLUX_DB); - client.printf("Host: %s\n", INFLUX_HOST); - client.printf("Authorization: Basic %s\n", INFLUX_AUTH_BASE64); - client.printf("Content-Length: %d\n", strlen(lineProtocolCommand)); - client.printf("Connection: close\n"); - client.printf("\n"); - client.write(lineProtocolCommand); - client.printf("\n"); +void handleHttpResponse() { + // print response from HTTP request + if (client.available()) { + int len = client.available(); + uint8_t buf[256]; + if (len > 256) { + len = 256; + } + client.read(buf, len); + + DEBUG.write(buf, len); } - else { - DEBUG.printf("Connection to InfluxDB failed\n"); +} + +void sendInfluxRequest(char *lineProtocolCommand) { + if (!client.connected()) { + if (!client.connect(INFLUX_HOST, INFLUX_PORT)) { + DEBUG.printf("Connection to InfluxDB failed\n"); + return; + } } + client.printf("POST /write?db=%s HTTP/1.1\n", INFLUX_DB); + client.printf("Host: %s\n", INFLUX_HOST); + client.printf("Authorization: Basic %s\n", INFLUX_AUTH_BASE64); + client.printf("Content-Length: %d\n", strlen(lineProtocolCommand)); + client.printf("Connection: close\n"); + client.printf("\n"); + client.write(lineProtocolCommand); + client.printf("\n"); + client.flush(); + while (client.connected()) { + handleHttpResponse(); + } } uint32_t readSerialNumber() { - uint8_t ret = mb.readInputRegisters(REG_SERIAL, 2); + uint8_t ret = mb.readHoldingRegisters(REG_SERIAL, 2); if (ret != mb.ku8MBSuccess) { DEBUG.printf("Error reading from ModBus. Status = %02X\n", ret); return 0; } - // TODO: check endianness return mb.getResponseBuffer(0) << 16 | mb.getResponseBuffer(1); } @@ -49,58 +67,89 @@ registerDefinition_t *getRegDef(uint16_t address) { } } -void readMeterChunk(uint16_t startAddr, uint8_t count) { - uint8_t ret = mb.readInputRegisters(startAddr, count); +#define MB_CHUNK_SIZE 64 - if (ret != mb.ku8MBSuccess) { - DEBUG.printf("Error reading from ModBus. Status = %02X\n", ret); +bool modbusReadBulk(int16_t *destBuf, uint16_t startAddr, uint16_t count) { + int iterations = (count + (MB_CHUNK_SIZE - 1)) / MB_CHUNK_SIZE; // division, but rounded up + for (int i = 0; i < iterations; i++) { + uint16_t mbStart = startAddr + i * MB_CHUNK_SIZE; + uint8_t mbCount = MB_CHUNK_SIZE; + if (MB_CHUNK_SIZE * (i + 1) > count) { + mbCount = count % MB_CHUNK_SIZE; + } + + DEBUG.printf("Reading Modbus Addr: %d, Count: %d\n", mbStart, mbCount); + + uint8_t ret = mb.readHoldingRegisters(mbStart, mbCount); + + if (ret != mb.ku8MBSuccess) { + DEBUG.printf("Error reading from ModBus. Status = %02X\n", ret); + return false; + } + + for (int j = 0; j < mbCount; j++) { + destBuf[i * MB_CHUNK_SIZE + j] = mb.getResponseBuffer(j); + } + } + return true; + +} + +int16_t modbusBuf[REG_DEF_NUM]; + +void readMeter() { + // readMeterChunk(200, 64); + bool success = modbusReadBulk(modbusBuf, 200, 226); + + if (!success) { + DEBUG.printf("Error reading bulk from ModBus\n"); return; } - char influxQueryBuf[2048]; - int influxQueryLen = 0; - influxQueryLen += sprintf(influxQueryBuf + influxQueryLen, "%s", INFLUX_MEASUREMENT); // measurement name - influxQueryLen += sprintf(influxQueryBuf + influxQueryLen, ",serial=%d ", _serialNumber); // print tags here + char influxQueryBuf[512]; + + for (int phase = 0; phase < P_TAG_NUM; phase++) { + const char* phaseStr = phaseTagStr[phase]; + + int influxQueryLen = 0; + influxQueryLen += sprintf(influxQueryBuf + influxQueryLen, "%s", INFLUX_MEASUREMENT); // measurement name + influxQueryLen += sprintf(influxQueryBuf + influxQueryLen, ",serial=%d,phase=%s ", _serialNumber, phaseStr); // print tags here - for (int i = 0; i < count; i++) { - uint16_t addr = startAddr + i; - int32_t mbValue = (int16_t)mb.getResponseBuffer(i); // get value and handle as signed integer - registerDefinition_t *regDef = getRegDef(addr); - - if (regDef) { - if (regDef->type == LONG) { - // TODO: check endianness - uint16_t lowWord = mb.getResponseBuffer(i + 1); - mbValue = mbValue << 16 | lowWord; - i++; // increment counter / skip next address + for (int i = 0; i < REG_DEF_NUM; i++) { + registerDefinition_t *regDef = ®isterDefinition[i]; + + if (regDef->address == 0) { // skip "disabled" addresses + continue; } - float val = (float)mbValue / (float)regDef->multiplier; + if (regDef->phaseTag == phase) { + int32_t mbValue = modbusBuf[i]; + if (regDef->type == LONG) { + mbValue = mbValue << 16 | (modbusBuf[i + 1] & 0xFFFF); + i++; // increment counter / skip next address + } - // apply current transformer ratio if needed - if (regDef->applyCtRatio) { - val *= (float)CT_RATIO; - } + float val = (float)mbValue / (float)regDef->multiplier; - // TODO: potentially need to enable printf float support - printf("%3d %s = %f\n", addr, regDef->influxStr, val); - influxQueryLen += sprintf(influxQueryBuf + influxQueryLen, "%s=%f,", regDef->influxStr, val); - } - else { - // will be just ignored later on - printf("%3d unknown = %d\n", addr, mbValue); - } - } + // apply current transformer ratio if needed + if (regDef->applyCtRatio) { + val *= (float)CT_RATIO; + } - influxQueryBuf[influxQueryLen - 1] = 0; // clear trailing comma + influxQueryLen += sprintf(influxQueryBuf + influxQueryLen, "%s=%g,", regDef->influxStr, val); + } + } - DEBUG.write(influxQueryBuf); + influxQueryBuf[influxQueryLen - 1] = 0; // clear trailing comma -} + DEBUG.write(influxQueryBuf); + DEBUG.println(); + + sendInfluxRequest(influxQueryBuf); + handleHttpResponse(); + } -void readMeter() { - readMeterChunk(200, 64); } void setup() { @@ -112,29 +161,20 @@ void setup() { _serialNumber = readSerialNumber(); DEBUG.printf("Serial: %d\n", _serialNumber); - readMeter(); + // readMeter(); initEthernet(); connectEthernet(); + + readMeter(); } uint32_t lastUpdate = 0; void loop() { if (millis() - lastUpdate >= UPDATE_INTERVAL) { - lastUpdate = UPDATE_INTERVAL; - // readMeter(); - } - - // print response from HTTP request - if (client.available()) { - int len = client.available(); - uint8_t buf[256]; - if (len > 256) { - len = 256; - } - client.read(buf, len); - - DEBUG.write(buf, len); + lastUpdate = millis(); + readMeter(); } + } \ No newline at end of file