Skip to content

Commit

Permalink
Support for XMP (Xfinity) protocol. (#1422)
Browse files Browse the repository at this point in the history
* Add `sendXmp()` & `decodeXmp()`.
  - Add `IRrecv::matchMarkRange()` & `IRrecv::matchSpaceRange()` to support the new protocol.
* Support checksum verification and calculation.
* Correctly identify potential XMP repeat messages.
* Use corrected values for an XMP message when sending a repeat.
* Unit test coverage.
  - Decode real example.
  - Self decode.
  - Decode & detect a real repeat message.
  - Sending repeat messages.
  - Housekeeping.

Fixes #1414
  • Loading branch information
crankyoldgit committed Mar 1, 2021
1 parent 0d2dec3 commit e27cf2f
Show file tree
Hide file tree
Showing 9 changed files with 444 additions and 1 deletion.
44 changes: 44 additions & 0 deletions src/IRrecv.cpp
Expand Up @@ -996,6 +996,10 @@ bool IRrecv::decode(decode_results *results, irparams_t *save,
if (decodeEcoclim(results, offset, kEcoclimBits) ||
decodeEcoclim(results, offset, kEcoclimShortBits)) return true;
#endif // DECODE_ECOCLIM
#if DECODE_XMP
DPRINTLN("Attempting XMP decode");
if (decodeXmp(results, offset, kXmpBits)) return true;
#endif // DECODE_XMP
// Typically new protocols are added above this line.
}
#if DECODE_HASH
Expand Down Expand Up @@ -1134,6 +1138,26 @@ bool IRrecv::matchMark(uint32_t measured, uint32_t desired, uint8_t tolerance,
return match(measured, desired + excess, tolerance);
}

/// Check if we match a mark signal(measured) with the desired within a
/// range (in uSeconds) either side of the desired, after an expected is excess
/// is added.
/// @param[in] measured The recorded period of the signal pulse.
/// @param[in] desired The expected period (in usecs) we are matching against.
/// @param[in] range The range limit from desired to accept in uSeconds.
/// @param[in] excess A non-scaling amount to reduce usecs by.
/// @return A Boolean. true if it matches, false if it doesn't.
bool IRrecv::matchMarkRange(const uint32_t measured, const uint32_t desired,
const uint16_t range, const int16_t excess) {
DPRINT("Matching MARK ");
DPRINT(measured * kRawTick);
DPRINT(" vs ");
DPRINT(desired);
DPRINT(" + ");
DPRINT(excess);
DPRINT(". ");
return match(measured, desired + excess, 0, range);
}

/// Check if we match a space signal(measured) with the desired within
/// +/-tolerance percent, after an expected is excess is removed.
/// @param[in] measured The recorded period of the signal pulse.
Expand All @@ -1153,6 +1177,26 @@ bool IRrecv::matchSpace(uint32_t measured, uint32_t desired, uint8_t tolerance,
return match(measured, desired - excess, tolerance);
}

/// Check if we match a space signal(measured) with the desired within a
/// range (in uSeconds) either side of the desired, after an expected is excess
/// is removed.
/// @param[in] measured The recorded period of the signal pulse.
/// @param[in] desired The expected period (in usecs) we are matching against.
/// @param[in] range The range limit from desired to accept in uSeconds.
/// @param[in] excess A non-scaling amount to reduce usecs by.
/// @return A Boolean. true if it matches, false if it doesn't.
bool IRrecv::matchSpaceRange(const uint32_t measured, const uint32_t desired,
const uint16_t range, const int16_t excess) {
DPRINT("Matching SPACE ");
DPRINT(measured * kRawTick);
DPRINT(" vs ");
DPRINT(desired);
DPRINT(" - ");
DPRINT(excess);
DPRINT(". ");
return match(measured, desired - excess, 0, range);
}

#if DECODE_HASH
/// Compare two tick values.
/// @param[in] oldval Nr. of ticks.
Expand Down
10 changes: 10 additions & 0 deletions src/IRrecv.h
Expand Up @@ -141,9 +141,15 @@ class IRrecv {
bool matchMark(const uint32_t measured, const uint32_t desired,
const uint8_t tolerance = kUseDefTol,
const int16_t excess = kMarkExcess);
bool matchMarkRange(const uint32_t measured, const uint32_t desired,
const uint16_t range = 100,
const int16_t excess = kMarkExcess);
bool matchSpace(const uint32_t measured, const uint32_t desired,
const uint8_t tolerance = kUseDefTol,
const int16_t excess = kMarkExcess);
bool matchSpaceRange(const uint32_t measured, const uint32_t desired,
const uint16_t range = 100,
const int16_t excess = kMarkExcess);
#ifndef UNIT_TEST

private:
Expand Down Expand Up @@ -719,6 +725,10 @@ class IRrecv {
const uint16_t nbits = kEcoclimBits,
const bool strict = true);
#endif // DECODE_ECOCLIM
#if DECODE_XMP
bool decodeXmp(decode_results *results, uint16_t offset = kStartOffset,
const uint16_t nbits = kXmpBits, const bool strict = true);
#endif // DECODE_XMP
};

#endif // IRRECV_H_
11 changes: 10 additions & 1 deletion src/IRremoteESP8266.h
Expand Up @@ -733,6 +733,13 @@
#define SEND_ECOCLIM _IR_ENABLE_DEFAULT_
#endif // SEND_ECOCLIM

#ifndef DECODE_XMP
#define DECODE_XMP _IR_ENABLE_DEFAULT_
#endif // DECODE_XMP
#ifndef SEND_XMP
#define SEND_XMP _IR_ENABLE_DEFAULT_
#endif // SEND_XMP

#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 || \
Expand Down Expand Up @@ -883,8 +890,9 @@ enum decode_type_t {
PANASONIC_AC32,
MILESTAG2,
ECOCLIM,
XMP,
// Add new entries before this one, and update it to point to the last entry.
kLastDecodeType = ECOCLIM,
kLastDecodeType = XMP,
};

// Message lengths & required repeat values
Expand Down Expand Up @@ -1108,6 +1116,7 @@ const uint16_t kWhirlpoolAcBits = kWhirlpoolAcStateLength * 8;
const uint16_t kWhirlpoolAcDefaultRepeat = kNoRepeat;
const uint16_t kWhynterBits = 32;
const uint8_t kVestelAcBits = 56;
const uint16_t kXmpBits = 64;
const uint16_t kZepealBits = 16;
const uint16_t kZepealMinRepeat = 4;
const uint16_t kVoltasBits = 80;
Expand Down
7 changes: 7 additions & 0 deletions src/IRsend.cpp
Expand Up @@ -747,6 +747,8 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) {
return kVoltasBits;
case WHIRLPOOL_AC:
return kWhirlpoolAcBits;
case XMP:
return kXmpBits;
// No default amount of bits.
case FUJITSU_AC:
case MWM:
Expand Down Expand Up @@ -1030,6 +1032,11 @@ bool IRsend::send(const decode_type_t type, const uint64_t data,
sendWhynter(data, nbits, min_repeat);
break;
#endif
#if SEND_XMP
case XMP:
sendXmp(data, nbits, min_repeat);
break;
#endif
#if SEND_ZEPEAL
case ZEPEAL:
sendZepeal(data, nbits, min_repeat);
Expand Down
4 changes: 4 additions & 0 deletions src/IRsend.h
Expand Up @@ -698,6 +698,10 @@ class IRsend {
void sendEcoclim(const uint64_t data, const uint16_t nbits = kEcoclimBits,
const uint16_t repeat = kNoRepeat);
#endif // SEND_ECOCLIM
#if SEND_XMP
void sendXmp(const uint64_t data, const uint16_t nbits = kXmpBits,
const uint16_t repeat = kNoRepeat);
#endif // SEND_XMP

protected:
#ifdef UNIT_TEST
Expand Down
1 change: 1 addition & 0 deletions src/IRtext.cpp
Expand Up @@ -281,5 +281,6 @@ const PROGMEM char *kAllProtocolNamesStr =
D_STR_PANASONIC_AC32 "\x0"
D_STR_MILESTAG2 "\x0"
D_STR_ECOCLIM "\x0"
D_STR_XMP "\x0"
///< New protocol strings should be added just above this line.
"\x0"; ///< This string requires double null termination.
226 changes: 226 additions & 0 deletions src/ir_Xmp.cpp
@@ -0,0 +1,226 @@
// Copyright 2021 David Conran

/// @file
/// @brief Support for XMP protocols.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1414
/// @see http://www.hifi-remote.com/wiki/index.php/XMP

// Supports:
// Brand: Xfinity, Model: XR2 remote
// Brand: Xfinity, Model: XR11 remote


#include <algorithm>
#include "IRrecv.h"
#include "IRsend.h"
#include "IRutils.h"

// Constants
const uint16_t kXmpMark = 210; ///< uSeconds.
const uint16_t kXmpBaseSpace = 760; ///< uSeconds
const uint16_t kXmpSpaceStep = 135; ///< uSeconds
const uint16_t kXmpFooterSpace = 13000; ///< uSeconds.
const uint32_t kXmpMessageGap = 80400; ///< uSeconds.
const uint8_t kXmpWordSize = kNibbleSize; ///< nr. of Bits in a word.
const uint8_t kXmpMaxWordValue = (1 << kXmpWordSize) - 1; // Max word value.
const uint8_t kXmpSections = 2; ///< Nr. of Data sections
const uint8_t kXmpRepeatCode = 0b1000;
const uint8_t kXmpRepeatCodeAlt = 0b1001;

using irutils::setBits;

namespace IRXmpUtils {
/// Get the current checksum value from an XMP data section.
/// @param[in] data The value of the data section.
/// @param[in] nbits The number of data bits in the section.
/// @return The value of the stored checksum.
/// @warning Returns 0 if we can't obtain a valid checksum.
uint8_t getSectionChecksum(const uint32_t data, const uint16_t nbits) {
// The checksum is the 2nd most significant nibble of a section.
return (nbits < 2 * kNibbleSize) ? 0 : GETBITS32(data,
nbits - (2 * kNibbleSize),
kNibbleSize);
}

/// Calculate the correct checksum value for an XMP data section.
/// @param[in] data The value of the data section.
/// @param[in] nbits The number of data bits in the section.
/// @return The value of the correct checksum.
uint8_t calcSectionChecksum(const uint32_t data, const uint16_t nbits) {
return (0xF & ~(irutils::sumNibbles(data, nbits / kNibbleSize, 0xF, false) -
getSectionChecksum(data, nbits)));
}

/// Recalculate a XMP message code ensuring it has the checksums valid.
/// @param[in] data The value of the XMP message code.
/// @param[in] nbits The number of data bits in the entire message code.
/// @return The corrected XMP message with valid checksum sections.
uint64_t updateChecksums(const uint64_t data, const uint16_t nbits) {
const uint16_t sectionbits = nbits / kXmpSections;
uint64_t result = data;
for (uint16_t sectionOffset = 0; sectionOffset < nbits;
sectionOffset += sectionbits) {
const uint16_t checksumOffset = sectionOffset + sectionbits -
(2 * kNibbleSize);
setBits(&result, checksumOffset, kNibbleSize,
calcSectionChecksum(GETBITS64(data, sectionOffset, sectionbits),
sectionbits));
}
return result;
}

/// Calculate the bit offset the repeat nibble in an XMP code.
/// @param[in] nbits The number of data bits in the entire message code.
/// @return The offset to the start of the XMP repeat nibble.
uint16_t calcRepeatOffset(const uint16_t nbits) {
return (nbits < 3 * kNibbleSize) ? 0
: (nbits / kXmpSections) -
(3 * kNibbleSize);
}

/// Test if an XMP message code is a repeat or not.
/// @param[in] data The value of the XMP message code.
/// @param[in] nbits The number of data bits in the entire message code.
/// @return true, if it looks like a repeat, false if not.
bool isRepeat(const uint64_t data, const uint16_t nbits) {
switch (GETBITS64(data, calcRepeatOffset(nbits), kNibbleSize)) {
case kXmpRepeatCode:
case kXmpRepeatCodeAlt:
return true;
default:
return false;
}
}

/// Adjust an XMP message code to make it a valid repeat or non-repeat code.
/// @param[in] data The value of the XMP message code.
/// @param[in] nbits The number of data bits in the entire message code.
/// @param[in] repeat_code The value of the XMP repeat nibble to use.
/// A value of `8` is the normal value for a repeat. `9` has also been seen.
/// A value of `0` will convert the code to a non-repeat code.
/// @return The valud of the modified XMP code.
uint64_t adjustRepeat(const uint64_t data, const uint16_t nbits,
const uint8_t repeat_code) {
uint64_t result = data;
setBits(&result, calcRepeatOffset(nbits), kNibbleSize, repeat_code);
return updateChecksums(result, nbits);
}
} // namespace IRXmpUtils

using IRXmpUtils::calcSectionChecksum;
using IRXmpUtils::getSectionChecksum;
using IRXmpUtils::isRepeat;
using IRXmpUtils::adjustRepeat;


#if SEND_XMP
/// Send a XMP packet.
/// Status: Beta / Untested against a real device.
/// @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::sendXmp(const uint64_t data, const uint16_t nbits,
const uint16_t repeat) {
enableIROut(38000);
if (nbits < 2 * kXmpWordSize) return; // Too small to send, abort!
uint64_t send_data = data;
for (uint16_t r = 0; r <= repeat; r++) {
uint16_t bits_so_far = kXmpWordSize;
for (uint64_t mask = ((uint64_t)kXmpMaxWordValue) << (nbits - kXmpWordSize);
mask;
mask >>= kXmpWordSize) {
uint8_t word = (send_data & mask) >> (nbits - bits_so_far);
mark(kXmpMark);
space(kXmpBaseSpace + word * kXmpSpaceStep);
bits_so_far += kXmpWordSize;
// Are we at a data section boundary?
if ((bits_so_far - kXmpWordSize) % (nbits / kXmpSections) == 0) { // Yes.
mark(kXmpMark);
space(kXmpFooterSpace);
}
}
space(kXmpMessageGap - kXmpFooterSpace);

// Modify the value if needed, to make it into a valid repeat code.
if (!isRepeat(send_data, nbits))
send_data = adjustRepeat(send_data, nbits, kXmpRepeatCode);
}
}
#endif // SEND_XMP

#if DECODE_XMP
/// Decode the supplied XMP packet/message.
/// Status: BETA / Probably works.
/// @param[in,out] results Ptr to the data to decode & where to store the result
/// @param[in] offset The starting index to use when attempting to decode the
/// raw data. Typically/Defaults to kStartOffset.
/// @param[in] nbits The number of data bits to expect.
/// @param[in] strict Flag indicating if we should perform strict matching.
/// @return True if it can decode it, false if it can't.
bool IRrecv::decodeXmp(decode_results *results, uint16_t offset,
const uint16_t nbits, const bool strict) {
uint64_t data = 0;

if (results->rawlen < 2 * (nbits / kXmpWordSize) + (kXmpSections * kFooter) +
offset - 1)
return false; // Not enough entries to ever be XMP.

// Compliance
if (strict && nbits != kXmpBits) return false;

// Data
// Sections
for (uint8_t section = 1; section <= kXmpSections; section++) {
for (uint16_t bits_so_far = 0; bits_so_far < nbits / kXmpSections;
bits_so_far += kXmpWordSize) {
if (!matchMarkRange(results->rawbuf[offset++], kXmpMark)) return 0;
uint8_t value = 0;
bool found = false;
for (; value <= kXmpMaxWordValue; value++) {
if (matchSpaceRange(results->rawbuf[offset],
kXmpBaseSpace + value * kXmpSpaceStep,
kXmpSpaceStep / 2, 0)) {
found = true;
break;
}
}
if (!found) return 0; // Failure.
data <<= kXmpWordSize;
data += value;
offset++;
}
// Section Footer
if (!matchMarkRange(results->rawbuf[offset++], kXmpMark)) return 0;
if (section < kXmpSections) {
if (!matchSpace(results->rawbuf[offset++], kXmpFooterSpace)) return 0;
} else { // Last section
if (offset < results->rawlen &&
!matchAtLeast(results->rawbuf[offset++], kXmpFooterSpace)) return 0;
}
}

// Compliance
if (strict) {
// Validate checksums.
uint64_t checksum_data = data;
const uint16_t section_size = nbits / kXmpSections;
// Each section has a checksum.
for (uint16_t section = 0; section < kXmpSections; section++) {
if (getSectionChecksum(checksum_data, section_size) !=
calcSectionChecksum(checksum_data, section_size))
return 0;
checksum_data >>= section_size;
}
}

// Success
results->value = data;
results->decode_type = decode_type_t::XMP;
results->bits = nbits;
results->address = 0;
results->command = 0;
// See if it is a repeat message.
results->repeat = isRepeat(data, nbits);
return true;
}
#endif // DECODE_XMP

0 comments on commit e27cf2f

Please sign in to comment.