Transcold: Add detailed support. (#1278)

* Manually merge in the work done by @iamDshetty
* Lots of code clean up to get it working properly.
  - Code style
  - Better constants.
  - Lint
  - Bugs
  - Remove dead/unused code & settings.
* Add unit test for basic features.
* Settings/Features supported:
  - Mode
  - Fan speed
  - Power
  - Temp
  - Swing

* Sending confirmed working in: https://github.com/crankyoldgit/IRremoteESP8266/issues/1256#issuecomment-696055698

Fixes #1256
This commit is contained in:
David Conran
2020-09-25 09:04:35 +10:00
committed by GitHub
parent efe4bf6dd8
commit 5e1731af81
9 changed files with 916 additions and 56 deletions

View File

@@ -41,6 +41,7 @@
#include "ir_Technibel.h"
#include "ir_Teco.h"
#include "ir_Toshiba.h"
#include "ir_Transcold.h"
#include "ir_Trotec.h"
#include "ir_Vestel.h"
#include "ir_Voltas.h"
@@ -261,6 +262,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) {
#if SEND_TOSHIBA_AC
case decode_type_t::TOSHIBA_AC:
#endif
#if SEND_TRANSCOLD
case decode_type_t::TRANSCOLD:
#endif
#if SEND_TROTEC
case decode_type_t::TROTEC:
#endif
@@ -2013,6 +2017,48 @@ void IRac::whirlpool(IRWhirlpoolAc *ac, const whirlpool_ac_remote_model_t model,
}
#endif // SEND_WHIRLPOOL_AC
#if SEND_TRANSCOLD
/// Send a Transcold A/C message with the supplied settings.
/// @note May result in multiple messages being sent.
/// @param[in, out] ac A Ptr to an IRTranscoldAc object to use.
/// @param[in] on The power setting.
/// @param[in] mode The operation mode setting.
/// @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.
/// @note -1 is Off, >= 0 is on.
void IRac::transcold(IRTranscoldAc *ac,
const bool on, const stdAc::opmode_t mode,
const float degrees, const stdAc::fanspeed_t fan,
const stdAc::swingv_t swingv,
const stdAc::swingh_t swingh) {
ac->begin();
ac->setPower(on);
if (!on) {
// after turn off AC no more commands should
// be accepted
ac->send();
return;
}
ac->setMode(ac->convertMode(mode));
ac->setTemp(degrees);
ac->setFan(ac->convertFan(fan));
// No Filter setting available.
// No Beep setting available.
// No Clock setting available.
// No Econo setting available.
// No Quiet setting available.
if (swingv != stdAc::swingv_t::kOff || swingh != stdAc::swingh_t::kOff) {
// Swing has a special command that needs to be sent independently.
ac->setSwing();
ac->send();
}
ac->send();
}
#endif // SEND_TRANSCOLD
/// Create a new state base on the provided state that has been suitably fixed.
/// @note This is for use with Home Assistant, which requires mode to be off if
/// the power is off.
@@ -2040,6 +2086,7 @@ stdAc::state_t IRac::handleToggles(const stdAc::state_t desired,
// Check if we have to handle toggle settings for specific A/C protocols.
switch (desired.protocol) {
case decode_type_t::COOLIX:
case decode_type_t::TRANSCOLD:
if ((desired.swingv == stdAc::swingv_t::kOff) ^
(prev->swingv == stdAc::swingv_t::kOff)) // It changed, so toggle.
result.swingv = stdAc::swingv_t::kAuto;
@@ -2569,6 +2616,15 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) {
break;
}
#endif // SEND_WHIRLPOOL_AC
#if SEND_TRANSCOLD
case TRANSCOLD:
{
IRTranscoldAc ac(_pin, _inverted, _modulation);
transcold(&ac, send.power, send.mode, degC, send.fanspeed, send.swingv,
send.swingh);
break;
}
#endif // SEND_TRANSCOLD_AC
default:
return false; // Fail, didn't match anything.
}
@@ -3272,6 +3328,14 @@ namespace IRAcUtils {
return ac.isValidLgAc() ? ac.toString() : "";
}
#endif // DECODE_LG
#if DECODE_TRANSCOLD
case decode_type_t::TRANSCOLD: {
IRTranscoldAc ac(kGpioUnused);
ac.on();
ac.setRaw(result->value); // TRANSCOLD uses value instead of state.
return ac.toString();
}
#endif // DECODE_TRANSCOLD
default:
return "";
}
@@ -3668,6 +3732,14 @@ namespace IRAcUtils {
break;
}
#endif // DECODE_WHIRLPOOL_AC
#if DECODE_TRANSCOLD
case decode_type_t::TRANSCOLD: {
IRTranscoldAc ac(kGpioUnused);
ac.setRaw(decode->value); // TRANSCOLD Uses value instead of state.
*result = ac.toCommon(prev);
break;
}
#endif // DECODE_TRANSCOLD
default:
return false;
}

View File

@@ -35,6 +35,7 @@
#include "ir_Technibel.h"
#include "ir_Teco.h"
#include "ir_Toshiba.h"
#include "ir_Transcold.h"
#include "ir_Trotec.h"
#include "ir_Vestel.h"
#include "ir_Voltas.h"
@@ -429,6 +430,12 @@ void electra(IRElectraAc *ac,
const bool turbo, const bool light,
const int16_t sleep = -1, const int16_t clock = -1);
#endif // SEND_WHIRLPOOL_AC
#if SEND_TRANSCOLD
void transcold(IRTranscoldAc *ac,
const bool on, const stdAc::opmode_t mode, const float degrees,
const stdAc::fanspeed_t fan,
const stdAc::swingv_t swingv, const stdAc::swingh_t swingh);
#endif // SEND_TRANSCOLD
static stdAc::state_t cleanState(const stdAc::state_t state);
static stdAc::state_t handleToggles(const stdAc::state_t desired,
const stdAc::state_t *prev = NULL);

