Roger B. DannenbergHome Publications Videos Opera Audacity |
Photo by Alisa. |
Using O2host with O2lite and MicroPython
O2host provides a simple way to connect browsers and microcontrollers to OSC, MIDI, and other browsers and microcontrollers. This is a guide to this versatile tool that runs on MacOS, Windows, and Linux.
O2 source code is open and free.
A video prepared for ICMC 2022 demonstrates location independence and discovery features of O2.
Building Music Systems with O2 and O2lite is an introduction to O2.
Articles on O2 are listed in my bibliography.
Increasingly, it makes sense to build applications and systems by combining ready-made components. It is great if you have the time and ability to write code based on various libraries, but sometimes, you would like to use complete applications (maybe because they have great user interfaces or maybe because they are just so complicated that it’s best not to deconstruct them). With the ubiquity of networks, particularly Wi-Fi, we can use networks to interconnect software components rather than writing code.
O2 is designed for this approach to software construction and integration. It uses “discovery” to form connections so you do not have to type IP addresses and port numbers to form connections, and it generally makes peer-to-peer connections, giving lower latency than routing through a server.
O2lite offers a subset of of O2 features. Mainly, O2lite connects directly to just one O2 “host” running the full O2 protocol. From there, messages can be relayed to any other O2 process. O2lite runs in browsers, using WebSockets to connect to the host, and on microcontrollers including ESP32 and Raspberry Pi.
O2host is a small application that implements a configurable host process that you can connect to from O2lite. O2host has many applications:
Let's create some small projects to illustrate each of these
possibilities. In this post, I will show how to connect physical
controls to a sofware synthesizer using MicroPython running on a
microcontroller and connecting using O2 and o2host
.
In future posts, I will describe:
For this project, I will demonstrate controlling an effect parameter
with a physical knob. The data flow is as follows: The knob connects
to an ESP32 Thing microcontroller's analog input. The data is read by
a small MicroPython program and converted to a number to send to the
effect. The number is sent via O2 over Wi-Fi to the
o2host
program running on my laptop. The
o2host
program is configured to forward incoming O2
messages to OSC over UDP. The OSC messages go to an effect module
running in Soundcool. Here’s a picture:
from machine import Pin, ADC import time pot = ADC(Pin(34)) pot.atten(ADC.ATTN_11DB) # full range: 3.3v led = Pin(5, Pin.OUT) while True: pot_value = pot.read() print(pot_value) time.sleep(0.05)
adctest.py
on my laptop
(here is the source), I
can run it with ampy
(your serial port name will be different):
% ampy --port /dev/tty.usbserial-D306EBLS run adctest.py
In the output, you will see the values read from the ADC.
screen
(adapting this to your own serial port name):
screen /dev/tty.usbserial-D306EBLS 115200
Now typing to the ESP32 Micropython, first connect to Wi-Fi:
> import network > sta_if = network.WLAN(network.STA_IF) > sta_if.active(True) > sta_if.connect("Fios-DSCGL", "password") > import mip > mip.install("github:cbrand/micropython-mdns")
Continuing on the ESP32, install o2litepy
:
> mip.install("github:rbdannenberg/o2/o2litepy")
Finally, to install globals
, exit from screen
(Control-a, Control-\, y), then:
% echo "# globals.py" > globals.py % ampy --port YOUR_SERIAL_PORT put src/globals.py globals.py
Note that the module globals
is just an empty
namespace used to convey the internal Wi-Fi IP address to o2litepy
.
from machine import Pin, ADC import time from o2litepy import O2lite pot = ADC(Pin(34)) pot.atten(ADC.ATTN_11DB) # full range: 3.3v led = Pin(5, Pin.OUT) o2l = O2lite() o2l.initialize("adctest") # ensemble name while True: pot_value = pot.read() print(pot_value) o2l.send("/useadc/1/fader2", 0, "f", pot_value) o2l.sleep(0.05)
This code is saved in adc_o2.py
.
Notice the use of o2l.sleep
instead of
time.sleep
. This is critical because
o2l.sleep
calls o2l.poll
, which is necessary
for O2lite to function. You should call either o2l.sleep
or o2l.poll
frequently in your program.
An important reason to call o2l.sleep
as opposed
to o2l.poll
in this case is that if you do not do
something to limit the rate of messages, it seems that something
blocks message sending periodically, so you will experience an
occasional long gap between messages. With o2l.sleep
to
limit the message rate to 20 per second, I measured a maximum interval
of about 67 msec between messages and a typical message period of 64
msec. The message rate could be increased, but at some point you will
saturate the network or at best start interfering with others
sharing the networ. Consider that while Wi-Fi might have a fairly
high bandwidth, that is only achieved with fewer and larger
messages. Sending many tiny messages like this is relatively inefficient.
Now, we'll see if we can receive the O2 messages using the
o2host
application. o2host
can be downloaded
from the O2 project on
Github. Run o2host
from a terminal.
You will see a form for options like this:
You can get a help screen by typing Control-H:
Type the Escape key to get back to the configuration screen and fill it out as shown above. Here are some details to note:
Configuration
is the name of a
configuration. You can keep up to 20 different configurations
in a preference file. Configurations are only saved when you type
“x” into the Save_
option. You can select and alternate
configuration and load it by typing “x” into the Load_
option, and you can delete a selected configuration with
Delete_
. After we entered all the details, we named
this configuration “adctest” and saved it.
Ensemble name
is needed to make connections in
O2. An ensemble is named collection of processes that
communicate in a peer-to-peer network. Since O2 potentially
comminicates world-wide, you need to uniquely identify the group you
want to communicate with; otherwise, you might end up sending and
receiving messages from someone around the world.
Debug flags
are listed in o2.h
(look around line 500). I
put r
(ecieve) here to print out incoming messages so I
could see what is coming from my ESP32.
Networking
and MQTT
selections say
that we want to limit communication to the local area network
(including Wi-Fi).
Fwd Service ___ to OSC ...
is used to
tell this o2host
program to forward particular messages
from O2 to Open Sound Control (OSC). This is because we are going to
use Soundcool as a synthesizer, and Soundcool can be controlled
using OSC. To get this line to appear type “x”
into New forward O2 to OSC: _
.
With o2host
configured, we are ready for a quick
test. (There is nothing to receive OSC messages, but at least we can
see if o2host
is receiving O2 messages from the ESP32.)
Be sure to Save_
your configuration. Next time you run
o2host
, the saved configuration will be loaded
automatically.
Now, type the Escape key to o2host
to start it. Leave
it running and use another terminal window to start
adc_o2.py
as follows:
Run the adc_o2.py
program (see
“Sending Data via O2” above). I use the runpy
shell script described in a
previous blog post, which will prepend Wi-Fi setup code to
adc_o2.py
and run it on the ESP32. In a terminal application, type:
% uprun adc_o2.py
After 5 or 10 seconds, the ESP32 should find o2host
and start sending messages to it. The ESP32 output should look like:
% uprun adc_o2.py Running make to copy new files to esp32 make: Nothing to be done for `upload'. make completed Running main.py with Wi-Fi setup prefix IP Address 192.168.1.186 1916 1943 1927 ...
The o2host
program output should look something like:
% ./o2host ------------------------------------------------ You have defined 2 of 20 maximum configurations Initializing O2 process for ensemble adctest Polling rate 20 Debug flags: r Reference clock: Y Network option: local network WebSockets: Disable MQTT: Disable O2 [L: 6097701.245]: ** new service _o2 is 0x13f808370 (HASH) active 1 ================================================================================== O2 [L: 0.000]: Local Process Name is @00000000:c0a801b6:ca4a (192.168.1.182:51786) Receive Port 52185 (0xcbd9) ================================================================================== O2 [0.001]: obtained clock sync at 0.00111125 O2 [0.001]: ** new service _cs is 0x13f808bd0 (HASH) active 1 O2 [0.001]: ** reference clock established, time is now 0.00113446 O2 to OSC Service useadc via UDP to IP 127.000.000.001 Port 8867 O2 [0.001]: ** new service useadc is 0x13f8099f0 (OSC_UDP_CLIENT) active 1 Configuration complete, running o2host now ... ^C to quit. ------------------------------------------------ O2 [21.258]: msg received (0x13f808800) at 21.2581s (local 21.2581s) by: BRIDGE /useadc/1/fader2 @ 0 by TCP 0.474481f O2 [21.316]: msg received (0x13f808800) at 21.3165s (local 21.3165s) by: BRIDGE /useadc/1/fader2 @ 0 by TCP 0.470574f O2 [21.372]: msg received (0x13f808800) at 21.3717s (local 21.3717s) by: BRIDGE /useadc/1/fader2 @ 0 by TCP 0.471306f O2 [21.431]: msg received (0x13f808800) at 21.4312s (local 21.4312s) by: BRIDGE /useadc/1/fader2 @ 0 by TCP 0.469109f O2 [21.496]: msg received (0x13f808800) at 21.4963s (local 21.4963s) by: BRIDGE /useadc/1/fader2 @ 0 by TCP 0.468376f ...
You can see that the ESP32 is printing raw data input
values. o2host
is receiving these as O2 messages, which
are printed as address, timestamp (0), “by TCP” and the
single floating point parameter, e.g. 0.474481f, which corresponds to
an ADC value (1943) divided by 4095 to normalize it.
Looking carefully, you might deduce that o2host
missed
the first message (ADC value 1916). When you send to a service that
does not exist, O2 simply drops the message, and in this case, the
ESP32 had not received any information about service
useadc
before the first send
command. It is
possible to check the status of services
and wait for
them to be established, but since we are sending continuously, it does
not matter for this application.
You can also send OSC from a program, and our goal is to connect the potentiometer on the ESP32 to the delay feedback parameter in Soundcool. Here is what the Soundcool patch looks like (the Soundcool patch is here):
o2host
.
Addresses are
also important. The Micropython program adc_o2.py
sends
data to /useadc/1/fader2
. o2host
forwards
service useadc
to OSC port 8867. When forwarding to OSC,
the service name is removed from the address, so the OSC address is
/1/fader2
, which is the address for the feedback
parameter. (And by the way, /1/fader1
controls the
feedback delay, so you could add additional potentiometers to the
ESP32 and control multiple parameters.)
Here is a video of the system in operation:
o2l.initialize("adctest")
call in
adc_o2.py
can take an optional parameter
debug_flags
, so if you write
o2l.initialize("adctest", debug_flags="a")
, then
“all” debug options (except the lowest-level
“b” option to print message content in binary) are enabled, so
you should see if/when o2host
is discovered, when
messages are sent, etc., displayed in the ESP32 serial (terminal) output.
This post shows how you can use O2lite and Micropython to connect a
microcontroller (ESP32) via Wi-Fi to a software synthesizer running on
a laptop. The o2host
program is used as an intermediary.
The microcontroller initializes O2lite which automatically
“discovers” the o2host
program running on the
laptop. o2host
is configured to forward O2 messages to
OSC, and the synthesizer (Soundcool) is configured to listen for OSC
messages. Using o2host
as a “gateway”
eliminates the need to write code to receive and forward messages.
These same techniques can be used to for many types of sensors
controlling many different types of programs, as long as they support
O2, OSC, or MIDI protocols. The o2litepy
module should
run in Micropython on a variety of microcontrollers with Wi-Fi or
Ethernet.
In the next post, I will show how you can use HTML and Javascript in a web browser to communicate through O2, OSC and MIDI.
Additional references appear in the left sidebar (scroll up).