Let there be light!

Martin Schuhfuss

Hi!

Martin Schuhfuss | m.schuhfuss@gmail.com@usefulthink

homemade electronics

(things that look way more like bombs than a clock does)

jsconf.eu last year..

my "electronics-lab" now

What is this all about?

use web-technology to control electronics-projects

having fun.

with lighting, electronics and javascript

do a project that doesn't need to be anything.

(inspired by Brad Bouse: "Usefulness of Uselessness", jsconf.eu 2014)

Switching things

Relays

USB-Charger

(ripped apart & shrinkwrapped)

"Debug"-Interface

(USB <–> Serial Adapter)

Screw-Terminals 230V AC

ESP8266-Module

(Wifi & Control)

Programmer

(and supporting electronics)

Internals

SERIOUS WARNING

DO NOT MESS WITH MAINS-VOLTAGE UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING!

ESP8266

  • extremely cheap (~12€ for 5pcs)
  • put them into anything and leave it there
  • 80MHz CPU / 160kB RAM / 4MB Flash
  • already runs a Lua interpreter
  • iot.js and duktape could make JS possible

PLEASE MAKE THIS HAPPEN! <3

nodemcu

-- configure the wifi-module as network-client
wifi.setmode(wifi.STATION);
-- set SSID and passphrase and connect
wifi.sta.config("networkSSID",
    "correct horse battery staple!");
wifi.sta.connect();

-- after a few seconds, we have an IP from DHCP 
print(wifi.sta.getip());

connect to a network

-- handle HTTP post-requests for /sockets
urest.post('^/sockets', function(req, params, body)
    local data = cjson.decode(body);

    if data.state == 1 then
        gpio.write(GPIO_PIN[data.socket], gpio.HIGH);
    else
        gpio.write(GPIO_PIN[data.socket], gpio.LOW);
    end

    return { success = 1 };
end)

handle HTTP-requests

ESP8266 firmware running a Lua interpreter

DEMO

let requestBody = { 
  socket: 1,
  state: 1 
};

request({
  method: 'POST', 
  url: 'http://powerstrip.jsconf/sockets',
  json: true, 
  body: requestBody
});

switch all the things using HTTP 

LEDs

monopixel

Dimming LEDs

Dimming LEDs

just turn it off and on again...

PWM (pulse-width modulation)

ESP8266-Module

(Wifi & Control)

12W RGBW-LEDs

5V Power-Supply

LED Power-Supplies

(1 per channel)

Internals

OpenPixelControl

a simple TCP message format to control RGB-LEDs

(0x00 == setPixelColors)

(RGB, 3 byte per pixel)

openpixelcontrol-stream

import {OpcClientStream} from 'openpixelcontrol-stream';

let opcStream, 
    rainbowPosition = 0, 
    buffer = new Uint32Array(1);

function start() {
  opcStream = new OpcClientStream();
  opcStream.pipe(net.createConnection(7890, 'monopixel.jsconf'));

  setInterval(loop, 50);
}

const buffer = new Uint32Array(1);
function loop() { // <-- now running at 20FPS
  buffer[0] = rainbow(rainbowPosition++);
  opcStream.setPixelColors(9, buffer);
  
  rainbowPosition %= 256;
}

a stream implementation of the opc-protocol

Direct Control

websocket to server, server sends opc-messages

more LEDs

ws2812

aka Neopixel

  • tiny RGB-LEDs
  • independently addressable
  • controlled with a special data-signal that transports the color-data

rpi_ws281x

  • C-library written by Jeremy Garff
  • does a lot of complicated things with the CPU so we don't have to

rpi-ws281x-native

  • node addon written in C++
  • glue-code to make C-API usable from node.js
  • learned a lot about V8 that way
export default ws281x = {
    init(numLeds, options) { … },
    /** @param {Uint32Array} ledData */
    render(ledData) { … }, 
    reset: function() { … }
};

rainbow x 100

import ws281x from 'rpi-ws281x-native';

const NUM_LEDS = 100,
  pixelData = new Uint32Array(NUM_LEDS);

// ---- initialize the library
ws281x.init(NUM_LEDS);

