├── README.md ├── izar_utils.cpp ├── izar_utils.h ├── izar_wmbus.cpp ├── izar_wmbus.h ├── library.json └── wmbus_t_cc1101_config.h /README.md: -------------------------------------------------------------------------------- 1 | # Izar water meter reader (ESP8266 and ESP32 -based) 2 | The idea behind the project is to read Izar water meter current consumption 3 | 4 | Implementation is naive, and hardcoded in many places. It was meant to be simple ;) 5 | 6 | ## Required components 7 | - NodeMCU (however any ESP8266-based device should work fine) 8 | - CC1101 module (ideally with an antena tuned to 868MHz *) 9 | 10 | _Note_: While CC1101 chip is versatile and may by configured programatically to use different frequency, some PCB components has to be selected for better performance. Therefore, please pay attention to get the right one.More info here: https://wiki.fhem.de/wiki/Selbstbau_CUL 11 | 12 | I was able to use CC1101 board tuned for 433MHz correctly, though ;) 13 | 14 | ## Usage 15 | 16 | ``` 17 | IzarWmbus reader; 18 | 19 | void setup() { 20 | //somewhere in the setup 21 | reader.init(0); 22 | } 23 | 24 | 25 | IzarResultData data; 26 | 27 | void loop() { 28 | FetchResult result = reader.fetchPacket(&data); 29 | if (result == FETCH_SUCCESSFUL) { 30 | // we have new package! 31 | } 32 | } 33 | 34 | ``` 35 | 36 | 37 | ## Wiring 38 | ### ESP8266 39 | ``` 40 | CC1101 -> NodeMCU 41 | ================== 42 | SCK -> D5 43 | MISO -> D6 44 | MOSI -> D7 45 | CSN -> D8 46 | VCC -> 3V 47 | GND -> GND 48 | ``` 49 | 50 | ### ESP32-CAM 51 | My main challenge with wiring ESP32-CAM was use of the default SPI connection by camera module, so I had to divert to other pin set 52 | ``` 53 | 54 | CC1101 -> ESP32(left side) 55 | ============================ 56 | 5V 57 | GND 58 | MISO -> GPIO12 59 | MOSI -> GPIO13 60 | CSN -> GPIO15 61 | SCK -> GPIO14 62 | GPIO2 63 | GPIO4 64 | 65 | 66 | (right side) 67 | VCC -> 3V3 68 | GDN -> GND 69 | ``` 70 | 71 | More details about wiring in library this tool is using for connecting with CC1101: https://github.com/LSatan/SmartRC-CC1101-Driver-Lib 72 | 73 | ### CC1101 868Mhz pinout 74 | (connectors on the left, ANT on the right) 75 | 76 | ``` 77 | VCC ----------------- 78 | GND | | 79 | MOSI | | 80 | SCK | CC1101 | GND 81 | MISO | | ANT 82 | GDO2 | | GND 83 | GDO0 | crystal | 84 | CSN |_______________- 85 | ``` 86 | 87 | ## Building and usage 88 | The library is based on PlatformIO and is build through PlatformIO's toolchain. 89 | 90 | ## Related materials: 91 | - Application note on using CC1101 with Wirless MBUS: https://www.ti.com/lit/an/swra234a/swra234a.pdf 92 | - CULFW source code: https://github.com/heliflieger/a-culfw/ 93 | - App filtering WMBUS packets from RTL: https://github.com/xaelsouth/rtl-wmbus 94 | - Almighty WMBUS meters decoders library: https://github.com/weetmuts/wmbusmeters 95 | -------------------------------------------------------------------------------- /izar_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "izar_utils.h" 2 | 3 | #include 4 | 5 | #define CRC_POLYNOM 0x3D65 6 | 7 | 8 | const uint8_t decoder[] = { 9 | 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 10 | 0xFF,0xFF,0xFF,0x03,0xFF,0x01,0x02,0xFF, 11 | 0xFF,0xFF,0xFF,0x07,0xFF,0xFF,0x00,0xFF, 12 | 0xFF,0x05,0x06,0xFF,0x04,0xFF,0xFF,0xFF, 13 | 0xFF,0xFF,0xFF,0x0B,0xFF,0x09,0x0A,0xFF, 14 | 0xFF,0x0F,0xFF,0xFF,0x08,0xFF,0xFF,0xFF, 15 | 0xFF,0x0D,0x0E,0xFF,0x0C,0xFF,0xFF,0xFF, 16 | 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF 17 | }; 18 | 19 | uint32_t uintFromBytes(uint8_t* data) { 20 | uint32_t result = data[0] << 24; 21 | result += data[1] << 16; 22 | result += data[2] << 8; 23 | result += data[3]; 24 | return result; 25 | } 26 | 27 | uint32_t uintFromBytesLittleEndian(uint8_t* data) { 28 | uint32_t result = data[3] << 24; 29 | result += data[2] << 16; 30 | result += data[1] << 8; 31 | result += data[0]; 32 | return result; 33 | } 34 | 35 | uint16_t uint16FromBytes(uint8_t* data) { 36 | uint16_t result = data[0] << 8; 37 | result += data[1]; 38 | return result; 39 | } 40 | 41 | uint32_t hashShiftKey(uint32_t key) { 42 | for (uint8_t i = 0; i < 8; i++) { 43 | uint8_t bit = GET_BIT(key, 1) ^ GET_BIT(key, 2) ^ GET_BIT(key, 11) ^ 44 | GET_BIT(key, 31); 45 | key <<= 1; 46 | key |= bit; 47 | } 48 | return key; 49 | } 50 | 51 | uint8_t decode3of6Single(uint8_t* encoded, uint8_t* decoded) { 52 | uint8_t data[4]; 53 | 54 | data[0] = decoder[encoded[2] & 0x3F]; 55 | data[1] = decoder[((encoded[2] & 0xC0) >> 6) | ((encoded[1] & 0x0F) << 2)]; 56 | data[2] = decoder[((encoded[1] & 0xF0) >> 4) | ((encoded[0] & 0x03) << 4)]; 57 | data[3] = decoder[((encoded[0] & 0xFC) >> 2)]; 58 | 59 | for (uint8_t i = 0; i < 4; i++) { 60 | if (data[i] == 0xFF) { 61 | return -1; 62 | } 63 | } 64 | 65 | // - Shift the encoded values into a byte buffer - 66 | decoded[0] = (data[3] << 4) | (data[2]); 67 | decoded[1] = (data[1] << 4) | (data[0]); 68 | 69 | return 0; 70 | } 71 | 72 | uint8_t decrypt(uint8_t* encoded, uint8_t len, uint8_t* decoded) { 73 | if (len < 15) { 74 | return 0; 75 | } 76 | 77 | uint32_t key = 0xdfd109e8; 78 | key ^= uintFromBytes(encoded + 2); 79 | key ^= uintFromBytes(encoded + 6); 80 | key ^= uintFromBytes(encoded + 10); 81 | 82 | const uint8_t size = len - 15; 83 | for (uint8_t i = 0; i < size; i++) { 84 | key = hashShiftKey(key); 85 | decoded[i] = encoded[i + 15] ^ (key & 0xFF); 86 | } 87 | 88 | if (decoded[0] != 0x4B) { 89 | return 0; 90 | } 91 | 92 | return size; 93 | } 94 | 95 | uint16_t crc16(uint16_t crcVal, uint8_t dataByte) { 96 | for (int i = 0; i < 8; i++) { 97 | if (((crcVal & 0x8000) >> 8) ^ (dataByte & 0x80)) { 98 | crcVal = (crcVal << 1) ^ CRC_POLYNOM; 99 | } else { 100 | crcVal = (crcVal << 1); 101 | } 102 | 103 | dataByte <<= 1; 104 | } 105 | 106 | return crcVal; 107 | } -------------------------------------------------------------------------------- /izar_utils.h: -------------------------------------------------------------------------------- 1 | #ifndef IZAR_UTILS 2 | #define IZAR_UTILS 3 | 4 | #include 5 | 6 | #define GET_BIT(var, pos) ((var >> pos) & 0x01) 7 | 8 | uint32_t uintFromBytes(uint8_t* data); 9 | uint32_t uintFromBytesLittleEndian(uint8_t* data); 10 | uint16_t uint16FromBytes(uint8_t* data); 11 | uint8_t decrypt(uint8_t* encoded, uint8_t len, uint8_t* decoded); 12 | uint8_t decode3of6Single(uint8_t* encoded, uint8_t* decoded); 13 | uint16_t crc16(uint16_t crcVal, uint8_t dataByte); 14 | 15 | #endif 16 | 17 | -------------------------------------------------------------------------------- /izar_wmbus.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "izar_wmbus.h" 3 | 4 | #include 5 | 6 | #include "izar_utils.h" 7 | #include "wmbus_t_cc1101_config.h" 8 | 9 | void IzarWmbus::init(uint32_t waterMeter) { 10 | #ifdef ESP32 11 | ELECHOUSE_cc1101.setSpiPin(14, 12, 13, 15); 12 | #endif 13 | 14 | if (waterMeter != 0) { 15 | waterMeterId = waterMeter; 16 | } 17 | 18 | if (ELECHOUSE_cc1101.getCC1101()) { 19 | Serial.println("Connection OK"); 20 | } else { 21 | Serial.println("Connection Error"); 22 | } 23 | ELECHOUSE_cc1101.Init(); 24 | 25 | for (uint8_t i = 0; i < WMBUS_T_CC1101_CONFIG_LEN; i++) { 26 | ELECHOUSE_cc1101.SpiWriteReg(WMBUS_T_CC1101_CONFIG_BYTES[i << 1], 27 | WMBUS_T_CC1101_CONFIG_BYTES[(i << 1) + 1]); 28 | } 29 | 30 | ELECHOUSE_cc1101.SpiStrobe(CC1101_SCAL); 31 | 32 | if (ELECHOUSE_cc1101.SpiReadStatus(CC1101_VERSION) != 4) { 33 | Serial.println( 34 | "WARNING! CC1101_VERSION should be equal 4! Is there any " 35 | "connection issue?"); 36 | } 37 | 38 | ELECHOUSE_cc1101.SetRx(); 39 | 40 | Serial.println("device initialized"); 41 | } 42 | 43 | uint8_t IzarWmbus::ReceiveData2(byte* rxBuffer) { 44 | uint8_t size = ELECHOUSE_cc1101.SpiReadStatus(CC1101_RXBYTES) & 0x7F; 45 | if (size) { 46 | ELECHOUSE_cc1101.SpiReadBurstReg(CC1101_RXFIFO, rxBuffer, size); 47 | } 48 | ELECHOUSE_cc1101.SpiStrobe(CC1101_SFRX); 49 | ELECHOUSE_cc1101.SpiStrobe(CC1101_SRX); 50 | return size; 51 | } 52 | 53 | uint8_t buffer[128] = {0}; 54 | uint8_t decoded[64] = {0}; 55 | uint8_t decrypted[64] = {0}; 56 | 57 | inline void dumpHex(uint8_t* data, int len) { 58 | for (int i = 0; i < len; i++) { 59 | Serial.print(data[i], HEX); 60 | Serial.print(" "); 61 | } 62 | Serial.println(); 63 | } 64 | 65 | bool IzarWmbus::checkCRCForSection(uint8_t* section, uint8_t sectionLen) { 66 | uint16_t crc = 0; 67 | for (int i = 0; i < sectionLen; i++) { 68 | crc = crc16(crc, section[i]); 69 | } 70 | crc = ~crc; 71 | return uint16FromBytes(section + sectionLen) == crc; 72 | } 73 | 74 | bool IzarWmbus::checkCRC(uint8_t* packet, uint8_t len) { 75 | if (!checkCRCForSection(packet, 10)) { 76 | return false; 77 | } 78 | 79 | for (int i = 12; i < len; i += 18) { 80 | uint8_t sectionLength = 16; 81 | // do we have full section? 82 | if (i + 18 > len) { 83 | sectionLength = len - i - 2; 84 | } 85 | 86 | if (!checkCRCForSection(packet + i, sectionLength)) { 87 | return false; 88 | } 89 | } 90 | 91 | return true; 92 | } 93 | 94 | int calculateBytesLengthBasedOnDataLength(int size) { 95 | int sections = ((size - 9) / 16); 96 | if ((size - 9) % 16) { 97 | sections++; 98 | } 99 | 100 | // 12 = 10 header (incl size byte) + 2 CRC 101 | return 12 + sections * 18; 102 | } 103 | 104 | FetchResult IzarWmbus::fetchPacket(IzarResultData* data) { 105 | if (ELECHOUSE_cc1101.CheckRxFifo(0)) { 106 | //====READ==== 107 | uint8_t len = ReceiveData2(buffer); 108 | uint8_t decodeErrors = 0; 109 | 110 | //====DECODE==== 111 | int decodedLen = decode3outOf6(buffer, len, decoded, decodeErrors); 112 | 113 | decodedLen = calculateBytesLengthBasedOnDataLength(decoded[0]); 114 | 115 | if (decodeErrors != 0) { 116 | return FETCH_3OF6_ERROR; 117 | } 118 | 119 | if (!checkCRC(decoded, decodedLen)) { 120 | return FETCH_CRC_ERROR; 121 | } 122 | 123 | uint32_t thisMeterId = uintFromBytesLittleEndian(decoded + 4); 124 | 125 | if (waterMeterId != 0) { 126 | if (thisMeterId != waterMeterId) { 127 | return FETCH_OTHER_METER; 128 | } 129 | } 130 | 131 | data->meterId = thisMeterId; 132 | 133 | // strip out WMBUS CRC for decryption 134 | for (int i = 12; i < decodedLen; i++) { 135 | decoded[i - 2] = decoded[i]; 136 | } 137 | decoded[decodedLen - 1] = 0; 138 | decoded[decodedLen] = 0; 139 | decodedLen -= 2; 140 | 141 | //====DECRYPT==== 142 | uint8_t decryptedLen = decrypt(decoded, decodedLen, decrypted); 143 | 144 | if (print_telegrams) { 145 | dumpHex(decoded, decodedLen); 146 | } 147 | if (print_decoded) { 148 | dumpHex(decrypted, decryptedLen); 149 | } 150 | 151 | data->waterUsage = uintFromBytesLittleEndian(decrypted + 1); 152 | 153 | if (!isSensibleResult(data)) { 154 | return FETCH_NON_SENSIBLE_DATA; 155 | } 156 | 157 | return FETCH_SUCCESSFUL; 158 | } else { 159 | return FETCH_NO_DATA; 160 | } 161 | } 162 | 163 | bool IzarWmbus::isSensibleResult(IzarResultData* data) { 164 | auto hasLastUsage = lastResults.find(data->waterUsage); 165 | 166 | if (hasLastUsage == lastResults.end()) { 167 | lastResults[data->meterId] = data->waterUsage; 168 | } 169 | 170 | long int diff = data->waterUsage; 171 | diff -= lastResults[data->meterId]; 172 | 173 | if (-SENSIBLE_RESULT_THRESHOLD < diff && diff < SENSIBLE_RESULT_THRESHOLD) { 174 | lastResults[data->meterId] = data->waterUsage; 175 | return true; 176 | } 177 | 178 | return false; 179 | } 180 | 181 | void IzarWmbus::ensureRx() { 182 | if ((ELECHOUSE_cc1101.SpiReadStatus(CC1101_MARCSTATE) & 0x0F) == 0x01) { 183 | ELECHOUSE_cc1101.SetRx(); 184 | } 185 | } 186 | 187 | int IzarWmbus::decode3outOf6(uint8_t* input, const uint8_t inputLen, 188 | uint8_t* output, uint8_t& errors) { 189 | int i = 0; 190 | errors = 0; 191 | for (i = 0; i < inputLen / 3; i++) { 192 | if (decode3of6Single(buffer + (i * 3), decoded + (i * 2)) == -1) { 193 | errors++; 194 | } 195 | } 196 | return i * 2; 197 | } 198 | -------------------------------------------------------------------------------- /izar_wmbus.h: -------------------------------------------------------------------------------- 1 | #ifndef IZAR_WMBUS 2 | #define IZAR_WMBUS 3 | #include 4 | #include 5 | 6 | 7 | enum FetchResult { 8 | FETCH_SUCCESSFUL, 9 | FETCH_NO_DATA, 10 | FETCH_OTHER_METER, 11 | FETCH_3OF6_ERROR, 12 | FETCH_CRC_ERROR, 13 | FETCH_NON_SENSIBLE_DATA 14 | }; 15 | 16 | struct IzarResultData { 17 | uint32_t meterId; 18 | uint32_t waterUsage; 19 | }; 20 | 21 | class IzarWmbus { 22 | public: 23 | const long int SENSIBLE_RESULT_THRESHOLD = 300000; 24 | 25 | 26 | void init(uint32_t waterMeter); 27 | FetchResult fetchPacket(IzarResultData* data); 28 | void ensureRx(); 29 | private: 30 | uint32_t waterMeterId = 0; 31 | const bool print_telegrams = 0; 32 | const bool print_decoded = 0; 33 | std::map lastResults; 34 | 35 | uint8_t ReceiveData2(byte *rxBuffer); 36 | static int decode3outOf6(uint8_t* input, uint8_t inputLen, uint8_t* output, uint8_t& errors); 37 | bool checkCRCForSection(uint8_t* section, uint8_t sectionLen); 38 | bool checkCRC(uint8_t* packet, uint8_t len); 39 | bool isSensibleResult(IzarResultData* data); 40 | 41 | }; 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "izar-wmbus-esp-lib", 3 | "description": "This library can be used to provide readouts from IZAR water meter through CC1101 module at ESP8266 and ESP32 platforms", 4 | "keywords": "", 5 | "authors": { 6 | "name": "MaciekN" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/maciekn/izar-wmbus-esp" 11 | }, 12 | "version": "1.0.0", 13 | "frameworks": [ 14 | "arduino" 15 | ], 16 | "platforms": [ 17 | "espressif32", 18 | "espressif8266" 19 | ], 20 | "dependencies": { 21 | "lsatan/SmartRC-CC1101-Driver-Lib": "^2.5.7", 22 | "SPI": "*" 23 | } 24 | } -------------------------------------------------------------------------------- /wmbus_t_cc1101_config.h: -------------------------------------------------------------------------------- 1 | #ifndef WMBUS_T_CC1101_CONFIG 2 | #define WMBUS_T_CC1101_CONFIG 3 | 4 | 5 | #include 6 | 7 | 8 | const uint8_t WMBUS_T_CC1101_CONFIG_LEN = 47; 9 | 10 | 11 | //based on https://www.ti.com/lit/an/swra234a/swra234a.pdf 12 | const uint8_t WMBUS_T_CC1101_CONFIG_BYTES[] = { 13 | CC1101_IOCFG2,0x06, 14 | CC1101_IOCFG1,0x2E, 15 | CC1101_IOCFG0,0x00, 16 | CC1101_FIFOTHR,0x7, 17 | CC1101_SYNC1,0x54, 18 | CC1101_SYNC0,0x3D, 19 | CC1101_PKTLEN,0xFF, 20 | CC1101_PKTCTRL1,0x0,// original: 0x4, disable APPEND_STATUS 21 | CC1101_PKTCTRL0,0x0, 22 | CC1101_ADDR,0x0, 23 | CC1101_CHANNR,0x0, 24 | CC1101_FSCTRL1,0x8, 25 | CC1101_FSCTRL0,0x0, 26 | CC1101_FREQ2,0x21, 27 | CC1101_FREQ1,0x6B, 28 | CC1101_FREQ0,0xD0, 29 | CC1101_MDMCFG4,0x5C, 30 | CC1101_MDMCFG3,0x4, 31 | CC1101_MDMCFG2,0x5, 32 | CC1101_MDMCFG1,0x22, 33 | CC1101_MDMCFG0,0xF8, 34 | CC1101_DEVIATN, 0x44, 35 | CC1101_MCSM2,0x7, 36 | CC1101_MCSM1,0x00, 37 | CC1101_MCSM0,0x18, 38 | CC1101_FOCCFG,0x2E, 39 | CC1101_BSCFG,0xBF, 40 | CC1101_AGCCTRL2,0x43, 41 | CC1101_AGCCTRL1,0x9, 42 | CC1101_AGCCTRL0,0xB5, 43 | CC1101_WOREVT1,0x87, 44 | CC1101_WOREVT0,0x6B, 45 | CC1101_WORCTRL,0xFB, 46 | CC1101_FREND1,0xB6, 47 | CC1101_FREND0,0x10, 48 | CC1101_FSCAL3,0xEA, 49 | CC1101_FSCAL2,0x2A, 50 | CC1101_FSCAL1,0x0, 51 | CC1101_FSCAL0,0x1F, 52 | CC1101_RCCTRL1,0x41, 53 | CC1101_RCCTRL0,0x0, 54 | CC1101_FSTEST,0x59, 55 | CC1101_PTEST,0x7F, 56 | CC1101_AGCTEST,0x3F, 57 | CC1101_TEST2,0x81, 58 | CC1101_TEST1,0x35, 59 | CC1101_TEST0,0x9 60 | }; 61 | 62 | 63 | #endif --------------------------------------------------------------------------------