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