Skip to content

Commit cd6636b

Browse files
committed
Add support for Apator Metra E-RM 30 water meter
1 parent 474feb5 commit cd6636b

File tree

3 files changed

+269
-0
lines changed

3 files changed

+269
-0
lines changed

include/rtl_433_devices.h

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@
283283
DECL(gridstream384) \
284284
DECL(revolt_zx7717) \
285285
DECL(tpms_gm) \
286+
DECL(apator_metra_erm30) \
286287

287288
/* Add new decoders here. */
288289

src/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ add_library(r_433 STATIC
5252
devices/ambientweather_tx8300.c
5353
devices/ambientweather_wh31e.c
5454
devices/ant_antplus.c
55+
devices/apator_metra_erm30.c
5556
devices/arad_ms_meter.c
5657
devices/archos_tbh.c
5758
devices/arexx_ml.c

src/devices/apator_metra_erm30.c

+267
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/** @file
2+
Apator Metra E-RM 30 Electronic Radio Module for Residential Water Meters.
3+
4+
This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
*/
9+
10+
/** @fn int apator_metra_erm30_decode(r_device *decoder, bitbuffer_t *bitbuffer)
11+
Apator Metra E-RM 30 Electronic Radio Module for Residential Water Meters.
12+
13+
All messages appear to have the same length and are transmitted with a preamble
14+
(0x55 0x55), followed by the 0x9665 syncword. The bitstream is inverted. The
15+
length and CRC-16 are transmitted in clear text, while the payload is encrypted
16+
with an algoritm that seems to be custom, based on 4x4 S-boxes.
17+
18+
Message layout:
19+
20+
0 1 2 3 ...........................0x13 0x15
21+
SSSS LL EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE CCCC
22+
23+
- S 16b: syncword: 0x9665 (16 bits)
24+
- L 8b: payload length (seems to be always 19 = 0x13; does not include length and CRC)
25+
- E 304b: encrypted payload (19 bytes)
26+
- C 16b: CRC-16 with poly=0x8005 and init=0xfcad over data (length field and
27+
encrypted payload) after sync and bitstream invert
28+
29+
30+
Payload fields:
31+
32+
0 1 2 3 4 5 6 7 .............. 0x10 ....
33+
IIIIIIII VVVVVVVV ?????????????? DDDD ????
34+
35+
- I 32b: id, visible on the radio module (not the one on the actual analog meter)
36+
- V 32b: volume in liters
37+
- ? 56b: unknown
38+
- D 16b: date, bitpacked before encryption
39+
- ? 16b: unknown
40+
41+
According to the technical manual, the radio module also transmits other fields,
42+
like reverse flow volume, date of magnetic tampering, date of mechanical tampering
43+
etc., but they were not (yet) identified
44+
45+
*/
46+
47+
#include "decoder.h"
48+
49+
#define MAX_LEN 256
50+
#define KEY_SCHEDULE_LEN 38
51+
52+
#define CRC_LEN 2
53+
#define LEN_LEN 1
54+
55+
#define CRC_STR_LEN 13
56+
#define ID_STR_LEN 9
57+
#define VOL_STR_LEN 9
58+
#define DATE_STR_LEN 10
59+
#define BIT_LEN_STR_LEN 6
60+
61+
static void decrypt_payload(uint8_t plen, uint8_t *payload_encr, uint8_t *payload_decr, uint8_t *decr_mask);
62+
static void extract_id(uint8_t *p, uint8_t *m, char *id_str);
63+
static void extract_volume(uint8_t *p, uint8_t *m, char *volume_str);
64+
static void extract_date(uint8_t *p, uint8_t *m, char *date_str);
65+
66+
static int apator_metra_erm30_decode(r_device *decoder, bitbuffer_t *bitbuffer)
67+
{
68+
uint8_t const preamble[] = {
69+
/* 0x55, ..., 0x55, */ 0x55, 0x55, // preamble
70+
0x96, 0x65 // sync word
71+
};
72+
73+
if (bitbuffer->num_rows != 1) {
74+
return DECODE_ABORT_EARLY;
75+
}
76+
77+
bitbuffer_invert(bitbuffer);
78+
79+
int row = 0;
80+
unsigned start_pos = bitbuffer_search(bitbuffer, row, 0, preamble, 8 * sizeof(preamble));
81+
82+
if (start_pos == bitbuffer->bits_per_row[row]) {
83+
return DECODE_ABORT_EARLY; // no preamble and / or sync word detected
84+
}
85+
86+
uint8_t len;
87+
bitbuffer_extract_bytes(bitbuffer, row, start_pos + 8 * sizeof(preamble), &len, 8);
88+
89+
uint8_t frame[MAX_LEN + CRC_LEN + LEN_LEN] = {0}; // uint8_t max bytes + 2 bytes crc + 1 byte length
90+
// get frame (length field and CRC16 non included in len)
91+
bitbuffer_extract_bytes(bitbuffer, row, start_pos + 8 * sizeof(preamble), frame, 8 * (len + CRC_LEN + LEN_LEN));
92+
93+
uint16_t frame_crc = frame[len + 1] << 8 | frame[len + 2];
94+
uint16_t computed_crc = crc16(frame, len + LEN_LEN, 0x8005, 0xfcad);
95+
if (frame_crc != computed_crc) {
96+
return DECODE_FAIL_MIC;
97+
}
98+
99+
char crc_str[CRC_STR_LEN + 1];
100+
sprintf(crc_str, "CRC16(0x%04x)", frame_crc);
101+
102+
uint8_t *payload_encr = frame + LEN_LEN;
103+
uint8_t payload_decr[MAX_LEN] = {0};
104+
uint8_t decr_mask[MAX_LEN] = {0};
105+
106+
decrypt_payload(len, payload_encr, payload_decr, decr_mask);
107+
108+
char id[ID_STR_LEN + 1];
109+
extract_id(payload_decr, decr_mask, id);
110+
111+
char volume[VOL_STR_LEN + 1];
112+
extract_volume(payload_decr, decr_mask, volume);
113+
114+
char date[DATE_STR_LEN + 1];
115+
extract_date(payload_decr, decr_mask, date);
116+
117+
/* clang-format off */
118+
data_t *data = data_make(
119+
"model", "", DATA_STRING, "ApatorMetra-ERM30",
120+
"id", "ID", DATA_STRING, id,
121+
"len", "Frame length", DATA_INT, len,
122+
"volume_m3", "Volume", DATA_STRING, volume,
123+
"date", "Date", DATA_STRING, date,
124+
"mic", "Integrity", DATA_STRING, crc_str,
125+
NULL);
126+
/* clang-format on */
127+
128+
decoder_output_data(decoder, data);
129+
return 1;
130+
}
131+
132+
static char const *const output_fields[] = {
133+
"model",
134+
"id",
135+
"len",
136+
"volume_m3",
137+
"date",
138+
"mic",
139+
NULL,
140+
};
141+
142+
r_device const apator_metra_erm30 = {
143+
.name = "Apator Metra E-RM 30",
144+
.modulation = FSK_PULSE_PCM,
145+
.short_width = 25,
146+
.long_width = 25,
147+
.reset_limit = 5000,
148+
.decode_fn = &apator_metra_erm30_decode,
149+
.fields = output_fields,
150+
};
151+
152+
153+
154+
/*
155+
Decrypts an encrypted payload according to the S-boxes and key-schedule.
156+
The encrypted payload is read from the payload_encr buffer and the decrypted payload is written in payload_decr.
157+
158+
There is also the decr_mask buffer, which acts like a "decryption bitmap": if a nibble was decrypted, the mask
159+
at the correspoding offset is 0x0, otherwise is 0xf. It's used when converting the (partially) decrypted values
160+
to strings.
161+
162+
It has been observed that there are 16 possible S-boxes. They are derived by writing the first one as a 4x4
163+
matrix and permutting the rows and columns. The "name" of the S-box related to where the "0" is in the
164+
corresponding matrix (e.g. sbox_2_3 has the 0 in row 2, column 3.
165+
166+
It couldn't be determined where all S-boxes are used, so the ones with unknown usage are listed here as commented.
167+
168+
The key_schedule array actually maps the offset of the encrypted nibble to the sbox that must be used for
169+
decryption. If we didn't figure out which sbox to use, it has NULL for that offset.
170+
*/
171+
static void decrypt_payload(uint8_t plen, uint8_t *payload_encr, uint8_t *payload_decr, uint8_t *decr_mask)
172+
{
173+
// uint8_t const sbox_0_0[16] = {0x0, 0x7, 0xf, 0x9, 0xe, 0xd, 0x3, 0x4, 0x2, 0x6, 0xc, 0xb, 0x1, 0x8, 0xa, 0x5};
174+
uint8_t const sbox_0_1[16] = {0x7, 0x0, 0x9, 0xf, 0xd, 0xe, 0x4, 0x3, 0x6, 0x2, 0xb, 0xc, 0x8, 0x1, 0x5, 0xa};
175+
uint8_t const sbox_0_2[16] = {0xf, 0x9, 0x0, 0x7, 0x3, 0x4, 0xe, 0xd, 0xc, 0xb, 0x2, 0x6, 0xa, 0x5, 0x1, 0x8};
176+
// uint8_t const sbox_0_3[16] = {0x9, 0xf, 0x7, 0x0, 0x4, 0x3, 0xd, 0xe, 0xb, 0xc, 0x6, 0x2, 0x5, 0xa, 0x8, 0x1};
177+
// uint8_t const sbox_1_0[16] = {0xe, 0xd, 0x3, 0x4, 0x0, 0x7, 0xf, 0x9, 0x1, 0x8, 0xa, 0x5, 0x2, 0x6, 0xc, 0xb};
178+
uint8_t const sbox_1_1[16] = {0xd, 0xe, 0x4, 0x3, 0x7, 0x0, 0x9, 0xf, 0x8, 0x1, 0x5, 0xa, 0x6, 0x2, 0xb, 0xc};
179+
uint8_t const sbox_1_2[16] = {0x3, 0x4, 0xe, 0xd, 0xf, 0x9, 0x0, 0x7, 0xa, 0x5, 0x1, 0x8, 0xc, 0xb, 0x2, 0x6};
180+
uint8_t const sbox_1_3[16] = {0x4, 0x3, 0xd, 0xe, 0x9, 0xf, 0x7, 0x0, 0x5, 0xa, 0x8, 0x1, 0xb, 0xc, 0x6, 0x2};
181+
uint8_t const sbox_2_0[16] = {0x2, 0x6, 0xc, 0xb, 0x1, 0x8, 0xa, 0x5, 0x0, 0x7, 0xf, 0x9, 0xe, 0xd, 0x3, 0x4};
182+
// uint8_t const sbox_2_1[16] = {0x6, 0x2, 0xb, 0xc, 0x8, 0x1, 0x5, 0xa, 0x7, 0x0, 0x9, 0xf, 0xd, 0xe, 0x4, 0x3};
183+
uint8_t const sbox_2_2[16] = {0xc, 0xb, 0x2, 0x6, 0xa, 0x5, 0x1, 0x8, 0xf, 0x9, 0x0, 0x7, 0x3, 0x4, 0xe, 0xd};
184+
uint8_t const sbox_2_3[16] = {0xb, 0xc, 0x6, 0x2, 0x5, 0xa, 0x8, 0x1, 0x9, 0xf, 0x7, 0x0, 0x4, 0x3, 0xd, 0xe};
185+
uint8_t const sbox_3_0[16] = {0x1, 0x8, 0xa, 0x5, 0x2, 0x6, 0xc, 0xb, 0xe, 0xd, 0x3, 0x4, 0x0, 0x7, 0xf, 0x9};
186+
uint8_t const sbox_3_1[16] = {0x8, 0x1, 0x5, 0xa, 0x6, 0x2, 0xb, 0xc, 0xd, 0xe, 0x4, 0x3, 0x7, 0x0, 0x9, 0xf};
187+
uint8_t const sbox_3_2[16] = {0xa, 0x5, 0x1, 0x8, 0xc, 0xb, 0x2, 0x6, 0x3, 0x4, 0xe, 0xd, 0xf, 0x9, 0x0, 0x7};
188+
// uint8_t const sbox_3_3[16] = {0x5, 0xa, 0x8, 0x1, 0xb, 0xc, 0x6, 0x2, 0x4, 0x3, 0xd, 0xe, 0x9, 0xf, 0x7, 0x0};
189+
190+
uint8_t const *const key_schedule[KEY_SCHEDULE_LEN] = {
191+
sbox_0_1, sbox_3_2, sbox_3_2, sbox_0_2, sbox_1_2, sbox_1_1, sbox_1_1, sbox_0_2,
192+
sbox_1_3, sbox_2_2, sbox_3_0, sbox_3_0, sbox_3_1, sbox_2_3, NULL, sbox_1_1,
193+
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
194+
NULL, NULL, NULL, NULL, NULL, NULL, sbox_2_2, sbox_2_3,
195+
sbox_2_0, sbox_0_2, NULL, NULL, NULL, NULL,
196+
};
197+
198+
for (int i = 0; i < 2 * plen; i++) {
199+
uint8_t nibble_encr, nibble_decr, nibble_mask;
200+
201+
unsigned int bitshift = (i % 2) ? 0 : 4;
202+
203+
if (i < KEY_SCHEDULE_LEN && key_schedule[i] != NULL) {
204+
nibble_encr = (payload_encr[i / 2] >> bitshift) & 0x0f;
205+
nibble_decr = key_schedule[i][nibble_encr];
206+
nibble_mask = 0x0;
207+
} else {
208+
nibble_decr = 0x0;
209+
nibble_mask = 0xf;
210+
}
211+
212+
payload_decr[i / 2] |= nibble_decr << bitshift;
213+
decr_mask[i / 2] |= nibble_mask << bitshift;
214+
}
215+
}
216+
217+
/*
218+
Converts the binary value of the ID field to a string that can be pretty-printed.
219+
If the field was only partially decrypted, the string will contain question marks.
220+
*/
221+
static void extract_id(uint8_t *p, uint8_t *m, char *id_str)
222+
{
223+
uint32_t id = p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
224+
uint32_t mask = m[3] << 24 | m[2] << 16 | m[1] << 8 | m[0];
225+
226+
if (mask == 0) {
227+
sprintf(id_str, "%09d", id);
228+
} else {
229+
sprintf(id_str, "????????");
230+
}
231+
}
232+
233+
234+
/*
235+
Converts the binary value of the Volume field to a string that can be pretty-printed.
236+
If the field was only partially decrypted, the string will contain question marks.
237+
*/
238+
static void extract_volume(uint8_t *p, uint8_t *m, char *volume_str)
239+
{
240+
uint32_t volume = ((p[7] << 24 | p[6] << 16 | p[5] << 8 | p[4]) & 0x0fffffff) >> 3;
241+
uint32_t mask = ((m[7] << 24 | m[6] << 24 | m[5] << 8 | m[4]) & 0x0fffffff) >> 3;
242+
243+
if (mask == 0) {
244+
sprintf(volume_str, "%.3f", volume / 1000.0);
245+
} else {
246+
sprintf(volume_str, "?????.???");
247+
}
248+
}
249+
250+
/*
251+
Converts the binary value of the Date field to a string that can be pretty-printed.
252+
If the field was only partially decrypted, the string will contain question marks.
253+
*/
254+
static void extract_date(uint8_t *p, uint8_t *m, char *date_str)
255+
{
256+
uint16_t date = p[16] << 8 | p[15];
257+
uint16_t mask = m[16] << 8 | m[15];
258+
259+
if (mask == 0) {
260+
uint8_t day = date & 0x1f;
261+
uint8_t month = (date >> 5) & 0x0f;
262+
uint8_t year = (date >> 9) & 0x7f;
263+
sprintf(date_str, "%04d-%02d-%02d", 2000 + year, month, day);
264+
} else {
265+
sprintf(date_str, "%s-%s-%s", "????", "??", "??");
266+
}
267+
}

0 commit comments

Comments
 (0)