Add support for Rhoss Idrowall MPCV 20-30-35-40 A/C protocol (#1630)

- Add send and decode routines
- Add support for:
  - Power
  - Temp (Celsius)
  - Swing
  - Mode
  - Fan Speed
  - Checksums
- Add Unit tests

Co-authored-by: Tom Rosenback <tom@carputec.com>
This commit is contained in:
tomrosenback
2021-10-09 01:09:32 +03:00
committed by GitHub
parent 1beea0c586
commit 2e16e385c6
15 changed files with 1009 additions and 6 deletions

View File

@@ -36,6 +36,7 @@
#include "ir_MitsubishiHeavy.h"
#include "ir_Neoclima.h"
#include "ir_Panasonic.h"
#include "ir_Rhoss.h"
#include "ir_Samsung.h"
#include "ir_Sanyo.h"
#include "ir_Sharp.h"
@@ -270,6 +271,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) {
#if SEND_PANASONIC_AC32
case decode_type_t::PANASONIC_AC32:
#endif
#if SEND_RHOSS
case decode_type_t::RHOSS:
#endif
#if SEND_SAMSUNG_AC
case decode_type_t::SAMSUNG_AC:
#endif
@@ -2362,6 +2366,35 @@ void IRac::transcold(IRTranscoldAc *ac,
}
#endif // SEND_TRANSCOLD
#if SEND_RHOSS
/// Send an Rhoss A/C message with the supplied settings.
/// @param[in, out] ac A Ptr to an IRRhossAc 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] swing The swing setting.
void IRac::rhoss(IRRhossAc *ac,
const bool on, const stdAc::opmode_t mode, const float degrees,
const stdAc::fanspeed_t fan, const stdAc::swingv_t swing) {
ac->begin();
ac->setPower(on);
ac->setMode(ac->convertMode(mode));
ac->setSwing(swing != stdAc::swingv_t::kOff);
ac->setTemp(degrees);
ac->setFan(ac->convertFan(fan));
// No Quiet setting available.
// No Light setting available.
// No Filter setting available.
// No Turbo setting available.
// No Economy setting available.
// No Clean setting available.
// No Beep setting available.
// No Sleep setting available.
ac->send();
}
#endif // SEND_RHOSS
/// 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.
@@ -2887,6 +2920,14 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) {
break;
}
#endif // SEND_PANASONIC_AC32
#if SEND_RHOSS
case RHOSS:
{
IRRhossAc ac(_pin, _inverted, _modulation);
rhoss(&ac, send.power, send.mode, degC, send.fanspeed, send.swingv);
break;
}
#endif // SEND_RHOSS
#if SEND_SAMSUNG_AC
case SAMSUNG_AC:
{
@@ -3789,6 +3830,13 @@ namespace IRAcUtils {
return ac.toString();
}
#endif // DECODE_TRANSCOLD
#if DECODE_RHOSS
case decode_type_t::RHOSS: {
IRRhossAc ac(kGpioUnused);
ac.setRaw(result->state);
return ac.toString();
}
#endif // DECODE_RHOSS
default:
return "";
}
@@ -4254,6 +4302,14 @@ namespace IRAcUtils {
break;
}
#endif // DECODE_TRANSCOLD
#if DECODE_RHOSS
case decode_type_t::RHOSS: {
IRRhossAc ac(kGpioUnused);
ac.setRaw(decode->state);
*result = ac.toCommon();
break;
}
#endif // DECODE_RHOSS
default:
return false;
}

View File

@@ -43,6 +43,7 @@
#include "ir_Vestel.h"
#include "ir_Voltas.h"
#include "ir_Whirlpool.h"
#include "ir_Rhoss.h"
// Constants
const int8_t kGpioUnused = -1; ///< A placeholder for not using an actual GPIO.
@@ -489,6 +490,11 @@ void electra(IRElectraAc *ac,
const stdAc::fanspeed_t fan,
const stdAc::swingv_t swingv, const stdAc::swingh_t swingh);
#endif // SEND_TRANSCOLD
#if SEND_RHOSS
void rhoss(IRRhossAc *ac,
const bool on, const stdAc::opmode_t mode, const float degrees,
const stdAc::fanspeed_t fan, const stdAc::swingv_t swing);
#endif // SEND_RHOSS
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

@@ -1040,6 +1040,10 @@ bool IRrecv::decode(decode_results *results, irparams_t *save,
DPRINTLN("Attempting Arris decode");
if (decodeArris(results, offset)) return true;
#endif // DECODE_ARRIS
#if DECODE_RHOSS
DPRINTLN("Attempting Rhoss decode");
if (decodeRhoss(results, offset)) return true;
#endif // DECODE_RHOSS
// Typically new protocols are added above this line.
}
#if DECODE_HASH

View File