// ---- animation-loop
let offset = 0;
setInterval(() => {
  for (var i = 0; i < NUM_LEDS; i++) {
    pixelData[i] = rainbow((offset + i) % 256);
  }

  offset = (offset + 1) % 256;
  ws281x.render(pixelData);
}, 1000 / 30);

(and this is all we need to get a rainbow on this box)

so we have an array of numbers

...but how to draw lines, circles, text, images?

<canvas>

  • 10x10 pixel canvas-element
  • just convert CanvasPixelArray to Uint32Array

we can also do GIFs.

well, actually its a sequence of png-files extracted from a GIF.
But we could do GIFs...

or just write code right away.

rendered in the browser and sent to the server.

// update state
state.x = 8 * Math.sin(t/800) + 4;

// render it..
ctx.clearRect(0,0,10,10);
ctx.fillStyle = 'yellow';
ctx.strokeStyle = 'red';
ctx.lineWidth = 1;

ctx.beginPath();
ctx.arc(state.x, 4, 3, 0, Math.PI*2, true);
ctx.closePath();

ctx.stroke();
ctx.fill();

Professional lighting

moving head spotlights

DMX512

  • protocol to control stage-equipment
  • one sender, multiple receivers
  • 512 Channels with 1 Byte each
  • full state is sent with up to 45 FPS
  • fixed addresses + multiple channels

DMX-Interface

  • Arduino UNO as USB-Interface
  • some more electronics to generate DMX-Signal

DMX-Driver

import {SerialPort} from 'serialport';
import DmxSerialDriver from '../lib/transport/DmxSerialDriver';

// create the driver for the serial protocol of the dmx-interface
const driver = new DmxSerialDriver(new SerialPort('/dev/cu.usbmodem1411', {
  baudRate: 115200
}));

// create the buffer to hold the values for all dmx-channels
const dmxBuffer = new Buffer(512);

// ... set dmx-values

driver.send(dmxBuffer);

Convert DMX-buffer into the protocol used by the USB-Interface

let base = 420; // device base-address (channel 421)

dmxBuffer[base + 0] = 128; // pan center
dmxBuffer[base + 2] = 128; // tilt center

dmxBuffer[base + 5] = 8; // dimmer: full brightness
dmxBuffer[base + 6] = 255; // color: full red
dmxBuffer[base + 7] = 0; // color: no green
dmxBuffer[base + 8] = 255; // color: full blue

setting values

  • vendor and device-specific channel-mapping
  • some features use multiple channels
  • some channels control multiple features

let's build an abstraction so we can stop thinking about byte-values, channels and array-indices.

once again an array of numbers

DmxDevice API

let device = new DmxDevice(421, paramDefinitions);

// values for motion in degrees
device.pan = 90;
device.tilt = 45;

device.dimmer = 1; // values [0..1] for most properties
device.color = 'magenta'; // css-color-value for RGB and CMY
  • hides vendor-sepcific channel-mappings
  • provides format-conversions (degrees, colors, ...)
  • getters/setters directly accessing DMX-buffer

Object.defineProperty() <3

let dmxBuffer = new Buffer(512),
  address = 420;

dmxBuffer[address + 0] = 128;
dmxBuffer[address + 2] = 128;
dmxBuffer[address + 5] = 8;
dmxBuffer[address + 6] = 255;
dmxBuffer[address + 7] = 0;
dmxBuffer[address + 8] = 255;
// define the device with it's parameters
const device = new DmxDevice(421, {
    pan: new HiResParam([1, 2], {min: -270, max: 270}),
    tilt: new HiResParam([3, 4], {min: -90, max: 110}),
    color: new RgbParam([7, 8, 9]),
    dimmer: new RangeParam(6, {rangeStart: 134, rangeEnd: 8})
});

// motion-values are in degrees
device.pan = 0;
device.tilt = 0;

// most parameters use values from 0 to 1
device.dimmer = 1;

// color accepts any valid css colorstring
device.color = 'magenta';

so this...

...can be written as

DmxDevice API

stop worrying about DMX being weird

DmxOutput

let output = new DmxOutput(new DmxSerialDriver(…));

let device = new DmxDevice(421, paramDefinitions);
device.setDmxBuffer(output.getBuffer());

