added CRC check, README update, bug fixes,

This commit is contained in:
Chester 2023-11-26 19:05:51 +01:00
parent b076743f54
commit ff61047706
13 changed files with 769 additions and 434 deletions

View file

@ -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).
<img align="right" src="multical21.png" alt="Multical21" width="300"/>
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:
<ul>
<li> total counter - total water consumption in m³
<li> target counter - water consumption till 1. day of the current month
<li> medium temperature - in °C
<li> ambient temperature - in °C
<li> info codes - BURST, LEAK, DRY, REVERSE, TAMPER, RADIO OFF
</ul>
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<CREDENTIAL> 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.

View file

@ -1,41 +0,0 @@
#ifndef __WMBUS_FRAME__
#define __WMBUS_FRAME__
#include <Arduino.h>
#include <Crypto.h>
#include <AES.h>
#include <CTR.h>
#include "credentials.h"
class WMBusFrame
{
public:
static const uint8_t MAX_LENGTH = 64;
private:
CTR<AESSmall128> 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__

View file

@ -17,7 +17,12 @@
#include <Arduino.h>
#include <SPI.h>
#include "WMBusFrame.h"
#include <Crypto.h>
#include <AES.h>
#include <CTR.h>
#include <PubSubClient.h>
#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<AESSmall128> 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_

67
include/config_template.h Normal file
View file

@ -0,0 +1,67 @@
#ifndef __CONFIG_H__
#define __CONFIG_H__
#include <vector>
#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<CREDENTIAL> 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__

View file

@ -0,0 +1,31 @@
#ifndef __CREDENTIALS_H__
#define __CREDENTIALS_H__
#include <vector>
// 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<CREDENTIAL> 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__

View file

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

15
include/utils.h Normal file
View file

@ -0,0 +1,15 @@
#ifndef __UTILS_H__
#define __UTILS_H__
#include <Arduino.h>
#include <inttypes.h>
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__

BIN
multical21.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View file

@ -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
upload_port = 10.0.0.131
upload_protocol = espota

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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);
}

View file

@ -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)
{
selectCC1101(); // Select CC1101
selectCC1101();
waitMiso(); // Wait until MISO goes low
SPI.transfer(regAddr); // Send register address
SPI.transfer(value); // Send value
deselectCC1101(); // Deselect CC1101
deselectCC1101();
}
// send a strobe command to CC1101
void WaterMeter::cmdStrobe(uint8_t cmd)
{
selectCC1101(); // Select CC1101
selectCC1101();
delayMicroseconds(5);
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,69 +71,81 @@ uint8_t WaterMeter::readReg(uint8_t regAddr, uint8_t regType)
uint8_t addr, val;
addr = regAddr | regType;
selectCC1101(); // Select CC1101
selectCC1101();
waitMiso(); // Wait until MISO goes low
SPI.transfer(addr); // Send register address
val = SPI.transfer(0x00); // Read result
deselectCC1101(); // Deselect CC1101
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<len ; i++)
for (i = 0; i < len; i++)
buffer[i] = SPI.transfer(0x00); // Read result byte by byte
delayMicroseconds(2);
deselectCC1101(); // Deselect CC1101
deselectCC1101();
}
// power on reset
void WaterMeter::reset(void)
{
deselectCC1101(); // Deselect CC1101
deselectCC1101();
delayMicroseconds(3);
digitalWrite(MOSI, LOW);
digitalWrite(SCK, HIGH); // see CC1101 datasheet 11.3
selectCC1101(); // Select CC1101
selectCC1101();
delayMicroseconds(3);
deselectCC1101(); // Deselect CC1101
deselectCC1101();
delayMicroseconds(45); // at least 40 us
selectCC1101(); // Select CC1101
selectCC1101();
waitMiso(); // Wait until MISO goes low
SPI.transfer(CC1101_SRES); // Send reset command strobe
waitMiso(); // Wait until MISO goes low
deselectCC1101(); // Deselect CC1101
deselectCC1101();
}
// set IDLE state, flush FIFO and (re)start receiver
void WaterMeter::startReceiver(void)
{
uint8_t regCount = 0;
cmdStrobe(CC1101_SIDLE); // Enter IDLE state
while (readReg(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) != MARCSTATE_IDLE);
while (readReg(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) != MARCSTATE_IDLE)
{
delay(1);
if (regCount++ > 100)
{
Serial.println("Enter idle state failed!\n");
restartRadio();
}
}
cmdStrobe(CC1101_SFRX); // flush receive queue
delay(5);
regCount = 0;
cmdStrobe(CC1101_SRX); // Enter RX state
while (readReg(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) != MARCSTATE_RX);
delay(10);
while (readReg(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) != MARCSTATE_RX)
{
delay(1);
if (regCount++ > 100)
{
Serial.println("Enter RX state failed!\n");
restartRadio();
}
}
}
@ -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");
// 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()
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
memcpy(aesKey, key, sizeof(aesKey));
aes128.setKey(aesKey, sizeof(aesKey));
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");
// 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);
uint8_t payloadLength = readByteFromFifo();
#if DEBUG
Serial.printf("%02x%02x", p1, p2);
#endif
// 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));
}

View file

@ -15,7 +15,6 @@
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <SoftwareSerial.h>
#elif defined(ESP32)
#include <WiFi.h>
#include <ESPmDNS.h>
@ -23,62 +22,24 @@
#include <PubSubClient.h>
#include <ArduinoOTA.h>
#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:
@ -96,45 +57,66 @@ bool ConnectWifi(void)
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,15 +305,27 @@ 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();
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
{
Serial.println("");
Serial.println("Connection failed.");
@ -342,18 +342,20 @@ 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 (mqttEnabled)
{
if (mqttConnect())
{
ControlState = StateConnected;
waterMeter.enableMqtt(true);
}
else
{
@ -362,6 +364,12 @@ void loop()
delay(1000);
// try again
}
}
else
{
// no MQTT is used at all
ControlState = StateConnected;
}
ArduinoOTA.handle();
break;
@ -369,6 +377,8 @@ void loop()
case StateConnected:
Serial.println("StateConnected:");
if (mqttEnabled)
{
if (!mqttClient.connected())
{
ControlState = StateMqttConnect;
@ -384,6 +394,11 @@ void loop()
Serial.println("StateOperating:");
//mqttDebug("up and running");
}
}
else
{
ControlState = StateOperating;
}
ArduinoOTA.handle();
break;
@ -397,21 +412,29 @@ void loop()
break; // exit (hopefully switch statement)
}
if (mqttEnabled)
{
if (!mqttClient.connected())
{
Serial.println("not connected to MQTT server");
ControlState = StateMqttConnect;
}
// here we go
waterMeterLoop();
mqttClient.loop();
}
// here we go
waterMeter.loop();
ArduinoOTA.handle();
break;
case StateOperatingNoWifi:
waterMeter.loop();
break;
default:
Serial.println("Error: invalid ControlState");
}

103
src/utils.cpp Normal file
View file

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