@@ -770,6 +770,10 @@ class IRrecv {
bool decodeBose(decode_results *results, uint16_t offset = kStartOffset,
const uint16_t nbits = kBoseBits, const bool strict = true);
#endif // DECODE_BOSE
#if DECODE_RHOSS
bool decodeRhoss(decode_results *results, uint16_t offset = kStartOffset,
const uint16_t nbits = kRhossBits, const bool strict = true);
#endif // DECODE_RHOSS
};
#endif // IRRECV_H_

View File

@@ -797,6 +797,13 @@
#define SEND_ARRIS _IR_ENABLE_DEFAULT_
#endif // SEND_ARRIS
#ifndef DECODE_RHOSS
#define DECODE_RHOSS _IR_ENABLE_DEFAULT_
#endif // DECODE_RHOSS
#ifndef SEND_RHOSS
#define SEND_RHOSS _IR_ENABLE_DEFAULT_
#endif // SEND_RHOSS
#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 || \
@@ -811,7 +818,7 @@
DECODE_HITACHI_AC344 || DECODE_CORONA_AC || DECODE_SANYO_AC || \
DECODE_VOLTAS || DECODE_MIRAGE || DECODE_HAIER_AC176 || \
DECODE_TEKNOPOINT || DECODE_KELON || DECODE_TROTEC_3550 || \
DECODE_SANYO_AC88 || \
DECODE_SANYO_AC88 || DECODE_RHOSS || \
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
@@ -959,8 +966,9 @@ enum decode_type_t {
SANYO_AC88, // 105
BOSE,
ARRIS,
RHOSS,
// Add new entries before this one, and update it to point to the last entry.
kLastDecodeType = ARRIS,
kLastDecodeType = RHOSS,
};
// Message lengths & required repeat values
@@ -1204,6 +1212,9 @@ const uint16_t kMilesTag2ShotBits = 14;
const uint16_t kMilesTag2MsgBits = 24;
const uint16_t kMilesMinRepeat = 0;
const uint16_t kBoseBits = 16;
const uint16_t kRhossStateLength = 12;
const uint16_t kRhossBits = kRhossStateLength * 8;
const uint16_t kRhossDefaultRepeat = 0;
// Legacy defines. (Deprecated)

View File

