From 6c7b52cd8779055578b10608d266ecc90597a2a4 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 17 Aug 2020 00:13:33 +1000 Subject: [PATCH] Voltas: Progress check-in * Checksum support. * Power, toString, toCommon, set/GetRaw etc. For #1238 --- src/IRac.cpp | 16 +++++ src/ir_Voltas.cpp | 143 +++++++++++++++++++++++++++++++++++++++- src/ir_Voltas.h | 128 +++++++++++++++++++++++++++++++++++ test/ir_Voltas_test.cpp | 44 +++++++++++++ 4 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 src/ir_Voltas.h diff --git a/src/IRac.cpp b/src/IRac.cpp index 4e54d26f0..8980433b5 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -42,6 +42,7 @@ #include "ir_Toshiba.h" #include "ir_Trotec.h" #include "ir_Vestel.h" +#include "ir_Voltas.h" #include "ir_Whirlpool.h" /// Class constructor @@ -3112,6 +3113,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_VESTEL_AC +#if DECODE_VOLTAS + case decode_type_t::VOLTAS: { + IRVoltas ac(kGpioUnused); + ac.setRaw(result->state); + return ac.toString(); + } +#endif // DECODE_VOLTAS #if DECODE_TECO case decode_type_t::TECO: { IRTecoAc ac(kGpioUnused); @@ -3513,6 +3521,14 @@ namespace IRAcUtils { break; } #endif // DECODE_VESTEL_AC +#if DECODE_VOLTAS + case decode_type_t::VOLTAS: { + IRVestelAc ac(kGpioUnused); + ac.setRaw(decode->state); + *result = ac.toCommon(); + break; + } +#endif // DECODE_VOLTAS #if DECODE_WHIRLPOOL_AC case decode_type_t::WHIRLPOOL_AC: { IRWhirlpoolAc ac(kGpioUnused); diff --git a/src/ir_Voltas.cpp b/src/ir_Voltas.cpp index f0fe910f7..8a1051481 100644 --- a/src/ir_Voltas.cpp +++ b/src/ir_Voltas.cpp @@ -4,13 +4,19 @@ /// @brief Support for Voltas A/C protocol /// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1238 -// Supports: -// Brand: Voltas, Model: 122LZF 4011252 Window A/C - +#include "ir_Voltas.h" +#include #include "IRrecv.h" #include "IRsend.h" +#include "IRtext.h" #include "IRutils.h" +using irutils::addBoolToString; +using irutils::addModeToString; +using irutils::addFanToString; +using irutils::addTempToString; +using irutils::minsToString; + // Constants const uint16_t kVoltasBitMark = 1026; ///< uSeconds. const uint16_t kVoltasOneSpace = 2553; ///< uSeconds. @@ -61,9 +67,140 @@ bool IRrecv::decodeVoltas(decode_results *results, uint16_t offset, kVoltasBitMark, kVoltasZeroSpace, kVoltasBitMark, kDefaultMessageGap, true)) return false; + // Compliance + if (strict && !IRVoltas::validChecksum(results->state, nbits / 8)) + return false; // Success results->decode_type = decode_type_t::VOLTAS; results->bits = nbits; return true; } #endif // DECODE_VOLTAS + +/// Class constructor +/// @param[in] pin GPIO to be used when sending. +/// @param[in] inverted Is the output signal to be inverted? +/// @param[in] use_modulation Is frequency modulation to be used? +IRVoltas::IRVoltas(const uint16_t pin, const bool inverted, + const bool use_modulation) + : _irsend(pin, inverted, use_modulation) { + stateReset(); +} + +// Reset the internal state to a fixed known good state. +void IRVoltas::stateReset() { + // This resets to a known-good state. + std::memset(_.raw, 0, sizeof _.raw); +} + +/// Set up hardware to be able to send a message. +void IRVoltas::begin() { _irsend.begin(); } + +#if SEND_VOLTAS +/// Send the current internal state as an IR message. +/// @param[in] repeat Nr. of times the message will be repeated. +void IRVoltas::send(const uint16_t repeat) { + _irsend.sendVoltas(getRaw(), kVoltasStateLength, repeat); +} +#endif // SEND_VOLTAS + +/// Get a PTR to the internal state/code for this protocol. +/// @return PTR to a code for this protocol based on the current internal state. +uint8_t* IRVoltas::getRaw(void) { + checksum(); // Ensure correct settings before sending. + return _.raw; +} + +/// Set the internal state from a valid code for this protocol. +/// @param[in] new_code A valid code for this protocol. +void IRVoltas::setRaw(const uint8_t new_code[]) { + std::memcpy(_.raw, new_code, kVoltasStateLength); +} + +/// Calculate and set the checksum values for the internal state. +void IRVoltas::checksum(void) { + _.Checksum = calcChecksum(_.raw, kVoltasStateLength - 1); +} + +/// Verify the checksum is valid for a given state. +/// @param[in] state The array to verify the checksum of. +/// @param[in] length The length of the state array. +/// @return true, if the state has a valid checksum. Otherwise, false. +bool IRVoltas::validChecksum(const uint8_t state[], const uint16_t length) { + if (length) return state[length - 1] == calcChecksum(state, length); + return true; +} + +/// Calculate the checksum is valid for a given state. +/// @param[in] state The array to calculate the checksum of. +/// @param[in] length The length of the state array. +/// @return The valid checksum value for the state. +uint8_t IRVoltas::calcChecksum(const uint8_t state[], const uint16_t length) { + uint8_t result = 0; + if (length) + result = sumBytes(state, length - 1); + return ~result; +} + +/// Change the power setting to On. +void IRVoltas::on() { setPower(true); } + +/// Change the power setting to Off. +void IRVoltas::off() { setPower(false); } + +/// Change the power setting. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRVoltas::setPower(const bool on) { _.Power = on; } + +/// Get the value of the current power setting. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getPower(void) const { return _.Power; } + +/// Convert the current internal state into its stdAc::state_t equivilant. +/// @return The stdAc equivilant of the native settings. +stdAc::state_t IRVoltas::toCommon() { + stdAc::state_t result; + result.protocol = decode_type_t::VOLTAS; + result.power = _.Power; + // result.mode = toCommonMode(getMode()); + result.celsius = true; + /* + result.degrees = getTemp(); + result.fanspeed = toCommonFanSpeed(getFan()); + if (getSwingVerticalAuto()) + result.swingv = stdAc::swingv_t::kAuto; + else + result.swingv = toCommonSwingV(getSwingVerticalPosition()); + */ + result.turbo = _.Turbo; + result.econo = _.Econo; + result.light = _.Light; + /* + result.clean = getXFan(); + result.sleep = getSleep() ? 0 : -1; + */ + // Not supported. + result.model = -1; + result.swingh = stdAc::swingh_t::kOff; + result.quiet = false; + result.filter = false; + result.beep = false; + result.clock = -1; + return result; +} + +/// Convert the current internal state into a human readable string. +/// @return A human readable string. +String IRVoltas::toString() { + String result = ""; + result.reserve(80); // Reserve some heap for the string to reduce fragging. + result += addBoolToString(_.Power, kPowerStr, false); + /* + result += addModeToString(getMode(), kVoltasAuto, kVoltasCool, kVoltasHeat, + kVoltasDry, kVoltasFan); + */ + result += addBoolToString(_.Turbo, kTurboStr); + result += addBoolToString(_.Wifi, kWifiStr); + result += addBoolToString(_.Light, kLightStr); + return result; +} diff --git a/src/ir_Voltas.h b/src/ir_Voltas.h new file mode 100644 index 000000000..a069e2551 --- /dev/null +++ b/src/ir_Voltas.h @@ -0,0 +1,128 @@ +// Copyright 2020 David Conran (crankyoldgit) +// Copyright 2020 manj9501 +/// @file +/// @brief Support for Voltas A/C protocol +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1238 + +// Supports: +// Brand: Voltas, Model: 122LZF 4011252 Window A/C +// +// Ref: https://docs.google.com/spreadsheets/d/1zzDEUQ52y7MZ7_xCU3pdjdqbRXOwZLsbTGvKWcicqCI/ +// Ref: https://www.corona.co.jp/box/download.php?id=145060636229 + +#ifndef IR_VOLTAS_H_ +#define IR_VOLTAS_H_ + +#define __STDC_LIMIT_MACROS +#include +#ifndef UNIT_TEST +#include +#endif +#include "IRremoteESP8266.h" +#include "IRsend.h" +#ifdef UNIT_TEST +#include "IRsend_test.h" +#endif + + +union VoltasProtocol { + uint8_t raw[kVoltasStateLength]; ///< The state in native IR code form + struct { + // Byte 0 + uint8_t SwingH :1; + uint8_t Unknown0 :7; + // Byte 1 + uint8_t Mode :4; + uint8_t :1; + uint8_t FanSpeed :3; + // Byte 2 + uint8_t SwingV :3; + uint8_t Wifi :1; + uint8_t :1; + uint8_t Turbo :1; + uint8_t Sleep :1; + uint8_t Power :1; + // Byte 3 + uint8_t Temp :4; + uint8_t Unknown3 :2; // Typically 0b01 + uint8_t Econo :1; + uint8_t TempSet :1; + // Byte 4 + uint8_t OffTimer24h4 :1; + uint8_t :7; // Typically 0b0011101 + // Byte 5 + uint8_t OffTimer24h5 :1; + uint8_t :6; // Typically 0b011101 + uint8_t TimerAdd12Hr :1; + // Byte 6 + uint8_t :8; // Typically 0b00111011(0x3B) + // Byte 7 + uint8_t :4; // Typically 0b0001 + uint8_t TimerHrs :4; // Nr of Hours. + // Byte 8 + uint8_t :5; // Typically 0b00000 + uint8_t Light :1; + uint8_t OffTimerEnable :1; + uint8_t :1; // Typically 0b0 + // Byte 9 + uint8_t Checksum :8; + }; +}; + +// Constants + +// Classes +/// Class for handling detailed Voltas A/C messages. +class IRVoltas { + public: + explicit IRVoltas(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); + void stateReset(); +#if SEND_VOLTAS + 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(); } +#endif // SEND_VOLTAS + void begin(); + static bool validChecksum(const uint8_t state[], + const uint16_t length = kVoltasStateLength); + void setPower(const bool on); + bool getPower(void) const; + void on(void); + void off(void); + void setTemp(const uint8_t temp); + uint8_t getTemp(void); + void setFan(const uint8_t speed); + uint8_t getFan(void); + void setMode(const uint8_t mode); + uint8_t getMode(void); + void setEcono(const bool on); + bool getEcono(void); + void setOffTimer(const uint16_t nr_of_mins); + uint16_t getOffTimer(void); + uint8_t* getRaw(void); + void setRaw(const uint8_t new_code[]); + uint8_t convertMode(const stdAc::opmode_t mode); + uint8_t convertFan(const stdAc::fanspeed_t speed); + static stdAc::opmode_t toCommonMode(const uint8_t mode); + static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed); + stdAc::state_t toCommon(void); + String toString(void); +#ifndef UNIT_TEST + + private: + IRsend _irsend; ///< Instance of the IR send class +#else + /// @cond IGNORE + IRsendTest _irsend; ///< Instance of the testing IR send class + /// @endcond +#endif + VoltasProtocol _; ///< The state of the IR remote. + void checksum(void); + static uint8_t calcChecksum(const uint8_t state[], + const uint16_t length = kVoltasStateLength); +}; +#endif // IR_VOLTAS_H_ diff --git a/test/ir_Voltas_test.cpp b/test/ir_Voltas_test.cpp index 8931fcab3..bbec7e26c 100644 --- a/test/ir_Voltas_test.cpp +++ b/test/ir_Voltas_test.cpp @@ -1,5 +1,6 @@ // Copyright 2020 crankyoldgit +#include "ir_Voltas.h" #include "IRac.h" #include "IRrecv.h" #include "IRrecv_test.h" @@ -38,6 +39,11 @@ TEST(TestDecodeVoltas, RealExample) { ASSERT_EQ(decode_type_t::VOLTAS, irsend.capture.decode_type); ASSERT_EQ(kVoltasBits, irsend.capture.bits); EXPECT_STATE_EQ(expected, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ( + "Power: On, Turbo: Off, WiFi: On, Light: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t r, p; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); } TEST(TestDecodeVoltas, SyntheticExample) { @@ -65,3 +71,41 @@ TEST(TestUtils, Housekeeping) { ASSERT_EQ(kVoltasBits, IRsend::defaultBits(decode_type_t::VOLTAS)); ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::VOLTAS)); } + +TEST(TestIRVoltasClass, Checksums) { + const uint8_t valid[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0xE6}; + EXPECT_TRUE(IRVoltas::validChecksum(valid)); + EXPECT_FALSE(IRVoltas::validChecksum(valid, kVoltasStateLength - 1)); + EXPECT_EQ(0xE6, IRVoltas::calcChecksum(valid)); +} + +TEST(TestIRVoltasClass, SetandGetRaw) { + const uint8_t valid[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0xE6}; + const uint8_t badchecksum[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0xE6}; + IRVoltas ac(kGpioUnused); + + ac.setRaw(valid); + EXPECT_STATE_EQ(valid, ac.getRaw(), kVoltasBits); + ac.setRaw(badchecksum); + EXPECT_STATE_EQ(valid, ac.getRaw(), kVoltasBits); +} + +TEST(TestIRVoltasClass, Power) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.on(); + EXPECT_TRUE(ac.getPower()); + + ac.off(); + EXPECT_FALSE(ac.getPower()); + + ac.setPower(true); + EXPECT_TRUE(ac.getPower()); + + ac.setPower(false); + EXPECT_FALSE(ac.getPower()); +}