Turn Beosound Ouverture On/Off Using WiFi?

15 replies

Hello,

I have a Beosound Ouverture and Beolab 1 speakers. I use the AUX output to use my loudspeakers for the TV, streaming, sending my iTunes music from my iMac, etc. Works great.

The problem is, I have no remote way to turn the Ouverture on and off other than using the Beo4 remote control. I tried a universal remote control hub, but sadly discovered it is unable to learn the Beo4 IR codes (and read somewhere on this forum about how different B&O is concerning the RC). I contacted B&O support, and they were less than helpful.

So is there any way to easily do this with commercially available products using an iOS app? If not, is there a DIY way to do this? Could MasterLink be used to do this in some way? Etc.

Thanks for your help.

Tony

You can use a blc nl/ml connected via master link to ouverture. The blc needs a wired connection to your network though.

Then it's just using the b&o app.

How about this: 

Location: Warwickshire, UK

My B&O Icons:

BeoCenter-9500.png BeoCenter-9500.png
Beoremote-Beo4.png Beoremote-Beo4.png
Beovision-10.png Beovision-10.png
Beotime.png Beotime.png
Beosound.png Beosound.png
Beosound-Explore.png Beosound-Explore.png
Beosound-Essence.png Beosound-Essence.png
Beoplay-P2.png Beoplay-P2.png
Beoplay-M3.png Beoplay-M3.png
Beoplay-H4.png Beoplay-H4.png
Beoplay-H3.png Beoplay-H3.png
Beoplay-Form-2i.png Beoplay-Form-2i.png
Beoplay-Earset.png Beoplay-Earset.png
Beoplay-A2.png Beoplay-A2.png

Guy wrote:

How about this: link

 

That's the guy who makes the Beo4 to remote plugs converter! A trustful seller.

 

Location: Paris France

soloAudio wrote:
I tried a universal remote control hub, but sadly discovered it is unable to learn the Beo4 IR codes
I have used a Logitech Harmony with a BeoSystem 6500.  It cannot learn Beo4 button-presses, but it comes pre-programmed with most of the buttons, once you configure it to add a BeoWhatever to its list of your components.  However, not sure if you can "drive it" with any commands except from Logitech's own iOS App.

Hi,

 

Seems interesting to me - I'm in need for a controller that can turn on a Beolab 3500 without using the Beo4 - would it be possible for you to elaborate further to how you made the setup and the code needed for the ESP?

Hi,

 

Seems interesting to me - I'm in need for a controller that can turn on a Beolab 3500 without using the Beo4 - would it be possible for you to elaborate further to how you made the setup and the code needed for the ESP?

Hi klbu,

Yes, I can provide details on how I did this. I'll need some time to gather this up and will post.

Thanks

Hi,

Thanks for your answer - that would be really great if you can provide me with the details.

/Klaus

Hi Klaus,

Here are the details of how I solved this. I hope this is helpful for you.

Thanks,
Tony

Hardware

Espressif ESP32-WROOM-32D ($14.95+tax)

But I see it now at a lower cost ($10+tax) at Mouser:

There are also many other variations of ESP32, and other choices might be simpler to use (includes JTAG interface, etc.).

I used a Vishay TSAL4400 emitter (940nm, 15ns rise time). You can get them at a variety of places, such as:

The circuit is a simple voltage divider for an LED. Per the specification, I aimed for the forward voltage of 1.35V (forward current of 100mA, 20ms), which calculates out to a 19.5ohm resistor (1/4W). But as I only had a 15ohm resistor, I used a 1/2W (130mA). Note that the radiant intensity is directly correlated to the forward current, and that the transmitter is only on for 200us max (see below), so if you want to extend the range, you certainly can. I found this circuit worked at around 15 feet (4.6m), so this was fine for my application.

Here’s what it looks like (using GPIO pin 2):


Software

I used the Arduino IDE and Espressif Arduino library for faster development:

But you can also do it with just Espressif’s library, and any IDE you want (Eclipse, PlatformIO, etc.):

For setting up your computer installation, connection to device, etc. I point you to the Espressif documentation, of which there is plenty and which I found to be surprisingly good.

