From 286028cf8673362f22a25056f4f916a5a6254148 Mon Sep 17 00:00:00 2001 From: Daniel Wallner <14888585+danielwallner@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:55:54 +0100 Subject: [PATCH 1/6] Operator precedence bug fixes. --- src/IRutils.cpp | 4 ++-- src/ir_Coolix.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/IRutils.cpp b/src/IRutils.cpp index e9af0b28f..2ff514264 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -262,8 +262,8 @@ String resultToSourceCode(const decode_results * const results) { // "uint32_t address = 0xDEADBEEF;\n" // "uint32_t command = 0xDEADBEEF;\n" // "uint64_t data = 0xDEADBEEFDEADBEEF;" = ~116 chars max. - output.reserve(55 + (length * 7) + hasState ? 25 + (results->bits / 8) * 6 - : 116); + output.reserve(55 + (length * 7) + (hasState ? 25 + (results->bits / 8) * 6 + : 116)); // Start declaration output += F("uint16_t "); // variable type output += F("rawData["); // array name diff --git a/src/ir_Coolix.cpp b/src/ir_Coolix.cpp index 6769ebb79..d10144e28 100644 --- a/src/ir_Coolix.cpp +++ b/src/ir_Coolix.cpp @@ -112,8 +112,8 @@ void IRCoolixAC::send(const uint16_t repeat) { // Typically repeat is `kCoolixDefaultRepeat` which is `1`, so this allows // it to be 0 normally for this command, and allows additional repeats if // requested rather always 0 for that command. - _irsend.sendCOOLIX(getRaw(), kCoolixBits, repeat - (getSwingVStep() && - repeat > 0) ? 1 : 0); + _irsend.sendCOOLIX(getRaw(), kCoolixBits, repeat - ((getSwingVStep() && + repeat > 0) ? 1 : 0)); // make sure to remove special state from the internal state // after command has being transmitted. recoverSavedState(); From 035c502018d706e8208cac140d8c680dd1832f3d Mon Sep 17 00:00:00 2001 From: Daniel Wallner <14888585+danielwallner@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:56:35 +0100 Subject: [PATCH 2/6] Deprecation warning fixes. --- test/ir_Argo_test.cpp | 8 ++++---- test/ir_Gorenje_test.cpp | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/ir_Argo_test.cpp b/test/ir_Argo_test.cpp index 817272a17..0705d2839 100644 --- a/test/ir_Argo_test.cpp +++ b/test/ir_Argo_test.cpp @@ -509,7 +509,7 @@ TEST_P(TestArgoConfigViaIRAc_Positive, EXPECT_EQ(state.command, r.command); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( TestIrAc, TestArgoConfigViaIRAc_Positive, ::testing::Values( @@ -546,7 +546,7 @@ TEST_P(TestArgoConfigViaIRAc_Negative, ASSERT_EQ(nullptr, irac._lastDecodeResults); // nothing got sent } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( TestIrAc, TestArgoConfigViaIRAc_Negative, ::testing::Values( @@ -565,7 +565,7 @@ INSTANTIATE_TEST_CASE_P( using IRArgoACBase_typelist = ::testing::Types; template struct TestArgoACBaseClass : public testing::Test {}; -TYPED_TEST_CASE(TestArgoACBaseClass, IRArgoACBase_typelist); +TYPED_TEST_SUITE(TestArgoACBaseClass, IRArgoACBase_typelist); TYPED_TEST(TestArgoACBaseClass, SetAndGetTemp) { @@ -1520,7 +1520,7 @@ TEST_P(TestArgoE2E, RealExampleCommands) { } // Test cases -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( TestDecodeArgo, TestArgoE2E, ::testing::Values( diff --git a/test/ir_Gorenje_test.cpp b/test/ir_Gorenje_test.cpp index 245b29b98..ebeab7680 100644 --- a/test/ir_Gorenje_test.cpp +++ b/test/ir_Gorenje_test.cpp @@ -84,7 +84,7 @@ TEST_P(TestDecodeGorenjeSyntheticSendTestFixture, SyntheticExample) { EXPECT_FALSE(irsend.capture.repeat); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( TestDecodeGorenje, TestDecodeGorenjeSyntheticSendTestFixture, ::testing::Values(0x2, 0x8, 0x4, 0x10, 0x20, 0x1)); @@ -112,7 +112,7 @@ TEST_P(TestDecodeGorenjeReceiveTestFixture, RealExample) { EXPECT_FALSE(irsend.capture.repeat); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( TestDecodeGorenje, TestDecodeGorenjeReceiveTestFixture, ::testing::Values( From 377819193db2e01e75215692ce34648ffbf66e97 Mon Sep 17 00:00:00 2001 From: Daniel Wallner <14888585+danielwallner@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:56:51 +0100 Subject: [PATCH 3/6] Cast warning fix. --- test/ir_Hitachi_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ir_Hitachi_test.cpp b/test/ir_Hitachi_test.cpp index 51664688a..5515b582c 100644 --- a/test/ir_Hitachi_test.cpp +++ b/test/ir_Hitachi_test.cpp @@ -1831,7 +1831,7 @@ TEST(TestIRHitachiAc1Class, FanSpeedInDryMode) { ac.setPower(true); ac.setPowerToggle(true); ac.setMode(kHitachiAc1Dry); - ac.setTemp(22.5); + ac.setTemp((uint8_t)22.5); ac.setFan(kHitachiAc1FanLow); ac.setSwingV(false); ac.setSwingH(false); From 48d7433dc7fa3fde06e6eda5e423a392df0bc438 Mon Sep 17 00:00:00 2001 From: Daniel Wallner <14888585+danielwallner@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:58:01 +0100 Subject: [PATCH 4/6] Unused constants warning fixes. --- src/ir_Airwell.cpp | 2 +- src/ir_Carrier.cpp | 4 ++-- src/ir_Denon.cpp | 8 ++++---- src/ir_Doshisha.cpp | 2 ++ src/ir_Inax.cpp | 2 +- src/ir_Pioneer.cpp | 2 +- src/ir_RCMM.cpp | 4 ++-- src/ir_Samsung.cpp | 6 +++--- src/ir_Sanyo.cpp | 8 ++++---- src/ir_Toto.cpp | 2 +- src/ir_Zepeal.cpp | 2 ++ 11 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/ir_Airwell.cpp b/src/ir_Airwell.cpp index 2aa244a41..ce84691cd 100644 --- a/src/ir_Airwell.cpp +++ b/src/ir_Airwell.cpp @@ -14,7 +14,7 @@ const uint8_t kAirwellOverhead = 4; const uint16_t kAirwellHalfClockPeriod = 950; // uSeconds const uint16_t kAirwellHdrMark = 3 * kAirwellHalfClockPeriod; // uSeconds const uint16_t kAirwellHdrSpace = 3 * kAirwellHalfClockPeriod; // uSeconds -const uint16_t kAirwellFooterMark = 5 * kAirwellHalfClockPeriod; // uSeconds +//const uint16_t kAirwellFooterMark = 5 * kAirwellHalfClockPeriod; // uSeconds using irutils::addBoolToString; using irutils::addModeToString; diff --git a/src/ir_Carrier.cpp b/src/ir_Carrier.cpp index ef51cc439..70f290f74 100644 --- a/src/ir_Carrier.cpp +++ b/src/ir_Carrier.cpp @@ -51,7 +51,7 @@ const uint16_t kCarrierAc84HdrMark = 5850; const uint16_t kCarrierAc84Zero = 1175; const uint16_t kCarrierAc84One = 430; const uint16_t kCarrierAc84HdrSpace = kCarrierAc84Zero; -const uint32_t kCarrierAc84Gap = kDefaultMessageGap; // A guess. +//const uint32_t kCarrierAc84Gap = kDefaultMessageGap; // A guess. const uint8_t kCarrierAc84ExtraBits = 4; const uint8_t kCarrierAc84ExtraTolerance = 5; @@ -64,7 +64,7 @@ const uint16_t kCarrierAc128OneSpace = 1000; const uint16_t kCarrierAc128ZeroSpace = 400; const uint16_t kCarrierAc128SectionGap = 20600; const uint16_t kCarrierAc128InterSpace = 6700; -const uint16_t kCarrierAc128SectionBits = kCarrierAc128Bits / 2; +//const uint16_t kCarrierAc128SectionBits = kCarrierAc128Bits / 2; #if SEND_CARRIER_AC /// Send a Carrier HVAC formatted message. diff --git a/src/ir_Denon.cpp b/src/ir_Denon.cpp index 700fd31cb..17ceb794a 100644 --- a/src/ir_Denon.cpp +++ b/src/ir_Denon.cpp @@ -28,13 +28,13 @@ const uint16_t kDenonOneSpaceTicks = 7; const uint16_t kDenonOneSpace = kDenonOneSpaceTicks * kDenonTick; const uint16_t kDenonZeroSpaceTicks = 3; const uint16_t kDenonZeroSpace = kDenonZeroSpaceTicks * kDenonTick; -const uint16_t kDenonMinCommandLengthTicks = 510; -const uint16_t kDenonMinGapTicks = +//const uint16_t kDenonMinCommandLengthTicks = 510; +/*const uint16_t kDenonMinGapTicks = kDenonMinCommandLengthTicks - (kDenonHdrMarkTicks + kDenonHdrSpaceTicks + kDenonBits * (kDenonBitMarkTicks + kDenonOneSpaceTicks) + - kDenonBitMarkTicks); -const uint32_t kDenonMinGap = kDenonMinGapTicks * kDenonTick; + kDenonBitMarkTicks);*/ +//const uint32_t kDenonMinGap = kDenonMinGapTicks * kDenonTick; const uint64_t kDenonManufacturer = 0x2A4CULL; #if SEND_DENON diff --git a/src/ir_Doshisha.cpp b/src/ir_Doshisha.cpp index 0a5fadedb..debc9308d 100644 --- a/src/ir_Doshisha.cpp +++ b/src/ir_Doshisha.cpp @@ -26,6 +26,7 @@ const uint64_t kRcz01Signature = 0x800B304800; const uint8_t kRcz01CommandMask = 0xFE; const uint8_t kRcz01ChannelMask = 0x01; +#ifndef UNIT_TEST // Known commands - Here for documentation rather than actual usage const uint8_t kRcz01CommandSwitchChannel = 0xD2; const uint8_t kRcz01CommandTimmer60 = 0x52; @@ -43,6 +44,7 @@ const uint8_t kRcz01CommandLevel4 = 0xD0; const uint8_t kRcz01CommandOn = 0xC0; const uint8_t kRcz01CommandNightLight = 0xC8; // end Known commands +#endif #if SEND_DOSHISHA /// Send a Doshisha formatted message. diff --git a/src/ir_Inax.cpp b/src/ir_Inax.cpp index bb68ff30d..0673806f0 100644 --- a/src/ir_Inax.cpp +++ b/src/ir_Inax.cpp @@ -13,7 +13,7 @@ #include "IRutils.h" // Constants -const uint16_t kInaxTick = 500; +//const uint16_t kInaxTick = 500; const uint16_t kInaxHdrMark = 9000; const uint16_t kInaxHdrSpace = 4500; const uint16_t kInaxBitMark = 560; diff --git a/src/ir_Pioneer.cpp b/src/ir_Pioneer.cpp index 6efe15c0a..55c20c5ec 100644 --- a/src/ir_Pioneer.cpp +++ b/src/ir_Pioneer.cpp @@ -25,7 +25,7 @@ // Constants // Ref: https://github.com/crankyoldgit/IRremoteESP8266/issues/1220 -const uint16_t kPioneerTick = 534; ///< uSeconds. +//const uint16_t kPioneerTick = 534; ///< uSeconds. const uint16_t kPioneerHdrMark = 8506; ///< uSeconds. const uint16_t kPioneerHdrSpace = 4191; ///< uSeconds. const uint16_t kPioneerBitMark = 568; ///< uSeconds. diff --git a/src/ir_RCMM.cpp b/src/ir_RCMM.cpp index 00cc58f02..ecac30ab7 100644 --- a/src/ir_RCMM.cpp +++ b/src/ir_RCMM.cpp @@ -14,7 +14,7 @@ #include "IRutils.h" // Constants -const uint16_t kRcmmTick = 28; // Technically it would be 27.777* +//const uint16_t kRcmmTick = 28; // Technically it would be 27.777* const uint16_t kRcmmHdrMarkTicks = 15; const uint16_t kRcmmHdrMark = 416; const uint16_t kRcmmHdrSpaceTicks = 10; @@ -29,7 +29,7 @@ const uint16_t kRcmmBitSpace2Ticks = 22; const uint16_t kRcmmBitSpace2 = 611; const uint16_t kRcmmBitSpace3Ticks = 28; const uint16_t kRcmmBitSpace3 = 777; -const uint16_t kRcmmRptLengthTicks = 992; +//const uint16_t kRcmmRptLengthTicks = 992; const uint32_t kRcmmRptLength = 27778; const uint16_t kRcmmMinGapTicks = 120; const uint32_t kRcmmMinGap = 3360; diff --git a/src/ir_Samsung.cpp b/src/ir_Samsung.cpp index 958f2665b..009ca868b 100644 --- a/src/ir_Samsung.cpp +++ b/src/ir_Samsung.cpp @@ -32,8 +32,8 @@ const uint16_t kSamsungOneSpaceTicks = 3; const uint16_t kSamsungOneSpace = kSamsungOneSpaceTicks * kSamsungTick; const uint16_t kSamsungZeroSpaceTicks = 1; const uint16_t kSamsungZeroSpace = kSamsungZeroSpaceTicks * kSamsungTick; -const uint16_t kSamsungRptSpaceTicks = 4; -const uint16_t kSamsungRptSpace = kSamsungRptSpaceTicks * kSamsungTick; +//const uint16_t kSamsungRptSpaceTicks = 4; +//const uint16_t kSamsungRptSpace = kSamsungRptSpaceTicks * kSamsungTick; const uint16_t kSamsungMinMessageLengthTicks = 193; const uint32_t kSamsungMinMessageLength = kSamsungMinMessageLengthTicks * kSamsungTick; @@ -46,7 +46,7 @@ const uint32_t kSamsungMinGap = kSamsungMinGapTicks * kSamsungTick; const uint16_t kSamsungAcHdrMark = 690; const uint16_t kSamsungAcHdrSpace = 17844; -const uint8_t kSamsungAcSections = 2; +//const uint8_t kSamsungAcSections = 2; const uint16_t kSamsungAcSectionMark = 3086; const uint16_t kSamsungAcSectionSpace = 8864; const uint16_t kSamsungAcSectionGap = 2886; diff --git a/src/ir_Sanyo.cpp b/src/ir_Sanyo.cpp index 4b99d0492..61058d873 100644 --- a/src/ir_Sanyo.cpp +++ b/src/ir_Sanyo.cpp @@ -35,18 +35,18 @@ using irutils::sumNibbles; // Constants // Sanyo SA 8650B -const uint16_t kSanyoSa8650bHdrMark = 3500; // seen range 3500 +/*const uint16_t kSanyoSa8650bHdrMark = 3500; // seen range 3500 const uint16_t kSanyoSa8650bHdrSpace = 950; // seen 950 const uint16_t kSanyoSa8650bOneMark = 2400; // seen 2400 const uint16_t kSanyoSa8650bZeroMark = 700; // seen 700 // usually see 713 - not using ticks as get number wrapround const uint16_t kSanyoSa8650bDoubleSpaceUsecs = 800; -const uint16_t kSanyoSa8650bRptLength = 45000; +const uint16_t kSanyoSa8650bRptLength = 45000;*/ // Sanyo LC7461 const uint16_t kSanyoLc7461AddressMask = (1 << kSanyoLC7461AddressBits) - 1; const uint16_t kSanyoLc7461CommandMask = (1 << kSanyoLC7461CommandBits) - 1; -const uint16_t kSanyoLc7461HdrMark = 9000; +/*const uint16_t kSanyoLc7461HdrMark = 9000; const uint16_t kSanyoLc7461HdrSpace = 4500; const uint16_t kSanyoLc7461BitMark = 560; // 1T const uint16_t kSanyoLc7461OneSpace = 1690; // 3T @@ -58,7 +58,7 @@ const uint16_t kSanyoLc7461MinGap = (kSanyoLc7461HdrMark + kSanyoLc7461HdrSpace + kSanyoLC7461Bits * (kSanyoLc7461BitMark + (kSanyoLc7461OneSpace + kSanyoLc7461ZeroSpace) / 2) + - kSanyoLc7461BitMark); + kSanyoLc7461BitMark);*/ const uint16_t kSanyoAcHdrMark = 8500; ///< uSeconds const uint16_t kSanyoAcHdrSpace = 4200; ///< uSeconds diff --git a/src/ir_Toto.cpp b/src/ir_Toto.cpp index 2997e760e..1511eb70c 100644 --- a/src/ir_Toto.cpp +++ b/src/ir_Toto.cpp @@ -19,7 +19,7 @@ const uint16_t kTotoBitMark = 600; const uint16_t kTotoOneSpace = 1634; const uint16_t kTotoZeroSpace = 516; const uint16_t kTotoGap = 38000; -const uint16_t kTotoSpecialGap = 42482; +//const uint16_t kTotoSpecialGap = 42482; const uint64_t kTotoPrefix = 0x0802; const uint16_t kTotoPrefixBits = 15; diff --git a/src/ir_Zepeal.cpp b/src/ir_Zepeal.cpp index 96017226c..09c299168 100644 --- a/src/ir_Zepeal.cpp +++ b/src/ir_Zepeal.cpp @@ -31,12 +31,14 @@ const uint8_t kZepealTolerance = 40; // but might need change (removal) if more devices are detected const uint8_t kZepealSignature = 0x6C; +#ifndef UNIT_TEST // Known Zepeal DRT-A3311(BG) Buttons - documentation rather than actual usage const uint16_t kZepealCommandSpeed = 0x6C82; const uint16_t kZepealCommandOffOn = 0x6C81; const uint16_t kZepealCommandRhythm = 0x6C84; const uint16_t kZepealCommandOffTimer = 0x6C88; const uint16_t kZepealCommandOnTimer = 0x6CC3; +#endif #if SEND_ZEPEAL /// Send a Zepeal formatted message. From 385336464d25fd098031a3013c58fecbe6389602 Mon Sep 17 00:00:00 2001 From: Daniel Wallner <14888585+danielwallner@users.noreply.github.com> Date: Sat, 24 Feb 2024 23:56:07 +0100 Subject: [PATCH 5/6] Support higher modulation frequencies and get better precision without the need for calibration. --- src/IRsend.cpp | 89 ++++++++++++++++++++++++++++++++++++++---- src/IRsend.h | 19 ++------- src/IRtimer.cpp | 10 +---- src/ir_GlobalCache.cpp | 2 +- src/ir_Pronto.cpp | 2 +- test/IRsend_test.cpp | 36 ++++++++--------- 6 files changed, 107 insertions(+), 51 deletions(-) diff --git a/src/IRsend.cpp b/src/IRsend.cpp index 10e440b32..b64af91c6 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -26,7 +26,7 @@ /// i.e. If not, assume a 100% duty cycle. Ignore attempts to change the /// duty cycle etc. IRsend::IRsend(uint16_t IRsendPin, bool inverted, bool use_modulation) - : IRpin(IRsendPin), periodOffset(kPeriodOffset) { + : IRpin(IRsendPin) { if (inverted) { outputOn = LOW; outputOff = HIGH; @@ -68,15 +68,12 @@ void IRsend::ledOn() { /// @param[in] use_offset Should we use the calculated offset or not? /// @return nr. of uSeconds. /// @note (T = 1/f) -uint32_t IRsend::calcUSecPeriod(uint32_t hz, bool use_offset) { +uint32_t IRsend::calcUSecPeriod(uint32_t hz) { if (hz == 0) hz = 1; // Avoid Zero hz. Divide by Zero is nasty. uint32_t period = (1000000UL + hz / 2) / hz; // The equiv of round(1000000/hz). - // Apply the offset and ensure we don't result in a <= 0 value. - if (use_offset) - return std::max((uint32_t)1, period + periodOffset); - else - return std::max((uint32_t)1, period); + // Ensure we don't result in a <= 0 value. + return std::max((uint32_t)1, period); } /// Set the output frequency modulation and duty cycle. @@ -101,11 +98,34 @@ void IRsend::enableIROut(uint32_t freq, uint8_t duty) { #ifdef UNIT_TEST _freq_unittest = freq; #endif // UNIT_TEST + +#ifndef UNIT_TEST + _fractionalBits = 14; + + // Maximum signed value that fits. + uint32_t maxValue = 0x7FFF >> _fractionalBits; + uint32_t period = calcUSecPeriod(freq); + + // Decrement the number of fractional bits until the period fits. + while (maxValue < period) + { + --_fractionalBits; + maxValue = 0x7FFF >> _fractionalBits; + } + + uint32_t fixedPointPeriod = ((1000000ULL << _fractionalBits) + freq / 2) / freq; + + // Nr. of uSeconds the LED will be on per pulse. + onTimePeriod = (fixedPointPeriod * _dutycycle) / kDutyMax; + // Nr. of uSeconds the LED will be off per pulse. + offTimePeriod = fixedPointPeriod - onTimePeriod; +#else uint32_t period = calcUSecPeriod(freq); // Nr. of uSeconds the LED will be on per pulse. onTimePeriod = (period * _dutycycle) / kDutyMax; // Nr. of uSeconds the LED will be off per pulse. offTimePeriod = period - onTimePeriod; +#endif } #if ALLOW_DELAY_CALLS @@ -166,6 +186,58 @@ uint16_t IRsend::mark(uint16_t usec) { // Not simple, so do it assuming frequency modulation. uint16_t counter = 0; IRtimer usecTimer = IRtimer(); +#ifndef UNIT_TEST +#if SEND_BANG_OLUFSEN && ESP8266 && F_CPU < 160000000L + // Free running loop to attempt to get close to the 455 kHz required by Bang & Olufsen. + // Define BANG_OLUFSEN_CHECK_MODULATION temporarily to test frequency and time. + // Runs at ~300 kHz on an 80 MHz ESP8266. + // This is far from ideal but works if the transmitter is close enough. + uint32_t periodUInt = (onTimePeriod + offTimePeriod) >> _fractionalBits; + periodUInt = std::max(uint32_t(1), periodUInt); + if (periodUInt <= 5) { + uint32_t nextCheck = usec / periodUInt / 2; // Assume we can at least run for this number of periods. + for (;;) { // nextStop is not updated in this loop. + ledOn(); + ledOff(); + counter++; + if (counter >= nextCheck) { + uint32_t now = usecTimer.elapsed(); + int32_t timeLeft = usec - now; + if (timeLeft <= 1) { + return counter; + } + uint32_t periodsToEnd = counter * timeLeft / now; + // Check again when we are half way closer to the end. + nextCheck = (periodsToEnd >> 2) + counter; + } + } + } +#endif + + // Use absolute time for zero drift (but slightly uneven period). + // Using IRtimer.elapsed() instead of _delayMicroseconds is better for short period times. + // Maxed out at ~190 kHz on an 80 MHz ESP8266. + // Maxed out at ~460 kHz on ESP32. + uint32_t nextStop = 0; // Must be 32 bits to not overflow when usec is near max. + while ((nextStop >> _fractionalBits) < usec) { // Loop until we've met/exceeded our required time. + ledOn(); + nextStop += onTimePeriod; + uint32_t nextStopUInt = std::min(nextStop >> _fractionalBits, uint32_t(usec)); + while(usecTimer.elapsed() < nextStopUInt); + ledOff(); + counter++; + nextStop += offTimePeriod; + nextStopUInt = std::min(nextStop >> _fractionalBits, uint32_t(usec)); + uint32_t now = usecTimer.elapsed(); + int32_t delay = nextStopUInt - now; + if (delay > 0) { + while(usecTimer.elapsed() < nextStopUInt); + } else { + // This means we ran past nextStop and need to reset to actual time to avoid playing catch-up with a far too short period. + nextStop = (now << _fractionalBits) + (offTimePeriod >> 1); + } + } +#else // Cache the time taken so far. This saves us calling time, and we can be // assured that we can't have odd math problems. i.e. unsigned under/overflow. uint32_t elapsed = usecTimer.elapsed(); @@ -184,6 +256,7 @@ uint16_t IRsend::mark(uint16_t usec) { std::min(usec - elapsed - onTimePeriod, (uint32_t)offTimePeriod)); elapsed = usecTimer.elapsed(); // Update & recache the actual elapsed time. } +#endif return counter; } @@ -207,7 +280,7 @@ void IRsend::space(uint32_t time) { int8_t IRsend::calibrate(uint16_t hz) { if (hz < 1000) // Were we given kHz? Supports the old call usage. hz *= 1000; - periodOffset = 0; // Turn off any existing offset while we calibrate. + int8_t periodOffset = 0; enableIROut(hz); IRtimer usecTimer = IRtimer(); // Start a timer *just* before we do the call. uint16_t pulses = mark(UINT16_MAX); // Generate a PWM of 65,535 us. (Max.) diff --git a/src/IRsend.h b/src/IRsend.h index 38491372a..9275dc660 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -21,17 +21,6 @@ // Constants // Offset (in microseconds) to use in Period time calculations to account for // code excution time in producing the software PWM signal. -#if defined(ESP32) -// Calculated on a generic ESP-WROOM-32 board with v3.2-18 SDK @ 240MHz -const int8_t kPeriodOffset = -2; -#elif (defined(ESP8266) && F_CPU == 160000000L) // NOLINT(whitespace/parens) -// Calculated on an ESP8266 NodeMCU v2 board using: -// v2.6.0 with v2.5.2 ESP core @ 160MHz -const int8_t kPeriodOffset = -2; -#else // (defined(ESP8266) && F_CPU == 160000000L) -// Calculated on ESP8266 Wemos D1 mini using v2.4.1 with v2.4.0 ESP core @ 40MHz -const int8_t kPeriodOffset = -5; -#endif // (defined(ESP8266) && F_CPU == 160000000L) const uint8_t kDutyDefault = 50; // Percentage const uint8_t kDutyMax = 100; // Percentage // delayMicroseconds() is only accurate to 16383us. @@ -905,13 +894,13 @@ class IRsend { #else uint32_t _freq_unittest; #endif // UNIT_TEST - uint16_t onTimePeriod; - uint16_t offTimePeriod; + uint16_t onTimePeriod; // Fixed point. + uint16_t offTimePeriod; // Fixed point. + uint8_t _fractionalBits; // Number of fractional bits in on/offTimePeriod. uint16_t IRpin; - int8_t periodOffset; uint8_t _dutycycle; bool modulation; - uint32_t calcUSecPeriod(uint32_t hz, bool use_offset = true); + uint32_t calcUSecPeriod(uint32_t hz); #if SEND_SONY void _sendSony(const uint64_t data, const uint16_t nbits, const uint16_t repeat, const uint16_t freq); diff --git a/src/IRtimer.cpp b/src/IRtimer.cpp index e1f098b28..e5662aefc 100644 --- a/src/IRtimer.cpp +++ b/src/IRtimer.cpp @@ -31,10 +31,7 @@ uint32_t IRtimer::elapsed() { #else uint32_t now = _IRtimer_unittest_now; #endif - if (start <= now) // Check if the system timer has wrapped. - return now - start; // No wrap. - else - return UINT32_MAX - start + now; // Has wrapped. + return now - start; // Wrap safe. } /// Add time to the timer to simulate elapsed time. @@ -64,10 +61,7 @@ uint32_t TimerMs::elapsed() { #else uint32_t now = _TimerMs_unittest_now; #endif - if (start <= now) // Check if the system timer has wrapped. - return now - start; // No wrap. - else - return UINT32_MAX - start + now; // Has wrapped. + return now - start; // Wrap safe. } /// Add time to the timer to simulate elapsed time. diff --git a/src/ir_GlobalCache.cpp b/src/ir_GlobalCache.cpp index e8ebac4af..2f70deaf9 100644 --- a/src/ir_GlobalCache.cpp +++ b/src/ir_GlobalCache.cpp @@ -35,7 +35,7 @@ const uint8_t kGlobalCacheStartIndex = kGlobalCacheRptStartIndex + 1; void IRsend::sendGC(uint16_t buf[], uint16_t len) { uint16_t hz = buf[kGlobalCacheFreqIndex]; // GC frequency is in Hz. enableIROut(hz); - uint32_t periodic_time = calcUSecPeriod(hz, false); + uint32_t periodic_time = calcUSecPeriod(hz); uint8_t emits = std::min(buf[kGlobalCacheRptIndex], (uint16_t)kGlobalCacheMaxRepeat); // Repeat diff --git a/src/ir_Pronto.cpp b/src/ir_Pronto.cpp index 2d4ffa759..d1bda8b7f 100644 --- a/src/ir_Pronto.cpp +++ b/src/ir_Pronto.cpp @@ -72,7 +72,7 @@ void IRsend::sendPronto(uint16_t data[], uint16_t len, uint16_t repeat) { uint16_t seq_1_start = kProntoDataOffset; uint16_t seq_2_start = kProntoDataOffset + seq_1_len; - uint32_t periodic_time_x10 = calcUSecPeriod(hz / 10, false); + uint32_t periodic_time_x10 = calcUSecPeriod(hz / 10); // Normal (1st sequence) case. // Is there a first (normal) sequence to send? diff --git a/test/IRsend_test.cpp b/test/IRsend_test.cpp index 51fae7499..5bcf3c4d8 100644 --- a/test/IRsend_test.cpp +++ b/test/IRsend_test.cpp @@ -239,18 +239,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt38kHz) { irsend.reset(); irsend.enableIROut(38000, 50); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]10usecs[Off]11usecs[On]10usecs[Off]11usecs[On]10usecs[Off]11usecs" - "[On]10usecs[Off]11usecs[On]10usecs[Off]6usecs", + "[On]13usecs[Off]13usecs[On]13usecs[Off]13usecs[On]13usecs[Off]13usecs" + "[On]13usecs[Off]9usecs", irsend.low_level_sequence); irsend.reset(); irsend.enableIROut(38000, 33); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]6usecs[Off]15usecs[On]6usecs[Off]15usecs[On]6usecs[Off]15usecs" - "[On]6usecs[Off]15usecs[On]6usecs[Off]10usecs", + "[On]8usecs[Off]18usecs[On]8usecs[Off]18usecs[On]8usecs[Off]18usecs" + "[On]8usecs[Off]14usecs", irsend.low_level_sequence); irsend.reset(); @@ -266,18 +266,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt36_7kHz) { irsend.reset(); irsend.enableIROut(36700, 50); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]11usecs[Off]11usecs[On]11usecs[Off]11usecs[On]11usecs[Off]11usecs" - "[On]11usecs[Off]11usecs[On]11usecs[Off]1usecs", + "[On]13usecs[Off]14usecs[On]13usecs[Off]14usecs[On]13usecs[Off]14usecs" + "[On]13usecs[Off]6usecs", irsend.low_level_sequence); irsend.reset(); irsend.enableIROut(36700, 33); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]7usecs[Off]15usecs[On]7usecs[Off]15usecs[On]7usecs[Off]15usecs" - "[On]7usecs[Off]15usecs[On]7usecs[Off]5usecs", + "[On]8usecs[Off]19usecs[On]8usecs[Off]19usecs[On]8usecs[Off]19usecs" + "[On]8usecs[Off]11usecs", irsend.low_level_sequence); irsend.reset(); @@ -293,18 +293,18 @@ TEST(TestLowLevelSend, MarkFrequencyModulationAt40kHz) { irsend.reset(); irsend.enableIROut(40000, 50); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs" - "[On]10usecs[Off]10usecs[On]10usecs[Off]10usecs", + "[On]12usecs[Off]13usecs[On]12usecs[Off]13usecs[On]12usecs[Off]13usecs" + "[On]12usecs[Off]13usecs", irsend.low_level_sequence); irsend.reset(); irsend.enableIROut(40000, 33); - EXPECT_EQ(5, irsend.mark(100)); + EXPECT_EQ(4, irsend.mark(100)); EXPECT_EQ( - "[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs" - "[On]6usecs[Off]14usecs[On]6usecs[Off]14usecs", + "[On]8usecs[Off]17usecs[On]8usecs[Off]17usecs[On]8usecs[Off]17usecs" + "[On]8usecs[Off]17usecs", irsend.low_level_sequence); irsend.reset(); 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 6/6] 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); +}