// 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 #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