From 6ea9ebb39fe0ee56fe7723e04b2c991e974a8718 Mon Sep 17 00:00:00 2001 From: Daniel Wallner <14888585+danielwallner@users.noreply.github.com> Date: Sun, 25 Feb 2024 00:37:42 +0100 Subject: [PATCH] Add support for Bang & Olufsen protocol. --- src/IRrecv.cpp | 4 + src/IRrecv.h | 6 + src/IRremoteESP8266.h | 11 +- src/IRsend.cpp | 7 + src/IRsend.h | 9 + src/IRtext.cpp | 2 + src/ir_BangOlufsen.cpp | 416 +++++++++++++++++++++++++++++++++++ src/locale/defaults.h | 3 + test/IRsend_test.cpp | 8 + test/ir_BangOlufsen_test.cpp | 248 +++++++++++++++++++++ 10 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 src/ir_BangOlufsen.cpp create mode 100644 test/ir_BangOlufsen_test.cpp diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index 173526104..086830f08 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -1185,6 +1185,10 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting York decode"); if (decodeYork(results, offset, kYorkBits)) return true; #endif // DECODE_YORK +#if DECODE_BANG_OLUFSEN + DPRINTLN("Attempting Bang & Olufsen decode"); + if (decodeBangOlufsen(results, offset)) return true; +#endif // DECODE_BANG_OLUFSEN // Typically new protocols are added above this line. } #if DECODE_HASH diff --git a/src/IRrecv.h b/src/IRrecv.h index 7adf5eb1b..7d19bfa24 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -883,6 +883,12 @@ class IRrecv { const uint16_t kYorkBits, const bool strict = true); #endif // DECODE_YORK +#if DECODE_BANG_OLUFSEN + bool decodeBangOlufsen(decode_results *results, + uint16_t offset = kStartOffset, + const uint16_t nbits = kBangOlufsenBits, + const bool strict = false); +#endif // DECODE_BANG_OLUFSEN }; #endif // IRRECV_H_ diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index 949de1ecf..ac8753d0a 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -952,6 +952,13 @@ #define SEND_YORK _IR_ENABLE_DEFAULT_ #endif // SEND_YORK +#ifndef DECODE_BANG_OLUFSEN +#define DECODE_BANG_OLUFSEN _IR_ENABLE_DEFAULT_ +#endif // DECODE_BANG_OLUFSEN +#ifndef SEND_BANG_OLUFSEN +#define SEND_BANG_OLUFSEN _IR_ENABLE_DEFAULT_ +#endif // SEND_BANG_OLUFSEN + #if (DECODE_ARGO || DECODE_DAIKIN || DECODE_FUJITSU_AC || DECODE_GREE || \ DECODE_KELVINATOR || DECODE_MITSUBISHI_AC || DECODE_TOSHIBA_AC || \ DECODE_TROTEC || DECODE_HAIER_AC || DECODE_HITACHI_AC || \ @@ -1137,8 +1144,9 @@ enum decode_type_t { WOWWEE, CARRIER_AC84, // 125 YORK, + BANG_OLUFSEN, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = YORK, + kLastDecodeType = BANG_OLUFSEN, }; // Message lengths & required repeat values @@ -1435,6 +1443,7 @@ const uint16_t kRhossDefaultRepeat = 0; const uint16_t kClimaButlerBits = 52; const uint16_t kYorkBits = 136; const uint16_t kYorkStateLength = 17; +const uint16_t kBangOlufsenBits = 16; // Legacy defines. (Deprecated) diff --git a/src/IRsend.cpp b/src/IRsend.cpp index b64af91c6..cf9545f18 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -871,6 +871,8 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { return kXmpBits; case YORK: return kYorkBits; + case BANG_OLUFSEN: + return kBangOlufsenBits; // No default amount of bits. case FUJITSU_AC: case MWM: @@ -911,6 +913,11 @@ bool IRsend::send(const decode_type_t type, const uint64_t data, sendArris(data, nbits, min_repeat); break; #endif // SEND_ARRIS +#if SEND_BANG_OLUFSEN + case BANG_OLUFSEN: + sendBangOlufsen(data, nbits, min_repeat); + break; +#endif // SEND_BANG_OLUFSEN #if SEND_BOSE case BOSE: sendBose(data, nbits, min_repeat); diff --git a/src/IRsend.h b/src/IRsend.h index 9275dc660..a4aa908bf 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -874,6 +874,15 @@ class IRsend { const uint16_t nbytes = kYorkStateLength, const uint16_t repeat = kNoRepeat); #endif // SEND_YORK +#if SEND_BANG_OLUFSEN + void sendBangOlufsen(const uint64_t data, + const uint16_t nbits = kBangOlufsenBits, + const uint16_t repeat = kNoRepeat); + void sendBangOlufsenRaw(const uint64_t rawData, + const uint16_t bits, + const bool datalink, + const bool backToBack); +#endif // SEND_BANG_OLUFSEN protected: #ifdef UNIT_TEST diff --git a/src/IRtext.cpp b/src/IRtext.cpp index 9cb39b772..8c45be360 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -555,6 +555,8 @@ IRTEXT_CONST_BLOB_DECL(kAllProtocolNamesStr) { D_STR_CARRIER_AC84, D_STR_UNSUPPORTED) "\x0" COND(DECODE_YORK || SEND_YORK, D_STR_YORK, D_STR_UNSUPPORTED) "\x0" + COND(DECODE_BANG_OLUFSEN || SEND_BANG_OLUFSEN, + D_STR_BANG_OLUFSEN, D_STR_UNSUPPORTED) "\x0" ///< New protocol (macro) strings should be added just above this line. "\x0" ///< This string requires double null termination. }; diff --git a/src/ir_BangOlufsen.cpp b/src/ir_BangOlufsen.cpp new file mode 100644 index 000000000..1ff0ce707 --- /dev/null +++ b/src/ir_BangOlufsen.cpp @@ -0,0 +1,416 @@ +// Copyright 2022,2024 Daniel Wallner + +/// @file +/// @brief Bang & Olufsen remote emulation +/// @see https://www.mikrocontroller.net/attachment/33137/datalink.pdf + +// Supports: +// Brand: Bang & Olufsen, Model: Beomaster 3500/4500/5500/6500/7000 +// Brand: Bang & Olufsen, Model: Beolink 1000 remote +// Brand: Bang & Olufsen, Model: Beolink 5000 remote +// Brand: Bang & Olufsen, Model: Beo4 remote + +// This protocol is unusual in three ways: + +// 1. The carrier frequency is 455 kHz + +// You can build your own receiver as Bang & Olufsen did (check old schematics) or use a TSOP7000. +// Vishay stopped producing TSOP7000 a long time ago so you will likely only find counterfeits: +// https://www.vishay.com/files/whatsnew/doc/ff_FastFacts_CounterfeitTSOP7000_Dec72018.pdf +// It is also likely that you will need an oscilloscope to debug a counterfeit TSOP7000. +// The specimen used to test this code was very noisy and had a very low output current. +// A somewhat working fix was to put a 4n7 capacitor across the output and ground followed by a pnp emitter follower. +// Other samples may require a different treatment. +// This particular receiver also did receive lower frequencies but rather poorly and with a lower delay than usual. +// This makes it hard to create a functional universal receiver by paralleling a TSOP7000 with another receiver. +// +// If the transmitter is close enough receivers will still pick up the signal even if the modulation frequency is 200 kHz or so. + +// IOREF -----------------*--- +// | +// R pull-up +// | +// *---- OUT +// PNP |v E +// TSOP out -> ----*----| B +// | |\ C +// C 4n7 | +// | | +// GND ------------*-----*--- + +// 2. Some remotes use two-way communication + +// One-way remotes such as Beolink 1000 use a 16-bit protocol (or 17 depending on how you count, see below) +// Some remotes like Beolink 5000 use a different protocol where the remote sends 21-bits and receives +// messages of different size of up to 58-bits. +// To receive 58-bit messages, kRawBuf must be increased + +// 3. A stream of messages can be sent back to back with a new message immediately following the previous stop space + +// The stop space is only 12.3 ms long which requires kTimeoutMs to be lower than this for a timeout before the next message. +// The start space is 15.425 ms and this would then break a single message into three pieces: +// The three AGC marks, the single stop mark, and the header and data part. +// This seems to only happen on one-way remote messages with repeat like volume up/down. + +// This can be handled in two ways: + +// Alt 1: Partial message decode (default) + +// Set kTimeoutMs to 12 to reliably treat the start space as a gap between messages. +// The start of a transmision will result in a dummy decode with 0 bits of data followed by the actual messages. +// If the receiver is not resumed within a ms or so partial messages will be decoded. +// Printing in the wrong place is very likely to break reception. +// Make sure to check the number of bits to filter dummy and incomplete messages! + +// Alt 1: Strict mode + +// Define BANG_OLUFSEN_STRICT and set kTimeoutMs to at least 16 to accomodate the unusually long start space. +// This is the most robust mode but will only receive single messages and repeats will result in overflow. + + +// Note that the datalink document shows a message with 22 bits where the top bit is zero. +// It is assumed here that the first bit is an always zero start bit that should not be counted +// and this message is treated here as a 21-bit message. +// It is still possible to encode this bit if it would be needed as encoding includes one extra bit. + +#include +#include "IRrecv.h" +#include "IRsend.h" +#include "IRutils.h" + +// Constants +const uint8_t kBeoDataBits = 8; +const uint16_t kBeoPulseLengthT1 = 3125; ///< uSeconds. +const uint16_t kBeoPulseLengthT2 = kBeoPulseLengthT1 * 2; ///< uSeconds. +const uint16_t kBeoPulseLengthT3 = kBeoPulseLengthT1 * 3; ///< uSeconds. +const uint16_t kBeoPulseLengthT4 = kBeoPulseLengthT1 * 4; ///< uSeconds. +const uint16_t kBeoPulseLengthT5 = kBeoPulseLengthT1 * 5; ///< uSeconds. +const uint16_t kBeoIRMark = 200; ///< uSeconds. +const uint16_t kBeoDatalinkMark = kBeoPulseLengthT1 / 2; ///< uSeconds. +const uint32_t kBeoFreq = 455000; // kHz. + +//#define BANG_OLUFSEN_STRICT +//#define BANG_OLUFSEN_LOCAL_DEBUG +//#define BANG_OLUFSEN_LOCAL_TRACE +//#define BANG_OLUFSEN_CHECK_MODULATION +//#define BANG_OLUFSEN_ACCEPT_NON_STRICT_ONE_START_BIT + +#ifdef BANG_OLUFSEN_LOCAL_DEBUG +#undef DPRINT +#undef DPRINTLN +#define DPRINT(x) do { Serial.print(x); } while (0) +#define DPRINTLN(x) do { Serial.println(x); } while (0) +#endif + +#ifdef BANG_OLUFSEN_LOCAL_TRACE +#define TPRINT(...) Serial.print(__VA_ARGS__) +#define TPRINTLN(...) Serial.println(__VA_ARGS__) +#else +#define TPRINT(...) void() +#define TPRINTLN(...) void() +#endif + +#if SEND_BANG_OLUFSEN +void IRsend::sendBangOlufsen(const uint64_t data, + const uint16_t nbits, + const uint16_t repeat) +{ + for (uint_fast8_t i = 0; i < repeat + 1; ++i) { + sendBangOlufsenRaw(data, nbits + 1, false, i != 0); + } +} + +void IRsend::sendBangOlufsenRaw(const uint64_t rawData, + const uint16_t bits, + const bool datalink, + const bool backToBack) +{ + uint16_t markLength = datalink ? kBeoDatalinkMark : kBeoIRMark; + + const uint32_t modulationFrequency = kBeoFreq; + enableIROut(modulationFrequency, datalink ? kDutyMax : kDutyDefault); + +#ifdef BANG_OLUFSEN_CHECK_MODULATION + // Don't send, just measure during 65 ms + Serial.print("Clock frequency: "); + Serial.print(ESP.getCpuFreqMHz()); + unsigned long startTime = micros(); + uint16_t testTime = -1; + uint16_t pulses = mark(testTime); + uint32_t timespent = micros() - startTime; + uint32_t measuredFrequency = (pulses * 1000000ULL + (timespent >> 2)) / timespent; + Serial.print("Modulation frequency: "); + Serial.print(measuredFrequency); + Serial.print(" (Target: "); + Serial.print(modulationFrequency); + Serial.println(")"); + Serial.print("Mark time: "); + Serial.print(timespent); + Serial.print(" (Target: "); + Serial.print(testTime); + Serial.println(")"); + Serial.print("Number of pulses: "); + Serial.print(pulses); + Serial.print(" (Target: "); + Serial.print(uint32_t((testTime * uint64_t(modulationFrequency) + 500000UL) / 1000000UL)); + Serial.println(")"); + return; +#endif + + // AGC / Start + if (!backToBack) { + mark(markLength); + } + space(kBeoPulseLengthT1 - markLength); + mark(markLength); + space(kBeoPulseLengthT1 - markLength); + mark(markLength); + space(kBeoPulseLengthT5 - markLength); + + bool lastBit = true; + + // Header / Data + uint64_t mask = 1UL << (bits - 1); + for (; mask; mask >>= 1) { + if (lastBit && !(rawData & mask)) { + mark(markLength); + space(kBeoPulseLengthT1 - markLength); + lastBit = false; + } else if (!lastBit && (rawData & mask)) { + mark(markLength); + space(kBeoPulseLengthT3 - markLength); + lastBit = true; + } else { + mark(markLength); + space(kBeoPulseLengthT2 - markLength); + } + } + + // Stop + mark(markLength); + space(kBeoPulseLengthT4 - markLength); + mark(markLength); +} + +#endif // SEND_BANG_OLUFSEN + +#if DECODE_BANG_OLUFSEN +static bool matchBeoLength(IRrecv *irr, uint16_t measuredTicks, uint16_t desiredMicros) +{ + return irr->match(measuredTicks, desiredMicros, 0, kBeoDatalinkMark); +} + +static bool matchBeoMark(IRrecv *irr, uint32_t measuredTicks, uint32_t desiredMicros) +{ + return irr->matchMark(measuredTicks, desiredMicros, 65, 40); +} + +bool IRrecv::decodeBangOlufsen(decode_results *results, + uint16_t offset, + const uint16_t nbits, + const bool strict) { +#ifdef BANG_OLUFSEN_STRICT + if (results->rawlen - offset < 43) { // 16 bits minimun +#else + if (results->rawlen - offset != 5 && results->rawlen - offset < 35) { // 16 bits minimun +#endif + return false; + } + +#if !defined(BANG_OLUFSEN_STRICT) && (defined(BANG_OLUFSEN_LOCAL_DEBUG) || defined(BANG_OLUFSEN_LOCAL_TRACE)) + if (results->rawlen - offset == 5) { + // Short circuit when debugging to avoid spending too much time printing and then miss the actual message + results->decode_type = BANG_OLUFSEN; + results->value = 0; + results->address = 0; + results->command = 0; + results->bits = 0; + return true; + } +#endif + + uint16_t protocolMarkLength = 0; + uint8_t lastBit = 1; + uint8_t pulseNum = 0; + uint8_t bits = 0; + uint64_t receivedData = 0; + bool complete = false; + + for (uint8_t rawPos = offset; rawPos < results->rawlen; rawPos += 2) { + uint16_t markLength = results->rawbuf[rawPos]; + uint16_t spaceLength = rawPos + 1 < results->rawlen ? results->rawbuf[rawPos + 1] : 0; + + if (pulseNum == 0) { + TPRINT("Pre space: "); + TPRINT(results->rawbuf[rawPos - 1] * kRawTick); + TPRINT(" raw len: "); + TPRINTLN(results->rawlen); + } + + TPRINT(pulseNum); + TPRINT(" "); + TPRINT(markLength * kRawTick); + TPRINT(" "); + TPRINT(spaceLength * kRawTick); + TPRINT(" ("); + TPRINT((markLength + spaceLength) * kRawTick); + TPRINTLN(") "); + +#ifndef BANG_OLUFSEN_STRICT + if (bits == 0 && rawPos + 1 == results->rawlen) { + TPRINTLN(": Jump to end"); + pulseNum = 3; + complete = true; + continue; + } +#endif + + // Check start + if (pulseNum < 3) { + if (protocolMarkLength == 0) { + if (matchBeoMark(this, markLength, kBeoIRMark)) { + protocolMarkLength = kBeoIRMark; + } + if (matchBeoMark(this, markLength, kBeoDatalinkMark)) { + protocolMarkLength = kBeoDatalinkMark; + } + if (!protocolMarkLength) { + DPRINTLN("DEBUG: decodeBangOlufsen: Start mark length 1 is wrong"); + return false; + } + } else { + if (!matchBeoMark(this, markLength, protocolMarkLength)) { + DPRINTLN("DEBUG: decodeBangOlufsen: Start mark length is wrong"); + return false; + } + } +#ifdef BANG_OLUFSEN_STRICT + if (!matchBeoLength(this, markLength + spaceLength, (pulseNum == 2) ? kBeoPulseLengthT5 : kBeoPulseLengthT1)) { + DPRINT("DEBUG: decodeBangOlufsen: Start length is wrong: "); + DPRINT(markLength * kRawTick); + DPRINT("/"); + DPRINT(spaceLength * kRawTick); + DPRINT(" ("); + DPRINT(pulseNum); + DPRINTLN(")"); + return false; + } +#else + if (matchSpace(results->rawbuf[rawPos - 1], kBeoPulseLengthT5 - kBeoIRMark)) { + // Jump to bits + TPRINTLN(": Jump to bits 1"); + pulseNum = 2; + rawPos = offset - 2; + } else if (matchBeoLength(this, markLength + spaceLength, kBeoPulseLengthT1) +#ifdef BANG_OLUFSEN_ACCEPT_NON_STRICT_ONE_START_BIT + // Decode messages that have a top/start bit that isn't zero + || matchBeoLength(this, markLength + spaceLength, kBeoPulseLengthT2) +#endif + ) { + if (pulseNum == 0 && rawPos + 3 < results->rawlen) { + // Check that we are not in start + uint16_t nextMarkLength = results->rawbuf[rawPos + 2]; + uint16_t nextSpaceLength = results->rawbuf[rawPos + 3]; + if (!(matchBeoLength(this, nextMarkLength + nextSpaceLength, kBeoPulseLengthT1) && + matchBeoLength(this, markLength + spaceLength, kBeoPulseLengthT1))) { + // This can be a false positive if we started in the middle of the message! + TPRINTLN(": Jump to bits 2"); + pulseNum = 2; + rawPos = offset - 2; + } + } else if (pulseNum == 2) { + DPRINTLN("DEBUG: decodeBangOlufsen: Start sequence is wrong"); + return false; + } + } else if (matchBeoLength(this, markLength + spaceLength, kBeoPulseLengthT5)) { + // Jump to bits + pulseNum = 2; + TPRINTLN(": Jump to bits 3"); + } else { + DPRINT("DEBUG: decodeBangOlufsen: Start length is wrong: "); + DPRINT(markLength * kRawTick); + DPRINT("/"); + DPRINT(spaceLength * kRawTick); + DPRINT(" ("); + DPRINT(pulseNum); + DPRINTLN(")"); + return false; + } +#endif + } + // Decode header / data + else { + if (!matchBeoMark(this, markLength, protocolMarkLength)) { + DPRINTLN("DEBUG: decodeBangOlufsen: Mark length is wrong"); + return false; + } + if (complete) { +#ifdef BANG_OLUFSEN_STRICT + if (rawPos + 1 != results->rawlen) { + DPRINTLN("DEBUG: decodeBangOlufsen: Extra data"); + return false; + } +#endif + break; + } + if (bits > kBeoDataBits) { + // Check for stop + if (matchBeoLength(this, markLength + spaceLength, kBeoPulseLengthT4)) { + if (rawPos + 2 < results->rawlen) { + complete = true; + continue; + } + DPRINTLN("DEBUG: decodeBangOlufsen: Incomplete"); + return false; + } + } + if (lastBit == 0 && matchBeoLength(this, markLength + spaceLength, kBeoPulseLengthT3)) { + lastBit = 1; + } else if (lastBit == 1 && matchBeoLength(this, markLength + spaceLength, kBeoPulseLengthT1)) { + lastBit = 0; +#ifndef BANG_OLUFSEN_STRICT + } else if (rawPos + 1 == results->rawlen && spaceLength == 0) { + DPRINTLN("Ignoring missing stop"); + complete = true; + continue; +#endif + } else if (!matchBeoLength(this, markLength + spaceLength, kBeoPulseLengthT2)) { + DPRINT("DEBUG: decodeBangOlufsen: Length "); + DPRINT((markLength + spaceLength) * kRawTick); + DPRINTLN(" is wrong"); + return false; + } + receivedData <<= 1; + receivedData |= lastBit; + ++bits; + TPRINT("Bits "); + TPRINT(bits); + TPRINT(" "); + TPRINT(uint32_t(receivedData >> kBeoDataBits), HEX); + TPRINT(" "); + TPRINTLN(uint8_t(receivedData & ((1 << kBeoDataBits) - 1)), HEX); + } + + ++pulseNum; + } + + if (!complete) { + DPRINTLN("DEBUG: decodeBangOlufsen: Not enough bits"); + return false; + } + + if (strict && bits < kBangOlufsenBits + 1) + return false; + if (strict && bits != nbits + 1) + return false; + + results->decode_type = BANG_OLUFSEN; + results->value = receivedData; + results->address = receivedData >> kBeoDataBits; // header bits + results->command = receivedData & ((1 << kBeoDataBits) - 1); // lower 8 bits + results->bits = bits ? bits - 1 : 0; + + return true; +} + +#endif // DECODE_BANG_OLUFSEN diff --git a/src/locale/defaults.h b/src/locale/defaults.h index 438cc5da3..82c1e3111 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -1120,6 +1120,9 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_ZEPEAL #define D_STR_ZEPEAL "ZEPEAL" #endif // D_STR_ZEPEAL +#ifndef D_STR_BANG_OLUFSEN +#define D_STR_BANG_OLUFSEN "BANG_OLUFSEN" +#endif // D_STR_BANG_OLUFSEN // IRrecvDumpV2+ #ifndef D_STR_TIMESTAMP diff --git a/test/IRsend_test.cpp b/test/IRsend_test.cpp index 5bcf3c4d8..287535b07 100644 --- a/test/IRsend_test.cpp +++ b/test/IRsend_test.cpp @@ -393,6 +393,14 @@ TEST(TestSend, GenericSimpleSendMethod) { EXPECT_EQ(kAiwaRcT501Bits, irsend.capture.bits); EXPECT_EQ(0x1234, irsend.capture.value); + irsend.reset(); + ASSERT_TRUE(irsend.send(BANG_OLUFSEN, 0x1234, kBangOlufsenBits)); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(BANG_OLUFSEN, irsend.capture.decode_type); + EXPECT_EQ(kBangOlufsenBits, irsend.capture.bits); + EXPECT_EQ(0x1234, irsend.capture.value); + irsend.reset(); ASSERT_TRUE(irsend.send(CARRIER_AC, 0x1234, kCarrierAcBits)); irsend.makeDecodeResult(); diff --git a/test/ir_BangOlufsen_test.cpp b/test/ir_BangOlufsen_test.cpp new file mode 100644 index 000000000..76f9dc044 --- /dev/null +++ b/test/ir_BangOlufsen_test.cpp @@ -0,0 +1,248 @@ +// Copyright 2024 Daniel Wallner + +#include "IRsend.h" +#include "IRsend_test.h" +#include "gtest/gtest.h" + +// Tests for sendBangOlufsen(). + +// Test sending typical data only. +TEST(TestSendBangOlufsen, SendDataOnly) { + IRsendTest irsend(4); + irsend.begin(); + + irsend.reset(); + irsend.sendBangOlufsen(0x1234); + EXPECT_EQ( + "f455000d50" + "m200s2925m200s2925m200s15425" + "m200s2925m200s6050m200s6050m200s6050m200s9175m200s2925" + "m200s6050m200s9175m200s2925m200s6050m200s6050m200s9175" + "m200s6050m200s2925m200s9175m200s2925m200s6050" + "m200s12300m200", + irsend.outputStr()); + + // Example from Datalink '86 document + // 22 bits are shown but the top bit is the start data bit which probably shouldn't be counted + irsend.reset(); + irsend.sendBangOlufsen(0x083E35, 21); + EXPECT_EQ( + "f455000d50" + "m200s2925m200s2925m200s15425" // START + "m200s2925m200s6050m200s9175m200s2925" // Format (0)010 + "m200s6050m200s6050m200s6050m200s6050m200s9175" // Address (to) 00001 + "m200s6050m200s6050m200s6050m200s6050m200s2925" // Address (from) 11110 + "m200s6050m200s6050m200s9175m200s6050m200s2925m200s9175m200s2925m200s9175" // DATA 00110101 + "m200s12300m200", // STOP + irsend.outputStr()); + + // Test sending 1 as start data bit (which may be illegal but is supported) + irsend.reset(); + irsend.sendBangOlufsen(0x15555); + EXPECT_EQ( + "f455000d50" + "m200s2925m200s2925m200s15425" + "m200s6050m200s2925m200s9175m200s2925m200s9175m200s2925" + "m200s9175m200s2925m200s9175m200s2925m200s9175m200s2925" + "m200s9175m200s2925m200s9175m200s2925m200s9175" + "m200s12300m200", + irsend.outputStr()); + + // Test sending with datalink timing + irsend.reset(); + irsend.sendBangOlufsenRaw(0xAB, 9, true, false); + EXPECT_EQ( + "f455000d100" + "m1562s1563m1562s1563m1562s14063" + "m1562s1563m1562s7813m1562s1563m1562s7813m1562s1563" + "m1562s7813m1562s1563m1562s7813m1562s4688" + "m1562s10938m1562", + irsend.outputStr()); +} + +// Test sending typical data with extra repeats. +TEST(TestSendBangOlufsen, SendDataWithRepeats) { + IRsendTest irsend(4); + irsend.begin(); + + irsend.reset(); + irsend.sendBangOlufsen(0x4321, 16, 2); + EXPECT_EQ( + "f455000d50" + "m200s2925m200s2925m200s15425" + "m200s2925m200s6050m200s9175m200s2925m200s6050m200s6050" + "m200s6050m200s9175m200s6050m200s2925m200s6050m200s9175" + "m200s2925m200s6050m200s6050m200s6050m200s9175" + "m200s12300" + "m200s2925m200s2925m200s15425" + "m200s2925m200s6050m200s9175m200s2925m200s6050m200s6050" + "m200s6050m200s9175m200s6050m200s2925m200s6050m200s9175" + "m200s2925m200s6050m200s6050m200s6050m200s9175" + "m200s12300" + "m200s2925m200s2925m200s15425" + "m200s2925m200s6050m200s9175m200s2925m200s6050m200s6050" + "m200s6050m200s9175m200s6050m200s2925m200s6050m200s9175" + "m200s2925m200s6050m200s6050m200s6050m200s9175" + "m200s12300m200", + irsend.outputStr()); + + // Send different messages back to back. + irsend.reset(); + irsend.sendBangOlufsenRaw(0x1234, 17, false, false); + irsend.sendBangOlufsenRaw(0x4321, 17, false, true); + EXPECT_EQ( + "f455000d50" + "m200s2925m200s2925m200s15425" + "m200s2925m200s6050m200s6050m200s6050m200s9175m200s2925" + "m200s6050m200s9175m200s2925m200s6050m200s6050m200s9175" + "m200s6050m200s2925m200s9175m200s2925m200s6050" + "m200s12300" + "m200s2925m200s2925m200s15425" + "m200s2925m200s6050m200s9175m200s2925m200s6050m200s6050" + "m200s6050m200s9175m200s6050m200s2925m200s6050m200s9175" + "m200s2925m200s6050m200s6050m200s6050m200s9175" + "m200s12300m200", + irsend.outputStr()); +} + +// Tests for decodeBangOlufsen(). + +// Decode normal messages. +TEST(TestDecodeBangOlufsen, NormalDecode) { + IRsendTest irsend(4); + IRrecv irrecv(4); + irsend.begin(); + + // Synthesised Normal 16-bit message. + irsend.reset(); + irsend.sendBangOlufsen(0xF00F); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(BANG_OLUFSEN, irsend.capture.decode_type); + EXPECT_EQ(kBangOlufsenBits, irsend.capture.bits); + EXPECT_EQ(0xF00F, irsend.capture.value); + EXPECT_EQ(0xF0, irsend.capture.address); + EXPECT_EQ(0x0F, irsend.capture.command); + + // Synthesised Normal 32-bit message. + irsend.reset(); + irsend.sendBangOlufsen(0xF2345678, 32); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(BANG_OLUFSEN, irsend.capture.decode_type); + EXPECT_EQ(32, irsend.capture.bits); + EXPECT_EQ(0xF2345678, irsend.capture.value); + EXPECT_EQ(0xF23456, irsend.capture.address); + EXPECT_EQ(0x78, irsend.capture.command); + + // Synthesised Normal 58-bit message which may be the longest ever being sent. + irsend.reset(); + ASSERT_TRUE(irsend.send(BANG_OLUFSEN, 0x123456789ABCULL, 58)); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(BANG_OLUFSEN, irsend.capture.decode_type); + EXPECT_EQ(58, irsend.capture.bits); + EXPECT_EQ(0x123456789ABCULL, irsend.capture.value); + + // Synthesised Repeated 16-bit message. + irsend.reset(); + irsend.sendBangOlufsenRaw(0x1234, 17, false, false); + irsend.sendBangOlufsenRaw(0x4321, 17, false, true); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); +#ifdef BANG_OLUFSEN_STRICT + EXPECT_EQ(UNKNOWN, irsend.capture.decode_type); +#else + EXPECT_EQ(BANG_OLUFSEN, irsend.capture.decode_type); + EXPECT_EQ(kBangOlufsenBits, irsend.capture.bits); + EXPECT_EQ(0x1234, irsend.capture.value); + EXPECT_EQ(0x12, irsend.capture.address); + EXPECT_EQ(0x34, irsend.capture.command); +#endif +} + +// Decode unexpected messages. i.e 1 start data bit. +TEST(TestDecodeBangOlufsen, UnexpectedDecode) { + IRsendTest irsend(4); + IRrecv irrecv(4); + irsend.begin(); + + // Synthesised 16-bit message with extra start data bit. + irsend.reset(); + irsend.sendBangOlufsen(0x3F00F); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(BANG_OLUFSEN, irsend.capture.decode_type); + EXPECT_EQ(kBangOlufsenBits, irsend.capture.bits); + EXPECT_EQ(0x1F00F, irsend.capture.value); + EXPECT_EQ(0x1F0, irsend.capture.address); + EXPECT_EQ(0x0F, irsend.capture.command); +} + +// Check decoding of messages split at start and stop. +TEST(TestDecodeBangOlufsen, DecodeGlobalCacheExample) { + IRsendTest irsend(4); + IRrecv irrecv(4); + irsend.begin(); + + // AGC / START + irsend.reset(); + uint16_t gc_test1[8] = {40000, 1, 1, 8, 117, 8, 117, 8}; + irsend.sendGC(gc_test1, 8); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); +#ifdef BANG_OLUFSEN_STRICT + EXPECT_EQ(UNKNOWN, irsend.capture.decode_type); +#else + EXPECT_EQ(BANG_OLUFSEN, irsend.capture.decode_type); + EXPECT_EQ(0, irsend.capture.bits); + EXPECT_EQ(0, irsend.capture.value); + EXPECT_EQ(0, irsend.capture.address); + EXPECT_EQ(0, irsend.capture.command); +#endif + + // Lone STOP + irsend.reset(); + uint16_t gc_test2[4] = {40000, 1, 1, 8}; + irsend.sendGC(gc_test2, 4); + irsend.makeDecodeResult(); + + ASSERT_FALSE(irrecv.decode(&irsend.capture)); + + // Example from Datalink '86 document without start and stop + irsend.reset(); + uint16_t gc_test3[48] = {40000, 1, 1, 8, 117, 8, 242, 8, 367, 8, 117, + 8, 242, 8, 242, 8, 242, 8, 242, 8, 367, + 8, 242, 8, 242, 8, 242, 8, 242, 8, 117, + 8, 242, 8, 242, 8, 367, 8, 242, 8, 117, 8, 367, 8, 117, 8, 367, + 8}; + irsend.sendGC(gc_test3, 48); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); +#ifdef BANG_OLUFSEN_STRICT + EXPECT_EQ(UNKNOWN, irsend.capture.decode_type); +#else + EXPECT_EQ(BANG_OLUFSEN, irsend.capture.decode_type); + EXPECT_EQ(21, irsend.capture.bits); + EXPECT_EQ(0x083E35, irsend.capture.value); + EXPECT_EQ(0x083E, irsend.capture.address); + EXPECT_EQ(0x35, irsend.capture.command); +#endif + + // Same as above except that the start data bit is 1 + // This does not currently decode as BANG_OLUFSEN since enabling this would make decoding split messages less robust + // It should only be enabled if 1 start data bits are actually used + irsend.reset(); + uint16_t gc_test4[48] = {40000, 1, 1, 8, 242, 8, 117, 8, 367, 8, 117, + 8, 242, 8, 242, 8, 242, 8, 242, 8, 367, + 8, 242, 8, 242, 8, 242, 8, 242, 8, 117, + 8, 242, 8, 242, 8, 367, 8, 242, 8, 117, 8, 367, 8, 117, 8, 367, + 8}; + irsend.sendGC(gc_test4, 48); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(UNKNOWN, irsend.capture.decode_type); +}