« 1 2 3 4 5 6 7 »

OpenBCI WiFi Shield DIY

2019-01-13 13:28 electronics

Here is how I built my own WiFi shield for the OpenBCI Cyton and Ganglion boards. The total cost was less than €10.

See the schematic.

On the white adapter board, I first removed the 0K resistor link in the middle (R2). Then I cut the two tracks that run from VCC to the left-hand side 10K resistor (R3). Next, I soldered on an NCP1117 on the backside (not visible in the photo) and finally added two 100nF caps.

Last I mounted the ESP+adapter on a Vero board, added male pin headers and the other components and wires.

IMG_20180626_160641 IMG_20180626_160755

It looks very rough but works great. Here are instructions on how to upload firmware and how to use it.

Bluetooth Module Repair

2019-01-13 12:24 electronics

This documents a repair of a Cyton board. The board would turn on and kind of work but one couldn't upload new firmware to it.

I localised the issue to the RFD22301 (aka RFDuino) module. To desolder it from the board I used some rose's metal - a material with low melting temperature (94°c) - and then put on new temporary pins to a few pads and put it on a breadboard.


The module sort of worked but it still wouldn't be programmed. After lifting the metal shield and checking all the connections, I found a broken trace from the chip out to the rf_rxd pad (gpio0/aref) on the module. Tracing the fault a bit further it turned out to be a broken via. I tried to refill the via with solder but it was just too small to repair. So I ended up adding a thin copper wire directly from the pin to the pad and wrapped it in some Kapton tape for isolation.


I had to cut out a slot in the metal shield so that my wire could get out without getting squeezed and shorting to ground. It doesn't look pretty, but the module now worked and I could finally upload new firmware. And after soldering the shield back and putting back the module, the Cyton board worked again.


I also repaired a broken trace on one of the serial lines. It's visible on the right-hand side in the photo.

Lidar Sensor

2019-01-12 11:10 visuals

Last summer I wrote code to talk to the YDLIDAR X4 Lidar – 360-degree Laser Range Scanner (10m). It was quite difficult to parse the data and get the correct readout of the point cloud. The X4_Lidar_Development_Manual.pdf had all the information but it was quite obscure.

lidar test screenshot

I also added tracking (red cross in the screenshot) to figure out coordinates of a person moving around in a room. Nothing fancy but worked ok.

The attached code is for Unity3d and written in C#.


Radar Sensor

2019-01-11 14:31 electronics

Last year I built this very simple motion detection device. It will trigger a noteOn MIDI message when someone enters a room, and a noteOff message when there's no activity. It's using a RCWL-0516 radar sensor and a Digispark clone (basically a ATtiny85).

radarsensor photo
//with Digispark (Default - 16.5MHz)
//RCWL-0516 Microw. Radar-ModulRCWL-0516

#define USB_CFG_DEVICE_NAME     'm','i','d','i','R','a','d','a','r'
#include <DigiMIDI.h>

#define PINLED 1  //onboard led
#define PINSENSOR 2 //radar sensor
#define NOTE 99  //midi note
#define VELO 64  //midi velocity
#define CHAN 9  //midi channel

DigiMIDIDevice midi;

int state = 0;

