Experimental basic support for Kelon 168 bit / 21 byte protocol. (#1747)

* Add `sendKelon168()` & `decodeKelon168()`
* Add & extend unit test coverage.
* Code style cleanup.

For #1745
Ref #1744
This commit is contained in:
David Conran
2022-02-20 23:09:22 +10:00
committed by GitHub
parent a3e2c81fe2
commit 673d38eba1
11 changed files with 265 additions and 45 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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.
};

View File

@@ -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:

View File

@@ -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 <algorithm>
#include <cassert>
#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

View File

@@ -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_

View File

@@ -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

View File

@@ -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));
}