Over the Air Protocol

It took a fair amount of research to figure out the codes for the Beo4, and how they are supposed to be properly transmitted. Here are the details I have gathered and how I used the ESP32 RMT functionality to generate the IR signals. Many of the details can be seen from the code itself, but I’ll provide an overview here. I should stress that I came across disparate descriptions of some of what I describe below. For example, one reference (from B&O) clearly indicated four start bits which included the link bit, but other references from various sources included the link bit with the data bits (but it basically amounts to the same thing). Also, some sources indicated the number of data bits could be either 16 (or 17 if you include the link bit) or 19 (or 20 if you include the link bit). I had decided to simplify things by including the link bit with the start bits, and only using 16 data bits. 

The Beo4 transmitter modulates via Pulse Distance Coding using a 455kHz carrier, thus a bit value is defined by the time between the pulses. The carrier pulse (CP) duration is always 200us, and itself consists of a series of on and off pulses of wavelength 2.197802198 us. The remaining time (with the CP duration included) is defined as one of five values:

t1: 3125us = 0 bit                      (a bit transition from 1 to 0)
t2: 6250us = same as last bit (a bit transition from 0 to 0 or 1 to 1)
t3: 9375us = 1 bit                      (a bit transition from 0 to 1)
t4: 12500us = stop bit
t5: 15625us = start bit

Codes (such as "Power All Off") are transmitted in a Frame, comprised of 4 start bits, 16 data bits, 1 stop bit, and 1 trailer bit. There are 16 data bits (8 address bits, 8 command bits). The trailer bit is 200us (i.e. just the carrier pulse, I assume to delineate the stop bit).

The first two starter bits are for AGC, and are each t1; I do not know what would happen if other bit value combinations are used. They are followed by the start bit t5, and then the link bit t1 (as I always used zero for the link bit). So the first four bits would look like:
Starter Bits: 200us CP + 2925us, 200us CP + 2925us, 200us CP + 15425us, 200us CP + 2925us (i.e. t1, t1, t5, t1).

Next comes the data bits. The upper 8 data bits (called the address bits) indicates the device code. For example, the device code “Source All” is 0x0F. The lower 8 bits are the key command. For example, the key command for “Power SysOff” is 0x0C. So the code to turn everything off is 0x0F0C. This would be followed by the Stop bit and the Trailer bit. For example, the entire transmission for "Power SysOff" would be:

t1, t1, t5, t1, t2, t2, t2, t2, t3, t2, t2, t2, t1, t2, t2, t2, t3, t2, t1, t2, t4, T (where T is the last carrier pulse for the Trailer bit)

Using the ESP32 Remote Control (RMT) Feature

The reason I picked the ESP32 is it had the RMT feature which allowed me to define the carrier frequency, as this is not a standard frequency used by nearly every other IR device. You can see this setup in rmt_tx_init (using config.tx_config.carrier_freq_hz). The RMT feature also allowed me to clock divide the clock used for transmission time. Since the default clock to RMT is 80MHz, I set the clock divide to 80 (using config.clk_div) so that data in RAM representing the signal could be indicated in nice units of microseconds. Note too that the RMT can access memory in one of two ways; either dedicated channels, or via direct writes from any RAM location to the transmitter (all handled by the provided driver). I chose the latter (using rmt_write_items) so I didn’t have to worry about long commands, or combining channel memories, etc. I did not find any issue with the corresponding increase in latency.

So in effect, the signals to transmit are laid out in memory as a sequence of 32 bit words. Each 32 bit word consists of two 16 bit operations. Each operation consists of a 15 bit duration (in units of 1us based on my clock divide), and one bit of either a value of 0 or 1. For example, if I wanted to turn the carrier on for 200us, and off for 200us, I would use two paired 16 bit values of (200, 1)(200, 0). The driver provides a data structure (rmt_item32_t) for setting this up. 

So, to send the “Power SysOff” command, the memory would look like this:

