Skip to content

Commit

Permalink
Merge pull request #99 from simonhyde/Implement_FineOffset
Browse files Browse the repository at this point in the history
Implement FineOffset WH2/ClimeMet CM9088
  • Loading branch information
cpainchaud authored Nov 28, 2023
2 parents e90e5a8 + 2447dd1 commit d0ce2f4
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 11 deletions.
15 changes: 12 additions & 3 deletions RFLink/7_Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ void reflect_nibbles(uint8_t message[], unsigned num_bytes)
}
}

void invert_bytes(uint8_t message[], unsigned num_bytes)
{
for (unsigned i = 0; i < num_bytes; ++i)
{
message[i] = ~message[i];
}
}

unsigned extract_nibbles_4b1s(uint8_t *message, unsigned offset_bits, unsigned num_bits, uint8_t *dst)
{
unsigned ret = 0;
Expand Down Expand Up @@ -378,9 +386,9 @@ inline bool value_between(uint32_t value, uint32_t min, uint32_t max)
return (value > min && value < max);
}

bool decode_pwm(uint8_t frame[], uint8_t expectedBitCount, uint16_t const pulses[], const int pulsesCount, int pulseIndex, uint16_t shortPulseMinDuration, uint16_t shortPulseMaxDuration, uint16_t longPulseMinDuration, uint16_t longPulseMaxDuration)
bool decode_pwm(uint8_t frame[], uint8_t expectedBitCount, uint16_t const pulses[], const int pulsesCount, int pulseIndex, uint16_t shortPulseMinDuration, uint16_t shortPulseMaxDuration, uint16_t longPulseMinDuration, uint16_t longPulseMaxDuration, uint8_t bitOffset)
{
if (pulseIndex + expectedBitCount * 2 > pulsesCount)
if (pulseIndex + (expectedBitCount - 1) * 2 > pulsesCount)
{
#ifdef PWM_DEBUG
Serial.print(F("PWM: Not enough pulses: pulseIndex = "));
Expand All @@ -397,8 +405,9 @@ bool decode_pwm(uint8_t frame[], uint8_t expectedBitCount, uint16_t const pulses

const uint8_t bitsPerByte = 8;
//const uint8_t expectedByteCount = expectedBitCount / bitsPerByte;
const uint8_t endBitCount = expectedBitCount + bitOffset;

for(int8_t bitIndex = 0; bitIndex < expectedBitCount; bitIndex++)
for(uint8_t bitIndex = bitOffset; bitIndex < endBitCount; bitIndex++)
{
int currentFrameByteIndex = bitIndex / bitsPerByte;
uint16_t bitDuration = pulses[pulseIndex];
Expand Down
32 changes: 24 additions & 8 deletions RFLink/7_Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ uint8_t reflect4(uint8_t x);
/// @param num_bytes number of bytes to reflect
void reflect_nibbles(uint8_t message[], unsigned num_bytes);

/// Invert each bit in each byte of a number of bytes.
///
/// @param message bytes of message data
/// @param num_bytes number of bytes to invert
void invert_bytes(uint8_t message[], unsigned num_bytes);

/// Unstuff nibbles with 1-bit separator (4B1S) to bytes, returns number of successfully unstuffed nibbles.
///
/// @param message bytes of message data
Expand Down Expand Up @@ -211,10 +217,12 @@ inline bool value_between(uint32_t value, uint32_t min, uint32_t max);
* @param pulses the pulses to decode
* @param pulsesCount the numnber of items inside @pulses
* @param pulseIndex the index of the first pulse to be decoded
* @param shortPulseMinDuration the minimum duration of a half bit
* @param shortPulseMaxDuration the maximum duration of a half bit
* @param longPulseMinDuration the minimum duration of a half bit
* @param longPulseMaxDuration the maximum duration of a half bit
* @param shortPulseMinDuration the minimum duration of a short (low) bit
* @param shortPulseMaxDuration the maximum duration of a short (low) bit
* @param longPulseMinDuration the minimum duration of a long (high) bit
* @param longPulseMaxDuration the maximum duration of a long (high) bit
* @param bitOffset offset (in bits) in output buffer to read first bit into
* @param
* @return true if pulses could be decoded, giving enough bits, false if not or pulses were not of valid lengths
The PWM encoding uses pair of pulses like this:
Expand All @@ -225,18 +233,26 @@ inline bool value_between(uint32_t value, uint32_t min, uint32_t max);
So, for instance, we would receive this: ----__----__----__--____
This gives 4 pairs, hence 4 bits, long/short, long/short, long/short, short/long
Note that for efficiency reasons, this method does not consider the second pulse of the pair and thus does not test
it for duration validity.
Because this method actually doesn't check the length of the second pulse of a PWM, it can also be used for devices
which have a constant-length gap between the data pulse and the next one, ie:
Long then gap : ----___
Short then gap : --___
This method uses the following truth table
Pair | Value
------------+--------
long/short | 1
short/long | 0
long/gap | 1
short/gap | 0
If you need the opposite, simply use the ~ operator on the frame bytes
Note that for efficiency reasons, this method does not consider the second pulse of the pair and thus does not test
it for duration validity.
Bits are placed in the frame in the order they appear, ie MSB first. To illustrate, consider the following set of pulses:
--_-__--_--_--_--_-__--_--_-__-__--_--_--_-__-__-__--_-__--_--_--_-__-__
Expand All @@ -255,7 +271,7 @@ inline bool value_between(uint32_t value, uint32_t min, uint32_t max);
But because the ESP32 is a little endian architecture, the bytes will be reversed two by two (0 - 3, 1 - 2)
Using ntohs or ntohl from <netinet/in.h> is thus recommended in that case
*/
bool decode_pwm(uint8_t frame[], uint8_t expectedBitCount, uint16_t const pulses[], const int pulsesCount, int pulseIndex, uint16_t shortPulseMinDuration, uint16_t shortPulseMaxDuration, uint16_t longPulseMinDuration, uint16_t longPulseMaxDuration);
bool decode_pwm(uint8_t frame[], uint8_t expectedBitCount, uint16_t const pulses[], const int pulsesCount, int pulseIndex, uint16_t shortPulseMinDuration, uint16_t shortPulseMaxDuration, uint16_t longPulseMinDuration, uint16_t longPulseMaxDuration, uint8_t bitOffset = 0);

/**
* Decodes the pulses as a Manchester encoded series of pulses
Expand Down
87 changes: 87 additions & 0 deletions RFLink/Plugins/Plugin_050.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
Fine Offset Electronics WH2 Temperature/Humidity sensor protocol,
also Agimex Rosenborg 66796 (sold in Denmark), collides with WH5,
also ClimeMET CM9088 (Sold in UK),
also TFA Dostmann/Wertheim 30.3157 (Temperature only!) (sold in Germany).
The sensor sends two identical packages of 48 bits each ~48s. The bits are PWM modulated with On Off Keying.
The data is grouped in 6 bytes / 12 nibbles.
[pre] [pre] [type] [id] [id] [temp] [temp] [temp] [humi] [humi] [crc] [crc]
There is an extra, unidentified 7th byte in WH2A packages.
- pre is always 0xFF
- type is always 0x4 (may be different for different sensor type?)
- id is a random id that is generated when the sensor starts
- temp is 12 bit signed magnitude scaled by 10 celsius
- humi is 8 bit relative humidity percentage
Based on
http://lucsmall.com/2012/04/29/weather-station-hacking-part-2/
and
https://github.com/lucsmall/WH2-Weather-Sensor-Library-for-Arduino
Comment detailing protocol from rtl_433 project.
*/
#define FINEOFFSET_PLUGIN_ID 050
#define PLUGIN_DESC_050 "FineOffset Temp/Humidity sensors"

#ifdef PLUGIN_050
#include "../4_Display.h"
#include "../1_Radio.h"
#include "../7_Utils.h"

#define PLUGIN_050_ID "FineOffset"

#define FINEOFFSET_PULSE_COUNT 96

#ifdef PLUGIN_050_DEBUG
#define SerialDebugActivated
#endif

boolean Plugin_050(byte function, const char *string)
{
if (RawSignal.Number == FINEOFFSET_PULSE_COUNT)
{
const int CM_LongLowMinDuration = 1200 / RawSignal.Multiply;
const int CM_LongLowMaxDuration = 1600 / RawSignal.Multiply;
const int CM_ShortHighMinDuration = 300 / RawSignal.Multiply;
const int CM_ShortHighMaxDuration = 600 / RawSignal.Multiply;
byte data[6] = {0, 0, 0, 0, 0, 0};
//Skip the first bit as the lengths tend to vary...
if(!decode_pwm(data, 47, RawSignal.Pulses,RawSignal.Number, 3, CM_ShortHighMinDuration, CM_ShortHighMaxDuration, CM_LongLowMinDuration, CM_LongLowMaxDuration, 1))
return false;
invert_bytes(data, 6);
byte calculated_crc = crc8(data + 1, 4, 0x31, 0);
#ifdef PLUGIN_050_DEBUG
const size_t buflen = sizeof(PLUGIN_050_ID ": packet = ") + 32;
char printbuf[buflen];
snprintf(printbuf, buflen, "%s%02x%02x%02x%02x%02x%02x, CRC=%02x", PLUGIN_088_ID ": packet = ", data[0], data[1], data[2], data[3],data[4], data[5], calculated_crc);
SerialDebugPrintln(printbuf);
#endif
if(calculated_crc != data[5])
{
#ifdef PLUGIN_050_DEBUG
SerialDebugPrintln(PLUGIN_050_ID ": CRC Check Failed");
#endif
return false;
}

uint16_t unitid = (data[1] << 4) | (data[2] >> 4);
uint16_t temperature = ((data[2] & 0xF) << 8) | data[3];
//MSB of 12-bit number actually indicates sign, this isn't standard two's complement
if(temperature & 0x800)
temperature = - (temperature & 0x7FF);
byte humidity = data[4];
display_Header();
display_Name(PLUGIN_050_ID);
display_IDn(unitid, 4); // unit id
display_TEMP(temperature);
display_HUM(humidity);
display_Footer();
}
return false;
}
#endif
1 change: 1 addition & 0 deletions RFLink/Plugins/_Plugin_Config_01.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
#define PLUGIN_047 // Auriol v4
#define PLUGIN_048 // Oregon Weather
#define PLUGIN_049 // Lacrosse TX141
#define PLUGIN_050 // FineOffset WH2 / ClimeMet CM9088 Temperature/Humidity
// -------------------
// Motion Sensors, include when needed
// -------------------
Expand Down

0 comments on commit d0ce2f4

Please sign in to comment.