Library for Bang & Olufsen Beo4 remote control, using a TSOP7000 IR receiver and a ESP32
Notes: The pioarduino based arduino-esp32 platform is used, in order to support newer boards like ESP32-C6.
-
arduino-esp32 migration 2.x to 3.0
https://docs.espressif.com/projects/arduino-esp32/en/latest/migration_guides/2.x_to_3.0.html#ledc -
arduino-esp32 for platformio
https://github.com/pioarduino/platform-espressif32
[env:esp32dev]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
board = esp32dev
framework = arduino
In previous releases there was the call back function beo_code_cb()
, but it turned out that it is suitable to just print the codes, but anything else will generate to disturbances of the receiver task ending in unreadable codes. Therefore the critical call back funtion beo_code_cb()
was removed. The queue based aproach is the better solution. A simple receiver main.cpp example looks 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() {
}
It shows how to start the transmit task only. The table beoCodes[]
has 10 entries with several beo4-Codes. A queue is created within setup_beo4_tx()
. Within the loop()
every 2 seconds the next code from the table is send to the queue and within the task beo4_tx_task()
the output signal is generated for GPIO32
, i.e. 455khz carrier pulses (length=200µs) framing the pauses according to the bits of the current beo4-code.
#include <Arduino.h>
#include "IrBeo4.h"
#include "IrBeo4Info.h"
// Beo4 stuff
constexpr uint8_t IR_TX_PIN = 32; // IR transmit pin
constexpr uint8_t numBeo = 10;
static IrBeo4 beo4(-1,IR_TX_PIN); // transmit only
static xQueueHandle beo4_tx_queue; // queue for beo4codes from transmitter
static uint32_t beoCnt=0; // loop counter thru beoCodes[]
static uint32_t beoCodes[numBeo] = { // sample of Beo4-Codes
((uint32_t)BEO_SRC_AUDIO << 8) + (uint32_t)BEO_CMD_VOL_UP,
((uint32_t)BEO_SRC_AUDIO << 8) + (uint32_t)BEO_CMD_VOL_DOWN,
((uint32_t)BEO_SRC_AUDIO << 8) + (uint32_t)BEO_CMD_RADIO,
((uint32_t)BEO_SRC_VIDEO << 8) + (uint32_t)BEO_CMD_TV,
((uint32_t)BEO_SRC_AUDIO << 8) + (uint32_t)BEO_CMD_CD,
((uint32_t)BEO_SRC_AUDIO << 8) + (uint32_t)BEO_CMD_NUM_1,
((uint32_t)BEO_SRC_AUDIO << 8) + (uint32_t)BEO_CMD_NUM_2,
((uint32_t)BEO_SRC_LIGHT << 8) + (uint32_t)BEO_CMD_NUM_0,
((uint32_t)BEO_SRC_LIGHT << 8) + (uint32_t)BEO_CMD_NUM_1,
((uint32_t)BEO_SRC_LIGHT << 8) + (uint32_t)BEO_CMD_NUM_2,
};
// example for a transmit only device: create beo4_tx_queue and call
// beo4.Begin(NULL,beo4_tx_queue), this will skip the beo4_rx_task()
// and start the beo4_tx_task() only
void setup_beo4_tx(void) {
pinMode(IR_TX_PIN, OUTPUT);
Serial.printf(PSTR("===> start beo4... "));
beo4_tx_queue = xQueueCreate(50, sizeof(uint32_t));
static int beo4_ok=beo4.Begin(NULL,beo4_tx_queue);
Serial.printf(PSTR("%s\n"),beo4_ok==0? "OK":"failed");
}
constexpr unsigned long period=2000; // do something every 2 sec.
static unsigned long t0=0; // timestamp to compare with
void setup() {
Serial.begin(115200);
setup_beo4_tx();
t0=millis();
}
void loop() {
unsigned long t1=millis();
if((t1-t0) > period) {
uint32_t beoCode=beoCodes[beoCnt];
xQueueSend(beo4_tx_queue,&beoCode,0);
beoCnt = (beoCnt < numBeo-1) ? beoCnt+1 : 0;
t0=millis();
}
}
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 | ||
Button | video | TV on |
Start | beoLink | beoSource | beoCommand | Stop | |
---|---|---|---|---|---|
PulseCode | 115 | 1 | 2223 2132 | 1222 3123 | 4 |
BitCode | 0 | 0001 1011 | 0000 1001 | ||
beoCode | 0 | 0x1B | 0x09 | ||
Button | light | # 9 |
Start | beoLink | beoSource | beoCommand | Stop | |
---|---|---|---|---|---|
PulseCode | 115 | 1 | 2222 2223 | 1321 2222 | 4 |
BitCode | 0 | 0000 0001 | 0110 0000 | ||
beoCode | 0 | 0x01 | 0x60 | ||
Button | audio | vol++ |
Start | beoLink | beoSource | beoCommand | Stop | |
---|---|---|---|---|---|
PulseCode | 115 | 1 | 2222 2223 | 1321 2312 | 4 |
BitCode | 0 | 0000 0001 | 0110 0100 | ||
beoCode | 0 | 0x01 | 0x64 | ||
Button | audio | vol-- |
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