static const rmt_item32_t beo4_cmds[] = {
    // All Off (0x0F0C)
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 1 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 2 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T5, 0 }}}, // Start Bit 3 - Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 4 - Link Bit
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data End
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T4, 0 }}}, // Stop Bit
    {{{ PULSE_ON_T1, 1, 0, 0 }}},                           // Trailer Pulse, End RMT
<snip>

In this way, different commands can be sent using different offsets into the array beo4_cmds. I think the code is self explanatory.

Application

I used the WiFi Server driver to easily do this. I experimented with making the ESP32 an Access Point and using the RemoteXY application, but I found this to be too cumbersome from the iPhone. Instead, the ESP32 connects to my LAN, and I use very basic HTML to send the request (ON, OFF, VOL UP,  VOL DOWN) and “GET” on the server to process the command and transmit the proper signal. While I found this to be a tad sluggish, it does work. When you boot and run the code, you can see the IP address displayed in the serial monitor. Then you can open a browser on your phone, and type that IP address as the URL. At this point, you should see the ON/OFF/VOL UP/VOL DOWN buttons displayed. Note that when using DHCP, I usually would get the same IP address assigned to the ESP32, but it could potentially change. So instead, I assigned my device using a static IP address (using WiFi.config).

Here’s what the browser looks like when you connect to the ESP32. The Beo4 Power button toggles between ON and OFF when pressed.


On the iPhone, I created a icon for easy access (to save the trouble of typing the IP address for the URL), and used a tools called CallMeIcons to jam an image onto  it. Here’s what that looks like on my iPhone:


Caveats/Issues

For my Beo4, I need to have it set to AUX ahead of time. That is, if I leave the Beo4 set to CD or something else, then use my application to turn on/off, it won’t work. I can manually press List then Aux on the Beo4, but if I try sending that sequence of commands via my device, it doesn’t seem to work. I am not entirely sure why this is the case. More experimentation is needed.

Future Additions

- Been experimenting with using WiFi power management to limit energy usage as this device is on all the time, and mostly doing very little. I haven’t had time to work on this yet.
- I want to write my own Apple application to see how this would work (and to learn).
- The ESP32 device is crazy powerful, with two cores! Surely there are other things that this device can be doing while sitting there as an RC emulator.

Code

Disclaimer: Please note that I borrowed heavily from other sources to build this code. There are many examples in the Espressif repo, and of course elsewhere on the internet.

Note: When you cut/paste this into Arduino, if you get any compiler errors, check the error lines for bad single quotes (‘) or double quotes (“) that were corrupted when pasted.

Note: Let me know if you have any issues. I couldn’t re-test the code before posting as I uninstalled the port drivers to work on another project, and I didn’t want to redo all that just yet.

/*
 * WiFiServer + ESP32 Remote Control Test Project for Beo4
 * soloAudio for Beoworld (https://archivedforum2.beoworld.org/)
 * February 22, 2020
 */

#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/rmt.h"
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiServer.h>
#include <ESPmDNS.h>

// set static IP address
IPAddress local_IP(W, X, Y, Z);      // e.g. W=192, X=168, W=1, Z=300 
IPAddress gateway(W, X, Y, Z);
IPAddress subnet(W, X, Y, Z);        // e.g. W=255, X=255, Y=255, Z=0
IPAddress primaryDNS(W, X, Y, Z);    // optional
IPAddress secondaryDNS(W, X, Y, Z);  // optional, could be W=0, X=0, Y=0, Z=0

// network credentials
const char *ssid = “<Your SSID>";
const char *password = “<Your Password>";

// Set web server port number to 80
WiFiServer server(80);

// Variable to store the HTTP request
String header;

// state variable for power on/off
String beo4PowerState = "off";

#define RMT_TX_CHANNEL RMT_CHANNEL_0
#define RMT_TX_GPIO    2

#define PULSE_ON_T1  200
#define PULSE_OFF_T1 2925    // 3125
#define PULSE_OFF_T2 6050    // 6250
#define PULSE_OFF_T3 9175    // 9375
#define PULSE_OFF_T4 12300   // 12500
#define PULSE_OFF_T5 15425   // 15625