View File

@@ -1050,7 +1050,8 @@ 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 kTranscoldBits = 48;
const uint16_t kTranscoldBits = 24;
const uint16_t kTranscoldDefaultRepeat = kNoRepeat;
const uint16_t kTrotecStateLength = 9;
const uint16_t kTrotecBits = kTrotecStateLength * 8;
const uint16_t kTrotecDefaultRepeat = kNoRepeat;

View File

@@ -628,6 +628,7 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) {
case MIDEA24:
case NIKAI:
case RCMM:
case TRANSCOLD:
return 24;
case LG:
case LG2:
@@ -655,7 +656,6 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) {
return kSanyoLC7461Bits; // 42
case GOODWEATHER:
case MIDEA:
case TRANSCOLD:
case PANASONIC:
return 48;
case MAGIQUEST:

View File

@@ -659,7 +659,7 @@ class IRsend {
#endif // SEND_METZ
#if SEND_TRANSCOLD
void sendTranscold(const uint64_t data, const uint16_t nbits = kTranscoldBits,
const uint16_t repeat = kNoRepeat);
const uint16_t repeat = kTranscoldDefaultRepeat);
#endif // SEND_TRANSCOLD
protected:

View File

@@ -1,93 +1,541 @@
// Copyright 2020 David Conran (crankyoldgit)
// Copyright 2020 Chandrashekar Shetty (iamDshetty)
// Copyright 2020 crankyoldgit
/// @file
/// @brief Support for Transcold protocol.
/// @brief Support for Transcold A/C protocols.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1256
// Supports:
// Brand: Transcold, Model: M1-F-NO-6 A/C
#include "ir_Transcold.h"
#include <algorithm>
#ifndef ARDUINO
#include <string>
#endif
#include "IRrecv.h"
#include "IRsend.h"
#include "IRtext.h"
#include "IRutils.h"
// Constants
const uint16_t kTranscoldHdrMark = 5944; ///< uSeconds.
const uint16_t kTranscoldBitMark = 555; ///< uSeconds.
const uint16_t kTranscoldHdrSpace = 7563; ///< uSeconds.
const uint16_t kTranscoldOneSpace = 3556; ///< uSeconds.
const uint16_t kTranscoldZeroSpace = 1526; ///< uSeconds.
const uint16_t kTranscoldFreq = 38000; ///< Hz.
using irutils::addBoolToString;
using irutils::addIntToString;
using irutils::addLabeledString;
using irutils::addModeToString;
using irutils::addTempToString;
using irutils::setBit;
using irutils::setBits;
#if SEND_TRANSCOLD
/// Send a Transcold formatted message.
/// Status: BETA / Probably works, needs to be tested on a real device.
/// @note Data bit ordering not yet confirmed. MSBF at present.
/// @param[in] data containing the IR command.
/// @param[in] nbits Nr. of bits to send. usually kTranscoldBits
/// @param[in] repeat Nr. of times the message is to be repeated.
void IRsend::sendTranscold(const uint64_t data, const uint16_t nbits,
const uint16_t repeat) {
/// Send a Transcold message
/// Status: STABLE / Confirmed 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::sendTranscold(uint64_t data, uint16_t nbits, uint16_t repeat) {
if (nbits % 8 != 0) return; // nbits is required to be a multiple of 8.
// Set IR carrier frequency
enableIROut(38);
for (uint16_t r = 0; r <= repeat; r++) {
sendGeneric(kTranscoldHdrMark, kTranscoldHdrSpace, // Header
kTranscoldBitMark, kTranscoldOneSpace, // Data
kTranscoldBitMark, kTranscoldZeroSpace,
kTranscoldBitMark, kTranscoldHdrSpace, // Footer (1 of 2).
data, nbits, // Payload
kTranscoldFreq, true, 0, // Repeat handled by outer loop.
kDutyDefault);
// Footer (2 of 2)
// Header
mark(kTranscoldHdrMark);
space(kTranscoldHdrSpace);
// Data
// Break data into byte segments, starting at the Most Significant
// Byte. Each byte then being sent normal, then followed inverted.
for (uint16_t i = 8; i <= nbits; i += 8) {
// Grab a bytes worth of data.
// uint8_t segment = (data >> (nbits - i)) & 0xFF;
uint8_t segment = GETBITS64(data, nbits - i, 8);
// Normal + Inverted
uint16_t both = (segment << 8) | (~segment & 0xFF);
sendData(kTranscoldBitMark, kTranscoldOneSpace, kTranscoldBitMark,
kTranscoldZeroSpace, both, 16, true);
}
// Footer
mark(kTranscoldBitMark);
space(kDefaultMessageGap); // A guess of the gap between messages.
space(kTranscoldHdrSpace);
mark(kTranscoldBitMark);
space(kDefaultMessageGap);
}
}
#endif // SEND_TRANSCOLD
/// 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?
IRTranscoldAc::IRTranscoldAc(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 IRTranscoldAc::stateReset() {
setRaw(kTranscoldKnownGoodState);
saved_state = kTranscoldKnownGoodState;
powerFlag = false;
swingFlag = false;
swingHFlag = false;
swingVFlag = false;
}
/// Set up hardware to be able to send a message.
void IRTranscoldAc::begin() { _irsend.begin(); }
#if SEND_TRANSCOLD
/// Send the current internal state as an IR message.
/// @param[in] repeat Nr. of times the message will be repeated.
void IRTranscoldAc::send(uint16_t repeat) {
_irsend.sendTranscold(remote_state, kTranscoldBits, repeat);
// make sure to remove special state from remote_state
// after command has being transmitted.
recoverSavedState();
}
#endif // SEND_TRANSCOLD
/// Get a copy of the internal state as a valid code for this protocol.
/// @return A valid code for this protocol based on the current internal state.
uint32_t IRTranscoldAc::getRaw() { 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 IRTranscoldAc::setRaw(const uint32_t new_code) {
powerFlag = true; // Everything that is not the special power off mesg is On.
if (!handleSpecialState(new_code)) {
// it isn`t special so might affect Temp|mode|Fan
if (new_code == kTranscoldCmdFan) {
setMode(kTranscoldFan);
return;
}
}
// must be a command changing Temp|Mode|Fan
// it is safe to just copy to remote var
remote_state = new_code;
}
/// Is the current state is a special state?
/// @return true, if it is. false if it isn't.
bool IRTranscoldAc::isSpecialState(void) {
switch (remote_state) {
case kTranscoldOff:
case kTranscoldSwing: return true;
default: return false;
}
}
/// Adjust any internal settings based on the type of special state we are
/// supplied. Does nothing if it isn't a special state.
/// @param[in] data The state we need to act upon.
/// @note Special state means commands that are not affecting
/// Temperature/Mode/Fan
/// @return true, if it is a special state. false if it isn't.
bool IRTranscoldAc::handleSpecialState(const uint32_t data) {
switch (data) {
case kTranscoldOff:
powerFlag = false;
break;
case kTranscoldSwing:
swingFlag = !swingFlag;
break;
default:
return false;
}
return true;
}
/// Backup the current internal state as long as it isn't a special state.
/// @note: Must be called before every special state to make sure the
/// remote_state is safe
void IRTranscoldAc::updateSavedState(void) {
if (!isSpecialState()) saved_state = remote_state;
}
/// Restore the current internal state from backup as long as it isn't a
/// special state.
void IRTranscoldAc::recoverSavedState(void) {
// If the current state is a special one, last known normal one.
if (isSpecialState()) remote_state = saved_state;
// If the saved_state was also a special state, reset as we expect a normal
// state out of all this.
if (isSpecialState()) stateReset();
}
/// Set the raw (native) temperature value.
/// @note Bypasses any checks.
/// @param[in] code The desired native temperature.
void IRTranscoldAc::setTempRaw(const uint8_t code) {
setBits(&remote_state, kTranscoldTempOffset, kTranscoldTempSize, code);
}
/// Get the raw (native) temperature value.
/// @return The native temperature value.
uint8_t IRTranscoldAc::getTempRaw() {
return GETBITS32(remote_state, kTranscoldTempOffset, kTranscoldTempSize);
}
/// Set the temperature.
/// @param[in] desired The temperature in degrees celsius.
void IRTranscoldAc::setTemp(const uint8_t desired) {
// Range check.
uint8_t temp = std::min(desired, kTranscoldTempMax);
temp = std::max(temp, kTranscoldTempMin) - kTranscoldTempMin + 1;
setTempRaw(reverseBits(invertBits(temp, kTranscoldTempSize),
kTranscoldTempSize));
}
/// Get the current temperature setting.
/// @return The current setting for temp. in degrees celsius.
uint8_t IRTranscoldAc::getTemp() {
return reverseBits(invertBits(getTempRaw(), kTranscoldTempSize),
kTranscoldTempSize) + kTranscoldTempMin - 1;
}
/// Get the value of the current power setting.
/// @return true, the setting is on. false, the setting is off.
bool IRTranscoldAc::getPower() {
// There is only an off state. Everything else is "on".
return powerFlag;
}
/// Change the power setting.
/// @param[in] on true, the setting is on. false, the setting is off.
void IRTranscoldAc::setPower(const bool on) {
if (!on) {
updateSavedState();
remote_state = kTranscoldOff;
} else if (!powerFlag) {
// at this point remote_state must be ready
// to be transmitted
recoverSavedState();
}
powerFlag = on;
}
/// Change the power setting to On.
void IRTranscoldAc::on(void) { setPower(true); }
/// Change the power setting to Off.
void IRTranscoldAc::off(void) { setPower(false); }
/// Get the Swing setting of the A/C.
/// @return true, the setting is on. false, the setting is off.
bool IRTranscoldAc::getSwing() { return swingFlag; }
/// Toggle the Swing mode of the A/C.
void IRTranscoldAc::setSwing() {
// Assumes that repeated sending "swing" toggles the action on the device.
updateSavedState();
remote_state = kTranscoldSwing;
swingFlag = !swingFlag;
}
/// Set the operating mode of the A/C.
/// @param[in] mode The desired operating mode.
void IRTranscoldAc::setMode(const uint8_t mode) {
uint32_t actualmode = mode;
switch (actualmode) {
case kTranscoldAuto:
case kTranscoldDry:
setFan(kTranscoldFanAuto0, false);
break;
case kTranscoldCool:
case kTranscoldHeat:
case kTranscoldFan:
setFan(kTranscoldFanAuto, false);
break;
default: // Anything else, go with Auto mode.
setMode(kTranscoldAuto);
setFan(kTranscoldFanAuto0, false);
return;
}
setTemp(getTemp());
// Fan mode is a special case of Dry.
if (mode == kTranscoldFan) {
actualmode = kTranscoldDry;
setTempRaw(kTranscoldFanTempCode);
}
setBits(&remote_state, kTranscoldModeOffset, kTranscoldModeSize, actualmode);
}
/// Get the operating mode setting of the A/C.
/// @return The current operating mode setting.
uint8_t IRTranscoldAc::getMode() {
uint8_t mode = GETBITS32(remote_state, kTranscoldModeOffset,
kTranscoldModeSize);
if (mode == kTranscoldDry)
if (getTempRaw() == kTranscoldFanTempCode) return kTranscoldFan;
return mode;
}
/// Get the current fan speed setting.
/// @return The current fan speed.
uint8_t IRTranscoldAc::getFan() {
return GETBITS32(remote_state, kTranscoldFanOffset, kTranscoldFanSize);
}
/// Set the speed of the fan.
/// @param[in] speed The desired setting.
/// @param[in] modecheck Do we enforce any mode limitations before setting?
void IRTranscoldAc::setFan(const uint8_t speed, const bool modecheck) {
uint8_t newspeed = speed;
switch (speed) {
case kTranscoldFanAuto: // Dry & Auto mode can't have this speed.
if (modecheck) {
switch (getMode()) {
case kTranscoldAuto:
case kTranscoldDry:
newspeed = kTranscoldFanAuto0;
break;
}
}
break;
case kTranscoldFanAuto0: // Only Dry & Auto mode can have this speed.
if (modecheck) {
switch (getMode()) {
case kTranscoldAuto:
case kTranscoldDry: break;
default: newspeed = kTranscoldFanAuto;
}
}
break;
case kTranscoldFanMin:
case kTranscoldFanMed:
case kTranscoldFanMax:
case kTranscoldFanZoneFollow:
case kTranscoldFanFixed:
break;
default: // Unknown speed requested.
newspeed = kTranscoldFanAuto;
break;
}
setBits(&remote_state, kTranscoldFanOffset, kTranscoldFanSize, newspeed);
}
/// Convert a standard A/C mode into its native mode.
/// @param[in] mode A stdAc::opmode_t to be converted to it's native equivalent.
/// @return The corresponding native mode.
uint8_t IRTranscoldAc::convertMode(const stdAc::opmode_t mode) {
switch (mode) {
case stdAc::opmode_t::kCool: return kTranscoldCool;
case stdAc::opmode_t::kHeat: return kTranscoldHeat;
case stdAc::opmode_t::kDry: return kTranscoldDry;
case stdAc::opmode_t::kFan: return kTranscoldFan;
default: return kTranscoldAuto;
}
}
/// Convert a stdAc::fanspeed_t enum into it's native speed.
/// @param[in] speed The enum to be converted.
/// @return The native equivilant of the enum.
uint8_t IRTranscoldAc::convertFan(const stdAc::fanspeed_t speed) {
switch (speed) {
case stdAc::fanspeed_t::kMin:
case stdAc::fanspeed_t::kLow: return kTranscoldFanMin;
case stdAc::fanspeed_t::kMedium: return kTranscoldFanMed;
case stdAc::fanspeed_t::kHigh:
case stdAc::fanspeed_t::kMax: return kTranscoldFanMax;
default: return kTranscoldFanAuto;
}
}
/// Convert a native mode to it's common stdAc::opmode_t equivalent.
/// @param[in] mode A native operation mode to be converted.
/// @return The corresponding common stdAc::opmode_t mode.
stdAc::opmode_t IRTranscoldAc::toCommonMode(const uint8_t mode) {
switch (mode) {
case kTranscoldCool: return stdAc::opmode_t::kCool;
case kTranscoldHeat: return stdAc::opmode_t::kHeat;
case kTranscoldDry: return stdAc::opmode_t::kDry;
case kTranscoldFan: return stdAc::opmode_t::kFan;
default: return stdAc::opmode_t::kAuto;
}
}
/// Convert a native fan speed into its stdAc equivilant.
/// @param[in] speed The native setting to be converted.
/// @return The stdAc equivilant of the native setting.
stdAc::fanspeed_t IRTranscoldAc::toCommonFanSpeed(const uint8_t speed) {
switch (speed) {
case kTranscoldFanMax: return stdAc::fanspeed_t::kMax;
case kTranscoldFanMed: return stdAc::fanspeed_t::kMedium;
case kTranscoldFanMin: return stdAc::fanspeed_t::kMin;
default: return stdAc::fanspeed_t::kAuto;
}
}
/// Convert the A/C state to it's common stdAc::state_t equivalent.
/// @param[in] prev Ptr to the previous state if required.
/// @return A stdAc::state_t state.
stdAc::state_t IRTranscoldAc::toCommon(const stdAc::state_t *prev) {
stdAc::state_t result;
// Start with the previous state if given it.
if (prev != NULL) {
result = *prev;
} else {
// Set defaults for non-zero values that are not implicitly set for when
// there is no previous state.
// e.g. Any setting that toggles should probably go here.
result.swingv = stdAc::swingv_t::kOff;
}
// Not supported.
result.model = -1; // No models used.
result.swingh = stdAc::swingh_t::kOff;
result.turbo = false;
result.clean = false;
result.light = false;
result.quiet = false;
result.econo = false;
result.filter = false;
result.beep = false;
result.clock = -1;
result.sleep = -1;
// Supported.
result.protocol = decode_type_t::TRANSCOLD;
result.celsius = true;
result.power = getPower();
// Power off state no other state info. Use the previous state if we have it.
if (!result.power) return result;
// Handle the special single command (Swing/Turbo/Light/Clean/Sleep) toggle
// messages. These have no other state info so use the rest of the previous
// state if we have it for them.
if (getSwing()) {
result.swingv = result.swingv != stdAc::swingv_t::kOff ?
stdAc::swingv_t::kOff : stdAc::swingv_t::kAuto; // Invert swing.
return result;
}
// Back to "normal" stateful messages.
result.mode = toCommonMode(getMode());
result.degrees = getTemp();
result.fanspeed = toCommonFanSpeed(getFan());
return result;
}
/// Convert the internal state into a human readable string.
/// @return The current internal state expressed as a human readable String.
String IRTranscoldAc::toString(void) {
String result = "";
result.reserve(100); // Reserve some heap for the string to reduce fragging.
result += addBoolToString(getPower(), kPowerStr, false);
if (!getPower()) return result; // If it's off, there is no other info.
// Special modes.
if (getSwing()) {
result += kCommaSpaceStr;
result += kSwingStr;
result += kColonSpaceStr;
result += kToggleStr;
return result;
}
result += addModeToString(getMode(), kTranscoldAuto, kTranscoldCool,
kTranscoldHeat, kTranscoldDry, kTranscoldFan);
result += addIntToString(getFan(), kFanStr);
result += kSpaceLBraceStr;
switch (getFan()) {
case kTranscoldFanAuto:
result += kAutoStr;
break;
case kTranscoldFanAuto0:
result += kAutoStr;
result += '0';
break;
case kTranscoldFanMax:
result += kMaxStr;
break;
case kTranscoldFanMin:
result += kMinStr;
break;
case kTranscoldFanMed:
result += kMedStr;
break;
case kTranscoldFanZoneFollow:
result += kZoneFollowStr;
break;
case kTranscoldFanFixed:
result += kFixedStr;
break;
default:
result += kUnknownStr;
}
result += ')';
// Fan mode doesn't have a temperature.
if (getMode() != kTranscoldFan) result += addTempToString(getTemp());
return result;
}
#if DECODE_TRANSCOLD
/// Decode the supplied Transcold message.
/// Status: BETA / Probably works.
/// @note Data bit ordering not yet confirmed. MSBF at present.
/// Decode the supplied Transcold A/C message.
/// Status: STABLE / Known 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::decodeTranscold(decode_results *results, uint16_t offset,
const uint16_t nbits, const bool strict) {
if (results->rawlen < 2 * nbits + kHeader + (2 * kFooter) - offset)
return false; // Too short a message to match.
if (strict && nbits != kTranscoldBits)
const uint16_t nbits, const bool strict) {
// The protocol sends the data normal + inverted, alternating on
// each byte. Hence twice the number of expected data bits.
if (results->rawlen <= 2 * 2 * nbits + kHeader + kFooter - 1 + offset)
return false;
if (strict && nbits != kTranscoldBits) return false;
if (nbits % 8 != 0) return false;
uint64_t data = 0;
uint64_t inverted = 0;
// Match Header + Data + Footer (1 of 2)
uint16_t used = matchGeneric(results->rawbuf + offset, &data,
results->rawlen - offset, nbits,
// Header
kTranscoldHdrMark, kTranscoldHdrSpace,
// Data
kTranscoldBitMark, kTranscoldOneSpace,
kTranscoldBitMark, kTranscoldZeroSpace,
// Footer (1 of 2)
kTranscoldBitMark, kTranscoldHdrSpace,
false, _tolerance, 0, true);
if (!used) return false; // Didn't fully match.
offset += used;
if (nbits > sizeof(data) * 8)
return false; // We can't possibly capture a Transcold packet that big.
// Footer (2 of 2)
if (!matchMark(results->rawbuf[offset++], kTranscoldBitMark))
return false;
if (offset <= results->rawlen &&
// Header
if (!matchMark(results->rawbuf[offset++], kTranscoldHdrMark)) return false;
if (!matchSpace(results->rawbuf[offset++], kTranscoldHdrSpace)) return false;
// Data
// Twice as many bits as there are normal plus inverted bits.
for (uint16_t i = 0; i < nbits * 2; i++, offset++) {
bool flip = (i / 8) % 2;
if (!matchMark(results->rawbuf[offset++], kTranscoldBitMark))
return false;
if (matchSpace(results->rawbuf[offset], kTranscoldOneSpace)) {
if (flip)
inverted = (inverted << 1) | 1;
else
data = (data << 1) | 1;
} else if (matchSpace(results->rawbuf[offset], kTranscoldZeroSpace)) {
if (flip)
inverted <<= 1;
else
data <<= 1;
} else {
return false;
}
}
// Footer
if (!matchMark(results->rawbuf[offset++], kTranscoldBitMark)) return false;
if (!matchSpace(results->rawbuf[offset++], kTranscoldHdrSpace)) return false;
if (!matchMark(results->rawbuf[offset++], kTranscoldBitMark)) return false;
if (offset < results->rawlen &&
!matchAtLeast(results->rawbuf[offset], kDefaultMessageGap))
return false;
// Compliance
if (strict && inverted != invertBits(data, nbits)) return false;
// Success
results->decode_type = decode_type_t::TRANSCOLD;
results->bits = nbits;
results->value = data;
results->command = 0;
results->address = 0;
results->command = 0;
return true;
}
#endif // DECODE_TRANSCOLD

170
src/ir_Transcold.h Normal file
View File

@@ -0,0 +1,170 @@
// Copyright 2020 Chandrashekar Shetty (iamDshetty)
// Copyright 2020 crankyoldgit
/// @file
/// @brief Support for Transcold A/C protocols.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1256
/// @see https://docs.google.com/spreadsheets/d/1qdoyB0FyJm85HPP9oXcfui0n4ztXBFlik6kiNlkO2IM/edit?usp=sharing
// Supports:
// Brand: Transcold, Model: M1-F-NO-6 A/C
/***************************************************************************************************************
Raw Data Calculation: (UR 12)
//ON button
ON 24 Auto cool close (right) 111011110001000001100001100111100101010010101011
//OFF button
OFF 24 Auto cool close (right) 111011110001000001110001100011100101010010101011
// MODE
Hot mode 24 auto hot close (right) 111010010001011010100001010111100101010010101011
Fan mode 0 (prev24) low fan close (right) "11101001 0001011000100001110111100101010010101011"
Dry mode 24 low dry close (right) "11101001 0001011011000001 00111110 0101010010101011"
Auto Mode 0(prev24) low auto close (right) "11101001 0001011011100001 00011110 0101010010101011"
Cool Mode 24 low cool close (right) "11101001 0001011001100001 10011110 0101010010101011"
//FAN SPEED
fan Speed low 24 low cool close (right) "11101001 0001011001100001 10011110 0101010010101011"
fan Speed medium 24 medium cool close (right) "11101101 000100100110000110011110 0101010010101011"
fan Speed high 24 high cool close (right) "11101011 000101000110000110011110 0101010010101011"
fan Speed auto 24 auto cool close (right) "11101111 000100000110000110011110 0101010010101011"
//SWING
Swing open 24 auto cool open (left) "11110111 000010000110000110011110 0101010010101011"
Swing close 24 auto cool close (right) "11101111 000100000110000110011110 0101010010101011"
//TEMPERATURE
temp 30degC Auto cool close (right) 111011110001000001100100100100010101010010101011
temp 29 Auto cool close (right) 111011110001000001101100100100010101010010101011
temp 28 Auto cool close (right) 111011110001000001100010100100010101010010101011
temp 27 Auto cool close (right) 111011110001000001101010100100010101010010101011
temp 26 Auto cool close (right) 111011110001000001100110100100010101010010101011
temp 25 Auto cool close (right) 111011110001000001101110100100010101010010101011
temp 24 Auto cool close (right) 111011110001000001100001100111100101010010101011
temp 23 Auto cool close (right) 111011110001000001101001100101100101010010101011
temp 22 Auto cool close (right) 111011110001000001100101100101100101010010101011
temp 21 Auto cool close (right) 111011110001000001101101100101100101010010101011
temp 20 Auto cool close (right) 111011110001000001100011100101100101010010101011
temp 19 Auto cool close (right) 111011110001000001101011100101100101010010101011
temp 18 Auto cool close (right) 111011110001000001100111100110000101010010101011
temp 17 Auto cool close (right) 111011110001000001100111100110000101010010101011
temp 16 Auto cool close (right) 111011110001000001100111100110000101010010101011
**************************************************************************************************************/
#ifndef IR_TRANSCOLD_H_
#define IR_TRANSCOLD_H_
#define __STDC_LIMIT_MACROS
#include <stdint.h>
#ifndef UNIT_TEST
#include <Arduino.h>
#endif
#include "IRremoteESP8266.h"
#include "IRsend.h"
#ifdef UNIT_TEST
#include "IRsend_test.h"
#endif
// Constants
// Modes
const uint8_t kTranscoldCool = 0b0110;
const uint8_t kTranscoldDry = 0b1100;
const uint8_t kTranscoldAuto = 0b1110;
const uint8_t kTranscoldHeat = 0b1010;
const uint8_t kTranscoldFan = 0b0010;
const uint8_t kTranscoldModeOffset = 12;
const uint8_t kTranscoldModeSize = 4;
// Fan Control
const uint8_t kTranscoldFanOffset = 16;
const uint8_t kTranscoldFanSize = 4;
const uint8_t kTranscoldFanMin = 0b1001;
const uint8_t kTranscoldFanMed = 0b1101;
const uint8_t kTranscoldFanMax = 0b1011;
const uint8_t kTranscoldFanAuto = 0b1111;
const uint8_t kTranscoldFanAuto0 = 0b0110;
const uint8_t kTranscoldFanZoneFollow = 0b0000;
const uint8_t kTranscoldFanFixed = 0b1100;
// Temperature
const uint8_t kTranscoldTempMin = 18; // Celsius
const uint8_t kTranscoldTempMax = 30; // Celsius
const uint8_t kTranscoldFanTempCode = 0b1111; // Part of Fan Mode.
const uint8_t kTranscoldTempOffset = 8;
const uint8_t kTranscoldTempSize = 4;
const uint8_t kTranscoldPrefix = 0b0000;
const uint8_t kTranscoldUnknown = 0xFF;
const uint32_t kTranscoldOff = 0b111011110111100101010100;
const uint32_t kTranscoldSwing = 0b111001110110000101010100;
const uint32_t kTranscoldSwingH = 0b111101110110000101010100; // NA
const uint32_t kTranscoldSwingV = 0b111001110110000101010100; // NA
const uint32_t kTranscoldCmdFan = 0b111011110110000101010100; // NA
const uint32_t kTranscoldKnownGoodState = 0xE96554;
// Classes
class IRTranscoldAc {
public:
explicit IRTranscoldAc(const uint16_t pin, const bool inverted = false,
const bool use_modulation = true);
void stateReset();
#if SEND_TRANSCOLD
void send(const uint16_t repeat = kTranscoldDefaultRepeat);
/// 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_TRANSCOLD
void begin();
void on();
void off();
void setPower(const bool state);
bool getPower();
void setTemp(const uint8_t temp);
uint8_t getTemp();
void setFan(const uint8_t speed, const bool modecheck = true);
uint8_t getFan();
void setMode(const uint8_t mode);
uint8_t getMode();
void setSwing();
bool getSwing();
uint32_t getRaw();
void setRaw(const uint32_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(const stdAc::state_t *prev = NULL);
String toString();
#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
// internal state
bool powerFlag;
bool swingFlag;
bool swingHFlag;
bool swingVFlag;
uint32_t remote_state; ///< The state of the IR remote in IR code form.
uint32_t saved_state; ///< Copy of the state if we required a special mode.
void setTempRaw(const uint8_t code);
uint8_t getTempRaw();
bool isSpecialState(void);
bool handleSpecialState(const uint32_t data);
void updateSavedState(void);
void recoverSavedState(void);
uint32_t getNormalState(void);
};
#endif // IR_TRANSCOLD_H_

View File

@@ -1467,6 +1467,36 @@ TEST(TestIRac, Toshiba) {
ac._irsend.outputStr());
}
TEST(TestIRac, Transcold) {
IRTranscoldAc ac(kGpioUnused);
IRac irac(kGpioUnused);
IRrecv capture(kGpioUnused);
char expected[] =
"Power: On, Mode: 6 (Cool), Fan: 11 (Max), Temp: 19C";
ac.begin();
irac.transcold(&ac,
true, // Power
stdAc::opmode_t::kCool, // Mode
19, // Celsius
stdAc::fanspeed_t::kMax, // Fan speed
stdAc::swingv_t::kOff, // Vertical swing
stdAc::swingh_t::kOff); // Horizontal swing
EXPECT_TRUE(ac.getPower());
EXPECT_EQ(kTranscoldCool, ac.getMode());
EXPECT_EQ(19, ac.getTemp());
EXPECT_EQ(kTranscoldFanMax, ac.getFan());
EXPECT_FALSE(ac.getSwing());
ASSERT_EQ(expected, ac.toString());
ac._irsend.makeDecodeResult();
EXPECT_TRUE(capture.decode(&ac._irsend.capture));
ASSERT_EQ(decode_type_t::TRANSCOLD, ac._irsend.capture.decode_type);
ASSERT_EQ(kTranscoldBits, ac._irsend.capture.bits);
ASSERT_EQ(expected, IRAcUtils::resultAcToString(&ac._irsend.capture));
stdAc::state_t r, p;
ASSERT_TRUE(IRAcUtils::decodeToState(&ac._irsend.capture, &r, &p));
}
TEST(TestIRac, Trotec) {
IRTrotecESP ac(kGpioUnused);
IRac irac(kGpioUnused);

View File

@@ -34,9 +34,14 @@ TEST(TestDecodeTranscold, RealExample) {
ASSERT_TRUE(irrecv.decode(&irsend.capture));
ASSERT_EQ(decode_type_t::TRANSCOLD, irsend.capture.decode_type);
ASSERT_EQ(kTranscoldBits, irsend.capture.bits);
EXPECT_EQ(0xE916659A54AB, irsend.capture.value);
EXPECT_EQ(0xE96554, irsend.capture.value);
EXPECT_EQ(0x0, irsend.capture.command);
EXPECT_EQ(0x0, irsend.capture.address);
EXPECT_EQ(
"Power: On, Mode: 6 (Cool), Fan: 9 (Min), Temp: 22C",
IRAcUtils::resultAcToString(&irsend.capture));
stdAc::state_t r, p;
ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p));
}
TEST(TestDecodeTranscold, SyntheticExample) {
@@ -44,13 +49,13 @@ TEST(TestDecodeTranscold, SyntheticExample) {
IRrecv irrecv(kGpioUnused);
irsend.begin();
irsend.reset();
irsend.sendTranscold(0xE916659A54AB);
irsend.sendTranscold(0xE96554);
irsend.makeDecodeResult();
ASSERT_TRUE(irrecv.decode(&irsend.capture));
EXPECT_EQ(decode_type_t::TRANSCOLD, irsend.capture.decode_type);
EXPECT_EQ(kTranscoldBits, irsend.capture.bits);
EXPECT_EQ(0xE916659A54AB, irsend.capture.value);
EXPECT_EQ(0xE96554, irsend.capture.value);
EXPECT_EQ(0x0, irsend.capture.command);
EXPECT_EQ(0x0, irsend.capture.address);
@@ -72,7 +77,134 @@ TEST(TestUtils, Housekeeping) {
ASSERT_EQ("TRANSCOLD", typeToString(decode_type_t::TRANSCOLD));
ASSERT_EQ(decode_type_t::TRANSCOLD, strToDecodeType("TRANSCOLD"));
ASSERT_FALSE(hasACState(decode_type_t::TRANSCOLD));
ASSERT_FALSE(IRac::isProtocolSupported(decode_type_t::TRANSCOLD));
ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::TRANSCOLD));
ASSERT_EQ(kTranscoldBits, IRsend::defaultBits(decode_type_t::TRANSCOLD));
ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::TRANSCOLD));
ASSERT_EQ(kTranscoldDefaultRepeat,
IRsend::minRepeats(decode_type_t::TRANSCOLD));
}
// Tests for the IRTranscoldAc class.
TEST(TestTranscoldAcClass, SetAndGetRaw) {
IRTranscoldAc ac(kGpioUnused);
ac.setRaw(0xB21F28);
EXPECT_EQ(0xB21F28, ac.getRaw());
ac.setRaw(kTranscoldKnownGoodState);
EXPECT_EQ(kTranscoldKnownGoodState, ac.getRaw());
}
TEST(TestTranscoldAcClass, SetAndGetTemp) {
IRTranscoldAc ac(kGpioUnused);
ac.setTemp(25);
EXPECT_EQ(25, ac.getTemp());
ac.setTemp(kTranscoldTempMin);
EXPECT_EQ(kTranscoldTempMin, ac.getTemp());
ac.setTemp(kTranscoldTempMax);
EXPECT_EQ(kTranscoldTempMax, ac.getTemp());
ac.setTemp(kTranscoldTempMin - 1);
EXPECT_EQ(kTranscoldTempMin, ac.getTemp());
ac.setTemp(kTranscoldTempMax + 1);
EXPECT_EQ(kTranscoldTempMax, ac.getTemp());
}
TEST(TestTranscoldAcClass, SetAndGetMode) {
IRTranscoldAc ac(kGpioUnused);
ac.setMode(kTranscoldHeat);
EXPECT_EQ(kTranscoldHeat, ac.getMode());
ac.setMode(kTranscoldCool);
EXPECT_EQ(kTranscoldCool, ac.getMode());
ac.setMode(kTranscoldDry);
EXPECT_EQ(kTranscoldDry, ac.getMode());
ac.setMode(kTranscoldAuto);
EXPECT_EQ(kTranscoldAuto, ac.getMode());
ac.setMode(kTranscoldFan);
EXPECT_EQ(kTranscoldFan, ac.getMode());
}
TEST(TestTranscoldAcClass, SetAndGetFan) {
IRTranscoldAc ac(kGpioUnused);
// This mode allows pretty much everything except Auto0 speed.
ac.setMode(kTranscoldCool);
ac.setFan(kTranscoldFanMax);
EXPECT_EQ(kTranscoldFanMax, ac.getFan());
ac.setFan(kTranscoldFanMin);
EXPECT_EQ(kTranscoldFanMin, ac.getFan());
ac.setFan(kTranscoldFanZoneFollow);
EXPECT_EQ(kTranscoldFanZoneFollow, ac.getFan());
ac.setFan(kTranscoldFanAuto);
EXPECT_EQ(kTranscoldFanAuto, ac.getFan());
ac.setFan(kTranscoldFanAuto0);
EXPECT_EQ(kTranscoldFanAuto, ac.getFan());
ac.setFan(kTranscoldFanMax);
EXPECT_EQ(kTranscoldFanMax, ac.getFan());
ASSERT_NE(3, kTranscoldFanAuto);
// Now try some unexpected value.
ac.setFan(3);
EXPECT_EQ(kTranscoldFanAuto, ac.getFan());
// These modes allows pretty much everything except Auto speed.
ac.setMode(kTranscoldDry);
EXPECT_EQ(kTranscoldFanAuto0, ac.getFan());
ac.setFan(kTranscoldFanMax);
EXPECT_EQ(kTranscoldFanMax, ac.getFan());
ac.setFan(kTranscoldFanAuto);
EXPECT_EQ(kTranscoldFanAuto0, ac.getFan());
ac.setMode(kTranscoldAuto);
EXPECT_EQ(kTranscoldFanAuto0, ac.getFan());
ac.setFan(kTranscoldFanMax);
EXPECT_EQ(kTranscoldFanMax, ac.getFan());
ac.setFan(kTranscoldFanAuto0);
EXPECT_EQ(kTranscoldFanAuto0, ac.getFan());
}
TEST(TestTranscoldAcClass, SpecialModesAndReset) {
IRTranscoldAc ac(kGpioUnused);
ASSERT_NE(kTranscoldSwing, ac.getRaw());
ac.setSwing();
ASSERT_EQ(kTranscoldSwing, ac.getRaw());
ac.stateReset();
ASSERT_NE(kTranscoldSwing, ac.getRaw());
}
TEST(TestTranscoldAcClass, HumanReadable) {
IRTranscoldAc ac(kGpioUnused);
ac.begin();
// Initial starting point.
EXPECT_EQ(
"Power: Off", ac.toString());
ac.setPower(true);
EXPECT_EQ(
"Power: On, Mode: 6 (Cool), Fan: 9 (Min), Temp: 22C",
ac.toString());
ac.setMode(kTranscoldHeat);
ac.setFan(kTranscoldFanMin);
ac.setTemp(22);
EXPECT_EQ(
"Power: On, Mode: 10 (Heat), Fan: 9 (Min), Temp: 22C",
ac.toString());
ac.setSwing();
EXPECT_EQ("Power: On, Swing: Toggle", ac.toString());
ac.setPower(false);
EXPECT_EQ("Power: Off", ac.toString());
}
TEST(TestTranscoldAcClass, BuildKnownState) {
IRTranscoldAc ac(kGpioUnused);
// "temp down, 19, Auto, cool, close (right)"
// Data from:
// https://docs.google.com/spreadsheets/d/1qdoyB0FyJm85HPP9oXcfui0n4ztXBFlik6kiNlkO2IM/edit#gid=694351627&range=A25:F25
const uint32_t state = 0xEF6B54;
ac.stateReset();
ac.on();
ac.setMode(kTranscoldCool);
ac.setFan(kTranscoldFanAuto);
ac.setTemp(19);
EXPECT_EQ("Power: On, Mode: 6 (Cool), Fan: 15 (Auto), Temp: 19C",
ac.toString());
ASSERT_EQ(state, ac.getRaw());
}