@@ -739,6 +739,8 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) {
return kNeoclimaBits;
case PANASONIC_AC:
return kPanasonicAcBits;
case RHOSS:
return kRhossBits;
case SAMSUNG_AC:
return kSamsungAcBits;
case SANYO_AC:
@@ -1253,6 +1255,11 @@ bool IRsend::send(const decode_type_t type, const uint8_t *state,
sendPanasonicAC(state, nbytes);
break;
#endif // SEND_PANASONIC_AC
#if SEND_RHOSS
case RHOSS:
sendRhoss(state, nbytes);
break;
#endif // SEND_RHOSS
#if SEND_SAMSUNG_AC
case SAMSUNG_AC:
sendSamsungAC(state, nbytes);

View File

@@ -749,6 +749,11 @@ class IRsend {
static uint32_t toggleArrisRelease(const uint32_t data);
static uint32_t encodeArris(const uint32_t command, const bool release);
#endif // SEND_ARRIS
#if SEND_RHOSS
void sendRhoss(const unsigned char data[],
const uint16_t nbytes = kRhossStateLength,
const uint16_t repeat = kRhossDefaultRepeat);
#endif // SEND_RHOSS
protected:
#ifdef UNIT_TEST

View File

@@ -322,6 +322,7 @@ IRTEXT_CONST_BLOB_DECL(kAllProtocolNamesStr) {
D_STR_SANYO_AC88 "\x0"
D_STR_BOSE "\x0"
D_STR_ARRIS "\x0"
D_STR_RHOSS "\x0"
///< New protocol strings should be added just above this line.
"\x0" ///< This string requires double null termination.
};

View File

@@ -199,6 +199,7 @@ bool hasACState(const decode_type_t protocol) {
case MWM:
case NEOCLIMA:
case PANASONIC_AC:
case RHOSS:
case SAMSUNG_AC:
case SANYO_AC:
case SANYO_AC88:

364
src/ir_Rhoss.cpp Normal file
View File

@@ -0,0 +1,364 @@
// Copyright 2021 Tom Rosenback
/// @file
/// @brief Support for Rhoss protocols.
#include "ir_Rhoss.h"
#include <algorithm>
#include <cstring>
#include "IRrecv.h"
#include "IRsend.h"
#include "IRtext.h"
#include "IRutils.h"
const uint16_t kRhossHdrMark = 3042;
const uint16_t kRhossHdrSpace = 4248;
const uint16_t kRhossBitMark = 648;
const uint16_t kRhossOneSpace = 1545;
const uint16_t kRhossZeroSpace = 457;
const uint32_t kRhossGap = kDefaultMessageGap;
const uint16_t kRhossFreq = 38;
using irutils::addBoolToString;
using irutils::addModeToString;
using irutils::addFanToString;
using irutils::addTempToString;
#if SEND_RHOSS
/// Send a Rhoss HVAC formatted message.
/// Status: STABLE / Reported as 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::sendRhoss(const unsigned char data[], const uint16_t nbytes,
const uint16_t repeat) {
// Check if we have enough bytes to send a proper message.
if (nbytes < kRhossStateLength) return;
// We always send a message, even for repeat=0, hence '<= repeat'.
for (uint16_t r = 0; r <= repeat; r++) {
sendGeneric(kRhossHdrMark, kRhossHdrSpace, kRhossBitMark,
kRhossOneSpace, kRhossBitMark, kRhossZeroSpace,
kRhossBitMark, kRhossZeroSpace,
data, nbytes, kRhossFreq, false, 0, kDutyDefault);
mark(kRhossBitMark);
// Gap
space(kRhossGap);
}
}
#endif // SEND_RHOSS
#if DECODE_RHOSS
/// Decode the supplied Rhoss formatted message.
/// Status: STABLE / Known working.
/// @param[in,out] results Ptr to the data to decode & where to store the result
/// @param[in] offset The starting index to use when attempting to decode the
/// raw data. Typically/Defaults to kStartOffset.
/// @param[in] nbits The number of data bits to expect.
/// @param[in] strict Flag indicating if we should perform strict matching.
bool IRrecv::decodeRhoss(decode_results *results, uint16_t offset,
const uint16_t nbits, const bool strict) {
if (strict && nbits != kRhossBits) return false;
if (results->rawlen <= 2 * nbits + kHeader + kFooter - 1 + offset) {
return false; // Can't possibly be a valid Rhoss message.
}
uint16_t used;
// Header + Data Block (96 bits) + Footer
used = matchGeneric(results->rawbuf + offset, results->state,
results->rawlen - offset, kRhossBits,
kRhossHdrMark, kRhossHdrSpace,
kRhossBitMark, kRhossOneSpace,
kRhossBitMark, kRhossZeroSpace,
kRhossBitMark, kRhossZeroSpace,
false, kUseDefTol, kMarkExcess, false);
if (!used) return false;
offset += used;
// Footer (Part 2)
if (!matchMark(results->rawbuf[offset++], kRhossBitMark)) {
return false;
}
if (offset < results->rawlen &&
!matchAtLeast(results->rawbuf[offset], kRhossGap)) {
return false;
}
if (strict && !IRRhossAc::validChecksum(results->state)) return false;
// Success
results->decode_type = decode_type_t::RHOSS;
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_RHOSS
/// 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?
IRRhossAc::IRRhossAc(const uint16_t pin, const bool inverted,
const bool use_modulation)
: _irsend(pin, inverted, use_modulation) { this->stateReset(); }
/// Set up hardware to be able to send a message.
void IRRhossAc::begin(void) { _irsend.begin(); }
#if SEND_RHOSS
/// Send the current internal state as an IR message.
/// @param[in] repeat Nr. of times the message will be repeated.
void IRRhossAc::send(const uint16_t repeat) {
_irsend.sendRhoss(getRaw(), kRhossStateLength, repeat);
}
#endif // SEND_RHOSS
/// Calculate the checksum for the supplied state.
/// @param[in] state The source state to generate the checksum from.
/// @param[in] length Length of the supplied state to checksum.
/// @return The checksum value.
uint8_t IRRhossAc::calcChecksum(const uint8_t state[], const uint16_t length) {
return sumBytes(state, length - 1);
}
/// Verify the checksum is valid for a given state.
/// @param[in] state The array to verify the checksum of.
/// @param[in] length The size of the state.
/// @return A boolean indicating if it's checksum is valid.
bool IRRhossAc::validChecksum(const uint8_t state[], const uint16_t length) {
return (state[length - 1] == IRRhossAc::calcChecksum(state, length));
}
/// Update the checksum value for the internal state.
void IRRhossAc::checksum(void) {
_.Sum = IRRhossAc::calcChecksum(_.raw, kRhossStateLength);
_.raw[kRhossStateLength - 1] = _.Sum;
}
/// Reset the internals of the object to a known good state.
void IRRhossAc::stateReset(void) {
for (uint8_t i = 1; i < kRhossStateLength; i++) _.raw[i] = 0x0;
_.raw[0] = 0xAA;
_.raw[2] = 0x60;
_.raw[6] = 0x54;
_.Power = kRhossDefaultPower;
_.Fan = kRhossDefaultFan;
_.Mode = kRhossDefaultMode;
_.Swing = kRhossDefaultSwing;
_.Temp = kRhossDefaultTemp - kRhossTempMin;
}
/// Get the raw state of the object, suitable to be sent with the appropriate
/// IRsend object method.
/// @return A PTR to the internal state.
uint8_t* IRRhossAc::getRaw(void) {
checksum(); // Ensure correct bit array before returning
return _.raw;
}
/// Set the raw state of the object.
/// @param[state] state The raw state from the native IR message.
void IRRhossAc::setRaw(const uint8_t state[]) {
std::memcpy(_.raw, state, kRhossStateLength);
}
/// Set the internal state to have the power on.
void IRRhossAc::on(void) { setPower(true); }
/// Set the internal state to have the power off.
void IRRhossAc::off(void) { setPower(false); }
/// Set the internal state to have the desired power.
/// @param[in] on The desired power state.
void IRRhossAc::setPower(const bool on) {
_.Power = (on ? kRhossPowerOn : kRhossPowerOff);
}
/// Get the power setting from the internal state.
/// @return A boolean indicating the power setting.
bool IRRhossAc::getPower(void) const {
return _.Power == kRhossPowerOn;
}
/// Set the temperature.
/// @param[in] degrees The temperature in degrees celsius.
void IRRhossAc::setTemp(const uint8_t degrees) {
uint8_t temp = std::max(kRhossTempMin, degrees);
_.Temp = std::min(kRhossTempMax, temp) - kRhossTempMin;
}
/// Get the current temperature setting.
/// @return Get current setting for temp. in degrees celsius.
uint8_t IRRhossAc::getTemp(void) const {
return _.Temp + kRhossTempMin;
}
/// Set the speed of the fan.
/// @param[in] speed The desired setting.
void IRRhossAc::setFan(const uint8_t speed) {
switch (speed) {
case kRhossFanAuto:
case kRhossFanMin:
case kRhossFanMed:
case kRhossFanMax:
_.Fan = speed;
break;
default:
_.Fan = kRhossFanAuto;
}
}
/// Get the current fan speed setting.
/// @return The current fan speed.
uint8_t IRRhossAc::getFan(void) const {
return _.Fan;
}
/// Set the Vertical Swing mode of the A/C.
/// @param[in] state true, the Swing is on. false, the Swing is off.
void IRRhossAc::setSwing(const bool state) {
_.Swing = state;
}
/// Get the Vertical Swing speed of the A/C.
/// @return The native swing speed setting.
uint8_t IRRhossAc::getSwing(void) const {
return _.Swing;
}
/// Get the current operation mode setting.
/// @return The current operation mode.
uint8_t IRRhossAc::getMode(void) const {
return _.Mode;
}
/// Set the desired operation mode.
/// @param[in] mode The desired operation mode.
void IRRhossAc::setMode(const uint8_t mode) {
switch (mode) {
case kRhossModeFan:
case kRhossModeCool:
case kRhossModeDry:
case kRhossModeHeat:
case kRhossModeAuto:
_.Mode = mode;
return;
default:
_.Mode = kRhossDefaultMode;
break;
}
}
/// 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 IRRhossAc::convertMode(const stdAc::opmode_t mode) {
switch (mode) {
case stdAc::opmode_t::kCool:
return kRhossModeCool;
case stdAc::opmode_t::kHeat:
return kRhossModeHeat;
case stdAc::opmode_t::kDry:
return kRhossModeDry;
case stdAc::opmode_t::kFan:
return kRhossModeFan;
case stdAc::opmode_t::kAuto:
return kRhossModeAuto;
default:
return kRhossDefaultMode;
}
}
/// 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 IRRhossAc::convertFan(const stdAc::fanspeed_t speed) {
switch (speed) {
case stdAc::fanspeed_t::kMin:
case stdAc::fanspeed_t::kLow:
return kRhossFanMin;
case stdAc::fanspeed_t::kMedium:
return kRhossFanMed;
case stdAc::fanspeed_t::kHigh:
case stdAc::fanspeed_t::kMax:
return kRhossFanMax;
default:
return kRhossDefaultFan;
}
}
/// 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 IRRhossAc::toCommonMode(const uint8_t mode) {
switch (mode) {
case kRhossModeCool: return stdAc::opmode_t::kCool;
case kRhossModeHeat: return stdAc::opmode_t::kHeat;
case kRhossModeDry: return stdAc::opmode_t::kDry;
case kRhossModeFan: return stdAc::opmode_t::kFan;
case kRhossModeAuto: return stdAc::opmode_t::kAuto;
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 IRRhossAc::toCommonFanSpeed(const uint8_t speed) {
switch (speed) {
case kRhossFanMax: return stdAc::fanspeed_t::kMax;
case kRhossFanMed: return stdAc::fanspeed_t::kMedium;
case kRhossFanMin: return stdAc::fanspeed_t::kMin;
case kRhossFanAuto:
default:
return stdAc::fanspeed_t::kAuto;
}
}
/// Convert the current internal state into its stdAc::state_t equivalent.
/// @return The stdAc equivalent of the native settings.
stdAc::state_t IRRhossAc::toCommon(void) const {
stdAc::state_t result;
result.protocol = decode_type_t::RHOSS;
result.power = getPower();
result.mode = toCommonMode(_.Mode);
result.celsius = true;
result.degrees = _.Temp;
result.fanspeed = toCommonFanSpeed(_.Fan);
result.swingv = _.Swing ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff;
// Not supported.
result.model = -1;
result.turbo = false;
result.swingh = stdAc::swingh_t::kOff;
result.light = false;
result.filter = false;
result.econo = false;
result.quiet = false;
result.clean = false;
result.beep = false;
result.sleep = -1;
result.clock = -1;
return result;
}
/// Convert the current internal state into a human readable string.
/// @return A human readable string.
String IRRhossAc::toString(void) const {
String result = "";
result.reserve(70); // Reserve some heap for the string to reduce fragging.
result += addBoolToString(getPower(), kPowerStr, false);
result += addModeToString(getMode(), kRhossModeAuto, kRhossModeCool,
kRhossModeHeat, kRhossModeDry, kRhossModeFan);
result += addTempToString(getTemp());
result += addFanToString(getFan(), kRhossFanMax, kRhossFanMin,
kRhossFanAuto, kRhossFanAuto,
kRhossFanMed);
result += addBoolToString(getSwing(), kSwingVStr);
return result;
}

145
src/ir_Rhoss.h Normal file
View File

@@ -0,0 +1,145 @@
// Copyright 2021 Tom Rosenback
/// @file
/// @brief Support for Rhoss A/C protocol
// Supports:
// Brand: Rhoss, Model: Idrowall MPCV 20-30-35-40
#ifndef IR_RHOSS_H_
#define IR_RHOSS_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
/// Native representation of a Rhoss A/C message.
union RhossProtocol{
uint8_t raw[kRhossStateLength]; // The state of the IR remote.
struct {
// Byte 0
uint8_t :8; // Typically 0xAA
// Byte 1
uint8_t Temp :4;
uint8_t :4; // Typically 0x0
// Byte 2
uint8_t :8; // Typically 0x60
// Byte 3
uint8_t :8; // Typically 0x0
// Byte 4
uint8_t Fan :2;
uint8_t :2; // Typically 0x0
uint8_t Mode :4;
// Byte 5
uint8_t Swing :1;
uint8_t :5; // Typically 0x0
uint8_t Power :2;
// Byte 6
uint8_t :8; // Typically 0x54
// Byte 7
uint8_t :8; // Typically 0x0
// Byte 8
uint8_t :8; // Typically 0x0
// Byte 9
uint8_t :8; // Typically 0x0
// Byte 10
uint8_t :8; // Typically 0x0
// Byte 11
uint8_t Sum :8;
};
};
// Constants
// Fan Control
const uint8_t kRhossFanAuto = 0b00;
const uint8_t kRhossFanMin = 0b01;
const uint8_t kRhossFanMed = 0b10;
const uint8_t kRhossFanMax = 0b11;
// Modes
const uint8_t kRhossModeHeat = 0b0001;
const uint8_t kRhossModeCool = 0b0010;
const uint8_t kRhossModeDry = 0b0011;
const uint8_t kRhossModeFan = 0b0100;
const uint8_t kRhossModeAuto = 0b0101;
// Temperature
const uint8_t kRhossTempMin = 16; // Celsius
const uint8_t kRhossTempMax = 30; // Celsius
// Power
const uint8_t kRhossPowerOn = 0b10; // 0x2
const uint8_t kRhossPowerOff = 0b01; // 0x1
// Swing
const uint8_t kRhossSwingOn = 0b1; // 0x1
const uint8_t kRhossSwingOff = 0b0; // 0x0
const uint8_t kRhossDefaultFan = kRhossFanAuto;
const uint8_t kRhossDefaultMode = kRhossModeCool;
const uint8_t kRhossDefaultTemp = 21; // Celsius
const bool kRhossDefaultPower = false;
const bool kRhossDefaultSwing = false;
// Classes
/// Class for handling detailed Rhoss A/C messages.
class IRRhossAc {
public:
explicit IRRhossAc(const uint16_t pin, const bool inverted = false,
const bool use_modulation = true);
void stateReset();
#if SEND_RHOSS
void send(const uint16_t repeat = kRhossDefaultRepeat);
/// 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_RHOSS
void begin();
static uint8_t calcChecksum(const uint8_t state[],
const uint16_t length = kRhossStateLength);
static bool validChecksum(const uint8_t state[],
const uint16_t length = kRhossStateLength);
void setPower(const bool state);
bool getPower(void) const;
void on(void);
void off(void);
void setTemp(const uint8_t temp);
uint8_t getTemp(void) const;
void setFan(const uint8_t speed);
uint8_t getFan(void) const;
void setSwing(const bool state);
uint8_t getSwing(void) const;
void setMode(const uint8_t mode);
uint8_t getMode(void) const;
uint8_t* getRaw(void);
void setRaw(const uint8_t state[]);
static uint8_t convertMode(const stdAc::opmode_t mode);
static 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) const;
String toString(void) const;
#ifndef UNIT_TEST
private:
IRsend _irsend;
#else
/// @cond IGNORE
IRsendTest _irsend;
/// @endcond
#endif
RhossProtocol _;
void checksum(void);
};
#endif // IR_RHOSS_H_

View File

@@ -748,6 +748,9 @@
#ifndef D_STR_RCMM
#define D_STR_RCMM "RCMM"
#endif // D_STR_RCMM
#ifndef D_STR_RHOSS
#define D_STR_RHOSS "RHOSS"
#endif // D_STR_RHOSS
#ifndef D_STR_SAMSUNG
#define D_STR_SAMSUNG "SAMSUNG"
#endif // D_STR_SAMSUNG

View File

@@ -4,7 +4,7 @@
# make TARGET - makes the given target.
# make run - makes everything and runs all the tests.
# make run_tests - run all tests
# make run_% - run specific test file (exclude _test.cpp)
# make run-% - run specific test file (exclude _test.cpp)
# replace % with given test file
# make clean - removes all files generated by make.
# make install-googletest - install the googletest code suite
@@ -59,7 +59,7 @@ run : all
run_tests : run
run_% :
run-% : all
echo "RUNNING: $*"; \
./$*

396
test/ir_Rhoss_test.cpp Normal file
View File

@@ -0,0 +1,396 @@
// Copyright 2021 Tom Rosenback
#include "IRac.h"
#include "ir_Rhoss.h"
#include "IRrecv.h"
#include "IRrecv_test.h"
#include "IRsend.h"
#include "IRsend_test.h"
#include "IRutils.h"
#include "gtest/gtest.h"
TEST(TestUtils, Housekeeping) {
ASSERT_EQ("RHOSS", typeToString(decode_type_t::RHOSS));
ASSERT_EQ(decode_type_t::RHOSS, strToDecodeType("RHOSS"));
ASSERT_TRUE(hasACState(decode_type_t::RHOSS));
ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::RHOSS));
}
// Test sending typical data only.
TEST(TestSendRhoss, SendDataOnly) {
IRsendTest irsend(kGpioUnused);
irsend.begin();
uint8_t expectedState[kRhossStateLength] = {
0xAA, 0x05, 0x60, 0x00, 0x50, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x33 };
irsend.reset();
irsend.sendRhoss(expectedState);
EXPECT_EQ(
"f38000d50"
"m3042s4248"
"m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457m648s1545"
"m648s1545m648s457m648s1545m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s1545m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s1545"
"m648s457m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s1545m648s1545m648s457m648s457m648s1545m648s1545m648s457m648s457"
"m648s457m648"
"s100000",
irsend.outputStr());
}
// Test send typical data with repeats
TEST(TestSendRhoss, SendWithRepeats) {
IRsendTest irsend(kGpioUnused);
irsend.begin();
irsend.reset();
uint8_t expectedState[kRhossStateLength] = {
0xAA, 0x05, 0x60, 0x00, 0x50, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x33 };
irsend.sendRhoss(expectedState, kRhossStateLength, 0); // 0 repeats.
EXPECT_EQ(
"f38000d50"
"m3042s4248"
"m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457m648s1545"
"m648s1545m648s457m648s1545m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s1545m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s1545"
"m648s457m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s1545m648s1545m648s457m648s457m648s1545m648s1545m648s457m648s457"
"m648s457m648"
"s100000",
irsend.outputStr());
irsend.sendRhoss(expectedState, kRhossStateLength, 2); // 2 repeats.
EXPECT_EQ(
"f38000d50"
"m3042s4248"
"m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457m648s1545"
"m648s1545m648s457m648s1545m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s1545m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s1545"
"m648s457m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s1545m648s1545m648s457m648s457m648s1545m648s1545m648s457m648s457"
"m648s457m648"
"s100000"
"m3042s4248"
"m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457m648s1545"
"m648s1545m648s457m648s1545m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s1545m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s1545"
"m648s457m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s1545m648s1545m648s457m648s457m648s1545m648s1545m648s457m648s457"
"m648s457m648"
"s100000"
"m3042s4248"
"m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457m648s1545"
"m648s1545m648s457m648s1545m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s1545m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s1545"
"m648s457m648s457m648s1545m648s457m648s1545m648s457m648s1545m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s457m648s457m648s457m648s457m648s457m648s457m648s457m648s457"
"m648s1545m648s1545m648s457m648s457m648s1545m648s1545m648s457m648s457"
"m648s457m648"
"s100000",
irsend.outputStr());
}
// Test send raw data
TEST(TestSendRhoss, RawData) {
IRsendTest irsend(kGpioUnused);
IRrecv irrecv(kGpioUnused);
irsend.begin();
irsend.reset();
// Power on, mode cool, temp 20, fan auto, swing off
const uint16_t rawData[197] = {
3044, 4248,
648, 458, 650, 1540, 646, 458, 650, 1538,
650, 458, 650, 1538, 650, 458, 650, 1540, // byte 0
648, 458, 650, 458, 650, 1540, 646, 484,
624, 456, 650, 456, 650, 456, 650, 456, // byte 1
650, 456, 650, 456, 650, 456, 650, 456,
650, 458, 650, 1540, 650, 1538, 650, 456, // byte 2
650, 456, 650, 456, 650, 456, 650, 458,
650, 456, 650, 456, 650, 456, 650, 458, // byte 3
650, 458, 650, 456, 650, 458, 650, 458,
650, 458, 650, 1538, 650, 458, 650, 458, // byte 4
650, 458, 648, 458, 674, 434, 648, 458,
672, 434, 648, 458, 650, 458, 648, 1540, // byte 5
672, 434, 650, 458, 672, 1518, 644, 488,
622, 1540, 644, 464, 672, 1516, 672, 434, // byte 6
672, 434, 672, 434, 650, 458, 648, 458,
672, 434, 674, 434, 672, 434, 650, 458, // byte 7
672, 434, 648, 458, 650, 458, 672, 434,
672, 436, 648, 458, 648, 456, 650, 458, // byte 8
650, 458, 650, 456, 674, 434, 650, 458,
650, 456, 650, 458, 674, 432, 650, 458, // byte 9
650, 456, 650, 456, 650, 458, 648, 458,
674, 432, 650, 456, 674, 434, 650, 458, // byte 10
650, 458, 650, 1538, 650, 458, 650, 458,
650, 456, 650, 458, 650, 456, 650, 458, // byte 11
650, 456,
650 }; // UNKNOWN 93E7BDB2
irsend.sendRaw(rawData, 197, 38);
irsend.makeDecodeResult();
ASSERT_TRUE(irrecv.decode(&irsend.capture));
ASSERT_EQ(RHOSS, irsend.capture.decode_type);
EXPECT_EQ(kRhossBits, irsend.capture.bits);
uint8_t expected[kRhossStateLength] = {
0xAA, 0x04, 0x60, 0x00, 0x20, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x02 };
EXPECT_STATE_EQ(expected, irsend.capture.state, kRhossBits);
EXPECT_EQ(
"Power: On, Mode: 2 (Cool), Temp: 20C, Fan: 0 (Auto), Swing(V): Off",
IRAcUtils::resultAcToString(&irsend.capture));
stdAc::state_t r, p;
ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p));
}
// Test synthetic decode
TEST(TestDecodeRhoss, SyntheticSelfDecode) {
IRsendTest irsend(kGpioUnused);
IRrecv irrecv(0);
IRRhossAc ac(0);
uint8_t expectedState[kRhossStateLength] = {
0xAA, 0x05, 0x60, 0x00, 0x50, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x33 };
irsend.begin();
irsend.reset();
irsend.sendRhoss(expectedState);
irsend.makeDecodeResult();
ASSERT_TRUE(irrecv.decode(&irsend.capture));
EXPECT_EQ(RHOSS, irsend.capture.decode_type);
EXPECT_EQ(kRhossBits, irsend.capture.bits);
EXPECT_STATE_EQ(expectedState, irsend.capture.state, irsend.capture.bits);
ac.setRaw(irsend.capture.state);
EXPECT_EQ(
"Power: On, Mode: 5 (Auto), Temp: 21C, Fan: 0 (Auto), Swing(V): Off",
ac.toString());
}
// Test strict decoding
TEST(TestDecodeRhoss, StrictDecode) {
IRsendTest irsend(kGpioUnused);
IRrecv irrecv(0);
IRRhossAc ac(0);
uint8_t expectedState[kRhossStateLength] = {
0xAA, 0x05, 0x60, 0x00, 0x50, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x33 };
irsend.begin();
irsend.reset();
irsend.sendRhoss(expectedState);
irsend.makeDecodeResult();
ASSERT_TRUE(
irrecv.decodeRhoss(&irsend.capture,
kStartOffset, kRhossBits, true));
EXPECT_EQ(RHOSS, irsend.capture.decode_type);
EXPECT_EQ(kRhossBits, irsend.capture.bits);
EXPECT_STATE_EQ(expectedState, irsend.capture.state, irsend.capture.bits);
ac.setRaw(irsend.capture.state);
EXPECT_EQ(
"Power: On, Mode: 5 (Auto), Temp: 21C, Fan: 0 (Auto), Swing(V): Off",
ac.toString());
}
// Tests for IRRhossAc class.
TEST(TestRhossAcClass, Power) {
IRRhossAc ac(0);
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());
}
TEST(TestRhossAcClass, Temperature) {
IRRhossAc ac(0);
ac.begin();
ac.setTemp(0);
EXPECT_EQ(kRhossTempMin, ac.getTemp());
ac.setTemp(255);
EXPECT_EQ(kRhossTempMax, ac.getTemp());
ac.setTemp(kRhossTempMin);
EXPECT_EQ(kRhossTempMin, ac.getTemp());
ac.setTemp(kRhossTempMax);
EXPECT_EQ(kRhossTempMax, ac.getTemp());
ac.setTemp(kRhossTempMin - 1);
EXPECT_EQ(kRhossTempMin, ac.getTemp());
ac.setTemp(kRhossTempMax + 1);
EXPECT_EQ(kRhossTempMax, ac.getTemp());
ac.setTemp(17);
EXPECT_EQ(17, ac.getTemp());
ac.setTemp(21);
EXPECT_EQ(21, ac.getTemp());
ac.setTemp(25);
EXPECT_EQ(25, ac.getTemp());
ac.setTemp(29);
EXPECT_EQ(29, ac.getTemp());
}
TEST(TestRhossAcClass, OperatingMode) {
IRRhossAc ac(0);
ac.begin();
ac.setMode(kRhossModeAuto);
EXPECT_EQ(kRhossModeAuto, ac.getMode());
ac.setMode(kRhossModeCool);
EXPECT_EQ(kRhossModeCool, ac.getMode());
ac.setMode(kRhossModeHeat);
EXPECT_EQ(kRhossModeHeat, ac.getMode());
ac.setMode(kRhossModeDry);
EXPECT_EQ(kRhossModeDry, ac.getMode());
ac.setMode(kRhossModeFan);
EXPECT_EQ(kRhossModeFan, ac.getMode());
ac.setMode(kRhossModeAuto + 1);
EXPECT_EQ(kRhossDefaultMode, ac.getMode());
ac.setMode(255);
EXPECT_EQ(kRhossDefaultMode, ac.getMode());
}
TEST(TestRhossAcClass, FanSpeed) {
IRRhossAc ac(0);
ac.begin();
ac.setFan(0);
EXPECT_EQ(kRhossFanAuto, ac.getFan());
ac.setFan(255);
EXPECT_EQ(kRhossFanAuto, ac.getFan());
ac.setFan(kRhossFanMax);
EXPECT_EQ(kRhossFanMax, ac.getFan());
ac.setFan(kRhossFanMax + 1);
EXPECT_EQ(kRhossFanAuto, ac.getFan());
ac.setFan(kRhossFanMax - 1);
EXPECT_EQ(kRhossFanMax - 1, ac.getFan());
ac.setFan(1);
EXPECT_EQ(1, ac.getFan());
ac.setFan(1);
EXPECT_EQ(1, ac.getFan());
ac.setFan(3);
EXPECT_EQ(3, ac.getFan());
}
TEST(TestRhossAcClass, Swing) {
IRRhossAc ac(0);
ac.begin();
ac.setSwing(false);
EXPECT_FALSE(ac.getSwing());
ac.setSwing(true);
EXPECT_TRUE(ac.getSwing());
}
TEST(TestRhossAcClass, Checksums) {
uint8_t state[kRhossStateLength] = {
0xAA, 0x05, 0x60, 0x00, 0x50, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x33 };
ASSERT_EQ(0x33, IRRhossAc::calcChecksum(state));
EXPECT_TRUE(IRRhossAc::validChecksum(state));
// Change the array so the checksum is invalid.
state[0] ^= 0xFF;
EXPECT_FALSE(IRRhossAc::validChecksum(state));
// Restore the previous change, and change another byte.
state[0] ^= 0xFF;
state[4] ^= 0xFF;
EXPECT_FALSE(IRRhossAc::validChecksum(state));
state[4] ^= 0xFF;
EXPECT_TRUE(IRRhossAc::validChecksum(state));
// Additional known good states.
uint8_t knownGood1[kRhossStateLength] = {
0xAA, 0x06, 0x60, 0x00, 0x50, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x34 };
EXPECT_TRUE(IRRhossAc::validChecksum(knownGood1));
ASSERT_EQ(0x34, IRRhossAc::calcChecksum(knownGood1));
uint8_t knownGood2[kRhossStateLength] = {
0xAA, 0x07, 0x60, 0x00, 0x50, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x35 };
EXPECT_TRUE(IRRhossAc::validChecksum(knownGood2));
ASSERT_EQ(0x35, IRRhossAc::calcChecksum(knownGood2));
uint8_t knownGood3[kRhossStateLength] = {
0xAA, 0x07, 0x60, 0x00, 0x53, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x38 };
EXPECT_TRUE(IRRhossAc::validChecksum(knownGood3));
ASSERT_EQ(0x38, IRRhossAc::calcChecksum(knownGood3));
// Validate calculation of checksum,
// same as knownGood3 except for the checksum.
uint8_t knownBad[kRhossStateLength] = {
0xAA, 0x07, 0x60, 0x00, 0x53, 0x80, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00 };
EXPECT_FALSE(IRRhossAc::validChecksum(knownBad));
IRRhossAc ac(0);
ac.setRaw(knownBad);
EXPECT_STATE_EQ(knownGood3, ac.getRaw(), kRhossBits);
}

View File

@@ -3,7 +3,7 @@
# make [all] - makes everything.
# make TARGET - makes the given target.
# make run_tests - makes everything and runs all test
# make run_% - run specific test file (exclude .py)
# make run-% - run specific test file (exclude .py)
# replace % with given test file
# make clean - removes all files generated by make.
@@ -41,7 +41,7 @@ run_tests : all
echo "PASS: \o/ \o/ All unit tests passed. \o/ \o/"; \
fi
run_% : all
run-% : all
echo "RUNNING: $*"; \
python3 ./$*.py;