diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index eb8857475..f6e065eaa 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -1114,6 +1114,11 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting Carrier AC 128-bit decode"); if (decodeCarrierAC128(results, offset)) return true; #endif // DECODE_CARRIER_AC128 +#if DECODE_TOTO + DPRINTLN("Attempting Toto 48/24-bit decode"); + if (decodeToto(results, offset, kTotoLongBits) || // Long needs to be first + decodeToto(results, offset, kTotoShortBits)) return true; +#endif // DECODE_TOTO // Typically new protocols are added above this line. } #if DECODE_HASH @@ -1128,7 +1133,7 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, if (!resumed) // Check if we have already resumed. resume(); return false; -} +} // NOLINT(readability/fn_size) /// Convert the tolerance percentage into something valid. /// @param[in] percentage An integer percentage. diff --git a/src/IRrecv.h b/src/IRrecv.h index 13fd02b24..b22d488fd 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -822,6 +822,11 @@ class IRrecv { const uint16_t nbits = kAirtonBits, const bool strict = true); #endif // DECODE_AIRTON +#if DECODE_TOTO + bool decodeToto(decode_results *results, uint16_t offset = kStartOffset, + const uint16_t nbits = kTotoBits, + const bool strict = true); +#endif // DECODE_TOTO }; #endif // IRRECV_H_ diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index ce89fdb33..027440764 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -882,6 +882,13 @@ #define SEND_HAIER_AC160 _IR_ENABLE_DEFAULT_ #endif // SEND_HAIER_AC160 +#ifndef DECODE_TOTO +#define DECODE_TOTO _IR_ENABLE_DEFAULT_ +#endif // DECODE_TOTO +#ifndef SEND_TOTO +#define SEND_TOTO _IR_ENABLE_DEFAULT_ +#endif // SEND_TOTO + #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 || \ @@ -1055,8 +1062,9 @@ enum decode_type_t { DAIKIN200, HAIER_AC160, // 115 CARRIER_AC128, + TOTO, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = CARRIER_AC128, + kLastDecodeType = TOTO, }; // Message lengths & required repeat values @@ -1298,6 +1306,10 @@ const uint16_t kToshibaACStateLengthShort = kToshibaACStateLength - 2; const uint16_t kToshibaACBitsShort = kToshibaACStateLengthShort * 8; const uint16_t kToshibaACStateLengthLong = kToshibaACStateLength + 1; const uint16_t kToshibaACBitsLong = kToshibaACStateLengthLong * 8; +const uint16_t kTotoBits = 24; +const uint16_t kTotoShortBits = kTotoBits; +const uint16_t kTotoLongBits = kTotoShortBits * 2; +const uint16_t kTotoDefaultRepeat = kSingleRepeat; const uint16_t kTranscoldBits = 24; const uint16_t kTranscoldDefaultRepeat = kNoRepeat; const uint16_t kTrotecStateLength = 9; diff --git a/src/IRsend.cpp b/src/IRsend.cpp index f4647728a..84983e7f2 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -571,6 +571,7 @@ uint16_t IRsend::minRepeats(const decode_type_t protocol) { case MULTIBRACKETS: case SHERWOOD: case TOSHIBA_AC: + case TOTO: return kSingleRepeat; // Special case AIRWELL: @@ -633,6 +634,7 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { case MIDEA24: case NIKAI: case RCMM: + case TOTO: case TRANSCOLD: return 24; case LG: @@ -1072,6 +1074,11 @@ bool IRsend::send(const decode_type_t type, const uint64_t data, sendTeco(data, nbits, min_repeat); break; #endif // SEND_TECO +#if SEND_TOTO + case TOTO: + sendToto(data, nbits, min_repeat); + break; +#endif // SEND_TOTO #if SEND_TRANSCOLD case TRANSCOLD: sendTranscold(data, nbits, min_repeat); diff --git a/src/IRsend.h b/src/IRsend.h index df9dafa49..ccc07fe23 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -806,6 +806,10 @@ class IRsend { void sendAirton(const uint64_t data, const uint16_t nbits = kAirtonBits, const uint16_t repeat = kAirtonDefaultRepeat); #endif // SEND_AIRTON +#if SEND_TOTO + void sendToto(const uint64_t data, const uint16_t nbits = kTotoBits, + const uint16_t repeat = kTotoDefaultRepeat); +#endif // SEND_TOTO protected: #ifdef UNIT_TEST diff --git a/src/IRtext.cpp b/src/IRtext.cpp index 959620b8c..0f681922b 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -399,6 +399,7 @@ IRTEXT_CONST_BLOB_DECL(kAllProtocolNamesStr) { D_STR_DAIKIN200 "\x0" D_STR_HAIER_AC160 "\x0" D_STR_CARRIER_AC128 "\x0" + D_STR_TOTO "\x0" ///< New protocol strings should be added just above this line. "\x0" ///< This string requires double null termination. }; diff --git a/src/ir_Toto.cpp b/src/ir_Toto.cpp new file mode 100644 index 000000000..2997e760e --- /dev/null +++ b/src/ir_Toto.cpp @@ -0,0 +1,131 @@ +// Copyright 2022 David Conran (crankyoldgit) +/// @file +/// @brief Support for the Toto Toilet IR protocols. +/// @see https://www.d-resi.jp/dt/nishi-shinjuku/limited/imgs/pdf/book6.pdf +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1806 + +// Supports: +// Brand: Toto, Model: Washlet Toilet NJ + +#include +#include "IRrecv.h" +#include "IRsend.h" +#include "IRutils.h" + +// Constants +const uint16_t kTotoHdrMark = 6197; +const uint16_t kTotoHdrSpace = 2754; +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 uint64_t kTotoPrefix = 0x0802; +const uint16_t kTotoPrefixBits = 15; + +#if SEND_TOTO +/// Send a Toto Toilet formatted message. +/// Status: BETA / Seems to work. +/// @param[in] data The message to be sent. +/// @param[in] nbits The number of bits of message to be sent. +/// @param[in] repeat The number of times the command is to be repeated. +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1806 +void IRsend::sendToto(const uint64_t data, const uint16_t nbits, + const uint16_t repeat) { + if (nbits <= kTotoShortBits) { // Short code message. + sendGeneric(kTotoHdrMark, kTotoHdrSpace, + kTotoBitMark, kTotoOneSpace, + kTotoBitMark, kTotoZeroSpace, + kTotoBitMark, kTotoGap, + (data << kTotoPrefixBits) | kTotoPrefix, + nbits + kTotoPrefixBits, + 38, false, repeat, kDutyDefault); + } else { // Long code message + // This is really two messages sent at least one extra time each. + sendToto(data >> kTotoShortBits, nbits - kTotoShortBits, repeat + 1); + sendToto(GETBITS64(data, 0, kTotoShortBits), kTotoShortBits, repeat + 1); + } +} +#endif // SEND_TOTO + +#if DECODE_TOTO +/// Decode the supplied Toto Toilet message. +/// Status: ALPHA / Untested. +/// @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. +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1806 +bool IRrecv::decodeToto(decode_results *results, uint16_t offset, + const uint16_t nbits, const bool strict) { + if (strict && nbits != kTotoShortBits && nbits != kTotoLongBits) + return false; // We expect Toto to be a certain sized messages. + + const uint16_t repeats = (nbits > kTotoShortBits) ? kTotoDefaultRepeat + 1 + : kTotoDefaultRepeat; + const uint16_t ksections = (nbits > kTotoShortBits) ? 2 : 1; + const uint16_t ksection_bits = nbits / ksections; + + if (results->rawlen < (2 * (nbits + kTotoPrefixBits) + kHeader + kFooter) * + (repeats + 1) - 1 + offset) + return false; // We don't have enough entries to possibly match. + + uint16_t used = 0; + + // Long messages have two sections, short have only one. + for (uint16_t section = 1; section <= ksections; section++) { + results->value <<= ksection_bits; + uint64_t data = 0; + uint64_t repeat_data = 0; + for (uint16_t r = 0; r <= repeats; r++) { // We expect a repeat. + uint64_t prefix = 0; + // Header + Prefix + used = matchGeneric(results->rawbuf + offset, &prefix, + results->rawlen - offset, kTotoPrefixBits, + kTotoHdrMark, kTotoHdrSpace, + kTotoBitMark, kTotoOneSpace, + kTotoBitMark, kTotoZeroSpace, + 0, 0, false, // No Footer + kUseDefTol, kMarkExcess, false); + if (!used) return false; // Didn't match, so fail. + offset += used; + if (strict && (prefix != kTotoPrefix)) return false; + // Data + Footer + Gap + used = matchGeneric(results->rawbuf + offset, &data, + results->rawlen - offset, ksection_bits, + 0, 0, // No Header + kTotoBitMark, kTotoOneSpace, + kTotoBitMark, kTotoZeroSpace, + kTotoBitMark, kTotoGap, true, + kUseDefTol, kMarkExcess, false); + if (!used) return false; // Didn't match, so fail. + offset += used; + if (strict) { + if (r && data != repeat_data) return false; // Repeat didn't match. + // Integrity check. + uint8_t result = 0; + uint64_t check = data; + uint16_t bits = 0; + // Loop over the data 8 bits at a time. + while (bits < ksection_bits) { + result ^= (check & 0b111111111); // Xor with the last 8 bits. + bits += 8; + check >>= 8; + } + if (result) return false; // Intergrity check failed. + } + repeat_data = data; + } + results->value |= data; + } + // Success + results->bits = nbits; + results->decode_type = decode_type_t::TOTO; + results->command = GETBITS64(results->value, 0, ksection_bits - 8); + results->address = GETBITS64(results->value, ksection_bits, + ksection_bits - 8); + return true; +} +#endif // DECODE_TOTO diff --git a/src/locale/defaults.h b/src/locale/defaults.h index b70179e2b..700673707 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -1009,6 +1009,9 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_TOSHIBA_AC #define D_STR_TOSHIBA_AC "TOSHIBA_AC" #endif // D_STR_TOSHIBA_AC +#ifndef D_STR_TOTO +#define D_STR_TOTO "TOTO" +#endif // D_STR_TOTO #ifndef D_STR_TRANSCOLD #define D_STR_TRANSCOLD "TRANSCOLD" #endif // D_STR_TRANSCOLD diff --git a/test/ir_Toto_test.cpp b/test/ir_Toto_test.cpp new file mode 100644 index 000000000..050bd01e6 --- /dev/null +++ b/test/ir_Toto_test.cpp @@ -0,0 +1,159 @@ +// Copyright 2022 crankyoldgit (David Conran) + +#include "IRac.h" +#include "IRsend.h" +#include "IRsend_test.h" +#include "IRutils.h" +#include "gtest/gtest.h" + + +// General housekeeping +TEST(TestToto, Housekeeping) { + ASSERT_EQ("TOTO", typeToString(TOTO)); + ASSERT_FALSE(hasACState(TOTO)); + ASSERT_EQ(kTotoBits, IRsend::defaultBits(decode_type_t::TOTO)); + ASSERT_EQ(kSingleRepeat, IRsend::minRepeats(decode_type_t::TOTO)); +} + +// Tests for decodeToto(). + +// Decode normal Toto messages. +TEST(TestDecodeToto, SyntheticShortDecode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + // Short Toto 24-bit message. + irsend.reset(); + irsend.sendToto(0x0D0D00); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(TOTO, irsend.capture.decode_type); + EXPECT_EQ(kTotoBits, irsend.capture.bits); + EXPECT_EQ(0x0D0D00, irsend.capture.value); + EXPECT_EQ(0, irsend.capture.address); + EXPECT_EQ(0x0D00, irsend.capture.command); +} + +// Decode real example via Issue #1806 +TEST(TestDecodeToto, RealShortDecode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + // Toto Full Flush from Issue #1806 + const uint16_t rawData[163] = { + 6266, 2734, + 598, 540, 598, 1626, 598, 512, 622, 516, 598, 514, 598, 510, 598, 514, + 628, 512, 596, 514, 600, 512, 598, 538, 600, 1622, 600, 512, 598, 540, + 602, 510, 598, 512, 598, 512, 624, 514, 598, 512, 598, 512, 598, 514, + 624, 512, 598, 514, 598, 1652, 596, 514, 598, 1626, 598, 1650, 598, 514, + 598, 512, 598, 540, 598, 514, 596, 1626, 626, 512, 574, 1648, 598, 1650, + 598, 514, 598, 512, 594, 544, 596, 514, + 598, 37996, + 6182, 2764, + 598, 514, 600, 1648, 600, 512, 596, 514, 598, 540, 598, 512, 600, 512, + 598, 512, 624, 514, 598, 514, 598, 512, 596, 1652, 598, 514, 598, 512, + 596, 540, 598, 514, 598, 512, 598, 512, 598, 540, 596, 516, 596, 514, 598, + 512, 574, 564, 598, 1626, 568, 542, 624, 1624, 626, 1622, 598, 514, 596, + 514, 598, 514, 596, 540, 600, 1622, 598, 512, 600, 1650, 598, 1624, 596, + 540, 600, 512, 598, 514, 596, 514, + 622}; // UNKNOWN 43BD67B3 + + irsend.sendRaw(rawData, 163, 38); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(TOTO, irsend.capture.decode_type); + EXPECT_EQ(kTotoBits, irsend.capture.bits); + EXPECT_EQ(0x0D0D00, irsend.capture.value); + EXPECT_EQ(0, irsend.capture.address); + EXPECT_EQ(0x0D00, irsend.capture.command); +} + +// Decode real example via Issue #1806 +TEST(TestDecodeToto, RealLongDecode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + // Oscillate Bidet Function from Issue #1806 + const uint16_t rawData[491] = { + 6262, 2738, + 600, 538, 596, 1624, 598, 512, 652, 486, 628, 484, 600, 510, 596, 516, + 650, 488, 622, 488, 626, 484, 598, 540, 600, 1624, 602, 510, 596, 540, + 602, 510, 598, 514, 598, 512, 626, 512, 598, 512, 598, 512, 598, 514, + 626, 510, 598, 514, 598, 512, 598, 1650, 600, 1622, 600, 538, 596, 514, + 600, 510, 596, 514, 596, 542, 598, 514, 598, 1624, 626, 1622, 598, 512, + 600, 512, 620, 516, 598, 512, 598, 514, + 598, 40244, + 6184, 2764, + 598, 514, 598, 1650, 598, 514, 596, 516, 596, 542, 598, 512, 596, 516, + 600, 510, 624, 512, 598, 512, 596, 516, 598, 1652, 600, 512, 598, 512, + 598, 540, 596, 518, 594, 514, 598, 512, 596, 542, 598, 512, 596, 514, + 598, 512, 598, 540, 598, 514, 598, 1622, 624, 1626, 598, 514, 598, 512, + 626, 512, 596, 514, 596, 514, 596, 540, 598, 1624, 600, 1650, 600, 512, + 600, 510, 598, 512, 596, 540, 598, 512, + 600, 40244, + 6186, 2760, + 600, 512, 596, 1626, 624, 514, 598, 512, 600, 512, 598, 512, 626, 512, + 598, 512, 598, 512, 596, 540, 600, 510, 600, 1622, 600, 538, 596, 514, + 596, 514, 598, 514, 624, 514, 596, 516, 596, 514, 600, 512, 622, 514, 600, + 512, 600, 510, 600, 538, 602, 1622, 600, 1648, 626, 512, 572, 512, 598, + 514, 650, 488, 598, 512, 594, 516, 598, 1652, 598, 1626, 598, 514, 624, + 514, 596, 512, 600, 512, 598, 540, + 596, 40246, + 6184, 2736, + 598, 538, 598, 1624, 598, 512, 598, 540, 598, 512, 600, 512, 596, 514, + 600, 538, 626, 486, 598, 514, 596, 514, 652, 1596, 594, 516, 624, 514, + 594, 516, 598, 514, 622, 490, 596, 540, 596, 514, 624, 488, 594, 514, 598, + 538, 598, 512, 598, 514, 594, 516, 596, 540, 600, 1622, 572, 566, 600, + 512, 570, 542, 598, 512, 598, 540, 600, 512, 594, 516, 598, 1652, 600, + 512, 598, 514, 596, 514, 624, 514, + 598, 42468, + 6182, 2764, + 624, 490, 594, 1652, 600, 512, 596, 514, 596, 540, 600, 512, 598, 516, + 596, 514, 598, 538, 598, 516, 598, 512, 570, 1678, 596, 514, 596, 514, + 598, 512, 624, 512, 594, 516, 596, 514, 600, 510, 626, 512, 596, 516, + 594, 516, 598, 538, 598, 512, 596, 514, 596, 514, 596, 1652, 600, 514, + 594, 516, 622, 514, 600, 512, 598, 514, 596, 514, 624, 514, 596, 1626, + 598, 514, 622, 516, 594, 516, 598, 514, + 596, 42496, + 6184, 2764, + 596, 516, 598, 1624, 626, 512, 598, 512, 596, 516, 600, 512, 624, 514, + 598, 514, 594, 516, 596, 516, 620, 516, 600, 1624, 596, 540, 598, 514, + 600, 512, 596, 516, 596, 542, 594, 516, 598, 514, 594, 516, 624, 512, 598, + 512, 596, 514, 596, 514, 624, 514, 596, 516, 598, 1650, 594, 518, 596, + 514, 596, 516, 596, 542, 596, 514, 596, 516, 596, 514, 596, 1652, 598, + 514, 596, 514, 624, 514, 622, 488, 596}; // UNKNOWN 4AC5E8B3 + + + irsend.sendRaw(rawData, 491, 38); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(TOTO, irsend.capture.decode_type); + EXPECT_EQ(kTotoLongBits, irsend.capture.bits); + EXPECT_EQ(0x60600080800, irsend.capture.value); + EXPECT_EQ(0x0600, irsend.capture.address); + EXPECT_EQ(0x0800, irsend.capture.command); +} + +TEST(TestDecodeToto, SyntheticLongDecode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + // Long Toto 48-bit message. + irsend.reset(); + irsend.sendToto(0x60600080800, kTotoLongBits); + irsend.makeDecodeResult(); + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(TOTO, irsend.capture.decode_type); + EXPECT_EQ(kTotoLongBits, irsend.capture.bits); + EXPECT_EQ(0x60600080800, irsend.capture.value); + EXPECT_EQ(0x0600, irsend.capture.address); + EXPECT_EQ(0x0800, irsend.capture.command); +}