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
(image by atnel.pl, source http://sklep.atnel.pl/pl/p/WS2812B-TASMA-RGB-W-0,5m/98)
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
draw a jslogo
let canvas = ws281xCanvas.create(10, 10),
ctx = canvas.getContext('2d'),
image = new canvas.Image();
ws281x.init(100);
image.src = fs.readFileSync('js-logo-10x10.png');
ctx.drawImage(image, 0, 0);
ws281x.render(canvas.toUint32Array());
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