1 /** @file
2 Blueline PowerCost Monitor protocol.
3
4 Copyright (C) 2020 Justin Brzozoski
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 #include <stdlib.h>
13 #include "fatal.h"
14 #include "decoder.h"
15
16 /**
17 BlueLine Innovations Power Cost Monitor, tested with BLI-28000.
18
19 Much of the groundwork for this implementation was based on reading the source and notes from older
20 implementations, but this implementation was a fresh rewrite by Justin Brzozoski in 2020. I would
21 not have been able to figure this out without the other implementations to look at, but I wanted an
22 implementation that didn't need to know the Kh factor or monitor ID ahead of time, which required
23 changes.
24
25 Some references used include:
26
27 https://github.com/merbanan/rtl_433/pull/38 - an abandoned pull request on rtl_433 by radredgreen
28
29 https://github.com/CapnBry/Powermon433 - a standalone Arduino-based Blueline monitor
30
31 http://scruss.com/blog/2013/12/03/blueline-black-decker-power-monitor-rf-packets/ - the blog post
32 where the other authors were trading notes in the comments
33
34 The IR-reader/sensor will transmit 3 bursts every ~30 seconds. The low-level encoding is on/off
35 keyed pulse-position modulation (OOK_PPM). The on pulses are always 0.5ms, while the off pulses
36 are either 0.5ms for logic 1 or 1.0ms for logic 0. Each burst is 32 bits long. The pauses
37 between the 3 grouped bursts is roughly 100ms.
38
39 Data is sent less significant byte first for multi-byte fields.
40
41 The basic layout of all bursts is as follows:
42
43 - First is a 1 byte header, which is always the value 0xFE.
44 - Second is a 2 byte payload, which is interpreted differently based on the two lowest bits of the first byte
45 - Finally is a 1 byte CRC, calculated across the 2 payload bytes (not the header)
46
47 The CRC is a CRC-8-ATM with polynomial 100000111, but it may be required to modify the payload bytes before
48 calculating it depending on message type.
49
50 There are 4 message types that can be indicated by the 2 lowest bits of the first payload byte:
51 - 0: ID message (payload is not offset)
52 - 1: power message (payload is offset)
53 - 2: temperature/status message (payload is offset)
54 - 3: energy message (payload is offset)
55
56 For the ID message (0), the CRC can be calculated directly on the payload as sent, and when the payload is
57 interpreted as a 16-bit integer it gives the ID of the transmitter. This message is sent when the
58 monitor is first powered on and if the button on the monitor is pressed briefly. If the button on the
59 monitor is held for >10 seconds, the monitor will change it's ID and report the new one.
60
61 While the transmitter ID's are 16-bit, none of them can have any of the two lowest bits set or they
62 would not be able to transmit their ID as message type 0, and when the offset is calculated (see below) they
63 would also change the message type.
64
65 For the 3 other message types, the payload must be offset before calculating the CRC or
66 interpreting the data. The offset is done by treating the whole payload as a single 16-bit integer and
67 then subtracting the ID of the transmitter. After the offset is done, then the CRC may be calculated
68 and the payload may be interpreted.
69
70 Note that if the transmitter's ID isn't known, the code can't easily determine if messages other than an
71 ID payload are good or bad, and can't interpret their data correctly. However, if the "auto" mode is enabled,
72 the system can try to learn the transmitter's ID by various methods. (See USAGE HINTS below)
73
74 For the power message (1), the offset payload gives the number of milliseconds gap between impulses for the most
75 recent impulses seen by the monitor. To convert from this 'gap' to kilowatts, you will need your meter's
76 Kh value. The Kh value is written obviously on the front of most meters, and 1.0 and 7.2 are very common.
77
78 kW = (3600/gap) * Kh
79
80 Note that the 'gap' value clamps to a maximum of 65533 (0xFFFD), so there is a non-zero floor when calculating the
81 kW value using this report. For example, with a Kh of 7.2, the lowest kW value you will ever see when monitoring
82 the 'gap' value is (3600/65533)*7.2 = 0.395kW. If you need power monitoring for impulse rates slower than every
83 65.533 seconds to do things like confirm that your power consumption is 0kW, you need to monitor the impulse
84 counts and timing between energy messages (see 3 below).
85
86 For the temperature message (2), the offset payload gives the temperature in an odd scaling in the last byte,
87 and has some flag bits in the first byte. The only known flag bit is the battery. rtl_433 handles scaling back to
88 degrees celsius automatically.
89
90 For the energy message (3), the offset payload contains a continuously running power impulse accumulator. I'm
91 not sure if there is a way to reset the accumulator. The intended way to use it is to remember the accumulator
92 value at the beginning of a time period, and then subtract that from the value at the end of the time period. The
93 accumulator will roll over to 0 after 65535.
94
95 kWh = 0.001 * (accumulated pulses) * Kh
96
97 Since the Kh value on all meters can vary, we do not handle it in rtl_433 and just report the raw millisecond
98 gap and accumulated impulses as received directly from the monitor.
99
100 ## Usage hints:
101
102 The requirement of knowing the ID before being able to receive a message means that this decoder
103 will generally require a parameter to be useful. When running in the default mode with no parameters,
104 the only message it is able to decode is the one that announces a monitor's ID. So, assuming you can get to
105 the monitor to power cycle it or hit the button, this is the recommended method:
106
107 - 1) Start rtl_433
108 - 2) Tap the button or power cycle the monitor
109 - 3) Look for the rtl_433 output indicating the BlueLine monitor ID and note the ID field
110 - 4) Stop rtl_433
111 - 5) Restart rtl_433, explicitly passing the ID as a parameter to this decoder
112
113 For example, if you see the ID 45364 in step 3, you would start the decoder with a command like:
114
115 rtl_433 -R 176:45364
116
117 If you are unable to access the monitor to have it send the ID message, you can also use the "auto" parameter:
118
119 rtl_433 -vv -R 176:auto
120
121 Verbose mode should be specified first on the command line to see what the "auto" mode is doing.
122
123 The auto parameter will try to brute-force the ID on any messages that look like they are from a
124 BlueLine monitor. This method usually succeeds within a few minutes, but is likely to get false positives
125 if there is more than one monitor in range or the messages being received are all identical (i.e. if the
126 meter is continuously reporting 0 watts). If it succeeds, it will start reporting data with the new ID,
127 which you should then use as a parameter when you re-run rtl_433 in the future.
128
129 Finally, passing a parameter to this decoder requires specifying it explicitly, which normally disables all
130 other default decoders. If you want to pass an option to this decoder without disabling all the other defaults,
131 the simplest method is to explicity exclude this one decoder (which implicitly says to leave all other defaults
132 enabled), then add this decoder back with a parameter. The command line looks like this:
133
134 rtl_433 -R -176 -R 176:45364
135
136 */
137
138 #define BLUELINE_BITLEN 32
139 #define BLUELINE_STARTBYTE 0xFE
140 #define BLUELINE_CRC_POLY 0x07
141 #define BLUELINE_CRC_INIT 0x00
142 #define BLUELINE_CRC_BYTELEN 2
143 #define BLUELINE_TXID_MSG 0x00
144 #define BLUELINE_POWER_MSG 0x01
145 #define BLUELINE_TEMPERATURE_MSG 0x02
146 #define BLUELINE_ENERGY_MSG 0x03
147
148 #define BLUELINE_ID_STEP_SIZE 4
149 #define MAX_POSSIBLE_BLUELINE_IDS (65536/BLUELINE_ID_STEP_SIZE)
150 #define BLUELINE_ID_GUESS_THRESHOLD 4
151
152 struct blueline_stateful_context {
153 unsigned id_guess_hits[MAX_POSSIBLE_BLUELINE_IDS];
154 uint16_t current_sensor_id;
155 unsigned searching_for_new_id;
156 };
157
rev_crc8(uint8_t const message[],unsigned nBytes,uint8_t polynomial,uint8_t remainder)158 static uint8_t rev_crc8(uint8_t const message[], unsigned nBytes, uint8_t polynomial, uint8_t remainder) {
159 unsigned byte, bit;
160
161 // Run a CRC backwards to find out what the init value would have been.
162 // Alternatively, put a known init value in the first byte, and it will
163 // return a value that could be used in that place to get that init.
164
165 // This logic only works assuming the polynomial has the lowest bit set,
166 // Which should be true for most CRC polynomials, but let's be safe...
167 if ((polynomial & 0x01) == 0) {
168 fprintf(stderr,"Cannot run reverse CRC-8 with this polynomial!\n");
169 return 0xFF;
170 }
171 polynomial = (polynomial >> 1) | 0x80;
172
173 byte = nBytes;
174 while (byte--) {
175 bit = 8;
176 while (bit--) {
177 if (remainder & 0x01) {
178 remainder = (remainder >> 1) ^ polynomial;
179 } else {
180 remainder = remainder >> 1;
181 }
182
183 }
184 remainder ^= message[byte];
185 }
186 return remainder;
187 }
188
guess_blueline_id(r_device * decoder,const uint8_t * current_row)189 static uint16_t guess_blueline_id(r_device *decoder, const uint8_t *current_row)
190 {
191 struct blueline_stateful_context *const context = decoder->decode_ctx;
192 const uint16_t start_value = ((current_row[2] << 8) | current_row[1]);
193 const uint8_t recv_crc = current_row[3];
194 const uint8_t rcv_msg_type = (current_row[1] & 0x03);
195 uint16_t working_value;
196 uint8_t working_buffer[2];
197 uint8_t reverse_crc_result;
198 uint16_t best_id;
199 unsigned best_hits;
200 unsigned num_at_best_hits;
201 unsigned high_byte_steps;
202
203 // TL;DR - Try all possible IDs against every incoming message, and count how many times each one
204 // succeeds. If one of them passes a threshold, assume it must be the right one and return it.
205
206 // We do some optimizations to try and do all those checks quickly, but it's still about the
207 // same as doing a CRC across 512 bytes of data for every 2 byte payload received.
208
209 working_buffer[0] = BLUELINE_CRC_INIT;
210 working_buffer[1] = current_row[2];
211 high_byte_steps = 256;
212 best_id = 0;
213 best_hits = 0;
214 num_at_best_hits = 0;
215 while (high_byte_steps--) {
216 reverse_crc_result = rev_crc8(working_buffer, BLUELINE_CRC_BYTELEN, BLUELINE_CRC_POLY, recv_crc);
217 // Would this byte value have been usable while still being the same type of message we received?
218 if ((reverse_crc_result & 0x03) == rcv_msg_type) {
219 working_value = ((working_buffer[1] << 8) | reverse_crc_result);
220 working_value = start_value - working_value;
221 context->id_guess_hits[(working_value/BLUELINE_ID_STEP_SIZE)]++;
222 if (context->id_guess_hits[(working_value/BLUELINE_ID_STEP_SIZE)] >= best_hits) {
223 if (context->id_guess_hits[(working_value/BLUELINE_ID_STEP_SIZE)] > best_hits) {
224 best_hits = context->id_guess_hits[(working_value / BLUELINE_ID_STEP_SIZE)];
225 best_id = working_value;
226 num_at_best_hits = 1;
227 } else {
228 num_at_best_hits++;
229 }
230 }
231 }
232 working_buffer[1] += 1;
233 }
234
235 if (decoder->verbose) {
236 fprintf(stderr, "Attempting Blueline autodetect: best_hits=%u num_at_best_hits=%u\n", best_hits, num_at_best_hits);
237 }
238 return ((best_hits >= BLUELINE_ID_GUESS_THRESHOLD) && (num_at_best_hits == 1)) ? best_id : 0;
239 }
240
blueline_decode(r_device * decoder,bitbuffer_t * bitbuffer)241 static int blueline_decode(r_device *decoder, bitbuffer_t *bitbuffer)
242 {
243 struct blueline_stateful_context *const context = decoder->decode_ctx;
244 data_t *data;
245 int row_index;
246 uint8_t *current_row;
247 int payloads_decoded = 0;
248 int most_applicable_failure = 0;
249 uint8_t calc_crc;
250 uint16_t offset_payload_u16 = 0;
251 uint8_t offset_payload_u8[BLUELINE_CRC_BYTELEN] = {0};
252
253 // Blueline uses inverted 0/1
254 bitbuffer_invert(bitbuffer);
255
256 // Look at each row we just received independently
257 for (row_index = 0; row_index < bitbuffer->num_rows; row_index++) {
258 current_row = bitbuffer->bb[row_index];
259
260 // All valid rows will have a fixed length and start with the same byte
261 if ((bitbuffer->bits_per_row[row_index] != BLUELINE_BITLEN) || (current_row[0] != BLUELINE_STARTBYTE)) {
262 if (DECODE_ABORT_LENGTH < most_applicable_failure) {
263 most_applicable_failure = DECODE_ABORT_LENGTH;
264 }
265 continue;
266 }
267
268 // We need to know which type of message to decide how to check CRC
269 const unsigned message_type = (current_row[1] & 0x03);
270 const uint8_t recv_crc = current_row[3];
271
272 if (message_type == BLUELINE_TXID_MSG) {
273 // No offset required before CRC or data handling
274 calc_crc = crc8(¤t_row[1], BLUELINE_CRC_BYTELEN, BLUELINE_CRC_POLY, BLUELINE_CRC_INIT);
275 } else {
276 // Offset required before CRC or datahandling
277 offset_payload_u16 = ((current_row[2] << 8) | current_row[1]) - context->current_sensor_id;
278 offset_payload_u8[0] = (offset_payload_u16 & 0xFF);
279 offset_payload_u8[1] = (offset_payload_u16 >> 8);
280 calc_crc = crc8(&offset_payload_u8[0], BLUELINE_CRC_BYTELEN, BLUELINE_CRC_POLY, BLUELINE_CRC_INIT);
281 }
282
283 // If the CRC didn't match up, ignore this row!
284 if (calc_crc != recv_crc) {
285 if ((context->searching_for_new_id) && (message_type != BLUELINE_TXID_MSG)) {
286 uint16_t id_guess = guess_blueline_id(decoder, current_row);
287 if (id_guess != 0) {
288 if (decoder->verbose) {
289 fprintf(stderr,"Switching to auto-detected Blueline ID %u\n", id_guess);
290 }
291 context->current_sensor_id = id_guess;
292 context->searching_for_new_id = 0;
293 }
294 }
295 if (DECODE_FAIL_MIC < most_applicable_failure) {
296 most_applicable_failure = DECODE_FAIL_MIC;
297 }
298 continue;
299 }
300
301 if (message_type == BLUELINE_TXID_MSG) {
302 const uint16_t received_sensor_id = ((current_row[2] << 8) | current_row[1]);
303 /* clang-format off */
304 data = data_make(
305 "model", "", DATA_STRING, "Blueline-PowerCost",
306 "id", "", DATA_INT, received_sensor_id,
307 "mic", "Integrity", DATA_STRING, "CRC",
308 NULL);
309 /* clang-format on */
310 decoder_output_data(decoder, data);
311 payloads_decoded++;
312 if (context->searching_for_new_id) {
313 if (decoder->verbose) {
314 fprintf(stderr,"Switching to received Blueline ID %u\n", received_sensor_id);
315 }
316 context->current_sensor_id = received_sensor_id;
317 context->searching_for_new_id = 0;
318 }
319 } else if (message_type == BLUELINE_POWER_MSG) {
320 const uint16_t ms_per_pulse = offset_payload_u16;
321 /* clang-format off */
322 data = data_make(
323 "model", "", DATA_STRING, "Blueline-PowerCost",
324 "id", "", DATA_INT, context->current_sensor_id,
325 "gap", "", DATA_INT, ms_per_pulse,
326 "mic", "Integrity", DATA_STRING, "CRC",
327 NULL);
328 /* clang-format on */
329 decoder_output_data(decoder, data);
330 payloads_decoded++;
331 } else if (message_type == BLUELINE_TEMPERATURE_MSG) {
332 // TODO - Confirm battery flag is working properly
333
334 // These were the estimates from Powermon433.
335 // But they didn't line up perfectly with my LCD display.
336 //
337 // A: deg_f = 0.823 * recvd_temp - 28.63
338 // B: deg_c = 0.457 * recvd_temp - 33.68
339
340 // I logged raw radio values and their resulting display temperatures
341 // for a range of -13 to 34 degrees C, and it's not perfectly linear.
342 // It's not so far off that I think we should use something other than
343 // a linear fit, but it's likely got some fixed-point truncation errors
344 // in the official display code.
345 //
346 // I put all the points I had into Excel and asked for the best
347 // linear estimate, and it gave me roughly this:
348 //
349 // deg_C = 0.436 * recvd_temp - 30.36
350
351 // In case anyone else wants to continue try and find a better equation,
352 // a full copy of my logged data is in the comments of this GitHub pull
353 // request:
354 //
355 // https://github.com/merbanan/rtl_433/pull/1590
356
357 const uint8_t temperature = offset_payload_u8[1];
358 const uint8_t flags = offset_payload_u8[0] >> 2;
359 const uint8_t battery = (flags & 0x20) >> 5;
360 const float temperature_C = (0.436 * temperature) - 30.36;
361 /* clang-format off */
362 data = data_make(
363 "model", "", DATA_STRING, "Blueline-PowerCost",
364 "id", "", DATA_INT, context->current_sensor_id,
365 "flags", "", DATA_FORMAT, "%02x", DATA_INT, flags,
366 "battery_ok", "Battery", DATA_INT, !battery,
367 "temperature_C", "", DATA_DOUBLE, temperature_C,
368 "mic", "Integrity", DATA_STRING, "CRC",
369 NULL);
370 /* clang-format on */
371 decoder_output_data(decoder, data);
372 payloads_decoded++;
373 } else { // Assume BLUELINE_ENERGY_MSG
374 // (The lowest two bits of the pulse count will always be the same because message_type is overlaid there)
375 const uint16_t pulses = offset_payload_u16;
376 /* clang-format off */
377 data = data_make(
378 "model", "", DATA_STRING, "Blueline-PowerCost",
379 "id", "", DATA_INT, context->current_sensor_id,
380 "impulses", "", DATA_INT, pulses,
381 "mic", "Integrity", DATA_STRING, "CRC",
382 NULL);
383 /* clang-format on */
384 decoder_output_data(decoder, data);
385 payloads_decoded++;
386 }
387 }
388
389 return ((payloads_decoded > 0) ? payloads_decoded : most_applicable_failure);
390 }
391
392 static char *output_fields[] = {
393 "model",
394 "id",
395 "flags",
396 "gap",
397 "impulses",
398 "battery_ok",
399 "temperature_C",
400 "mic",
401 NULL,
402 };
403
404 r_device blueline;
405
blueline_create(char * arg)406 static r_device *blueline_create(char *arg)
407 {
408 r_device *r_dev = create_device(&blueline);
409 if (!r_dev) {
410 fprintf(stderr, "blueline_create() failed\n");
411 return NULL; // NOTE: returns NULL on alloc failure.
412 }
413
414 struct blueline_stateful_context *context = malloc(sizeof(*context));
415 if (!context) {
416 WARN_MALLOC("blueline_create()");
417 free(r_dev);
418 return NULL; // NOTE: returns NULL on alloc failure.
419 }
420 memset(context,0,sizeof(*context));
421 r_dev->decode_ctx = context;
422
423 if (arg != NULL) {
424 if (strcmp(arg, "auto") == 0) {
425 // Setup for auto identification
426 context->searching_for_new_id = 1;
427 //fprintf(stderr, "Blueline decoder will try to autodetect ID.\n");
428 } else {
429 // Assume user is trying to pass in hex ID
430 context->current_sensor_id = strtoul(arg, NULL, 0);
431 //fprintf(stderr, "Blueline decoder using ID %u\n", context->current_sensor_id);
432 }
433 }
434
435 return r_dev;
436 }
437
438 r_device blueline = {
439 .name = "BlueLine Innovations Power Cost Monitor",
440 .modulation = OOK_PULSE_PPM,
441 .short_width = 500,
442 .long_width = 1000,
443 .gap_limit = 2000,
444 .reset_limit = 8000,
445 .decode_fn = &blueline_decode,
446 .create_fn = &blueline_create,
447 .disabled = 0,
448 .fields = output_fields,
449 };
450