From 1c10d377dd278ced6c44b84c29584273467d9d84 Mon Sep 17 00:00:00 2001 From: quangthai Date: Sat, 29 Jun 2024 12:44:45 +0700 Subject: [PATCH] supported FUNIKI remote AC --- src/IRac.cpp | 79 ++++++ src/IRac.h | 11 +- src/IRrecv.cpp | 4 + src/IRrecv.h | 5 + src/IRremoteESP8266.h | 16 +- src/IRsend.cpp | 7 + src/IRsend.h | 12 +- src/IRtext.cpp | 2 + src/IRutils.cpp | 1 + src/ir_Funiki.cpp | 617 ++++++++++++++++++++++++++++++++++++++++++ src/ir_Funiki.h | 203 ++++++++++++++ src/locale/defaults.h | 4 +- 12 files changed, 955 insertions(+), 6 deletions(-) create mode 100644 src/ir_Funiki.cpp create mode 100644 src/ir_Funiki.h diff --git a/src/IRac.cpp b/src/IRac.cpp index 602e0018d..bb33a4a33 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -373,6 +373,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #endif #if SEND_WHIRLPOOL_AC case decode_type_t::WHIRLPOOL_AC: +#endif +#if SEND_FUNIKI + case decode_type_t::FUNIKI: #endif return true; default: @@ -1274,7 +1277,56 @@ void IRac::fujitsu(IRFujitsuAC *ac, const fujitsu_ac_remote_model_t model, ac->send(); } #endif // SEND_FUJITSU_AC +#if SEND_FUNIKI +/// Send a Funiki A/C message with the supplied settings. +/// @param[in, out] ac A Ptr to an IRFujitsuAC object to use. +/// @param[in] model The A/C model to use. +/// @param[in] on The power setting. +/// @param[in] mode The operation mode setting. +/// @param[in] celsius Temperature units. True is Celsius, False is Fahrenheit. +/// @param[in] degrees The temperature setting in degrees. +/// @param[in] fan The speed setting for the fan. +/// @param[in] swingv The vertical swing setting. +/// @param[in] swingh The horizontal swing setting. +/// @param[in] clock The clock setting. +/// @param[in] quiet Run the device in quiet/silent mode. +/// @param[in] turbo Run the device in turbo/powerful mode. +/// @param[in] econo Run the device in economical mode. +/// @param[in] filter Turn on the (ion/pollen/etc) filter mode. +/// @param[in] clean Turn on the self-cleaning mode. e.g. Mould, dry filters etc +/// @param[in] sleep Nr. of minutes for sleep mode. <= 0 is Off, > 0 is on. +void IRac::funiki(IRFunikiAC *ac, const funiki_ac_remote_model_t model, + const bool on, const stdAc::opmode_t mode, const bool celsius, + const float degrees, const stdAc::fanspeed_t fan, + const stdAc::swingv_t swingv, const stdAc::swingh_t swingh, + const int16_t clock, + const bool iFeel, const bool turbo, const bool econo, + const bool light, const bool clean, const int16_t sleep) { + ac->begin(); + ac->setModel(model); + ac->setPower(on); + ac->setMode(ac->convertMode(mode)); + ac->setTemp(degrees, !celsius); + ac->setFan(ac->convertFan(fan)); + ac->setSwingVertical(swingv == stdAc::swingv_t::kAuto, // Set auto flag. + ac->convertSwingV(swingv)); + ac->setSleep(sleep >= 0); // Sleep on this A/C is either on or off. + // No Econo setting available. + (void)(swingh); + (void)(iFeel); + (void)(turbo); + (void)(econo); + (void)(light); + (void)(clean); + // No Filter setting available. + // No Beep setting available. + // No Quiet setting available. + // No Clock setting available. + if (clock >= 0) ac->setClock(clock); + ac->send(); +} +#endif // SEND_FUNIKI #if SEND_GOODWEATHER /// Send a Goodweather A/C message with the supplied settings. /// @param[in, out] ac A Ptr to an IRGoodweatherAc object to use. @@ -3244,6 +3296,18 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { break; } #endif // SEND_FUJITSU_AC +#if SEND_FUNIKI + case FUNIKI: + { + IRFunikiAC ac(_pin, (funiki_ac_remote_model_t)send.model, _inverted, + _modulation); + funiki(&ac, (funiki_ac_remote_model_t)send.model, send.power, send.mode, + send.celsius, send.degrees, send.fanspeed, send.swingv, send.swingh, + send.clock, + send.turbo, send.econo, send.light, send.clean, send.sleep); + break; + } +#endif // SEND_FUNIKI #if SEND_GOODWEATHER case GOODWEATHER: { @@ -4215,6 +4279,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_FUJITSU_AC +#if DECODE_FUNIKI + case decode_type_t::FUNIKI: { + IRFunikiAC ac(kGpioUnused); + ac.setRaw(result->state); + return ac.toString(); + } +#endif // DECODE_FUNIKI #if DECODE_GOODWEATHER case decode_type_t::GOODWEATHER: { IRGoodweatherAc ac(kGpioUnused); @@ -4716,6 +4787,14 @@ namespace IRAcUtils { break; } #endif // DECODE_FUJITSU_AC +#if DECODE_FUNIKI + case decode_type_t::FUNIKI: { + IRFunikiAC ac(kGpioUnused); + ac.setRaw(decode->state); + *result = ac.toCommon(); + break; + } +#endif // DECODE_FUNIKI #if DECODE_GOODWEATHER case decode_type_t::GOODWEATHER: { IRGoodweatherAc ac(kGpioUnused); diff --git a/src/IRac.h b/src/IRac.h index e3c261d0d..2f2c45ab1 100644 --- a/src/IRac.h +++ b/src/IRac.h @@ -50,7 +50,7 @@ #include "ir_Voltas.h" #include "ir_Whirlpool.h" #include "ir_York.h" - +#include "ir_Funiki.h" // Constants const int8_t kGpioUnused = -1; ///< A placeholder for not using an actual GPIO. @@ -276,6 +276,15 @@ void electra(IRElectraAc *ac, const bool quiet, const bool turbo, const bool econo, const bool filter, const bool clean, const int16_t sleep = -1); #endif // SEND_FUJITSU_AC +#if SEND_FUNIKI + void funiki(IRFunikiAC *ac, const funiki_ac_remote_model_t model, + const bool on, const stdAc::opmode_t mode, const bool celsius, + const float degrees, const stdAc::fanspeed_t fan, + const stdAc::swingv_t swingv, const stdAc::swingh_t swingh, + const int16_t clock, + const bool iFeel, const bool turbo, const bool econo, + const bool light, const bool clean, const int16_t sleep = -1); +#endif // SEND_FUNIKI #if SEND_GOODWEATHER void goodweather(IRGoodweatherAc *ac, const bool on, const stdAc::opmode_t mode, diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index 173526104..63bcbbc18 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -1185,6 +1185,10 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting York decode"); if (decodeYork(results, offset, kYorkBits)) return true; #endif // DECODE_YORK +#if DECODE_FUNIKI + DPRINTLN("Attempting Funiki decode"); + if (decodeFuniki(results, offset)) return true; +#endif // Typically new protocols are added above this line. } #if DECODE_HASH diff --git a/src/IRrecv.h b/src/IRrecv.h index 7adf5eb1b..19b2db198 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -883,6 +883,11 @@ class IRrecv { const uint16_t kYorkBits, const bool strict = true); #endif // DECODE_YORK +#if DECODE_FUNIKI + bool decodeFuniki(decode_results *results, uint16_t offset = kStartOffset, + const uint16_t nbits = kFunikiBits, + const bool strict = true); +#endif // DECODE_FUNIKI }; #endif // IRRECV_H_ diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index 949de1ecf..48ffa5454 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -952,6 +952,13 @@ #define SEND_YORK _IR_ENABLE_DEFAULT_ #endif // SEND_YORK +#ifndef DECODE_FUNIKI +#define DECODE_FUNIKI _IR_ENABLE_DEFAULT_ +#endif // DECODE_FUNIKI +#ifndef SEND_FUNIKI +#define SEND_FUNIKI _IR_ENABLE_DEFAULT_ +#endif // SEND_FUNIKI + #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 || \ @@ -970,7 +977,7 @@ DECODE_KELON168 || DECODE_HITACHI_AC296 || DECODE_CARRIER_AC128 || \ DECODE_DAIKIN200 || DECODE_HAIER_AC160 || DECODE_TCL96AC || \ DECODE_BOSCH144 || DECODE_SANYO_AC152 || DECODE_DAIKIN312 || \ - DECODE_CARRIER_AC84 || DECODE_YORK || \ + DECODE_CARRIER_AC84 || DECODE_YORK || DECODE_FUNIKI ||\ 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 @@ -1137,8 +1144,9 @@ enum decode_type_t { WOWWEE, CARRIER_AC84, // 125 YORK, + FUNIKI, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = YORK, + kLastDecodeType = FUNIKI, }; // Message lengths & required repeat values @@ -1435,7 +1443,9 @@ const uint16_t kRhossDefaultRepeat = 0; const uint16_t kClimaButlerBits = 52; const uint16_t kYorkBits = 136; const uint16_t kYorkStateLength = 17; - +const uint16_t kFunikiStateLength = 10; +const uint16_t kFunikiBits = kFunikiStateLength * 8; +const uint16_t kFunikiDefaultRepeat = kNoRepeat; // Legacy defines. (Deprecated) #define AIWA_RC_T501_BITS kAiwaRcT501Bits diff --git a/src/IRsend.cpp b/src/IRsend.cpp index 10e440b32..1a0e252fe 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -798,6 +798,8 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { return kXmpBits; case YORK: return kYorkBits; + case FUNIKI: + return kFunikiBits; // No default amount of bits. case FUJITSU_AC: case MWM: @@ -1434,6 +1436,11 @@ bool IRsend::send(const decode_type_t type, const uint8_t *state, sendYork(state, nbytes); break; #endif // SEND_YORK +#if SEND_FUNIKI + case FUNIKI: + sendFuniki(state, nbytes); + break; +#endif // SEND_FUNIKI default: return false; } diff --git a/src/IRsend.h b/src/IRsend.h index 38491372a..7abb54e4d 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -150,7 +150,10 @@ enum fujitsu_ac_remote_model_t { ARRY4, ///< (5) AR-RY4 (Same as AR-RAH2E but with clean & filter) ARREW4E, ///< (6) Similar to ARRAH2E, but with different temp config. }; - +/// Funiki A/C model numbers +enum funiki_ac_remote_model_t { + UNKOWN = 1, // (1)(Default) +}; /// Gree A/C model numbers enum gree_ac_remote_model_t { YAW1F = 1, // (1) Ultimate, EKOKAI, RusClimate (Default) @@ -885,6 +888,13 @@ class IRsend { const uint16_t nbytes = kYorkStateLength, const uint16_t repeat = kNoRepeat); #endif // SEND_YORK +#if SEND_FUNIKI + // void sendFuniki(const uint64_t data, const uint16_t nbits = kFunikiBits, + // const uint16_t repeat = kFunikiDefaultRepeat); + void sendFuniki(const uint8_t data[], + const uint16_t nbytes = kFunikiStateLength, + const uint16_t repeat = kFunikiDefaultRepeat); +#endif // SEND_FUNIKI protected: #ifdef UNIT_TEST diff --git a/src/IRtext.cpp b/src/IRtext.cpp index 9cb39b772..6474e12df 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -555,6 +555,8 @@ IRTEXT_CONST_BLOB_DECL(kAllProtocolNamesStr) { D_STR_CARRIER_AC84, D_STR_UNSUPPORTED) "\x0" COND(DECODE_YORK || SEND_YORK, D_STR_YORK, D_STR_UNSUPPORTED) "\x0" + COND(DECODE_FUNIKI || SEND_FUNIKI, + D_STR_FUNIKI, D_STR_UNSUPPORTED) "\x0" ///< New protocol (macro) 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 e9af0b28f..5e6c5faa6 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -184,6 +184,7 @@ bool hasACState(const decode_type_t protocol) { case DAIKIN312: case ELECTRA_AC: case FUJITSU_AC: + case FUNIKI: case GREE: case HAIER_AC: case HAIER_AC_YRW02: diff --git a/src/ir_Funiki.cpp b/src/ir_Funiki.cpp new file mode 100644 index 000000000..7e36a7dae --- /dev/null +++ b/src/ir_Funiki.cpp @@ -0,0 +1,617 @@ +// Copyright 2024 QuangThai2297 + +/// @file +/// @brief Support for FUNIKI A/C protocols. +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/2112 + + +#include "ir_Funiki.h" +#include +#include +#ifndef ARDUINO +#include +#endif +#include "IRrecv.h" +#include "IRremoteESP8266.h" +#include "IRsend.h" +#include "IRtext.h" +#include "IRutils.h" +#include "ir_Kelvinator.h" + +// Constants +const uint16_t kFunikiHdrMark = 9000; +///< See #684 & real example in unit tests +const uint16_t kFunikiHdrSpace = 4500; +const uint16_t kFunikiBitMark = 620; +const uint16_t kFunikiOneSpace = 1600; +const uint16_t kFunikiZeroSpace = 540; +///< See #1508, #386, & Kelvinator +const uint16_t kFunikiMsgSpace = 19980; +const uint8_t kFunikiBlockFooter = 0b101; +const uint8_t kFunikiBlockFooterBits = 3; + +using irutils::addBoolToString; +using irutils::addIntToString; +using irutils::addLabeledString; +using irutils::addModeToString; +using irutils::addModelToString; +using irutils::addFanToString; +using irutils::addSwingHToString; +using irutils::addTempToString; +using irutils::minsToString; +using irutils::bcdToUint8; +using irutils::uint8ToBcd; + +#if SEND_FUNIKI +/// Send a Funiki Heat Pump formatted message. +/// Status: STABLE / 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::sendFuniki(const uint8_t data[], const uint16_t nbytes, + const uint16_t repeat) { + if (nbytes < kFunikiStateLength) + return; // Not enough bytes to send a proper message. + + for (uint16_t r = 0; r <= repeat; r++) { + // Block #1 + sendGeneric(kFunikiHdrMark, kFunikiHdrSpace, kFunikiBitMark, + kFunikiOneSpace, + kFunikiBitMark, kFunikiZeroSpace, 0, 0, // No Footer. + data, nbytes, 38, false, 0, 50); + // Footer #1 + sendGeneric(0, 0, // No Header + kFunikiBitMark, kFunikiOneSpace, kFunikiBitMark, + kFunikiZeroSpace, + kFunikiBitMark, kFunikiMsgSpace, 0b101, 3, 38, false, 0, 50); + } +} + +/// Send a Funiki Heat Pump formatted message. +/// Status: STABLE / Working. +/// @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. +// void IRsend::sendFuniki(const uint64_t data, const uint16_t nbits, +// const uint16_t repeat) { +// if (nbits != kFunikiBits) +// return; // Wrong nr. of bits to send a proper message. +// // Set IR carrier frequency +// enableIROut(38); + +// for (uint16_t r = 0; r <= repeat; r++) { +// // Header +// mark(kFunikiHdrMark); +// space(kFunikiHdrSpace); + +// // Data +// for (int16_t i = 8; i <= nbits; i += 8) { +// sendData(kFunikiBitMark, kFunikiOneSpace, kFunikiBitMark, +// kFunikiZeroSpace, +// (data >> (nbits - i)) & 0xFF, 8, false); +// if (i == nbits / 2) { +// // Send the mid-message Footer. +// sendData(kFunikiBitMark, kFunikiOneSpace, kFunikiBitMark, +// kFunikiZeroSpace, +// 0b010, 3); +// mark(kFunikiBitMark); +// space(kFunikiMsgSpace); +// } +// } +// // Footer +// mark(kFunikiBitMark); +// space(kFunikiMsgSpace); +// } +// } +#endif // SEND_FUNIKI + +/// Class constructor +/// @param[in] pin GPIO to be used when sending. +/// @param[in] model The enum of the model to be emulated. +/// @param[in] inverted Is the output signal to be inverted? +/// @param[in] use_modulation Is frequency modulation to be used? +IRFunikiAC::IRFunikiAC(const uint16_t pin, const funiki_ac_remote_model_t model, + const bool inverted, const bool use_modulation) + : _irsend(pin, inverted, use_modulation) { + stateReset(); + setModel(model); +} + +/// Reset the internal state to a fixed known good state. +void IRFunikiAC::stateReset(void) { + // This resets to a known-good state to Power Off, Fan Auto, Mode Auto, 25C. + std::memset(_.remote_state, 0, sizeof _.remote_state); + _.Temp = 9; // _.remote_state[1] = 0x09; + _.unknown1 = 0x60; // _.remote_state[5] = 0x20; +} + +/// Fix up the internal state so it is correct. +/// @note Internal use only. +void IRFunikiAC::fixup(void) { + setPower(getPower()); // Redo the power bits as they differ between models. + checksum(); // Calculate the checksums +} + +/// Set up hardware to be able to send a message. +void IRFunikiAC::begin(void) { _irsend.begin(); } + +#if SEND_FUNIKI +/// Send the current internal state as an IR message. +/// @param[in] repeat Nr. of times the message will be repeated. +void IRFunikiAC::send(const uint16_t repeat) { + _irsend.sendFuniki(getRaw(), kFunikiStateLength, repeat); +} +#endif // SEND_FUNIKI + +/// 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* IRFunikiAC::getRaw(void) { + fixup(); // Ensure correct settings before sending. + return _.remote_state; +} + +/// Set the internal state from a valid code for this protocol. +/// @param[in] new_code A valid code for this protocol. +void IRFunikiAC::setRaw(const uint8_t new_code[]) { + std::memcpy(_.remote_state, new_code, kFunikiStateLength); + // We can only detect the difference between models when the power is on. + if (_.Power) { + _model = funiki_ac_remote_model_t::UNKOWN; + } +} + +/// Calculate and set the checksum values for the internal state. +/// @param[in] length The size/length of the state array to fix the checksum of. +void IRFunikiAC::checksum(const uint16_t length) { + (void)(length); + // Funiki uses the same checksum alg. as Kelvinator's block checksum. + // _.Sum = IRKelvinatorAC::calcBlockChecksum(_.remote_state, length); +} + +/// 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 IRFunikiAC::validChecksum(const uint8_t state[], const uint16_t length) { + // Top 4 bits of the last byte in the state is the state's checksum. + return GETBITS8(state[length - 1], kHighNibble, kNibbleSize) == + IRKelvinatorAC::calcBlockChecksum(state, length); +} + +/// Set the model of the A/C to emulate. +/// @param[in] model The enum of the appropriate model. +void IRFunikiAC::setModel(const funiki_ac_remote_model_t model) { + switch (model) { + case funiki_ac_remote_model_t::UNKOWN: + default: _model = funiki_ac_remote_model_t::UNKOWN; + } +} + +/// Get/Detect the model of the A/C. +/// @return The enum of the compatible model. +funiki_ac_remote_model_t IRFunikiAC::getModel(void) const { return _model; } + +/// Change the power setting to On. +void IRFunikiAC::on(void) { setPower(true); } + +/// Change the power setting to Off. +void IRFunikiAC::off(void) { setPower(false); } + +/// Change the power setting. +/// @param[in] on true, the setting is on. false, the setting is off. +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/814 +void IRFunikiAC::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. +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/814 +bool IRFunikiAC::getPower(void) const { + // See #814. Not checking/requiring: (_.ModelA) + return _.Power; +} + + +/// Set the temp. in degrees +/// @param[in] temp Desired temperature in Degrees. +/// @param[in] fahrenheit Use units of Fahrenheit and set that as units used. +/// false is Celsius (Default), true is Fahrenheit. +/// @note The unit actually works in Celsius with a special optional +/// "extra degree" when sending Fahrenheit. +void IRFunikiAC::setTemp(const uint8_t temp, const bool fahrenheit) { + float safecelsius = temp; + if (fahrenheit) + // Covert to F, and add a fudge factor to round to the expected degree. + // Why 0.6 you ask?! Because it works. Ya'd thing 0.5 would be good for + // rounding, but Noooooo! + safecelsius = fahrenheitToCelsius(temp + 0.6); + + // Make sure we have desired temp in the correct range. + safecelsius = std::max(static_cast(kFunikiMinTempC), safecelsius); + safecelsius = std::min(static_cast(kFunikiMaxTempC), safecelsius); + // An operating mode of Auto locks the temp to a specific value. Do so. + // if (_.Mode == kFunikiAuto) safecelsius = 25; + if (_.Mode != kFunikiCool || _.AutoMode == kFunikiAutoModeOn) + safecelsius = kFunikiMinTempC; + // Set the "main" Celsius degrees. + _.Temp = safecelsius - kFunikiMinTempC; + // Deal with the extra degree fahrenheit difference. + // _.TempExtraDegreeF = (static_cast(safecelsius * 2) & 1); +} + +/// Get the set temperature +/// @return The temperature in degrees in the current units (C/F) set. +uint8_t IRFunikiAC::getTemp(void) const { + uint8_t deg = kFunikiMinTempC + _.Temp; + return deg; +} + +/// Set the speed of the fan. +/// @param[in] speed The desired setting. 0 is auto, 1-3 is the speed. +void IRFunikiAC::setFan(const uint8_t speed) { + uint8_t fan = std::min(kFunikiFanMax, speed); // Bounds check + if (_.Mode == kFunikiDry) fan = 1; // DRY mode is always locked to fan 1. + // Set the basic fan values. + _.Fan = fan; +} + +/// Get the current fan speed setting. +/// @return The current fan speed. +uint8_t IRFunikiAC::getFan(void) const { return _.Fan; } + +/// Set the operating mode of the A/C. +/// @param[in] new_mode The desired operating mode. +void IRFunikiAC::setMode(const uint8_t new_mode) { + uint8_t mode = new_mode; + switch (mode) { + // AUTO is locked to 25C + case kFunikiAuto: setTemp(25); break; + // DRY always sets the fan to 1. + case kFunikiDry: setFan(1); break; + case kFunikiCool: + case kFunikiFan: break; + // If we get an unexpected mode, default to AUTO. + default: mode = kFunikiAuto; + } + if (mode == kFunikiAuto) { + _.Mode = kFunikiCool; + _.AutoMode = kFunikiAutoModeOn; + } else { + _.Mode = mode; + _.AutoMode = kFunikiAutoModeOff; + } +} + +/// Get the operating mode setting of the A/C. +/// @return The current operating mode setting. +uint8_t IRFunikiAC::getMode(void) const { return _.Mode; } + +/// Set the Sleep setting of the A/C. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRFunikiAC::setSleep(const bool on) { _.Sleep = on; } + +/// Get the Sleep setting of the A/C. +/// @return true, the setting is on. false, the setting is off. +bool IRFunikiAC::getSleep(void) const { return _.Sleep; } + + +/// Set the Vertical Swing mode of the A/C. +/// @param[in] automatic Do we use the automatic setting? +/// @param[in] position The position/mode to set the vanes to. +void IRFunikiAC::setSwingVertical(const bool automatic, + const uint8_t position) { + uint8_t new_position = position; + if (!automatic) { + switch (position) { + case kFunikiSwingUp: + case kFunikiSwingMiddleUp: + case kFunikiSwingMiddle: + case kFunikiSwingMiddleDown: + case kFunikiSwingDown: + break; + default: + new_position = kFunikiSwingLastPos; + } + } else { + new_position = kFunikiSwingAuto; + } + _.SwingV = new_position; +} + +/// Get the Vertical Swing Automatic mode setting of the A/C. +/// @return true, the setting is on. false, the setting is off. +bool IRFunikiAC::getSwingVerticalAuto(void) const { + return (_.SwingV == kFunikiSwingAuto); +} + +/// Get the Vertical Swing position setting of the A/C. +/// @return The native position/mode. +uint8_t IRFunikiAC::getSwingVerticalPosition(void) const { return _.SwingV; } + + + +/// Get the clock time of the A/C unit. +/// @return Nr. of seconds past midnight. +int16_t IRFunikiAC::getClock(void) const { + return (bcdToUint8((_.Hours1 << kNibbleSize) | _.Hours2) * 60) + + bcdToUint8((_.Minutes1 << kNibbleSize) | _.Minutes2); +} + + +void IRFunikiAC::setClock(const int16_t nr_of_minutes) { + uint32_t remaining = nr_of_minutes; + _.Minutes1 = uint8ToBcd(remaining % 60)>>4; + _.Minutes2 = uint8ToBcd(remaining % 60) & 0x0F; + remaining /= 60; + _.Hours1 = uint8ToBcd(remaining % 60)>>4; + _.Hours2 = uint8ToBcd(remaining % 60) & 0x0F; +} + + +// /// Set the timer enable setting of the A/C. +// /// @param[in] on true, the setting is on. false, the setting is off. +// void IRFunikiAC::setTimerEnabled(const bool on) { _.TimerEnabled = on; } + +// /// Get the timer enabled setting of the A/C. +// /// @return true, the setting is on. false, the setting is off. +bool IRFunikiAC::getTimerOnEnabled(void) const { return _.TimerOnEnable; } +bool IRFunikiAC::getTimerOffEnabled(void) const { return _.TimerOffEnable; } + +// /// Get the timer time value from the A/C. +// /// @return The number of minutes the timer is set for. +uint16_t IRFunikiAC::getTimerOn(void) const { + uint16_t hrs = irutils::bcdToUint8((_.TimerOnHours1 << kNibbleSize) | + _.TimerOnHours2); + return hrs * 60 + (_.TimerOnMinutes * 10); +} +// /// Get the timer time value from the A/C. +// /// @return The number of minutes the timer is set for. +uint16_t IRFunikiAC::getTimerOff(void) const { + uint16_t hrs = irutils::bcdToUint8((_.TimerOffHours1 << kNibbleSize) | + _.TimerOffHours2); + return hrs * 60 + (_.TimerOffMinutes * 10); +} + +// /// Set the A/C's timer to turn off in X many minutes. +// /// @param[in] minutes The number of minutes the timer should be set for. +// /// @note Stores time internally in 30 min units. +// /// e.g. 5 mins means 0 (& Off), 95 mins is 90 mins (& On). Max is 24 hours. +// void IRFunikiAC::setTimer(const uint16_t minutes) { +// uint16_t mins = std::min(kFunikiTimerMax, minutes); // Bounds check. +// setTimerEnabled(mins >= 30); // Timer is enabled when >= 30 mins. +// uint8_t hours = mins / 60; +// // Set the half hour bit. +// _.TimerHalfHr = (mins % 60) >= 30; +// // Set the "tens" digit of hours. +// _.TimerTensHr = hours / 10; +// // Set the "units" digit of hours. +// _.TimerHours = hours % 10; +// } + + +/// Convert a stdAc::opmode_t enum into its native mode. +/// @param[in] mode The enum to be converted. +/// @return The native equivalent of the enum. +uint8_t IRFunikiAC::convertMode(const stdAc::opmode_t mode) { + switch (mode) { + case stdAc::opmode_t::kCool: return kFunikiCool; + case stdAc::opmode_t::kDry: return kFunikiDry; + case stdAc::opmode_t::kFan: return kFunikiFan; + default: return kFunikiAuto; + } +} + +/// Convert a stdAc::fanspeed_t enum into it's native speed. +/// @param[in] speed The enum to be converted. +/// @return The native equivalent of the enum. +uint8_t IRFunikiAC::convertFan(const stdAc::fanspeed_t speed) { + switch (speed) { + case stdAc::fanspeed_t::kMin: return kFunikiFanMin; + case stdAc::fanspeed_t::kLow: + case stdAc::fanspeed_t::kMedium: return kFunikiFanMax - 1; + case stdAc::fanspeed_t::kHigh: + case stdAc::fanspeed_t::kMax: return kFunikiFanMax; + default: return kFunikiFanAuto; + } +} + +/// Convert a stdAc::swingv_t enum into it's native setting. +/// @param[in] swingv The enum to be converted. +/// @return The native equivalent of the enum. +uint8_t IRFunikiAC::convertSwingV(const stdAc::swingv_t swingv) { + switch (swingv) { + case stdAc::swingv_t::kHighest: return kFunikiSwingUp; + case stdAc::swingv_t::kHigh: return kFunikiSwingMiddleUp; + case stdAc::swingv_t::kMiddle: return kFunikiSwingMiddle; + case stdAc::swingv_t::kLow: return kFunikiSwingMiddleDown; + case stdAc::swingv_t::kLowest: return kFunikiSwingDown; + default: return kFunikiSwingAuto; + } +} + + +/// Convert a native mode into its stdAc equivalent. +/// @param[in] mode The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +stdAc::opmode_t IRFunikiAC::toCommonMode(const uint8_t mode, + uint8_t isAutoMode) { + if (isAutoMode == kFunikiAutoModeOn) { + return stdAc::opmode_t::kAuto; + } + switch (mode) { + case kFunikiCool: return stdAc::opmode_t::kCool; + // case kFunikiHeat: return stdAc::opmode_t::kHeat; + case kFunikiDry: return stdAc::opmode_t::kDry; + case kFunikiFan: return stdAc::opmode_t::kFan; + default: return stdAc::opmode_t::kAuto; + } +} + +/// Convert a native fan speed into its stdAc equivalent. +/// @param[in] speed The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +stdAc::fanspeed_t IRFunikiAC::toCommonFanSpeed(const uint8_t speed) { + switch (speed) { + case kFunikiFanMax: return stdAc::fanspeed_t::kMax; + case kFunikiFanMax - 1: return stdAc::fanspeed_t::kMedium; + case kFunikiFanMin: return stdAc::fanspeed_t::kMin; + default: return stdAc::fanspeed_t::kAuto; + } +} + +/// Convert a native Vertical Swing into its stdAc equivalent. +/// @param[in] pos The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +stdAc::swingv_t IRFunikiAC::toCommonSwingV(const uint8_t pos) { + switch (pos) { + case kFunikiSwingUp: return stdAc::swingv_t::kHighest; + case kFunikiSwingMiddleUp: return stdAc::swingv_t::kHigh; + case kFunikiSwingMiddle: return stdAc::swingv_t::kMiddle; + case kFunikiSwingMiddleDown: return stdAc::swingv_t::kLow; + case kFunikiSwingDown: return stdAc::swingv_t::kLowest; + default: return stdAc::swingv_t::kAuto; + } +} + + + +/// Convert the current internal state into its stdAc::state_t equivalent. +/// @return The stdAc equivalent of the native settings. +stdAc::state_t IRFunikiAC::toCommon(void) { + stdAc::state_t result{}; + result.protocol = decode_type_t::FUNIKI; + result.model = _model; + result.power = _.Power; + result.mode = toCommonMode(_.Mode, _.AutoMode); + result.degrees = getTemp(); + // no support for Sensor temp. + result.fanspeed = toCommonFanSpeed(_.Fan); + if (_.SwingV == kFunikiSwingAuto) + result.swingv = stdAc::swingv_t::kAuto; + else + result.swingv = toCommonSwingV(_.SwingV); + + result.sleep = _.Sleep ? 0 : -1; + // Not supported. + // result.quiet = false; + // result.filter = false; + // result.beep = false; + result.clock = getClock(); + return result; +} + +/// Convert the current internal state into a human readable string. +/// @return A human readable string. +String IRFunikiAC::toString(void) { + String result = ""; + result.reserve(220); // Reserve some heap for the string to reduce fragging. + result += addModelToString(decode_type_t::FUNIKI, _model, false); + result += addBoolToString(_.Power, kPowerStr); + if (_.AutoMode == kFunikiAutoModeOn) { + result += addModeToString(kFunikiAuto, kFunikiAuto, kFunikiCool, + kFunikiHeat, kFunikiDry, kFunikiFan); + } else { + result += addModeToString(_.Mode, kFunikiAuto, kFunikiCool, kFunikiHeat, + kFunikiDry, kFunikiFan); + } + result += addTempToString(getTemp(), true); + result += addFanToString(_.Fan, kFunikiFanMax, kFunikiFanMin, kFunikiFanAuto, + kFunikiFanAuto, kFunikiFanMed); + result += addBoolToString(_.Sleep, kSleepStr); + result += addIntToString(_.SwingV, kSwingVStr); + result += kSpaceLBraceStr; + switch (_.SwingV) { + case kFunikiSwingLastPos: + result += kLastStr; + break; + case kFunikiSwingAuto: + result += kAutoStr; + break; + default: result += kUnknownStr; + } + result += ')'; + result += addLabeledString( + getTimerOnEnabled() ? + minsToString(getTimerOn()) : kOffStr, kOnTimerStr); + + result += addLabeledString( + getTimerOffEnabled() ? + minsToString(getTimerOff()) : kOffStr, kOffTimerStr); + + result += addLabeledString(minsToString(getClock()), kClockStr); + return result; +} + +#if DECODE_FUNIKI +/// Decode the supplied Gree HVAC message. +/// Status: STABLE / 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::decodeFuniki(decode_results* results, uint16_t offset, + const uint16_t nbits, bool const strict) { + // for(int i = 0; i < results->rawlen; i++) + // { + // printf("%d, ",results->rawbuf[i]); + // } + if (results->rawlen <= + 2 * (nbits + kFunikiBlockFooterBits) + (kHeader) - 1 + offset) + return false; // Can't possibly be a valid Funiki message. + if (strict && nbits != kFunikiBits) + return false; // Not strictly a Funiki message. + + // printf("\nlen: %d\n", results->rawlen); + // There are two blocks back-to-back in a full Funiki IR message + // sequence. + uint8_t tole = _tolerance + 20; + uint16_t used; + // Header + Data Block #1 (80 bits) + used = matchGeneric(results->rawbuf + offset, results->state, + results->rawlen - offset, nbits, + kFunikiHdrMark, kFunikiHdrSpace, + kFunikiBitMark, kFunikiOneSpace, + kFunikiBitMark, kFunikiZeroSpace, + 0, 0, false, + tole, kMarkExcess, false); + if (used == 0) return false; + offset += used; + // printf("\n ooffset: %d\n", offset); + // Block #1 footer (3 bits, B101) + match_result_t data_result; + data_result = matchData(&(results->rawbuf[offset]), kFunikiBlockFooterBits, + kFunikiBitMark, kFunikiOneSpace, kFunikiBitMark, + kFunikiZeroSpace, tole, kMarkExcess, false); + if (data_result.success == false) return false; + // printf("\n foot: %lld\n", data_result.data); + + if (data_result.data != kFunikiBlockFooter) return false; + offset += data_result.used; + // // Inter-block gap + Data Block #2 (32 bits) + Footer + // if (!matchGeneric(results->rawbuf + offset, results->state + 4, + // results->rawlen - offset, nbits / 2, + // kFunikiBitMark, kFunikiMsgSpace, + // kFunikiBitMark, kFunikiOneSpace, + // kFunikiBitMark, kFunikiZeroSpace, + // kFunikiBitMark, kFunikiMsgSpace, true, + // _tolerance, kMarkExcess, false)) return false; + + // // Compliance + // if (strict) { + // // Verify the message's checksum is correct. + // if (!IRFunikiAC::validChecksum(results->state)) return false; + // } + + // Success + results->decode_type = FUNIKI; + 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_FUNIKI diff --git a/src/ir_Funiki.h b/src/ir_Funiki.h new file mode 100644 index 000000000..82aa80507 --- /dev/null +++ b/src/ir_Funiki.h @@ -0,0 +1,203 @@ +// Copyright 2024 QuangThai2297 + +/// @file +/// @brief Support for FUNIKI A/C protocols. +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/2112 + + + +#ifndef IR_FUNIKI_H_ +#define IR_FUNIKI_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 + +/// Native representation of a Funiki A/C message. +union FunikiProtocol{ + uint8_t remote_state[kFunikiStateLength]; ///< The state in native IR code form + struct { + // BYte 0 + uint8_t Mode :2; + uint8_t Sleep :1; + uint8_t Power :1; + uint8_t Fan :2; + uint8_t TimerOnEnable :1; + uint8_t :1; + // Byte 1 + uint8_t Temp :4; + uint8_t TimerOnHours1 :2; // + uint8_t :2; + // Byte 2 + uint8_t TimerOnHours2 :4; // + uint8_t TimerOnMinutes :3; // + uint8_t :1; + // Byte 3 + uint8_t unknown1; //0x60 + // Byte 4 + uint8_t :4; + uint8_t :1; + uint8_t TimerOffEnable :1; + uint8_t AutoMode :1; + uint8_t :1; + // Byte 5 + uint8_t SwingV :3; + uint8_t :5; + // Byte 6 + uint8_t TimerOffHours1:4; + uint8_t TimerOffHours2:4; + // Byte 7 + uint8_t TimerOffMinutes:3; + uint8_t :5; + // Byte 8 + uint8_t Hours1:4; + uint8_t Hours2:4; + // Byte 9 + uint8_t Minutes1:4; + uint8_t Minutes2:4; + }; +}; + +// Constants + +const uint8_t kFunikiCool = 0; +const uint8_t kFunikiDry = 3; +const uint8_t kFunikiFan = 1; +const uint8_t kFunikiAuto = 2; +const uint8_t kFunikiHeat = 4; + +const uint8_t kFunikiAutoModeOff = 0; +const uint8_t kFunikiAutoModeOn = 1; + +const uint8_t kFunikiFanAuto = 0; +const uint8_t kFunikiFanMin = 1; +const uint8_t kFunikiFanMed = 2; +const uint8_t kFunikiFanMax = 3; + +const uint8_t kFunikiMinTempC = 16; // Celsius +const uint8_t kFunikiMaxTempC = 31; // Celsius +const uint8_t kFunikiMinTempF = 61; // Fahrenheit +const uint8_t kFunikiMaxTempF = 86; // Fahrenheit +const uint16_t kFunikiTimerMax = 24 * 60; + +const uint8_t kFunikiSwingLastPos = 0b000; // 0 +const uint8_t kFunikiSwingAuto = 0b001; // 1 +const uint8_t kFunikiSwingUp = 0b010; // 2 +const uint8_t kFunikiSwingMiddleUp = 0b011; // 3 +const uint8_t kFunikiSwingMiddle = 0b100; // 4 +const uint8_t kFunikiSwingMiddleDown = 0b101; // 5 +const uint8_t kFunikiSwingDown = 0b110; // 6 + +const uint8_t kFunikiSwingHOff = 0b000; // 0 +const uint8_t kFunikiSwingHAuto = 0b001; // 1 +const uint8_t kFunikiSwingHMaxLeft = 0b010; // 2 +const uint8_t kFunikiSwingHLeft = 0b011; // 3 +const uint8_t kFunikiSwingHMiddle = 0b100; // 4 +const uint8_t kFunikiSwingHRight = 0b101; // 5 +const uint8_t kFunikiSwingHMaxRight = 0b110; // 6 + +const uint8_t kFunikiDisplayTempOff = 0b00; // 0 +const uint8_t kFunikiDisplayTempSet = 0b01; // 1 +const uint8_t kFunikiDisplayTempInside = 0b10; // 2 +const uint8_t kFunikiDisplayTempOutside = 0b11; // 3 + +// Legacy defines. +#define FUINIKI_AUTO kFunikiAuto +#define FUINIKI_COOL kFunikiCool +#define FUINIKI_DRY kFunikiDry +#define FUINIKI_FAN kFunikiFan +#define FUINIKI_HEAT kFunikiHeat +#define FUINIKI_MIN_TEMP kFunikiMinTempC +#define FUINIKI_MAX_TEMP kFunikiMaxTempC +#define FUINIKI_FAN_MAX kFunikiFanMax +#define FUINIKI_SWING_LAST_POS kFunikiSwingLastPos +#define FUINIKI_SWING_AUTO kFunikiSwingAuto +#define FUINIKI_SWING_UP kFunikiSwingUp +#define FUINIKI_SWING_MIDDLE_UP kFunikiSwingMiddleUp +#define FUINIKI_SWING_MIDDLE kFunikiSwingMiddle +#define FUINIKI_SWING_MIDDLE_DOWN kFunikiSwingMiddleDown +#define FUINIKI_SWING_DOWN kFunikiSwingDown +#define FUINIKI_SWING_DOWN_AUTO kFunikiSwingDownAuto +#define FUINIKI_SWING_MIDDLE_AUTO kFunikiSwingMiddleAuto +#define FUINIKI_SWING_UP_AUTO kFunikiSwingUpAuto + +// Classes +/// Class for handling detailed Funiki A/C messages. +class IRFunikiAC { + public: + explicit IRFunikiAC( + const uint16_t pin, + const funiki_ac_remote_model_t model = funiki_ac_remote_model_t::UNKOWN, + const bool inverted = false, const bool use_modulation = true); + void stateReset(void); +#if SEND_FUNIKI + void send(const uint16_t repeat = kFunikiDefaultRepeat); + /// 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_FUNIKI + void begin(void); + void on(void); + void off(void); + void setModel(const funiki_ac_remote_model_t model); + funiki_ac_remote_model_t getModel(void) const; + void setPower(const bool on); + bool getPower(void) const; + void setTemp(const uint8_t temp, const bool fahrenheit = false); + uint8_t getTemp(void) const; + void setFan(const uint8_t speed); + uint8_t getFan(void) const; + void setMode(const uint8_t new_mode); + uint8_t getMode(void) const; + void setSleep(const bool on); + bool getSleep(void) const; + void setSwingVertical(const bool automatic, const uint8_t position); + bool getSwingVerticalAuto(void) const; + uint8_t getSwingVerticalPosition(void) const; + int16_t getClock(void) const; + void setClock(const int16_t nr_of_minutes); + uint16_t getTimerOn(void) const; + uint16_t getTimerOff(void) const; + bool getTimerOnEnabled(void) const; + bool getTimerOffEnabled(void) const; + // void setTimer(const uint16_t minutes); + + static uint8_t convertMode(const stdAc::opmode_t mode); + static uint8_t convertFan(const stdAc::fanspeed_t speed); + static uint8_t convertSwingV(const stdAc::swingv_t swingv); + static stdAc::opmode_t toCommonMode(const uint8_t mode, uint8_t isAutoMode); + static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed); + static stdAc::swingv_t toCommonSwingV(const uint8_t pos); + stdAc::state_t toCommon(void); + uint8_t* getRaw(void); + void setRaw(const uint8_t new_code[]); + static bool validChecksum(const uint8_t state[], + const uint16_t length = kFunikiStateLength); + String toString(void); +#ifndef UNIT_TEST + + private: + IRsend _irsend; ///< Instance of the IR send class +#else // UNIT_TEST + /// @cond IGNORE + IRsendTest _irsend; ///< Instance of the testing IR send class + /// @endcond +#endif // UNIT_TEST + FunikiProtocol _; + funiki_ac_remote_model_t _model; + void checksum(const uint16_t length = kFunikiStateLength); + void fixup(void); + // void setTimerEnabled(const bool on); + // bool getTimerEnabled(void) const; +}; + +#endif // IR_FUNIKI_H_ diff --git a/src/locale/defaults.h b/src/locale/defaults.h index 438cc5da3..8cea8ff39 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -1120,7 +1120,9 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_ZEPEAL #define D_STR_ZEPEAL "ZEPEAL" #endif // D_STR_ZEPEAL - +#ifndef D_STR_FUNIKI +#define D_STR_FUNIKI "FUNIKI" +#endif // D_STR_FUNIKI // IRrecvDumpV2+ #ifndef D_STR_TIMESTAMP #define D_STR_TIMESTAMP "Timestamp"