#define BEO4_CMD_SIZE        22                     // sizeof(beo4_cmds) / sizeof(beo4_cmds[0])
#define ALL_OFF_OFFSET       (BEO4_CMD_SIZE * 0)
#define AUDIO_GO_OFFSET      (BEO4_CMD_SIZE * 1)
#define AUDIO_LIST_OFFSET    (BEO4_CMD_SIZE * 2)
#define AUDIO_VUP_OFFSET     (BEO4_CMD_SIZE * 3)
#define AUDIO_VDOWN_OFFSET   (BEO4_CMD_SIZE * 4)

// Remote Control Commands Using 1MHz clock
static const rmt_item32_t beo4_cmds[] = {
    // All Off (0x0F0C)
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 1 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 2 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T5, 0 }}}, // Start Bit 3 - Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 4 - Link Bit
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data End
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T4, 0 }}}, // Stop Bit
    {{{ PULSE_ON_T1, 1, 0, 0 }}},            // Trailer Pulse, End RMT
    // Audio-Go (0x0135)
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 1 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 2 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T5, 0 }}}, // Start Bit 3 - Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 4 - Link Bit
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}}, // Data End
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T4, 0 }}}, // Stop Bit
    {{{ PULSE_ON_T1, 1, 0, 0 }}},            // Trailer Pulse, End RMT
    // Audio-List (0x0158)
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 1 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 2 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T5, 0 }}}, // Start Bit 3 - Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 4 - Link Bit
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data End
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T4, 0 }}}, // Stop Bit
    {{{ PULSE_ON_T1, 1, 0, 0 }}},            // Trailer Pulse, End RMT
    // Audio-Volume Up (0x0160)
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 1 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 2 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T5, 0 }}}, // Start Bit 3 - Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 4 - Link Bit
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data End
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T4, 0 }}}, // Stop Bit
    {{{ PULSE_ON_T1, 1, 0, 0 }}},            // Trailer Pulse, End RMT
    // Audio-Volume Down (0x0164)
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 1 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 2 - AGC
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T5, 0 }}}, // Start Bit 3 - Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}}, // Start Bit 4 - Link Bit
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data Start
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T3, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T1, 0 }}},
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T2, 0 }}}, // Data End
    {{{ PULSE_ON_T1, 1, PULSE_OFF_T4, 0 }}}, // Stop Bit
    {{{ PULSE_ON_T1, 1, 0, 0 }}}             // Trailer Pulse, End RMT
};

// RMT Tx Channel Initialization
static void rmt_tx_init(void)
{
    rmt_config_t config;

    config.rmt_mode = RMT_MODE_TX;
    config.channel  = RMT_TX_CHANNEL;
    config.gpio_num = (gpio_num_t)RMT_TX_GPIO;
    config.mem_block_num = 1;
    config.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH;
    config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
    config.tx_config.loop_en = false;
    config.tx_config.idle_output_en = true;
    config.tx_config.carrier_en = true;
    config.tx_config.carrier_duty_percent = 50;
    config.tx_config.carrier_freq_hz = 455000;
    config.clk_div = 80;

    ESP_ERROR_CHECK(rmt_config(&config));
    
    ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, 0));
}

// Arduino Setup
void setup() 
{
  periph_module_enable(PERIPH_RMT_MODULE);

  ESP_LOGI(TAG, "Configuring transmitter");
    
  rmt_tx_init();

  rmt_set_source_clk(RMT_TX_CHANNEL, RMT_BASECLK_APB); // RMT_BASECLK_APB or RMT_BASECLK_MAX or RMT_BASECLK_REF

  WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS);

  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");
  
  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(500);
    Serial.print(".");
  }
  
  // Print local IP address and start web server
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin("esp32")) 
  {
    Serial.println("MDNS responder started");
  }

  // force B&O off
  ESP_ERROR_CHECK(rmt_write_items(RMT_TX_CHANNEL, &beo4_cmds[ALL_OFF_OFFSET], BEO4_CMD_SIZE, true));
  
  server.begin();
  Serial.println("WiFi server started");
}

