/* * ir_BangOlufsen.hpp * * Contains functions for receiving and sending Bang & Olufsen IR and Datalink '86 protocols * To receive B&O and ENABLE_BEO_WITHOUT_FRAME_GAP is NOT defined, you must set RECORD_GAP_MICROS to * at least 16000 to accommodate the unusually long 3. start space. * * This file is part of Arduino-IRremote https://github.com/Arduino-IRremote/Arduino-IRremote. * ************************************************************************************ * MIT License * * Copyright (c) 2022-2023 Daniel Wallner and Armin Joachimsmeyer * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is furnished * to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ************************************************************************************ */ #ifndef _IR_BANG_OLUFSEN_HPP #define _IR_BANG_OLUFSEN_HPP //============================================================================== // // // Bang & Olufsen // // //============================================================================== // https://www.mikrocontroller.net/attachment/33137/datalink.pdf // https://www.mikrocontroller.net/articles/IRMP_-_english#B&O // This protocol is unusual in two ways: // 1. The carrier frequency is 455 kHz // You can build your own receiver as Bang & Olufsen did (check old schematics) or use a TSOP7000 // Vishay stopped producing TSOP7000 since 2009 so you will probably only find counterfeits: // https://www.vishay.com/files/whatsnew/doc/ff_FastFacts_CounterfeitTSOP7000_Dec72018.pdf // It is also likely that you will need an oscilloscope to debug a counterfeit TSOP7000 // The specimen used to test this code was very noisy and had a very low output current // A somewhat working fix was to put a 4n7 capacitor across the output and ground followed by a pnp emitter follower // Other examples may require a different treatment // This particular receiver also did receive lower frequencies but rather poorly and with a lower delay than usual // If you need to parallel a receiver with another one you may need to delay the signal to get in phase with the other receiver // 2. A stream of messages can be sent back to back with a new message immediately following the previous stop space // It might be that this only happens over IR and not on the datalink protocol // You can choose to support this or not: // Mode 1: Mode with gaps between frames // Set RECORD_GAP_MICROS to at least 16000 to accept the unusually long 3. start space // Can only receive single messages. Back to back repeats will result in overflow // Mode 2: Break at start mode // Define ENABLE_BEO_WITHOUT_FRAME_GAP and set RECORD_GAP_MICROS to less than 15000 // to treat the 3. start space of 15.5 ms as a gap between messages, which makes decoding easier :-). // The receiving of a transmission will then result in a dummy decode of the first 2 start bits with 0 bits data // followed by a 15.5 ms gap and a data frame with one start bit (originally sent as 4. start bit). // If the receiver is not resumed within a ms or so, partial messages will be decoded // Debug printing in the wrong place is very likely to break reception // Make sure to check the number of bits to filter dummy and incomplete messages // !!! We assume that the real implementations never set the official first header bit to anything other than 0 !!! // !!! We therefore use 4 start bits instead of the specified 3 and in turn ignore the first header bit of the specification !!! // IR messages are 16 bits long. Datalink messages have different lengths. // This implementation supports up to 40 bits total length split into 8 bit data/command and a header/address of variable length // Header data with more than 16 bits is stored in decodedIRData.extra // B&O is a pulse distance protocol, but it has 3 bit values 0, 1 and (equal/repeat) as well as a special start and trailing bit. // // MSB first, 4 start bits + 8 to 16? bit address + 8 bit command + 1 special trailing bit + 1 stop bit. // Address can be longer than 8 bit. /* * Options for this decoder * */ #define ENABLE_BEO_WITHOUT_FRAME_GAP // Requires additional 30 bytes program memory. Enabled by default, see https://github.com/Arduino-IRremote/Arduino-IRremote/discussions/1181 //#define SUPPORT_BEO_DATALINK_TIMING_FOR_DECODE // This also supports headers up to 32 bit. Requires additional 150 bytes program memory. #if defined(DECODE_BEO) # if defined(ENABLE_BEO_WITHOUT_FRAME_GAP) # if RECORD_GAP_MICROS > 15000 #warning If defined ENABLE_BEO_WITHOUT_FRAME_GAP, RECORD_GAP_MICROS must be set to <= 15000 by "#define RECORD_GAP_MICROS 12750" # endif # else # if RECORD_GAP_MICROS < 16000 #error If not defined ENABLE_BEO_WITHOUT_FRAME_GAP, RECORD_GAP_MICROS must be set to a value >= 16000 by "#define RECORD_GAP_MICROS 16000" # endif # endif #endif #define BEO_DATA_BITS 8 // Command or character #define BEO_UNIT 3125 // All timings are in microseconds #define BEO_IR_MARK 200 // The length of a mark in the IR protocol #define BEO_DATALINK_MARK (BEO_UNIT / 2) // The length of a mark in the Datalink protocol #define BEO_PULSE_LENGTH_ZERO BEO_UNIT // The length of a one to zero transition #define BEO_PULSE_LENGTH_EQUAL (2 * BEO_UNIT) // 6250 The length of an equal bit #define BEO_PULSE_LENGTH_ONE (3 * BEO_UNIT) // 9375 The length of a zero to one transition #define BEO_PULSE_LENGTH_TRAILING_BIT (4 * BEO_UNIT) // 12500 The length of the stop bit #define BEO_PULSE_LENGTH_START_BIT (5 * BEO_UNIT) // 15625 The length of the start bit // It is not allowed to send two ones or zeros, you must send a one or zero and a equal instead. //#define BEO_LOCAL_DEBUG //#define BEO_LOCAL_TRACE #ifdef BEO_LOCAL_DEBUG # define BEO_DEBUG_PRINT(...) Serial.print(__VA_ARGS__) # define BEO_DEBUG_PRINTLN(...) Serial.println(__VA_ARGS__) #else # define BEO_DEBUG_PRINT(...) void() # define BEO_DEBUG_PRINTLN(...) void() #endif #ifdef BEO_LOCAL_TRACE # undef BEO_TRACE_PRINT # undef BEO_TRACE_PRINTLN # define BEO_TRACE_PRINT(...) Serial.print(__VA_ARGS__) # define BEO_TRACE_PRINTLN(...) Serial.println(__VA_ARGS__) #else # define BEO_TRACE_PRINT(...) void() # define BEO_TRACE_PRINTLN(...) void() #endif /************************************ * Start of send and decode functions ************************************/ /* * TODO aNumberOfRepeats are handled not correctly if ENABLE_BEO_WITHOUT_FRAME_GAP is defined */ void IRsend::sendBangOlufsen(uint16_t aHeader, uint8_t aData, int_fast8_t aNumberOfRepeats, int8_t aNumberOfHeaderBits) { for (int_fast8_t i = 0; i < aNumberOfRepeats + 1; ++i) { sendBangOlufsenRaw((uint32_t(aHeader) << 8) | aData, aNumberOfHeaderBits + 8, i != 0); } } void IRsend::sendBangOlufsenDataLink(uint32_t aHeader, uint8_t aData, int_fast8_t aNumberOfRepeats, int8_t aNumberOfHeaderBits) { for (int_fast8_t i = 0; i < aNumberOfRepeats + 1; ++i) { sendBangOlufsenRawDataLink((uint64_t(aHeader) << 8) | aData, aNumberOfHeaderBits + 8, i != 0, true); } } /* * @param aBackToBack If true send data back to back, which cannot be decoded if ENABLE_BEO_WITHOUT_FRAME_GAP is NOT defined */ void IRsend::sendBangOlufsenRaw(uint32_t aRawData, int_fast8_t aBits, bool aBackToBack) { #if defined(USE_NO_SEND_PWM) || defined(SEND_PWM_BY_TIMER) || BEO_KHZ == 38 // BEO_KHZ == 38 is for unit test which runs the B&O protocol with 38 kHz /* * 455 kHz PWM is currently only supported with SEND_PWM_BY_TIMER defined, otherwise maximum is 180 kHz */ # if !defined(USE_NO_SEND_PWM) # if defined(SEND_PWM_BY_TIMER) enableHighFrequencyIROut (BEO_KHZ); # elif (BEO_KHZ == 38) enableIROut (BEO_KHZ); // currently only for unit test # endif # endif // AGC / Start - 3 bits + first constant 0 header bit described in the official documentation if (!aBackToBack) { mark(BEO_IR_MARK); } space(BEO_PULSE_LENGTH_ZERO - BEO_IR_MARK); mark(BEO_IR_MARK); space(BEO_PULSE_LENGTH_ZERO - BEO_IR_MARK); mark(BEO_IR_MARK); space(BEO_PULSE_LENGTH_START_BIT - BEO_IR_MARK); // First bit of header is assumed to be a constant 0 to have a fixed state to begin with the equal decisions. // So this first 0 is treated as the last bit of AGC mark(BEO_IR_MARK); space(BEO_PULSE_LENGTH_ZERO - BEO_IR_MARK); bool tLastBitValueWasOne = false; // Header / Data uint32_t mask = 1UL << (aBits - 1); for (; mask; mask >>= 1) { if (tLastBitValueWasOne && !(aRawData & mask)) { mark(BEO_IR_MARK); space(BEO_PULSE_LENGTH_ZERO - BEO_IR_MARK); tLastBitValueWasOne = false; } else if (!tLastBitValueWasOne && (aRawData & mask)) { mark(BEO_IR_MARK); space(BEO_PULSE_LENGTH_ONE - BEO_IR_MARK); tLastBitValueWasOne = true; } else { mark(BEO_IR_MARK); space(BEO_PULSE_LENGTH_EQUAL - BEO_IR_MARK); } } // Stop mark(BEO_IR_MARK); space(BEO_PULSE_LENGTH_TRAILING_BIT - BEO_IR_MARK); mark(BEO_IR_MARK); #else (void) aRawData; (void) aBits; (void) aBackToBack; #endif } /* * Version with 64 bit aRawData, which can send both timings, but costs more program memory * @param aBackToBack If true send data back to back, which cannot be decoded if ENABLE_BEO_WITHOUT_FRAME_GAP is NOT defined * @param aUseDatalinkTiming if false it does the same as sendBangOlufsenRaw() */ void IRsend::sendBangOlufsenRawDataLink(uint64_t aRawData, int_fast8_t aBits, bool aBackToBack, bool aUseDatalinkTiming) { #if defined(USE_NO_SEND_PWM) || BEO_KHZ == 38 // BEO_KHZ == 38 is for unit test which runs the B&O protocol with 38 kHz instead 0f 455 kHz uint16_t tSendBEOMarkLength = aUseDatalinkTiming ? BEO_DATALINK_MARK : BEO_IR_MARK; /* * 455 kHz PWM is currently not supported, maximum is 180 kHz */ #if !defined(USE_NO_SEND_PWM) enableIROut (BEO_KHZ); #endif // AGC / Start - 3 bits + first constant 0 header bit described in the official documentation if (!aBackToBack) { mark(tSendBEOMarkLength); } space(BEO_PULSE_LENGTH_ZERO - tSendBEOMarkLength); mark(tSendBEOMarkLength); space(BEO_PULSE_LENGTH_ZERO - tSendBEOMarkLength); mark(tSendBEOMarkLength); space(BEO_PULSE_LENGTH_START_BIT - tSendBEOMarkLength); // First bit of header is assumed to be a constant 0 to have a fixed state to begin with the equal decisions. // So this first 0 is treated as the last bit of AGC mark(tSendBEOMarkLength); space(BEO_PULSE_LENGTH_ZERO - tSendBEOMarkLength); bool tLastBitValueWasOne = false; // Header / Data uint32_t mask = 1UL << (aBits - 1); for (; mask; mask >>= 1) { if (tLastBitValueWasOne && !(aRawData & mask)) { mark(tSendBEOMarkLength); space(BEO_PULSE_LENGTH_ZERO - tSendBEOMarkLength); tLastBitValueWasOne = false; } else if (!tLastBitValueWasOne && (aRawData & mask)) { mark(tSendBEOMarkLength); space(BEO_PULSE_LENGTH_ONE - tSendBEOMarkLength); tLastBitValueWasOne = true; } else { mark(tSendBEOMarkLength); space(BEO_PULSE_LENGTH_EQUAL - tSendBEOMarkLength); } } // Stop mark(tSendBEOMarkLength); space(BEO_PULSE_LENGTH_TRAILING_BIT - tSendBEOMarkLength); mark(tSendBEOMarkLength); #else (void) aRawData; (void) aBits; (void) aUseDatalinkTiming; (void) aBackToBack; #endif } #define BEO_MATCH_DELTA (BEO_UNIT / 2 - MICROS_PER_TICK) static bool matchBeoLength(uint16_t aMeasuredTicks, uint16_t aMatchValueMicros) { const uint16_t tMeasuredMicros = aMeasuredTicks * MICROS_PER_TICK; return aMatchValueMicros - BEO_MATCH_DELTA < tMeasuredMicros && tMeasuredMicros < aMatchValueMicros + BEO_MATCH_DELTA; } bool IRrecv::decodeBangOlufsen() { #if defined(ENABLE_BEO_WITHOUT_FRAME_GAP) if (decodedIRData.rawlen != 6 && decodedIRData.rawlen < 36) { // 16 bits minimum #else if (decodedIRData.rawlen < 44) { // 16 bits minimum #endif return false; } #if defined(SUPPORT_BEO_DATALINK_TIMING_FOR_DECODE) uint16_t protocolMarkLength = 0; // contains BEO_IR_MARK or BEO_DATALINK_MARK depending of 4. mark received uint64_t tDecodedRawData = 0; #else uint32_t tDecodedRawData = 0; #endif uint8_t tLastDecodedBitValue = 0; // the last start bit is assumed to be zero uint8_t tPulseNumber = 0; uint8_t tBitNumber = 0; BEO_TRACE_PRINT(F("Pre gap: ")); BEO_TRACE_PRINT(decodedIRData.initialGapTicks * 50); BEO_TRACE_PRINT(F(" raw len: ")); BEO_TRACE_PRINTLN(decodedIRData.rawlen); #if defined(ENABLE_BEO_WITHOUT_FRAME_GAP) /* * Check if we have the AGC part of the first frame, i.e. start bit 1 and 2. */ if (decodedIRData.rawlen == 6) { if ((matchMark(decodedIRData.rawDataPtr->rawbuf[3], BEO_IR_MARK) || matchMark(decodedIRData.rawDataPtr->rawbuf[3], BEO_DATALINK_MARK)) && (matchSpace(decodedIRData.rawDataPtr->rawbuf[4], BEO_PULSE_LENGTH_ZERO - BEO_IR_MARK) || matchSpace(decodedIRData.rawDataPtr->rawbuf[4], BEO_PULSE_LENGTH_ZERO - BEO_DATALINK_MARK))) { BEO_TRACE_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_TRACE_PRINTLN(F("B&O: AGC only part (start bits 1 + 2 of 4) detected")); } else { return false; // no B&O protocol } } else { /* * Check if leading gap is trailing bit of first frame */ if (!matchSpace(decodedIRData.initialGapTicks, BEO_PULSE_LENGTH_START_BIT)) { BEO_TRACE_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_TRACE_PRINTLN(F(": Leading gap is wrong")); // Leading gap is trailing bit of first frame return false; // no B&O protocol } if (matchMark(decodedIRData.rawDataPtr->rawbuf[1], BEO_IR_MARK)) { # if defined(SUPPORT_BEO_DATALINK_TIMING_FOR_DECODE) protocolMarkLength = BEO_IR_MARK; } else if (matchMark(decodedIRData.rawDataPtr->rawbuf[1], BEO_DATALINK_MARK)) { protocolMarkLength = BEO_DATALINK_MARK; # endif } else { BEO_TRACE_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_TRACE_PRINTLN(F(": mark length is wrong")); return false; } // skip first zero header bit for (uint8_t tRawBufferMarkIndex = 3; tRawBufferMarkIndex < decodedIRData.rawlen; tRawBufferMarkIndex += 2) { #else for (uint8_t tRawBufferMarkIndex = 1; tRawBufferMarkIndex < decodedIRData.rawlen; tRawBufferMarkIndex += 2) { #endif // defined(ENABLE_BEO_WITHOUT_FRAME_GAP) uint16_t markLength = decodedIRData.rawDataPtr->rawbuf[tRawBufferMarkIndex]; uint16_t spaceLength = decodedIRData.rawDataPtr->rawbuf[tRawBufferMarkIndex + 1]; BEO_TRACE_PRINT(tPulseNumber); BEO_TRACE_PRINT(' '); BEO_TRACE_PRINT(markLength * MICROS_PER_TICK); BEO_TRACE_PRINT(' '); BEO_TRACE_PRINT(spaceLength * MICROS_PER_TICK); BEO_TRACE_PRINT(F(" (")); BEO_TRACE_PRINT((markLength + spaceLength) * MICROS_PER_TICK); BEO_TRACE_PRINTLN(F(") ")); #if !defined(ENABLE_BEO_WITHOUT_FRAME_GAP) /* * Handle the first 4 start bits * Check if the 3. bit is the long start bit. If we see the long start bit earlier, synchronize bit counter. */ if (tPulseNumber < 4) { if (tPulseNumber < 2) { // bit 0 and 1 if (matchSpace(spaceLength, BEO_PULSE_LENGTH_START_BIT - BEO_IR_MARK)) { BEO_TRACE_PRINTLN(F(": detected long start bit -> synchronize state now")); tPulseNumber = 2; } } else { if (tPulseNumber == 3) { if (matchMark(markLength, BEO_IR_MARK)) { # if defined(SUPPORT_BEO_DATALINK_TIMING_FOR_DECODE) protocolMarkLength = BEO_IR_MARK; } else if (matchMark(markLength, BEO_DATALINK_MARK)) { protocolMarkLength = BEO_DATALINK_MARK; # endif } else { BEO_DEBUG_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_DEBUG_PRINTLN(F(": 4. (start) mark length is wrong")); return false; } } // bit 2 and 3 if (!matchBeoLength(markLength + spaceLength, (tPulseNumber == 2) ? BEO_PULSE_LENGTH_START_BIT : BEO_PULSE_LENGTH_ZERO)) { BEO_DEBUG_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_DEBUG_PRINTLN(F(": Start length is wrong")); return false; } } } else { #endif // !defined(ENABLE_BEO_WITHOUT_FRAME_GAP) /* * Decode header / data * First check for length of mark */ #if defined(SUPPORT_BEO_DATALINK_TIMING_FOR_DECODE) if (!matchMark(markLength, protocolMarkLength)) { #else if (!matchMark(markLength, BEO_IR_MARK)) { #endif BEO_DEBUG_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_DEBUG_PRINTLN(F(": Mark length is wrong")); return false; } /* * Check for stop after receiving at least 8 bits for data and 4 bits for header */ if (tBitNumber > BEO_DATA_BITS + 4) { if (matchBeoLength(markLength + spaceLength, BEO_PULSE_LENGTH_TRAILING_BIT)) { BEO_DEBUG_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_DEBUG_PRINTLN(F(": Trailing bit detected")); break; } #if !defined(ENABLE_BEO_WITHOUT_FRAME_GAP) if (tRawBufferMarkIndex >= decodedIRData.rawlen - 3) { // (rawlen - 3) is index of trailing bit mark BEO_DEBUG_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_DEBUG_PRINTLN(F(": End of buffer, but no trailing bit detected")); return false; } #endif } /* * Decode bit */ if (tLastDecodedBitValue == 0 && matchBeoLength(markLength + spaceLength, BEO_PULSE_LENGTH_ONE)) { tLastDecodedBitValue = 1; } else if (tLastDecodedBitValue == 1 && matchBeoLength(markLength + spaceLength, BEO_PULSE_LENGTH_ZERO)) { tLastDecodedBitValue = 0; } else if (!matchBeoLength(markLength + spaceLength, BEO_PULSE_LENGTH_EQUAL)) { BEO_DEBUG_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_DEBUG_PRINT(F(": Index=")); BEO_DEBUG_PRINT(tRawBufferMarkIndex); BEO_DEBUG_PRINT(F(" Length ")); BEO_DEBUG_PRINT((markLength + spaceLength) * MICROS_PER_TICK); BEO_DEBUG_PRINTLN(F(" is wrong")); return false; } tDecodedRawData <<= 1; tDecodedRawData |= tLastDecodedBitValue; ++tBitNumber; BEO_TRACE_PRINT(F("Bits ")); BEO_TRACE_PRINT(tBitNumber); BEO_TRACE_PRINT(F(" ")); BEO_TRACE_PRINT(uint32_t(tDecodedRawData >> BEO_DATA_BITS), HEX); BEO_TRACE_PRINT(F(" ")); BEO_TRACE_PRINTLN(uint8_t(tDecodedRawData & ((1 << BEO_DATA_BITS) - 1)), HEX); // End of bit decode #if !defined(ENABLE_BEO_WITHOUT_FRAME_GAP) } #else /* * Check for last bit after decoding it */ if (tRawBufferMarkIndex >= decodedIRData.rawlen - 3) { // (rawlen - 3) is index of last bit mark BEO_TRACE_PRINT(::getProtocolString(BANG_OLUFSEN)); BEO_TRACE_PRINTLN(F(": Last bit reached")); break; } #endif ++tPulseNumber; } #if defined(ENABLE_BEO_WITHOUT_FRAME_GAP) } #endif decodedIRData.protocol = BANG_OLUFSEN; decodedIRData.address = tDecodedRawData >> BEO_DATA_BITS; // lower header tBitNumber decodedIRData.command = tDecodedRawData & ((1 << BEO_DATA_BITS) - 1); // lower 8 tBitNumber decodedIRData.extra = tDecodedRawData >> (BEO_DATA_BITS + 16); // upper header tBitNumber decodedIRData.numberOfBits = tBitNumber; decodedIRData.flags = IRDATA_FLAGS_IS_MSB_FIRST; decodedIRData.decodedRawData = tDecodedRawData; return true; } #endif // _IR_BANG_OLUFSEN_HPP