Skip to content

Commit 264a56c

Browse files
authored
Add support for Gridstream RF protocol from Landis & Gyr meters (#2616)
1 parent a3c2124 commit 264a56c

File tree

5 files changed

+290
-0
lines changed

5 files changed

+290
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ See [CONTRIBUTING.md](./docs/CONTRIBUTING.md).
356356
[268] Bresser Thermo-/Hygro-Sensor Explore Scientific ST1005H
357357
[269] DeltaDore X3D devices
358358
[270]* Quinetic
359+
[271] Landis & Gyr Gridstream Power Meters 9.6k
360+
[272] Landis & Gyr Gridstream Power Meters 19.2k
361+
[273] Landis & Gyr Gridstream Power Meters 38.4k
359362
360363
* Disabled by default, use -R n or a conf file to enable
361364

conf/rtl_433.example.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,9 @@ convert si
497497
protocol 268 # Bresser Thermo-/Hygro-Sensor Explore Scientific ST1005H
498498
protocol 269 # DeltaDore X3D devices
499499
# protocol 270 # Quinetic
500+
protocol 271 # Landis & Gyr Gridstream Power Meters 9.6k
501+
protocol 272 # Landis & Gyr Gridstream Power Meters 19.2k
502+
protocol 273 # Landis & Gyr Gridstream Power Meters 38.4k
500503

501504
## Flex devices (command line option "-X")
502505

include/rtl_433_devices.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@
278278
DECL(bresser_st1005h) \
279279
DECL(deltadore_x3d) \
280280
DECL(quinetic) \
281+
DECL(gridstream96) \
282+
DECL(gridstream192) \
283+
DECL(gridstream384) \
281284

282285
/* Add new decoders here. */
283286

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ add_library(r_433 STATIC
134134
devices/generic_temperature_sensor.c
135135
devices/geo_minim.c
136136
devices/govee.c
137+
devices/gridstream.c
137138
devices/gt_tmbbq05.c
138139
devices/gt_wt_02.c
139140
devices/gt_wt_03.c

src/devices/gridstream.c

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/** @file
2+
Decoder for Gridstream RF devices produced by Landis & Gyr.
3+
4+
Copyright (C) 2023 krvmk
5+
6+
This program is free software; you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation; either version 2 of the License, or
9+
(at your option) any later version.
10+
*/
11+
12+
/** @fn int gridstream_decode(r_device *decoder, bitbuffer_t *bitbuffer)
13+
Landis & Gyr Gridstream Power Meters.
14+
15+
- Center Frequency: 915 Mhz
16+
- Frequency Range: 902-928 MHz
17+
- Channel Spacing: 100kHz, 300kHz
18+
- Modulation: FSK-PCM (2-FSK, GFSK)
19+
- Bitrates: 9600, 19200, 38400
20+
- Preamble: 0xAAAA
21+
- Syncword v4: 0b0000000001 0b0111111111
22+
- Syncword v5: 0b0000000001 0b11111111111
23+
24+
This decoder is based on the information from: https://wiki.recessim.com/view/Landis%2BGyr_GridStream_Protocol
25+
Datastream is variable length and bitrate depending on type fields
26+
Preamble
27+
Bytes after preamble are encoded with standard uart settings with start bit, 8 data bits and stop bit.
28+
Data layouts:
29+
Subtype 55:
30+
AAAAAA SSSS TT YY LLLL KK BBBBBBBBBB WWWWWWWWWW II MMMMMMMM KKKK EEEEEEEE KKKK KKKKKK CCCC KKKK XXXX KK
31+
Subtype D2:
32+
AAAAAA SSSS TT YY LL K----------K XXXX
33+
Subtype D5:
34+
AAAAAA SSSS TT YY LLLL KK DDDDDDDD EEEEEEEE II K----------K CCCC KKKK XXXX
35+
- A - Preamble
36+
- S - Syncword
37+
- T - Type
38+
- Y - Subtype
39+
- L - Length
40+
- B - Broadcast
41+
- D - Dest Address
42+
- E - Source Address
43+
- M - Uptime (time since last outage in seconds)
44+
- I - Counter
45+
- C - Clock
46+
- K - Unknown
47+
- X - CRC (poly 0x1021, init set by provider)
48+
49+
*/
50+
51+
#include "decoder.h"
52+
53+
struct crc_init {
54+
uint16_t value;
55+
char const *location;
56+
char const *provider;
57+
};
58+
59+
/*
60+
Decoder will iterate through the known values until checksum is validated.
61+
62+
In order to identify new values, the reveng application, https://reveng.sourceforge.io/,
63+
can determine a missing init value if given several fixed length packet streams.
64+
Subtype 0x55 with a data length of 0x23 can be used for this.
65+
66+
Known CRC init values can be added to the code via PR when they have been identified.
67+
68+
*/
69+
static const struct crc_init known_crc_init[] = {
70+
{0xe623, "Kansas City, MO", "Evergy-Missouri West"},
71+
{0x5fd6, "Dallas, TX", "Oncor"},
72+
{0xD553, "Austin, TX", "Austin Energy"},
73+
{0x45F8, "Dallas, TX", "CoServ"},
74+
{0x62C1, "Quebec, CAN", "Hydro-Quebec"},
75+
{0x23D1, "Seattle, WA", "Seattle City Light"},
76+
{0x2C22, "Santa Barbara, CA", "Southern California Edison"},
77+
{0x142A, "Washington", "Puget Sound Energy"},
78+
{0x47F7, "Pennsylvania", "PPL Electric"},
79+
{0x22c6, "Long Island, NY", "PSEG Long Island"},
80+
{0x8819, "Alameda, CA", "Alameda Municipal Power"},
81+
{0x4E2D, "Milwaukee, WI", "We Energies"},
82+
{0x1D65, "Phoenix, AZ", "APS"}};
83+
84+
static int gridstream_checksum(int fulllength, uint16_t length, uint8_t *bits, int adjust)
85+
{
86+
uint16_t crc_count = 0;
87+
int crc_ok = 0;
88+
uint16_t crc;
89+
90+
if ((fulllength - 4 + adjust) < length) {
91+
return DECODE_ABORT_LENGTH;
92+
}
93+
crc = (bits[2 + length + adjust] << 8) | bits[3 + length + adjust];
94+
do {
95+
if (crc16(&bits[4 + adjust], length - 2, 0x1021, known_crc_init[crc_count].value) == crc) {
96+
crc_ok = 1;
97+
}
98+
else {
99+
crc_count++;
100+
}
101+
} while (crc_count < (sizeof(known_crc_init) / sizeof(struct crc_init)) && crc_ok == 0);
102+
if (!crc_ok) {
103+
return DECODE_FAIL_MIC;
104+
}
105+
else {
106+
return crc_count;
107+
}
108+
}
109+
110+
static int gridstream_decode(r_device *decoder, bitbuffer_t *bitbuffer)
111+
{
112+
data_t *data;
113+
uint8_t const preambleV4[] = {
114+
0xAA,
115+
0xAA,
116+
0x00,
117+
0x5F,
118+
0xF0,
119+
};
120+
uint8_t const preambleV5[] = {
121+
0xAA,
122+
0xAA,
123+
0x00,
124+
0x7F,
125+
0xF8,
126+
};
127+
/* Maximum data length is not yet known, but 256 should be a sufficient buffer size. */
128+
uint8_t b[256];
129+
uint16_t stream_len;
130+
char found_crc[5] = "";
131+
char destwanaddress_str[13] = "";
132+
char srcwanaddress_str[13] = "";
133+
char srcaddress_str[9] = "";
134+
char destaddress_str[9] = "";
135+
int srcwanaddress = 0;
136+
uint32_t uptime = 0;
137+
int clock = 0;
138+
int subtype;
139+
unsigned offset;
140+
int protocol_version;
141+
int subtype_mod = 0;
142+
int crcidx;
143+
int decoded_len;
144+
offset = bitbuffer_search(bitbuffer, 0, 0, preambleV4, 36);
145+
if (offset >= bitbuffer->bits_per_row[0]) {
146+
offset = bitbuffer_search(bitbuffer, 0, 0, preambleV5, 37);
147+
if (offset >= bitbuffer->bits_per_row[0]) {
148+
return DECODE_FAIL_SANITY;
149+
}
150+
else {
151+
decoded_len = extract_bytes_uart(bitbuffer->bb[0], offset + 37, bitbuffer->bits_per_row[0] - offset - 37, b);
152+
protocol_version = 5;
153+
}
154+
}
155+
else {
156+
decoded_len = extract_bytes_uart(bitbuffer->bb[0], offset + 36, bitbuffer->bits_per_row[0] - offset - 36, b);
157+
protocol_version = 4;
158+
}
159+
if (decoded_len >= 5) {
160+
switch (b[0]) {
161+
case 0x2A:
162+
subtype = b[1];
163+
if (subtype == 0xD2) {
164+
stream_len = b[2];
165+
subtype_mod = -1;
166+
}
167+
else {
168+
stream_len = (b[2] << 8) | b[3];
169+
}
170+
crcidx = gridstream_checksum(decoded_len, stream_len, b, subtype_mod);
171+
if (crcidx < 0) {
172+
decoder_log(decoder, 1, __func__, "Bad CRC or unknown init value. ");
173+
if ((stream_len == 0x23) && (subtype = 0xAA)) {
174+
/* These data types can be used to find new init values. See comment block on line 67. */
175+
decoder_log_bitrow(decoder, 1, __func__, &b[4], decoded_len * 8, "Use RevEng to find init value.");
176+
}
177+
return DECODE_FAIL_MIC;
178+
}
179+
sprintf(found_crc, "%04x", known_crc_init[crcidx].value);
180+
switch (subtype) {
181+
case 0x55:
182+
sprintf(destwanaddress_str, "%02x%02x%02x%02x%02x%02x", b[5], b[6], b[7], b[8], b[9], b[10]);
183+
sprintf(srcwanaddress_str, "%02x%02x%02x%02x%02x%02x", b[11], b[12], b[13], b[14], b[15], b[16]);
184+
srcwanaddress = 1;
185+
sprintf(srcaddress_str, "%02x%02x%02x%02x", b[24], b[25], b[26], b[27]);
186+
uptime = ((uint32_t)b[18] << 24) | (b[19] << 16) | (b[20] << 8) | b[21];
187+
break;
188+
case 0xD5:
189+
sprintf(destaddress_str, "%02x%02x%02x%02x", b[5], b[6], b[7], b[8]);
190+
sprintf(srcaddress_str, "%02x%02x%02x%02x", b[9], b[10], b[11], b[12]);
191+
if (stream_len == 0x47) {
192+
clock = ((uint32_t)b[14] << 24) | (b[15] << 16) | (b[16] << 8) | b[17];
193+
uptime = ((uint32_t)b[22] << 24) | (b[23] << 16) | (b[24] << 8) | b[25];
194+
sprintf(srcwanaddress_str, "%02x%02x%02x%02x%02x%02x", b[30], b[31], b[32], b[33], b[34], b[35]);
195+
srcwanaddress = 1;
196+
}
197+
break;
198+
}
199+
200+
/* clang-format off */
201+
data = data_make(
202+
"model", "", DATA_STRING, "LandisGyr-GS",
203+
"networkID", "Network ID", DATA_STRING, found_crc,
204+
"location", "Location", DATA_STRING, known_crc_init[crcidx].location,
205+
"provider", "Provider", DATA_STRING, known_crc_init[crcidx].provider,
206+
"subtype", "", DATA_INT, subtype,
207+
"protoversion", "", DATA_INT, protocol_version,
208+
"mic", "Integrity", DATA_STRING, "CRC",
209+
"id", "Source Meter ID", DATA_COND, subtype != 0xD2, DATA_STRING, srcaddress_str,
210+
"wanaddress", "Source Meter WAN ID", DATA_COND, srcwanaddress == 1, DATA_STRING, srcwanaddress_str,
211+
"destaddress", "Target Meter WAN ID", DATA_COND, subtype == 0x55, DATA_STRING, destwanaddress_str,
212+
"destaddress", "Target Meter ID", DATA_COND, subtype == 0xD5, DATA_STRING, destaddress_str,
213+
"timestamp", "Timestamp", DATA_COND, subtype == 0xD5 && stream_len == 0x47, DATA_INT, clock,
214+
"uptime", "Uptime", DATA_COND, uptime > 0, DATA_INT, uptime,
215+
NULL);
216+
/* clang-format on */
217+
218+
decoder_output_data(decoder, data);
219+
break;
220+
}
221+
decoder_log_bitrow(decoder, 0, __func__, b, decoded_len * 8, "Decoded frame data");
222+
// Return 1 if message successfully decoded
223+
return 1;
224+
}
225+
else {
226+
return DECODE_FAIL_SANITY;
227+
}
228+
}
229+
230+
static char const *const output_fields[] = {
231+
"model",
232+
"networkID",
233+
"location",
234+
"provider",
235+
"id",
236+
"subtype",
237+
"wanaddress",
238+
"destaddress",
239+
"uptime",
240+
"srclocation",
241+
"destlocation",
242+
"timestamp",
243+
"protoversion",
244+
"framedata",
245+
"mic",
246+
NULL,
247+
};
248+
249+
r_device const gridstream96 = {
250+
.name = "Landis & Gyr Gridstream Power Meters 9.6k",
251+
.modulation = FSK_PULSE_PCM,
252+
.short_width = 104,
253+
.long_width = 104,
254+
.reset_limit = 20000,
255+
.decode_fn = &gridstream_decode,
256+
.disabled = 0,
257+
.fields = output_fields,
258+
};
259+
260+
r_device const gridstream192 = {
261+
.name = "Landis & Gyr Gridstream Power Meters 19.2k",
262+
.modulation = FSK_PULSE_PCM,
263+
.short_width = 52,
264+
.long_width = 52,
265+
.reset_limit = 20000,
266+
.decode_fn = &gridstream_decode,
267+
.disabled = 0,
268+
.fields = output_fields,
269+
};
270+
271+
r_device const gridstream384 = {
272+
.name = "Landis & Gyr Gridstream Power Meters 38.4k",
273+
.modulation = FSK_PULSE_PCM,
274+
.short_width = 22,
275+
.long_width = 22,
276+
.reset_limit = 20000,
277+
.decode_fn = &gridstream_decode,
278+
.disabled = 0,
279+
.fields = output_fields,
280+
};

0 commit comments

Comments
 (0)