wireless midi <-> osc bridge using an esp8266-01. this circuit is extremely cheap to build. schematics, arduino code and examples for supercollider below.

i'm using the great Arduino MIDI Library that allows for both sending and receiving a multitude of midi messages including sysex, system realtime and time code messages. my arduino code just converts all these to/from osc and send or broadcast them over wifi network.

note: sending midi over wifi udp is generally a bad idea. there will be delays, glitches and even lost messages (hanging notes). this is specially problematic for midi time code (sync) messages. that said, in many situations this is ok and in my tests with simple note on/off messages + bend and control, things seem to work just fine.


the circuit takes in 5v and then the regulator steps this down to 3.3v. notice the huge 220uF capacitor that's needed to provide power for the esp8266 during its infamous current draw spikes. it wouldn't be hard to add a midi thru section if needed.


supercollider example code...


OSCFunc.trace(true, true);  //your laptop should have ip x.x.x.99 to receive - check in the arduino code

n= NetAddr("", 18120);  //ip of esp8266

n.sendMsg(\noteOn, 66, 127, 1);  //(note, velo, chan)

n.sendMsg(\noteOff, 66, 0, 1);  //(note, velo, chan)

n.sendMsg(\afterTouchPoly, 50, 60, 3);  //poly pressure (note, press, chan)

n.sendMsg(\controlChange, 1, 64, 3);  //(num, val, chan)

n.sendMsg(\programChange, 10, 4);  //(num, chan)  note the -1 offset

n.sendMsg(\afterTouchChannel, 40, 2);  //(press, chan)

n.sendMsg(\pitchBend, -8000, 1);  //(bend, chan)  -8192 - 8191

n.sendMsg(\sysEx, 240, 14, 5, 0, 5, 247);  //(sysex) - 240 a b c d e ... 247

