Library for Bang & Olufsen Beo4 remote control, using a TSOP7000 IR receiver and a ESP32
It turned out that the callback function is suitable to just print the codes, but time consuming usages will lead to disturbances of the receiver task. So the queue based beo4_task alternative is the recommended solution, like so:
#include <Arduino.h>
#include "IrBeo4.h"
#define IR_RX_PIN 34 // IR receive pin
IrBeo4 beo4(IR_RX_PIN); // instance
xQueueHandle beo4_rx_queue; // queue
TaskHandle_t beo4_task_h; // task handle
void beo4_task(void *parameter) {
static uint32_t beo4Code=0;
while(1) {
if(pdTRUE==xQueueReceive(beo4_rx_queue,(void*)(&beo4Code),portMAX_DELAY)) {
Serial.printf("beo4_task: %04x %s %s\n",
beo4Code,
beo_src_tbl(beo4Code),
beo_cmd_tbl(beo4Code));
}
}
}
void setup() {
Serial.begin(115200);
beo4_rx_queue = xQueueCreate(50, sizeof(uint32_t));
xTaskCreatePinnedToCore(beo4_task,"beo4_task",10000,NULL,0,&beo4_task_h,0);
beo4.Begin(beo4_rx_queue);
printf("beo4 started, RX=%d\n",IR_RX_PIN);
}
void loop() {
}
Using the MQTT auto discovery feature for Home Assistant integration Details see --> https://github.com/aanban/esp32_beo4/tree/main/examples/esp32_beo4_HA/readme.md
The Bang & Olufsen IR remote control Beo4 works with a carrier frequency of 455kHz. A suitable decoder device is the TSOP7000 from Vishay. However, the production has been stopped in 2009 . Replicas appeared a few years ago , but unfortunately they do not work the same way and have bad habits producing dummy pulses and having hiccups sometimes.
The tsop7000 replicas, that I have tested, in principle decodes a low-activ pulse from a 455kHz carrier burst, as expected. The Beo4 carrier bursts have a length of 200μs, i.e. after the 200μs low pulse the ir_raw signal should toggle and stay at high-level to indicate the pause. But my tested replicas do have this bad habit of not switching to high in a proper manner. The picture below shows the dummy pulses that occur sporadically after 200μs. (blue = TSOP7000 output signal).
Another issue is that the tested TSOP7000 are quite sensitive and react to sunlight or whatever, anyway I observed sporadic pulses on the output even without pressing any button on the Beo4 remote control.
The low pulses are not important for decoding the received signal, i.e. only the total length (falling edge --> falling edge) is important.
Note
The PulseWidth is measured as time between two falling edges and converted to PulseCodes.
PulseWidth[µs] = tnew_edge - tprevious_edge
The low pulses can therefore also be extended in order to suppress the interference pulses. I have tested two work arrounds, a hardware-based monoflop approach and a software-based debouncing solution.
In terms of hardware, a monoflop can suppress the interference pulses. The following circuit with a TLC555 has worked. The low pulse is extended to ca. 620µs. The output signal ir_out
is high-active, therefore mode=RISING
must be set in the attachInterrupt()
function instead of mode=FALLING
.
The software-based work-arround is easier to implement. Debouncing (similar to debouncing of buttons) is implemented within the interrupt service routine. Timestamps are measured for each interrupt and compared with the previous interrupt. The timestamp is put into the g_isr_queue
queue only, if the previous interrupt was more than t_debounce=600µs
ago, i.e. the short interference pulses will be suppressed.
// interrupt service routine
void IRAM_ATTR ir_pulse_isr(void) {
static int64_t tsPre=0; // timestamp of previous edge
int64_t tsNew=esp_timer_get_time(); // timestamp of new edge
if((tsNew-tsPre) > t_debounce ) { // debounce TSOP7000 output
xQueueSend(g_isr_queue,&tsNew,0); // send timestamp to queue if valid low pulse
tsPre=tsNew;
}
}
The TSOP7000 output is low-activ
, therefor mode=FALLING
must be set in the attachInterrupt()
function.
// attach interrupt to TSOP7000 output, and set to falling edge
if(-1!=m_rx_pin) {
pinMode(m_rx_pin, INPUT);
attachInterrupt(m_rx_pin, ir_pulse_isr, FALLING);
}
Further information about the Beo4 remote control code-format can be found here:
Comment | Link |
---|---|
data-link manual |
https://www.mikrocontroller.net/attachment/33137/datalink.pdf |
Beomote |
https://github.com/christianlykke9/Beomote |
Pulses have different widths and corresponding PulseCodes as seen in the table below.
Note
The different pulse-widths are multiples of 3125us, i.e. the PulseCode can be calculated this way:
PulseCode = (tnew_edge - tprevious_edge + 1562) / 3125
PulseWidth | PulseCode | define | Comment |
---|---|---|---|
3125 µs | 1 | BEO_ZERO |
bit = 0 |
6250 µs | 2 | BEO_SAME |
bit = same as previous bit |
9375 µs | 3 | BEO_ONE |
bit = 1 |
12500 µs | 4 | BEO_STOP |
stop-code |
15625 µs | 5 | BEO_START |
start-code |
The picture below shows the frame start with two short pulses and long pulse, that results in a PulseCode sequence BEO_ZERO
,BEO_ZERO
, BEO_START
The Beo4 remote control generates the PulseCode sequences with a total length of 21 pulses as seen below
A complete Beo4 frame with 21 PulseCodes consists of the start-sequence BEO_ZERO
, BEO_ZERO
, BEO_START
, followed by the 17 payload data codes each in the range [BEO_ZERO
, BEO_SAME
, BEO_ONE
] and ends with the BEO_STOP
code.
Within the payload data the PulseCodes are mapped to BitCodes as followed:
PulseCode BitCode
--------- ----------
BEO_ZERO --> BitCode=0
BEO_ONE --> BitCode=1
BEO_SAME --> BitCode=previous BitCode
Note
The BEO_SAME
code was probably introduced to get approximately the same frame length for all Beo4 buttons. Otherwise, Beo4 commands with many 0-data-bits (e.g. TV-0 button) would become much shorter than those with many 1-data-bits. BEO_ZERO
consumes 3.2ms and BEO_ONE
consumes 9.3ms.
The 17 Bit payload is devided into 3 data fields beoLink
(1-Bit), beoSource
(8-Bit) , and beoCommand
(8-Bit). The beoSource
indicates the device, e.g Audio
, Video
, Light
and so on. The beoCommand
indicates the current button, e.g. 0
, left
, right
, volume ++
and so on.
// _ ___________________ ____________
// ir_raw |_| |_|
// ___ ___
// ir_out _| |_________________| |__________
//
// |<----PulseWidth----->|
//
// Start Data Stop
// ir_out |_|_|_____|_|__|__|__|___|__|_|___|__|_|__|__|__|___|_|__|___|____|
// PulseCodes 1 1 5 1 2 2 2 3 2 1 3 2 1 2 2 2 3 1 2 3 4
// BitCodes 0 0 0 0 1 1 0 1 1 0 0 0 0 1 0 0 1
// Payload beoLink__/ |<---beoSource------->| |<---beoCommand---->|
//
// BeoCode = 0 00011011 00001001 = 0x01B09
// beoLink = 0
// beoSource = 0x1B = LIGHT
// beoCommand = 0x09 = 9
//
Note
It turned out that the decoded data field beoLink
is always=0 for all buttons I tested with the Beo4 remote control. Well, this information can be skipped so that a complete BeoCode fits into two bytes.
Start | beoLink | beoSource | beoCommand | Stop | |
---|---|---|---|---|---|
PulseCode | 115 | 1 | 2222 2222 | 3122 2222 | 4 |
BitCode | 0 | 0000 0000 | 1000 0000 | ||
beoCode | 0 | 0x00 | 0x80 |
beoCode = 0x0080
beoLink = 0
beoSource = 0x00 --> video
beoCommand = 0x80 --> TV
Button = TV on
Start | beoLink | beoSource | beoCommand | Stop | |
---|---|---|---|---|---|
PulseCode | 115 | 1 | 2223 2132 | 1222 3123 | 4 |
BitCode | 0 | 0001 1011 | 0000 1001 | ||
beoCoode | 0 | 0x1B | 0x09 |
beoCode = 0x1B09
beoLink = 0
beoSource = 0x1B --> LIGHT
beoCommand = 0x09 --> 9
Button = LIGHT / 9
Note
During my tests with the Beo4 remote and the TSOP7000 hiccups I noticed that the Light
button behaves quite special. I got valid frames without having touched the Beo4-remote at all. Once the Light
button is pressed, after a minute or so the Beo4-remote automatically jumps back to the mode, that has been active before, e.g. Radio
, TV
and so on. In that case the following frame is send: beoSource=0x1B=Light
and beoCommand=0x58=list