diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index 7ab71aa8d..1799b6b41 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -729,6 +729,11 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting Sharp decode"); if (decodeSharp(results, offset)) return true; #endif +#if DECODE_BOSCH144 + DPRINTLN("Attempting Bosch 144-bit decode"); + // Bosch is similar to Coolix, so it must be attempted before decodeCOOLIX. + if (decodeBosch144(results, offset)) return true; +#endif // DECODE_BOSCH144 #if DECODE_COOLIX DPRINTLN("Attempting Coolix 24-bit decode"); if (decodeCOOLIX(results, offset)) return true; diff --git a/src/IRrecv.h b/src/IRrecv.h index 042863e94..d3d91ee17 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -839,6 +839,12 @@ class IRrecv { const uint16_t nbits = kTcl96AcBits, const bool strict = true); #endif // DECODE_TCL96AC +#if DECODE_BOSCH144 + bool decodeBosch144(decode_results *results, + uint16_t offset = kStartOffset, + const uint16_t nbits = kBosch144Bits, + const bool strict = true); +#endif // DECODE_BOSCH144 }; #endif // IRRECV_H_ diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index cb1b15012..73de8c1d0 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -903,6 +903,13 @@ #define SEND_CLIMABUTLER _IR_ENABLE_DEFAULT_ #endif // SEND_CLIMABUTLER +#ifndef DECODE_BOSCH144 +#define DECODE_BOSCH144 _IR_ENABLE_DEFAULT_ +#endif // DECODE_BOSCH144 +#ifndef SEND_BOSCH144 +#define SEND_BOSCH144 _IR_ENABLE_DEFAULT_ +#endif // SEND_BOSCH144 + #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 || \ @@ -920,6 +927,7 @@ DECODE_SANYO_AC88 || DECODE_RHOSS || DECODE_HITACHI_AC264 || \ DECODE_KELON168 || DECODE_HITACHI_AC296 || DECODE_CARRIER_AC128 || \ DECODE_DAIKIN200 || DECODE_HAIER_AC160 || DECODE_TCL96AC || \ + DECODE_BOSCH144 || \ false) // Add any DECODE to the above if it uses result->state (see kStateSizeMax) // you might also want to add the protocol to hasACState function @@ -1079,8 +1087,9 @@ enum decode_type_t { TOTO, CLIMABUTLER, TCL96AC, + BOSCH144, // 120 // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = TCL96AC, + kLastDecodeType = BOSCH144, }; // Message lengths & required repeat values @@ -1101,6 +1110,8 @@ const uint16_t kArgoStateLength = 12; const uint16_t kArgoBits = kArgoStateLength * 8; const uint16_t kArgoDefaultRepeat = kNoRepeat; const uint16_t kArrisBits = 32; +const uint16_t kBosch144StateLength = 18; +const uint16_t kBosch144Bits = kBosch144StateLength * 8; const uint16_t kCoolixBits = 24; const uint16_t kCoolix48Bits = kCoolixBits * 2; const uint16_t kCoolixDefaultRepeat = kSingleRepeat; diff --git a/src/IRsend.cpp b/src/IRsend.cpp index 495015b0b..a3c37faf9 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -686,6 +686,8 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { return 64; case ARGO: return kArgoBits; + case BOSCH144: + return kBosch144Bits; case CORONA_AC: return kCoronaAcBits; case CARRIER_AC128: @@ -1148,6 +1150,11 @@ bool IRsend::send(const decode_type_t type, const uint8_t *state, sendArgo(state, nbytes); break; #endif // SEND_ARGO +#if SEND_BOSCH144 + case BOSCH144: + sendBosch144(state, nbytes); + break; +#endif // SEND_BOSCH144 #if SEND_CARRIER_AC128 case CARRIER_AC128: sendCarrierAC128(state, nbytes); diff --git a/src/IRsend.h b/src/IRsend.h index 8258c154d..1e70e8f67 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -820,6 +820,11 @@ class IRsend { const uint16_t nbits = kClimaButlerBits, const uint16_t repeat = kNoRepeat); #endif // SEND_CLIMABUTLER +#if SEND_BOSCH144 + void sendBosch144(const unsigned char data[], + const uint16_t nbytes = kBosch144StateLength, + const uint16_t repeat = kNoRepeat); +#endif // SEND_BOSCH144 protected: #ifdef UNIT_TEST diff --git a/src/IRtext.cpp b/src/IRtext.cpp index 84c193047..92ca34493 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -402,6 +402,7 @@ IRTEXT_CONST_BLOB_DECL(kAllProtocolNamesStr) { D_STR_TOTO "\x0" D_STR_CLIMABUTLER "\x0" D_STR_TCL96AC "\x0" + D_STR_BOSCH144 "\x0" ///< New protocol strings should be added just above this line. "\x0" ///< This string requires double null termination. }; diff --git a/src/IRutils.cpp b/src/IRutils.cpp index da7a4589b..66ef959b5 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -172,6 +172,7 @@ bool hasACState(const decode_type_t protocol) { // This is kept sorted by name case AMCOR: case ARGO: + case BOSCH144: case CARRIER_AC128: case CORONA_AC: case DAIKIN: diff --git a/src/ir_Bosch.cpp b/src/ir_Bosch.cpp new file mode 100644 index 000000000..f6e26eb52 --- /dev/null +++ b/src/ir_Bosch.cpp @@ -0,0 +1,112 @@ +// Copyright 2022 David Conran +/// @file +/// @brief Support for the Bosch A/C / heatpump protocol +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1787 + +// Supports: +// Brand: Bosch, Model: CL3000i-Set 26 E A/C +// Brand: Bosch, Model: RG10A(G2S)BGEF remote + +#include +#ifndef ARDUINO +#include +#endif +#include "IRrecv.h" +#include "IRsend.h" +#include "IRtext.h" +#include "IRutils.h" + +// Constants +const uint16_t kBoschHdrMark = 4366; +const uint16_t kBoschBitMark = 502; +const uint16_t kBoschHdrSpace = 4415; +const uint16_t kBoschOneSpace = 1645; +const uint16_t kBoschZeroSpace = 571; +const uint16_t kBoschFooterSpace = 5235; +const uint16_t kBoschFreq = 38000; // Hz. (Guessing the most common frequency.) +const uint16_t kBosch144NrOfSections = 3; + +#if SEND_BOSCH144 +/// Send a Bosch 144-bit / 18-byte message +/// Status: STABLE / Confirmed Working. +/// @param[in] data The message to be sent. +/// @param[in] nbytes The number of bytes of message to be sent. +/// @param[in] repeat The number of times the command is to be repeated. +void IRsend::sendBosch144(const unsigned char data[], const uint16_t nbytes, + const uint16_t repeat) { + // nbytes is required to be a multiple of kBosch144NrOfSections. + if (nbytes % kBosch144NrOfSections != 0) return; + + // Set IR carrier frequency + enableIROut(kBoschFreq); + + for (uint16_t r = 0; r <= repeat; r++) { + const uint16_t kSectionByteSize = nbytes / kBosch144NrOfSections; + for (uint16_t offset = 0; offset < nbytes; offset += kSectionByteSize) + // Section Header + Data + Footer + sendGeneric(kBoschHdrMark, kBoschHdrSpace, + kBoschBitMark, kBoschOneSpace, + kBoschBitMark, kBoschZeroSpace, + kBoschBitMark, kBoschFooterSpace, + data + offset, kSectionByteSize, + kBoschFreq, true, 0, kDutyDefault); + space(kDefaultMessageGap); // Complete guess + } +} + +#endif // SEND_BOSCH144 + +#if DECODE_BOSCH144 +/// Decode the supplied Bosch 144-bit / 18-byte A/C message. +/// Status: STABLE / Confirmed Working. +/// @param[in,out] results Ptr to the data to decode & where to store the decode +/// result. +/// @param[in] offset The starting index to use when attempting to decode the +/// raw data. Typically/Defaults to kStartOffset. +/// @param[in] nbits The number of data bits to expect. +/// @param[in] strict Flag indicating if we should perform strict matching. +/// @return A boolean. True if it can decode it, false if it can't. +bool IRrecv::decodeBosch144(decode_results *results, uint16_t offset, + const uint16_t nbits, const bool strict) { + if (results->rawlen < 2 * nbits + + kBosch144NrOfSections * (kHeader + kFooter) - + 1 + offset) + return false; // Can't possibly be a valid BOSCH144 message. + if (strict && nbits != kBosch144Bits) + return false; // Not strictly a BOSCH144 message. + if (nbits % 8 != 0) // nbits has to be a multiple of nr. of bits in a byte. + return false; + if (nbits % kBosch144NrOfSections != 0) + return false; // nbits has to be a multiple of kBosch144NrOfSections. + const uint16_t kSectionBits = nbits / kBosch144NrOfSections; + const uint16_t kSectionBytes = kSectionBits / 8; + const uint16_t kNBytes = kSectionBytes * kBosch144NrOfSections; + // Capture each section individually + for (uint16_t pos = 0, section = 0; + pos < kNBytes; + pos += kSectionBytes, section++) { + uint16_t used = 0; + // Section Header + Section Data + Section Footer + used = matchGeneric(results->rawbuf + offset, results->state + pos, + results->rawlen - offset, kSectionBits, + kBoschHdrMark, kBoschHdrSpace, + kBoschBitMark, kBoschOneSpace, + kBoschBitMark, kBoschZeroSpace, + kBoschBitMark, kBoschFooterSpace, + section >= kBosch144NrOfSections - 1, + _tolerance, kMarkExcess, true); + if (!used) return false; // Didn't match. + offset += used; + } + + // Compliance + + // Success + results->decode_type = decode_type_t::BOSCH144; + results->bits = nbits; + // No need to record the state as we stored it as we decoded it. + // As we use result->state, we don't record value, address, or command as it + // is a union data type. + return true; +} +#endif // DECODE_BOSCH144 diff --git a/src/locale/defaults.h b/src/locale/defaults.h index 8774a15cc..f87f310e7 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -706,6 +706,12 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_ARRIS #define D_STR_ARRIS "ARRIS" #endif // D_STR_ARRIS +#ifndef D_STR_BOSCH +#define D_STR_BOSCH "BOSCH" +#endif // D_STR_BOSCH +#ifndef D_STR_BOSCH144 +#define D_STR_BOSCH144 D_STR_BOSCH "144" +#endif // D_STR_BOSCH144 #ifndef D_STR_BOSE #define D_STR_BOSE "BOSE" #endif // D_STR_BOSE diff --git a/test/ir_Bosch_test.cpp b/test/ir_Bosch_test.cpp new file mode 100644 index 000000000..e9b6dc9ce --- /dev/null +++ b/test/ir_Bosch_test.cpp @@ -0,0 +1,100 @@ +// Copyright 2022 David Conran + +#include "IRac.h" +#include "IRrecv.h" +#include "IRrecv_test.h" +#include "IRsend.h" +#include "IRsend_test.h" +#include "gtest/gtest.h" + + +TEST(TestUtils, Housekeeping) { + // Bosch144 + ASSERT_EQ("BOSCH144", typeToString(decode_type_t::BOSCH144)); + ASSERT_EQ(decode_type_t::BOSCH144, strToDecodeType("BOSCH144")); + ASSERT_TRUE(hasACState(decode_type_t::BOSCH144)); + ASSERT_FALSE(IRac::isProtocolSupported(decode_type_t::BOSCH144)); + ASSERT_EQ(kBosch144Bits, IRsend::defaultBits(decode_type_t::BOSCH144)); + ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::BOSCH144)); +} + +// Tests for decodeBosch144(). + +// Decode normal Bosch144 messages. +TEST(TestDecodeBosch144, RealExample) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + + // Ref: https://github.com/crankyoldgit/IRremoteESP8266/issues/1787#issuecomment-1099993189 + // Mode: Cool; Fan: 100% ; Temp: 16°C + const uint16_t rawData[299] = { + 4380, 4400, + 528, 1646, 504, 570, 504, 1646, 504, 1646, 504, 572, 502, 570, 504, 1646, + 504, 570, 504, 572, 502, 1646, 504, 570, 502, 570, 502, 1648, 502, 1646, + 502, 570, 502, 1646, 504, 572, 502, 572, 502, 1644, 504, 1646, 504, 1646, + 504, 1646, 502, 1648, 500, 1646, 504, 1646, 504, 1646, 504, 572, 502, 570, + 504, 570, 504, 570, 504, 570, 504, 570, 506, 570, 502, 572, 502, 570, 502, + 572, 502, 572, 502, 572, 502, 572, 502, 572, 500, 1648, 502, 1644, 502, + 1646, 504, 1646, 502, 1646, 504, 1646, 504, 1644, 504, 1646, + 504, 5234, + 4360, 4422, + 504, 1646, 502, 596, 478, 1670, 478, 1646, 504, 570, 504, 572, 500, 1646, + 502, 572, 502, 572, 502, 1644, 506, 570, 502, 570, 504, 1644, 506, 1644, + 502, 574, 502, 1644, 504, 570, 504, 570, 504, 1644, 504, 1646, 504, 1644, + 506, 1644, 504, 1646, 504, 1646, 504, 1644, 504, 1646, 502, 570, 504, 570, + 504, 570, 504, 570, 502, 570, 504, 570, 502, 572, 502, 570, 504, 570, 504, + 570, 504, 570, 502, 572, 502, 570, 506, 570, 504, 1646, 502, 1646, 504, + 1646, 504, 1646, 504, 1646, 502, 1644, 504, 1644, 504, 1646, + 502, 5236, + 4360, 4424, + 504, 1646, 504, 1646, 502, 572, 504, 1644, 504, 570, 504, 1646, 504, 570, + 502, 1644, 504, 570, 504, 1644, 506, 1646, 502, 572, 502, 572, 502, 1646, + 504, 570, 504, 570, 504, 570, 502, 572, 504, 570, 504, 570, 504, 570, 502, + 572, 502, 570, 504, 570, 502, 570, 504, 572, 502, 572, 502, 1646, 504, + 570, 504, 570, 504, 570, 502, 574, 502, 572, 502, 572, 502, 572, 502, 572, + 502, 572, 502, 570, 504, 572, 502, 572, 502, 572, 502, 1646, 504, 572, + 502, 570, 502, 1646, 504, 572, 504, 570, 504, 1644, + 504}; // COOLIX B23F00 + const uint8_t expectedState[kBosch144StateLength] = { + 0xB2, 0x4D, 0x3F, 0xC0, 0x00, 0xFF, + 0xB2, 0x4D, 0x3F, 0xC0, 0x00, 0xFF, + 0xD5, 0x64, 0x00, 0x10, 0x00, 0x49}; + irsend.begin(); + irsend.reset(); + + irsend.sendRaw(rawData, 299, 38000); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::BOSCH144, irsend.capture.decode_type); + EXPECT_EQ(kBosch144Bits, irsend.capture.bits); + EXPECT_STATE_EQ(expectedState, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ( + "", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t result, prev; + ASSERT_FALSE(IRAcUtils::decodeToState(&irsend.capture, &result, &prev)); +} + +TEST(TestDecodeBosch144, SyntheticSelfDecode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + const uint8_t expectedState[kBosch144StateLength] = { + 0xB2, 0x4D, 0x3F, 0xC0, 0x00, 0xFF, + 0xB2, 0x4D, 0x3F, 0xC0, 0x00, 0xFF, + 0xD5, 0x64, 0x00, 0x10, 0x00, 0x49}; + irsend.sendBosch144(expectedState); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::BOSCH144, irsend.capture.decode_type); + EXPECT_EQ(kBosch144Bits, irsend.capture.bits); + EXPECT_STATE_EQ(expectedState, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ( + "", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t result, prev; + ASSERT_FALSE(IRAcUtils::decodeToState(&irsend.capture, &result, &prev)); +}