diff --git a/.gitignore b/.gitignore
index 425213f..ca51fc1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
.piolibdeps
src/credentials.h
*.sublime-workspace
+web/node_modules/
diff --git a/case/StairCase.scad b/case/StairCase.scad
new file mode 100644
index 0000000..74bcce5
--- /dev/null
+++ b/case/StairCase.scad
@@ -0,0 +1,97 @@
+caseBottom();
+
+$fa = 1;
+$fs = 1;
+
+module caseBottom()
+{
+ innerX = 80;
+ innerY = 70;
+ innerZ = 35;
+ wallThickness = 2;
+
+ totalX = innerX + (2 * wallThickness);
+ totalY = innerY + (2 * wallThickness);
+ totalZ = innerZ + wallThickness;
+
+ difference()
+ {
+ cube([totalX, totalY, totalZ]);
+ translate([wallThickness, wallThickness, wallThickness])
+ cube([innerX, innerY, totalZ]);
+
+ // Hole for the LED cables
+ translate([-1, totalY / 2, 25])
+ rotate([0, 90, 0])
+ cylinder(d = 15, h = wallThickness + 2);
+
+ // Hole for the power cable
+ translate([62, totalY + 1, wallThickness + 2.5])
+ rotate([90, 0, 0])
+ cylinder(d = 5, h = wallThickness + 2);
+ }
+
+ translate([wallThickness, wallThickness, wallThickness])
+ {
+ translate([4, 4, 0])
+ PCA9685Mount();
+
+ translate([40, 20, 0])
+ ESP8266PlusPowerMount();
+ }
+}
+
+
+module PCA9685Mount()
+{
+ totalX = 25.5;
+ totalY = 62.5;
+ pinDistanceX = 19;
+ pinDistanceY = 56;
+ pinDiameter = 2.3;
+ pinHeight = 3;
+ supportDiameter = 4;
+ supportHeight = 4;
+
+ offsetX = (totalX - pinDistanceX) / 2;
+ offsetY = (totalY - pinDistanceY) / 2;
+
+ for (x = [offsetX, offsetX + pinDistanceX])
+ for (y = [offsetY, offsetY + pinDistanceY])
+ translate([x, y, 0])
+ {
+ cylinder(d = supportDiameter, h = supportHeight);
+ translate([0, 0, supportHeight])
+ cylinder(d = pinDiameter, h = pinHeight);
+ }
+}
+
+
+// I didn't put any actual mount points in the perfboard,
+// so I'll settle for some corner pieces to align it and
+// use the ultimate maker's friend: hot glue.
+module ESP8266PlusPowerMount()
+{
+ innerX = 34;
+ innerY = 35;
+ cornerSize = 6;
+ cornerThickness = 2;
+ cornerHeight = 2;
+
+ totalX = innerX + (2 * cornerThickness);
+ totalY = innerY + (2 * cornerThickness);
+
+ difference()
+ {
+ cube([totalX, totalY, cornerHeight]);
+
+ translate([cornerThickness, cornerThickness, -1])
+ cube([innerX, innerY, cornerHeight + 2]);
+
+ translate([-1, cornerSize, -1])
+ cube([totalX + 2, totalY - (2 * cornerSize), cornerHeight + 2]);
+
+ translate([cornerSize, -1, -1])
+ cube([totalX - (2 * cornerSize), totalY + 2, cornerHeight + 2]);
+ }
+}
\ No newline at end of file
diff --git a/docs/protocol.md b/docs/protocol.md
index dba5acd..a544416 100644
--- a/docs/protocol.md
+++ b/docs/protocol.md
@@ -110,5 +110,6 @@ Lights one step at a time, moving up or down.
Parameters:
**interval** (word): How long each step is lit before moving to the next.
+**brightness** (word): value in range 0 (off) to 4095 (fully on).
**direction** (byte): Determines the starting step / direction. Either Bottom/Up (0) or Top/Down (1).
**fadeOutTime** (word): If greater than 0 each step will fade out instead of turning off instantly after moving to the next. Specified in milliseconds.
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 96837d1..b550923 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -134,11 +134,13 @@ void handleRequest(uint8_t* packet)
{
case Command::Ping:
udpServer.write(Command::Ping);
+ udpServer.write(StepCount);
break;
case Command::GetMode:
udpServer.write(Command::GetMode);
udpServer.write(currentModeIdentifier);
+ currentMode->write(&udpServer);
break;
case Command::SetMode:
@@ -163,6 +165,7 @@ void handleRequest(uint8_t* packet)
{
udpServer.write(Command::Error);
udpServer.write(Command::SetMode);
+ udpServer.write(newIdentifier);
}
break;
@@ -186,7 +189,7 @@ IMode* createMode(uint8_t identifier)
switch (identifier)
{
case Mode::Static: return new StaticMode();
- //case Mode::Custom: return new CustomMode();
+ case Mode::Custom: return new CustomMode();
case Mode::Alternate: return new AlternateMode();
//case Mode::Slide: return new SlideMode();
//case Mode::ADC: return new ADCInputMode();
diff --git a/web/app.js b/web/app.js
index d110d35..cbdadb3 100644
--- a/web/app.js
+++ b/web/app.js
@@ -1,50 +1,55 @@
-var protocol = require('./protocol');
-var dgram = require('dgram');
+var express = require('express');
+var client = require('./client');
-var on = 0;
-var speed = 256;
+var httpPort = 3127;
+
+var stairsHost = '10.138.2.12';
+var stairsUdpPort = 3126;
-function lsb(value) { return value & 0xFF; }
-function msb(value) { return (value >> 8) & 0xFF; }
+client.init(stairsHost, stairsUdpPort);
+var app = express();
-
-/*
-Alternating
-
-var message = new Buffer([protocol.Command.SetMode, protocol.Mode.Alternate, lsb(500), msb(500), lsb(128), msb(128)]);
-client.send(message, 0, message.length, 3126, '10.138.2.12', function(err, bytes) {
- if (err) throw err;
- console.log('UDP message sent');
-});
-*/
-
-
-var client = dgram.createSocket('udp4');
-client.on('listening', function()
+app.get('/ping', function(req, res)
{
- var address = client.address();
- console.log('UDP client listening on ' + address.address + ":" + address.port);
-});
+ client.ping(function(data, error)
+ {
+ if (error)
+ res.status(500);
-client.on('message', function (message, remote)
-{
- console.log('< ' + remote.address + ':' + remote.port +' - ' + message.toString('hex'));
-});
-
-
-setInterval(function()
-{
- // 0x00, 0x10 = 4096
- on += speed;
- if (on <= 0 || on >= 4096)
- speed = -speed;
-
- var message = new Buffer([protocol.Command.SetMode, protocol.Mode.Static, lsb(on), msb(on)]);
- client.send(message, 0, message.length, 3126, '10.138.2.12', function(err, bytes) {
- if (err) throw err;
- console.log('> ' + '10.138.2.12' + ':' + '3126' + ' - ' + message.toString('hex'));
+ res.send(data);
});
-}, 200);
\ No newline at end of file
+});
+
+app.get('/getMode', function(req, res)
+{
+ client.getMode(function(data, error)
+ {
+ if (error)
+ res.status(500);
+
+ res.send(data);
+ });
+});
+
+app.get('/setMode/:mode', function(req, res)
+{
+ client.setMode(req.params.mode, req.query, function(data, error)
+ {
+ if (error)
+ res.status(500);
+
+ res.send(data);
+ });
+});
+
+app.use(express.static('static'));
+
+
+
+app.listen(httpPort, function ()
+{
+ console.log('Stairs ReST service running on port ' + httpPort);
+});
\ No newline at end of file
diff --git a/web/client.js b/web/client.js
new file mode 100644
index 0000000..c314f22
--- /dev/null
+++ b/web/client.js
@@ -0,0 +1,233 @@
+var dgram = require('dgram');
+var protocol = require('./protocol');
+var BufferReader = require('buffer-reader');
+
+
+var responseHandlers = {};
+
+function registerResponseHandler(command, callback)
+{
+ if (!responseHandlers.hasOwnProperty(command))
+ responseHandlers[command] = [callback];
+ else
+ responseHandlers[command].push(callback);
+}
+
+
+function callResponseHandlers(command, reader, error)
+{
+ if (!responseHandlers.hasOwnProperty(command))
+ return;
+
+ newHandlers = [];
+ responseHandlers[command].forEach(function(callback)
+ {
+ if (!callback(reader, error))
+ newHandlers.push(callback);
+ });
+
+ responseHandlers[command] = newHandlers;
+}
+
+
+var serverHost = '';
+var serverPort = 0;
+
+var client = dgram.createSocket('udp4');
+client.on('message', function (message, remote)
+{
+ console.log(message.toString('hex'));
+ if (message.length < 2)
+ return;
+
+ var reader = new BufferReader(message);
+
+ if (reader.nextInt8() !== protocol.Command.Reply)
+ return;
+
+ var command = reader.nextInt8();
+ if (command === protocol.Command.Error)
+ callResponseHandlers(reader.nextInt8(), reader, true)
+ else
+ callResponseHandlers(command, reader, false);
+});
+
+
+function requestResponse(buffer, callback)
+{
+ if (buffer === null || buffer.length == 0) return;
+ console.log('> ' + buffer.toString('hex'));
+
+ var command = buffer.readInt8(0);
+ var cancelled = false;
+
+ var timeout = setTimeout(function()
+ {
+ cancelled = true;
+ callback(null, true);
+ clearTimeout(timeout);
+ }, 2000);
+
+ registerResponseHandler(command, function(reader, error)
+ {
+ if (cancelled) return;
+ clearTimeout(timeout);
+
+ callback(reader, error);
+ return true;
+ });
+
+ client.send(buffer, 0, buffer.length, serverPort, serverHost, function(err, bytes)
+ {
+ if (err)
+ onError();
+ });
+}
+
+
+function readModeData(mode, reader)
+{
+ switch (mode)
+ {
+ case protocol.Mode.Static:
+ return {
+ brightness: reader.nextInt16LE()
+ };
+
+ case protocol.Mode.Custom:
+ var values = [];
+ while (reader.tell() < reader.buf.length)
+ values.push(reader.nextInt16LE());
+
+ return {
+ brightness: values
+ };
+
+ case protocol.Mode.Alternate:
+ return {
+ interval: reader.nextInt16LE(),
+ brightness: reader.nextInt16LE()
+ };
+
+ case protocol.Mode.Slide:
+ return {
+ interval: reader.nextInt16LE(),
+ brightness: reader.nextInt16LE(),
+ direction: reader.nextInt8(),
+ fadeOutTime: reader.nextInt16LE()
+ };
+ }
+
+ return null;
+}
+
+
+function lsb(value) { return value & 0xFF; }
+function msb(value) { return (value >> 8) & 0xFF; }
+
+function writeModeData(mode, data)
+{
+ switch (mode)
+ {
+ case protocol.Mode.Static:
+ if (typeof(data.brightness) == 'undefined') data.brightness = 0;
+
+ return new Buffer([protocol.Command.SetMode, mode, lsb(data.brightness), msb(data.brightness)]);
+
+ case protocol.Mode.Custom:
+ var brightness = typeof(data.brightness) !== 'undefined' ? data.brightness.split(',') : [];
+
+ var valueCount = Math.min(16, brightness.length);
+ var buffer = Buffer.alloc(2 + (valueCount * 2));
+ buffer.writeInt8(protocol.Command.SetMode, 0);
+ buffer.writeInt8(mode, 1);
+
+ for (var index = 0; index < valueCount; index++)
+ buffer.writeInt16LE(brightness[index], 2 + (index * 2));
+
+ return buffer;
+
+ case protocol.Mode.Alternate:
+ if (typeof(data.brightness) == 'undefined') data.brightness = 0;
+ if (typeof(data.interval) == 'undefined') data.interval = 500;
+
+ return new Buffer([protocol.Command.SetMode, mode,
+ lsb(data.interval), msb(data.interval),
+ lsb(data.brightness), msb(data.brightness)]);
+
+ case protocol.Mode.Slide:
+ if (typeof(data.brightness) == 'undefined') data.brightness = 0;
+ if (typeof(data.interval) == 'undefined') data.interval = 500;
+ if (typeof(data.direction) == 'undefined') data.direction = 0;
+ if (typeof(data.fadeOutTime) == 'undefined') data.fadeOutTime = 0;
+
+ return new Buffer([protocol.Command.SetMode, mode,
+ lsb(data.interval), msb(data.interval),
+ lsb(data.brightness), msb(data.brightness),
+ data.direction,
+ lsb(data.fadeOutTime), msb(data.fadeOutTime)]);
+ }
+}
+
+
+module.exports =
+{
+ init: function(host, port)
+ {
+ serverHost = host;
+ serverPort = port;
+ },
+
+
+ ping: function(callback)
+ {
+ requestResponse(new Buffer([protocol.Command.Ping]),
+ function(reader, error)
+ {
+ if (!error)
+ {
+ callback(
+ {
+ stepCount: reader.nextInt8()
+ }, false);
+ }
+ else
+ callback(null, true);
+ });
+ },
+
+ getMode: function(callback)
+ {
+ requestResponse(new Buffer([protocol.Command.GetMode]),
+ function(reader, error)
+ {
+ if (!error)
+ {
+ var data = { mode: reader.nextInt8() };
+ data.data = readModeData(data.mode, reader);
+ callback(data, false);
+ }
+ else
+ callback(null, true);
+ });
+ },
+
+ setMode: function(mode, data, callback)
+ {
+ if (!protocol.Mode.hasOwnProperty(mode))
+ return;
+
+ requestResponse(writeModeData(protocol.Mode[mode], data),
+ function(reader, error)
+ {
+ if (!error)
+ {
+ var data = { mode: reader.nextInt8() };
+ data.data = readModeData(data.mode, reader);
+ callback(data, false);
+ }
+ else
+ callback(null, true);
+ });
+ }
+}
\ No newline at end of file
diff --git a/web/package.json b/web/package.json
index 2056609..77a8089 100644
--- a/web/package.json
+++ b/web/package.json
@@ -4,5 +4,9 @@
"description": "Stairs lighting project",
"main": "app.js",
"author": "Mark van Renswoude",
- "license": "ISC"
+ "license": "ISC",
+ "dependencies": {
+ "buffer-reader": "^0.1.0",
+ "express": "^4.15.2"
+ }
}
diff --git a/web/static/index.html b/web/static/index.html
new file mode 100644
index 0000000..4e5c0fa
--- /dev/null
+++ b/web/static/index.html
@@ -0,0 +1,66 @@
+
+
+