Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Holman ws5029, Add support for AOK-5056 and correction for Emax #2419

Merged
merged 16 commits into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ See [CONTRIBUTING.md](./docs/CONTRIBUTING.md).
[131] Microchip HCS200/HCS300 KeeLoq Hopping Encoder based remotes
[132] TFA Dostmann 30.3196 T/H outdoor sensor
[133] Rubicson 48659 Thermometer
[134] Holman Industries iWeather WS5029 weather station (newer PCM)
[134] AOK Weather Station rebrand Holman Industries iWeather WS5029, Conrad AOK-5056, Optex 990018
[135] Philips outdoor temperature sensor (type AJ7010)
[136] ESIC EMT7110 power meter
[137] Globaltronics QUIGG GT-TMBBQ-05
Expand Down
266 changes: 187 additions & 79 deletions src/devices/holman_ws5029.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @file
Decoder for Holman Industries WS5029 weather station.
AOK Electronic Limited weather station.

Copyright (C) 2023 Bruno OCTAU (ProfBoc75) (improve integrity check for all devices here and add support for AOK-5056 weather station PR #2419)
Copyright (C) 2023 Christian W. Zuckschwerdt <zany@triq.net> ( reverse galois and xor_shift_bytes check algorithms PR #2419)
Copyright (C) 2019 Ryan Mounce <ryan@mounce.com.au> (PCM version)
Copyright (C) 2018 Brad Campbell (PWM version)

Expand All @@ -10,30 +12,37 @@
(at your option) any later version.
*/
/**
Decoder for Holman Industries WS5029 weather station,
a.k.a. Holman iWeather Station.
https://www.holmanindustries.com.au/products/iweather-station/
AOK Electronic Limited weather station.

Appears to be related to the Fine Offset WH1080 and Digitech XC0348.
Known Rebrand compatible with:
- Holman iWeather Station ws5029. https://www.holmanindustries.com.au/products/iweather-station/
- Conrad Renkforce AOK-5056
- Optex Electronique 990018 SM-018 5056

Appears to be related to the Fine pos WH1080 and Digitech XC0348.

- Modulation: FSK PCM
- Frequency: 917.0 MHz +- 40 kHz
- 10 kb/s bitrate, 100 us symbol/bit time

A transmission burst is sent every 57 seconds. Each burst consists of 3
repititions of the same 192 bit "package" separated by a 1 ms gap.
repetitions of the same "package" separated by a 1 ms gap.
The length of 196 or 218 bits depends on the device type.

Package format:
- Preamble {48}0xAAAAAAAAAAAA
- Header {24}0x98F3A5
- Payload {96} see below
- Checksum {8} unidentified
- Trailer/Postamble {16} ???
- Payload {96 or 146} see below
- zeros {36} 0 with battery ?
- Checksum/CRC {8} xor 12 bytes then reverse Galois algorithm (gen = 0x00, key = 0x31) PR #2419
- Trailer/postamble {20} direction (previous ?) and 3 zeros

Payload format:
Payload format: Without UV Lux sensor

Fixed Values 0x : AA AA AA AA AA AA 98 F3 A5

Byte (dec) 09 10 11 12 13 14 15 16 17 18 19 20
Nibble key II II CC CH HR RR WW Dx xx xx xx xx
Byte position : 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
Payload : II II CC CH HR RR WW Dx xx xx ?x xx ss 0d 00 0

- IIII station ID (randomised on each battery insertion)
- CCC degrees C, signed, in multiples of 0.1 C
Expand All @@ -42,80 +51,157 @@ Payload format:
- WW wind speed in km/h
- D wind direction (0 = N, 4 = E, 8 = S, 12 = W)
- xxxxxxxxx ???, usually zero
- ss xor 12 bytes then reverse Galois algorithm (gen = 0x00 , key = 0x31) PR #2419

Payload format: With UV Lux sensor

Fixed Values 0x : AA AA AA AA AA AA 98 F3 A5

Byte position : 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18
Payload : II II CC CH HR RR WW | | NN SS 0D 00 00 00 00 0
+-----------+ +-------------+
| |
| 07 08 09 10 |
bits details : DDDDUUUU ULLLLLLL LLLLLLLL LLBBNNNN

- I station ID (randomised on each battery insertion)
- C degrees C, signed, in multiples of 0.1 C
- H humidity %
- R cumulative rain in mm
- W wind speed in km/h
- D wind direction (0 = N, 4 = E, 8 = S, 12 = W)
- U Index UV
- L Lux
- B Battery
- N Payload number, increase at each message 000->FFF but not always, strange behavior. no clue
- S xor 12 bytes then reverse Galois algorithm (gen = 0x00 , key = 0x31) PR #2419
- D Previous Wind direction
- Fixed values to 9 zeros

To get raw data
$ rtl_433 -f 917M -X 'name=WS5029,modulation=FSK_PCM,short=100,long=100,preamble={48}0xAAAAAAAAAAAA,reset=19200'
$ rtl_433 -f 917M -X 'name=AOK,modulation=FSK_PCM,short=100,long=100,preamble={48}0xAAAAAA98F3A5,reset=22000'

@sa holman_ws5029pwm_decode()

*/

#include "decoder.h"

// see #2419 for more details about the xor_shift_bytes , used by PWM device
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 14 now needs to be
/** int holman_ws5029pcm_decode(r_device *decoder, bitbuffer_t *bitbuffer)
otherwise the doc-comment above attaches to this helper function

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, but I moved the xor_shift_bytes function to the bottom and enrich both doc-comments for PCM and PWM.
Let me know.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good, thanks. One thing: %.01f is not a sensible format ;) It is often in our code but it should be %.1f.

Copy link
Collaborator Author

@ProfBoc75 ProfBoc75 Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was a copy paste of %.01f from the initial author, I just commit these "minor" corrections.

static uint8_t xor_shift_bytes(uint8_t const message[], unsigned num_bytes, uint8_t shift_up)
{
uint8_t result0 = 0;
for (unsigned i = 0; i < num_bytes; i += 2) {
result0 ^= message[i];
}
uint8_t result1 = 0;
for (unsigned i = 1; i < num_bytes; i += 2) {
result1 ^= message[i];
}
uint8_t resultx = 0;
for (unsigned j = 0; j < 7; ++j) {
if (shift_up & (1 << j))
resultx ^= result0 << (j + 1);
}
return result0 ^ result1 ^ resultx;
}

static int holman_ws5029pcm_decode(r_device *decoder, bitbuffer_t *bitbuffer)
{
int const wind_dir_degr[] = {0, 23, 45, 68, 90, 113, 135, 158, 180, 203, 225, 248, 270, 293, 315, 338};
uint8_t const preamble[] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x98, 0xF3, 0xA5};
uint8_t const preamble[] = {0xAA, 0xAA, 0xAA, 0x98, 0xF3, 0xA5};

data_t *data;
uint8_t b[24];
uint8_t b[18];

if (bitbuffer->num_rows != 1) {
if (decoder->verbose) {
fprintf(stderr, "%s: wrong number of rows (%d)\n", __func__, bitbuffer->num_rows);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always use log functions

}
return DECODE_ABORT_EARLY;
}

unsigned bits = bitbuffer->bits_per_row[0];

// FSK sometimes decodes an extra bit at the start
// and likely extra 2-4 bits at the end
// let's allow for the leading bit and the whole gap period
if (bits < 192 || bits > 203) {
if (bits < 192 ) { // too small
return DECODE_ABORT_LENGTH;
}

unsigned offset = bitbuffer_search(bitbuffer, 0, 0, preamble, sizeof (preamble) * 8);
if (offset + 192 > bitbuffer->bits_per_row[0]) {
unsigned pos = bitbuffer_search(bitbuffer, 0, 0, preamble, sizeof (preamble) * 8);

if (pos >= bits) {
return DECODE_ABORT_EARLY;
}
bitbuffer_extract_bytes(bitbuffer, 0, offset, b, 192);

// byte 21 looks like a checksum - no success with brute force
/*
for (uint8_t firstbyte = 0; firstbyte < 21; firstbyte++) {
for (uint8_t poly=0; poly<255; poly++) {
if (crc8(&b[firstbyte], 21-firstbyte, poly, 0x00) == b[21]) {
decoder_logf(decoder, 3, __func__, "CORRECT CRC8 with offset %u poly 0x%x", firstbyte, poly);
}
if (crc8le(&b[firstbyte], 21-firstbyte, poly, 0x00) == b[21]) {
decoder_logf(decoder, 3, __func__, "CORRECT CRC8LE with offset %u poly 0x%x", firstbyte, poly);
}
}
}
*/

int device_id = (b[9] << 8) | b[10];
int temp_raw = (int16_t)((b[11] << 8) | (b[12] & 0xf0)); // uses sign-extend
float temp_c = (temp_raw >> 4) * 0.1f;
int humidity = ((b[12] & 0x0f) << 4) | ((b[13] & 0xf0) >> 4);
int rain_raw = ((b[13] & 0x0f) << 8) | b[14];
float rain_mm = rain_raw * 0.79f;
int speed_kmh = b[15];
int direction_deg = wind_dir_degr[(b[16] & 0xf0) >> 4];
decoder_logf(decoder, 2, __func__, "Found AOK preamble pos: %d", pos);

/* clang-format off */
data = data_make(
"model", "", DATA_STRING, "Holman-WS5029",
"id", "StationID", DATA_FORMAT, "%04X", DATA_INT, device_id,
"temperature_C", "Temperature", DATA_FORMAT, "%.01f C", DATA_DOUBLE, temp_c,
"humidity", "Humidity", DATA_FORMAT, "%u %%", DATA_INT, humidity,
"rain_mm", "Total rainfall", DATA_FORMAT, "%.01f mm", DATA_DOUBLE, rain_mm,
"wind_avg_km_h", "Wind avg speed", DATA_FORMAT, "%u km/h", DATA_INT, speed_kmh,
"wind_dir_deg", "Wind Direction", DATA_INT, direction_deg,
NULL);
/* clang-format on */
pos += sizeof(preamble) * 8;

decoder_output_data(decoder, data);
return 1;
bitbuffer_extract_bytes(bitbuffer, 0, pos, b, sizeof(b) * 8);

uint8_t chk_digest = b[12];
uint8_t chk_calc = xor_bytes(b, 12);
// reverse Galois algorithm then (gen = 0x00, key = 0x31) PR #2419
int chk_expected = lfsr_digest8_reflect(&chk_calc, 1, 0x00, 0x31);

if (chk_expected != chk_digest) {
return DECODE_FAIL_MIC;
}

int device_id = (b[0] << 8) | b[1];
int temp_raw = (int16_t)((b[2] << 8) | (b[3] & 0xf0)); // uses sign-extend
float temp_c = (temp_raw >> 4) * 0.1f;
int humidity = ((b[3] & 0x0f) << 4) | ((b[4] & 0xf0) >> 4);
int rain_raw = ((b[4] & 0x0f) << 8) | b[5];
float speed_kmh = (float)b[6];
int direction_deg = wind_dir_degr[(b[7] & 0xf0) >> 4];

if (bits < 200) { // model without UV LUX
float rain_mm = rain_raw * 0.79f;

/* clang-format off */
data = data_make(
"model", "", DATA_STRING, "Holman-WS5029",
"id", "StationID", DATA_FORMAT, "%04X", DATA_INT, device_id,
"temperature_C", "Temperature", DATA_FORMAT, "%.1f C", DATA_DOUBLE, temp_c,
"humidity", "Humidity", DATA_FORMAT, "%u %%", DATA_INT, humidity,
"rain_mm", "Total rainfall", DATA_FORMAT, "%.1f mm", DATA_DOUBLE, rain_mm,
"wind_avg_km_h", "Wind avg speed", DATA_FORMAT, "%.1f km/h", DATA_DOUBLE, speed_kmh,
"wind_dir_deg", "Wind Direction", DATA_INT, direction_deg,
"mic", "Integrity", DATA_STRING, "CHECKSUM",
NULL);
/* clang-format on */

decoder_output_data(decoder, data);
return 1;
}
else if (bits < 221) { // model with UV LUX
float rain_mm = rain_raw * 1.0f;
int uv_index = ((b[7] & 0x07) << 1) | ((b[8] & 0x80) >> 7);
int light_lux = ((b[8] & 0x7F) << 10) | (b[9] << 2) | ((b[10] & 0xC0) >> 6);
int battery_low = ((b[10] & 0x30) >> 4);
int counter = ((b[10] & 0x0f) << 8 | b[11]);
/* clang-format off */
data = data_make(
"model", "", DATA_STRING, "AOK-5056",
"id", "StationID", DATA_FORMAT, "%04X", DATA_INT, device_id,
"temperature_C", "Temperature", DATA_FORMAT, "%.1f C", DATA_DOUBLE, temp_c,
"humidity", "Humidity", DATA_FORMAT, "%u %%", DATA_INT, humidity,
"rain_mm", "Total rainfall", DATA_FORMAT, "%.1f mm", DATA_DOUBLE, rain_mm,
"wind_avg_km_h", "Wind avg speed", DATA_FORMAT, "%.1f km/h", DATA_DOUBLE, speed_kmh,
"wind_dir_deg", "Wind Direction", DATA_INT, direction_deg,
"uv", "UV Index", DATA_FORMAT, "%u", DATA_INT, uv_index,
"light_lux", "Lux", DATA_FORMAT, "%u", DATA_INT, light_lux,
"counter", "Counter", DATA_FORMAT, "%u", DATA_INT, counter,
"battery_ok", "battery", DATA_FORMAT, "%u", DATA_INT, !battery_low,
"mic", "Integrity", DATA_STRING, "CHECKSUM",
NULL);
/* clang-format on */

decoder_output_data(decoder, data);
return 1;
}
return 0;
}

static char const *output_fields[] = {
Expand All @@ -127,12 +213,15 @@ static char const *output_fields[] = {
"rain_mm",
"wind_avg_km_h",
"wind_dir_deg",
"uv",
"light_lux",
"counter",
"mic",
NULL,
};

r_device const holman_ws5029pcm = {
.name = "Holman Industries iWeather WS5029 weather station (newer PCM)",
.name = "AOK Weather Station rebrand Holman Industries iWeather WS5029, Conrad AOK-5056, Optex 990018",
.modulation = FSK_PULSE_PCM,
.short_width = 100,
.long_width = 100,
Expand All @@ -144,26 +233,40 @@ r_device const holman_ws5029pcm = {
/**
Holman Industries WS5029 weather station using PWM.

- The checksum used is an xor of all 11 bytes.
- The bottom nybble results in 0. The top does not
- and I've been unable to figure out why. We only
- check the bottom nybble therefore.
- Have tried all permutations of init/poly for lfsr8 & crc8
- Rain is 0.79mm / count
618 counts / 488.2mm - 190113 - Multiplier is exactly 0.79
- Wind is discrete kph
- Preamble is 0xaa 0xa5. Device is 0x98
Package format: (invert)
- Preamble {24} 0xAAA598
- Payload {56} [ see below ]
- Checksum/CRC {8} xor_shift_bytes (key = 0x18) PR #2419
- Trailer/postamble {8} 0x00 or 0x80

Payload format:

Byte position : 00 01 02[03 04 05 06 07 08 09]10 11
Payload : AA A5 98 II BC CC HH RR RW WD SS 00

- I station ID
- B battery low indicator
- C degrees C, signed, in multiples of 0.1 C
- H Humidity 0-100 %
- R Rain is 0.79mm / count , 618 counts / 488.2mm - 190113 - Multiplier is exactly 0.79
- W Wind speed in km/h
- D Wind direction, clockwise from North, in multiples of 22.5 deg
- S xor_shift_bytes , see PR #2419

To get the raw data :
$ rtl_433 -f 433.92M -X "n=Holman-WS5029-PWM,m=FSK_PWM,s=488,l=976,g=2000,r=6000,invert"

*/

static int holman_ws5029pwm_decode(r_device *decoder, bitbuffer_t *bitbuffer)
{
uint8_t const preamble[] = {0x55, 0x5a, 0x67}; // Preamble/Device inverted

data_t *data;
uint8_t *b;
uint16_t temp_raw;
int id, humidity, speed_kmh, wind_dir, battery_low;
float temp_c, rain_mm;
int id, humidity, wind_dir, battery_low;
float temp_c, rain_mm, speed_kmh;

// Data is inverted, but all these checks can be performed
// and validated prior to inverting the buffer. Invert
Expand All @@ -178,31 +281,36 @@ static int holman_ws5029pwm_decode(r_device *decoder, bitbuffer_t *bitbuffer)
if (memcmp(b, preamble, 3))
return DECODE_FAIL_SANITY;

// Test Checksum.
if ((xor_bytes(b, 11) & 0xF) ^ 0xF)
return DECODE_FAIL_MIC;

// Invert data for processing
bitbuffer_invert(bitbuffer);

uint8_t chk_digest = b[10];
// xor_shift_bytes , see PR #2419
int chk_calc = xor_shift_bytes(b, 10, 0x18);
//fprintf(stderr, "%s: 11th byte %02x chk_calc %02x \n", __func__, chk_digest, chk_calc );

if (chk_calc != chk_digest) {
return DECODE_FAIL_MIC;
}

id = b[3]; // changes on each power cycle
battery_low = (b[4] & 0x80); // High bit is low battery indicator
temp_raw = (int16_t)(((b[4] & 0x0f) << 12) | (b[5] << 4)); // uses sign-extend
temp_c = (temp_raw >> 4) * 0.1f; // Convert sign extended int to float
humidity = b[6]; // Simple 0-100 RH
rain_mm = ((b[7] << 4) + (b[8] >> 4)) * 0.79f; // Multiplier tested empirically over 618 pulses
speed_kmh = ((b[8] & 0xF) << 4) + (b[9] >> 4); // In discrete kph
rain_mm = ((b[7] << 4) + (b[8] >> 4)) * 0.79f; // Multiplier tested empirically over 618 pulses
speed_kmh = (float)(((b[8] & 0xF) << 4) + (b[9] >> 4)); // In discrete kph
wind_dir = b[9] & 0xF; // 4 bit wind direction, clockwise from North

/* clang-format off */
data = data_make(
"model", "", DATA_STRING, "Holman-WS5029",
"id", "", DATA_INT, id,
"battery_ok", "Battery", DATA_INT, !battery_low,
"temperature_C", "Temperature", DATA_FORMAT, "%.01f C", DATA_DOUBLE, temp_c,
"humidity", "Humidity", DATA_FORMAT, "%u %%", DATA_INT, humidity,
"rain_mm", "Total rainfall", DATA_FORMAT, "%.01f mm", DATA_DOUBLE, rain_mm,
"wind_avg_km_h", "Wind avg speed", DATA_FORMAT, "%u km/h", DATA_INT, speed_kmh,
"temperature_C", "Temperature", DATA_FORMAT, "%.01f C", DATA_DOUBLE, temp_c,
"humidity", "Humidity", DATA_FORMAT, "%u %%", DATA_INT, humidity,
"rain_mm", "Total rainfall", DATA_FORMAT, "%.01f mm", DATA_DOUBLE, rain_mm,
"wind_avg_km_h", "Wind avg speed", DATA_FORMAT, "%.01f km/h", DATA_DOUBLE, speed_kmh,
"wind_dir_deg", "Wind Direction", DATA_INT, (int)(wind_dir * 22.5),
"mic", "Integrity", DATA_STRING, "CHECKSUM",
NULL);
Expand Down