diff --git a/README.md b/README.md index 8043da2..9964cc0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,79 @@ # esp-multical21 -ESP8266 decrypts wireless MBus frames from a Multical21 water meter +ESP8266/ESP32 decrypts wireless MBus frames from a Multical21 water meter. -A CC1101 868 MHz modul is connected via SPI to the ESP8266 an configured to receive Wireless MBus frames. -The Multical21 is sending every 16 seconds wireless MBus frames (Mode C1, frame type B). The encrypted -frames are received from the ESP8266 an it decrypts them with AES-128-CTR. The meter information -(total counter, target counter, medium temperature, ambient temperature, alalm flags (BURST, LEAK, DRY, -REVERSE) are sent via MQTT to a smarthomeNG/smartVISU service (running on a raspberry). +Multical21 +A CC1101 868 MHz module is connected via SPI to the ESP8266/ESP32. +The Multical21 is transmitting encrypted MBus frames (Mode C1, frame type B) every 16 seconds. +The ESP8266/ESP32 does some validation (right serial number, crc checking) and then +decrypts them with AES-128-CTR. + +The serial number (8 digits) is printed on the water meter (above the LCD). +Ask your water supplier for the decryption key (16 bytes). I got mine packed in a so called +KEM-file. To extract the key i used a python script [kem-decryptor.py](https://gist.github.com/mbursa/caa654a01b9e804ad44d1f00208a2490) + + +The Multical21 provides the following meter values: + + +The ESP8266/ESP32 prints out the current meter values via UART (baudrate: 115200). +The UART output looks something like this: +``` +total: 1636.265 m³ - target: 1624.252 m³ - 13 °C - 22 °C - 0x00 +``` +Additionally the values are sent via MQTT to a given broker. + +Rename [config_template.h](include/config_template.h) to _**config.h**_ and fill in some information. + +Provide your water meter serial number and decryption key. + +``` +// ask your water supplier for your personal encryption key +#define ENCRYPTION_KEY 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF +// serial number is printed on your multical21 +#define SERIAL_NUMBER 0x63, 0x00, 0x05, 0x43 +``` + +Add your Wifi credentials (ssid, password). Add your MQTT broker ip address. If your +broker uses authentication add MQTT username/password. + +``` +// more than one wifi credentials are supported, upper one wins +// "ssid", "wifi_passphrase", "mqtt_broker", "mqtt_username", "mqtt_password" +std::vector const credentials = { + { "ssid1", "********", "", "", ""} // no MQTT + , { "ssid2", "********", "10.14.0.1", "", ""} // MQTT without auth + , { "ssid3", "********", "10.0.0.111", "mqttuser", "mqtt1234"} // MQTT with auth +}; +``` + +Change the MQTT prefix and the topic names as you like. Currently the water counter value +is published in **watermeter/0/total** +``` +#define MQTT_PREFIX "watermeter/0" +#define MQTT_total "/total" +#define MQTT_target "/target" +#define MQTT_ftemp "/flowtemp" +#define MQTT_atemp "/ambienttemp" +#define MQTT_info "/infocode" +``` + + +Connect your ESP8266/ESP32 to the CC1101 868Mhz module: +| CC1101 | ESP8266 | ESP32 | +|--------|:-------:|:-----:| +| VCC | 3V3 | 3V3 | +| GND | GND | GND | +| CSN | D8 | 4 | +| MOSI | D7 | 23 | +| MISO | D6 | 19 | +| SCK | D5 | 18 | +| GD0 | D2 | 32 | +| GD2 | not connected| not connected| Thanks to [weetmuts](https://github.com/weetmuts) for his great job on the wmbusmeters. diff --git a/include/WMbusFrame.h b/include/WMbusFrame.h deleted file mode 100644 index e59d8da..0000000 --- a/include/WMbusFrame.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef __WMBUS_FRAME__ -#define __WMBUS_FRAME__ - -#include -#include -#include -#include -#include "credentials.h" - -class WMBusFrame -{ - public: - static const uint8_t MAX_LENGTH = 64; - private: - CTR aes128; - const uint8_t meterId[4] = { W_SERIAL_NUMBER }; // Multical21 serial number - const uint8_t key[16] = { W_ENCRYPTION_KEY }; // AES-128 key - uint8_t cipher[MAX_LENGTH]; - uint8_t plaintext[MAX_LENGTH]; - uint8_t iv[16]; - void check(void); - void printMeterInfo(uint8_t *data, size_t len); - - public: - // check frame and decrypt it - void decode(void); - - // true, if meter information is valid for the last received frame - bool isValid = false; - - // payload length - uint8_t length = 0; - - // payload data - uint8_t payload[MAX_LENGTH]; - - // constructor - WMBusFrame(); -}; - -#endif // __WMBUS_FRAME__ diff --git a/include/WaterMeter.h b/include/WaterMeter.h index 60821e2..9c32d98 100644 --- a/include/WaterMeter.h +++ b/include/WaterMeter.h @@ -17,7 +17,12 @@ #include #include -#include "WMBusFrame.h" +#include +#include +#include +#include +#include "config.h" +#include "utils.h" #define MARCSTATE_SLEEP 0x00 #define MARCSTATE_IDLE 0x01 @@ -179,50 +184,72 @@ class WaterMeter { private: + const uint32_t RECEIVE_TIMEOUT = 300000UL; // in millis + const uint32_t PACKET_TIMEOUT = 180000UL; // in seconds + uint32_t lastPacketDecoded = -PACKET_TIMEOUT; + uint32_t lastFrameReceived = 0; + volatile boolean packetAvailable = false; + uint8_t meterId[4]; + uint8_t aesKey[16]; inline void selectCC1101(void); inline void deselectCC1101(void); inline void waitMiso(void); + static const uint8_t MAX_LENGTH = 64; + CTR aes128; + uint8_t cipher[MAX_LENGTH]; + uint8_t plaintext[MAX_LENGTH]; + uint8_t iv[16]; + bool isValid = false; // true, if meter information is valid for the last received frame + uint8_t length = 0; // payload length + uint8_t payload[MAX_LENGTH]; // payload data + uint32_t totalWater; + uint32_t targetWater; + uint32_t lastTarget=0; + uint8_t flowTemp; + uint8_t ambientTemp; + uint8_t infoCodes; + + PubSubClient &mqttClient; + bool mqttEnabled; + + // reset HW and restart receiver + void restartRadio(void); // flush fifo and (re)start receiver void startReceiver(void); - // burst write registers of cc1101 - void writeBurstReg(uint8_t regaddr, uint8_t* buffer, uint8_t len); - - // burst read registers of cc1101 + //void writeBurstReg(uint8_t regaddr, uint8_t* buffer, uint8_t len); void readBurstReg(uint8_t * buffer, uint8_t regaddr, uint8_t len); - - // strobe command void cmdStrobe(uint8_t cmd); - - // read a register of cc1101 uint8_t readReg(uint8_t regaddr, uint8_t regtype); - - // read a byte from fifo uint8_t readByteFromFifo(void); - - // write a register of cc1101 void writeReg(uint8_t regaddr, uint8_t value); - - // initialize cc1101 registers void initializeRegisters(void); - - // reset cc1101 void reset(void); + // static ISR calls instanceISR via this pointer + IRAM_ATTR static void cc1101Isr(void *p); + // receive a wmbus frame - void receive(WMBusFrame *payload); + void receive(void); // read frame from CC1101 + bool checkFrame(void); // check id, CRC + void getMeterInfo(uint8_t *data, size_t len); + void publishMeterInfo(); public: // constructor - WaterMeter(void); + WaterMeter(PubSubClient &mqtt); + + void enableMqtt(bool enable); // startup CC1101 for receiving wmbus mode c - void begin(); + void begin(uint8_t *key, uint8_t *id); - // must be called frequently, returns true if a valid frame was received - bool isFrameAvailable(void); + // must be called frequently + void loop(void); + + IRAM_ATTR void instanceCC1101Isr(); }; -#endif // _WATERMETER_H_ \ No newline at end of file +#endif // _WATERMETER_H_ diff --git a/include/config_template.h b/include/config_template.h new file mode 100644 index 0000000..87cbacc --- /dev/null +++ b/include/config_template.h @@ -0,0 +1,67 @@ +#ifndef __CONFIG_H__ +#define __CONFIG_H__ + +#include + +#define DEBUG 1 // set to 1 for detailed logging at UART +#define ESP_NAME "esp-multical21" // for dns resolving +#define MQTT_PREFIX "watermeter/0" // mqtt prefix topic +#define MQTT_total "/total" +#define MQTT_target "/target" +#define MQTT_ftemp "/flowtemp" +#define MQTT_atemp "/ambienttemp" +#define MQTT_info "/infocode" + +// ask your water supplier for your personal encryption key +#define ENCRYPTION_KEY 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF +// serial number is printed on your multical21 +#define SERIAL_NUMBER 0x12, 0x34, 0x56, 0x78 + +// WIFI configuration, supports more than one WIFI, first found first served +// if you dont use MQTT, leave broker/user/pass empty ("") +// if you dont need user/pass for MQTT, leave it empty ("") +struct CREDENTIAL { + char const* ssid; // Wifi ssid + char const* password; // Wifi password + char const* mqtt_broker; // MQTT broker ip address + char const* mqtt_username; // MQTT username + char const* mqtt_password; // MQTT password +}; + +// more than one wifi credentials are supported, upper one wins +// "ssid", "wifi_passphrase", "mqtt_broker", "mqtt_username", "mqtt_password" +std::vector const credentials = { + { "ssid1", "********", "", "", ""} // no MQTT + , { "ssid2", "********", "10.14.0.1", "", ""} // MQTT without auth + , { "ssid3", "********", "10.0.0.111", "mqttuser", "mqtt1234"} // MQTT with auth +}; + +#if defined(ESP8266) +// Attach CC1101 pins to ESP8266 SPI pins +// VCC => 3V3 +// GND => GND +// CSN => D8 +// MOSI => D7 +// MISO => D6 +// SCK => D5 +// GD0 => D2 A valid interrupt pin for your platform (defined below this) +// GD2 => not connected + #define CC1101_GDO0 D2 // GDO0 input interrupt pin + #define PIN_LED_BUILTIN D4 +#elif defined(ESP32) +// Attach CC1101 pins to ESP32 SPI pins +// VCC => 3V3 +// GND => GND +// CSN => 4 +// MOSI => 23 +// MISO => 19 +// SCK => 18 +// GD0 => 32 any valid interrupt pin for your platform will do +// GD2 => not connected + +// attach CC1101 pins to ESP32 SPI pins + + #define CC1101_GDO0 32 + #define PIN_LED_BUILTIN 2 +#endif +#endif // __CONFIG_H__ \ No newline at end of file diff --git a/include/credentials_template.h b/include/credentials_template.h new file mode 100644 index 0000000..08f1eeb --- /dev/null +++ b/include/credentials_template.h @@ -0,0 +1,31 @@ +#ifndef __CREDENTIALS_H__ +#define __CREDENTIALS_H__ + +#include + +// ask your supplier for your personal encryption key (16 bytes) +#define ENCRYPTION_KEY 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF +// serial number is printed on your multical21 +#define SERIAL_NUMBER 0x12, 0x34, 0x56, 0x78 + +// WIFI configuration, supports more than one WIFI, first found first served +// if you dont use MQTT, leave it empty ("") +// if you dont need user/pass for MQTT, leave it empty ("") +struct CREDENTIAL { + char const* ssid; + char const* password; + char const* mqtt_broker; // MQTT broker + char const* mqtt_username; // MQTT username + char const* mqtt_password; // MQTT password +}; + +CREDENTIAL currentWifi; // global to store found wifi + +// "ssid", "wifi_passphrase", "mqtt_broker", "mqtt_username", "mqtt_password" +std::vector const credentials = { + { "ssid1", "********", "", "", ""} // no MQTT - just serial output + , { "ssid3", "********", "10.0.0.11", "", ""} // MQTT without authentication + , { "ssid2", "********", "10.0.0.10", "loxone", "loxy1234"} // MQTT with user/pass +}; + +#endif // __CREDENTIALS_H__ \ No newline at end of file diff --git a/include/hwconfig.h b/include/hwconfig.h deleted file mode 100644 index dc97e52..0000000 --- a/include/hwconfig.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef __HWCONFIG_H__ -#define __HWCONFIG_H__ - -#if defined(ESP8266) -// Attach CC1101 pins to ESP8266 SPI pins -// VCC => 3V3 -// GND => GND -// CSN => D8 -// MOSI => D7 -// MISO => D6 -// SCK => D5 -// GD0 => D2 A valid interrupt pin for your platform (defined below this) -// GD2 => not connected - #define CC1101_GDO0 D2 // GDO0 input interrupt pin - #define PIN_LED_BUILTIN D4 -#elif defined(ESP32) -// Attach CC1101 pins to ESP32 SPI pins -// VCC => 3V3 -// GND => GND -// CSN => 4 -// MOSI => 23 -// MISO => 19 -// SCK => 18 -// GD0 => 32 any valid interrupt pin for your platform will do -// GD2 => not connected - -// attach CC1101 pins to ESP32 SPI pins - - #define CC1101_GDO0 32 - #define PIN_LED_BUILTIN 2 -#endif - -#endif //__HWCONFIG_H__ \ No newline at end of file diff --git a/include/utils.h b/include/utils.h new file mode 100644 index 0000000..18acc99 --- /dev/null +++ b/include/utils.h @@ -0,0 +1,15 @@ +#ifndef __UTILS_H__ +#define __UTILS_H__ + +#include +#include + +void printHex(uint8_t * buf, size_t len); + +uint16_t crcEN13575(uint8_t *payload, uint16_t length); +uint16_t mirror(uint16_t crc, uint8_t bitnum); +uint16_t crcInternal(uint8_t *p, uint16_t len, uint16_t poly, uint16_t init, bool revIn, bool revOut); +void bin2hex(char *xp, uint8_t *bb, int n); +void hex2bin(const char *in, size_t len, uint8_t *out); + +#endif //__UTILS_H__ \ No newline at end of file diff --git a/multical21.png b/multical21.png new file mode 100644 index 0000000..e6a6052 Binary files /dev/null and b/multical21.png differ diff --git a/platformio.ini b/platformio.ini index aadf99b..cf7ac8f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,8 +9,8 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -;default_envs = esp8266-test -default_envs = esp32, esp8266 +default_envs = esp8266 +;default_envs = esp32, esp8266 [env:esp32] framework = arduino @@ -25,6 +25,7 @@ platform = espressif8266 board = d1_mini_lite board_build.mcu = esp8266 lib_deps = rweather/Crypto @ ^0.2.0 +monitor_speed=115200 ; OTA -;upload_port = 10.0.0.86 -;upload_protocol = espota \ No newline at end of file +upload_port = 10.0.0.131 +upload_protocol = espota \ No newline at end of file diff --git a/src/WMBusFrame.cpp b/src/WMBusFrame.cpp deleted file mode 100644 index c3c484d..0000000 --- a/src/WMBusFrame.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/* - Copyright (C) 2020 chester4444@wolke7.net - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include "WMbusFrame.h" - -WMBusFrame::WMBusFrame() -{ - aes128.setKey(key, sizeof(key)); -} - -void WMBusFrame::check() -{ - // check meterId - for (uint8_t i = 0; i< 4; i++) - { - if (meterId[i] != payload[6-i]) - { - isValid = false; - return; - } - } - - // TBD: check crc - isValid = true; -} - -void WMBusFrame::printMeterInfo(uint8_t *data, size_t len) -{ - // init positions for compact frame - int pos_tt = 9; // total consumption - int pos_tg = 13; // target consumption - int pos_ic = 7; // info codes - int pos_ft = 17; // flow temp - int pos_at = 18; // ambient temp - - if (data[2] == 0x78) // long frame - { - // overwrite it with long frame positions - pos_tt = 10; - pos_tg = 16; - pos_ic = 6; - pos_ft = 22; - pos_at = 25; - } - - char total[10]; - uint32_t tt = data[pos_tt] - + (data[pos_tt+1] << 8) - + (data[pos_tt+2] << 16) - + (data[pos_tt+3] << 24); - snprintf(total, sizeof(total), "%d.%03d", tt/1000, tt%1000 ); - Serial.printf("total: %s m%c - ", total, 179); - - char target[10]; - uint32_t tg = data[pos_tg] - + (data[pos_tg+1] << 8) - + (data[pos_tg+2] << 16) - + (data[pos_tg+3] << 24); - snprintf(target, sizeof(target), "%d.%03d", tg/1000, tg%1000 ); - Serial.printf("target: %s m%c - ", target, 179); - - char flow_temp[3]; - snprintf(flow_temp, sizeof(flow_temp), "%2d", data[pos_ft]); - Serial.printf("%s %cC - ", flow_temp, 176); - - char ambient_temp[3]; - snprintf(ambient_temp, sizeof(ambient_temp), "%2d", data[pos_at]); - Serial.printf("%s %cC\n\r", ambient_temp, 176); -} - -void WMBusFrame::decode() -{ - // check meterId, CRC - check(); - if (!isValid) return; - - uint8_t cipherLength = length - 2 - 16; // cipher starts at index 16, remove 2 crc bytes - memcpy(cipher, &payload[16], cipherLength); - - memset(iv, 0, sizeof(iv)); // padding with 0 - memcpy(iv, &payload[1], 8); - iv[8] = payload[10]; - memcpy(&iv[9], &payload[12], 4); - - aes128.setIV(iv, sizeof(iv)); - aes128.decrypt(plaintext, (const uint8_t *) cipher, cipherLength); - -/* - Serial.printf("C: "); - for (size_t i = 0; i < cipherLength; i++) - { - Serial.printf("%02X", cipher[i]); - } - Serial.println(); - Serial.printf("P(%d): ", cipherLength); - for (size_t i = 0; i < cipherLength; i++) - { - Serial.printf("%02X", plaintext[i]); - } - Serial.println(); -*/ - - printMeterInfo(plaintext, cipherLength); -} \ No newline at end of file diff --git a/src/WaterMeter.cpp b/src/WaterMeter.cpp index 7b1682e..9363124 100644 --- a/src/WaterMeter.cpp +++ b/src/WaterMeter.cpp @@ -13,12 +13,18 @@ */ #include "WaterMeter.h" -#include "hwconfig.h" -WaterMeter::WaterMeter() +WaterMeter::WaterMeter(PubSubClient &mqtt) + : mqttClient (mqtt) + , mqttEnabled (false) { } +void WaterMeter::enableMqtt(bool enabled) +{ + mqttEnabled = enabled; +} + // ChipSelect assert inline void WaterMeter::selectCC1101(void) { @@ -34,28 +40,29 @@ inline void WaterMeter::deselectCC1101(void) // wait for MISO pulling down inline void WaterMeter::waitMiso(void) { - while(digitalRead(MISO) == HIGH); + while (digitalRead(MISO) == HIGH) + ; } // write a single register of CC1101 -void WaterMeter::writeReg(uint8_t regAddr, uint8_t value) +void WaterMeter::writeReg(uint8_t regAddr, uint8_t value) { - selectCC1101(); // Select CC1101 - waitMiso(); // Wait until MISO goes low - SPI.transfer(regAddr); // Send register address - SPI.transfer(value); // Send value - deselectCC1101(); // Deselect CC1101 + selectCC1101(); + waitMiso(); // Wait until MISO goes low + SPI.transfer(regAddr); // Send register address + SPI.transfer(value); // Send value + deselectCC1101(); } // send a strobe command to CC1101 -void WaterMeter::cmdStrobe(uint8_t cmd) +void WaterMeter::cmdStrobe(uint8_t cmd) { - selectCC1101(); // Select CC1101 + selectCC1101(); delayMicroseconds(5); - waitMiso(); // Wait until MISO goes low - SPI.transfer(cmd); // Send strobe command + waitMiso(); // Wait until MISO goes low + SPI.transfer(cmd); // Send strobe command delayMicroseconds(5); - deselectCC1101(); // Deselect CC1101 + deselectCC1101(); } // read CC1101 register (status or configuration) @@ -64,74 +71,86 @@ uint8_t WaterMeter::readReg(uint8_t regAddr, uint8_t regType) uint8_t addr, val; addr = regAddr | regType; - selectCC1101(); // Select CC1101 - waitMiso(); // Wait until MISO goes low - SPI.transfer(addr); // Send register address - val = SPI.transfer(0x00); // Read result - deselectCC1101(); // Deselect CC1101 + selectCC1101(); + waitMiso(); // Wait until MISO goes low + SPI.transfer(addr); // Send register address + val = SPI.transfer(0x00); // Read result + deselectCC1101(); return val; } -// -void WaterMeter::readBurstReg(uint8_t * buffer, uint8_t regAddr, uint8_t len) +// +void WaterMeter::readBurstReg(uint8_t *buffer, uint8_t regAddr, uint8_t len) { uint8_t addr, i; - + addr = regAddr | READ_BURST; - selectCC1101(); // Select CC1101 + selectCC1101(); delayMicroseconds(5); - waitMiso(); // Wait until MISO goes low - SPI.transfer(addr); // Send register address - for(i=0 ; i 100) + { + Serial.println("Enter idle state failed!\n"); + restartRadio(); + } } - - cmdStrobe(CC1101_SFRX); // flush receive queue - cmdStrobe(CC1101_SRX); // Enter RX state - while (readReg(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) != MARCSTATE_RX); + cmdStrobe(CC1101_SFRX); // flush receive queue + delay(5); + + regCount = 0; + cmdStrobe(CC1101_SRX); // Enter RX state + delay(10); + while (readReg(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) != MARCSTATE_RX) { - delay(1); + if (regCount++ > 100) + { + Serial.println("Enter RX state failed!\n"); + restartRadio(); + } } } // initialize all the CC1101 registers -void WaterMeter::initializeRegisters(void) +void WaterMeter::initializeRegisters(void) { writeReg(CC1101_IOCFG2, CC1101_DEFVAL_IOCFG2); writeReg(CC1101_IOCFG0, CC1101_DEFVAL_IOCFG0); @@ -173,56 +192,181 @@ void WaterMeter::initializeRegisters(void) writeReg(CC1101_TEST0, CC1101_DEFVAL_TEST0); } -volatile boolean packetAvailable = false; -void ICACHE_RAM_ATTR GD0_ISR(void); - -// handle interrupt from CC1101 via GDO0 -void GD0_ISR(void) { +IRAM_ATTR void WaterMeter::instanceCC1101Isr() +{ // set the flag that a package is available packetAvailable = true; } +// static ISR method, that calls the right instance +IRAM_ATTR void WaterMeter::cc1101Isr(void *p) +{ + WaterMeter *ptr = (WaterMeter *)p; + ptr->instanceCC1101Isr(); +} + // should be called frequently, handles the ISR flag // does the frame checkin and decryption -bool WaterMeter::isFrameAvailable(void) +void WaterMeter::loop(void) { if (packetAvailable) { - //Serial.println("packet received"); - // Disable wireless reception interrupt + // Serial.println("packet received"); + // Disable wireless reception interrupt detachInterrupt(digitalPinToInterrupt(CC1101_GDO0)); - + // clear the flag packetAvailable = false; - - WMBusFrame frame; - - receive(&frame); + receive(); // Enable wireless reception interrupt - attachInterrupt(digitalPinToInterrupt(CC1101_GDO0), GD0_ISR, FALLING); - return frame.isValid; + attachInterruptArg(digitalPinToInterrupt(CC1101_GDO0), cc1101Isr, this, FALLING); + } + + if (millis() - lastFrameReceived > RECEIVE_TIMEOUT) + { + // workaround: reset CC1101, since it stops receiving from time to time + restartRadio(); } - return false; } -// Initialize CC1101 to receive WMBus MODE C1 -void WaterMeter::begin() +// Initialize CC1101 to receive WMBus MODE C1 +void WaterMeter::begin(uint8_t *key, uint8_t *id) { - pinMode(SS, OUTPUT); // SS Pin -> Output - SPI.begin(); // Initialize SPI interface - pinMode(CC1101_GDO0, INPUT); // Config GDO0 as input + pinMode(SS, OUTPUT); // SS Pin -> Output + SPI.begin(); // Initialize SPI interface + pinMode(CC1101_GDO0, INPUT); // Config GDO0 as input - reset(); // power on CC1101 + memcpy(aesKey, key, sizeof(aesKey)); + aes128.setKey(aesKey, sizeof(aesKey)); - //Serial.println("Setting CC1101 registers"); - initializeRegisters(); // init CC1101 registers + memcpy(meterId, id, sizeof(meterId)); + + restartRadio(); + attachInterruptArg(digitalPinToInterrupt(CC1101_GDO0), cc1101Isr, this, FALLING); + lastFrameReceived = millis(); +} + +void WaterMeter::restartRadio() +{ + Serial.println("resetting CC1101"); + + reset(); // power on CC1101 + + // Serial.println("Setting CC1101 registers"); + initializeRegisters(); // init CC1101 registers cmdStrobe(CC1101_SCAL); delay(1); - attachInterrupt(digitalPinToInterrupt(CC1101_GDO0), GD0_ISR, FALLING); startReceiver(); + lastFrameReceived = millis(); +} + +bool WaterMeter::checkFrame(void) +{ +#if DEBUG + Serial.printf("frame serial ID: "); + for (uint8_t i = 0; i < 4; i++) + { + Serial.printf("%02x", payload[7-i]); + } + Serial.printf(" - %d", length); + Serial.println(); +#endif + + // check meterId + for (uint8_t i = 0; i < 4; i++) + { + if (meterId[i] != payload[7 - i]) + { +#if DEBUG + Serial.println("Meter serial doesnt match!"); +#endif + return false; + } + } + +#if DEBUG + Serial.println("Frame payload:"); + for (uint8_t i = 0; i <= length; i++) + { + Serial.printf("%02x", payload[i]); + } + Serial.println(); +#endif + + uint16_t crc = crcEN13575(payload, length - 1); // -2 (CRC) + 1 (L-field) + if (crc != (payload[length - 1] << 8 | payload[length])) + { + Serial.println("CRC Error"); + Serial.printf("%04x - %02x%02x\n", crc, payload[length - 1], payload[length]); + return false; + } + + return true; +} + +void WaterMeter::getMeterInfo(uint8_t *data, size_t len) +{ + // init positions for compact frame + int pos_tt = 9; // total consumption + int pos_tg = 13; // target consumption + int pos_ic = 7; // info codes + int pos_ft = 17; // flow temp + int pos_at = 18; // ambient temp + + if (data[2] == 0x78) // long frame + { + // overwrite it with long frame positions + pos_tt = 10; + pos_tg = 16; + pos_ic = 6; + pos_ft = 22; + pos_at = 25; + } + + totalWater = data[pos_tt] + (data[pos_tt + 1] << 8) + (data[pos_tt + 2] << 16) + (data[pos_tt + 3] << 24); + + targetWater = data[pos_tg] + (data[pos_tg + 1] << 8) + (data[pos_tg + 2] << 16) + (data[pos_tg + 3] << 24); + + flowTemp = data[pos_ft]; + ambientTemp = data[pos_at]; + infoCodes = data[pos_ic]; +} + +void WaterMeter::publishMeterInfo() +{ + char total[12]; + snprintf(total, sizeof(total), "%d.%03d", totalWater/ 1000, totalWater % 1000); + Serial.printf("total: %s m%c - ", total, 179); + + char target[12]; + snprintf(target, sizeof(target), "%d.%03d", targetWater / 1000, targetWater % 1000); + Serial.printf("target: %s m%c - ", target, 179); + + char flow_temp[4]; + snprintf(flow_temp, sizeof(flow_temp), "%2d", flowTemp); + Serial.printf("%s %cC - ", flow_temp, 176); + + char ambient_temp[4]; + snprintf(ambient_temp, sizeof(ambient_temp), "%2d", ambientTemp); + Serial.printf("%s %cC - ", ambient_temp, 176); + + char info_codes[3]; + snprintf(info_codes, sizeof(info_codes), "%02x", infoCodes); + Serial.printf("0x%s \n\r", info_codes); + + if (!mqttEnabled) return; // no MQTT broker connected, leave + + // change the topics as you like + mqttClient.publish(MQTT_PREFIX MQTT_total, total); + mqttClient.publish(MQTT_PREFIX MQTT_target, target); + mqttClient.loop(); + mqttClient.publish(MQTT_PREFIX MQTT_ftemp, flow_temp); + mqttClient.publish(MQTT_PREFIX MQTT_atemp, ambient_temp); + mqttClient.publish(MQTT_PREFIX MQTT_info, info_codes); + mqttClient.loop(); } // reads a single byte from the RX fifo @@ -232,35 +376,79 @@ uint8_t WaterMeter::readByteFromFifo(void) } // handles a received frame and restart the CC1101 receiver -void WaterMeter::receive(WMBusFrame * frame) +void WaterMeter::receive() { // read preamble, should be 0x543D uint8_t p1 = readByteFromFifo(); uint8_t p2 = readByteFromFifo(); - //Serial.printf("%02x%02x", p1, p2); + +#if DEBUG + Serial.printf("%02x%02x", p1, p2); +#endif - uint8_t payloadLength = readByteFromFifo(); + // get length + payload[0] = readByteFromFifo(); // is it Mode C1, frame B and does it fit in the buffer - if ( (payloadLength < WMBusFrame::MAX_LENGTH ) - && (p1 == 0x54) && (p2 == 0x3D) ) - { + if ((payload[0] < MAX_LENGTH) && (p1 == 0x54) && (p2 == 0x3D)) + { // 3rd byte is payload length - frame->length = payloadLength; + length = payload[0]; - //Serial.printf("%02X", lfield); +#if DEBUG + Serial.printf("%02X", length); +#endif // starting with 1! index 0 is lfield - for (int i = 0; i < payloadLength; i++) + for (int i = 0; i < length; i++) { - frame->payload[i] = readByteFromFifo(); + payload[i + 1] = readByteFromFifo(); } - // do some checks: my meterId, crc ok - frame->decode(); + // check meterId, CRC + if (checkFrame()) + { + uint8_t cipherLength = length - 2 - 16; // cipher starts at index 16, remove 2 crc bytes + memcpy(cipher, &payload[17], cipherLength); + + memset(iv, 0, sizeof(iv)); // padding with 0 + memcpy(iv, &payload[2], 8); + iv[8] = payload[11]; + memcpy(&iv[9], &payload[13], 4); + +#if DEBUG + printHex(iv, sizeof(iv)); + printHex(cipher, cipherLength); +#endif + + aes128.setIV(iv, sizeof(iv)); + aes128.decrypt(plaintext, (const uint8_t *)cipher, cipherLength); + + /* + Serial.printf("C: "); + for (size_t i = 0; i < cipherLength; i++) + { + Serial.printf("%02X", cipher[i]); + } + Serial.println(); + Serial.printf("P(%d): ", cipherLength); + for (size_t i = 0; i < cipherLength; i++) + { + Serial.printf("%02X", plaintext[i]); + } + Serial.println(); + */ + + // received packet is ok + lastPacketDecoded = millis(); + + lastFrameReceived = millis(); + getMeterInfo(plaintext, cipherLength); + publishMeterInfo(); + } } // flush RX fifo and restart receiver startReceiver(); - //Serial.printf("rxStatus: 0x%02x\n\r", readStatusReg(CC1101_RXBYTES)); + // Serial.printf("rxStatus: 0x%02x\n\r", readStatusReg(CC1101_RXBYTES)); } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 10283ad..156ccdf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,7 +15,6 @@ #if defined(ESP8266) #include #include - #include #elif defined(ESP32) #include #include @@ -23,62 +22,24 @@ #include #include #include "WaterMeter.h" -#include "credentials.h" -#include "hwconfig.h" +//#include "config.h" -#define ESP_NAME "WaterMeter" +CREDENTIAL currentWifi; // global to store found wifi -#define DEBUG 0 - -#if defined(ESP32) - #define LED_BUILTIN 4 -#endif - -//Wifi settings: SSID, PW, MQTT broker -#define NUM_SSID_CREDENTIALS 3 -const char *credentials[NUM_SSID_CREDENTIALS][4] = - // SSID, PW, MQTT - { {SSID1, PW1, MQTT1 } - , {SSID2, PW2, MQTT2 } - , {SSID3, PW3, MQTT3 } - }; - -WaterMeter waterMeter; +uint8_t wifiConnectCounter = 0; // count retries WiFiClient espMqttClient; PubSubClient mqttClient(espMqttClient); +WaterMeter waterMeter(mqttClient); char MyIp[16]; int cred = -1; +bool mqttEnabled = false; // true, if a broker is given in credentials.h -int getWifiToConnect(int numSsid) -{ - for (int i = 0; i < NUM_SSID_CREDENTIALS; i++) - { - //Serial.println(WiFi.SSID(i)); - - for (int j = 0; j < numSsid; ++j) - { - /*Serial.print(j); - Serial.print(": "); - Serial.print(WiFi.SSID(i).c_str()); - Serial.print(" = "); - Serial.println(credentials[j][0]);*/ - if (strcmp(WiFi.SSID(j).c_str(), credentials[i][0]) == 0) - { - Serial.println("Credentials found for: "); - Serial.println(credentials[i][0]); - return i; - } - } - } - return -1; -} - -// connect to wifi – returns true if successful or false if not bool ConnectWifi(void) { int i = 0; + bool isWifiValid = false; Serial.println("starting scan"); // scan for nearby networks: @@ -93,48 +54,69 @@ bool ConnectWifi(void) Serial.println("Couldn't get a wifi connection"); return false; } - + for (int i = 0; i < numSsid; i++) { - Serial.print(i+1); - Serial.print(") "); - Serial.println(WiFi.SSID(i)); + Serial.print(i + 1); + Serial.print(". "); + Serial.print(WiFi.SSID(i)); + Serial.print(" "); + Serial.println(WiFi.RSSI(i)); } // search for given credentials - cred = getWifiToConnect(numSsid); - if (cred == -1) + for (CREDENTIAL credential : credentials) { - Serial.println("No Wifi!"); + for (int j = 0; j < numSsid; ++j) + { + if (strcmp(WiFi.SSID(j).c_str(), credential.ssid) == 0) + { + Serial.print("credentials found for: "); + Serial.println(credential.ssid); + currentWifi = credential; + isWifiValid = true; + } + } + } + + if (!isWifiValid) + { + Serial.println("no matching credentials"); return false; } // try to connect - WiFi.begin(credentials[cred][0], credentials[cred][1]); + Serial.println(WiFi.macAddress()); + + // try to connect WPA + WiFi.begin(currentWifi.ssid, currentWifi.password); + WiFi.setHostname(ESP_NAME); Serial.println(""); Serial.print("Connecting to WiFi "); - Serial.println(credentials[cred][0]); + Serial.println(currentWifi.ssid); i = 0; while (WiFi.status() != WL_CONNECTED) { - digitalWrite(LED_BUILTIN, LOW); + digitalWrite(PIN_LED_BUILTIN, LOW); delay(300); - Serial.print("."); - digitalWrite(LED_BUILTIN, HIGH); + Serial.print(F(".")); + digitalWrite(PIN_LED_BUILTIN, HIGH); delay(300); - if (i++ > 30) + if (i++ > 50) { // giving up - return false; + ESP.restart(); + return false; // gcc shut up } } + return true; } void mqttDebug(const char* debug_str) { - String s="/watermeter/debug"; + String s=MQTT_PREFIX"/debug"; mqttClient.publish(s.c_str(), debug_str); } @@ -151,24 +133,18 @@ void mqttCallback(char* topic, byte* payload, unsigned int len) Serial.print(" "); Serial.println((char)payload[0]); // FIXME LEN */ - if (strstr(topic, "/smarthomeNG/start")) + if (strstr(topic, "smarthomeNG/start")) { if (len == 4) // True { // maybe to something } } - else if (strstr(topic, "/espmeter/reset")) + else if (strstr(topic, MQTT_PREFIX "/reset")) { if (len == 4) // True { // maybe to something - const char *topic = "/espmeter/reset/status"; - const char *msg = "False"; - mqttClient.publish(topic, msg); - mqttClient.loop(); - delay(200); - // reboot ESP.restart(); } @@ -179,45 +155,59 @@ void mqttCallback(char* topic, byte* payload, unsigned int len) bool mqttConnect() { - mqttClient.setServer(credentials[cred][2], 1883); + bool connected=false; + + Serial.print("try to connect to MQTT server "); + Serial.println(currentWifi.mqtt_broker); + + // use given MQTT broker + mqttClient.setServer(currentWifi.mqtt_broker, 1883); + + // connect client with retainable last will message + if (strlen(currentWifi.mqtt_username) && strlen(currentWifi.mqtt_password)) + { + Serial.print("with user: "); + Serial.println(currentWifi.mqtt_username); + // connect with user/pass + connected = mqttClient.connect( ESP_NAME + , currentWifi.mqtt_username + , currentWifi.mqtt_password + , MQTT_PREFIX"/online" + , 0 + , true + , "False" + ); + } + else + { + // connect without user/pass + connected = mqttClient.connect(ESP_NAME, MQTT_PREFIX"/online", 0, true, "False"); + } + mqttClient.setCallback(mqttCallback); - // connect client to retainable last will message - return mqttClient.connect(ESP_NAME, "/watermeter/online", 0, true, "False"); + return connected; } void mqttSubscribe() { - String s; // publish online status - s = "/watermeter/online"; - mqttClient.publish(s.c_str(), "True", true); + mqttClient.publish(MQTT_PREFIX "/online", "True", true); // Serial.print("MQTT-SEND: "); // Serial.print(s); // Serial.println(" True"); // publish ip address - s="/watermeter/ipaddr"; IPAddress MyIP = WiFi.localIP(); snprintf(MyIp, 16, "%d.%d.%d.%d", MyIP[0], MyIP[1], MyIP[2], MyIP[3]); - mqttClient.publish(s.c_str(), MyIp, true); + mqttClient.publish(MQTT_PREFIX"/ipaddr", MyIp, true); // Serial.print("MQTT-SEND: "); // Serial.print(s); // Serial.print(" "); // Serial.println(MyIp); - // if smarthome.py restarts -> publish init values - s = "/smarthomeNG/start"; - mqttClient.subscribe(s.c_str()); - - // if True; meter data are published every 5 seconds - // if False: meter data are published once a minute - s = "/watermeter/liveData"; - mqttClient.subscribe(s.c_str()); - // if True -> perform an reset - s = "/espmeter/reset"; - mqttClient.subscribe(s.c_str()); + mqttClient.subscribe(MQTT_PREFIX"/reset"); } void setupOTA() @@ -259,23 +249,16 @@ void setupOTA() ArduinoOTA.begin(); } -// receive encrypted packets -> send it via MQTT to decrypter -void waterMeterLoop() -{ - if (waterMeter.isFrameAvailable()) - { - // publish meter info via MQTT - - } -} - void setup() { pinMode(LED_BUILTIN, OUTPUT); Serial.begin(115200); - waterMeter.begin(); + uint8_t key[16] = { ENCRYPTION_KEY }; // AES-128 key + uint8_t id[4] = { SERIAL_NUMBER }; // Multical21 serial number + + waterMeter.begin(key, id); Serial.println("Setup done..."); } @@ -286,6 +269,7 @@ enum ControlStateType , StateMqttConnect , StateConnected , StateOperating + , StateOperatingNoWifi }; ControlStateType ControlState = StateInit; @@ -309,7 +293,11 @@ void loop() case StateWifiConnect: //Serial.println("StateWifiConnect:"); // station mode - ConnectWifi(); + if (ConnectWifi() == false) + { + ControlState = StateOperatingNoWifi; + break; + } delay(500); @@ -317,13 +305,25 @@ void loop() { Serial.println(""); Serial.print("Connected to "); - Serial.println(credentials[cred][0]); // FIXME + Serial.println(currentWifi.ssid); Serial.print("IP address: "); Serial.println(WiFi.localIP()); setupOTA(); - ControlState = StateMqttConnect; + if (strlen(currentWifi.mqtt_broker)) // MQTT is used + { + mqttEnabled = true; + ControlState = StateMqttConnect; + } + else + { + // no MQTT server -> go operating + mqttEnabled = false; + ControlState = StateOperating; + Serial.println(F("MQTT not enabled")); + Serial.println(F("StateOperating:")); + } } else { @@ -342,25 +342,33 @@ void loop() Serial.println("StateMqttConnect:"); digitalWrite(LED_BUILTIN, HIGH); // off + waterMeter.enableMqtt(false); + if (WiFi.status() != WL_CONNECTED) { ControlState = StateNotConnected; break; // exit (hopefully) switch statement } - Serial.print("try to connect to MQTT server "); - Serial.println(credentials[cred][2]); // FIXME - - if (mqttConnect()) + if (mqttEnabled) { - ControlState = StateConnected; + if (mqttConnect()) + { + ControlState = StateConnected; + waterMeter.enableMqtt(true); + } + else + { + Serial.println("MQTT connect failed"); + + delay(1000); + // try again + } } else { - Serial.println("MQTT connect failed"); - - delay(1000); - // try again + // no MQTT is used at all + ControlState = StateConnected; } ArduinoOTA.handle(); @@ -369,20 +377,27 @@ void loop() case StateConnected: Serial.println("StateConnected:"); - if (!mqttClient.connected()) + if (mqttEnabled) { - ControlState = StateMqttConnect; - delay(1000); + if (!mqttClient.connected()) + { + ControlState = StateMqttConnect; + delay(1000); + } + else + { + // subscribe to given topics + mqttSubscribe(); + + ControlState = StateOperating; + digitalWrite(LED_BUILTIN, LOW); // on + Serial.println("StateOperating:"); + //mqttDebug("up and running"); + } } else { - // subscribe to given topics - mqttSubscribe(); - ControlState = StateOperating; - digitalWrite(LED_BUILTIN, LOW); // on - Serial.println("StateOperating:"); - //mqttDebug("up and running"); } ArduinoOTA.handle(); @@ -397,21 +412,29 @@ void loop() break; // exit (hopefully switch statement) } - if (!mqttClient.connected()) + if (mqttEnabled) { - Serial.println("not connected to MQTT server"); - ControlState = StateMqttConnect; + if (!mqttClient.connected()) + { + Serial.println("not connected to MQTT server"); + ControlState = StateMqttConnect; + } + + mqttClient.loop(); } // here we go - waterMeterLoop(); - - mqttClient.loop(); + waterMeter.loop(); ArduinoOTA.handle(); break; + case StateOperatingNoWifi: + + waterMeter.loop(); + break; + default: Serial.println("Error: invalid ControlState"); } diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..fc2c451 --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,103 @@ + +#include "utils.h" + +void printHex(uint8_t *buf, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + Serial.printf("%02X ", buf[i]); + if ((i+1) % 16 == 0) Serial.println(); + } + Serial.println(); +} + +uint16_t crcX25(uint8_t *payload, uint16_t length) +{ + return crcInternal(payload, length, 0x1021, 0xffff, true, true); +} + +uint16_t crcEN13575(uint8_t *payload, uint16_t length) +{ + return crcInternal(payload, length, 0x3D65, 0x0000, false, false); +} + +uint16_t mirror(uint16_t crc, uint8_t bitnum) +{ + // mirrors the lower 'bitnum' bits of 'crc' + + uint16_t i, j = 1, crcout = 0; + + for (i = (uint16_t)1 << (bitnum - 1); i; i >>= 1) + { + if (crc & i) + { + crcout |= j; + } + j <<= 1; + } + return crcout; +} + +uint16_t crcInternal(uint8_t *p, uint16_t len, uint16_t poly, uint16_t init, bool revIn, bool revOut) +{ + uint16_t i, j, c, bit, crc; + + crc = init; + for (i = 0; i < 16; i++) + { + bit = crc & 1; + if (bit) crc ^= poly; + crc >>= 1; + if (bit) crc |= 0x8000; + } + + // bit by bit algorithm with augmented zero bytes. + // does not use lookup table, suited for polynom orders between 1...32. + + for (i = 0; i < len; i++) + { + c = (uint16_t)*p++; + if (revIn) c = mirror(c, 8); + + for (j = 0x80; j; j >>= 1) + { + bit = crc & 0x8000; + crc <<= 1; + if (c & j) crc |= 1; + if (bit) crc ^= poly; + } + } + + for (i = 0; i < 16; i++) + { + bit = crc & 0x8000; + crc <<= 1; + if (bit) crc ^= poly; + } + + if (revOut) crc = mirror(crc, 16); + crc ^= 0xffff; // crcxor + + return crc; +} + + +// convert _in_ to _len_ hex numbers stored in _out_ +// _in_ "EF01" to 2 hex numbers: 0xEf, 0x01 +void hex2bin(const char *in, size_t len, uint8_t *out) +{ + const char *pos = in; + + for(size_t count = 0; count < len; count++) + { + char buf[5] = {'0', 'x', pos[0], pos[1], 0}; + out[count] = strtol(buf, NULL, 0); + pos += 2 * sizeof(char); + } +} + +void bin2hex(char *xp, uint8_t *bb, int n) +{ + const char xx[]= "0123456789ABCDEF"; + while (--n >= 0) xp[n] = xx[(bb[n>>1] >> ((1 - (n&1)) << 2)) & 0xF]; +} \ No newline at end of file