diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index d20aaa7ca..5626db2c7 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -1050,8 +1050,12 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting Teknopoint decode"); if (decodeTeknopoint(results, offset)) return true; #endif // DECODE_TEKNOPOINT +#if DECODE_KELON168 + DPRINTLN("Attempting Kelon 168-bit decode"); + if (decodeKelon168(results, offset)) return true; +#endif // DECODE_KELON168 #if DECODE_KELON - DPRINTLN("Attempting Kelon decode"); + DPRINTLN("Attempting Kelon 48-bit decode"); if (decodeKelon(results, offset)) return true; #endif // DECODE_KELON #if DECODE_SANYO_AC88 diff --git a/src/IRrecv.h b/src/IRrecv.h index 379e45902..f9ff4b0ef 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -773,6 +773,11 @@ class IRrecv { bool decodeKelon(decode_results *results, uint16_t offset = kStartOffset, const uint16_t nbits = kKelonBits, const bool strict = true); #endif // DECODE_KELON +#if DECODE_KELON168 + bool decodeKelon168(decode_results *results, uint16_t offset = kStartOffset, + const uint16_t nbits = kKelon168Bits, + const bool strict = true); +#endif // DECODE_KELON168 #if DECODE_BOSE bool decodeBose(decode_results *results, uint16_t offset = kStartOffset, const uint16_t nbits = kBoseBits, const bool strict = true); diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index 26a05414c..94027ff39 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -847,6 +847,13 @@ #define SEND_AIRTON _IR_ENABLE_DEFAULT_ #endif // SEND_AIRTON +#ifndef DECODE_KELON168 +#define DECODE_KELON168 _IR_ENABLE_DEFAULT_ +#endif // DECODE_KELON168 +#ifndef SEND_KELON168 +#define SEND_KELON168 _IR_ENABLE_DEFAULT_ +#endif // SEND_KELON168 + #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 || \ @@ -862,6 +869,7 @@ DECODE_VOLTAS || DECODE_MIRAGE || DECODE_HAIER_AC176 || \ DECODE_TEKNOPOINT || DECODE_KELON || DECODE_TROTEC_3550 || \ DECODE_SANYO_AC88 || DECODE_RHOSS || DECODE_HITACHI_AC264 || \ + DECODE_KELON168 || \ 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 @@ -1013,8 +1021,9 @@ enum decode_type_t { AIRTON, COOLIX48, // 110 HITACHI_AC264, + KELON168, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = HITACHI_AC264, + kLastDecodeType = KELON168, }; // Message lengths & required repeat values @@ -1134,6 +1143,8 @@ const uint16_t kInaxBits = 24; const uint16_t kInaxMinRepeat = kSingleRepeat; const uint16_t kJvcBits = 16; const uint16_t kKelonBits = 48; +const uint16_t kKelon168StateLength = 21; +const uint16_t kKelon168Bits = kKelon168StateLength * 8; const uint16_t kKelvinatorStateLength = 16; const uint16_t kKelvinatorBits = kKelvinatorStateLength * 8; const uint16_t kKelvinatorDefaultRepeat = kNoRepeat; diff --git a/src/IRsend.cpp b/src/IRsend.cpp index 642be72d3..6df1aea96 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -724,6 +724,8 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { return kHitachiAc344Bits; case HITACHI_AC424: return kHitachiAc424Bits; + case KELON168: + return kKelon168Bits; case KELVINATOR: return kKelvinatorBits; case MILESTAG2: @@ -1227,6 +1229,11 @@ bool IRsend::send(const decode_type_t type, const uint8_t *state, sendHitachiAc424(state, nbytes); break; #endif // SEND_HITACHI_AC424 +#if SEND_KELON168 + case KELON168: + sendKelon168(state, nbytes); + break; +#endif // SEND_KELON168 #if SEND_KELVINATOR case KELVINATOR: sendKelvinator(state, nbytes); diff --git a/src/IRsend.h b/src/IRsend.h index e60e39bd0..e51a214b0 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -760,6 +760,11 @@ class IRsend { void sendKelon(const uint64_t data, const uint16_t nbits = kKelonBits, const uint16_t repeat = kNoRepeat); #endif // SEND_KELON +#if SEND_KELON168 + void sendKelon168(const unsigned char data[], + const uint16_t nbytes = kKelon168StateLength, + const uint16_t repeat = kNoRepeat); +#endif // SEND_KELON168 #if SEND_BOSE void sendBose(const uint64_t data, const uint16_t nbits = kBoseBits, const uint16_t repeat = kNoRepeat); diff --git a/src/IRtext.cpp b/src/IRtext.cpp index ad99e7e74..bd65ffd98 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -392,6 +392,7 @@ IRTEXT_CONST_BLOB_DECL(kAllProtocolNamesStr) { D_STR_AIRTON "\x0" D_STR_COOLIX48 "\x0" D_STR_HITACHI_AC264 "\x0" + D_STR_KELON168 "\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 bb1029440..166813035 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -193,6 +193,7 @@ bool hasACState(const decode_type_t protocol) { case HITACHI_AC264: case HITACHI_AC344: case HITACHI_AC424: + case KELON168: case KELVINATOR: case MIRAGE: case MITSUBISHI136: diff --git a/src/ir_Kelon.cpp b/src/ir_Kelon.cpp index 36f69d0e4..bb8f16093 100644 --- a/src/ir_Kelon.cpp +++ b/src/ir_Kelon.cpp @@ -1,7 +1,8 @@ // Copyright 2021 Davide Depau +// Copyright 2022 David Conran /// @file -/// @brief Support for Kelan AC protocol. +/// @brief Support for Kelon AC protocols. /// Both sending and decoding should be functional for models of series /// KELON ON/OFF 9000-12000. /// All features of the standard remote are implemented. @@ -12,6 +13,7 @@ /// - Fahrenheit. #include +#include #include "ir_Kelon.h" @@ -39,8 +41,13 @@ const uint16_t kKelonZeroSpace = 600; const uint32_t kKelonGap = 2 * kDefaultMessageGap; const uint16_t kKelonFreq = 38000; +const uint32_t kKelon168FooterSpace = 8000; +const uint16_t kKelon168Section1Size = 6; +const uint16_t kKelon168Section2Size = 8; +const uint16_t kKelon168Section3Size = 7; + #if SEND_KELON -/// Send a Kelon message. +/// Send a Kelon 48-bit message. /// Status: STABLE / Working. /// @param[in] data The data to be transmitted. /// @param[in] nbits Nr. of bits of data to be sent. @@ -52,12 +59,12 @@ void IRsend::sendKelon(const uint64_t data, const uint16_t nbits, kKelonBitMark, kKelonZeroSpace, kKelonBitMark, kKelonGap, data, nbits, kKelonFreq, false, // LSB First. - repeat, 50); + repeat, kDutyDefault); } #endif // SEND_KELON #if DECODE_KELON -/// Decode the supplied Kelon message. +/// Decode the supplied Kelon 48-bit message. /// Status: STABLE / Working. /// @param[in,out] results Ptr to the data to decode & where to store the result /// @param[in] offset The starting index to use when attempting to decode the @@ -440,3 +447,105 @@ String IRKelonAc::toString() const { result += addBoolToString(true, kSwingVToggleStr); return result; } + +#if SEND_KELON168 +/// Send a Kelon 168 bit / 21 byte message. +/// Status: BETA / Probably works. +/// @param[in] data The data to be transmitted. +/// @param[in] nbytes Nr. of bytes of data to be sent. +/// @param[in] repeat The number of times the command is to be repeated. +void IRsend::sendKelon168(const uint8_t data[], const uint16_t nbytes, + const uint16_t repeat) { + assert(kKelon168StateLength == kKelon168Section1Size + kKelon168Section2Size + + kKelon168Section3Size); + // Enough bytes to send a proper message? + if (nbytes < kKelon168StateLength) return; + + for (uint16_t r = 0; r <= repeat; r++) { + // Section #1 (48 bits) + sendGeneric(kKelonHdrMark, kKelonHdrSpace, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelon168FooterSpace, + data, kKelon168Section1Size, kKelonFreq, false, // LSB First. + 0, // No repeats here + kDutyDefault); + // Section #2 (64 bits) + sendGeneric(0, 0, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelon168FooterSpace, + data + kKelon168Section1Size, kKelon168Section2Size, + kKelonFreq, false, // LSB First. + 0, // No repeats here + kDutyDefault); + // Section #3 (56 bits) + sendGeneric(0, 0, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelonGap, + data + kKelon168Section1Size + kKelon168Section2Size, + nbytes - (kKelon168Section1Size + kKelon168Section2Size), + kKelonFreq, false, // LSB First. + 0, // No repeats here + kDutyDefault); + } +} +#endif // SEND_KELON168 + +#if DECODE_KELON168 +/// Decode the supplied Kelon 168 bit / 21 byte message. +/// Status: BETA / Probably Working. +/// @param[in,out] results Ptr to the data to decode & where to store the 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 True if it can decode it, false if it can't. +bool IRrecv::decodeKelon168(decode_results *results, uint16_t offset, + const uint16_t nbits, const bool strict) { + if (strict && nbits != kKelon168Bits) return false; + if (results->rawlen <= 2 * nbits + kHeader + kFooter * 2 - 1 + offset) + return false; // Can't possibly be a valid Kelon 168 bit message. + + uint16_t used = 0; + + used = matchGeneric(results->rawbuf + offset, results->state, + results->rawlen - offset, kKelon168Section1Size * 8, + kKelonHdrMark, kKelonHdrSpace, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelon168FooterSpace, + false, _tolerance, 0, false); + if (!used) return false; // Failed to match. + offset += used; + + used = matchGeneric(results->rawbuf + offset, + results->state + kKelon168Section1Size, + results->rawlen - offset, kKelon168Section2Size * 8, + 0, 0, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelon168FooterSpace, + false, _tolerance, 0, false); + if (!used) return false; // Failed to match. + offset += used; + + used = matchGeneric(results->rawbuf + offset, + results->state + (kKelon168Section1Size + + kKelon168Section2Size), + results->rawlen - offset, + nbits - (kKelon168Section1Size + + kKelon168Section2Size) * 8, + 0, 0, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelonGap, + true, _tolerance, 0, false); + if (!used) return false; // Failed to match. + + results->decode_type = decode_type_t::KELON168; + results->bits = nbits; + return true; +} +#endif // DECODE_KELON168 diff --git a/src/ir_Kelon.h b/src/ir_Kelon.h index 498650623..126669940 100644 --- a/src/ir_Kelon.h +++ b/src/ir_Kelon.h @@ -2,16 +2,23 @@ /// @file /// @brief Support for Kelan AC protocol. -/// Both sending and decoding should be functional for models of series KELON -/// ON/OFF 9000-12000. +/// @note Both sending and decoding should be functional for models of series +/// KELON ON/OFF 9000-12000. /// All features of the standard remote are implemented. /// /// @note Unsupported: /// - Explicit on/off due to AC unit limitations /// - Explicit swing position due to AC unit limitations /// - Fahrenheit. +/// +/// For KELON168: +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1745 + // Supports: -// Brand: Kelon, Model: ON/OFF 9000-12000 +// Brand: Kelon, Model: ON/OFF 9000-12000 (KELON) +// Brand: Kelon, Model: DG11R2-01 remote (KELON168) +// Brand: Kelon, Model: AST-09UW4RVETG00A A/C (KELON168) +// Brand: Hisense, Model: AST-09UW4RVETG00A A/C (KELON168) #ifndef IR_KELON_H_ #define IR_KELON_H_ @@ -71,84 +78,50 @@ class IRKelonAc { public: explicit IRKelonAc(uint16_t pin, bool inverted = false, bool use_modulation = true); - void stateReset(void); - #if SEND_KELON - void send(const uint16_t repeat = kNoRepeat); - /// Run the calibration to calculate uSec timing offsets for this platform. /// @return The uSec timing offset needed per modulation of the IR Led. /// @note This will produce a 65ms IR signal pulse at 38kHz. /// Only ever needs to be run once per object instantiation, if at all. int8_t calibrate(void) { return _irsend.calibrate(); } - /// Since the AC does not support actually setting the power state to a known /// value, this utility allow ensuring the AC is on or off by exploiting /// the fact that the AC, according to the user manual, will always turn on /// when setting it to "smart" or "super" mode. void ensurePower(const bool on); - - #endif + #endif // SEND_KELON void begin(void); - void setTogglePower(const bool toggle); - bool getTogglePower(void) const; - void setTemp(const uint8_t degrees); - uint8_t getTemp(void) const; - void setFan(const uint8_t speed); - uint8_t getFan(void) const; - void setDryGrade(const int8_t grade); - int8_t getDryGrade(void) const; - void setMode(const uint8_t mode); - uint8_t getMode(void) const; - void setToggleSwingVertical(const bool toggle); - bool getToggleSwingVertical(void) const; - void setSleep(const bool on); - bool getSleep(void) const; - void setSupercool(const bool on); - bool getSupercool(void) const; - void setTimer(const uint16_t mins); - uint16_t getTimer(void) const; - void setTimerEnabled(const bool on); - bool getTimerEnabled(void) const; - uint64_t getRaw(void) const; - void setRaw(const uint64_t new_code); - static uint8_t convertMode(const stdAc::opmode_t mode); - static uint8_t convertFan(const stdAc::fanspeed_t fan); - static stdAc::opmode_t toCommonMode(const uint8_t mode); - static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed); - stdAc::state_t toCommon(const stdAc::state_t *prev = nullptr) const; - String toString(void) const; private: @@ -166,5 +139,4 @@ class IRKelonAc { uint8_t _previousTemp = kKelonMinTemp; uint8_t _previousFan = kKelonFanAuto; }; - #endif // IR_KELON_H_ diff --git a/src/locale/defaults.h b/src/locale/defaults.h index c412ce972..4fb188f6c 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -826,6 +826,9 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_KELON #define D_STR_KELON "KELON" #endif // D_STR_KELON +#ifndef D_STR_KELON168 +#define D_STR_KELON168 D_STR_KELON "168" +#endif // D_STR_KELON168 #ifndef D_STR_KELVINATOR #define D_STR_KELVINATOR "KELVINATOR" #endif // D_STR_KELVINATOR diff --git a/test/ir_Kelon_test.cpp b/test/ir_Kelon_test.cpp index 1c056a5cb..dec439a24 100644 --- a/test/ir_Kelon_test.cpp +++ b/test/ir_Kelon_test.cpp @@ -425,12 +425,21 @@ TEST(TestIRKelonClass, toCommonToggles) { } TEST(TestUtils, Housekeeping) { + // KELON ASSERT_EQ("KELON", typeToString(decode_type_t::KELON)); ASSERT_EQ(decode_type_t::KELON, strToDecodeType("KELON")); ASSERT_FALSE(hasACState(decode_type_t::KELON)); ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::KELON)); ASSERT_EQ(kKelonBits, IRsend::defaultBits(decode_type_t::KELON)); ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::KELON)); + + // KELON168 + ASSERT_EQ("KELON168", typeToString(decode_type_t::KELON168)); + ASSERT_EQ(decode_type_t::KELON168, strToDecodeType("KELON168")); + ASSERT_TRUE(hasACState(decode_type_t::KELON168)); + ASSERT_FALSE(IRac::isProtocolSupported(decode_type_t::KELON168)); + ASSERT_EQ(kKelon168Bits, IRsend::defaultBits(decode_type_t::KELON168)); + ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::KELON168)); } TEST(TestDecodeKelon, Discussion1744) { @@ -497,3 +506,96 @@ TEST(TestDecodeKelon, Discussion1744) { EXPECT_NE(KELON, irsend.capture.decode_type); // Not a KELON message EXPECT_NE(kKelonBits, irsend.capture.bits); // Not a 48 bit message. } + +TEST(TestDecodeKelon168, RealExample) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + // Ref: https://github.com/crankyoldgit/IRremoteESP8266/discussions/1744#discussioncomment-2061968 + const uint16_t rawData[343] = { + 8922, 4494, // Header + 548, 1714, 524, 1718, 524, 572, 524, 550, // Byte 0 + 550, 556, 550, 560, 548, 586, 524, 1724, + 526, 564, 524, 1692, 550, 1696, 550, 552, // Byte 1 + 550, 554, 550, 560, 548, 586, 524, 550, + 548, 1688, 550, 1694, 548, 548, 550, 552, // Byte 2 + 548, 556, 548, 582, 524, 586, 524, 576, + 524, 540, 548, 568, 526, 572, 526, 548, // Byte 3 + 550, 1702, 550, 1706, 550, 1734, 526, 574, + 526, 564, 526, 544, 550, 548, 548, 550, // Byte 4 + 550, 554, 550, 582, 526, 586, 524, 550, + 548, 540, 550, 544, 550, 570, 526, 550, // Byte 5 + 550, 558, 546, 582, 524, 586, 524, 562, + 524, 7978, // Section Footer + 548, 1690, 548, 568, 524, 1696, 548, 1700, // Byte 6 + 548, 554, 550, 560, 548, 560, 550, 1722, + 526, 1686, 550, 1692, 548, 1696, 550, 576, // Byte 7 + 524, 1704, 548, 558, 550, 562, 548, 550, + 550, 564, 524, 570, 524, 548, 548, 576, // Byte 8 + 524, 578, 526, 558, 550, 586, 524, 574, + 524, 566, 524, 568, 526, 548, 548, 576, // Byte 9 + 526, 556, 548, 558, 550, 560, 550, 550, + 550, 564, 526, 544, 548, 548, 548, 552, // Byte 10 + 548, 556, 548, 582, 526, 564, 548, 550, + 550, 540, 548, 568, 526, 546, 550, 550, // Byte 11 + 550, 580, 524, 558, 550, 560, 550, 1698, + 550, 542, 548, 542, 550, 546, 550, 1722, // Byte 12 + 524, 1706, 548, 582, 526, 586, 524, 574, + 526, 1690, 548, 544, 550, 546, 550, 552, // Byte 13 + 548, 1726, 526, 1730, 524, 1734, 526, 562, + 524, 7976, // Section footer + 550, 566, 524, 544, 550, 570, 526, 550, // Byte 14 + 548, 554, 550, 1706, 550, 562, 548, 574, + 526, 542, 548, 544, 550, 572, 526, 548, // Byte 15 + 552, 554, 550, 582, 526, 584, 526, 574, + 524, 542, 548, 544, 548, 572, 524, 552, // Byte 16 + 548, 578, 524, 560, 548, 562, 548, 550, + 550, 540, 550, 546, 548, 572, 524, 576, // Byte 17 + 526, 556, 548, 560, 546, 564, 548, 574, + 526, 540, 550, 546, 546, 546, 550, 1698, // Byte 18 + 550, 1728, 524, 1706, 548, 564, 548, 574, + 524, 544, 548, 568, 526, 548, 548, 574, // Byte 19 + 524, 554, 550, 558, 550, 562, 548, 550, + 550, 566, 524, 544, 548, 546, 552, 1698, // Byte 20 + 548, 1702, 550, 560, 546, 586, 524, 538, + 546 // Footer + }; // KELON 178D000070030683 + + const uint8_t expected[kKelon168StateLength] = { + 0x83, 0x06, 0x03, 0x70, 0x00, 0x00, 0x8D, + 0x17, 0x00, 0x00, 0x00, 0x80, 0x18, 0x71, + 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x18}; + + irsend.begin(); + irsend.reset(); + irsend.sendRaw(rawData, 343, 38000); + irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::KELON168, irsend.capture.decode_type); + EXPECT_EQ(kKelon168Bits, irsend.capture.bits); + EXPECT_STATE_EQ(expected, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ("", IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t r, p; + ASSERT_FALSE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); +} + +TEST(TestDecodeKelon168, SyntheticExample) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + + const uint8_t expected[kKelon168StateLength] = { + 0x83, 0x06, 0x03, 0x70, 0x00, 0x00, 0x8D, + 0x17, 0x00, 0x00, 0x00, 0x80, 0x18, 0x71, + 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x18}; + + irsend.begin(); + irsend.reset(); + irsend.sendKelon168(expected); + irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::KELON168, irsend.capture.decode_type); + EXPECT_EQ(kKelon168Bits, irsend.capture.bits); + EXPECT_STATE_EQ(expected, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ("", IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t r, p; + ASSERT_FALSE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); +}