void setup() {
  pinMode(PINLED, OUTPUT);
  midi.sendNoteOff(NOTE, VELO, CHAN);

void loop() {

  if (digitalRead(PINSENSOR) == 1) {
    if (state == 0) {
      digitalWrite(PINLED, HIGH);
      midi.sendNoteOn(NOTE, VELO, CHAN);
      state = 1;
  } else {
    if (state == 1) {
      digitalWrite(PINLED, LOW);
      midi.sendNoteOff(NOTE, VELO, CHAN);
      state = 0;

The device shows up as a MIDI device and the following code is an example of how to connect to it in SuperCollider...

MIDIdef.noteOn(\radarOn, {|...args| [\radarOn, args].postln}, 99);
MIDIdef.noteOff(\radarOff, {|...args| [\radarOff, args].postln}, 99);



2017-12-16 11:49 electronics, supercollider

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 (see API) 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 especially problematic for MIDI time code (sync) messages. That said, in many situations this is ok and in my tests with a simple note on/off messages + bend and control, things seem to work just fine.

f0mid photo

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.

f0mid schematics

SuperCollider example code...


OSCFunc.trace(true, true);

n= NetAddr("f0mid.local", 18120);  //IP of ESP8266
n.sendMsg(\ip, 192, 168, 1, 99);  //receiver IP (laptop - by default this is x.x.x.255 (broadcast))
n.sendMsg(\port, 57120);  //set receiver port (by default this is 57120)

n.sendMsg(\thru, 0);  //off
n.sendMsg(\thru, 1);  //full (default)
n.sendMsg(\thru, 2);  //same channel
n.sendMsg(\thru, 3);  //different channel

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= EnvGen.ar(Env.adsr(0.01, 1, 0.85, 0.1), gate, amp, doneAction:2);
    var bend= In.kr(busBend).lag(0.01);
    var cf= In.kr(busCF).lag(0.01);
    var rq= In.kr(busRQ).lag(0.01);
    var vol= In.kr(busVol).lag(0.01);
    var pan= In.kr(busPan).lag(0.01);
    var src= BLowPass4.ar(VarSaw.ar((freq+bend).midicps), cf, rq);
    OffsetOut.ar(0, Pan2.ar(src*env, pan, vol));
  d= ();
  OSCdef(\f0mid, {|msg|
      \activeSensing, {},
      \noteOn, {
        d.at(msg[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, {
        d.at(msg[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;




2017-11-22 00:34 electronics

This simple addition/hack lets me control the Velleman K8064 DC dimmer kit via wireless OSC or serial. It's based on an ESP8266. The kit is isolated, can dim 220V/110V and is rated for up to 750W loads.

Normally one control it using 0-12V but by replacing a resistor (R16) it's possible to do it with the 0-3.3V PWM signal that the ESP outputs.

f0dim photo

I probably should cut some air ventilation holes, but so far it hasn't gotten hot inside.

f0dim schematics

Arduino code for the ESP8266...


// * install OSC from https://github.com/CNMAT/OSC
// * edit where it says EDIT below
// * choose board: "Generic ESP8266 Module" 160 MHz

//OSC protocol: /dim PWM fade
//  PWM should be 0.0-1.0
//  fade should be 0.0-1.0 (1.0=instant)
//serial protocol: 253 254 PWMhi PWMlo fadehi fadelow 255
//  PWM hi and lo should be 0-1023
//  fade hi and lo should be 0-1023 (1023=instant)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <OSCMessage.h>

#define PORT 15551  //receiving OSC port
#define PINPWM 0
#define PAYLOAD 4
#define UPDATERATE 16
#define TRIES 100  //how many times try check for wifi connection at startup

const char *ssid = "wifinetwork"; //EDIT your accessPoint network name
const char *password = "wifipassword";  //EDIT your password
const char *espname = "f0dim";
WiFiUDP Udp;
bool wifi;

byte serial_index = 0;
byte serial_data[PAYLOAD];
unsigned long next_time;
float dim = 0.0, dim_target = 0.0, dim_fade = 1.0;

void setup() {
  analogWriteFreq(5000);   //5khz pwm
  Serial.begin(115200, SERIAL_8N1, SERIAL_RX_ONLY);
  pinMode(PINPWM, OUTPUT);
  analogWrite(PINPWM, 0);
  WiFi.begin(ssid, password);
  byte cnt = 0;
  wifi = false;
  while ((WiFi.status() != WL_CONNECTED) && (cnt < TRIES)) {
    digitalWrite(LED_BUILTIN, cnt % 2);
  if (WiFi.status() == WL_CONNECTED) {
    wifi = true;
  digitalWrite(LED_BUILTIN, !wifi); //blue led on if connected to wifi

void oscDim(OSCMessage &msg) {
  dim_target = msg.getFloat(0);
  dim_fade = msg.getFloat(1);

void loop() {

  //--serial input
  while (Serial.available() > 0) {
    byte val = Serial.read();
    if ((serial_index == 0) && (val == 253)) {
      serial_index = 1;
    } else if ((serial_index == 1) && (val == 254)) {
      serial_index = 2;
    } else if ((serial_index >= 2) && (serial_index < (PAYLOAD + 2))) {
      serial_data[serial_index - 2] = val;
    } else if ((serial_index == (PAYLOAD + 2)) && (val == 255)) {
      dim_target = ((serial_data[0] << 8) + serial_data[1]) / 1023.0;
      dim_fade = ((serial_data[2] << 8) + serial_data[3]) / 1023.0;
      serial_index = 0;
    } else {
      serial_index = 0;

  //--OSC input
  if (wifi) {
    int packetSize = Udp.parsePacket();
    if (packetSize) {
      OSCMessage oscMsg;
      while (packetSize--) {
      if (!oscMsg.hasError()) {
        oscMsg.dispatch("/dim", oscDim);

  if (millis() >= next_time) {
    next_time = millis() + UPDATERATE;
    if (dim < dim_target) {
      dim = dim + dim_fade;
      if (dim > dim_target) {
        dim = dim_target;
    } else if (dim > dim_target) {
      dim = dim - dim_fade;
      if (dim < dim_target) {
        dim = dim_target;
    analogWrite(PINPWM, int(dim * 1023));

SuperCollider example code...

//f0dim can be controlled either via OSC or serial
n= NetAddr("", 15551);
n.sendMsg(\dim, 1.0, 1.0);  //set 100% instantly
n.sendMsg(\dim, 0.5, 1.0);  //set 50% instantly
n.sendMsg(\dim, 0.0, 1.0);  //set 0% instantly
n.sendMsg(\dim, 1.0, 0.001);  //slow fade in
n.sendMsg(\dim, 0.0, 0.0005);  //slower fade out

~port= SerialPort("/dev/tty.usbserial123", 115200, crtscts: true);  //EDIT serialport
f= {|pwm= 1023, fade= 1023|
  Int8Array[253, 254, pwm>>8, pwm%256, fade>>8, fade%256, 255];

~port.putAll(f.value(1023, 1023));  //set 100% instantly
~port.putAll(f.value(512, 1023));  //set 50% instantly
~port.putAll(f.value(0, 1023));  //set 0% instantly
~port.putAll(f.value(1023, 3));  //slow fade in
~port.putAll(f.value(0, 2));  //slower fade out


RPi Audio Codec

2017-10-12 15:30 supercollider

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 this 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...

alsamixer screenshot

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 with only 5.3ms latency.

Solar Powered SuperCollider

2017-08-01 17:07 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 live coding SuperCollider via the terminal or use the standard SC-IDE 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.


solarpoweredsupercollider photo 2


solarpoweredsupercollider photo 1

The connections are pretty straightforward...

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

The DC-DC 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 Zero. Note that the amplifier also needs 5V and here I have taken that from pins 4 and 6 on the RPi.

The power bank 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 as 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:

Download Raspbian Jessie (here Jessie Desktop 2017-07-05-raspbian-jessie.zip) and burn it onto the SD card with balenaEtcher.

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 tutorial)...

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 ar:a.ar(midicps(Duty.ar(c=a.ar(1/[12,8])+3/24,0,Dseq([0,8,5,1,5,4,5]*round(c*18),inf))+60),c*2)/4)}// #SuperCollider

If the sound is distorting try lowering the volume in alsamixer.

« 1 2 3 4 5 6 7 »