// Arduino Loop
void loop() 
{
  WiFiClient client;

  // listen for incoming clients
  client = server.available();
  
  if (client) 
  { 
    // if a new client connects,
    Serial.println("New Client.");          // print a message out in the serial port
    String currentLine = "";                // make a String to hold incoming data from the client
    
    while (client.connected()) 
    { 
      // loop while the client's connected
      if (client.available()) 
      { 
        // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') 
        { 
          // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) 
          {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();
            
            // check the command received
            if (header.indexOf("GET /Beo4_ON") >= 0) 
            {
              beo4PowerState = "on";

              ESP_ERROR_CHECK(rmt_write_items(RMT_TX_CHANNEL, &beo4_cmds[AUDIO_LIST_OFFSET], BEO4_CMD_SIZE, true));
              vTaskDelay(20 / portTICK_PERIOD_MS);
              ESP_ERROR_CHECK(rmt_write_items(RMT_TX_CHANNEL, &beo4_cmds[AUDIO_GO_OFFSET], BEO4_CMD_SIZE, true));
              
              ESP_LOGI(TAG, "Transmission complete");
              Serial.println("B&O ON\n");
            } 
            else if (header.indexOf("GET /Beo4_OFF") >= 0) 
            {
              beo4PowerState = "off";

              ESP_ERROR_CHECK(rmt_write_items(RMT_TX_CHANNEL, &beo4_cmds[ALL_OFF_OFFSET], BEO4_CMD_SIZE, true));
              
              ESP_LOGI(TAG, "Transmission complete");
              Serial.println("B&O OFF\n");
            }
            else if (header.indexOf("GET /Beo4_VOL_UP") >= 0)
            {
              ESP_ERROR_CHECK(rmt_write_items(RMT_TX_CHANNEL, &beo4_cmds[AUDIO_VUP_OFFSET], BEO4_CMD_SIZE, true));
            }
            else if (header.indexOf("GET /Beo4_VOL_DOWN") >= 0)
            {
              ESP_ERROR_CHECK(rmt_write_items(RMT_TX_CHANNEL, &beo4_cmds[AUDIO_VDOWN_OFFSET], BEO4_CMD_SIZE, true));
            }
            
            // display the HTML web page
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // CSS to style the on/off buttons 
            // Feel free to change the background-color and font-size attributes to fit your preferences
            client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
            client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println("</style></head>");
            
            // Web Page Heading
            client.println("<body><h1>Bang & Olufsen Beo4 Remote Control</h1>");
            
            // display current state, and ON/OFF button 
            client.println("<p>Beo4 Power - State " + beo4PowerState + "</p>");
            
            // if the beo4PowerState is off, it displays the ON button       
            if (beo4PowerState=="off") 
            {
              client.println("<p><a href=\"/Beo4_ON\"><button class=\"button\">ON</button></a></p>");
            } 
            else // if the beo4PowerState is on, it displays the OFF button
            {
              client.println("<p><a href=\"/Beo4_OFF\"><button class=\"button\">OFF</button></a></p>");
            }

            client.println("<p>Beo4 Volume Up</p>");
            client.println("<p><a href=\"/Beo4_VOL_UP\"><button class=\"button\">VOL UP</button></a></p>");
            client.println("<p>Beo4 Volume Down</p>");
            client.println("<p><a href=\"/Beo4_VOL_DOWN\"><button class=\"button\">VOL DOWN</button></a></p>");
               
            client.println("</body></html>");
            
            // the HTTP response ends with another blank line
            client.println();

            // break out of the while loop
            break;
          } 
          else  // if you got a newline, then clear currentLine
          {
            currentLine = "";
          }
        } 
        else if (c != '\r') 
        {  
          // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    
    // clear the header variable
    header = "";

    // close the connection
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}


or
BeoWorld Gold Membership card
Join BeoWorld

Register to access the forums, reference material, and exclusive member benefits — including our monthly prize draw.

Become a Member

December 2026 Prize Draw

Gold Members Only

As a Gold member you're automatically entered into our exclusive prize draw — your chance to win iconic Bang & Olufsen products. A unique benefit of BeoWorld Gold membership, every draw brings a new opportunity to win something special.