var clock= 0xf8;  //248
var start= 0xfa;  //250
var continue= 0xfb;  //251
var stop= 0xfc;  //252{
        n.sendMsg(\realTime, start);{
                n.sendMsg(\realTime, clock);
        n.sendMsg(\realTime, stop);
        n.sendMsg(\realTime, continue);{
                n.sendMsg(\realTime, clock);
        n.sendMsg(\realTime, stop);
n.sendMsg(\realTime, 0xfe);  //active sensing
n.sendMsg(\realTime, 0xff);  //system reset

n.sendMsg(\songPosition, 100);
n.sendMsg(\songSelect, 3);

n.sendMsg(\beginNrpn, 10, 3);  //(number, channel)
n.sendMsg(\nrpnDecrement, 40, 3);  //(amount, channel)
n.sendMsg(\nrpnIncrement, 30, 3);  //(amount, channel)
n.sendMsg(\endNrpn, 3);  //(channel)

n.sendMsg(\beginRpn, 10, 4);  //(number, channel)
n.sendMsg(\rpnDecrement, 40, 4);  //(amount, channel)
n.sendMsg(\rpnIncrement, 30, 4);  //(amount, channel)
n.sendMsg(\endRpn, 4);  //(channel)

//--simple midi synth example
s.latency= 0.02;
        var busBend= Bus.control(s);
        var busCF= Bus.control(s);
        var busRQ= Bus.control(s);
        var busVol= Bus.control(s);
        var busPan= Bus.control(s);
        busBend.value= 0;
        busCF.value= 1000;
        busRQ.value= 0.5;
        busVol.value= 0.5;
        busPan.value= 0;
        SynthDef(\note, {|freq= 400, amp= 0.5, gate= 1, busBend, busCF, busRQ, busVol, busPan|
                var env=, 1, 0.85, 0.1), gate, amp, doneAction:2);
                var bend=;
                var cf=;
                var rq=;
                var vol=;
                var pan=;
                var src=, cf, rq);
      ,*env, pan, vol));
        d= ();
        OSCdef(\f0mid, {|msg|
                        \activeSensing, {},
                        \noteOn, {
                      [2]).set(\gate, 0);
                                d.put(msg[2], Synth(\note, [
                                        \freq, msg[2],
                                        \amp, msg[3].lincurve(0, 127, 0, 0.75, 4),
                                        \busBend, busBend,
                                        \busCF, busCF,
                                        \busRQ, busRQ,
                                        \busVol, busVol,
                                        \busPan, busPan
                        \noteOff, {
                      [2]).set(\gate, 0);
                                d.put(msg[2], nil);
                        \pitchBend, {
                                busBend.value= msg[2]/8192;
                        \controlChange, {
                                        1, {
                                                busCF.value= msg[3].linexp(0, 127, 400, 4000);
                                        7, {
                                                busVol.value= msg[3].lincurve(0, 127, 0, 1, 0);
                                        10, {
                                                busPan.value= msg[3].linlin(0, 127, -1, 1);
                                        74, {
                                                busRQ.value= msg[3].linlin(0, 127, 2, 0.1);
                                        {("todo control: "+msg).postln}
                        {("todo command: "+msg).postln}
        }, \f0mid);

//mtc - receive
var a= MIDISMPTEAssembler({|time, format, dropFrame, srcID|
        [time, format, dropFrame, srcID].postln;
OSCdef(\f0mid, {|msg, time, addr|
        var chan, valu;
        if(msg[1]==\mtcQF, {
                chan= msg[2].rightShift(4);  //nibble high
                valu= msg[2].bitAnd(15);  //nibble low
                if(chan==7, {
                        valu= switch(valu,
                                6, {valu= 96},  //30fps
                                4, {valu= 64},  //30fps drop
                                2, {valu= 32},  //25fps
                                0, {valu= 0}     //24fps
                a.value(addr.addr.bitAnd(255), chan, valu);
}, \f0mid);

//mtc - send (kind of works - wslib quark required)
var startSec= 0;
var t= Main.elapsedTime-startSec;
var a= SMPTE(0, 30);{
        var chan= 0, valu= 0;{
                        0, {valu= a.frames.asInteger.bitAnd(15)},
                        1, {valu= a.frames.asInteger.rightShift(4)},
                        2, {valu= a.seconds.asInteger.bitAnd(15)},
                        3, {valu= a.seconds.asInteger.rightShift(4)},
                        4, {valu= a.minutes.asInteger.bitAnd(15)},
                        5, {valu= a.minutes.asInteger.rightShift(4)},
                        6, {valu= a.hours.asInteger.bitAnd(15)},
                        7, {
                                valu= a.hours.asInteger.bitAnd(1).rightShift(4);
                                        30, {valu= valu.bitOr(6)},  //30fps
                                        //30fps drop not supported
                                        25, {valu= valu.bitOr(2)},  //25fps
                                        //24, {valu= valu.bitOr(0)}     //24fps
                n.sendMsg(\mtcQF, chan.leftShift(4)+valu.bitAnd(15));
                chan= chan+1;
                if(chan==8, {
                        chan= 0;

Package icon f0mid_firmware.zip3.49 KB

rpi audio codec

here's how to set up the proto WM8731 based audio codec module from MikroElektronika and use it with supercollider on a raspberry pi 3.

power off the rpi and connect the proto board to the rpi with jump wires like this...

proto               raspberry
-----                -----
sck           ->    rpi 12
miso         ->    rpi 38
mosi         ->    rpi 40
adcl+dacl  ->    rpi 35  //both proto pins go to the same rpi pin
sda           ->    rpi 3
scl            ->    rpi 5
3.3v         ->    rpi 1
gnd          ->    rpi 6

see pinout diagram for help with the rpi gpio numbering.

power on the rpi and open a terminal and type...

sudo nano /boot/config.txt

find and uncomment the first line and add the second...


press ctrl+o to save and ctrl+x to exit.

sudo reboot

again open a terminal and type...


first press F5 to show all controls, then...
* enable item 'Mic' (space)
* set item 'Mic Boost' to 100 (up arrow key)
* enable item 'Playback Deemphasis' (m key)
* disable item 'ADC High Pass Filter' (m key)
* set item 'Input Mux' to Mic (arrow keys)
* enable item 'Output Mixer HiFi' (m key)


now you should be able to start jackd with for example...

jackd -P75 -dalsa -dhw:0 -r48000 -p256 -n2

and get decent in/out sound at 5.3ms jack latency.

solar powered supercollider

here's how to run supercollider on power coming from the sun...

the main component is a raspberry pi zero with wifi that at startup creates a wireless access point, starts jackd+supercollider and launches a default sound patch.
to play around with the system and change the default sound log on to the access point with a laptop and start livecoding supercollider via the terminal or use the standard scide via vnc. one can for example also set up a couple of osc responders and let friends log on with their phones to control sounds.



the connections are pretty straightforward...

solarpanel -> dcdc converter -> battery -> rpi0 -> soundcard -> amplifier -> speaker(s)

the dcdc converter is taking the higher voltage coming out of the solar panel (~6v) and turns it into a stable 5v. this is then either charging the battery, or directly powering the raspberry pi. note that the amplifier also needs 5v and here i have taken that from pins 4 and 6 on the pi.

the powerbank battery is optional and can be omitted but then the solar panel will have to stay in the sun at all times - else the system will turn off or reboot when the power from the panel drops. the battery acts like a reservoir for when clouds are passing by but not only that - it also lets the system be used for a couple of hours in the evening.

material/modules needed:

* rpi zero w
* 8gb micro sd card
* 5v usb powerbank (best if it can charge and output power at the same time)
* 6v 6watt solar panel ( )
* dc-dc converter ( )
* usb sound adapter
* pam8403 stereo amplifier module
* two full range speakers
* wooden board, double adhesive tape + various cables and screws

download raspbian jessie (here jessie desktop and burn it onto the sd card with etcher.

do the usual setup (change default password, activate ssh), optionally activate vnc and then install supercolliderStandaloneRPI1.

to set up a wifi access point do the following (basically the same as this)...

* sudo apt-get install dnsmasq hostapd
* sudo systemctl stop dnsmasq
* sudo systemctl stop hostapd
* sudo nano /etc/dhcpcd.conf  #and add...
        denyinterfaces wlan0
* sudo nano /etc/network/interfaces  #and make sure wlan0 looks like...
        allow-hotplug wlan0
        iface wlan0 inet static
* sudo service dhcpcd restart
* sudo ifdown wlan0
* sudo ifup wlan0
* sudo nano /etc/dnsmasq.conf  #and add the following...
* sudo nano /etc/hostapd/hostapd.conf  #and add the following...
* sudo nano /etc/default/hostapd  #and change to the following...
* sudo service hostapd start
* sudo service dnsmasq start

last change the file mycode.scd and add this default sound (tweet0340)...

        play{a=SinOscFB;Mix(AllpassN[12,8])+3/24,0,Dseq([0,8,5,1,5,4,5]*round(c*18),inf))+60),c*2)/4)}// #SuperCollider

if it is distorting try lowering the volume in alsamixer.


after many years i finally got around to rebuild one of these boxes.

so this old soviet made device is now a wireless controller that send out osc. there are in total 34 buttons, 16 knobs and an additional rgb status led. it automatically connects via wifi to max or supercollider and run on 5v (usb powerbank).

kicad schematics, arduino firmware, supercollider classes and maxmsp abstractions attached below.


the inside is quite a mess. i use an atmega168 together with six 4051 multiplexers to read all the inputs. the wifi module is an esp8266-01.


Package icon udssrKontroll_1.0.zip116.03 KB

supercollider firmata 3

reading digital inputs from an arduino with the SCFirmata is a little bit more complicated than needed.

here an example that reads 6 analog and 6 digital at the same time.

NOTE: use resistors (10K) to pull up or pull down the digital inputs. (i couldn't figure out how to activate the built in pullups.)

d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds

~analog= [0, 1, 2, 3, 4, 5];  //A0-A5
~digital= [2, 3, 4, 5, 6, 12];  //some digital input pins
s.latency= 0.05;
        var freqsArr= 0!~analog.size;
        var ampsArr= 0!~digital.size;
        Ndef(\snd3, {\, 0.05), 0, \}).play;{|x|
                f.reportAnalogPin(x, true);      //start reading analog pins
        f.analogPinAction= {|num, val|
                //[num, val].postln;
                freqsArr.put(~analog.indexOf(num), val);
                Ndef(\snd3).setn(\freqs, freqsArr);
                f.setPinMode(x, \INPUT);
        f.reportDigitalPort(0, true);
        f.reportDigitalPort(1, true);
        f.digitalPortAction= {|port, mask|
                var dig;
                //[port, mask, mask.asBinaryString].postln;
                dig= ~digital.collect{|x| ((mask<<(port*8))&(1<<x)==(1<<x)).binaryValue};
                Ndef(\snd3).set(\amps, dig.postln);

        f.reportAnalogPin(i, false);     //stop reading A0-Anum
f.reportDigitalPort(0, false);
f.reportDigitalPort(1, false);

previous articles...

supercollider firmata 2

+2 years ago i put up a simple example of how to use firmata with arduino and supercollider here. that code still work but it only show how to read a single analog input on the arduino.

here is how one can read both A0 and A1 and map those to synth parameters in supercollider...

//how to read pins A0 and A1 with SCFirmata...
//tested with Arduino1.8.0 and SC3.8.0
//first in Arduino IDE:
//  * select File / Examples / Firmata / StandardFirmata
//  * upload this example to an arduino
//then in SC install the SCFirmata classes
//  * download zip file
//  * extract files and put them in your sc application support directory
//  * recompile sc

d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds


Ndef(\snd, {|freq1= 400, freq2= 500, amp= 0.5|[freq1, freq2].lag(0.08), 0, amp.lag(0.08)).tanh}).play;
f.reportAnalogPin(0, true);      //start reading A0
f.reportAnalogPin(1, true);      //start reading A1
f.analogPinAction= {|num, val|
        [num, val].postln;
                0, {
                        Ndef(\snd).set(\freq1, val.linexp(0, 1023, 400, 800)); //A0 mapped to freq1
                1, {
                        Ndef(\snd).set(\freq2, val.linexp(0, 1023, 400, 800)); //A1 mapped to freq2

f.reportAnalogPin(0, false);     //stop reading A0
f.reportAnalogPin(1, false);     //stop reading A1

and to read all six analog inputs (A0-A5) one can do...

d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds

~numberOfAna= 6;  //number of analog inputs (here A0-A5)

var freqsArr= 0!~numberOfAna;
Ndef(\snd2, {|amp= 0.5|\, 0.05), 0, amp.lag(0.08)).tanh)}).play;{|i|
        f.reportAnalogPin(i, true);      //start reading A0-Anum
f.analogPinAction= {|num, val|
        [num, val].postln;
        freqsArr.put(num, val);
        Ndef(\snd2).setn(\freqs, freqsArr);

        f.reportAnalogPin(i, false);     //stop reading A0-Anum


this board is using an old raspberry pi 1 to control the speed of computer fans. the electronics are pretty simple (see attached schematics below): it takes 7-36V input power, has twelve mosfets for pwm control and finally a dc/dc converter to power the rpi.
it was built for controlling pc cooling fans but can also drive other types of dc motors, lightbulbs or solenoids.
the off button is there to safely power down the raspberry pi.

the trick with this though is that the system can be livecoded over wifi using supercollider, maxmsp or any other osc capable program. so when you start the board the rpi sets up a wireless access point and starts a python script that accepts incoming opensoundcontrol messages. at startup the rpi1 will also start supercollider and load a file (dragspelFans.scd) that is meant to contain whatever code you'd like to run as default. this file you later overwrite with your own sc code that you've developed/livecoded using your laptop.


below are step-by-step instructions on how i set this up plus the relevant python and supercollider code. it will work on most rpi models but here the rpi1 or rpi0 is assumed.

* download and install raspbian-stretch-lite onto a 2gb sd card
* to enable ssh create an empty file on the sd card. call it ssh. (this terminal command touch /Volumes/boot/ssh will do it on osx or just create an empty textfile and save it without any file extension)
* connect your rpi to your home router via ethernet and type the following in terminal on your laptop:
* ssh-keygen -R raspberrypi.local
* ssh pi@raspberrypi.local #default passwork is raspberry
* sudo raspi-config #change password to _____, set memory split to 16 under advanced, change hostname to fans under network, update, finish and reboot (sudo reboot)
* ssh pi@fans.local #log in again from your laptop
* sudo apt-get update
* sudo apt-get upgrade
* sudo apt-get dist-upgrade

//--wifi softap
this section is optional. it will set up a wifi access point served from the rpi. skip down to python section if you rather want to connect the rpi as a normal client to a separate router or your home/venue wifi.
* sudo apt-get install dnsmasq hostapd firmware-atheros firmware-ralink firmware-realtek
* sudo nano /etc/hostapd/hostapd.conf #and add the following:


* sudo nano /etc/default/hostapd #and change one line to the following:


* sudo nano /etc/dnsmasq.conf #and add the following two lines to the bottom:


* sudo nano /etc/network/interfaces #and edit eth0 to look like:

allow-hotplug eth0
#auto eth0
iface eth0 inet dhcp

* and also change/add wlan0 to look like:

allow-hotplug wlan0
auto wlan0
iface wlan0 inet static
    wireless-power off

this section will install osc and gpio libraries for python and also set up the python script to automatically start at system boot.
* sudo apt-get install python-liblo pigpio python-pigpio
* sudo crontab -e #and add the following line at the end

@reboot /usr/bin/pigpiod -s 5 && /usr/bin/python /home/pi/

* nano /home/pi/ #and copy&paste in the following:

#pwm control for 12 fans/motors/leds

#NOTE: make sure to run this in terminal first...
# sudo pigpiod -s 5

import sys
from os import system
from time import sleep
import pigpio
from liblo import *

inport= 9999  #for osc commands to this python script
pinoff= 2  #bcm numbering
pins= [3, 4, 14, 15, 17, 18, 27, 22, 23, 24, 10, 9]  #bcm numbering - one can add more here
target= ('', 57120)  #for osc to sclang
hz= 800  #pwm frequency in hz - note may need to adapt -s option in sudo pigpio -s 5 above
range= 100  #duty cycle range 0 to 100

pi= pigpio.pi()
pi.set_mode(pinoff, pigpio.INPUT)  #no internal pullup needed
for pin in pins:
  pi.set_mode(pin, pigpio.OUTPUT)
  pi.set_PWM_frequency(pin, hz)
  pi.set_PWM_range(pin, range)
  pi.set_PWM_dutycycle(pin, 0)

class MyServer(ServerThread):
        def __init__(self):
                ServerThread.__init__(self, inport)
        @make_method('/pwms', 'i'*len(pins))
        def pwms_callback(self, path, args):
                #print args  #debug
                i= 0
                for pin in pins:
                        pi.set_PWM_dutycycle(pin, min(max(0, args[i]), range))
                        i= i+1
        @make_method('/shutdown', '')
        def shutdown_callback(self, path, args):
                stop('sudo halt -p')  #turn off rpi
        @make_method('/reboot', '')
        def reboot_callback(self, path, args):
                stop('sudo reboot')  #reboot rpi
        @make_method('/start', '')
        def start_callback(self, path, args):
                send(target, '/start', 1)  #start default program in supercollider
        @make_method('/stop', '')
        def stop_callback(self, path, args):
                send(target, '/stop', 0)  #stop default program in supercollider
                for pin in pins:  #and also set all pwm to 0
                        pi.set_PWM_dutycycle(pin, 0)
        @make_method(None, None)
        def fallback(self, path, args):
                print 'received unknown message "%s"' % path

def stop(cmd):
        system('sudo killall pigpiod jackd sclang scsynth')

        server= MyServer()
except ServerError, err:
        print str(err)

def main():
        while True:
                        print 'shutting down...'
                        stop('sudo halt -p')

if __name__ == '__main__':
        except KeyboardInterrupt:

use ctrl+o and ctrl+x to save and exit.

this section is basically the same as installing sc for rpi1/rpi0 from here, if rpi2 or rpi3 look here instead and change all references to supercolliderStandaloneRPI1 below to supercolliderStandaloneRPI2
* sudo apt-get install libqt5webkit5 libqt5sensors5 libqt5positioning5 libfftw3-bin libcwiid1
//* sudo apt-get install git dbus-x11 xvfb jackd2 #enable realtime when asked
* git clone --depth 1
* mkdir -p ~/.config/SuperCollider
* cp supercolliderStandaloneRPI1/sc_ide_conf_temp.yaml ~/.config/SuperCollider/sc_ide_conf.yaml
* cd supercolliderStandaloneRPI1
* nano #and change the two lines to look like this:

//    /usr/bin/jackd -P95 -dalsa -dhw:0 -p1024 -n3 -s &
./sclang -a -l sclang.yaml ../dragspelFans.scd

* crontab -e #and add the following to the end (note no sudo here this time)

@reboot cd /home/pi/supercolliderStandaloneRPI1 && xvfb-run ./

* nano share/user/startup.scd #and add the following two lines

OSCFunc({"/home/pi/dragspelFans.scd".load}, \start).permanent= true;
OSCFunc({}, \stop).permanent= true;

* mkdir share/user/Extensions

* from your laptop copy over some files using these commands...

scp ~/arbeten/dragspel/ pi@fans.local:
scp ~/arbeten/dragspel/ pi@fans.local:supercolliderStandaloneRPI1/share/user/Extensions
scp ~/arbeten/dragspel/dragspelFans.scd pi@fans.local:

now sudo reboot on the rpi, log on to dragspel wifi network from your laptop and try to send some osc commands using sc on your laptop.

for logging on to the rpi and start supercollider from terminal
* pkill sclang
* xvfb-run --auto-servernum ./sclang -a -l sclang.yaml

and here's the default demo file that will be run by the rpi at startup. it just uses a pbind to set random pwm duty cycles (0-100) on all twelve pins. overwrite this file with your own code. save this as dragspelFans.scd

Event.addEventType(\fans, {d.val(~index, ~val)});
Pbind(\type, \fans, \dur, 0.5, \index, Pseq([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], inf), \val, Pwhite(0, 100, inf)).play;

or to control it manually without the DragspelFans class...

n= NetAddr("fans.local", 9999);
n.sendMsg(\pwms, *[50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
n.sendMsg(\pwms, *[25, 50, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0])
n.sendMsg(\pwms, *[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
n.sendMsg(\shutdown)  //shut down the rpi

here's a screenshot of a simple max patch to manually control the fans...


update 180225: rewrite to use pigpio instead of RPi.GPIO and also raspbian stretch instead of jessie. pwm works much better.

PDF icon dragspel_schematics.pdf50.67 KB
Binary Data DragspelFans.sc2.02 KB


next week in Bucharest we'll be setting up the subjective frequency transducer for the third time. i described the sound/vibration generating part of this system before but didn't write much about how the controllers work.

so for each sound channel (i.e. each bass transducer) there's a wireless controller that enables the audience to set their preferred frequency. technically it's done with a rotary encoder, a esp8266 wifi module, a mega168 and a big 7-segment lcd. the circuit runs off two AAA batteries.

when someone touches the rotary encoder, the circuit wakes up and starts sending osc messages to a laptop running supercollider. supercollider receives the values, starts playing an oscillator and sends the sound to the corresponding audio channel. when done, sc fades out the oscillator and sends an off message to the circuit and the controller goes back to sleep mode.

i spent quite some time optimising the microcontroller (atmega168) code. it was hard to both reduce power consumption and still being able to quickly wake up and react on user input as well as on incoming osc messages. it's a common problem with battery powered radio devices.

also getting the esp8266 to handle osc messages was a pain. here and here are some more info and simplified versions of that.

in the end, the code for talking to these circuits in supercollider looked like this:

//sc example: sending. turn off circuit 3 and set it back to initial frequency
~encode= {|id, on, hi, lo| (id&255<<24)|(on&255<<16)|(hi&255<<8)|(lo&255)};
~encode.value(3, 0, 0, ~initFreq);
//sc example: receiving. decoding data from the four esp8266
OSCdef(\sti, {|msg, time, addr|
        var id= msg[1]>>24;
        var onoff= (msg[1]>>16)&255;
        var freq= (msg[1]&65280)+(msg[1]&255);
        [\id, id, \onoff, onoff, \freq, freq].post;
}, \sti);

the microcontroller code could still be improved. i'd like it to wake up on both wdt and uart. at the moment the circuit is drawing 22mA average in off+idle state, and 33mA average with display set to '20' which is okey but not optimal. and when sending osc you get current spikes of a few hundred milliamps but there's no way around that.

//f.olofsson 2015-2016

#define ID 3
#define FREQ 0 //start frequency
#define FREQ_MIN 0
#define FREQ_MAX 9999
#define WLAN_ADDR "" //laptop static ip
#define WLAN_PORT 1112
String tag = "/tap"; //incomming osc addy

#include <avr/sleep.h>
#include <avr/power.h>
#include <avr/wdt.h>

#include <Encoder.h>

Encoder myEnc(3, 2);
float freq = FREQ;  //starting frequency
int freqLast = -999;

byte state = 0;
int enc = 0;
byte dig = 0;
byte cnt = 0;
boolean resp;

uint8_t buf[16];  //osc message

void setup() {
  pinMode(2, INPUT_PULLUP);  //encoder a
  pinMode(3, INPUT_PULLUP);  //encoder b
  pinMode(4, INPUT_PULLUP);  //encoder button
  DDRB = B11111111;  //segments
  DDRC = B00001111;  //digits selector

  //--set up wifi
  resp = Serial.find("ready\r\n");
  resp = Serial.find("OK\r\n");
  do {
    resp = Serial.find("OK\r\n");
  } while (!resp);
  resp = Serial.find("OK\r\n");
  Serial.print("\",57120,");  //supercollider default port
  resp = Serial.find("OK\r\n");

  //--osc message
  buf[0] = 47;   // /
  buf[1] = 115;  // s
  buf[2] = 116;  // t
  buf[3] = 105;  // i
  buf[4] = 0;
  buf[5] = 0;
  buf[6] = 0;
  buf[7] = 0;
  buf[8] = 44;   // ,
  buf[9] = 105;  // i
  buf[10] = 0;
  buf[11] = 0;
  buf[12] = ID;  // a high   (id)
  buf[13] = state; // a low  (onoff)
  buf[14] = 0;   // b high   (freq hi)
  buf[15] = 0;   // b low    (freq lo)

  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  OCR1A = 32768;  //62.5Hz display updaterate
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (1 << CS10);  //prescaler divide by 1
  TIMSK1 |= (1 << OCIE1A);

  MCUSR &= ~(1 << WDRF);
  WDTCSR |= (1 << WDCE) | (1 << WDE);
  WDTCSR = 1 << WDP0 | 1 << WDP1;

volatile int f_wdt = 1; //watchdog wakeup
ISR(WDT_vect) {
  if (f_wdt == 0) {
    f_wdt = 1;
void enterSleep(void) {

ISR(TIMER1_COMPA_vect) {  //update display periodically
  if (state == 2) {

void sendOsc() {
  buf[13] = state;
  buf[14] = int(freq) >> 8;
  buf[15] = int(freq) & 255;
  Serial.write(buf, sizeof(buf));
  resp = Serial.find("OK\r\n");

void loop() {
  dig = 1 - ((PIND >> 4) & 1);  //encoder momentary button
  switch (state) {
    case 2:  //running (display on)
      enc =;
      if (enc != 0) {
        float incStep = enc / 2.0;
        freq = max(FREQ_MIN, min(FREQ_MAX, freq + incStep));
        if (int(freq) != freqLast) {
          freqLast = int(freq);
      if (dig == 1) {  //TODO: or timeout here?
        state = 3;
    case 0:  //sleeping (display off)
      f_wdt = 0;
      enc =;
      if ((dig == 1) || (enc != 0)) {
        state = 1;
        freq = FREQ; //reset
    case 3:  //turning off when button released
      if (dig == 0) {
        state = 0;
    case 1:  //turning on when button released
      if ((dig == 0) || (enc != 0)) {
        state = 2;

  //--receive osc
  while (Serial.available()) {
    String abc = Serial.readStringUntil('\n');
    if (abc.startsWith("+IPD,4,16:" + tag)) {
      //if(abc[22]==ID) { //optional filter by device ID
      if (abc[23] == 0) {
        state = 0;
      } else {
        state = 2;
      freq = (abc[24] << 8) + abc[25];
void displayClear() {
  PORTC = B00001111;
  PORTB = B00000000;
void progressDot(byte index) {
  setChr(255, true);
void displayFreq() {
  int val = freq; //cuts off fraction
  switch (cnt) {
    case 0:
      if (val > 999) {
        setChr((val % 10000) / 1000, false);
      } else {
        setChr(255, false);
      cnt = 1;
    case 1:
      if (val > 99) {
        setChr((val % 1000) / 100, false);
      } else {
        setChr(255, false);
      cnt = 2;
    case 2:
      if (val > 9) {
        setChr((val % 100) / 10, false);
      } else {
        setChr(255, false);
      cnt = 3;
    case 3:
      setChr(val % 10, false);
      cnt = 0;

void selDig(byte index) {
  switch (index) {
    case 1:
      PORTC = B00001110;
    case 2:
      PORTC = B00001101;
    case 3:
      PORTC = B00001011;
    case 4:
      PORTC = B00000111;

void setChr(byte chr, bool dot) {
  switch (chr) {
    case 255:  //clear
      PORTB = B00000000;
    case 0:
      PORTB = B11111100;
    case 1:
      PORTB = B01100000;
    case 2:
      PORTB = B11011010;
    case 3:
      PORTB = B11110010;
    case 4:
      PORTB = B01100110;
    case 5:
      PORTB = B10110110;
    case 6:
      PORTB = B10111110;
    case 7:
      PORTB = B11100000;
    case 8:
      PORTB = B11111110;
    case 9:
      PORTB = B11100110;
        case 10:  //A
        case 11:  //B
        case 12:  //C
        case 13:  //D
        case 14:  //E
        case 15:  //F
        case 16:  //G
        case 17:  //H

  if (dot) {
    PORTB |= B00000001;


Subscribe to RSS - supercollider