output.start(20); // output will send dmx-data with 20 FPS
output.requestDmxFrame(loop);

// the output will call this before a dmx-frame is sent
function loop(time) {
    output.requestDmxFrame(loop);

    device.dimmer = 1;
    device.color = 'magenta';
    device.pan = Math.cos(time / 4000) * 180;
    device.tilt = Math.sin(time / 1000) * 80;
}

provides output-buffer and interval-handling

let's scale that up a little.

I got the lighting schedule for the conferences...

how to handle lots of devices?

introducing DeviceGroup and DeviceRegistry

let devices = [
    new DmxDevice(…),
    new DmxDevice(…),
    …
]

let group = new DeviceGroup(devices);

// groups provide the same interface 
// as the contained devices
group.setDmxBuffer(dmxBuffer);

group.dimmer = 1;
group.shutter = 'open';
group.color = 'green';
import registry from './jsconf/dmx-registry';

// registry provides access to devices using 
// something not unlike css class-names
let all = registry.getAll(),
    frontSpots = registry.select('.spot.front'),
    washlights = registry.select('.wash');

// ..and it doesn't really matter if we are 
// dealing with single devices or device-groups
all.pan = all.tilt = 0;
all.shutter = 'open';
all.dimmer = 1;
all.color = 'white';

washlights.dimmer = .5;
frontSpots.color = 'magenta';

groups of mixed device-types use the union of all members parameters

lets do that again.

const output = new DmxOutput(…);

// the output will call this before a dmx-frame is sent
const allSpots = registry.select('.spot'),
    leftSpots = registry.select('.spot.left'),
    rightSpots = registry.select('.spot.right');

output.start(20); // output will send dmx-data with 20 FPS
output.requestDmxFrame(loop);

function loop(time) {
    output.requestDmxFrame(loop);

    allSpots.dimmer = 1;
    allSpots.shutter = 'open';
    allSpots.color = 'magenta';

    leftSpots.pan = -90;
    rightSpots.pan = 90;
    
    allSpots.tilt = Math.sin(time/2000) * 45;
}

but there's even more

  • there are quite a lot of different settings
  • I need to stay sane editing them
  • So we need things like default-values, inheritance of settings and a simple syntax

css to the rescue

pretend dmx-params were css-properties

// define default-values for all devices...
* {
    pan: 0; tilt: 0;
    dimmer: 0; shutter: open;
    color: white;
}

// ...or just a specific subset of devices.
.spot {
    focus: .43;
    zoom: 0;
}

// define a light-setting to point a spot 
// on the mirrorball
.spot-on-mirrorball .spot.front.left { 
    pan: -48deg;
    tilt: -78.5deg; 
    color: white;
    dimmer: 1;
    iris: 1;
}
let dmxOutput = new DmxOutput(…);
let cueLoader = new CssCueLoader(dmxOutput);

dmxOutput.start(20);

cueLoader.loadCss(fs.readFileSync('styles.css'));
cueLoader.setCue('.spot-on-mirrorball');

just load the css-file and set a light-scene to run.

  • uses rework to parse css
  • "computed style" results from applying all properties in reverse specificity-order

finally...

  • codemirror-editor for scss-code
  • send scss to server
  • compile to css with node-sass
  • throw at lighting-css engine, see what happens

finally...

@import 'position-presets';

* {
  pan: 0; tilt: 0; speed: 1;
  dimmer: 0;
  color: white;
  shutter: open;

  .spot {
    focus: .4; zoom: 0; iris: 0;
    gobo: 0
  }
}

.demo {
  @extend .spot-position-mirrorball;
  @extend .wash-position-roof;

  .spot {
    dimmer: 1;
    shutter: open;
  }

  .wash {
    dimmer: 1;
    color: magenta;
  }
}

what's next?

  • that CSS-idea seems to actually work
  • implement transitions, animations
  • wire it up with other protocols, so i can write css for the light in my home.

Thank you so much.

You'll find me at the party :)

Martin Schuhfuss | m.schuhfuss@gmail.com@usefulthink

Let there be light! – jsconf.eu 2015

By Martin Schuhfuss

Let there be light! – jsconf.eu 2015

  • 3,370