Merge tag '2.0' into develop

Release version 2.0
This commit is contained in:
Mark van Renswoude 2018-02-17 23:13:11 +01:00
commit afd9390523
159 changed files with 14609 additions and 11451 deletions

9
.gitignore vendored
View File

@ -1,9 +1,6 @@
.pioenvs
.piolibdeps
src/credentials.h
bin
*.sublime-workspace
web/node_modules/
web/update/
src/version.h
web/version.js
web/static/bower_components/
node_modules
src/secret.h

325
API.md Normal file
View File

@ -0,0 +1,325 @@
# API
- [GET /api/set](#get-apiset)
- [GET /api/status](#get-apistatus)
- [GET /api/connection/status](#get-apiconnectionstatus)
- [GET /api/connection](#get-apiconnection)
- [POST /api/connection](#post-apiconnection)
- [GET /api/system](#get-apisystem)
- [POST /api/system](#post-apisystem)
- [GET /api/steps/values](#get-apistepsvalues)
- [POST /api/steps/values](#post-apistepsvalues)
- [GET /api/triggers/time](#get-apitriggerstime)
- [POST /api/triggers/time](#post-apitriggerstime)
- [GET /api/triggers/motion](#get-apitriggersmotion)
- [POST /api/triggers/motion](#post-apitriggersmotion)
- [POST /api/firmware](#post-apifirmware)
- [GET /api/stacktrace/get](#get-apistacktraceget)
- [GET /api/stacktrace/delete](#get-apistacktracedelete)
#### Debug API
- [GET /api/crash/exception](#get-apicrashexception)
- [GET /api/crash/softwdt](#get-apicrashsoftwdt)
- [GET /api/crash/wdt](#get-apicrashwdt)
## GET /api/set
Intended for integration into scripts or home automation systems, like Domoticz, where it's often easier to perform GET calls than it is to post JSON data.
The following query parameters are supported:
| Parameter name | Description |
| -------------- | - |
| value | Brightness value from 0 to 255. Applied to all steps. |
| percent | Percentage value from 0 to 100. Same behaviour as value. |
| time | Optional. Transition time in milliseconds. |
| from | Optional. Where to start the fade. Can be either 'top' or 'bottom'. If omitted or any other value, all steps change brightness at the same time. |
Either value or percent is required.
*Example request*
```http://192.168.4.1/api/set?percent=50&time=1000&from=top```
## GET /api/status
Returns the unique identifier of the chip, the version of the firmware and various other bits of status information.
*Example response:*
```json
{
"systemID": "st41r",
"version": "2.0.0-beta.1+6",
time: 1518467160,
timeOffset: 3600,
resetReason: 2,
stackTrace: true
}
```
## GET /api/connection/status
Returns the status of the WiFi connections.
The value of the 'status' element corresponds to the ```wl_status_t``` enum as defined in [wl_definitions.h](https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/include/wl_definitions.h).
*Example response:*
```json
{
"ap": {
"enabled": true,
"ip": "192.168.4.1"
},
"station": {
"enabled": true,
"status": 3,
"ip": "10.138.1.10"
}
}
```
## GET /api/connection
Returns the settings of the WiFi connections.
*Example response:*
```json
{
"hostname": "stairs",
"accesspoint": true,
"station": true,
"ssid": "MyWiFi",
"password": "12345678",
"dhcp": true,
"ip": "",
"subnetmask": "",
"gateway": ""
}
```
## POST /api/connection
Updates the settings of the WiFi connections. The module will apply the new settings immediately and will break existing connections.
*Example request:*
```json
{
"hostname": "LivingRoomStairs",
"accesspoint": false,
"station": true,
"ssid": "MyWiFi",
"password": "12345678",
"dhcp": false,
"ip": "10.138.1.100",
"subnetmask": "255.255.255.0",
"gateway": "10.138.1.1"
}
```
## GET /api/system
*Example response:*
```json
{
"ntpServer": "eu.pool.ntp.org",
"ntpInterval": 5,
"lat": 52.370216,
"lng": 4.895168,
"pins": {
"ledAP": 4,
"ledSTA": 5,
"apButton": 2,
"pwmSDA": 13,
"pwmSCL": 12
},
"pwmAddress": 64,
"pwmFrequency": 1600,
"mapsAPIKey": ""
}
```
## POST /api/system
## GET /api/steps/values
Returns the current brightness value for each step. The number of items in the array is equal to the number of configured steps. Each value has a range of 0 to 255.
*Example response:*
```json
[
0, 10, 30, 50, 80, 110, 130, 150,
160, 170, 180, 190, 200, 230, 255
]
```
## POST /api/steps/values
Changes the brightness value for each step. If the number of values in the array is less than the number of configured steps, each subsequent step is considered to be off.
An optional element 'transitionTime' can be included which specifies how long the transition from the current value of each step to it's new value should take, the module will then smoothly fade between the values. The transition time must be specified in milliseconds. Assume a maximum of 30 seconds, because I did not test with higher values. Ain't nobody got time for that! If no transition time or 0 is specified, the new values will be applied immediately.
An optional array 'startTime' can be included which specifies the delay, for each step individually, before the transition will start. The example request uses this to create a sweeping effect. If no or not enough values are provided, they are assumed to be 0.
*Example request:*
```json
{
"transitionTime": 500,
"values": [
128, 128, 128, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128
],
"startTime": [
0, 50, 100, 150, 200, 250, 300, 350,
400, 450, 500, 550, 600, 650, 700
]
}
```
## GET /api/triggers/time
Returns the current settings for the time triggers.
time:
Meaning depends on the triggerType
| triggerType | description |
| ----------- | - |
| 0 | fixed time, set time to the number of minutes since midnight |
| 1 | relative to sunrise, set time to the number of minutes before or after sunrise to trigger (negative numbers mean before sunrise) |
| 2 | relative to sunset, set time to the number of minutes before or after sunset to trigger (negative numbers mean before sunset) |
daysOfWeek:
Flags determining which days of the week the trigger is active.
| value | day |
| ----- | --- |
| 1 | Monday |
| 2 | Tuesday |
| 4 | Wednesday |
| 8 | Thursday |
| 16 | Friday |
| 32 | Saturday |
| 64 | Sunday |
Therefore 127 means every day of the week.
brightness: value from 0 to 255
enabled: whether or not this trigger is enabled
*Example response:*
```json
{
"enabled": true,
"transitionTime": 1000,
"triggers": [
{
"time": 480,
"daysOfWeek": 127,
"brightness": 0,
"triggerType": 0,
"enabled": true
},
{
"time": 1200,
"daysOfWeek": 127,
"brightness": 255,
"triggerType": 0,
"enabled": true
}
]
}
```
## POST /api/triggers/time
Changes the time trigger settings. Request body format is the same as is returned in the GET request.
If the "triggers" array is omitted entirely, the items will not be cleared or overwritten.
## GET /api/triggers/motion
Returns the current settings for the motion triggers.
delay: How long to keep the lights on after the last sensor stops detecting motion.
pin: GPIO pin to which the motion sensor is connected. High is assumed to be active.
direction:
Enumeration determining from which side the sweep animation starts if transitionTime is set.
| value | description |
| ----- | --- |
| 1 | Non-directional. All steps change brightness at the same time. |
| 2 | Top-down. Starts a sweeping fade from the top step. |
| 3 | Bottom-up. Starts a sweeping fade from the bottom step. |
brightness: value from 0 to 255
enabled: whether or not this trigger is enabled
*Example response:*
```json
{
"enabled": true,
"enabledDuringTimeTrigger": true,
"enabledDuringDay": false,
"transitionTime": 1000,
"delay": 30000,
"triggers": [
{
"pin": 14,
"brightness": 64,
"direction": 2,
"enabled": true
},
{
"pin": 15,
"brightness": 64,
"direction": 3,
"enabled": true
}
]
}
```
## POST /api/triggers/motion
Changes the motion trigger settings. Request body format is the same as is returned in the GET request.
If the "triggers" array is omitted entirely, the items will not be cleared or overwritten.
## POST /api/firmware
Uploads new firmware. The bin file should be posted as a multipart/form-data file attachment. Name is not relevant.
## GET /api/stacktrace/get
If an exception occurs and the stack trace was recorded before the device reset, this will return the stack trace as a file named "stacktrace.txt".
## GET /api/stacktrace/delete
Removes any recorded stack trace.
# Debug API
These APIs are hopefully never enabled unless you've changed the config.h for the purpose of testing the exception handler. Don't forget to turn it back off afterwards.
## GET /api/crash/exception
Causes a crash due to an unhandled exception. Should provide a stack trace afterwards.
## GET /api/crash/softwdt
Causes the software watchdog to reset.
## GET /api/crash/wdt
Disables the software watchdog and causes the hardware watchdog.

61
DEVELOPING.md Normal file
View File

@ -0,0 +1,61 @@
# Stairs
This Stairs firmware was developed using [PlatformIO Core](http://platformio.org/). You can probably use the PlatformIO IDE as well, although I have no experience using it, so this guide will only use the command line tools.
## Programming the ESP8266
You can either use an ESP8266 module with built-in USB like the Wemos D1, a programming fixture (my method of choice, search for "esp8266 fixture" on Google or AliExpress) or wire it up yourself using a CH340 or FTDI USB-to-serial module.
To upload the code, open a console, go to the Stairs folder and run:
```
platformio run -t upload
```
It should auto-detect the USB COM port.
## Frontend development
The frontend is compiled into C++ source files so that all the files can be served directly from the ESP8266, since there is no internet connection when running in access point mode. These steps are performed by a [Gulp script](https://gulpjs.com/). The Gulp script also updates the version based on the [GitVersion](http://gitversion.readthedocs.io/en/stable/) of the working copy.
Note that GitVersion requires Windows, so some changes are probably required if you want to build on a different platform.
To get started:
1. Install [Node.js](https://nodejs.org/en/)
1. Install the [GitVersion command line](http://gitversion.readthedocs.io/en/stable/usage/command-line/) tool
1. Open a command line and navigate to the Stairs folder
1. Run ```npm update``` to install all the dependencies
### Compiling the assets
Run ```gulp``` to compile the SASS files, and embed the CSS, JavaScript, images and HTML into C++ header files located in the src\assets folder.
You may need to run ```npm install -g gulp``` to install Gulp into the global Node packages.
### Development server
To make it easier to develop the frontend, a development server is included which serves the webpages and acts as a mock service for the API.
To start the development server, run:
```node devserver.js```
You can now open the frontend on [http://localhost:3000/](http://localhost:3000/).
If you make any changes to the SCSS files, make sure to run ```gulp compileSass``` and ```gulp compileJS``` to update the CSS/JS files so your changes are visible in the development server. To keep gulp running and watch for changes in the SCSS and JS files, run ```gulp watch```
## Building and/or uploading
To rebuild all the assets and compile or upload the source in one go, two tasks have been added to the gulpfile.js:
1. ```gulp build``` first runs all the tasks run by a regular ```gulp```, then builds the source code using ```platformio run```
1. ```gulp upload``` is similar, but executes ```platformio run -t upload``` to directly upload the newly compiled source to the ESP8266
### version.h
The version.h file is generated based on the current GitVersion, which means it changes if you build again right after committing, which causes a change that needs to be committed... [did you mean: recursion?](https://www.google.nl/search?q=recursion) The best way I found to deal with this is commit your changes, build, then [amend the commit](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) with the updated version.h before pushing the changes. This ensures the version.h is in sync when cloning the repository.

17
README.md Normal file
View File

@ -0,0 +1,17 @@
# Stairs
ESP8266 firmware for controlling up to 16 LED strips on a flight of stairs.
## Features
- Configurable using WiFi (can act as an access point or connect to a router)
- Turn on or off at specific times
- Turn on when movement is detected by connecting one or two PIR sensors
- ReST API for configuration and controlling the lights
Most notably it does not support RGB LED strips out of the box, but feel free to add support if you're up to the task!
## Further reading
- [Wiring information](WIRING.md)
- [Programming the ESP8266 and/or modifying the source](DEVELOPING.md)

View File

@ -5,18 +5,21 @@
"path": ".",
"file_exclude_patterns": ["*.sublime-project"]
}
]/*,
"build_systems":
[
{
"name": "PlatformIO - Build",
"cmd": ["platformio", "run"],
"working_dir": "$project_path"
},
{
"name": "PlatformIO - Upload",
"cmd": ["platformio", "run", "--target", "upload"],
"working_dir": "$project_path"
],
"completions":[
["t", "{{ \\$t('${1:}') }}"]
],
"settings": {
"todoreview": {
"exclude_folders": [
"*.pioenvs*",
"*.piolibdeps*",
"*bin*",
"*node_modules*"
],
"patterns": {
"TODO": "TODO[\\s]*(?P<todo>.*)$",
}
}
]*/
}
}

5
WIRING.md Normal file
View File

@ -0,0 +1,5 @@
## Bill of materials
1. ESP8266 module (I based mine on a vanilla ESP8266 ESP-12F, but friendlier modules like the Wemos D1 should work as well)
1. PCA9685 16-channel PWM module
1. TODO complete this list

View File

@ -1,3 +0,0 @@
& .\updateversion.ps1
& platformio run
Copy-Item .\.pioenvs\esp12e\firmware.bin .\web\update\

View File

@ -1,97 +0,0 @@
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]);
}
}

256
devserver.js Normal file
View File

@ -0,0 +1,256 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use(express.static('web'));
app.use(express.static('web/dist'));
app.get('/api/status', function(req, res)
{
res.send({
systemID: 'dev-server',
version: 'dev-server',
time: 1518467160,
timeOffset: 3600,
resetReason: 2,
stackTrace: true
});
});
app.get('/api/connection', function(req, res)
{
res.send({
hostname: 'dev-server',
accesspoint: true,
station: true,
ssid: 'MyWiFiSSID',
password: 'supersecret',
dhcp: true,
ip: '192.168.1.234',
subnetmask: '255.255.255.0',
gateway: '192.168.1.0'
});
});
app.get('/api/connection/status', function(req, res)
{
res.send({
"ap": {
"enabled": true,
"ip": "192.168.4.1"
},
"station": {
"enabled": true,
"status": 1,
"ip": "0.0.0.0"
}
});
});
app.post('/api/connection', function(req, res)
{
res.sendStatus(200);
});
app.post('/api/firmware', function(req, res)
{
res.sendStatus(200);
});
var system = {
ntpServer: "eu.pool.ntp.org",
ntpInterval: 300,
lat: 52.370216,
lng: 4.895168,
pins: {
ledAP: 4,
ledSTA: 5,
apButton: 2,
pwmSDA: 13,
pwmSCL: 12
},
pwmAddress: 64,
pwmFrequency: 1600,
mapsAPIKey: ""
};
app.get('/api/system', function(req, res)
{
res.send(system);
});
app.post('/api/system', function(req, res)
{
var body = req.body;
if (body)
{
system.lat = body.lat || system.lat;
system.lng = body.lng || system.lng;
system.pins.ledAP = body.pins.ledAP || system.pins.ledAP;
system.pins.ledSTA = body.pins.ledSTA || system.pins.ledSTA;
system.pins.apButton = body.pins.apButton || system.pins.apButton;
system.pins.pwmSDA = body.pins.pwmSDA || system.pins.pwmSDA;
system.pins.pwmSCL = body.pins.pwmSCL || system.pins.pwmSCL;
system.pwmAddress = body.pwmAddress || system.pwmAddress;
system.pwmFrequency = body.pwmFrequency || system.pwmFrequency;
system.mapsAPIKey = body.mapsAPIKey || system.mapsAPIKey;
res.sendStatus(200);
}
else
res.sendStatus(400);
});
var steps = {
count: 14,
useCurve: true,
ranges: [
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 },
{ start: 0, end: 4095 }
]
};
app.get('/api/steps', function(req, res)
{
res.send(steps);
});
app.post('/api/steps', function(req, res)
{
steps = req.body;
res.sendStatus(200);
});
var stepsValues = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
app.get('/api/steps/values', function(req, res)
{
res.send(stepsValues);
});
app.post('/api/steps/values', function(req, res)
{
var body = req.body;
if (body && body.hasOwnProperty('values'))
{
for (var i = 0; i < Math.min(stepsValues.length, body.values.length); i++)
stepsValues[i] = parseInt(body.values[i], 10) || 0;
res.sendStatus(200);
}
else
res.sendStatus(400);
});
var timeTriggers = {
"enabled": true,
"transitionTime": 1000,
"triggers": [
{
"time": 480,
"daysOfWeek": 127,
"brightness": 0,
"triggerType": 0,
"enabled": true
},
{
"time": 1200,
"daysOfWeek": 127,
"brightness": 255,
"triggerType": 0,
"enabled": true
}
]
};
app.get('/api/triggers/time', function(req, res)
{
res.send(timeTriggers);
});
app.post('/api/triggers/time', function(req, res)
{
res.sendStatus(200);
});
var motionTriggers = {
enabled: true,
enabledDuringTimeTrigger: false,
enabledDuringDay: true,
transitionTime: 1000,
delay: 30000,
triggers: [
{
pin: 14,
brightness: 64,
direction: 2,
enabled: true
},
{
pin: 15,
brightness: 64,
direction: 3,
enabled: true
}
]
};
app.get('/api/triggers/motion', function(req, res)
{
res.send(motionTriggers);
});
app.post('/api/triggers/motion', function(req, res)
{
res.sendStatus(200);
});
app.get('/api/stacktrace/get', function(req, res)
{
res.send("Nothing to see here, move along!");
});
app.get('/api/stacktrace/delete', function(req, res)
{
res.sendStatus(200);
});
app.listen(3000, function()
{
console.log('Development server listening on port 3000')
console.log('Press Ctrl-C to stop')
});

Binary file not shown.

Binary file not shown.

BIN
docs/PIR sensor.fzpz Normal file

Binary file not shown.

4889
docs/Pinout.ai Normal file

File diff suppressed because one or more lines are too long

BIN
docs/Wiring.fzz Normal file

Binary file not shown.

BIN
docs/Wiring.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

View File

@ -1,170 +0,0 @@
The Stairs firmware on the ESP8266 can be accessed using a custom light-weight UDP protocol. It is not intended to be accessed directly from the internet, and thus there is no security on the device itself. This is by design. Authentication should in my opinion be handled by a device with more processing power, such as a Raspberry Pi.
A Node.js application is included which provides a ReST interface. It is also not intended to be accessible from the internet, and does not provide any authentication either!
Protocol
========
The default port for UDP communication is 3126. Every request message will result in a response message which is either an error, the requested information or a confirmation of the newly stored data. Each message should be a separate packet.
16-bit (word) values are expected in little endian order (least significant byte first).
Request
-------
The first byte of a request is the command. Further data depends on the command.
| Command | Name |
| ------- | ---- |
| 0x01 | Ping |
| 0x03 | GetMode |
| 0x04 | SetMode |
| 0x05 | GetRange |
| 0x06 | SetRange |
| 0xFF | UpdateFirmware |
Response
--------
A response is sent to the source address and port and starts with the Reply command.
| Command | Name |
| ------- | ---- |
| 0x02 | Reply |
The second byte is the request command for which this reply is intended, or Error if the request command was not recognized or contained invalid parameters.
| Command | Name |
| ------- | ---- |
| 0x00 | Error |
In the case of an Error, the third byte will be the actual request command (or unrecognized value).
### Ping
A no-op command which can be used to tell if the device is responding. Returns the number of steps.
Input parameters:<br>
_none_
Output parameters:<br>
**steps** (byte): The number of steps.
### GetMode
Returns the current mode.
Input parameters:<br>
_none_
Output parameters:<br>
**mode** (byte): The identifier of the current mode. See Modes below.<br>
**data** (0..n bytes): The parameters specific to the current mode. See Modes below.
### SetMode
Changed the current mode.
Input parameters:<br>
**mode** (byte): The identifier of the current mode.<br>
**data** (0..n bytes): The parameters specific to the current mode.
Output parameters:<br>
_Same as input parameters_
### GetRange
Gets the current range configuration. Each step has it's own parameters which adjust the PWM curve, which can be used to compensate for the differences in LED strips or signal strength.
The ranges are stored on the ESP8266's filesystem and will be restored after a power loss.
Input parameters:<br>
_none_
Output parameters:<br>
*useScaling* (byte): 0 (off) or 1 (on), default 1. If enabled, the brightness value will be converted to a PWM value using an exponential function. If disabled, the brightness is used as is (but still accounting for rangeStart/rangeEnd).
_repeated for each step:_<br>
*rangeStart* (word): value in range 0 (off) to 4095 (fully on), default 0. Determines the minimum PWM on value for a brightness of 1.
*rangeEnd* (word): value in range 0 (off) to 4095 (fully on), default 4094. Determines the maximum PWM on value for a brightness of 4094.
### SetRange
Sets the current range configuration.
Input parameters:<br>
_See GetRange_
Output parameters:<br>
_Same as input parameters_
### UpdateFirmware
Updates the firmware over WiFi. This functionality may be limited in the configuration which is hardcoded into the firmware. There are three modes of operation:
1. Disabled, firmware can not be updated over WiFi
2. Enabled with a hardcoded URL. You may request an update check but any source provided will be ignored.
3. Enabled with the URL as input parameters.
You can only request an update once every 5 seconds (by default), otherwise an error will be returned.
Please note that the entire packet must not exceed 254 bytes or the remainder will be discarded.
For further information on implementing an update server, see [Arduino ESP8266's HTTPUpdate reference](https://github.com/esp8266/Arduino/blob/master/doc/ota_updates/readme.md#advanced-updater-1).
Input parameters:<br>
_If enabled_
*port* (word): the port on which the HTTP update server runs
*host* (null-terminated string): the host name or IP address on which the HTTP update server runs
*path* (null-terminated string): the path to
Output parameters:<br>
1 if succesful, 0 if no updates are available.
Modes
=====
| Mode | Name |
| ---- | ---- |
| 0x01 | Static |
| 0x02 | Custom |
| 0x03 | Alternate |
| 0x04 | Slide |
### Static
Sets all steps to the same brightness.
Parameters:<br>
**brightness** (word): value in range 0 (off) to 4095 (fully on).
**easeTime** (word): the time in milliseconds to ease into the new brightness value (only applies if the mode was already set to static before).
### Custom
Sets the brightness for each of the steps individually.
Parameters:<br>
**brightness** (word[stepCount]): array of brightness values in range 0 - 4095. The number of values must be equal to the number of steps are reported in the Ping response. Bottom step first.
### Alternate
Alternates between even and odd steps being lit. Bring out our next contestant!
Parameters:<br>
**interval** (word): The time each set of steps is lit in milliseconds.<br>
**brightness** (word): value in range 0 (off) to 4095 (fully on).
### Slide
Lights one step at a time, moving up or down.
Parameters:<br>
**interval** (word): How long each step is lit before moving to the next.<br>
**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).<br>
**fadeOutTime** (word): If greater than 0 each step will fade out instead of turning off instantly after moving to the next. Specified in milliseconds.

168
gulp-cppstringify.js Normal file
View File

@ -0,0 +1,168 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
'use strict';
// Borrowed heavily from gulp-concat:
// https://github.com/contra/gulp-concat/
//
// It's very much hardcoded for the ESP8266 Arduino at the moment,
// but feel free to hack away at it if you need it for other purposes!
var through = require('through2');
var path = require('path');
var File = require('vinyl');
var _ = require('lodash');
var Readable = require('stream').Readable;
function escapeContent(content, lineLength)
{
var lineRegexp = new RegExp('(.{1,' + (lineLength - 1) + '}[^\\\\])', 'g');
return content
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\r?\n/g, '\\r\\n')
.replace(lineRegexp, ' "$1"\r\n')
.replace(/\r\n$/, '');
};
function escapeContentAsByteArray(content, lineLength)
{
var bytesPerLine = Math.floor(lineLength / 5);
var lineRegexp = new RegExp('((?:0x..,){1,' + bytesPerLine + '})', 'g');
return content
.replace(/(.{2})/g, '0x$1,')
.replace(lineRegexp, ' $1\r\n')
.replace(/,\r\n$/, '');
};
function encodeFile(file, opts)
{
var variableName;
if (opts.map.hasOwnProperty(file.relative))
variableName = opts.map[file.relative];
else
variableName = file.relative.replace(/[^a-zA-Z0-9_]/g, '_');
if (variableName === null)
return '';
variableName = opts.variablePrefix + variableName;
var escapedContent;
var output;
if (opts.byteArray)
{
escapedContent = escapeContentAsByteArray(file.contents.toString('hex'), opts.lineLength);
output = "const uint8_t " + variableName + "[] PROGMEM = {\r\n" + escapedContent + "};\r\n\r\n";
}
else
{
escapedContent = escapeContent(file.contents.toString('utf-8'), opts.lineLength);
output = "const char " + variableName + "[] PROGMEM = \r\n" + escapedContent + ";\r\n\r\n";
}
return output;
}
module.exports = function(file, opts)
{
if (!file)
throw new Error('gulp-cppstringify: Missing file option');
opts = _.extend({
map: [],
headerDefineName: '__Embedded',
variablePrefix: 'Embedded',
lineLength: 100,
byteArray: false
}, opts || {});
var fileName;
var latestFile = false;
var latestMod = 0;
var output = null;
if (typeof file === 'string')
fileName = file;
else if (typeof file.path === 'string')
fileName = path.basename(file.path);
else
throw new Error('gulp-cppstringify: Missing path in file options');
function bufferContents(file, enc, cb)
{
if (file.isNull())
{
cb();
return;
}
if (file.isStream())
{
this.emit('error', new Error('gulp-cppstringify: Streaming not supported'));
cb();
return;
}
if (!latestMod || file.stat && file.stat.mtime > latestMod)
{
latestFile = file;
latestMod = file.stat && file.stat.mtime;
}
if (output == null)
{
output = new Readable();
output._read = function noop() {};
output.push("#ifndef " + opts.headerDefineName + "\r\n");
output.push("#define " + opts.headerDefineName + "\r\n\r\n");
output.push("#include <pgmspace.h>\r\n\r\n");
}
output.push(encodeFile(file, opts));
cb();
}
function endStream(cb)
{
if (!latestFile)
{
cb();
return;
}
var headerFile;
if (typeof file === 'string')
{
headerFile = latestFile.clone({contents: false});
headerFile.path = path.join(latestFile.base, file);
}
else
headerFile = new File(file);
output.push("#endif\r\n");
output.push(null);
headerFile.contents = output;
this.push(headerFile);
cb();
}
return through.obj(bufferContents, endStream);
};

295
gulpfile.js Normal file
View File

@ -0,0 +1,295 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
'use strict';
const gulp = require('gulp');
const htmlmin = require('gulp-htmlmin');
const cppstringify = require('./gulp-cppstringify');
const fs = require('fs');
const plumber = require('gulp-plumber');
const sass = require('gulp-sass');
const cleanCSS = require('gulp-clean-css');
const watch = require('gulp-debounced-watch');
const uglify = require('gulp-uglify');
const concat = require('gulp-concat');
const print = require('gulp-print');
const path = require('path');
const gzip = require('gulp-gzip');
const config = {
assetsPath: 'web/',
distPath: 'web/dist/',
outputPath: 'src/assets/',
firmwareArtifact: '.pioenvs/esp12e/firmware.bin',
firmwareOutputPath: 'bin/'
};
const HTMLMap = {
'index.html': 'Index'
};
const JSMap = {
'bundle.js': 'BundleJS'
};
const CSSMap = {
'bundle.css': 'BundleCSS'
};
// There is an issue in the AsyncWebServer where it's apparantly running
// out of memory on simultaneous requests. We'll work around it by
// merging all the JS into one big file.
//
// https://github.com/me-no-dev/ESPAsyncWebServer/issues/256
const JSSrc = [
'node_modules/axios/dist/axios.min.js',
'node_modules/vue/dist/vue.min.js',
'node_modules/vue-i18n/dist/vue-i18n.min.js',
'web/lang.js',
'web/app.js'
];
const SCSSSrc = [
'web/site.scss'
]
gulp.task('default',
[
'embedAssets'
],
function(){});
gulp.task('watch',
[
'compileScss',
'compileJS'
],
function()
{
watch(config.assetsPath + '*.scss', function() { gulp.start('compileScss'); });
watch(config.assetsPath + '*.js', function() { gulp.start('compileJS'); });
});
gulp.task('embedHTML', function()
{
return gulp.src(config.assetsPath + '*.html')
.pipe(print(function(filepath) { return 'HTML: ' + filepath }))
.pipe(htmlmin({
collapseWhitespace: true,
removeComments: true,
minifyCSS: true,
minifyJS: true
}))
.pipe(gzip({ append: false }))
.pipe(cppstringify('html.h', {
headerDefineName: '__assets_html',
map: HTMLMap,
byteArray: true
}))
.pipe(gulp.dest(config.outputPath));
});
gulp.task('compileScss', function()
{
return gulp.src(SCSSSrc)
.pipe(plumber({
errorHandler: function (error)
{
console.log(error.toString());
this.emit('end');
}}))
.pipe(print(function(filepath) { return 'SCSS: ' + filepath }))
.pipe(sass({
includePaths: ['node_modules/milligram/src']
}))
.pipe(cleanCSS({compatibility: 'ie9'}))
.pipe(concat('bundle.css'))
.pipe(gulp.dest(config.distPath));
});
gulp.task('compileJS', function()
{
return gulp.src(JSSrc)
.pipe(plumber({
errorHandler: function (error)
{
console.log(error.toString());
this.emit('end');
}}))
.pipe(print(function(filepath) { return 'JS: ' + filepath }))
.pipe(concat('bundle.js'))
.pipe(uglify())
.pipe(gulp.dest(config.distPath));
});
gulp.task('embedJS', ['compileJS'], function()
{
return gulp.src([config.distPath + 'bundle.js'])
.pipe(gzip({ append: false }))
.pipe(cppstringify('js.h', {
headerDefineName: '__assets_js',
map: JSMap,
byteArray: true
}))
.pipe(gulp.dest(config.outputPath));
});
gulp.task('embedCSS', ['compileScss'], function()
{
return gulp.src([config.distPath + 'bundle.css'])
.pipe(gzip({ append: false }))
.pipe(cppstringify('css.h', {
headerDefineName: '__embed_css',
map: CSSMap,
byteArray: true
}))
.pipe(gulp.dest(config.outputPath));
});
var version = false;
function getVersion(callback)
{
if (version !== false)
{
callback(version);
return;
}
var versionData = '';
const cmd = spawn('gitversion');
cmd.stdout.on('data', function(data)
{
versionData += data;
});
cmd.stderr.on('data', function(data)
{
console.log(data.toString().trim());
});
cmd.on('exit', function(code)
{
if (code != 0) return;
var version = JSON.parse(versionData);
callback(version);
});
}
gulp.task('embedVersion', function(cb)
{
getVersion(function(version)
{
var headerFile = "#ifndef __assets_version\r\n";
headerFile += "#define __assets_version\r\n\r\n";
headerFile += "const uint8_t VersionMajor = " + version.Major + ";\r\n";
headerFile += "const uint8_t VersionMinor = " + version.Minor + ";\r\n";
headerFile += "const uint8_t VersionPatch = " + version.Patch + ";\r\n";
headerFile += "const uint8_t VersionMetadata = " + version.BuildMetaData + ";\r\n";
headerFile += "const char VersionBranch[] = \"" + version.BranchName + "\";\r\n";
headerFile += "const char VersionSemVer[] = \"" + version.SemVer + "\";\r\n";
headerFile += "const char VersionFullSemVer[] = \"" + version.FullSemVer + "\";\r\n";
headerFile += "const char VersionCommitDate[] = \"" + version.CommitDate + "\";\r\n";
headerFile += "\r\n#endif\r\n";
fs.writeFile(config.outputPath + 'version.h', headerFile, function(err)
{
if (err) throw err;
cb();
});
});
});
gulp.task('copyBinary', function(cb)
{
if (!fs.existsSync(config.firmwareOutputPath))
fs.mkdirSync(config.firmwareOutputPath, '0777', true);
getVersion(function(version)
{
var target = path.join(config.firmwareOutputPath, version.FullSemVer + '.bin');
console.log('Target: ' + target);
var reader = fs.createReadStream(config.firmwareArtifact);
reader.pipe(fs.createWriteStream(target));
reader.on('end', cb);
});
});
gulp.task('embedAssets', ['embedHTML', 'embedJS', 'embedCSS', 'embedVersion'], function() { })
// PlatformIO
const spawn = require('child_process').spawn;
const argv = require('yargs').argv;
var platformio = function(target)
{
var args = ['run'];
if ("e" in argv)
{
args.push('-e');
args.push(argv.e);
}
if ("p" in argv)
{
args.push('--upload-port');
args.push(argv.p);
}
if (target)
{
args.push('-t');
args.push(target);
}
const cmd = spawn('platformio', args);
cmd.stdout.on('data', function(data)
{
console.log(data.toString().trim());
});
cmd.stderr.on('data', function(data)
{
console.log(data.toString().trim());
});
cmd.on('exit', function(code)
{
if (code != 0) return;
gulp.start('copyBinary');
});
}
gulp.task('upload', ['embedHTML', 'embedAssets'], function() { platformio('upload'); });
gulp.task('build', ['embedHTML', 'embedAssets'], function() { platformio(false); });

3
hosted/timezone.php Normal file
View File

@ -0,0 +1,3 @@
<?php
echo file_get_contents('https://maps.googleapis.com/maps/api/timezone/json?' . $_SERVER['QUERY_STRING']);
?>

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "stairs",
"version": "2.0.0",
"description": "Stairs",
"main": "gulpfile.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "node devserver.js"
},
"repository": {
"type": "git",
"url": "https://git.x2software.net/pub/Stairs.git"
},
"author": "Mark van Renswoude <mark@x2software.net>",
"license": "Unlicense",
"devDependencies": {
"axios": "^0.17.1",
"body-parser": "^1.18.2",
"child_process": "^1.0.2",
"express": "^4.16.2",
"gulp": "^3.9.1",
"gulp-clean-css": "^3.9.0",
"gulp-concat": "^2.6.1",
"gulp-debounced-watch": "^1.0.4",
"gulp-gzip": "^1.4.1",
"gulp-htmlmin": "^3.0.0",
"gulp-plumber": "^1.1.0",
"gulp-print": "^2.0.1",
"gulp-sass": "^3.1.0",
"gulp-uglify": "^3.0.0",
"lodash": "^4.17.4",
"path": "^0.12.7",
"through2": "^2.0.3",
"vinyl": "^2.1.0",
"vue": "^2.5.13",
"vue-i18n": "^7.3.3",
"yargs": "^10.0.3"
}
}

View File

@ -0,0 +1,9 @@
@Echo Off
REM Enable this line if you're not using MapsAPIViaProxyScript
REM echo -DASYNC_TCP_SSL_ENABLED=1
if exist src\secret.h (
echo -DSecretsPresent=1
)
echo -D

View File

@ -9,7 +9,15 @@
; http://docs.platformio.org/page/projectconf.html
[env:esp12e]
platform = espressif8266_stage
platform = https://github.com/platformio/platform-espressif8266.git#feature/stage
board = esp12e
framework = arduino
upload_speed = 115200
upload_speed = 115200
lib_deps =
ArduinoJson
ESP Async WebServer
NTPClient
Time
Dusk2Dawn
EspSaveCrash
build_flags = !platformio-buildflags.bat

105
src/assets/css.h Normal file
View File

@ -0,0 +1,105 @@
#ifndef __embed_css
#define __embed_css
#include <pgmspace.h>
const uint8_t EmbeddedBundleCSS[] PROGMEM = {
0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x0b,0xad,0x59,0xdb,0x6e,0xa3,0x38,0x18,0x7e,0x95,0x48,
0xd5,0x48,0xd3,0x15,0x20,0x92,0x94,0xb4,0x05,0xcd,0x6a,0x57,0xfb,0x06,0x7b,0xb1,0x37,0xa3,0x5e,0x18,
0x30,0xc1,0x2a,0xc1,0xc8,0x38,0x4d,0x5b,0xc4,0xbb,0xef,0xef,0x13,0xb1,0xc1,0xc9,0x64,0x46,0x23,0xd4,
0x26,0xd8,0xfe,0x8f,0xfe,0x0f,0x9f,0x9d,0x9a,0x1f,0x9a,0x21,0xa7,0xef,0x61,0x4f,0x3e,0x49,0xbb,0x4f,
0x73,0xca,0x4a,0xcc,0x42,0x18,0xc9,0x2a,0xda,0x72,0x31,0x8c,0xd3,0xdd,0x26,0x4a,0xbe,0x8c,0x7f,0x04,
0x29,0xaa,0x38,0x66,0x41,0x9a,0xe3,0x8a,0x32,0x6c,0x93,0x91,0xb6,0xc6,0x8c,0xf0,0x31,0xa7,0xe5,0xc7,
0x90,0xa3,0xe2,0x75,0xcf,0xe8,0xb1,0x2d,0xc3,0x82,0x36,0x94,0xa5,0x77,0x71,0x1c,0x67,0xfa,0x6b,0x55,
0x55,0x8a,0x73,0x85,0x0e,0xa4,0xf9,0x48,0xff,0xc3,0xac,0x44,0x2d,0x0a,0xfe,0x66,0x04,0x35,0x41,0x8f,
0xda,0x3e,0xec,0x81,0x55,0x65,0x89,0x5f,0x47,0x5b,0x7c,0x50,0xef,0x27,0x4c,0xf6,0x35,0x4f,0xb7,0xc0,
0xaf,0xc1,0x1c,0x94,0x09,0xfb,0x0e,0x15,0x42,0x83,0x28,0x5e,0xc3,0xa2,0x86,0xb4,0x38,0xac,0xd5,0x22,
0x20,0xcb,0x3a,0x54,0x96,0x30,0x0b,0xf6,0x70,0x4e,0x0f,0xe9,0x96,0xe1,0xc3,0xf8,0xd7,0x01,0x97,0x04,
0xad,0xfa,0x82,0x61,0xdc,0xae,0x50,0x5b,0xae,0xbe,0x1e,0x48,0x1b,0x9e,0x48,0xc9,0xeb,0xf4,0x71,0xf7,
0xd4,0xbd,0xdf,0x0f,0xd2,0x0e,0x43,0xcc,0x69,0xa7,0x28,0x47,0x34,0x70,0xfc,0xce,0xc3,0x12,0x17,0x94,
0x21,0x4e,0x68,0x9b,0xb6,0xb4,0xc5,0xe3,0xf7,0xb7,0xb0,0x68,0x28,0x7a,0x7d,0x19,0x4a,0xd2,0x77,0x0d,
0xfa,0x50,0xc3,0x77,0x05,0xa8,0x8c,0x40,0x23,0x66,0xb9,0x24,0xbd,0xdb,0xc4,0xe2,0xc9,0x0e,0x88,0xed,
0x41,0xac,0x60,0xbe,0x01,0xe6,0x46,0xd5,0x74,0x2d,0x5e,0xa4,0x6b,0x6b,0x54,0xd2,0x53,0x1a,0xaf,0xe2,
0x55,0x12,0x77,0xef,0xab,0xbb,0xaa,0xa8,0x76,0x45,0x95,0xa9,0x2d,0x4a,0x7b,0xda,0x90,0x72,0xb5,0x16,
0x13,0xe0,0xde,0x9b,0xac,0xb2,0x14,0xb2,0xc6,0x8d,0x26,0x0d,0xae,0x78,0x8a,0x8e,0x9c,0x9a,0x01,0x26,
0xdd,0x28,0x46,0xc6,0x31,0xaa,0x31,0x02,0xa9,0x43,0x47,0x7b,0x22,0x0d,0x67,0xb8,0x01,0x0f,0xbc,0x61,
0x33,0xb3,0x22,0x87,0xfd,0x50,0x81,0x17,0x78,0x2a,0x18,0xb9,0x3c,0xd6,0x97,0xfc,0x8e,0xde,0x27,0x0d,
0x1f,0x85,0x86,0x86,0x59,0x74,0x22,0x15,0xe9,0x39,0xe2,0xc7,0x7e,0x28,0x1a,0x8c,0x18,0x04,0x26,0xaf,
0x6d,0x9f,0xa9,0x0d,0xb9,0xc5,0x6a,0x1f,0xcf,0xc9,0x0c,0x94,0x83,0x1f,0x8f,0x1c,0x67,0x4a,0xd1,0x38,
0x13,0xbc,0xe3,0xc9,0x5e,0x9b,0x68,0x15,0x91,0xb6,0x24,0x05,0xe2,0x94,0x4d,0xfb,0x4c,0x5a,0x19,0x71,
0x79,0x43,0x8b,0xd7,0x4c,0x49,0x95,0xfb,0x67,0x42,0x50,0xed,0xa5,0x4c,0x29,0x86,0x4a,0x72,0xec,0xd3,
0x24,0xfe,0xe2,0xfa,0x26,0x4a,0x84,0x25,0xd7,0xe5,0x7d,0x2f,0x11,0x47,0xa1,0x1a,0xfe,0x06,0x9b,0xd8,
0xe2,0x82,0xe3,0xf2,0xc5,0x93,0x69,0xdb,0xe7,0xdd,0xcf,0xf0,0x02,0x3b,0x6c,0x76,0xf3,0xc8,0xda,0x33,
0xfc,0xf1,0x0b,0xaa,0x41,0x18,0xfb,0x74,0xab,0x9e,0xb7,0x3f,0xc3,0x0c,0x33,0x46,0x99,0x8f,0x4f,0x01,
0xe1,0x1e,0xe5,0x47,0xc8,0xe8,0x36,0x88,0x8a,0x1a,0x17,0xaf,0xab,0x48,0x44,0x36,0xa3,0x4d,0x10,0xb5,
0x94,0x03,0xe7,0x42,0x66,0x67,0x10,0x75,0xa8,0xc5,0xcd,0x4a,0x7d,0x84,0x22,0xa9,0x67,0x43,0x4a,0x9b,
0x20,0x12,0x9b,0x43,0x2d,0x2e,0x27,0xc4,0x5a,0xb0,0x22,0xd0,0x52,0xea,0x6d,0x40,0xda,0xee,0xc8,0xbf,
0xf3,0x8f,0x0e,0x7f,0xeb,0x8f,0xf9,0x81,0xf0,0x97,0xa0,0xc7,0x0d,0x18,0x6b,0xbc,0x26,0xfc,0xa5,0x3c,
0x77,0xb7,0x5e,0xaf,0x67,0xbb,0xbe,0x85,0x34,0xb3,0x72,0x9a,0xb4,0x3d,0xe6,0x90,0xd7,0x82,0x86,0xed,
0x73,0xf4,0x75,0x93,0x24,0x81,0xf9,0x8b,0xd6,0xf7,0x81,0x59,0x10,0x8a,0x15,0x5b,0xb3,0x2a,0x0e,0xc4,
0x13,0x6d,0xcf,0xf3,0xf1,0x45,0x26,0xf1,0xd3,0x7d,0xa0,0xe6,0x36,0x33,0xf2,0x75,0x72,0x6f,0xdc,0x17,
0xa1,0x42,0xe4,0x70,0xa0,0x5f,0x53,0xfd,0xea,0x4e,0xba,0x73,0x96,0x1f,0xda,0xe3,0x21,0xc7,0xec,0xc5,
0x1e,0xea,0x50,0xdf,0x9f,0xc0,0xf2,0x17,0x8f,0xbf,0xa2,0x25,0x07,0x3d,0xe3,0xe1,0x2d,0xea,0xec,0x4b,
0x20,0xfe,0x23,0x86,0xd1,0x75,0x1f,0x9f,0x9b,0x8c,0x1c,0x36,0x73,0x5e,0x6f,0xcf,0x9d,0xb1,0x49,0x8c,
0x9b,0xbc,0x2e,0x1c,0x75,0x00,0x48,0xcd,0x86,0x5b,0xfa,0xd6,0x14,0x99,0x36,0xa5,0x63,0xad,0xbf,0x78,
0x98,0xf2,0x0f,0xca,0x80,0x8e,0xa6,0x5b,0x96,0x65,0x99,0xd9,0xbd,0xe3,0x21,0x16,0x4f,0x56,0x1c,0x59,
0x0f,0xd3,0x1d,0x25,0x2d,0xf4,0x40,0xa7,0xeb,0xc9,0xba,0x68,0xb6,0xb7,0xa2,0xc5,0xb1,0x9f,0x76,0xd7,
0x7d,0xab,0xe9,0x1b,0x44,0xbe,0xb3,0xd0,0x59,0xe7,0x2c,0xf3,0xec,0xa6,0x5a,0xe5,0xd9,0xcc,0x8b,0x13,
0x92,0xd5,0x70,0xc1,0xb2,0x24,0x16,0x4f,0x46,0x8f,0x5c,0x58,0x93,0xc6,0xbf,0x27,0x44,0x6f,0x0d,0x3d,
0xa3,0x55,0x51,0x14,0x8e,0x56,0x9b,0x27,0xf1,0x18,0x5d,0xc2,0x8e,0x11,0xa8,0xdd,0x1f,0xbe,0x4d,0x75,
0xa8,0x36,0xbb,0x04,0xad,0xe7,0x54,0xee,0x6e,0x98,0xd1,0xd4,0x3f,0xfa,0xdb,0xdd,0xee,0xe8,0xb7,0x7b,
0xdc,0xe4,0xbb,0x11,0x69,0xa1,0x7e,0x50,0x13,0xb5,0xe8,0x8d,0xec,0xe5,0xc0,0xb5,0x16,0x1c,0x71,0x68,
0xa1,0x7f,0x1a,0x4e,0x36,0x94,0x10,0xb5,0x6b,0x56,0x06,0x63,0x77,0x79,0x5a,0x11,0xd6,0xf3,0xb0,0xa8,
0x49,0x53,0x3a,0xa4,0xf1,0xb2,0x7c,0xca,0x52,0x07,0x9f,0x33,0x0e,0x0d,0x9a,0x18,0xcc,0x44,0xc9,0xb2,
0x29,0x09,0xe7,0x42,0x85,0xa3,0x96,0x40,0x26,0xfb,0x0c,0xa1,0x0f,0xe1,0xf7,0x14,0x36,0x0e,0x1c,0xd6,
0x4b,0xbb,0x65,0x54,0x88,0x2e,0x68,0xc1,0xd1,0xa7,0x8e,0x67,0xd2,0x63,0xa8,0x21,0xfb,0x36,0x2d,0xb0,
0xcc,0xc1,0x19,0x9c,0x1b,0x9d,0x46,0xf4,0xcf,0x84,0xbb,0x26,0xb1,0x15,0x79,0xc7,0x65,0x36,0xa1,0x3f,
0x23,0x7c,0xb7,0xdb,0xdd,0x06,0x6a,0xfc,0xec,0xd5,0x9a,0x44,0x96,0x10,0xe9,0x49,0x80,0x1b,0xa3,0xab,
0x8b,0x1b,0x09,0xcf,0x8f,0x28,0x7f,0x9a,0xc3,0xcd,0x75,0xac,0x51,0xa5,0x0d,0xda,0x67,0x35,0xc7,0xd4,
0xab,0x28,0x01,0xed,0xb5,0xf1,0x1a,0x65,0x2b,0x38,0xbb,0x00,0x8a,0x3f,0x6d,0xd6,0xa0,0xc2,0x68,0x69,
0xc2,0x2a,0x3a,0xe0,0xbe,0x47,0x7b,0x3c,0x9c,0x6a,0xc2,0xb1,0x3c,0x03,0xe0,0xb4,0x63,0xd8,0x5d,0x16,
0x49,0x04,0xe1,0xd8,0xfb,0xfc,0xb8,0x45,0x5b,0xc8,0x67,0x89,0x1a,0x74,0xdb,0xf7,0xd7,0xe3,0x99,0xb9,
0xc7,0x5e,0x9c,0x35,0x64,0xc3,0x97,0xe9,0x91,0xd9,0x92,0x5b,0x7a,0x62,0xa8,0x1b,0x17,0x58,0xc4,0x45,
0x15,0x83,0xa7,0x90,0x5f,0xc3,0x91,0x3b,0xd8,0x43,0x83,0x23,0xc5,0x77,0x0f,0xf4,0xd6,0x02,0x1b,0x94,
0xe3,0xb3,0x38,0xf9,0xe6,0x37,0xca,0xce,0x31,0xb9,0x71,0x10,0xe8,0x1c,0xbc,0xd5,0xe8,0x58,0x86,0x70,
0xd4,0x4c,0xd5,0x7f,0x5c,0xce,0xad,0x59,0x8c,0x3b,0x56,0xed,0x62,0xf1,0x18,0x16,0xa0,0x02,0xca,0x1b,
0x5c,0x1a,0x52,0xf3,0x3e,0x68,0xdf,0xc2,0x5e,0x81,0xdc,0x86,0x9e,0x70,0x39,0x2e,0x10,0x98,0xfb,0x2e,
0x30,0xa2,0x3c,0x47,0xcd,0xb1,0xf4,0x9c,0xd0,0x2c,0xb4,0x0e,0x9e,0x4b,0xac,0x2f,0xb2,0xee,0xc1,0x64,
0x88,0xf8,0xa2,0x3c,0x6e,0x39,0x7c,0x27,0x0a,0x8d,0xdf,0xe0,0x49,0x15,0xcb,0x6e,0xd9,0x36,0xec,0x1c,
0x5a,0xcf,0x60,0x86,0x80,0x5c,0x6e,0x74,0x18,0x36,0x7e,0xed,0x92,0xab,0xda,0xc9,0x32,0xe8,0xdf,0x26,
0xd7,0x51,0x1a,0xca,0x2f,0xa1,0xcd,0xd3,0xbd,0xa9,0xb0,0x8a,0xb9,0xc8,0xfa,0x8d,0x82,0x47,0x19,0x67,
0x00,0x66,0xe0,0x80,0x7f,0x48,0x19,0x05,0x24,0x8e,0xbf,0x86,0x0f,0x49,0x89,0xf7,0xf7,0xb6,0x85,0x12,
0x97,0xc6,0x2e,0x92,0x72,0x80,0x94,0x65,0xb7,0x60,0x15,0x9a,0x68,0xb1,0xca,0x24,0x9c,0xd7,0xc7,0x5f,
0x40,0x93,0x73,0x64,0xe8,0x41,0x47,0xe7,0xa2,0xe5,0x54,0x29,0x9d,0x57,0x31,0x44,0x8d,0x81,0xee,0x37,
0x13,0xdb,0x9a,0x82,0x7f,0xf6,0xf8,0xc5,0x31,0x85,0x2d,0x8a,0xa0,0x3c,0xf2,0xd6,0xeb,0xe1,0xdc,0x37,
0x36,0xe7,0x45,0xd0,0x08,0xeb,0x8d,0x8e,0xd1,0x9e,0x34,0x90,0x85,0xce,0x75,0xc7,0x6c,0xe5,0xd6,0xee,
0x43,0x4b,0x74,0xb2,0x24,0x3d,0x2b,0x2e,0x95,0x78,0x18,0xec,0x15,0x0f,0x6c,0xb2,0xc6,0x24,0xe4,0x8b,
0x27,0x23,0xb3,0x4b,0x32,0x77,0xe2,0x19,0xdd,0x22,0xe3,0x54,0x17,0xe1,0x10,0x4f,0x57,0x90,0x5e,0x54,
0xc5,0x29,0x54,0x45,0x69,0x70,0xce,0xc1,0x9b,0x5b,0xef,0x66,0xa2,0x9a,0x32,0xf2,0x29,0x5a,0x5e,0x63,
0x01,0x93,0xd1,0x1a,0x5e,0x5d,0x2e,0x81,0xce,0x32,0x4f,0xf4,0x5d,0x98,0x3e,0x47,0xe2,0x85,0x05,0x2a,
0x2a,0xed,0xc9,0x29,0x42,0xbd,0x95,0x58,0xdd,0x94,0x48,0xcb,0x75,0x5c,0xca,0x62,0x76,0xa6,0x57,0x77,
0x6d,0xb6,0x81,0x30,0x0b,0xbd,0x68,0xe6,0x73,0x17,0x96,0x58,0x7b,0x36,0x8b,0x46,0x7d,0xcb,0x00,0x52,
0x45,0x64,0x0c,0x33,0x14,0xb7,0xc4,0x33,0x63,0x04,0xf0,0x91,0x76,0xa2,0x32,0xf5,0x0e,0x34,0x4b,0xac,
0xf3,0x85,0x73,0x37,0x73,0x15,0x12,0xc9,0x6c,0x88,0x7a,0x28,0x46,0x80,0xac,0x86,0x05,0x56,0xea,0x39,
0xee,0x1c,0x29,0x5b,0x2b,0xa1,0xd8,0x74,0xae,0xf1,0xb5,0x40,0x41,0xba,0xd2,0x9c,0xcf,0xd7,0x5b,0x0e,
0xe9,0x83,0x11,0xb1,0x8a,0xde,0x50,0x73,0xc4,0x3f,0xb8,0xfd,0x89,0x64,0x36,0x9f,0x5d,0x39,0x5e,0xe2,
0x3e,0x37,0x6c,0x08,0x4f,0x38,0x7f,0x25,0xe0,0x85,0xae,0x03,0xb7,0xa0,0x56,0x42,0x03,0x81,0x15,0xa6,
0xc2,0x63,0x2a,0xb8,0xdc,0x8e,0x19,0xc8,0x85,0xb3,0xa8,0x1c,0x5c,0xd6,0x24,0xeb,0x50,0xa4,0x24,0xa5,
0xa9,0x11,0xa5,0xde,0x43,0x5e,0x43,0xfc,0x5e,0x94,0xef,0xd7,0x67,0x63,0x5d,0x54,0x6d,0xfc,0x17,0x55,
0xb6,0x2e,0xfa,0xf6,0xd1,0xc5,0x45,0x96,0x42,0x07,0xfa,0x19,0xca,0xca,0xa8,0x95,0xf9,0x9d,0x42,0xf4,
0x75,0x8c,0x07,0xca,0xfd,0x10,0x87,0x2e,0xf6,0xaa,0xa5,0xe2,0x92,0xc9,0x2e,0xa9,0x9e,0xe0,0x97,0x71,
0xed,0x54,0x17,0x79,0x5f,0x34,0xf8,0x80,0xae,0x39,0xb8,0x8f,0xbe,0xab,0xa5,0x61,0x79,0x90,0xd1,0x87,
0x99,0x6c,0xba,0x6a,0x17,0xbc,0x4c,0x1f,0x76,0xed,0xb9,0xda,0x9f,0xbc,0xf2,0x74,0xe9,0xb3,0x0a,0xbe,
0xb0,0xd9,0xb7,0x50,0x9e,0x89,0x45,0x0e,0x5b,0xa5,0xe8,0xc2,0x4a,0x8d,0x2f,0x7d,0x73,0xc6,0x91,0x1e,
0x85,0xd4,0x6f,0x01,0xb3,0x43,0xd9,0x74,0x2c,0x73,0x6c,0xdb,0xc6,0xe2,0x99,0x6c,0x57,0x45,0x41,0x72,
0xd1,0x27,0xf7,0xb9,0x53,0x6d,0xda,0xfc,0x01,0x25,0x4f,0x8e,0x5f,0x74,0x7b,0xb9,0x02,0xae,0xe5,0xa5,
0x76,0x74,0xc2,0xf8,0xb5,0x44,0x1f,0xfd,0x32,0xa1,0xcd,0x8c,0x41,0xd3,0x8a,0xea,0x49,0x4c,0x55,0xa0,
0x41,0x28,0xc3,0x24,0x34,0x57,0x13,0x72,0x08,0x62,0xe5,0x0d,0xeb,0xa1,0x41,0x62,0x28,0x55,0x63,0xa8,
0xf8,0x81,0x82,0x7f,0xac,0xa2,0xa4,0xb7,0x89,0x1d,0x2a,0x4e,0x07,0xbd,0x4c,0x84,0x91,0xcc,0x22,0x27,
0xf8,0xe4,0x08,0x54,0x39,0x68,0x28,0xdc,0x73,0x72,0xbd,0x62,0xe8,0xc3,0xf3,0x17,0x97,0xfc,0x96,0x5a,
0xe9,0xae,0xbf,0xa5,0x66,0xaa,0xfe,0xe2,0x14,0x4d,0xcd,0x04,0xb7,0xe5,0xad,0x1a,0x5f,0xe8,0x89,0x67,
0x46,0x17,0x75,0x57,0x50,0xd9,0x52,0x5d,0x2e,0xbe,0xa4,0xb8,0xbe,0x62,0xb8,0xa6,0xf7,0xb2,0xfb,0x46,
0x0c,0xf7,0x98,0xff,0x8b,0x51,0x3f,0xbb,0xe6,0xd8,0x80,0xd8,0xff,0x01,0xe8,0x09,0xf1,0xde,0x46,0x1b,
0x00,0x00};
#endif

181
src/assets/html.h Normal file
View File

@ -0,0 +1,181 @@
#ifndef __assets_html
#define __assets_html
#include <pgmspace.h>
const uint8_t EmbeddedIndex[] PROGMEM = {
0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x0b,0xed,0x5b,0xe9,0x57,0xe3,0x38,0x12,0xff,0x57,0x4c,
0x66,0x77,0xa0,0xb7,0x9b,0x23,0x90,0x66,0x1a,0x06,0x98,0x09,0x39,0x20,0x40,0x42,0x48,0xc2,0xf9,0x65,
0x9f,0x62,0x2b,0xb1,0xc0,0xb1,0x8d,0x64,0x27,0xa4,0x67,0xfa,0x7f,0x5f,0x1d,0x3e,0x64,0x5b,0x4e,0x1c,
0xb6,0xf7,0xf5,0xcc,0x7b,0xdb,0x1f,0x3a,0x58,0x75,0xe8,0xa7,0x52,0x55,0xa9,0x24,0xcb,0x47,0x6b,0xf5,
0xeb,0xda,0xe0,0xb1,0xdb,0xd0,0x4c,0x6f,0x62,0x9d,0x1c,0x05,0xff,0x43,0x60,0x9c,0x1c,0x4d,0xa0,0x07,
0x34,0xdd,0x04,0x98,0x40,0xef,0xb8,0x74,0x3b,0x68,0x6e,0x7e,0x29,0x9d,0x1c,0x79,0xc8,0xb3,0xe0,0xc9,
0xd1,0x76,0xf0,0xcb,0x99,0x6c,0x30,0x81,0xc7,0x25,0xcf,0x84,0x13,0xb8,0xa9,0x3b,0x96,0x83,0x4b,0x9a,
0xee,0xd8,0x1e,0xb4,0xa9,0xdc,0x4f,0x3b,0xfc,0x5f,0x29,0xc1,0x3a,0x45,0x70,0xe6,0x3a,0xd8,0x93,0xf8,
0x66,0xc8,0xf0,0xcc,0x63,0x03,0x4e,0x91,0x0e,0x37,0xf9,0xc3,0x27,0x64,0x23,0x0f,0x01,0x6b,0x93,0xe8,
0xc0,0x82,0xc7,0x65,0xaa,0xc2,0x42,0xf6,0x8b,0x86,0xa1,0x75,0x5c,0x22,0xde,0xdc,0x82,0xc4,0x84,0x90,
0xea,0x30,0x31,0x1c,0x1d,0x97,0x86,0xbe,0x6d,0x58,0x70,0x4b,0x27,0x84,0x32,0x12,0x1d,0x23,0xd7,0xd3,
0x08,0xd6,0x23,0xc2,0x33,0x6b,0xdf,0x16,0x04,0xfa,0x87,0x18,0xe3,0xd0,0x31,0xe6,0x27,0x47,0x06,0x9a,
0x6a,0xc8,0x38,0x2e,0x01,0xd7,0x2d,0x89,0xa7,0xe9,0xa6,0x6e,0x39,0xe0,0x45,0x3c,0xe8,0x16,0x20,0xe4,
0xb8,0x64,0x3b,0x1e,0x1a,0x21,0x1d,0x78,0xc8,0xb1,0x6b,0x14,0x36,0x40,0x36,0xc4,0xa5,0x5c,0x96,0x92,
0x76,0x18,0xb4,0xfe,0xa1,0x41,0x8c,0x1d,0x7c,0xa8,0xc9,0x64,0x6d,0xed,0x58,0xb3,0x7d,0xcb,0xd2,0x7e,
0xfe,0x39,0xd1,0xbe,0xc5,0x79,0xb5,0x6f,0x25,0x8a,0x01,0x8d,0x92,0x2a,0x43,0x99,0x92,0xf6,0xbb,0x6e,
0x21,0xfd,0x65,0xcb,0xc5,0x70,0xca,0xad,0x67,0x22,0x03,0x76,0xe4,0xce,0xa9,0x09,0x5c,0x60,0x87,0xb8,
0x26,0x90,0x10,0x30,0x86,0xa5,0x93,0x3f,0xfe,0x48,0x76,0x16,0x10,0xb4,0x6f,0xdf,0xa8,0x69,0xa8,0x00,
0x35,0x0c,0x1d,0x4e,0xf8,0x7f,0x68,0x17,0x5d,0x3d,0x5a,0x66,0x42,0xde,0x86,0x26,0x63,0x61,0x6a,0x03,
0x78,0xe0,0x10,0x4d,0xa8,0xc6,0x6d,0xd7,0x1e,0xff,0x3a,0x04,0x04,0xee,0x57,0x3e,0xa1,0xbb,0xd3,0xeb,
0xde,0x6c,0xe7,0xf2,0x6c,0xec,0x54,0xe9,0xbf,0x4e,0xff,0xd6,0x6c,0xdc,0x8e,0xe9,0x5f,0x75,0xf6,0x58,
0x9d,0xd5,0xaa,0x8f,0xf4,0xe7,0xf4,0xa1,0x3a,0x9d,0x9c,0xb3,0x86,0xb3,0x87,0x5e,0xf3,0xfe,0xbc,0x37,
0x18,0xee,0x3e,0xed,0x18,0xbb,0xcd,0xf9,0xd3,0xcd,0xe9,0xe9,0xd3,0xd9,0x01,0x7a,0xea,0x9f,0x5e,0x0c,
0xef,0x9b,0xf6,0xd3,0xdd,0x85,0xf5,0x78,0xdf,0xfb,0xac,0xeb,0x96,0xd5,0x65,0x02,0x0f,0xa7,0x17,0xbd,
0x46,0xf3,0x16,0x76,0x30,0xb9,0x37,0x1a,0x9d,0xf1,0x73,0xf5,0xe6,0x4a,0x7f,0x3c,0xd5,0xab,0x5d,0xbd,
0x5a,0x33,0x6e,0x3a,0x95,0x6a,0x67,0xb7,0x5d,0xab,0x8c,0x7b,0xe4,0xf1,0xe2,0xa0,0xd1,0x31,0xaa,0xdd,
0xc7,0x6a,0x1d,0x54,0xeb,0xd0,0x35,0x6e,0xcd,0x76,0xf9,0xb5,0xf9,0xec,0xe3,0xb1,0x7b,0xd0,0xd7,0xdb,
0xe7,0x63,0xe3,0x97,0xf2,0xde,0xdd,0xde,0xc8,0xbb,0x75,0x3f,0xc3,0xf3,0x71,0xbb,0x59,0xc6,0xf8,0xac,
0x01,0xfc,0xfd,0xbb,0xf3,0xfa,0xee,0x79,0x7b,0x78,0xfe,0xf9,0xf5,0xe2,0xfa,0xea,0x1c,0x83,0x8f,0xa3,
0x97,0xaf,0x43,0xf2,0xd8,0x23,0x66,0xfb,0x8b,0x7b,0x35,0x18,0xdf,0xb6,0xc6,0xfd,0xf1,0xd4,0x6f,0xb7,
0x9d,0xc7,0xd9,0x47,0xd4,0x7e,0x1c,0xe0,0xfd,0x1b,0xb3,0xf3,0xd8,0xc6,0x1d,0xd4,0x99,0xcf,0x5a,0x57,
0xd6,0xfc,0xee,0xd2,0xd0,0xe7,0xf3,0x2e,0x99,0xe8,0x3d,0x32,0xbf,0xfd,0xbc,0xf3,0x32,0x3e,0xf7,0x6e,
0x6e,0xfc,0xdd,0xaa,0xd1,0xb9,0x68,0xba,0xf5,0x97,0xea,0x65,0xa5,0xb5,0x7d,0xd5,0xba,0x6f,0x0f,0x77,
0xab,0xa4,0x75,0xaa,0xbf,0xee,0xa0,0xde,0x19,0xbc,0x39,0xeb,0x0e,0x9e,0x46,0x77,0xfb,0x37,0x8d,0x9d,
0x8f,0xe3,0xfa,0x59,0x73,0x17,0x3b,0xe4,0xac,0x31,0x6e,0xdf,0xbc,0xb5,0xaa,0xa6,0xfd,0x54,0x45,0xdd,
0xce,0x97,0x8a,0xef,0xf6,0x46,0x3b,0xdb,0xd7,0x96,0x4b,0xae,0x6a,0xa7,0xee,0xde,0xfc,0x75,0x47,0x37,
0xc7,0x5e,0xed,0xf6,0xf6,0x09,0xf7,0x66,0xfb,0x37,0xf5,0xeb,0xbd,0xc6,0xfd,0x79,0xff,0xb5,0x79,0xe0,
0x01,0xfc,0x04,0xfa,0x97,0x17,0x0f,0xf0,0xa2,0x6e,0x0c,0x6f,0x2c,0xd2,0xd8,0xb9,0xac,0xef,0x5f,0x74,
0xb6,0x2f,0x9d,0x1e,0x39,0x33,0xdf,0x1e,0x2e,0x6b,0x56,0xed,0xf2,0xfc,0xa2,0x35,0x7a,0x19,0x98,0xb3,
0xf6,0xbd,0x59,0xdd,0x37,0x4e,0xfb,0x8e,0xd5,0x43,0xcf,0x2f,0x17,0xd7,0x46,0xf9,0xe9,0x76,0x7a,0x30,
0xbf,0x39,0xb8,0x76,0x5f,0x87,0xe7,0x2e,0x02,0xb7,0x77,0xa0,0x31,0x7c,0x6a,0xfc,0xe2,0xb5,0x5a,0xcf,
0xce,0xe9,0xe5,0xc3,0x9c,0x38,0xa4,0xac,0x57,0xee,0xbe,0xc0,0xe1,0x55,0xc3,0x18,0x4e,0x77,0x87,0x7a,
0x9b,0x34,0x7e,0x19,0x3f,0xfb,0xa7,0xc6,0xf4,0xa1,0xd7,0xbf,0xa8,0x34,0x3f,0x6e,0xcf,0x5e,0x5b,0x0f,
0x0f,0xb8,0x75,0x36,0x9b,0x3c,0xec,0x7d,0x9d,0x01,0xfd,0xaa,0x6e,0xc2,0xce,0xf5,0x41,0xf9,0xfa,0xf9,
0xea,0xe6,0xd2,0x28,0x57,0xee,0xda,0xf5,0x9a,0xfd,0x38,0xae,0xbd,0xdd,0x3d,0xb7,0xf6,0x3a,0x03,0x58,
0x9e,0xf4,0x9d,0x6e,0xbd,0x72,0xf0,0x56,0xe9,0x63,0xea,0x1c,0x07,0xaf,0x5d,0xbb,0x02,0x9d,0x69,0xad,
0xcd,0xbd,0xa7,0x61,0x35,0x07,0x2f,0x7d,0xff,0x66,0x52,0xab,0x51,0x4f,0x34,0xcb,0xcc,0xc5,0xff,0xe1,
0x6d,0xac,0xf3,0xfc,0xb4,0xfe,0x81,0x7b,0x36,0x6d,0x3d,0x32,0x77,0x19,0x85,0x78,0xc0,0xf3,0xc9,0x16,
0x99,0x13,0x0f,0x4e,0x5a,0x75,0x1a,0x50,0x41,0x14,0xfe,0xc6,0x85,0xc2,0x76,0x2a,0xf7,0x51,0x5b,0x3f,
0xd4,0xd6,0xe9,0x4f,0x5a,0x84,0xb6,0xae,0x0b,0xad,0xbb,0x89,0x68,0x98,0xd1,0xa8,0x12,0xbc,0xc9,0x28,
0xa1,0xc1,0x63,0x43,0x3d,0x08,0x4a,0xa9,0x1d,0xd9,0x06,0x8b,0x42,0x96,0x2c,0x0f,0x59,0xdc,0x6c,0x0a,
0x61,0xa1,0xa8,0x2f,0x3a,0x05,0xee,0x16,0xb4,0xc1,0xd0,0x82,0x06,0x05,0xb8,0x1e,0x68,0x82,0xc6,0x3a,
0x03,0x61,0x20,0x12,0x37,0x94,0x82,0x90,0x0d,0x06,0x2f,0xab,0xd0,0x75,0x1a,0xe3,0xae,0x83,0x6c,0x6f,
0x2b,0xb6,0x89,0x46,0x19,0xf3,0xfa,0x49,0xb6,0x23,0x97,0x76,0xb6,0x40,0x27,0x85,0xc1,0x05,0x03,0x53,
0x47,0x79,0xe3,0xbd,0xa3,0x1f,0x43,0xef,0x1e,0x35,0x79,0x4f,0x54,0x48,0x74,0xb8,0xf1,0x21,0x7f,0x7c,
0x44,0x30,0x4e,0x1c,0x03,0xa6,0xc6,0xa7,0xd2,0x34,0x80,0x6f,0xde,0xc6,0xbb,0x91,0x26,0x80,0xca,0xf6,
0x4f,0xc1,0x13,0xab,0xd8,0x00,0x4d,0x04,0x18,0x81,0xa5,0x1e,0x35,0xc6,0xdd,0xa7,0x93,0xad,0x48,0xf9,
0x74,0xe5,0x31,0x90,0x3d,0x2e,0x85,0x08,0xc2,0xe7,0x50,0x7d,0xf0,0x1c,0x0d,0x34,0x78,0x6e,0x85,0x40,
0x93,0xc3,0x13,0x3a,0xd7,0x02,0x26,0xb6,0xdc,0xd0,0xe5,0x14,0x0d,0xb1,0x58,0x51,0x8e,0x83,0x00,0x48,
0x8e,0x79,0x06,0xb0,0xcd,0x11,0x08,0x61,0x13,0x90,0x1e,0xa4,0x55,0x40,0x83,0x2d,0x4d,0x94,0xd3,0x0d,
0x91,0xf0,0xb5,0x6a,0x0b,0x47,0xb4,0xc0,0x09,0x5c,0xca,0x12,0xaa,0xe2,0xc4,0x1e,0x04,0x84,0xd9,0x35,
0x2b,0x26,0x28,0x5b,0x52,0xa0,0x49,0xcd,0x92,0x36,0x01,0x84,0x44,0x73,0xae,0xbf,0x0c,0x30,0xd0,0x61,
0x4a,0x65,0x4c,0x88,0x91,0xc4,0x36,0x50,0x48,0x1f,0x81,0x10,0xe7,0xd0,0xf7,0x3c,0x6a,0x0f,0xf1,0xb3,
0xe9,0x62,0xba,0x8a,0xe1,0x79,0x58,0x5b,0x6c,0x03,0x17,0x6d,0x73,0x39,0x8f,0xc9,0x6d,0xd3,0xe9,0xcc,
0xed,0xb9,0xee,0xcc,0x6c,0x66,0xec,0x00,0x01,0x38,0xd1,0xd2,0x9d,0x84,0xcb,0x37,0xf5,0x21,0x68,0x41,
0x0f,0xf6,0x97,0x0f,0xa7,0xce,0x19,0x23,0x95,0x19,0xc7,0x09,0xeb,0x0f,0x30,0x45,0x63,0x31,0xb1,0x1e,
0x18,0x92,0xec,0xf8,0xe4,0xa2,0x64,0x1d,0x50,0x6f,0x9f,0x42,0x9a,0xe6,0xc4,0x1f,0x03,0x30,0xa4,0xee,
0xa0,0xad,0x0b,0x33,0xad,0xb3,0x12,0x24,0xc4,0x29,0x31,0x44,0xf4,0x08,0x6b,0x60,0x56,0xda,0xe1,0x40,
0xca,0xb9,0xe0,0x5d,0x7d,0x7b,0x18,0x8d,0xc7,0x10,0x2f,0xe8,0x3d,0xe2,0x88,0xfa,0x0f,0x5b,0xbe,0x0f,
0x82,0x38,0x0d,0xe4,0x63,0x90,0x78,0x22,0x14,0x71,0xdb,0xf7,0xc1,0x21,0xd6,0x9b,0x05,0xb3,0x20,0xe8,
0xf1,0x2c,0xf0,0x67,0x45,0xdf,0xe9,0x4c,0xa0,0x9c,0x6c,0xb6,0x74,0xee,0xa5,0x27,0x54,0x5e,0x41,0xf7,
0xb8,0x8a,0x93,0x23,0x4c,0xb3,0x88,0xa3,0x1d,0x72,0xda,0x71,0x49,0xe2,0x06,0x96,0xd5,0xf7,0xa0,0x4b,
0x06,0xd8,0xa7,0x42,0x2c,0x73,0xb0,0x94,0x4c,0x2b,0xf5,0x90,0x40,0x07,0xcd,0x6a,0x49,0x8f,0xd2,0x59,
0xba,0xe4,0x8a,0x24,0x74,0x05,0x54,0x37,0x81,0x45,0x16,0xea,0x1e,0x31,0x06,0x95,0xf2,0xd0,0xfc,0xc4,
0xa2,0x75,0x32,0x4e,0x2d,0xd0,0xd4,0x6e,0x6e,0x98,0xe9,0x22,0x85,0xc9,0xfa,0x79,0x0a,0x2c,0x5f,0x04,
0x67,0x1b,0x78,0xe6,0xd6,0xc8,0x72,0x1c,0xbc,0x11,0xf2,0xde,0x31,0xa2,0xb6,0xad,0xed,0x7e,0xfe,0xac,
0xfd,0x4b,0x2b,0xef,0xec,0x30,0x93,0xfd,0x33,0xac,0xa7,0x33,0xbd,0xcb,0xc5,0x34,0xb2,0x5d,0xdf,0xd3,
0xbc,0xb9,0x4b,0x07,0x8c,0x81,0x4d,0x2b,0x74,0x6d,0x82,0xec,0xe3,0xd2,0x0e,0xfd,0x05,0x6f,0xc7,0x25,
0xaa,0xb2,0x94,0x94,0x8e,0x06,0xbf,0x65,0xfb,0x93,0x21,0xc4,0x31,0xe4,0x3b,0x81,0x31,0x2f,0x35,0xc8,
0xa3,0x5c,0x8b,0xed,0x36,0xdd,0x1c,0x39,0x54,0xc9,0x06,0x23,0x7f,0xd2,0xe8,0x5a,0x07,0xdf,0x3e,0xd0,
0x1f,0x8d,0x14,0xb5,0x02,0x63,0xdc,0x9a,0xfe,0x58,0x13,0xc4,0x18,0x52,0xe3,0x57,0xaf,0xaf,0x39,0x29,
0x87,0xca,0x52,0x5b,0x4c,0xb4,0xdf,0x89,0x3f,0x9c,0x20,0x2f,0xde,0x5c,0xd1,0x6d,0xa1,0x35,0x67,0x0b,
0xf7,0x20,0x60,0x4d,0x44,0x4b,0x9c,0x7e,0x18,0x47,0x2a,0x66,0x74,0x13,0xea,0x2f,0x11,0xde,0xa1,0xe3,
0x58,0x10,0xd8,0x2c,0x08,0x24,0x99,0xb0,0xf4,0x2a,0x25,0x7c,0x3f,0xc1,0xd2,0xb0,0x83,0x1a,0x8b,0x8d,
0x8f,0xeb,0x94,0xc7,0xa3,0xd6,0xb6,0x68,0x41,0x5f,0xcb,0x16,0x50,0x51,0x01,0xf8,0xe7,0x9f,0x9a,0x82,
0x2a,0x42,0x91,0x6d,0x40,0xf7,0x14,0x89,0x97,0xf6,0xdb,0xa2,0xfb,0x78,0x6c,0x43,0x4f,0xae,0x03,0x2d,
0x30,0x84,0x96,0xc6,0x1d,0x8c,0x1b,0x87,0xce,0x2f,0x41,0x4c,0x1d,0x33,0xa6,0x5a,0x4d,0x92,0x27,0x50,
0xc6,0xf5,0x24,0x5d,0x45,0x4c,0x7d,0x89,0x6f,0x4f,0x15,0xba,0x33,0x2e,0x92,0xb4,0x91,0x97,0x42,0x72,
0x64,0x56,0x4e,0x7a,0x70,0x0c,0x2d,0x42,0xa7,0xad,0x92,0x6f,0xdb,0xe8,0xc9,0x82,0xf6,0xd8,0x33,0xa3,
0xc3,0x02,0x11,0x43,0x01,0x55,0x0e,0x23,0xb5,0x78,0xe4,0xd0,0x34,0x3c,0xa0,0x25,0xaf,0x06,0xc2,0x35,
0x0f,0x43,0xb9,0x68,0x52,0xbe,0x25,0xa7,0x93,0xcb,0x6d,0x46,0xfb,0xef,0xc5,0x5e,0x56,0xc0,0xbf,0x02,
0xc7,0x56,0xb9,0x99,0x9c,0x00,0x00,0x5f,0xdf,0x44,0x59,0x21,0x0a,0xa3,0x9f,0xb2,0x47,0x11,0xa2,0xa6,
0x91,0xc2,0x65,0x43,0x98,0x43,0x3d,0xdf,0x99,0xc2,0x26,0x93,0x32,0x74,0x3a,0x18,0x9c,0x9f,0xd7,0x84,
0x29,0xd8,0x39,0x0e,0xcb,0x56,0x54,0x9b,0xee,0xe5,0x4d,0x7e,0x68,0xff,0x01,0xf5,0xa0,0x52,0x5c,0xde,
0x5b,0x34,0x13,0x51,0x59,0xc7,0xe5,0x75,0x13,0x4f,0x23,0x2c,0xfd,0x28,0xe1,0x36,0xd1,0x1b,0x34,0x24,
0xcf,0x14,0x42,0x69,0xe1,0xb2,0x5a,0xb8,0xef,0xdb,0x18,0x91,0xc5,0xa2,0xbb,0xb9,0xa2,0x24,0x8a,0xad,
0x50,0x72,0x5b,0x0c,0x77,0xe9,0xb0,0x47,0x21,0xe8,0xf4,0xa0,0x93,0x3e,0x2e,0x9b,0x87,0x65,0xc6,0x1d,
0xc9,0x28,0x9b,0x51,0x10,0x33,0xaf,0x8e,0x14,0xb2,0x85,0x37,0x00,0xee,0x85,0x21,0xcd,0xb6,0x3a,0x88,
0xb8,0x16,0xe0,0x29,0x73,0x83,0xb5,0x7f,0xd2,0xf8,0xca,0xfc,0x3e,0xf4,0x18,0x5a,0x80,0xe7,0x6b,0xf5,
0x00,0x68,0xcc,0xc2,0x3c,0x9c,0xb2,0x68,0x61,0xa8,0xac,0x40,0xc9,0x41,0x2a,0x27,0x54,0x08,0x5f,0x0c,
0x30,0x27,0xcb,0xa3,0x6f,0xe2,0xd8,0x94,0x71,0x41,0xf0,0xb5,0x39,0x43,0x22,0xe8,0x96,0xa8,0xf4,0x7c,
0x48,0x16,0xeb,0x1c,0x08,0x8e,0x55,0x94,0xce,0xa0,0x61,0x2f,0x53,0x7b,0x1f,0xf2,0xac,0x84,0xd6,0xf4,
0xf1,0x32,0xb8,0x01,0xcb,0x2a,0x6a,0x47,0x18,0x2d,0x56,0xda,0xe4,0x0c,0xab,0xa8,0x24,0x74,0x8d,0xc3,
0x8b,0x95,0xf6,0x03,0x96,0x95,0xd4,0xfa,0x4b,0x3c,0xa0,0xef,0x67,0x3c,0x40,0x5d,0xbf,0x15,0xa8,0xc7,
0xc2,0x4e,0x87,0xf4,0xd7,0xf4,0xe8,0x6c,0x91,0x1f,0x57,0x97,0x65,0xb1,0x2c,0xa8,0xcf,0xe4,0x2a,0x8d,
0x05,0x75,0x7c,0xa6,0xce,0x8e,0x5d,0xd4,0x69,0xb1,0xe3,0xd4,0x29,0x2d,0xe7,0xe8,0x49,0xec,0xb5,0x58,
0x97,0xc1,0xde,0xfe,0x30,0x3c,0xab,0xa2,0x88,0xe9,0x46,0x99,0x95,0x43,0xe9,0xc5,0x0b,0x18,0x86,0xb4,
0x72,0xa9,0x3b,0xad,0x1a,0xe1,0xde,0x5e,0xe8,0xcd,0x5d,0x9a,0x62,0x00,0xb2,0x31,0x45,0x75,0x59,0x52,
0xa1,0x09,0x13,0x94,0x78,0x0e,0xce,0x24,0x79,0xf5,0x79,0xca,0x55,0xf5,0x79,0x3b,0xed,0xfc,0x30,0x4d,
0x59,0xff,0x10,0x5b,0x96,0x15,0xb1,0x0b,0x4b,0xd9,0xb6,0xc3,0x2b,0x9f,0x85,0xc5,0xec,0x44,0xf0,0xac,
0x58,0xce,0x0a,0xa9,0x25,0x05,0x87,0x60,0x2a,0x58,0xd2,0xa6,0x34,0xae,0x0a,0xa1,0xee,0x63,0x6a,0xb1,
0x7a,0x6e,0xf4,0x25,0xb0,0x44,0xcc,0x2b,0x04,0xb7,0xba,0x43,0xd9,0x87,0x0a,0x77,0x2c,0x09,0x25,0x00,
0x48,0x55,0x74,0x30,0x2b,0x4b,0xea,0x68,0x15,0x57,0xb1,0x4a,0x5a,0xa9,0x3f,0xbf,0x96,0x0e,0x86,0x9e,
0xa9,0xa6,0x33,0x88,0x69,0x95,0x47,0xa7,0x20,0x07,0x28,0x27,0xae,0x82,0x4f,0x68,0x5b,0x0a,0xcb,0x10,
0x9d,0x16,0xa8,0xed,0xa3,0x71,0xbc,0xb7,0xba,0x4f,0x29,0xf8,0x4b,0xd5,0xf7,0x13,0x39,0xda,0xbf,0x63,
0x85,0x9f,0xc8,0x22,0xb9,0x35,0x7e,0x34,0x63,0xdf,0xb1,0xca,0x17,0xee,0x75,0xc8,0xa7,0x65,0x5d,0x74,
0xc0,0xce,0x8f,0x39,0x04,0xf6,0xde,0xe6,0xdf,0x2e,0xb2,0x15,0xc7,0x83,0x82,0xb3,0x4b,0x69,0x4b,0x7d,
0x8d,0x9f,0x26,0xe5,0xab,0xce,0x5d,0xe8,0x28,0xb5,0x08,0x3e,0x03,0xe1,0xf4,0xf1,0x61,0xda,0x60,0x11,
0x47,0x02,0x6b,0x50,0x2a,0xe7,0xc3,0x93,0x34,0xe7,0x82,0x8c,0x78,0x32,0x9b,0x1e,0xc5,0xbe,0x25,0x05,
0xa7,0x23,0xfd,0x0d,0xac,0x55,0x77,0x32,0x29,0x65,0x03,0xc7,0x65,0x87,0xe5,0x0b,0xb5,0x28,0x4e,0x1b,
0x52,0x5a,0x4e,0x1d,0xba,0x00,0x4e,0x6e,0xdd,0xbc,0xcd,0xd1,0xff,0x6b,0xa8,0xfc,0x1a,0x4a,0x98,0xf2,
0x7f,0x51,0x45,0x25,0xb2,0x43,0x5e,0xc7,0x7f,0x8f,0x4a,0x6a,0xf1,0xf1,0x61,0xe2,0x5d,0xc0,0xa2,0xaa,
0xab,0x26,0xbf,0x5f,0x8c,0x2b,0x2e,0xf9,0xbd,0x41,0xb1,0x6a,0x4b,0x92,0x90,0x5e,0xc4,0x26,0xb3,0xbf,
0x9a,0x27,0x37,0xed,0x9b,0x4c,0x81,0x02,0x91,0x24,0x7b,0xce,0xe5,0xa5,0x8b,0x22,0xcb,0xd1,0x05,0x67,
0x87,0xb9,0xc8,0xa4,0x57,0xb7,0x2b,0x23,0x93,0x64,0x33,0xc8,0xa4,0xf2,0x83,0x10,0x64,0x28,0xc5,0x69,
0x7b,0xfe,0x32,0xe0,0xc1,0x37,0x4f,0x14,0x1c,0x5c,0x3e,0x7e,0xdf,0x90,0xd2,0x90,0x70,0xbd,0x35,0xc5,
0xc8,0x13,0x58,0x5c,0x3a,0xa0,0x99,0x83,0x95,0x78,0x42,0x5a,0x3e,0xa6,0x48,0x9a,0xe3,0x8a,0x9f,0x54,
0xd8,0x62,0xea,0x52,0x7c,0xcb,0x67,0xd1,0x30,0x75,0x77,0xa9,0xa2,0xbc,0x29,0x66,0xc2,0xec,0x8d,0x4d,
0x30,0x9b,0x2c,0x3c,0x36,0x59,0x86,0xc4,0x8e,0xb5,0xe2,0x84,0x33,0x4d,0x99,0x99,0x96,0xb3,0xaf,0x3f,
0x14,0xa9,0x9f,0x24,0xad,0x8e,0x5c,0x95,0x36,0xe4,0xd2,0x1c,0x85,0xa9,0x6f,0x17,0x71,0x02,0xe4,0xaa,
0xcd,0x8c,0x96,0xda,0x85,0x1d,0xa6,0xa7,0x4d,0x99,0xf4,0x4f,0x7f,0x68,0x43,0x6f,0x02,0xc8,0x8b,0xd2,
0x4b,0x23,0x6a,0x21,0x5f,0x8d,0x75,0xa9,0x3d,0x56,0xa2,0xff,0x97,0xb0,0xc7,0xc0,0x83,0x33,0xa9,0xa2,
0x97,0x98,0x03,0x52,0x11,0xc0,0xa1,0x16,0x25,0xda,0x88,0xf8,0x1e,0xa8,0x99,0xd7,0x0f,0xa6,0x43,0x3c,
0x76,0xf5,0x50,0x85,0x38,0xa4,0x2d,0x83,0x7c,0xe8,0x5a,0x40,0x87,0xa6,0x63,0x19,0x6c,0x5d,0xce,0x51,
0xd2,0x8d,0x79,0x98,0xdf,0xb3,0x61,0x46,0x7d,0x2b,0xc7,0x19,0x53,0x97,0xc6,0xea,0xdf,0x63,0x41,0x8c,
0x5e,0x4c,0xe7,0x2c,0x86,0xbe,0xcb,0x6e,0x47,0x34,0x11,0x9e,0xcc,0x00,0x86,0xc9,0x17,0xcf,0xe2,0x1d,
0xf6,0x28,0xa0,0xa5,0x4f,0x1f,0xe4,0xa1,0x8e,0x90,0x05,0x85,0x79,0x43,0xee,0x26,0x6b,0xf9,0x0b,0x18,
0x29,0xb6,0x8b,0x18,0x69,0x17,0x3b,0x63,0x96,0x69,0xf8,0x5d,0xb3,0xe0,0x0d,0x35,0x1d,0x6f,0x8a,0xc8,
0x4b,0x4a,0xd9,0xc8,0x19,0xab,0x84,0xb7,0x76,0x32,0x66,0xc9,0xcd,0x9e,0x69,0xc1,0x45,0xe9,0x33,0xb6,
0xd5,0xb2,0x1b,0x31,0xe9,0x62,0x8f,0xfa,0x27,0xf6,0x6a,0xf1,0x95,0xa2,0xdc,0xee,0x43,0x4b,0x25,0x2f,
0x26,0x2c,0xa8,0x98,0xfa,0x5c,0x81,0xca,0x43,0x6c,0xcf,0x4d,0x5b,0xe1,0x87,0xbf,0xec,0x94,0x2f,0xc6,
0x3a,0x18,0x7d,0x65,0xfb,0x00,0x2b,0x99,0x36,0x29,0xec,0x3e,0xc4,0x53,0xa9,0x20,0x8e,0xc7,0x23,0x08,
0x45,0xd2,0x66,0xac,0x25,0x4e,0x28,0x69,0x3d,0x25,0x45,0x11,0xbd,0x00,0x15,0x1f,0xcf,0x94,0x11,0x32,
0xb8,0x42,0x52,0xb1,0x03,0x1a,0x59,0x97,0x0a,0x5d,0xdc,0x51,0x71,0x7c,0x16,0xc8,0xb8,0x34,0xd5,0x74,
0x05,0xbc,0x22,0xc6,0x62,0xc2,0x19,0x20,0x5c,0xe3,0x0a,0x00,0xa4,0xbb,0x77,0x12,0x80,0xe0,0x06,0xde,
0x32,0x00,0xc2,0x11,0x53,0x00,0x98,0x46,0xc5,0x19,0x7f,0x4e,0x0d,0x63,0x39,0xe1,0x95,0xf2,0xf0,0x46,
0x20,0xa3,0x6d,0x86,0xaf,0x2e,0xb3,0xd0,0x02,0xfe,0x42,0xf8,0x22,0xdd,0x11,0xc8,0xa8,0xe9,0x44,0x0b,
0x37,0x7d,0x99,0xa0,0x87,0x00,0xeb,0xe6,0x55,0x24,0x2b,0x27,0x53,0x4e,0xa2,0xf1,0x17,0x51,0xf3,0x01,
0xf6,0x39,0xaf,0x72,0x0f,0x98,0x89,0x79,0x17,0xd9,0x64,0x41,0xd0,0xe7,0x4d,0x1e,0x15,0xbb,0x6a,0xd4,
0xab,0xdd,0x34,0x8a,0xb0,0xbd,0x98,0x5f,0x47,0x5a,0xb2,0x77,0x4f,0x62,0x74,0x5b,0x74,0xfc,0xac,0xa3,
0xe2,0x9e,0x25,0xd4,0xf6,0x07,0x55,0x35,0x3a,0x4a,0x58,0x05,0x1e,0xd3,0xb3,0x0c,0x1f,0xef,0x6b,0x25,
0x80,0xd5,0xae,0x48,0xdc,0x0a,0x88,0x21,0xa9,0x30,0xc8,0x48,0xd7,0x42,0x98,0xc0,0x0d,0x7b,0x5c,0x09,
0x68,0xf7,0xbe,0x5d,0xc7,0xb4,0x10,0xc1,0xfd,0xba,0xca,0x9e,0x32,0xb9,0x30,0xe0,0x84,0xce,0x85,0xa0,
0xdd,0xd9,0x84,0xf7,0xfb,0x4e,0xc8,0xb5,0xab,0x85,0x90,0x6b,0x57,0xef,0x80,0x4c,0x75,0x2e,0x85,0xcc,
0xfa,0x5d,0x01,0xf2,0x6c,0x52,0x15,0xfb,0xa6,0x0c,0xda,0x88,0x52,0x10,0x68,0xac,0x29,0x17,0xa3,0xd4,
0xd9,0x4a,0x10,0x9b,0x18,0xbe,0xfa,0xd0,0xd6,0xe7,0x0a,0x90,0x11,0xad,0x30,0xcc,0x58,0xdb,0x02,0xa0,
0x52,0x97,0x79,0xf9,0x6b,0x02,0xdc,0x4c,0xfe,0x92,0x5f,0x94,0x50,0x72,0xb5,0xdb,0xba,0x84,0x19,0xd4,
0x31,0xa5,0x48,0x3e,0x97,0xf4,0xe4,0xe1,0x95,0xbb,0xd2,0x96,0x96,0x8f,0x31,0xb7,0x59,0xa8,0x7e,0xfc,
0x61,0x1b,0x92,0xa2,0x57,0xde,0xd7,0x94,0x57,0xde,0xe3,0xab,0xd3,0xaa,0x5b,0xd3,0xd9,0xa2,0xd7,0x71,
0x13,0x35,0xef,0xcf,0x16,0x78,0xf5,0x9d,0x5f,0xb5,0x70,0x7b,0x19,0xd3,0xb6,0x86,0x40,0x7f,0x51,0xd7,
0xbe,0xd2,0x29,0xa0,0xc4,0xaf,0xb8,0x77,0xbb,0xcc,0xe9,0xd9,0xc1,0x76,0xcd,0xf1,0xe5,0x93,0x13,0x49,
0xa1,0xce,0x28,0xc5,0xfc,0x3d,0x56,0x94,0x71,0x9e,0x8c,0xc6,0xe0,0x30,0xbb,0x1c,0x1c,0x66,0x97,0xf7,
0x13,0xf3,0x2c,0xb3,0xcf,0xd0,0x57,0x80,0x0d,0x76,0xe3,0x54,0x3b,0x39,0xd6,0xca,0x8a,0xad,0xd2,0x62,
0x6e,0xb3,0xa2,0x1a,0x56,0xe2,0xe2,0x6b,0x68,0xaf,0xca,0x0f,0xbb,0x71,0x9b,0x77,0x92,0x26,0x21,0xf6,
0x09,0xac,0xf9,0xb4,0x3c,0x4f,0x1d,0x97,0x29,0x38,0x96,0x9f,0x99,0xa9,0x6d,0xc2,0x87,0x44,0xd4,0xc6,
0x08,0x86,0x9b,0x77,0xe3,0x37,0xab,0xa6,0xc4,0xee,0x64,0xd3,0x3f,0xa4,0xf2,0x55,0xbc,0x40,0xd9,0xe6,
0xcd,0x05,0x4e,0xeb,0x57,0xdd,0x4b,0xda,0x34,0x8d,0x49,0x61,0xd5,0xe7,0xdd,0x89,0x61,0x9a,0x80,0x74,
0xb2,0xd4,0x8d,0x0f,0xfc,0xc3,0x28,0x09,0x3a,0x53,0x11,0x44,0x1b,0xfb,0x4a,0x2a,0xe1,0x30,0x72,0x0e,
0x59,0xf2,0x25,0x05,0x7f,0x1f,0x39,0x42,0x6f,0xf9,0xaf,0x24,0xe9,0x0a,0x4b,0xe4,0xea,0x56,0x77,0xdc,
0x39,0x7f,0x47,0x23,0x74,0x0f,0xb1,0xf4,0xbd,0x59,0xc0,0x9b,0xfe,0xdc,0x2c,0x3c,0xba,0xb8,0x13,0x64,
0xfe,0xd5,0x59,0x4a,0x22,0xfa,0xda,0x2c,0xf3,0x96,0x27,0xf8,0xc4,0xd5,0xa2,0x53,0xe1,0x83,0x31,0xf5,
0xa5,0x67,0x30,0x05,0xa2,0xb1,0x74,0xc2,0x37,0xe5,0x55,0x97,0xda,0x47,0xfa,0xe4,0x55,0x7c,0xec,0xba,
0xcd,0xbf,0xf1,0xfd,0x0f,0x58,0xcc,0x40,0x6c,0xf9,0x3b,0x00,0x00};
#endif

2384
src/assets/js.h Normal file

File diff suppressed because it is too large Load Diff

13
src/assets/version.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef __assets_version
#define __assets_version
const uint8_t VersionMajor = 2;
const uint8_t VersionMinor = 0;
const uint8_t VersionPatch = 0;
const uint8_t VersionMetadata = 43;
const char VersionBranch[] = "release/2.0";
const char VersionSemVer[] = "2.0.0-beta.1";
const char VersionFullSemVer[] = "2.0.0-beta.1+43";
const char VersionCommitDate[] = "2018-02-16";
#endif

44
src/charproperties.cpp Normal file
View File

@ -0,0 +1,44 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./charproperties.h"
#include <cstddef>
#include <string.h>
#include "./debug.h"
void assignChar(char** field, const char* newValue)
{
if (*field != nullptr)
delete *field;
if (newValue != nullptr)
{
// Include the terminating null character
size_t length = strlen(newValue) + 1;
if (length > 0)
{
*field = new char[length];
strncpy(*field, newValue, length);
}
else
*field = nullptr;
}
else
*field = nullptr;
}
bool sameStr(const char* value1, const char* value2)
{
if ((value1 == nullptr) != (value2 == nullptr))
return true;
if (value1 == nullptr)
return false;
return strcmp(value1, value2) == 0;
}

15
src/charproperties.h Normal file
View File

@ -0,0 +1,15 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __charproperties
#define __charproperties
#include <stdint.h>
void assignChar(char** field, const char* newValue);
bool sameStr(const char* value1, const char* value2);
#endif

View File

@ -3,7 +3,7 @@
* Copyright 2017 (c) Mark van Renswoude
*/
#include <stdint.h>
#include "PCA9685.h"
#include "./PCA9685.h"
#include <Wire.h>
#include <Arduino.h>
@ -18,7 +18,7 @@ void PCA9685::setAddress(uint8_t address, uint8_t pinSDA, uint8_t pinSCL)
void PCA9685::setAddress(uint8_t address)
{
this->address = address;
this->mAddress = address;
}
@ -26,11 +26,11 @@ uint8_t PCA9685::read(uint8_t registerAddress)
{
uint8_t result = 0;
Wire.beginTransmission(this->address);
Wire.beginTransmission(this->mAddress);
Wire.write(registerAddress);
Wire.endTransmission();
Wire.requestFrom(this->address, (uint8_t)1);
Wire.requestFrom(this->mAddress, (uint8_t)1);
if (Wire.available())
result = Wire.read();
@ -40,7 +40,7 @@ uint8_t PCA9685::read(uint8_t registerAddress)
void PCA9685::write(uint8_t registerAddress, uint8_t value)
{
Wire.beginTransmission(this->address);
Wire.beginTransmission(this->mAddress);
Wire.write(registerAddress);
Wire.write(value);
Wire.endTransmission();
@ -97,7 +97,7 @@ void PCA9685::setPWM(uint8_t pin, uint16_t value)
void PCA9685::setPWM(uint8_t pin, uint16_t on, uint16_t off)
{
Wire.beginTransmission(this->address);
Wire.beginTransmission(this->mAddress);
this->write(PCA9685::RegisterLED0OnL + (4 * pin));
this->write(on);
this->write(on >> 8);
@ -124,7 +124,7 @@ void PCA9685::setAll(uint16_t value)
void PCA9685::setAll(uint16_t on, uint16_t off)
{
Wire.beginTransmission(this->address);
Wire.beginTransmission(this->mAddress);
this->write(PCA9685::RegisterAllLEDOnL);
this->write(on);
this->write(on >> 8);

View File

@ -11,7 +11,7 @@
class PCA9685
{
private:
uint8_t address;
uint8_t mAddress;
protected:
uint8_t read(uint8_t registerAddress);

View File

@ -1,56 +1,67 @@
#ifndef __Config
#define __Config
#ifndef __config
#define __config
#include <Arduino.h>
#include <stdint.h>
#include "credentials.h"
// Enables debug information to be output through the standard
// Serial connection, disable in production units to improve performance
//#define SerialDebug
// The name of this device on the network
static const char* WiFiHostname = "Stairs";
// The number of steps (assumed to be <= 16, as the code currently only controls 1 PCA9685 board)
static const uint8_t StepCount = 14;
// The port number on which the UDP server listens
static const uint16_t UDPPort = 3126;
// Pins for the I2C bus
static const uint8_t PinSDA = 13;
static const uint8_t PinSCL = 12;
// I2C address and PWM frequency of the PCA9685 board
static const uint8_t PWMDriverAddress = 0x40;
static const uint16_t PWMDriverPWMFrequency = 1600;
// Determines if OTA firmware updates are enabled
// 0 - Disabled
// 1 - Enabled (fixed URL)
// 2 - Enabled (use URL in command)
static const uint8_t OTAUpdateEnabled = 2;
static const char* OTAUpdateFixedHost = "";
static const uint16_t OTAUpdateFixedPort = 80;
static const char* OTAUpdateFixedPath = "/";
// The minimum amount of time (in milliseconds) between update requests
static const uint32_t OTAUpdateThrottle = 5000;
// Enables the crash API methods to cause crashes, you probably never
// want to leave this on unless you're debugging the exception handler
//#define EnableCrashAPI
#ifdef SerialDebug
#define _d(msg) Serial.print(msg)
#define _dln(msg) Serial.println(msg)
#else
#define _d(msg) do { } while (0)
#define _dln(msg) do { } while (0)
static const uint32_t SerialDebugBaudrate = 115200;
static const uint32_t SerialDebugStartupDelay = 2000;
#endif
static const char* ConnectionSettingsFile = "/connection.json";
static const char* SystemSettingsFile = "/system.json";
static const char* StepsSettingsFile = "/steps.json";
static const char* TimeTriggerSettingsFile = "/timetriggers.json";
static const char* MotionTriggerSettingsFile = "/motiontriggers.json";
static const char* DefaultAPSSIDPrefix = "Stairs-";
static const char* DefaultNTPServer = "pool.ntp.org";
// Timeout when in AP + station mode (otherwise trying to connect
// to the STA will block the AP)
static const uint32_t StationModeTimeout = 30000;
static const uint16_t APButtonHoldTime = 2000;
// Only used if the timezone has not been succesfully retrieved yet, otherwise
// the configurable NTP interval is used
static const uint32_t TimezoneRetryInterval = 60000;
// SSL takes quite a bit of memory (and I haven't been optimizing much),
// which seems to cause memory-related exceptions when getting the timezone
// information from Google's HTTPS API. Google requires HTTPS. The workaround
// is hosting a small proxy script on HTTP, which is included in the "hosted" folder
// of this project. Note that this completely defeats any security, and may
// cause your Google API key and location data to leak. My advice is simply to not
// specify your location too precisely :-)
//
// If you want to host your own version of the script because you don't trust
// that mine will not log anything, or want to disable the proxy script
// completely, change these definitions below. Also update platformio-buildflags.bat
// to enable SSL support in ESPAsyncTCP.
//
// If you can fix my sloppy code and get a direct SSL connection working,
// I'd be interested in the changes as well!
#define MapsAPIViaProxyScript
#ifdef MapsAPIViaProxyScript
static const char* TimezoneProxyScriptHost = "api.x2software.net";
static const char* TimezoneProxyScriptPath = "/timezone.php";
#endif
#endif

View File

@ -1,3 +0,0 @@
// Create a copy of this file called "credentials.h"
static const char* WiFiSSID = "example";
static const char* WiFiPassword = "example";

18
src/debug.cpp Normal file
View File

@ -0,0 +1,18 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./debug.h"
void _dinit()
{
#ifdef SerialDebug
Serial.begin(SerialDebugBaudrate);
// Enable if you want detailed WiFi state logging
//Serial.setDebugOutput(true);
delay(SerialDebugStartupDelay);
#endif
}

24
src/debug.h Normal file
View File

@ -0,0 +1,24 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __serialdebug
#define __serialdebug
#include "./config.h"
#include <Arduino.h>
void _dinit();
#ifdef SerialDebug
#define _d(msg) Serial.print(msg)
#define _dln(msg) Serial.println(msg)
#else
#define _d(msg) do { } while (0)
#define _dln(msg) do { } while (0)
#endif
#endif

35
src/global.cpp Normal file
View File

@ -0,0 +1,35 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./global.h"
ConnectionSettings* connectionSettings = new ConnectionSettings();
bool connectionSettingsChanged = false;
SystemSettings* systemSettings = new SystemSettings();
bool systemSettingsChanged = false;
StepsSettings* stepsSettings = new StepsSettings();
bool stepsSettingsChanged = false;
TimeTriggerSettings* timeTriggerSettings = new TimeTriggerSettings();
bool timeTriggerSettingsChanged = false;
MotionTriggerSettings* motionTriggerSettings = new MotionTriggerSettings();
bool motionTriggerSettingsChanged = false;
Stairs* stairs;
bool shouldReboot = false;
uint32_t currentTime;
NTPClient* ntpClient = nullptr;
bool hasTimezone = false;
uint32_t timezoneOffset = 0;
IPAddress emptyIP(0, 0, 0, 0);

49
src/global.h Normal file
View File

@ -0,0 +1,49 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __global
#define __global
#include <stdint.h>
#include <stdbool.h>
#include <IPAddress.h>
#include <NTPClient.h>
#include "./settings/connection.h"
#include "./settings/system.h"
#include "./settings/steps.h"
#include "./settings/triggers/time.h"
#include "./settings/triggers/motion.h"
#include "./stairs.h"
extern ConnectionSettings* connectionSettings;
extern bool connectionSettingsChanged;
extern SystemSettings* systemSettings;
extern bool systemSettingsChanged;
extern StepsSettings* stepsSettings;
extern bool stepsSettingsChanged;
extern TimeTriggerSettings* timeTriggerSettings;
extern bool timeTriggerSettingsChanged;
extern MotionTriggerSettings* motionTriggerSettings;
extern bool motionTriggerSettingsChanged;
extern Stairs* stairs;
extern bool shouldReboot;
extern uint32_t currentTime;
extern NTPClient* ntpClient;
extern bool hasTimezone;
extern uint32_t timezoneOffset;
extern IPAddress emptyIP;
#endif

View File

@ -1,365 +1,153 @@
/*
* Stairs lighting
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include <stdint.h>
#include <Stream.h>
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <ESP8266httpUpdate.h>
#include <WiFiUDP.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncTCP.h>
#include <TimeLib.h>
#include <ArduinoJson.h>
#include <EspSaveCrash.h>
#include "config.h"
#include "protocol.h"
#include "components\PCA9685.h"
//#include "modes\adc.h"
#include "modes\alternate.h"
#include "modes\custom.h"
#include "modes\slide.h"
#include "modes\static.h"
#include "stairs.h"
#include "version.h"
extern "C" {
#include <user_interface.h>
}
#include "./config.h"
#include "./debug.h"
#include "./global.h"
#include "./components/PCA9685.h"
#include "./settings/connection.h"
#include "./server/static.h"
#include "./server/settings.h"
#include "./server/firmware.h"
#include "./server/api.h"
#include "./main.wifi.h"
#include "./main.debug.h"
#include "./main.led.h"
#include "./main.triggers.h"
PCA9685* pwmDriver;
Stairs* stairs;
WiFiUDP udpServer;
uint8_t currentModeIdentifier;
IMode* currentMode;
ADC_MODE(ADC_VCC);
// Forward declarations
void checkRequest();
void handleRequest(uint8_t* packet);
IMode* createMode(uint8_t identifier);
void setCurrentMode(IMode *mode, uint8_t identifier);
void handleCurrentMode();
void handleNotFound(AsyncWebServerRequest* request);
AsyncWebServer server(80);
PCA9685* pwmDriver;
void setup()
{
#ifdef SerialDebug
Serial.begin(115200);
delay(5000);
#endif
_dinit();
currentTime = millis();
if (!SPIFFS.begin())
_dln("Setup :: failed to mount file system");
connectionSettings->read();
systemSettings->read();
stepsSettings->read();
timeTriggerSettings->read();
motionTriggerSettings->read();
pinMode(systemSettings->pinAPButton(), INPUT_PULLUP);
pinMode(systemSettings->pinLEDAP(), OUTPUT);
pinMode(systemSettings->pinLEDSTA(), OUTPUT);
initMotionPins();
_dln("Initializing PCA9685");
_d("Version: ");
_dln(FirmwareVersion);
_dln("Setup :: initializing PCA9685");
pwmDriver = new PCA9685();
pwmDriver->setAddress(PWMDriverAddress, PinSDA, PinSCL);
pwmDriver->setPWMFrequency(PWMDriverPWMFrequency);
_dln("Initializing Stairs");
pwmDriver->setAddress(systemSettings->pwmDriverAddress(), systemSettings->pinPWMDriverSDA(), systemSettings->pinPWMDriverSCL());
pwmDriver->setPWMFrequency(systemSettings->pwmDriverFrequency());
pwmDriver->setAll(0);
_dln("Setup :: initializing Stairs");
stairs = new Stairs();
stairs->init(pwmDriver);
_dln("Initializing WiFi");
WiFi.mode(WIFI_STA);
WiFi.hostname(WiFiHostname);
WiFi.begin(WiFiSSID, WiFiPassword);
_dln("Starting initialization sequence");
stairs->setAll(IStairs::Off);
stairs->set(0, IStairs::On);
_dln("Setup :: starting initialization sequence");
stairs->set(0, 255);
delay(300);
for (int step = 1; step < StepCount; step++)
uint8_t stepCount = stepsSettings->count();
for (int step = 1; step < stepCount; step++)
{
stairs->set(step - 1, IStairs::Off);
stairs->set(step, IStairs::On);
stairs->set(step - 1, 0);
stairs->set(step, 255);
delay(300);
}
stairs->set(StepCount - 1, IStairs::Off);
stairs->set(stepCount - 1, 0);
_dln("Setup :: initializing WiFi");
WiFi.persistent(false);
WiFi.mode(WIFI_OFF);
_dln("Waiting for WiFi");
initDebug();
initWiFi();
// Pulsate the bottom step while WiFi is connecting
uint16_t brightness = 0;
uint16_t speed = 16;
_dln("Setup :: registering routes");
registerStaticRoutes(&server);
registerAPIRoutes(&server);
registerSettingsRoutes(&server);
registerFirmwareRoutes(&server);
while (WiFi.status() != WL_CONNECTED)
{
brightness += speed;
if (brightness <= 0 || brightness >= 1024)
speed = -speed;
stairs->set(0, brightness);
delay(16);
}
setCurrentMode(new StaticMode(), Mode::Static);
_d("IP address: ");
_dln(WiFi.localIP());
_dln("Starting UDP server");
// Start the UDP server
udpServer.begin(UDPPort);
_dln("Setup :: starting HTTP server");
server.onNotFound(handleNotFound);
server.begin();
}
uint32_t currentTime;
// Note: the packet size must at least be able to accomodate the
// command with the largest parameter list, there is no overflow
// checking in the mode classes!
const uint8_t maxPacketSize = 255;
uint8_t packet[maxPacketSize];
uint8_t* packetRef;
void loop()
{
if (shouldReboot || systemSettingsChanged)
{
_dln("Loop :: reboot requested, so long and thanks for all the fish!");
delay(100);
ESP.restart();
}
if (motionTriggerSettingsChanged)
{
initMotionPins();
motionTriggerSettingsChanged = false;
}
currentTime = millis();
checkRequest();
handleCurrentMode();
}
updateDebugStatus();
void checkRequest()
{
int packetSize = udpServer.parsePacket();
if (packetSize)
if (connectionSettingsChanged)
{
_dln("Handling incoming packet");
memset(packet, 0, sizeof(packet));
int length = udpServer.read(packet, maxPacketSize - 1);
if (length && packet[0])
{
packetRef = packet;
handleRequest(packetRef);
}
}
}
void handlePing(uint8_t* packet)
{
_dln("Handling Ping");
udpServer.write(Command::Ping);
udpServer.write(StepCount);
}
void handleGetMode(uint8_t* packet)
{
_dln("Handling GetMode");
udpServer.write(Command::GetMode);
udpServer.write(currentModeIdentifier);
currentMode->write(&udpServer);
}
void handleSetMode(uint8_t* packet)
{
_dln("Handling SetMode");
uint8_t newIdentifier = *packet;
packet++;
IMode* newMode = createMode(newIdentifier);
if (newMode != NULL)
{
newMode->read(packet);
udpServer.write(Command::SetMode);
udpServer.write(newIdentifier);
newMode->write(&udpServer);
_dln("Updating current mode");
setCurrentMode(newMode, newIdentifier);
}
else
{
udpServer.write(Command::Error);
udpServer.write(Command::SetMode);
udpServer.write(newIdentifier);
}
}
void handleGetRange(uint8_t* packet)
{
udpServer.write(Command::GetRange);
stairs->getRange(&udpServer);
}
void handleSetRange(uint8_t* packet)
{
stairs->setRange(packet);
udpServer.write(Command::SetRange);
stairs->getRange(&udpServer);
currentMode->init(stairs, currentTime);
}
uint32_t lastUpdateCheck = 0;
void handleUpdateFirmware(uint8_t* packet)
{
_dln("Handling UpdateFirmware");
HTTPUpdateResult result;
if (currentTime - lastUpdateCheck <= OTAUpdateThrottle)
{
udpServer.write(Command::Error);
udpServer.write(Command::UpdateFirmware);
udpServer.write((uint8_t)2);
return;
_dln("Loop :: connection settings changed");
initWiFi();
connectionSettingsChanged = false;
}
lastUpdateCheck = currentTime;
switch (OTAUpdateEnabled)
{
case 1:
_dln("Checking for update (fixed)");
result = ESPhttpUpdate.update(OTAUpdateFixedHost, OTAUpdateFixedPort, OTAUpdateFixedPath, FirmwareVersion);
break;
updateWiFi();
updateLED();
updateNTPClient();
checkTriggers();
case 2:
{
_dln("Checking for update (client defined)");
uint16_t port;
memcpy(&port, packet, sizeof(port));
packet += sizeof(port);
_d("Port: ");
_dln(port);
char host[255];
char path[255];
strcpy(host, (char*)packet);
packet += strlen(host) + 1;
strcpy(path, (char*)packet);
_d("Host: ");
_dln(host);
_d("Path: ");
_dln(path);
result = ESPhttpUpdate.update(host, port, path, FirmwareVersion);
break;
}
default:
udpServer.write(Command::Error);
udpServer.write(Command::UpdateFirmware);
udpServer.write((uint8_t)0);
return;
}
switch (result)
{
case HTTP_UPDATE_NO_UPDATES:
_dln("No updates");
udpServer.write(Command::UpdateFirmware);
udpServer.write((uint8_t)0);
break;
case HTTP_UPDATE_OK:
_dln("Update OK");
udpServer.write(Command::UpdateFirmware);
udpServer.write((uint8_t)1);
break;
default:
_d("Error while updating: ");
_dln(ESPhttpUpdate.getLastError());
_dln(ESPhttpUpdate.getLastErrorString().c_str());
udpServer.write(Command::Error);
udpServer.write(Command::UpdateFirmware);
udpServer.write((uint8_t)2);
break;
}
stairs->tick();
}
void handleRequest(uint8_t* packet)
void handleNotFound(AsyncWebServerRequest *request)
{
_d("Handling request: ");
_dln(*packet);
// Every request will result in a reply, either containing the
// requested data or a copy of the input parameters for verification.
//
// Apparantly this also makes the ESP8266 more stable, as reports
// have been made that UDP communication can stall if no replies are sent.
udpServer.beginPacket(udpServer.remoteIP(), udpServer.remotePort());
udpServer.write(Command::Reply);
uint8_t command = *packet;
packet++;
switch (command)
{
case Command::Ping: handlePing(packet); break;
case Command::GetMode: handleGetMode(packet); break;
case Command::SetMode: handleSetMode(packet); break;
case Command::GetRange: handleGetRange(packet); break;
case Command::SetRange: handleSetRange(packet); break;
case Command::UpdateFirmware: handleUpdateFirmware(packet); break;
default:
udpServer.write(Command::Error);
udpServer.write(command);
break;
}
udpServer.endPacket();
}
IMode* createMode(uint8_t identifier)
{
if (identifier == currentModeIdentifier)
return currentMode;
switch (identifier)
{
case Mode::Static: return new StaticMode();
case Mode::Custom: return new CustomMode();
case Mode::Alternate: return new AlternateMode();
//case Mode::Slide: return new SlideMode();
//case Mode::ADC: return new ADCInputMode();
}
return NULL;
}
void setCurrentMode(IMode* mode, uint8_t identifier)
{
currentModeIdentifier = identifier;
currentMode = mode;
currentMode->init(stairs, currentTime);
}
void handleCurrentMode()
{
currentMode->tick(stairs, currentTime);
_d("HTTP :: not found: "); _dln(request->url());
request->send(404);
}

77
src/main.debug.h Normal file
View File

@ -0,0 +1,77 @@
#ifdef SerialDebug
void wifiEvent(WiFiEvent_t event);
void initDebug()
{
// onEvent is already deprecated, but since I'm only using it
// for debug purposes we'll see how long it lasts...
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
WiFi.onEvent(wifiEvent);
_d("WiFi :: MAC address: ");
_dln(WiFi.macAddress());
#pragma GCC diagnostic pop
}
void wifiEvent(WiFiEvent_t event)
{
switch (event)
{
case WIFI_EVENT_STAMODE_CONNECTED:
_dln("WiFi:: station mode: connected"); break;
case WIFI_EVENT_STAMODE_DISCONNECTED:
_dln("WiFi:: station mode: disconnected"); break;
case WIFI_EVENT_STAMODE_AUTHMODE_CHANGE:
_dln("WiFi:: station mode: authmode change"); break;
case WIFI_EVENT_STAMODE_GOT_IP:
_dln("WiFi:: station mode: got IP");
_dln(WiFi.localIP());
break;
case WIFI_EVENT_STAMODE_DHCP_TIMEOUT:
_dln("WiFi:: station mode: DHCP timeout"); break;
case WIFI_EVENT_SOFTAPMODE_STACONNECTED:
_dln("WiFi:: soft AP mode: station connected"); break;
case WIFI_EVENT_SOFTAPMODE_STADISCONNECTED:
_dln("WiFi:: soft AP mode: station disconnected"); break;
}
}
uint32_t debugStatusTime = 0;
void updateDebugStatus()
{
if (currentTime - debugStatusTime < 5000) return;
debugStatusTime = currentTime;
_d("Status :: available heap: ");
_dln(ESP.getFreeHeap());
if (ntpClient != nullptr)
{
_d("Status :: time: ");
uint32_t time = ntpClient->getEpochTime();
_d(day(time)); _d("-"); _d(month(time)); _d("-"); _d(year(time)); _d(" ");
_d(hour(time)); _d(":"); _d(minute(time)); _d(":"); _dln(second(time));
_d("Status :: offset: ");
_dln(timezoneOffset);
}
}
#else
#define initDebug() do { } while (0)
#define updateDebugStatus() do { } while (0)
#endif

70
src/main.led.h Normal file
View File

@ -0,0 +1,70 @@
enum LEDState
{
Off,
BlinkLow,
BlinkHigh,
On
};
bool ledAP = false;
LEDState ledWiFi = Off;
uint32_t blinkOnTime = 0;
void updateLED()
{
uint8_t value = (currentTime - blinkOnTime >= 1000) ? LOW : HIGH;
WiFiMode_t mode = WiFi.getMode();
if (mode == WIFI_AP_STA || mode == WIFI_AP)
{
if (!ledAP)
{
digitalWrite(systemSettings->pinLEDAP(), HIGH);
ledAP = true;
}
}
else
{
if (ledAP)
{
digitalWrite(systemSettings->pinLEDAP(), LOW);
ledAP = false;
}
}
if (mode == WIFI_AP_STA || mode == WIFI_STA)
{
wl_status_t status = WiFi.status();
if (status == WL_CONNECTED)
{
if (ledWiFi != On)
{
digitalWrite(systemSettings->pinLEDSTA(), HIGH);
ledWiFi = On;
}
}
else
{
LEDState expectedState = value == HIGH ? BlinkHigh : BlinkLow;
if (ledWiFi != expectedState)
{
digitalWrite(systemSettings->pinLEDSTA(), value);
ledWiFi = expectedState;
}
}
}
else
{
if (ledWiFi != Off)
{
digitalWrite(systemSettings->pinLEDSTA(), LOW);
ledWiFi = Off;
}
}
if (currentTime - blinkOnTime >= 2000)
blinkOnTime = currentTime;
}

399
src/main.triggers.h Normal file
View File

@ -0,0 +1,399 @@
WiFiUDP ntpUDP;
uint32_t lastTimeTriggerChecked = 0;
TimeTrigger* lastTimeTrigger = nullptr;
TimeTrigger* activeTimeTrigger = nullptr;
uint32_t lastTimezoneUpdate = 0;
AsyncClient* timezoneClient = nullptr;
char* response = nullptr;
uint16_t responseSize = 0;
static const uint16_t ResponseMaxSize = 1024;
void initMotionPins()
{
if (!motionTriggerSettings->enabled())
return;
for (uint8_t i = 0; i < motionTriggerSettings->triggerCount(); i++)
{
MotionTrigger* trigger = motionTriggerSettings->trigger(i);
if (trigger->enabled)
pinMode(trigger->pin, INPUT);
}
}
void parseResponse()
{
if (response == nullptr || responseSize == 0)
return;
_dln("Timezone :: response:");
_dln(response);
char* data = response;
if (strncmp(data, "HTTP/1.", 7) != 0)
{
_dln("Timezone :: not an HTTP response");
return;
}
data += 9;
if (strncmp(data, "200", 3) != 0)
{
_dln("Timezone :: invalid HTTP status code");
return;
}
data = strstr(data, "\r\n\r\n");
if (data == nullptr)
{
_dln("Timezone :: end of HTTP headers not found");
return;
}
data += 4;
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(5) + 200);
JsonObject& root = jsonBuffer.parseObject(data);
if (!sameStr(root["status"], "OK"))
{
_dln("Timezone :: invalid status in response");
return;
}
timezoneOffset = root["rawOffset"];
hasTimezone = true;
}
void updateTimezone()
{
if (timezoneClient != nullptr)
return;
timezoneClient = new AsyncClient();
if (!timezoneClient)
return;
timezoneClient->onError([](void* arg, AsyncClient* client, int error)
{
_d("Timezone :: error ");
_dln(error);
timezoneClient = nullptr;
delete client;
lastTimezoneUpdate = currentTime;
}, nullptr);
timezoneClient->onConnect([](void* arg, AsyncClient* client)
{
response = (char*)malloc(ResponseMaxSize + 1);
responseSize = 0;
timezoneClient->onError(nullptr, nullptr);
client->onDisconnect([](void * arg, AsyncClient * c)
{
timezoneClient = nullptr;
delete c;
lastTimezoneUpdate = currentTime;
parseResponse();
free(response);
response = nullptr;
}, nullptr);
client->onData([](void* arg, AsyncClient* c, void* data, size_t len)
{
uint16_t copyLen = responseSize == ResponseMaxSize ? 0 :
responseSize + len > ResponseMaxSize ? ResponseMaxSize - responseSize :
len;
if (copyLen > 0)
{
memcpy(response + responseSize, data, copyLen);
responseSize += copyLen;
response[responseSize] = 0;
}
}, nullptr);
uint32_t timestamp = ntpClient->getEpochTime();
#ifdef MapsAPIViaProxyScript
String request = String("GET ") + TimezoneProxyScriptPath + "?location=" +
#else
String request = "GET /maps/api/timezone/json?location=" +
#endif
String(systemSettings->latitude(), 7) + "," + String(systemSettings->longitude(), 7) +
"8&timestamp=" +
String(timestamp);
if (systemSettings->mapsAPIKey() != nullptr)
request = request + "&key=" + systemSettings->mapsAPIKey();
_d("Timezone :: request: ");
_dln(request);
#ifdef MapsAPIViaProxyScript
request = request + " HTTP/1.0\r\nHost: " + TimezoneProxyScriptHost + "\r\n\r\n";
#else
request = request + " HTTP/1.0\r\nHost: maps.googleapis.com\r\n\r\n";
#endif
client->write(request.c_str());
}, nullptr);
_d("Timezone :: available heap: ");
_dln(ESP.getFreeHeap());
#ifdef MapsAPIViaProxyScript
if(!timezoneClient->connect(TimezoneProxyScriptHost, 80))
#else
if(!timezoneClient->connect("maps.googleapis.com", 443, true))
#endif
{
_dln("Timezone :: failed to connect to host");
AsyncClient * client = timezoneClient;
timezoneClient = nullptr;
delete client;
lastTimezoneUpdate = currentTime;
}
}
void updateNTPClient()
{
if (ntpClient == nullptr && WiFi.status() == WL_CONNECTED &&
systemSettings->ntpServer() != nullptr && systemSettings->ntpInterval() > 0)
{
_dln("NTP :: initializing NTP client");
ntpClient = new NTPClient(ntpUDP, systemSettings->ntpServer(), 0, systemSettings->ntpInterval() * 60 * 1000);
ntpClient->begin();
}
// Only update if we're not in the middle of a transition, as it will block
// the loop until the NTP server responds or times out (up to a second)
if (ntpClient != nullptr && !stairs->inTransition())
{
ntpClient->update();
// Lat/lng 0,0 is off the African coast, I think we can safely assume nobody
// will have WiFi enabled stair lighting at that location.
if (timezoneClient == nullptr && systemSettings->latitude() && systemSettings->longitude())
{
uint32_t interval = hasTimezone ? systemSettings->ntpInterval() * 60 * 1000 : TimezoneRetryInterval;
if (lastTimezoneUpdate == 0 || currentTime - lastTimezoneUpdate > interval)
{
updateTimezone();
lastTimezoneUpdate = currentTime;
}
}
}
}
uint32_t lastTimeElementsChecked = 0;
uint32_t epochTime = 0;
tmElements_t timeElements;
bool isDayTime = true;
tmElements_t* getTimeElements()
{
if (ntpClient == nullptr || !hasTimezone)
return nullptr;
if (lastTimeElementsChecked != 0 && currentTime - lastTimeElementsChecked < 10000)
return epochTime > 0 ? &timeElements : nullptr;
lastTimeElementsChecked = currentTime;
epochTime = ntpClient->getEpochTime();
if (epochTime == 0)
{
_dln("Triggers:: time not synchronised yet");
return nullptr;
}
_dln("Triggers:: updating time elements");
breakTime(epochTime + timezoneOffset, timeElements);
// TODO this is a copy of what is in time.cpp. This code, and probably a lot more
// in this file, could use some cleanup.
Dusk2Dawn location(systemSettings->latitude(), systemSettings->longitude(), timezoneOffset / 3600.0f);
// DST is always hardcoded as false, since it is already included in timezoneOffset
int16_t sunriseMinutes = location.sunrise(timeElements.Year, timeElements.Month, timeElements.Day, false);
int16_t sunsetMinutes = location.sunset(timeElements.Year, timeElements.Month, timeElements.Day, false);
int16_t timeMinutes = (timeElements.Hour * 60) + timeElements.Minute;
isDayTime = timeMinutes >= sunriseMinutes && timeMinutes <= sunsetMinutes;
_d("Triggers:: isDayTime = "); _dln(isDayTime);
return &timeElements;
}
void updateTimeTrigger()
{
if (ntpClient == nullptr || !hasTimezone || !timeTriggerSettings->enabled())
{
activeTimeTrigger = nullptr;
return;
}
if (timeTriggerSettingsChanged)
{
// Time trigger settings changed, activeTimeTrigger pointer is considered
// invalid, force recheck
timeTriggerSettingsChanged = false;
}
else if (currentTime - lastTimeTriggerChecked < 10000)
return;
lastTimeTriggerChecked = currentTime;
tmElements_t* time = getTimeElements();
if (time == nullptr)
{
activeTimeTrigger = nullptr;
return;
}
activeTimeTrigger = timeTriggerSettings->getActiveTrigger(*time);
#ifdef SerialDebug
_d("Triggers:: active time trigger: ");
if (activeTimeTrigger != nullptr)
{
_d(activeTimeTrigger->brightness);
_d(" @ ");
switch (activeTimeTrigger->triggerType)
{
case RelativeToSunrise: _d("sunrise "); break;
case RelativeToSunset: _d("sunset "); break;
}
_dln(activeTimeTrigger->time);
}
else
_dln("null");
#endif
}
uint32_t activeMotionStart = 0;
uint16_t activeMotionBrightness = 0;
MotionDirection activeMotionDirection = Nondirectional;
bool lastMotion = false;
void updateMotionTrigger()
{
if (!motionTriggerSettings->enabled() || !motionTriggerSettings->triggerCount())
{
activeMotionStart = 0;
return;
}
if (!motionTriggerSettings->enabledDuringDay() && isDayTime)
{
activeMotionStart = 0;
return;
}
for (uint8_t i = 0; i < motionTriggerSettings->triggerCount(); i++)
{
MotionTrigger* trigger = motionTriggerSettings->trigger(i);
if (trigger->enabled && digitalRead(trigger->pin) == HIGH)
{
if (activeMotionStart == 0)
{
activeMotionDirection = trigger->direction;
activeMotionBrightness = trigger->brightness;
}
activeMotionStart = currentTime;
}
}
if (activeMotionStart != 0 && currentTime - activeMotionStart >= motionTriggerSettings->delay())
activeMotionStart = 0;
}
void checkTriggers()
{
if (!timeTriggerSettings->enabled() && activeTimeTrigger == nullptr &&
!motionTriggerSettings->enabled() && activeMotionStart == 0)
return;
updateTimeTrigger();
updateMotionTrigger();
bool inTimeTrigger = timeTriggerSettings->enabled() &&
activeTimeTrigger != nullptr &&
activeTimeTrigger->brightness;
bool timeTriggerChanged = activeTimeTrigger != lastTimeTrigger;
lastTimeTrigger = activeTimeTrigger;
bool inMotionTrigger = (activeMotionStart > 0) && (!inTimeTrigger || motionTriggerSettings->enabledDuringTimeTrigger());
bool motionChanged = (activeMotionStart > 0) != lastMotion;
lastMotion = (activeMotionStart > 0);
if (!motionChanged && !timeTriggerChanged)
return;
if (motionChanged)
{
if (inMotionTrigger)
{
_dln("Triggers :: start motion trigger");
if (activeMotionDirection == Nondirectional || motionTriggerSettings->transitionTime() == 0)
stairs->setAll(activeMotionBrightness, motionTriggerSettings->transitionTime(), 0);
else
stairs->sweep(activeMotionBrightness, motionTriggerSettings->transitionTime(), activeMotionDirection == TopDown);
}
else
{
if (inTimeTrigger)
{
_dln("Triggers :: motion stopped, falling back to time trigger");
// Fall back to time trigger value
stairs->setAll(activeTimeTrigger->brightness, motionTriggerSettings->transitionTime(), 0);
}
else
{
_dln("Triggers :: motion stopped, turning off");
// No more motion, no active time trigger, turn off
stairs->setAll(0, motionTriggerSettings->transitionTime(), 0);
}
}
}
else if (timeTriggerChanged && !inMotionTrigger)
{
_dln("Triggers :: time trigger changed");
// Set to time trigger value
stairs->setAll(activeTimeTrigger->brightness, timeTriggerSettings->transitionTime(), 0);
}
}

121
src/main.wifi.h Normal file
View File

@ -0,0 +1,121 @@
bool accessPoint = false;
bool stationMode = false;
uint32_t stationModeStart = 0;
uint32_t apButtonStart = 0;
void startAccessPoint();
void startStationMode();
void initWiFi()
{
WiFi.disconnect();
WiFi.softAPdisconnect();
accessPoint = connectionSettings->flag(AccessPoint);
stationMode = connectionSettings->flag(StationMode) && connectionSettings->ssid() != nullptr;
WiFi.mode(accessPoint && stationMode ? WIFI_AP_STA :
accessPoint ? WIFI_AP :
stationMode ? WIFI_STA :
WIFI_OFF);
if (accessPoint)
startAccessPoint();
if (stationMode)
startStationMode();
}
void updateWiFi()
{
if (stationModeStart > 0)
{
bool isConnected = WiFi.status() == WL_CONNECTED;
if (isConnected)
{
_d("WiFi :: connected, IP address: ");
_dln(WiFi.localIP());
stationModeStart = 0;
}
else if (stationMode && accessPoint &&
currentTime - stationModeStart >= StationModeTimeout)
{
_dln("WiFi :: unable to connect, switching off station mode, status:");
_dln(WiFi.status());
#ifdef SerialDebug
WiFi.printDiag(Serial);
#endif
// Connecting to access point is taking too long and is blocking
// the access point mode, stop trying
stationMode = false;
WiFi.disconnect();
WiFi.mode(WIFI_AP);
}
}
if (!accessPoint)
{
if (digitalRead(systemSettings->pinAPButton()) == LOW)
{
if (apButtonStart == 0)
apButtonStart = currentTime;
else if (currentTime - apButtonStart >= APButtonHoldTime)
{
connectionSettings->flag(AccessPoint, true);
connectionSettings->write();
startAccessPoint();
apButtonStart = 0;
}
}
else if (apButtonStart > 0)
apButtonStart = 0;
}
}
void startAccessPoint()
{
_dln("WiFi :: starting access point");
String ssidString = DefaultAPSSIDPrefix + String(ESP.getChipId(), HEX);
if (WiFi.softAP((const char *)ssidString.c_str()))
{
_d("WiFi :: IP address: ");
_dln(WiFi.softAPIP());
}
else
_d("WiFi :: failed to start soft access point");
}
void startStationMode()
{
_d("WiFi :: starting station mode to: ");
_dln(connectionSettings->ssid());
stationModeStart = currentTime;
if (connectionSettings->hostname() != nullptr)
WiFi.hostname(connectionSettings->hostname());
if (WiFi.begin(connectionSettings->ssid(), connectionSettings->password()))
{
if (connectionSettings->flag(DHCP))
// I've had the same issue as described here with config(0, 0, 0):
// https://stackoverflow.com/questions/40069654/how-to-clear-static-ip-configuration-and-start-dhcp
wifi_station_dhcpc_start();
else
WiFi.config(connectionSettings->ip(), connectionSettings->gateway(), connectionSettings->subnetMask());
}
else
_d("WiFi :: failed to start station mode");
}

View File

@ -1,29 +0,0 @@
#ifndef __Mode
#define __Mode
#include <stdint.h>
#include <Stream.h>
class IStairs
{
public:
static const uint16_t Off = 0;
static const uint16_t On = 4095;
virtual uint8_t getCount() = 0;
virtual void set(uint8_t step, uint16_t value) = 0;
virtual void setAll(uint16_t value) = 0;
};
class IMode
{
public:
virtual void read(uint8_t* data) = 0;
virtual void write(Stream* stream) = 0;
virtual void init(IStairs* stairs, uint32_t currentTime) = 0;
virtual void tick(IStairs* stairs, uint32_t currentTime) = 0;
};
#endif

View File

@ -1,27 +0,0 @@
#include "alternate.h"
#include <Stream.h>
void AlternateMode::init(IStairs* stairs, uint32_t currentTime)
{
stairs->setAll(0);
this->lastChange = currentTime;
this->even = false;
}
void AlternateMode::tick(IStairs* stairs, uint32_t currentTime)
{
if (currentTime - this->lastChange < this->parameters.interval)
return;
this->lastChange = currentTime;
this->even = !this->even;
uint8_t stepCount = stairs->getCount();
for (uint8_t step = 0; step < stepCount; step++)
{
stairs->set(step, ((step % 2) == 0) == this->even ? this->parameters.brightness : 0);
}
}

View File

@ -1,33 +0,0 @@
#ifndef __AlternateMode
#define __AlternateMode
#include <stdint.h>
#include "base.h"
#include "../config.h"
struct AlternateModeParameters
{
uint16_t interval;
uint16_t brightness;
};
class AlternateMode : public BaseMode<AlternateModeParameters>
{
private:
uint32_t lastChange;
bool even = false;
public:
AlternateMode()
{
parameters.interval = 500;
parameters.brightness = IStairs::On;
}
void init(IStairs* stairs, uint32_t currentTime);
void tick(IStairs* stairs, uint32_t currentTime);
};
#endif

View File

@ -1,32 +0,0 @@
#ifndef __BaseMode
#define __BaseMode
#include <stdint.h>
#include <Stream.h>
#include "../config.h"
#include "../mode.h"
template <class T>
class BaseMode : public IMode
{
protected:
T parameters;
public:
virtual void read(uint8_t* data)
{
_d("Reading parameters, size ");
_dln(sizeof(T));
memcpy(&this->parameters, data, sizeof(T));
}
virtual void write(Stream* stream)
{
_d("Writing parameters, size ");
_dln(sizeof(T));
stream->write((uint8_t*)&this->parameters, sizeof(T));
}
};
#endif

View File

@ -1,29 +0,0 @@
#include "custom.h"
#include <stdint.h>
void CustomMode::read(uint8_t* data)
{
// The packet is zeroed before we get our hands on it and
// the size should also be larger as noted in main.cpp,
// so a straight-up copy should be safe.
memcpy(this->values, data, sizeof(this->values));
}
void CustomMode::write(Stream* stream)
{
stream->write((uint8_t*)&this->values, sizeof(this->values));
}
void CustomMode::init(IStairs* stairs, uint32_t currentTime)
{
for (uint8_t step = 0; step < StepCount; step++)
stairs->set(step, this->values[step]);
}
void CustomMode::tick(IStairs* stairs, uint32_t currentTime)
{
}

View File

@ -1,21 +0,0 @@
#ifndef __CustomMode
#define __CustomMode
#include <stdint.h>
#include "base.h"
#include "../config.h"
class CustomMode : public IMode
{
private:
uint16_t values[StepCount];
public:
void read(uint8_t* data);
void write(Stream* stream);
void init(IStairs* stairs, uint32_t currentTime);
void tick(IStairs* stairs, uint32_t currentTime);
};
#endif

View File

@ -1,15 +0,0 @@
#ifndef __SlideMode
#define __SlideMode
#include <stdint.h>
#include "base.h"
#include "../config.h"
class SlideMode : public IMode
{
private:
uint16_t interval;
bool up;
};
#endif

View File

@ -1,52 +0,0 @@
#include "static.h"
void StaticMode::init(IStairs* stairs, uint32_t currentTime)
{
_dln("Initializing static mode:");
_d("currentBrightness: "); _dln(this->currentBrightness);
_d("brightness: "); _dln(this->parameters.brightness);
_d("easeTime: "); _dln(this->parameters.easeTime);
if (this->parameters.easeTime > 0 && this->parameters.brightness != this->currentBrightness)
{
_dln("Easing...");
this->easeStartTime = currentTime;
this->easeStartBrightness = currentBrightness;
if (this->parameters.brightness > this->currentBrightness)
this->easeState = Up;
else
this->easeState = Down;
}
else
{
_dln("Updating immediately...");
this->easeState = None;
stairs->setAll(this->parameters.brightness);
this->currentBrightness = this->parameters.brightness;
}
}
void StaticMode::tick(IStairs* stairs, uint32_t currentTime)
{
if (this->easeState == None)
return;
uint32_t elapsedTime = currentTime - this->easeStartTime;
uint32_t diff = this->easeState == Up ? this->parameters.brightness - this->easeStartBrightness : this->easeStartBrightness - this->parameters.brightness;
uint32_t delta = (diff * elapsedTime) / this->parameters.easeTime;
this->currentBrightness = this->easeState == Up ? this->easeStartBrightness + delta : this->easeStartBrightness - delta;
if (elapsedTime >= this->parameters.easeTime)
{
this->currentBrightness = this->parameters.brightness;
this->easeState = None;
}
stairs->setAll(this->currentBrightness);
}

View File

@ -1,45 +0,0 @@
#ifndef __StaticMode
#define __StaticMode
#include <stdint.h>
#include "base.h"
#include "../config.h"
struct StaticModeParameters
{
uint16_t brightness;
uint16_t easeTime;
};
enum EaseState
{
None,
Up,
Down
};
class StaticMode : public BaseMode<StaticModeParameters>
{
private:
uint16_t currentBrightness;
uint32_t easeStartTime;
uint16_t easeStartBrightness;
EaseState easeState;
public:
StaticMode()
{
parameters.brightness = 0;
parameters.easeTime = 0;
easeState = None;
currentBrightness = 0;
}
void init(IStairs* stairs, uint32_t currentTime);
void tick(IStairs* stairs, uint32_t currentTime);
};
#endif

View File

@ -1,31 +0,0 @@
#ifndef __Protocol
#define __Protocol
#include <stdint.h>
class Command
{
public:
static const uint8_t Error = 0x00;
static const uint8_t Ping = 0x01;
static const uint8_t Reply = 0x02;
static const uint8_t GetMode = 0x03;
static const uint8_t SetMode = 0x04;
static const uint8_t GetRange = 0x05;
static const uint8_t SetRange = 0x06;
static const uint8_t UpdateFirmware = 0xFF;
};
class Mode
{
public:
static const uint8_t Static = 0x01;
static const uint8_t Custom = 0x02;
static const uint8_t Alternate = 0x03;
static const uint8_t Slide = 0x04;
//static const uint8_t ADC = 0x05;
};
#endif

190
src/server/api.cpp Normal file
View File

@ -0,0 +1,190 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./api.h"
#include <ArduinoJson.h>
#include <IPAddress.h>
#include <ESP8266WiFi.h>
#include "./shared.h"
#include "../assets/version.h"
#include "../debug.h"
#include "../global.h"
#include "../settings/connection.h"
#include "../settings/triggers/time.h"
void handleSet(AsyncWebServerRequest *request)
{
_dln("API :: set");
AsyncWebParameter* param;
uint8_t value = 0;
param = request->getParam("value");
if (param != nullptr)
{
value = param->value().toInt();
}
else
{
param = request->getParam("percent");
if (param != nullptr)
{
value = map(param->value().toInt(), 0, 100, 0, 255);
}
else
{
request->send(400);
return;
}
}
uint16_t time = 0;
uint8_t from = 0;
param = request->getParam("time");
if (param != nullptr)
time = param->value().toInt();
param = request->getParam("from");
if (param != nullptr)
{
if (param->value() == "top")
from = 1;
else if (param->value() == "bottom")
from = 2;
}
if (from == 0 || time == 0)
stairs->setAll(value, time, 0);
else
stairs->sweep(value, time, from == 1);
request->send(200);
}
void handleGetStepValues(AsyncWebServerRequest *request)
{
_dln("API :: get steps");
DynamicJsonBuffer jsonBuffer(JSON_ARRAY_SIZE(17));
bool target = !request->hasParam("current");
JsonArray& root = jsonBuffer.createArray();
for (uint8_t step = 0; step < stepsSettings->count(); step++)
root.add(stairs->get(step, target));
AsyncResponseStream *response = request->beginResponseStream("application/json");
root.printTo(*response);
request->send(response);
}
void handlePostStepValues(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post steps");
DynamicJsonBuffer jsonBuffer(2*JSON_ARRAY_SIZE(17) + JSON_OBJECT_SIZE(3) + 130);
JsonObject& root = jsonBuffer.parseObject((char*)data);
if (!root.success())
{
request->send(400);
return;
}
uint16_t transitionTime = root["transitionTime"];
JsonArray& values = root["values"];
JsonArray& startTime = root["startTime"];
size_t startTimeCount = startTime.size();
size_t valueCount = values.size();
if (valueCount > stepsSettings->count())
valueCount = stepsSettings->count();
for (uint8_t step = 0; step < valueCount; step++)
stairs->set(step, values[step], transitionTime, step < startTimeCount ? startTime[step] : 0);
request->send(200);
}
void handleGetTimeTriggers(AsyncWebServerRequest *request)
{
_dln("API :: get time triggers");
AsyncResponseStream *response = request->beginResponseStream("application/json");
timeTriggerSettings->toJson(*response);
request->send(response);
}
void handlePostTimeTriggers(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post time triggers");
bool changed;
if (timeTriggerSettings->fromJson((char*)data, &changed))
{
timeTriggerSettings->write();
if (changed)
timeTriggerSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
void handleGetMotionTriggers(AsyncWebServerRequest *request)
{
_dln("API :: get motion triggers");
AsyncResponseStream *response = request->beginResponseStream("application/json");
motionTriggerSettings->toJson(*response);
request->send(response);
}
void handlePostMotionTriggers(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post motion triggers");
bool changed;
if (motionTriggerSettings->fromJson((char*)data, &changed))
{
motionTriggerSettings->write();
if (changed)
motionTriggerSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
void registerAPIRoutes(AsyncWebServer* server)
{
server->on("/api/set", HTTP_GET, handleSet);
server->on("/api/steps/values", HTTP_GET, handleGetStepValues);
server->on("/api/steps/values", HTTP_POST, devNullRequest, devNullFileUpload, handlePostStepValues);
server->on("/api/triggers/time", HTTP_GET, handleGetTimeTriggers);
server->on("/api/triggers/time", HTTP_POST, devNullRequest, devNullFileUpload, handlePostTimeTriggers);
server->on("/api/triggers/motion", HTTP_GET, handleGetMotionTriggers);
server->on("/api/triggers/motion", HTTP_POST, devNullRequest, devNullFileUpload, handlePostMotionTriggers);
}

13
src/server/api.h Normal file
View File

@ -0,0 +1,13 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __server_api
#define __server_api
#include <ESPAsyncWebServer.h>
void registerAPIRoutes(AsyncWebServer* server);
#endif

74
src/server/firmware.cpp Normal file
View File

@ -0,0 +1,74 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./firmware.h"
#include "../config.h"
#include "../debug.h"
#include "../global.h"
void handleFirmware(AsyncWebServerRequest *request)
{
shouldReboot = !Update.hasError();
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", shouldReboot ? "OK" : "FAIL");
response->addHeader("Connection", "close");
request->send(response);
}
void handleFirmwareFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
_d("Firmware :: file upload: index = "); _d(index);
_d(", len = "); _d(len);
_d(", final = "); _dln(final);
if (!index)
{
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
_d("Firmware :: update start, max sketch space: ");
_dln(maxSketchSpace);
Update.runAsync(true);
if (!Update.begin(maxSketchSpace))
{
#ifdef SerialDebug
Update.printError(Serial);
#endif
}
}
if (!Update.hasError())
{
if (Update.write(data, len) != len)
{
#ifdef SerialDebug
Update.printError(Serial);
#endif
}
}
if (final)
{
if (Update.end(true))
{
_dln("Firmware :: success");
}
else
{
_dln("Firmware :: failed");
#ifdef SerialDebug
Update.printError(Serial);
#endif
}
}
}
void registerFirmwareRoutes(AsyncWebServer* server)
{
server->on("/api/firmware", HTTP_POST, handleFirmware, handleFirmwareFile);
}

13
src/server/firmware.h Normal file
View File

@ -0,0 +1,13 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __server_firmware
#define __server_firmware
#include <ESPAsyncWebServer.h>
void registerFirmwareRoutes(AsyncWebServer* server);
#endif

245
src/server/settings.cpp Normal file
View File

@ -0,0 +1,245 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./settings.h"
#include <ArduinoJson.h>
#include <IPAddress.h>
#include <ESP8266WiFi.h>
#include <EspSaveCrash.h>
#include "./shared.h"
#include "../assets/version.h"
#include "../config.h"
#include "../debug.h"
#include "../global.h"
#include "../settings/connection.h"
extern "C" {
#include <user_interface.h>
}
void handleStatus(AsyncWebServerRequest *request)
{
_dln("API :: status");
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(6));
JsonObject& root = jsonBuffer.createObject();
root["systemID"] = String(ESP.getChipId(), HEX);
root["version"] = String(VersionFullSemVer);
if (ntpClient != nullptr && hasTimezone)
{
root["time"] = ntpClient->getEpochTime();
root["timeOffset"] = timezoneOffset;
}
else
{
root["time"] = false;
root["timeOffset"] = 0;
}
root["resetReason"] = ESP.getResetInfoPtr()->reason;
root["stackTrace"] = SaveCrash.count() > 0;
AsyncResponseStream *response = request->beginResponseStream("application/json");
root.printTo(*response);
request->send(response);
}
void handleConnectionStatus(AsyncWebServerRequest *request)
{
_dln("API :: connection status");
WiFiMode_t mode = WiFi.getMode();
DynamicJsonBuffer jsonBuffer((2 * JSON_OBJECT_SIZE(2)) + JSON_OBJECT_SIZE(3));
JsonObject& root = jsonBuffer.createObject();
JsonObject& ap = root.createNestedObject("ap");
ap["enabled"] = (mode == WIFI_AP || mode == WIFI_AP_STA);
ap["ip"] = WiFi.softAPIP().toString();
JsonObject& station = root.createNestedObject("station");
station["enabled"] = (mode == WIFI_STA || mode == WIFI_AP_STA);
station["status"] = (uint8_t)WiFi.status();
station["ip"] = WiFi.localIP().toString();
AsyncResponseStream *response = request->beginResponseStream("application/json");
root.printTo(*response);
request->send(response);
}
void handleGetConnection(AsyncWebServerRequest *request)
{
_dln("API :: get connection");
AsyncResponseStream *response = request->beginResponseStream("application/json");
connectionSettings->toJson(*response);
request->send(response);
}
void handlePostConnection(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post connection");
bool changed;
if (connectionSettings->fromJson((char*)data, &changed))
{
connectionSettings->write();
if (changed)
connectionSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
void handleGetSystem(AsyncWebServerRequest *request)
{
_dln("API :: get system");
AsyncResponseStream *response = request->beginResponseStream("application/json");
systemSettings->toJson(*response);
request->send(response);
}
void handlePostSystem(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post system");
bool changed;
if (systemSettings->fromJson((char*)data, &changed))
{
systemSettings->write();
if (changed)
systemSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
void handleGetSteps(AsyncWebServerRequest *request)
{
_dln("API :: get steps");
AsyncResponseStream *response = request->beginResponseStream("application/json");
stepsSettings->toJson(*response);
request->send(response);
}
void handlePostSteps(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
_dln("API :: post steps");
bool changed;
if (stepsSettings->fromJson((char*)data, &changed))
{
stepsSettings->write();
if (changed)
stepsSettingsChanged = true;
request->send(200);
}
else
request->send(400);
}
void handleGetStackTrace(AsyncWebServerRequest *request)
{
_dln("API :: get stack trace");
if (SaveCrash.count() == 0)
{
request->send(404);
return;
}
AsyncResponseStream *response = request->beginResponseStream("application/octet-stream");
response->addHeader("Content-Disposition", "attachment; filename=\"stacktrace.txt\"");
SaveCrash.print(*response);
request->send(response);
}
void handleDeleteStackTrace(AsyncWebServerRequest *request)
{
_dln("API :: delete stack trace");
SaveCrash.clear();
request->send(200);
}
#ifdef EnableCrashAPI
#pragma "!!! Crash API is enabled on this build !!!"
void handleCrashException(AsyncWebServerRequest *request)
{
_dln("API :: crash exception");
int* i = nullptr;
*i = 42;
}
void handleCrashSoftWDT(AsyncWebServerRequest *request)
{
_dln("API :: crash soft WDT");
while (true);
}
void handleCrashWDT(AsyncWebServerRequest *request)
{
_dln("API :: crash WDT");
ESP.wdtDisable();
while (true);
}
#endif
void registerSettingsRoutes(AsyncWebServer* server)
{
server->on("/api/status", HTTP_GET, handleStatus);
server->on("/api/connection/status", HTTP_GET, handleConnectionStatus);
server->on("/api/connection", HTTP_GET, handleGetConnection);
server->on("/api/connection", HTTP_POST, devNullRequest, devNullFileUpload, handlePostConnection);
server->on("/api/system", HTTP_GET, handleGetSystem);
server->on("/api/system", HTTP_POST, devNullRequest, devNullFileUpload, handlePostSystem);
server->on("/api/steps", HTTP_GET, handleGetSteps);
server->on("/api/steps", HTTP_POST, devNullRequest, devNullFileUpload, handlePostSteps);
server->on("/api/stacktrace/get", HTTP_GET, handleGetStackTrace);
server->on("/api/stacktrace/delete", HTTP_GET, handleDeleteStackTrace);
#ifdef EnableCrashAPI
server->on("/api/crash/exception", HTTP_GET, handleCrashException);
server->on("/api/crash/softwdt", HTTP_GET, handleCrashSoftWDT);
server->on("/api/crash/wdt", HTTP_GET, handleCrashWDT);
#endif
}

13
src/server/settings.h Normal file
View File

@ -0,0 +1,13 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __server_settings
#define __server_settings
#include <ESPAsyncWebServer.h>
void registerSettingsRoutes(AsyncWebServer* server);
#endif

10
src/server/shared.cpp Normal file
View File

@ -0,0 +1,10 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./shared.h"
void devNullRequest(AsyncWebServerRequest *request) {}
void devNullFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {}

15
src/server/shared.h Normal file
View File

@ -0,0 +1,15 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __server_shared
#define __server_shared
#include <ESPAsyncWebServer.h>
void devNullRequest(AsyncWebServerRequest *request);
void devNullFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final);
#endif

29
src/server/static.cpp Normal file
View File

@ -0,0 +1,29 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./static.h"
#include "../debug.h"
#include "../assets/html.h"
#include "../assets/js.h"
#include "../assets/css.h"
void handleGzipped(AsyncWebServerRequest *request, const String& contentType, const uint8_t * content, size_t len)
{
_d("HTTP :: static: "); _dln(request->url());
AsyncWebServerResponse *response = request->beginResponse_P(200, contentType, content, len);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
}
void registerStaticRoutes(AsyncWebServer* server)
{
server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) { handleGzipped(request, "text/html", EmbeddedIndex, sizeof(EmbeddedIndex)); });
server->on("/bundle.js", HTTP_GET, [](AsyncWebServerRequest *request) { handleGzipped(request, "text/javascript", EmbeddedBundleJS, sizeof(EmbeddedBundleJS)); });
server->on("/bundle.css", HTTP_GET, [](AsyncWebServerRequest *request) { handleGzipped(request, "text/css", EmbeddedBundleCSS, sizeof(EmbeddedBundleCSS)); });
}

14
src/server/static.h Normal file
View File

@ -0,0 +1,14 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __server_static
#define __server_static
#include <ESPAsyncWebServer.h>
void registerStaticRoutes(AsyncWebServer* server);
#endif

View File

@ -0,0 +1,72 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./abstractjson.h"
#include <FS.h>
void AbstractJsonSettings::read()
{
_d(getDebugPrefix()); _dln(" :: opening file");
File settingsFile = SPIFFS.open(getFilename(), "r");
if (!settingsFile)
{
_d(getDebugPrefix()); _dln(" :: failed to open file");
return;
}
size_t size = settingsFile.size();
if (size > 1024)
{
_d(getDebugPrefix()); _dln(" :: file size is too large");
return;
}
if (size == 0)
{
_d(getDebugPrefix()); _dln(" :: zero size file");
return;
}
std::unique_ptr<char[]> buf(new char[size]);
settingsFile.readBytes(buf.get(), size);
settingsFile.close();
_dln(buf.get());
if (fromJson(buf.get()))
{
_d(getDebugPrefix());
_dln(" :: read from file");
}
else
{
_d(getDebugPrefix());
_dln(" :: failed to parse file");
}
}
void AbstractJsonSettings::write()
{
_d(getDebugPrefix()); _dln(" :: opening file for writing");
File settingsFile = SPIFFS.open(getFilename(), "w");
if (!settingsFile)
{
_d(getDebugPrefix()); _dln(" :: failed to open file for writing");
return;
}
toJson(settingsFile);
settingsFile.close();
_d(getDebugPrefix()); _dln(" :: written to file");
}
bool AbstractJsonSettings::fromJson(char* data)
{
return fromJson(data, nullptr);
}

View File

@ -0,0 +1,28 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __settingsjson
#define __settingsjson
#include "../debug.h"
class AbstractJsonSettings
{
protected:
virtual const char* getFilename() = 0;
virtual const char* getDebugPrefix() = 0;
public:
void read();
void write();
virtual void toJson(Print &print) = 0;
virtual bool fromJson(char* data, bool* changed) = 0;
bool fromJson(char* data);
};
#endif

View File

@ -0,0 +1,93 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./connection.h"
#include <ArduinoJson.h>
#include "./abstractjson.h"
#include "../debug.h"
#include "../config.h"
#include "../global.h"
void ConnectionSettings::toJson(Print &print)
{
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(9));
JsonObject& root = jsonBuffer.createObject();
root["hostname"] = hostname();
root["accesspoint"] = flag(AccessPoint);
root["station"] = flag(StationMode);
root["ssid"] = ssid();
root["password"] = password();
root["dhcp"] = flag(DHCP);
root["ip"] = ip() != 0 ? ip().toString() : "";
root["subnetmask"] = subnetMask() != 0 ? subnetMask().toString() : "";
root["gateway"] = gateway() != 0 ? gateway().toString() : "";
root.printTo(print);
}
bool ConnectionSettings::fromJson(char* data, bool* changed)
{
if (changed != nullptr)
*changed = false;
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(9) + 250);
JsonObject& root = jsonBuffer.parseObject(data);
if (!root.success())
return false;
IPAddress jsonIP;
IPAddress jsonSubnetMask;
IPAddress jsonGateway;
const char* jsonHostname = root["hostname"];
bool jsonAccessPoint = root["accesspoint"];
bool jsonStation = root["station"];
const char* jsonSSID = root["ssid"];
const char* jsonPassword = root["password"];
bool jsonDHCP = root["dhcp"];
const char* jsonIPText = root["ip"];
const char* jsonSubnetMaskText = root["subnetmask"];
const char* jsonGatewayText = root["gateway"];
if (jsonIPText == nullptr || !jsonIP.fromString(jsonIPText)) jsonIP = emptyIP;
if (jsonSubnetMaskText == nullptr || !jsonSubnetMask.fromString(jsonSubnetMaskText)) jsonSubnetMask = emptyIP;
if (jsonGatewayText == nullptr || !jsonGateway.fromString(jsonGatewayText)) jsonGateway = emptyIP;
if (!(jsonAccessPoint || jsonStation))
jsonAccessPoint = true;
if ((!sameStr(jsonHostname, hostname())) ||
(jsonAccessPoint != flag(AccessPoint)) ||
(jsonStation != flag(StationMode)) ||
(!sameStr(jsonSSID, ssid())) ||
(!sameStr(jsonPassword, password())) ||
(jsonDHCP != flag(DHCP)) ||
(jsonIP != ip()) ||
(jsonSubnetMask != subnetMask()) ||
(jsonGateway != gateway()))
{
hostname(jsonHostname);
flag(AccessPoint, jsonAccessPoint);
flag(StationMode, jsonStation);
ssid(jsonSSID);
password(jsonPassword);
flag(DHCP, jsonDHCP);
ip(jsonIP);
subnetMask(jsonSubnetMask);
gateway(jsonGateway);
if (changed != nullptr)
*changed = true;
}
return true;
}

74
src/settings/connection.h Normal file
View File

@ -0,0 +1,74 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __settingsconnection
#define __settingsconnection
#include <stdint.h>
#include <stdbool.h>
#include <IPAddress.h>
#include "./abstractjson.h"
#include "../charproperties.h"
#include "../config.h"
enum ConnectionSettingsFlags
{
AccessPoint = 1,
StationMode = 2,
DHCP = 4
};
class ConnectionSettings : public AbstractJsonSettings
{
private:
char* mHostname = nullptr;
uint8_t mFlags = AccessPoint | DHCP;
char* mSSID = nullptr;
char* mPassword = nullptr;
IPAddress mIP = (uint32_t)0;
IPAddress mSubnetMask = (uint32_t)0;
IPAddress mGateway = (uint32_t)0;
protected:
virtual const char* getFilename() { return ConnectionSettingsFile; };
virtual const char* getDebugPrefix() { return "ConnectionSettings"; };
public:
void toJson(Print &print);
bool fromJson(char* data, bool* changed);
char* hostname() { return mHostname; }
void hostname(const char* value) { assignChar(&mHostname, value); }
bool flag(ConnectionSettingsFlags flag) { return (mFlags & flag) != 0; }
void flag(ConnectionSettingsFlags flag, bool enabled)
{
if (enabled)
mFlags |= flag;
else
mFlags &= ~flag;
}
char* ssid() { return mSSID; }
void ssid(const char* value) { assignChar(&mSSID, value); }
char* password() { return mPassword; }
void password(const char* value) { assignChar(&mPassword, value); }
IPAddress ip() { return mIP; }
void ip(IPAddress value) { mIP = value; }
IPAddress subnetMask() { return mSubnetMask; }
void subnetMask(IPAddress value) { mSubnetMask = value; }
IPAddress gateway() { return mGateway; }
void gateway(IPAddress value) { mGateway = value; }
};
#endif

91
src/settings/steps.cpp Normal file
View File

@ -0,0 +1,91 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./steps.h"
#include <ArduinoJson.h>
#include "../debug.h"
StepsSettings::StepsSettings()
{
for (uint8_t i = 0; i < MaxStepCount; i++)
{
mRange[i].start = 0;
mRange[i].end = 4095;
}
}
void StepsSettings::toJson(Print &print)
{
DynamicJsonBuffer jsonBuffer(JSON_ARRAY_SIZE(16) + JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(2));
JsonObject& root = jsonBuffer.createObject();
root["count"] = count();
root["useCurve"] = useCurve();
JsonArray& jsonRanges = root.createNestedArray("ranges");
for (uint8_t step = 0; step < MaxStepCount; step++)
{
JsonObject& jsonRange = jsonBuffer.createObject();
jsonRange["start"] = rangeStart(step);
jsonRange["end"] = rangeEnd(step);
jsonRanges.add(jsonRange);
}
root.printTo(print);
}
bool StepsSettings::fromJson(char* data, bool* changed)
{
if (changed != nullptr)
*changed = false;
DynamicJsonBuffer jsonBuffer(JSON_ARRAY_SIZE(16) + JSON_OBJECT_SIZE(3) + 80);
JsonObject& root = jsonBuffer.parseObject(data);
if (!root.success())
return false;
uint8_t jsonCount = root["count"];
bool jsonUseCurve = root["useCurve"];
if (jsonCount != count() ||
jsonUseCurve != useCurve())
{
count(jsonCount);
useCurve(jsonUseCurve);
if (changed != nullptr)
*changed = true;
}
JsonArray& jsonRanges = root["ranges"];
uint8_t stepCount = jsonRanges.size();
if (stepCount >= MaxStepCount)
stepCount = MaxStepCount - 1;
for (uint8_t step = 0; step < stepCount; step++)
{
JsonObject& jsonRange = jsonRanges[step];
uint16_t start = jsonRange["start"];
uint16_t end = jsonRange["end"];
if (start != rangeStart(step) || end != rangeEnd(step))
{
rangeStart(step, start);
rangeEnd(step, end);
if (changed != nullptr)
*changed = true;
}
}
return true;
}

57
src/settings/steps.h Normal file
View File

@ -0,0 +1,57 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __settingssteps
#define __settingssteps
#include <stdint.h>
#include "./abstractjson.h"
#include "../config.h"
#define MaxStepCount 16
struct StepRange
{
uint16_t start;
uint16_t end;
};
class StepsSettings : public AbstractJsonSettings
{
private:
uint8_t mCount = MaxStepCount;
bool mUseCurve = true;
StepRange mRange[MaxStepCount];
protected:
virtual const char* getFilename() { return StepsSettingsFile; };
virtual const char* getDebugPrefix() { return "StepsSettings"; };
public:
StepsSettings();
void toJson(Print &print);
bool fromJson(char* data, bool* changed);
uint8_t count() { return mCount; }
void count(uint8_t value) { mCount = value; }
bool useCurve() { return mUseCurve; }
void useCurve(bool value) { mUseCurve = value; }
uint16_t rangeStart(uint8_t step) { return step < MaxStepCount ? mRange[step].start : 0; }
uint16_t rangeStart(uint8_t step, uint16_t value) { if (step < MaxStepCount) mRange[step].start = value; }
uint16_t rangeEnd(uint8_t step) { return step < MaxStepCount ? mRange[step].end : 0; }
uint16_t rangeEnd(uint8_t step, uint16_t value) { if (step < MaxStepCount) mRange[step].end = value; }
};
#endif

115
src/settings/system.cpp Normal file
View File

@ -0,0 +1,115 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./system.h"
#include <ArduinoJson.h>
#include <FS.h>
#include "../debug.h"
#include "../global.h"
#include "../config.h"
void SystemSettings::toJson(Print &print)
{
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(8) + JSON_OBJECT_SIZE(5));
JsonObject& root = jsonBuffer.createObject();
root["ntpServer"] = ntpServer();
root["ntpInterval"] = ntpInterval();
root["lat"] = latitude();
root["lng"] = longitude();
JsonObject& pins = root.createNestedObject("pins");
pins["ledAP"] = pinLEDAP();
pins["ledSTA"] = pinLEDSTA();
pins["apButton"] = pinAPButton();
pins["pwmSDA"] = pinPWMDriverSDA();
pins["pwmSCL"] = pinPWMDriverSCL();
root["pwmAddress"] = pwmDriverAddress();
root["pwmFrequency"] = pwmDriverFrequency();
root["mapsAPIKey"] = mapsAPIKey();
root.printTo(print);
}
bool SystemSettings::fromJson(char* data, bool* changed)
{
if (changed != nullptr)
*changed = false;
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(8) + JSON_OBJECT_SIZE(5) + 500);
JsonObject& root = jsonBuffer.parseObject(data);
if (!root.success())
return false;
const char* jsonNTPServer = root["ntpServer"];
uint32_t jsonNTPInterval = root["ntpInterval"];
double jsonLat = root["lat"];
double jsonLng = root["lng"];
JsonObject& pins = root["pins"];
uint8_t jsonPinLEDAP = pins["ledAP"];
uint8_t jsonPinLEDSTA = pins["ledSTA"];
uint8_t jsonPinAPButton = pins["apButton"];
uint8_t jsonPinPWMDriverSDA = pins["pwmSDA"];
uint8_t jsonPinPWMDriverSCL = pins["pwmSCL"];
uint8_t jsonPWMDriverAddress = root["pwmAddress"];
uint16_t jsonPWMDriverFrequency = root["pwmFrequency"];
const char* jsonMapAPIKey = root["mapsAPIKey"];
if (jsonNTPServer == nullptr) jsonNTPServer = DefaultNTPServer;
if (jsonNTPInterval == 0) jsonNTPInterval = 5;
if (jsonPinLEDAP == 0) jsonPinLEDAP = pinLEDAP();
if (jsonPinLEDSTA == 0) jsonPinLEDSTA = pinLEDSTA();
if (jsonPinAPButton == 0) jsonPinAPButton = pinAPButton();
if (jsonPinPWMDriverSDA == 0) jsonPinPWMDriverSDA = pinPWMDriverSDA();
if (jsonPinPWMDriverSCL == 0) jsonPinPWMDriverSCL = pinPWMDriverSCL();
if (jsonPWMDriverAddress == 0) jsonPWMDriverAddress = pwmDriverAddress();
if (jsonPWMDriverFrequency == 0) jsonPWMDriverFrequency = pwmDriverFrequency();
if ((jsonPinLEDAP != pinLEDAP()) ||
(jsonPinLEDSTA != pinLEDSTA()) ||
(jsonPinAPButton != pinAPButton()) ||
(jsonPinPWMDriverSDA != pinPWMDriverSDA()) ||
(jsonPinPWMDriverSCL != pinPWMDriverSCL()) ||
(jsonPWMDriverAddress != pwmDriverAddress()) ||
(jsonPWMDriverFrequency != pwmDriverFrequency()) ||
(!sameStr(jsonMapAPIKey, mapsAPIKey())) ||
(jsonLat != latitude()) ||
(jsonLng != longitude()) ||
(!sameStr(jsonNTPServer, ntpServer())) ||
(jsonNTPInterval != ntpInterval()))
{
latitude(jsonLat);
longitude(jsonLng);
pinLEDAP(jsonPinLEDAP);
pinLEDSTA(jsonPinLEDSTA);
pinAPButton(jsonPinAPButton);
pinPWMDriverSDA(jsonPinPWMDriverSDA);
pinPWMDriverSCL(jsonPinPWMDriverSCL);
pwmDriverAddress(jsonPWMDriverAddress);
pwmDriverFrequency(jsonPWMDriverFrequency);
mapsAPIKey(jsonMapAPIKey);
ntpServer(jsonNTPServer);
ntpInterval(jsonNTPInterval);
if (changed != nullptr)
*changed = true;
}
return true;
}

90
src/settings/system.h Normal file
View File

@ -0,0 +1,90 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __settingssystem
#define __settingssystem
#include <Arduino.h>
#include <stdint.h>
#include <stdbool.h>
#include "../charproperties.h"
#include "./abstractjson.h"
class SystemSettings : public AbstractJsonSettings
{
private:
char* mNTPServer;
uint32_t mNTPInterval = 5;
double mLatitude = 0;
double mLongitude = 0;
uint8_t mPinLEDAP = 4;
uint8_t mPinLEDSTA = 5;
uint8_t mPinAPButton = 2;
uint8_t mPinPWMDriverSDA = 13;
uint8_t mPinPWMDriverSCL = 12;
uint8_t mPWMDriverAddress = 0x40;
uint16_t mPWMDriverFrequency = 1600;
char* mMapsAPIKey = nullptr;
protected:
virtual const char* getFilename() { return SystemSettingsFile; };
virtual const char* getDebugPrefix() { return "SystemSettings"; };
public:
SystemSettings()
{
assignChar(&mNTPServer, DefaultNTPServer);
}
void toJson(Print &print);
bool fromJson(char* data, bool* changed);
char* ntpServer() { return mNTPServer; }
void ntpServer(const char* value) { assignChar(&mNTPServer, value); }
double ntpInterval() { return mNTPInterval; }
void ntpInterval(double value) { mNTPInterval = value; }
double latitude() { return mLatitude; }
void latitude(double value) { mLatitude = value; }
double longitude() { return mLongitude; }
void longitude(double value) { mLongitude = value; }
uint8_t pinLEDAP() { return mPinLEDAP; }
void pinLEDAP(uint8_t value) { mPinLEDAP = value; }
uint8_t pinLEDSTA() { return mPinLEDSTA; }
void pinLEDSTA(uint8_t value) { mPinLEDSTA = value; }
uint8_t pinAPButton() { return mPinAPButton; }
void pinAPButton(uint8_t value) { mPinAPButton = value; }
uint8_t pinPWMDriverSDA() { return mPinPWMDriverSDA; }
void pinPWMDriverSDA(uint8_t value) { mPinPWMDriverSDA = value; }
uint8_t pinPWMDriverSCL() { return mPinPWMDriverSCL; }
void pinPWMDriverSCL(uint8_t value) { mPinPWMDriverSCL = value; }
uint8_t pwmDriverAddress() { return mPWMDriverAddress; }
void pwmDriverAddress(uint8_t value) { mPWMDriverAddress = value; }
uint16_t pwmDriverFrequency() { return mPWMDriverFrequency; }
void pwmDriverFrequency(uint16_t value) { mPWMDriverFrequency = value; }
char* mapsAPIKey() { return mMapsAPIKey; }
void mapsAPIKey(const char* value) { assignChar(&mMapsAPIKey, value); }
};
#endif

View File

@ -0,0 +1,86 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./motion.h"
#include <string.h>
#include <ArduinoJson.h>
#include "../../debug.h"
#include "../../global.h"
void MotionTriggerSettings::toJson(Print &print)
{
DynamicJsonBuffer jsonBuffer(JSON_ARRAY_SIZE(2) + 2*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5));
JsonObject& root = jsonBuffer.createObject();
root["enabled"] = enabled();
root["enabledDuringTimeTrigger"] = enabledDuringTimeTrigger();
root["enabledDuringDay"] = enabledDuringDay();
root["transitionTime"] = transitionTime();
root["delay"] = delay();
JsonArray& jsonTriggers = root.createNestedArray("triggers");
for (uint8_t i = 0; i < triggerCount(); i++)
{
MotionTrigger* triggerItem = trigger(i);
JsonObject& jsonTrigger = jsonTriggers.createNestedObject();
jsonTrigger["pin"] = triggerItem->pin;
jsonTrigger["brightness"] = triggerItem->brightness;
jsonTrigger["direction"] = (uint8_t)triggerItem->direction;
jsonTrigger["enabled"] = triggerItem->enabled;
}
root.printTo(print);
}
bool MotionTriggerSettings::fromJson(char* data, bool* changed)
{
if (changed != nullptr)
*changed = false;
DynamicJsonBuffer jsonBuffer(JSON_ARRAY_SIZE(2) + 2*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + 200);
JsonObject& root = jsonBuffer.parseObject(data);
if (!root.success())
return false;
enabled(root["enabled"]);
enabledDuringTimeTrigger(root["enabledDuringTimeTrigger"]);
enabledDuringDay(root["enabledDuringDay"]);
transitionTime(root["transitionTime"]);
delay(root["delay"]);
if (root.containsKey("triggers"))
{
JsonArray& jsonTriggers = root["triggers"];
if (mTriggers != nullptr)
delete [] mTriggers;
mTriggerCount = jsonTriggers.size();
mTriggers = new MotionTrigger[mTriggerCount];
for (uint8_t i = 0; i < mTriggerCount; i++)
{
JsonObject& jsonTrigger = jsonTriggers[i];
MotionTrigger* trigger = &mTriggers[i];
trigger->pin = jsonTrigger["pin"];
trigger->brightness = jsonTrigger["brightness"];
trigger->direction = (MotionDirection)(uint8_t)jsonTrigger["direction"];
trigger->enabled = jsonTrigger["enabled"];
}
}
if (changed != nullptr)
*changed = true;
return true;
}

View File

@ -0,0 +1,71 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __settingstriggersmotion
#define __settingstriggersmotion
#include <Arduino.h>
#include "../../config.h"
#include "../abstractjson.h"
enum MotionDirection
{
Nondirectional = 1,
TopDown = 2,
BottomUp = 3
};
struct MotionTrigger
{
uint8_t pin;
uint8_t brightness;
MotionDirection direction;
bool enabled;
};
class MotionTriggerSettings : public AbstractJsonSettings
{
private:
bool mEnabled = false;
bool mEnabledDuringTimeTrigger = false;
bool mEnabledDuringDay = false;
uint16_t mTransitionTime = 500;
uint32_t mDelay = 30000;
uint8_t mTriggerCount = 0;
MotionTrigger* mTriggers = nullptr;
protected:
virtual const char* getFilename() { return MotionTriggerSettingsFile; };
virtual const char* getDebugPrefix() { return "MotionTriggerSettings"; };
public:
void toJson(Print &print);
bool fromJson(char* data, bool* changed);
bool enabled() { return mEnabled; }
void enabled(bool value) { mEnabled = value; }
bool enabledDuringTimeTrigger() { return mEnabledDuringTimeTrigger; }
void enabledDuringTimeTrigger(bool value) { mEnabledDuringTimeTrigger = value; }
bool enabledDuringDay() { return mEnabledDuringDay; }
void enabledDuringDay(bool value) { mEnabledDuringDay = value; }
uint16_t transitionTime() { return mTransitionTime; }
void transitionTime(uint16_t value) { mTransitionTime = value; }
uint32_t delay() { return mDelay; }
void delay(uint32_t value) { mDelay = value; }
uint8_t triggerCount() { return mTriggerCount; }
MotionTrigger* trigger(uint8_t index) { return &mTriggers[index]; }
};
#endif

View File

@ -0,0 +1,196 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include "./time.h"
#include <string.h>
#include <ArduinoJson.h>
#include "../../debug.h"
#include "../../global.h"
timeDayOfWeek_t toTimeDayOfWeek(DayOfWeek day)
{
switch (day)
{
case Monday: return dowMonday;
case Tuesday: return dowTuesday;
case Wednesday: return dowWednesday;
case Thursday: return dowThursday;
case Friday: return dowFriday;
case Saturday: return dowSaturday;
case Sunday: return dowSunday;
}
return dowInvalid;
}
DayOfWeek toDayOfWeek(timeDayOfWeek_t timeDay)
{
switch (timeDay)
{
case dowSunday: return Sunday;
case dowMonday: return Monday;
case dowTuesday: return Tuesday;
case dowWednesday: return Wednesday;
case dowThursday: return Thursday;
case dowFriday: return Friday;
case dowSaturday: return Saturday;
}
return Monday;
}
void TimeTriggerSettings::toJson(Print &print)
{
DynamicJsonBuffer jsonBuffer(JSON_ARRAY_SIZE(10) + JSON_OBJECT_SIZE(3) + 10*JSON_OBJECT_SIZE(5));
JsonObject& root = jsonBuffer.createObject();
root["enabled"] = enabled();
root["transitionTime"] = transitionTime();
JsonArray& jsonTriggers = root.createNestedArray("triggers");
for (uint8_t i = 0; i < triggerCount(); i++)
{
TimeTrigger* triggerItem = trigger(i);
JsonObject& jsonTrigger = jsonTriggers.createNestedObject();
jsonTrigger["time"] = triggerItem->time;
jsonTrigger["daysOfWeek"] = triggerItem->daysOfWeek;
jsonTrigger["brightness"] = triggerItem->brightness;
jsonTrigger["triggerType"] = (uint8_t)triggerItem->triggerType;
jsonTrigger["enabled"] = triggerItem->enabled;
}
root.printTo(print);
}
bool TimeTriggerSettings::fromJson(char* data, bool* changed)
{
if (changed != nullptr)
*changed = false;
DynamicJsonBuffer jsonBuffer(JSON_ARRAY_SIZE(10) + JSON_OBJECT_SIZE(3) + 10*JSON_OBJECT_SIZE(5) + 270);
JsonObject& root = jsonBuffer.parseObject(data);
if (!root.success())
return false;
enabled(root["enabled"]);
transitionTime(root["transitionTime"]);
if (root.containsKey("triggers"))
{
JsonArray& jsonTriggers = root["triggers"];
if (mTriggers != nullptr)
delete [] mTriggers;
mTriggerCount = jsonTriggers.size();
mTriggers = new TimeTrigger[mTriggerCount];
for (uint8_t i = 0; i < mTriggerCount; i++)
{
JsonObject& jsonTrigger = jsonTriggers[i];
TimeTrigger* trigger = &mTriggers[i];
trigger->time = jsonTrigger["time"];
trigger->daysOfWeek = jsonTrigger["daysOfWeek"];
trigger->brightness = jsonTrigger["brightness"];
trigger->triggerType = (TimeTriggerType)(uint8_t)jsonTrigger["triggerType"];
trigger->enabled = jsonTrigger["enabled"];
}
}
if (changed != nullptr)
*changed = true;
return true;
}
TimeTrigger* TimeTriggerSettings::getActiveTrigger(tmElements_t &time)
{
if (mTriggerCount == 0)
return nullptr;
DayOfWeek dayOfWeek = toDayOfWeek((timeDayOfWeek_t)time.Wday);
DayOfWeek startDayOfWeek = dayOfWeek;
TimeTrigger* activeTrigger = nullptr;
int16_t activeTriggerTime = 0;
Dusk2Dawn location(systemSettings->latitude(), systemSettings->longitude(), timezoneOffset / 3600.0f);
// Praise the sun \o/
// DST is always hardcoded as false, since it is already included in timezoneOffset
int16_t sunriseMinutes = location.sunrise(time.Year, time.Month, time.Day, false);
int16_t sunsetMinutes = location.sunset(time.Year, time.Month, time.Day, false);
int16_t dayTime = (time.Hour * 60) + time.Minute;
_d("TimeTrigger :: sunrise: "); _dln(sunriseMinutes);
_d("TimeTrigger :: sunset: "); _dln(sunsetMinutes);
_d("TimeTrigger :: current time: "); _dln(dayTime);
do
{
for (uint16_t i = 0; i < mTriggerCount; i++)
{
TimeTrigger* trigger = &mTriggers[i];
if (trigger->enabled && (trigger->daysOfWeek & dayOfWeek))
{
int16_t triggerTime = trigger->time;
switch (trigger->triggerType)
{
case RelativeToSunrise:
triggerTime += sunriseMinutes;
break;
case RelativeToSunset:
triggerTime += sunsetMinutes;
break;
}
// Check if the current time is after the time set in the trigger, and
// if this trigger is later than any previously found trigger, so that
// we'll always get the most recent match
if (triggerTime <= dayTime && (activeTrigger == nullptr || triggerTime > activeTriggerTime))
{
activeTrigger = trigger;
activeTriggerTime = triggerTime;
}
}
}
if (activeTrigger != nullptr)
return activeTrigger;
// If there are no active triggers on this day, go back
// one weekday and try again until we've come around completely
if (dayOfWeek == Monday)
dayOfWeek = Sunday;
else
dayOfWeek = (DayOfWeek)((uint8_t)dayOfWeek / 2);
// Set the comparison time to the end of the day, so the last
// trigger for that day will match
dayTime = 24 * 60;
} while (dayOfWeek != startDayOfWeek);
return nullptr;
}

View File

@ -0,0 +1,80 @@
/*
* Stairs
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#ifndef __settingstriggerstime
#define __settingstriggerstime
#include <Arduino.h>
#include <TimeLib.h>
#include <Dusk2Dawn.h>
#include "../../config.h"
#include "../abstractjson.h"
enum DayOfWeek
{
Monday = 1,
Tuesday = 2,
Wednesday = 4,
Thursday = 8,
Friday = 16,
Saturday = 32,
Sunday = 64
};
extern timeDayOfWeek_t toTimeDayOfWeek(DayOfWeek day);
extern DayOfWeek toDayOfWeek(timeDayOfWeek_t timeDay);
enum TimeTriggerType
{
FixedTime = 0,
RelativeToSunrise = 1,
RelativeToSunset = 2
};
struct TimeTrigger
{
int16_t time;
uint8_t daysOfWeek;
uint8_t brightness;
TimeTriggerType triggerType;
bool enabled;
};
class TimeTriggerSettings : public AbstractJsonSettings
{
private:
bool mEnabled = false;
uint16_t mTransitionTime = 0;
uint8_t mTriggerCount = 0;
TimeTrigger* mTriggers = nullptr;
protected:
virtual const char* getFilename() { return TimeTriggerSettingsFile; };
virtual const char* getDebugPrefix() { return "TimeTriggerSettings"; };
public:
void toJson(Print &print);
bool fromJson(char* data, bool* changed);
TimeTrigger* getActiveTrigger(tmElements_t &time);
bool enabled() { return mEnabled; }
void enabled(bool value) { mEnabled = value; }
uint16_t transitionTime() { return mTransitionTime; }
void transitionTime(uint16_t value) { mTransitionTime = value; }
uint8_t triggerCount() { return mTriggerCount; }
TimeTrigger* trigger(uint8_t index) { return &mTriggers[index]; }
};
#endif

View File

@ -1,149 +1,221 @@
#include "./stairs.h"
#include <Math.h>
#include <FS.h>
#include "stairs.h"
#include "./debug.h"
#include "./global.h"
const static float factorBase = log10(2) / log10(PCA9685::On);
struct Header
{
uint8_t version;
uint8_t rangeCount;
bool useScaling;
};
const static float CurveFactor = log10(2) / log10(4095);
const static float LinearFactor = 4095.0f / 255.0f;
void Stairs::init(PCA9685* pwmDriver)
{
this->useScaling = false;
mPWMDriver = pwmDriver;
for (uint8_t i = 0; i < StepCount; i++)
memset(&mStep[0], 0, sizeof(mStep));
}
uint8_t Stairs::ease(uint8_t startValue, uint8_t targetValue, uint16_t transitionTime, uint16_t elapsedTime)
{
bool up = targetValue > startValue;
uint16_t diff = up ? targetValue - startValue : startValue - targetValue;
uint16_t delta = (diff * elapsedTime) / transitionTime;
int16_t currentValue = up ? startValue + delta : startValue - delta;
if (currentValue < 0) currentValue = 0;
if (currentValue > 255) currentValue = 255;
return currentValue;
}
inline void Stairs::updateCurrentValue(Step* stepState)
{
int32_t stepElapsedTime = -stepState->startTime;
stepState->currentValue = ease(stepState->startValue, stepState->targetValue, stepState->remainingTime + stepElapsedTime, stepElapsedTime);
}
inline void Stairs::applyCurrentValue(uint8_t step)
{
mPWMDriver->setPWM(step, this->getPWMValue(step, mStep[step].currentValue));
}
void Stairs::tick()
{
if (stepsSettingsChanged)
{
this->ranges[i].start = IStairs::Off;
this->ranges[i].end = IStairs::On;
// Re-apply all values in case the PWM value changed
for (uint8_t step = 0; step < stepsSettings->count(); step++)
applyCurrentValue(step);
stepsSettingsChanged = false;
}
this->pwmDriver = pwmDriver;
_dln("Loading range configuration");
SPIFFS.begin();
this->readRange();
}
if (!mTick) return;
uint32_t elapsedTime = mLastTransitionTime != 0 ? currentTime - mLastTransitionTime : 0;
if (!elapsedTime) return;
uint8_t Stairs::getCount()
{
return StepCount;
}
mLastTransitionTime = currentTime;
mTick = false;
void Stairs::set(uint8_t step, uint16_t brightness)
{
pwmDriver->setPWM(step, this->getPWMValue(step, brightness));
}
void Stairs::setAll(uint16_t brightness)
{
//pwmDriver->setAll(this->getPWMValue(brightness));
for (uint8_t step = 0; step < StepCount; step++)
pwmDriver->setPWM(step, this->getPWMValue(step, brightness));
}
uint16_t Stairs::getPWMValue(uint8_t step, uint16_t brightness)
{
_d("Getting PWM value for step "); _d(step); _d(", brightness "); _dln(brightness);
if (brightness == IStairs::Off || brightness == IStairs::On)
for (uint8_t step = 0; step < stepsSettings->count(); step++)
{
_dln("Full on/off, returning input");
return brightness;
Step* stepState = &mStep[step];
if (stepState->currentValue != stepState->targetValue)
{
// If there is a startup delay request, wait for it first
if (stepState->startTime > 0)
{
stepState->startTime -= elapsedTime;
if (stepState->startTime < 0)
{
if (stepState->remainingTime > -stepState->startTime)
{
// Shift the remaining time equally
stepState->remainingTime += stepState->startTime;
updateCurrentValue(stepState);
mTick = true;
}
else
{
// End of the transition
stepState->remainingTime = 0;
stepState->currentValue = stepState->targetValue;
}
applyCurrentValue(step);
}
else
mTick = true;
}
else if (elapsedTime >= stepState->remainingTime)
{
// End of the transition
stepState->remainingTime = 0;
stepState->currentValue = stepState->targetValue;
applyCurrentValue(step);
}
else
{
stepState->startTime -= elapsedTime;
stepState->remainingTime -= elapsedTime;
updateCurrentValue(stepState);
applyCurrentValue(step);
mTick = true;
}
}
}
if (step < 0 || step >= StepCount)
{
_dln("Step out of bounds, returning input");
return brightness;
}
if (!mTick)
mLastTransitionTime = 0;
}
Range* range = &this->ranges[step];
_d("Start: "); _dln(range->start);
_d("End: "); _dln(range->end);
if (this->useScaling)
uint8_t Stairs::get(uint8_t step, bool target)
{
if (step >= MaxStepCount) return 0;
return target ? mStep[step].targetValue : mStep[step].currentValue;
}
void Stairs::set(uint8_t step, uint8_t brightness, uint16_t transitionTime, uint16_t startTime)
{
_d("Stairs :: set step = "); _d(step);
_d(", brightness = "); _d(brightness);
_d(", transitionTime = "); _d(transitionTime);
_d(", startTime = "); _dln(startTime);
if (step >= MaxStepCount) return;
if (mStep[step].currentValue == brightness)
return;
mStep[step].targetValue = brightness;
if (transitionTime > 0)
{
_dln("Using scaling");
float factor = ((range->end - range->start) + 1) * factorBase;
brightness = pow(2, (brightness / factor)) - 1 + range->start;
mStep[step].startValue = mStep[step].currentValue;
mStep[step].startTime = startTime;
mStep[step].remainingTime = transitionTime;
if (!mLastTransitionTime)
mLastTransitionTime = currentTime;
mTick = true;
}
else
{
_dln("Not using scaling");
if (brightness < range->start) brightness = range->start;
if (brightness > range->end) brightness = range->end;
mStep[step].currentValue = brightness;
applyCurrentValue(step);
}
}
void Stairs::setAll(uint8_t brightness, uint16_t transitionTime, uint16_t startTime)
{
for (uint8_t step = 0; step < stepsSettings->count(); step++)
set(step, brightness, transitionTime, startTime);
}
void Stairs::sweep(uint8_t brightness, uint16_t transitionTime, bool topDown)
{
uint8_t stepsCount = stepsSettings->count();
uint16_t offsetIncrement = stepsCount > 0 ? (transitionTime / stepsCount) * 1.5 : 0;
uint16_t offset = topDown ? 0 : (stepsCount - 1) * offsetIncrement;
for (uint8_t step = 0; step < stepsCount; step++)
{
set(step, brightness, transitionTime, offset);
if (topDown)
offset += offsetIncrement;
else
offset -= offsetIncrement;
}
}
uint16_t Stairs::getPWMValue(uint8_t step, uint8_t brightness)
{
//_d("Stairs :: Getting PWM value for step "); _d(step); _d(", brightness "); _dln(brightness);
if (brightness == 0 || brightness == 255)
{
//_dln("Stairs :: Full on/off, returning input");
return brightness == 0 ? 0 : 4095;
}
_d("Output: "); _dln(brightness);
return brightness;
}
uint16_t pwmValue;
uint16_t rangeStart = stepsSettings->rangeStart(step);
uint16_t rangeEnd = stepsSettings->rangeEnd(step);
if (stepsSettings->useCurve())
{
//_dln("Stairs :: Using curve");
float factor = ((rangeEnd - rangeStart) + 1) * CurveFactor;
pwmValue = pow(2, ((brightness * LinearFactor) / factor)) - 1 + rangeStart;
}
else
{
//_dln("Stairs :: Not using curve");
float factor = ((rangeEnd - rangeStart) + 1) / 255.0f;
pwmValue = (brightness * factor) + rangeStart;
}
void Stairs::getRange(Stream* stream)
{
stream->write(this->useScaling ? 1 : 0);
stream->write((uint8_t*)&this->ranges, sizeof(this->ranges));
}
void Stairs::setRange(uint8_t* data)
{
this->useScaling = *data;
data++;
memcpy(this->ranges, data, sizeof(this->ranges));
this->writeRange();
}
void Stairs::readRange()
{
File f = SPIFFS.open("/range", "r");
if (!f)
return;
if (!f.available())
return;
Header header;
f.readBytes((char*)&header, sizeof(Header));
if (header.version != 1)
return;
this->useScaling = (header.useScaling == 1);
f.readBytes((char*)&this->ranges, header.rangeCount * sizeof(Range));
f.close();
_d("- useScaling: ");
_dln(this->useScaling);
}
void Stairs::writeRange()
{
File f = SPIFFS.open("/range", "w");
if (!f)
return;
Header header;
header.version = 1;
header.useScaling = this->useScaling;
header.rangeCount = StepCount;
f.write((uint8_t*)&header, sizeof(Header));
f.write((uint8_t*)&this->ranges, sizeof(this->ranges));
f.close();
//_d("Stairs :: Output: "); _dln(pwmValue);
return pwmValue;
}

View File

@ -1,41 +1,45 @@
#ifndef __Stairs
#define __Stairs
#include "components/PCA9685.h"
#include "modes/base.h"
#include "config.h"
#include "./components/PCA9685.h"
#include "./config.h"
#include "./settings/steps.h"
struct Range
struct Step
{
uint16_t start;
uint16_t end;
uint8_t currentValue;
uint8_t startValue;
uint8_t targetValue;
int16_t startTime;
uint16_t remainingTime;
};
class Stairs : public IStairs
class Stairs
{
private:
PCA9685* pwmDriver;
PCA9685* mPWMDriver;
Step mStep[MaxStepCount];
bool useScaling;
Range ranges[StepCount];
uint32_t mLastTransitionTime;
bool mTick = false;
protected:
void readRange();
void writeRange();
uint16_t getPWMValue(uint8_t step, uint16_t brightness);
uint8_t ease(uint8_t startValue, uint8_t targetValue, uint16_t transitionTime, uint16_t elapsedTime);
inline void updateCurrentValue(Step* stepState);
inline void applyCurrentValue(uint8_t step);
uint16_t getPWMValue(uint8_t step, uint8_t brightness);
public:
void init(PCA9685* pwmDriver);
void tick();
uint8_t getCount();
void set(uint8_t step, uint16_t brightness);
void setAll(uint16_t brightness);
bool inTransition() { return mTick; }
void getRange(Stream* stream);
void setRange(uint8_t* data);
uint8_t get(uint8_t step, bool target = true);
void set(uint8_t step, uint8_t brightness, uint16_t transitionTime = 0, uint16_t startTime = 0);
void setAll(uint8_t brightness, uint16_t transitionTime = 0, uint16_t startTime = 0);
void sweep(uint8_t brightness, uint16_t transitionTime, bool topDown);
};
#endif

View File

@ -1,23 +0,0 @@
$output = & GitVersion /output json /nofetch
if ($LASTEXITCODE -ne 0) {
Write-Verbose "$output"
throw "GitVersion failed with exit code: $LASTEXITCODE"
}
$version = $output | ConvertFrom-Json
@"
#ifndef __Version
#define __Version
static const char* FirmwareVersion = "{0}";
#endif
"@ -f $version.FullSemVer | Out-File -Encoding UTF8 .\src\version.h
@"
module.exports =
{{
Version: "{0}"
}};
"@ -f $version.FullSemVer | Out-File -Encoding UTF8 .\web\version.js

View File

@ -1,2 +0,0 @@
& .\updateversion.ps1
& platformio run --target upload

View File

@ -1,3 +0,0 @@
{
"directory": "static/bower_components"
}

1186
web/app.js

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
{
"name": "stairs",
"description": "Stairs lighting project",
"main": "index.html",
"authors": [
"Mark van Renswoude"
],
"license": "ISC",
"homepage": "",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"src/bower_components"
],
"dependencies": {
"knockout": "^3.4.2",
"jquery": "^3.2.1",
"crossroads": "^0.12.2",
"hasher": "^1.2.0",
"requirejs": "^2.3.3",
"text": "requirejs/text#^2.0.15",
"bootstrap": "v4.0.0-alpha.6",
"nprogress": "^0.2.0"
}
}

View File

@ -1,342 +0,0 @@
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, withTimeout)
{
if (buffer === null || buffer.length == 0) return;
console.log('> ' + buffer.toString('hex'));
var command = buffer.readInt8(0);
var cancelled = false;
if (typeof(withTimeout) == 'undefined') withTimeout = true;
if (withTimeout)
{
var timeout = setTimeout(function()
{
cancelled = true;
callback(null, true);
clearTimeout(timeout);
}, 2000);
}
registerResponseHandler(command, function(reader, error)
{
if (cancelled) return;
if (withTimeout) 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(),
easeTime: 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 readRangeData(reader)
{
var data = { useScaling: reader.nextInt8() == 1, values: [] };
while (reader.tell() < reader.buf.length)
data.values.push({ start: reader.nextInt16LE(), end: reader.nextInt16LE() });
return data;
}
function lsb(value) { return value & 0xFF; }
function msb(value) { return (value >> 8) & 0xFF; }
function getBrightness(value)
{
if (typeof(value) == 'string' && value.substr(-1) === '%')
return (Number(value.substr(0, value.length - 1)) * 4096 / 100);
return Number(value) || 0;
}
function writeModeData(mode, data)
{
switch (mode)
{
case protocol.Mode.Static:
var brightness = getBrightness(data.brightness);
return new Buffer([protocol.Command.SetMode, mode, lsb(brightness), msb(brightness), lsb(500), msb(500)]);
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.writeUInt8(protocol.Command.SetMode, 0);
buffer.writeUInt8(mode, 1);
for (var index = 0; index < valueCount; index++)
buffer.writeUInt16LE(getBrightness(brightness[index]), 2 + (index * 2));
return buffer;
case protocol.Mode.Alternate:
var brightness = getBrightness(data.brightness);
if (typeof(data.interval) == 'undefined') data.interval = 500;
return new Buffer([protocol.Command.SetMode, mode,
lsb(data.interval), msb(data.interval),
lsb(brightness), msb(brightness)]);
case protocol.Mode.Slide:
var brightness = getBrightness(data.brightness);
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(brightness), msb(brightness),
data.direction,
lsb(data.fadeOutTime), msb(data.fadeOutTime)]);
}
}
function writeRangeData(data)
{
var start = typeof(data.start) !== 'undefined' ? data.start.split(',') : [];
var end = typeof(data.end) !== 'undefined' ? data.end.split(',') : [];
var valueCount = Math.min(16, start.length, end.length);
var buffer = Buffer.alloc(2 + (valueCount * 4));
buffer.writeUInt8(protocol.Command.SetRange, 0);
buffer.writeUInt8(data.useScaling ? 1 : 0, 1);
for (var index = 0; index < valueCount; index++)
{
buffer.writeUInt16LE(getBrightness(start[index]), 2 + (index * 4));
buffer.writeUInt16LE(getBrightness(end[index]), 4 + (index * 4));
}
return buffer;
}
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);
});
},
getRange: function(callback)
{
requestResponse(new Buffer([protocol.Command.GetRange]),
function(reader, error)
{
if (!error)
{
callback(readRangeData(reader), false);
}
else
callback(null, true);
});
},
setRange: function(data, callback)
{
requestResponse(writeRangeData(data),
function(reader, error)
{
if (!error)
{
callback(readRangeData(reader), false);
}
else
callback(null, true);
});
},
updateFirmware: function(data, callback)
{
if (typeof(data.host) == 'undefined') data.host = '';
if (typeof(data.port) == 'undefined') data.port = 80;
if (typeof(data.path) == 'undefined') data.path = '';
var buffer = Buffer.alloc(1 + (data.host.length + 1) + 2 + (data.path.length + 1));
buffer.writeUInt8(protocol.Command.UpdateFirmware, 0);
var position = 1;
buffer.writeUInt16LE(data.port, position);
position += 2;
buffer.write(data.host, position);
position += data.host.length;
buffer.writeUInt8(0, position);
position++;
buffer.write(data.path, position);
position += data.path.length;
buffer.writeUInt8(0, position);
requestResponse(buffer,
function(reader, error)
{
if (!error)
{
var data = { hasUpdates: reader.nextInt8() == 1 };
callback(data, false);
}
else
callback(null, true);
}, false);
}
}

1
web/dist/bundle.css vendored Normal file

File diff suppressed because one or more lines are too long

1
web/dist/bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,96 +0,0 @@
'use strict';
var gulp = require('gulp');
var ts = require('gulp-typescript');
var uglify = require('gulp-uglify');
var sass = require('gulp-sass');
var cleanCSS = require('gulp-clean-css');
var concat = require('gulp-concat');
var watch = require('gulp-debounced-watch');
var plumber = require('gulp-plumber');
var config =
{
dest: 'static/assets/dist/',
typescriptBase: 'static/assets/ts/',
sassBase: 'static/assets/sass/',
assets: ['static/assets/ts/**/*.html', 'static/assets/js/**/*.js']
};
config.typescriptSrc = config.typescriptBase + '**/*.ts';
config.sassSrc = config.sassBase + '**/*.scss';
gulp.task('default',
[
'compileTypescript',
'compileSass',
'copyAssets'
],
function(){});
gulp.task('watch',
[
'compileTypescript',
'compileSass',
'copyAssets'
],
function()
{
watch(config.typescriptSrc, function() { gulp.start('compileTypescript'); });
watch(config.sassSrc, function() { gulp.start('compileSass'); });
watch(config.assets, function() { gulp.start('copyAssets'); });
});
gulp.task('compileTypescript', function()
{
return gulp.src(config.typescriptSrc, { base: config.typescriptBase })
.pipe(plumber({
errorHandler: function (error)
{
console.log(error.message);
this.emit('end');
}}))
.pipe(ts(
{
noImplicitAny: true,
removeComments: true,
preserveConstEnums: true,
sourceMap: true,
module: 'amd'
}))
.pipe(uglify())
.pipe(gulp.dest(config.dest));
});
gulp.task('compileSass', function()
{
return gulp.src(config.sassSrc)
.pipe(plumber({
errorHandler: function (error)
{
console.log(error.message);
this.emit('end');
}}))
.pipe(sass())
.pipe(concat('bundle.css'))
.pipe(cleanCSS())
.pipe(gulp.dest(config.dest));
});
gulp.task('copyAssets', function()
{
return gulp.src(config.assets)
.pipe(plumber({
errorHandler: function (error)
{
console.log(error.message);
this.emit('end');
}}))
.pipe(gulp.dest(config.dest));
});

444
web/index.html Normal file
View File

@ -0,0 +1,444 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<meta name="theme-color" content="#000000">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="bundle.css">
<script src="bundle.js"></script>
</head>
<body>
<div id="app">
<div v-cloak>
<div class="notificationContainer">
<div class="notification" :class="{ error: notification != null && notification.error }" v-if="notification != null" @click.prevent="hideNotification">
<span class="message">{{ notification.message }}</span>
</div>
</div>
<div id="container">
<div class="header">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAXBJREFUeNrsWdENgjAQLcYBcAPcACdQN4AN2MC4gRsYJ9ENdAPYADaADepdUhM1qFjurgp9ScMHgd713V3ftUp5eHgMF1rrGEau6VHD2HMbH5qJOLHra+fkzbsYRshM8pLTgUIgSgvuMMoYw+iMYTr6QhNYMrNiNywILlyVKdcyyPsmcRsyU50kgHtQQu2AdNJFpDkAK4I/LIWMb2AsIBcq0iRGeQGPTZfV6QE0+gDGF2rosGEgMQxIAhnZAiPN84upRf0/OlpsLCBp3yq0chgtCUUZrRw6QDO3EWHSqF9tarZaSKJXeJDdbQlsE0KD6JN/KoRsGhxXKClCKHJIfkThwMWhA6dBSolRijkJOd1ZUv9yQ9OpqbHpiaUVaEbZE7tIIjoBKXysos1c4V8ebLEdbv2bcMsE7gjuBdvXRSJ4F+/wqIXXrIGwmX3zwacLDheNO91OjLQKd14VMDCnYgCxVjI3NTe1mSoPD49x4SrAAG9qPn4eovCMAAAAAElFTkSuQmCC" />
<h1>{{ $t('title') }}</h1>
<h2>{{ status.systemID !== null ? $t('systemID') + ': ' + status.systemID : '' }}</h2>
<div class="wifistatus">
<div class="connection">
<div class="indicator" :data-status="wifiStatus.ap.enabled ? 'connected' : 'disconnected'"></div> {{ $t('wifiStatus.accesspoint.title') }} {{ wifiStatus.ap.enabled ? wifiStatus.ap.ip : $t('wifiStatus.accesspoint.disabled') }}
</div>
<div class="connection">
<div class="indicator" :data-status="getWiFiStationStatus()"></div> {{ $t('wifiStatus.stationmode.title') }} {{ getWiFiStationStatusText() }}
</div>
<div class="connection">
<div class="indicator" data-status="disconnected"></div> {{ $t('deviceTime') }}{{ getDeviceTime }}
</div>
</div>
</div>
<div v-if="loading" class="loading">
{{ $t('loading') }} {{ loadingIndicator }}
</div>
<div v-if="!loading && calibration === null">
<div class="warning" v-if="hasResetError">
<p>
{{ $t('error.resetError') }}
</p>
<p class="resetReason">
{{ $t('error.resetReason.' + status.resetReason) }}
</p>
<p v-if="status.stackTrace">
{{ $t('error.stackTrace') }}
</p>
<div v-if="status.stackTrace">
<a class="button button-primary" href="/api/stacktrace/get">{{ $t('error.stackTraceDownload') }}</a>
<a class="button" @click="deleteStackTrace">{{ $t('error.stackTraceDelete') }}</a>
</div>
</div>
<div class="navigation tabs">
<a class="button" :class="{ 'active': activeTab == 'status' }" @click="activeTab = 'status'">{{ $t('status.tabTitle') }}</a><a class="button" :class="{ 'active': activeTab == 'triggers' }" @click="activeTab = 'triggers'">{{ $t('triggers.tabTitle') }}</a><a class="button" :class="{ 'active': activeTab == 'connection' }" @click="activeTab = 'connection'">{{ $t('connection.tabTitle') }}</a><a class="button" :class="{ 'active': activeTab == 'system' }" @click="activeTab = 'system'">{{ $t('system.tabTitle') }}</a>
</div>
<div v-if="activeTab == 'status'">
<!--
Status tab
-->
<h3>{{ $t('status.title') }}</h3>
<div><radio :title="$t('status.allStepsTrue')" v-model="allSteps" :id="true"></radio></div>
<div><radio :title="$t('status.allStepsFalse')" v-model="allSteps" :id="false"></radio></div>
<div class="sliders">
<div class="step" v-if="allSteps">
<span class="value">{{ Math.floor(allStepsValue / 255 * 100) }}%</span>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider" v-model.number="allStepsValue">
</div>
</div>
<div class="step" v-if="!allSteps" v-for="(step, index) in steps">
<span class="value">{{ Math.floor(step.value / 255 * 100) }}%</span>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider" v-model.number="step.value">
</div>
</div>
</div>
</div>
<div v-if="activeTab == 'triggers'">
<!--
Triggers tab
-->
<form @submit.prevent="applyTimeTriggers">
<h3>{{ $t('triggers.timeTitle') }}</h3>
<check v-model.boolean="triggers.time.enabled" :title="$t('triggers.timeEnabled')"></check>
<div v-if="triggers.time.enabled">
<div class="warning" v-if="!wifiStatus.station.enabled || wifiStatus.station.status != 3">
{{ $t('triggers.timeInternet') }}
</div>
<label for="timeTransitionTime">{{ $t('triggers.timeTransitionTime') }}</label>
<input type="number" id="timeTransitionTime" v-model.number="triggers.time.transitionTime">
<h4>Regels</h4>
<div v-if="triggers.time.triggers.length">
<div v-for="(trigger, index) in triggers.time.triggers" class="panel" :class="{ active: trigger.enabled }">
<div class="panel-header">
<check v-model.boolean="trigger.enabled" :title="$t('triggers.timeTriggerEnabled')"></check>
<span class="actions">
<a href="#" @click.prevent="deleteTimeTrigger(index)">{{ $t('triggers.timeDelete') }}</a>
</span>
<div class="clear"></div>
</div>
<div class="panel-body">
<select v-model.number="trigger.triggerType" class="inline">
<option value="0">{{ $t('triggers.timeFixedTime') }}</option>
<option value="1">{{ $t('triggers.timeSunrise') }}</option>
<option value="2">{{ $t('triggers.timeSunset') }}</option>
</select>
<select v-model.number="trigger.fixedTime" class="inline" v-if="trigger.triggerType == 0">
<option v-for="time in fixedTimes" :value="time">{{ getDisplayTime(time, false) }}</option>
</select>
<select v-model.number="trigger.relativeTime" class="inline" v-else>
<option v-for="time in relativeTimes" :value="time">{{ getDisplayTime(time, true) }}</option>
</select>
<div class="weekdays">
<check v-model.boolean="trigger.monday" :title="$t('triggers.timeMonday')"></check>
<check v-model.boolean="trigger.tuesday" :title="$t('triggers.timeTuesday')"></check>
<check v-model.boolean="trigger.wednesday" :title="$t('triggers.timeWednesday')"></check>
<check v-model.boolean="trigger.thursday" :title="$t('triggers.timeThursday')"></check>
<check v-model.boolean="trigger.friday" :title="$t('triggers.timeFriday')"></check>
<check v-model.boolean="trigger.saturday" :title="$t('triggers.timeSaturday')"></check>
<check v-model.boolean="trigger.sunday" :title="$t('triggers.timeSunday')"></check>
</div>
<div class="step">
<span class="value">{{ Math.floor(trigger.brightness / 255 * 100) }}%</span>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider" v-model.number="trigger.brightness">
</div>
</div>
</div>
</div>
</div>
<div v-else class="nodata">
{{ $t('triggers.timeNoData') }}
</div>
<div class="buttons">
<button :disabled="saving" @click.prevent="addTimeTrigger">{{ $t('triggers.timeAdd') }}</button>
</div>
</div>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
<form @submit.prevent="applyMotionTriggers">
<h3>{{ $t('triggers.motionTitle') }}</h3>
<check v-model.boolean="triggers.motion.enabled" :title="$t('triggers.motionEnabled')"></check>
<div v-if="triggers.motion.enabled">
<check v-model.boolean="triggers.motion.enabledDuringDay" :title="$t('triggers.motionEnabledDuringDay')"></check>
<check v-model.boolean="triggers.motion.enabledDuringTimeTrigger" :title="$t('triggers.motionEnabledDuringTimeTrigger')"></check>
<label for="motionTransitionTime">{{ $t('triggers.motionTransitionTime') }}</label>
<input type="number" id="motionTransitionTime" v-model.number="triggers.motion.transitionTime">
<label for="motionDelay">{{ $t('triggers.motionDelay') }}</label>
<input type="number" id="motionDelay" v-model.number="triggers.motion.delay">
<h4>Regels</h4>
<div v-if="triggers.motion.triggers.length">
<div v-for="(trigger, index) in triggers.motion.triggers" class="panel" :class="{ active: trigger.enabled }">
<div class="panel-header">
<check v-model.boolean="trigger.enabled" :title="$t('triggers.motionTriggerEnabled')"></check>
<span class="actions">
<a href="#" @click.prevent="deleteMotionTrigger(index)">{{ $t('triggers.motionDelete') }}</a>
</span>
<div class="clear"></div>
</div>
<div class="panel-body">
<label :for="'motion' + index + '_pin'">{{ $t('triggers.motionPin') }}</label>
<input type="number" :id="'motion' + index + '_pin'" v-model.number="trigger.pin">
<label :for="'motion' + index + '_direction'">{{ $t('triggers.motionDirection') }}</label>
<select :id="'motion' + index + '_direction'" v-model.number="trigger.direction">
<option value="1">{{ $t('triggers.motionDirectionNonDirectional') }}</option>
<option value="2">{{ $t('triggers.motionDirectionTopDown') }}</option>
<option value="3">{{ $t('triggers.motionDirectionBottomUp') }}</option>
</select>
<div class="step">
<span class="value">{{ Math.floor(trigger.brightness / 255 * 100) }}%</span>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider" v-model.number="trigger.brightness">
</div>
</div>
</div>
</div>
</div>
<div v-else class="nodata">
{{ $t('triggers.motionNoData') }}
</div>
<div class="buttons">
<button :disabled="saving" @click.prevent="addMotionTrigger">{{ $t('triggers.motionAdd') }}</button>
</div>
</div>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
</div>
<div v-if="activeTab == 'connection'">
<!--
Connection tab
-->
<form @submit.prevent="applyConnection">
<h3>{{ $t('connection.title') }}</h3>
<check v-model.boolean="connection.accesspoint" :title="$t('connection.accesspoint')"></check>
<span class="hint">{{ $t('connection.accesspointHint') }}</span>
<check v-model.boolean="connection.station" :title="$t('connection.stationmode')"></check>
<span class="hint">{{ $t('connection.stationmodeHint') }}</span>
<label for="ssid">{{ $t('connection.ssid') }}</label>
<input type="text" id="ssid" v-model="connection.ssid" :disabled="!connection.station">
<label for="password">{{ $t('connection.password') }}</label>
<input type="password" id="password" v-model="connection.password" :disabled="!connection.station">
<check v-model.boolean="connection.dhcp" :disabled="!connection.station" :title="$t('connection.dhcp')" class="form-control"></check>
<span class="hint">{{ $t('connection.dhcpHint') }}</span>
<div class="suboptions">
<label for="ip">{{ $t('connection.ipaddress') }}</label>
<input type="text" id="ip" v-model="connection.ip" :disabled="!connection.station || connection.dhcp">
<label for="subnetmask">{{ $t('connection.subnetmask') }}</label>
<input type="text" id="subnetmask" v-model="connection.subnetmask" :disabled="!connection.station || connection.dhcp">
<label for="gateway">{{ $t('connection.gateway') }}</label>
<input type="text" id="gateway" v-model="connection.gateway" :disabled="!connection.station || connection.dhcp">
</div>
<label for="hostname">{{ $t('connection.hostname') }}</label>
<input type="text" :placeholder="$t('connection.hostnamePlaceholder')" id="hostname" v-model="connection.hostname" :disabled="!connection.station">
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
</div>
<div v-if="activeTab == 'system'">
<!--
System tab
-->
<form @submit.prevent="uploadFirmware">
<h3>{{ $t('system.firmwareTitle') }}</h3>
<input type="file" id="firmwareFile">
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
<div v-if="uploadProgress !== false">
{{ uploadProgress }}%
</div>
</form>
<h3>{{ $t('system.calibrateTitle') }}</h3>
<span class="hint">{{ $t('system.calibrateHint') }}</span>
<div class="buttons">
<a class="button button-primary" @click.prevent="startCalibration">{{ $t('system.calibrateButton') }}</a>
</div>
<form @submit.prevent="applySystem">
<h3>{{ $t('system.ntpTitle') }}</h3>
<div class="warning" v-if="!wifiStatus.station.enabled || wifiStatus.station.status != 3">
{{ $t('triggers.timeInternet') }}
</div>
<div class="horizontal">
<label for="ntpServer">{{ $t('system.ntpServer') }}</label>
<input type="text" id="ntpServer" v-model="system.ntpServer">
</div>
<div class="horizontal">
<label for="ntpInterval">{{ $t('system.ntpInterval') }}</label>
<input type="number" id="ntpInterval" v-model="system.ntpInterval">
</div>
<div class="horizontal">
<label for="lat">{{ $t('system.ntpLat') }}</label>
<input type="text" id="lat" v-model="system.lat">
</div>
<div class="horizontal">
<label for="lng">{{ $t('system.ntpLng') }}</label>
<input type="text" id="lng" v-model="system.lng">
</div>
<div class="suboptions">
<label for="location" class="label-inline">{{ $t('system.ntpLocation') }}</label>
<input type="text" id="location" v-model="location">
<button @click.prevent="searchLocation" :disabled="searchingLocation">{{ $t('system.ntpLocationSearch') }}</button>
</div>
<h3>{{ $t('system.pinsTitle') }}</h3>
<div class="horizontal">
<label for="pinLEDAP">{{ $t('system.pinLEDAP') }}</label>
<input type="number" id="pinLEDAP" v-model.number="system.pins.ledAP">
</div>
<div class="horizontal">
<label for="pinLEDSTA">{{ $t('system.pinLEDSTA') }}</label>
<input type="number" id="pinLEDSTA" v-model.number="system.pins.ledSTA">
</div>
<div class="horizontal">
<label for="pinAPButton">{{ $t('system.pinAPButton') }}</label>
<input type="number" id="pinAPButton" v-model.number="system.pins.apButton">
</div>
<div class="horizontal">
<label for="pinPWMDriverSDA">{{ $t('system.pinPWMDriverSDA') }}</label>
<input type="number" id="pinPWMDriverSDA" v-model.number="system.pins.pwmSDA">
</div>
<div class="horizontal">
<label for="pinPWMDriverSCL">{{ $t('system.pinPWMDriverSCL') }}</label>
<input type="number" id="pinPWMDriverSCL" v-model.number="system.pins.pwmSCL">
</div>
<div class="horizontal">
<label for="pwmAddress">{{ $t('system.pwmAddress') }}</label>
<input type="number" id="pwmAddress" v-model.number="system.pwmAddress">
</div>
<div class="horizontal">
<label for="pwmFrequency">{{ $t('system.pwmFrequency') }}</label>
<input type="number" id="pwmFrequency" v-model.number="system.pwmFrequency">
</div>
<h3>{{ $t('system.mapsTitle') }}</h3>
<label for="mapsAPIKey">{{ $t('system.mapsAPIKey') }}</label>
<input type="text" id="mapsAPIKey" v-model.number="system.mapsAPIKey">
<span class="hint">{{ $t('system.mapsAPIKeyhint') }}</span>
<div class="buttons">
<input type="submit" :disabled="saving" :value="saving ? $t('applyButtonSaving') : $t('applyButton')">
</div>
</form>
</div>
</div>
<!--
Calibration
-->
<div v-if="!loading && calibration !== null">
<div class="navigation">
<a class="button" @click.prevent="stopCalibration">&laquo; {{ $t('calibration.backButton') }}</a>
</div>
<h3>{{ $t('calibration.title') }}</h3>
<div class="horizontal">
<label for="stepCount">{{ $t('calibration.count') }}</label>
<input type="number" id="stepCount" v-model.number="calibration.count" min="1" max="16" :disabled="calibration.wizardStep >= 1">
</div>
<div v-if="calibration.wizardStep >= 1">
<h4>{{ $t('calibration.allStepsValue') }}</h4>
<div class="slidercontainer">
<input type="range" min="0" max="255" class="slider" v-model.number="allStepsValue">
</div>
<check v-model.boolean="calibration.useCurve" :title="$t('calibration.useCurve')" class="form-control"></check>
<h4>{{ $t('calibration.ranges') }}</h4>
<div class="range" v-for="(step, index) in calibration.ranges">
<range v-model="step"></range>
</div>
</div>
<div class="buttons">
<a class="button button-primary" @click.prevent="nextCalibrationStep">{{ $t(hasNextCalibrationStep() ? 'calibration.nextButton' : 'calibration.applyButton') }}</a>
</div>
</div>
<div class="clearfix"></div>
</div>
<div class="version">
{{ $t('copyright') }}<br>
{{ status.version !== null ? $t('firmwareVersion') + status.version : '' }}
</div>
</div>
</div>
<script language="javascript">
startApp();
</script>
</body>
</html>

367
web/lang.js Normal file
View File

@ -0,0 +1,367 @@
var messages = {
en: {
title: 'Stairs',
systemID: 'System ID',
firmwareVersion: 'Firmware version: ',
copyright: 'Copyright © 2017 Mark van Renswoude',
loading: 'Please wait, loading configuration...',
rebootPending: 'The system will be rebooted, please refresh this page afterwards',
applyButton: 'Apply',
applyButtonSaving: 'Saving...',
deviceTime: 'Time: ',
wifiStatus: {
accesspoint: {
title: 'AP: ',
disabled: 'Disabled'
},
stationmode: {
title: 'WiFi: ',
disabled: 'Disabled',
idle: 'Idle',
noSSID: 'SSID not found',
scanCompleted: 'Scan completed',
connectFailed: 'Failed to connect',
connectionLost: 'Connection lost',
disconnected: 'Disconnected'
}
},
status: {
tabTitle: 'Status',
title: 'Current status',
allStepsTrue: 'Set intensity for all steps',
allStepsFalse: 'Set intensity individually'
},
triggers: {
tabTitle: 'Triggers',
timeTitle: 'Time',
timeInternet: 'Please note that time triggers require an internet connection.',
timeNoData: 'No time triggers defined yet',
timeEnabled: 'Enable time triggers',
timeTransitionTime: 'Transition time in milliseconds',
timeAdd: 'Add',
timeDelete: 'Delete',
timeTriggerEnabled: 'Enabled',
timeFixedTime: 'Fixed time',
timeSunrise: 'Sunrise',
timeSunset: 'Sunset',
timeTime: 'Time in minutes',
timeMonday: 'Monday',
timeTuesday: 'Tuesday',
timeWednesday: 'Wednesday',
timeThursday: 'Thursday',
timeFriday: 'Friday',
timeSaturday: 'Saturday',
timeSunday: 'Sunday',
motionTitle: 'Motion',
motionNoData: 'No motion triggers defined yet',
motionEnabled: 'Enable motion triggers',
motionEnabledDuringTimeTrigger: 'Activate even if a time trigger is already active',
motionEnabledDuringDay: 'Activate during the day (between sunrise and sunset)',
motionTransitionTime: 'Transition time in milliseconds',
motionDelay: 'Keep on time in milliseconds',
motionTriggerEnabled: 'Enabled',
motionAdd: 'Add',
motionDelete: 'Delete',
motionPin: 'GPIO pin (active high)',
motionDirection: 'Sweep animation',
motionDirectionNonDirectional: 'None (all steps at the same time)',
motionDirectionTopDown: 'Top down',
motionDirectionBottomUp: 'Bottom up'
},
connection: {
tabTitle: 'Connection',
title: 'Connection parameters',
accesspoint: 'Enable access point',
accesspointHint: 'Allows for a direct connection from your device to this Stairs module for configuration purposes. The Stairs configuration is available on http://192.168.1.4/ when you are connected to it. Turn it off as soon as station mode is configured, as it is not secured in any way. You can always turn this option back on by pushing the access point button until the LED lights up.',
stationmode: 'Enable station mode',
stationmodeHint: 'Connect this Stairs module to your own WiFi router. Please enter the SSID, password and further configuration below.',
ssid: 'SSID',
password: 'Password',
dhcp: 'Use DHCP',
dhcpHint: 'Automatically assigns an IP address to this Stairs module. You probably want to keep this on unless you know what you\'re doing.',
ipaddress: 'IP address',
subnetmask: 'Subnet mask',
gateway: 'Gateway',
hostname: 'Hostname',
hostnamePlaceholder: 'Default: mac address'
},
system: {
tabTitle: 'System',
ntpTitle: 'Time synchronisation (NTP)',
pinsTitle: 'Hardware pinout',
mapsTitle: 'Google Maps API',
firmwareTitle: 'Firmware update',
calibrateTitle: 'Calibrate',
calibrateButton: 'Calibrate steps',
calibrateHint: 'Use the button below to configure the number of steps, and to adjust the brightness of each individual step',
ntpServer: 'NTP server',
ntpInterval: 'Refresh interval (in minutes)',
ntpLat: 'Latitude',
ntpLng: 'Longitude',
ntpLocation: 'Get latitude / longitude from location',
ntpLocationSearch: 'Search',
pinLEDAP: 'Access Point status LED pin (+3.3v)',
pinLEDSTA: 'Station Mode status LED pin (+3.3v)',
pinAPButton: 'Enable Access Point button pin (active low)',
pinPWMDriverSDA: 'PCA9685 PWM driver SDA pin (data)',
pinPWMDriverSCL: 'PCA9685 PWM driver SCL pin (clock)',
pwmAddress: 'PCA9685 PWM driver I²C address',
pwmFrequency: 'PCA9685 PWM driver frequency',
mapsAPIKey: 'Google Maps API key',
mapsAPIKeyhint: 'Recommended if using time triggers. Used for looking up the current timezone. Will work without an API key, but Google might throttle your request. Register for a free API key at http://console.developers.google.com/ and activate it\'s use for the Maps API.'
},
error: {
loadStatus: 'Could not load system status',
loadConnection: 'Could not load connection settings',
loadSystem: 'Could not load system settings',
loadTimeTriggers: 'Could not load time trigger settings',
loadMotionTriggers: 'Could not load motion trigger settings',
applyConnection: 'Could not save connection settings',
applySystem: 'Could not save system settings',
updateWiFiStatus: 'Could not retrieve WiFi status',
uploadFirmware: 'Error while uploading firmware',
updateSteps: 'Could not apply new step values',
searchLocation: 'Could not look up location coordinates',
applyTimeTriggers: 'Could not save time trigger settings',
applyMotionTriggers: 'Could not save motion trigger settings',
loadSteps: 'Could not load calibration settings',
updateCalibration: 'Could not save calibration settings',
resetError: 'The system reports that it has been reset unexpectedly. The last power up status is:',
resetReason: {
0: 'Normal startup',
1: 'Unresponsive, reset by hardware watchdog',
2: 'Unhandled exception',
3: 'Unresponsive, reset by software watchdog',
4: 'System restart requested',
5: 'Wake up from deep sleep',
6: 'System reset'
},
stackTrace: 'A stack trace is available. Please send it to your nearest developer and/or delete it from this Stairs device to remove this message.',
stackTraceDownload: 'Download',
stackTraceDelete: 'Remove',
stackTraceDeleteError: 'Could not remove stack trace'
},
calibration: {
title: 'Calibration wizard',
backButton: 'Back',
count: 'Number of steps',
nextButton: 'Next',
applyButton: 'Complete',
allStepsValue: 'Intensity for all steps',
ranges: 'Min / max values per step',
useCurve: 'Use logarithmic curve for intensity (recommended for LEDs)'
}
},
nl: {
title: 'Trap',
systemID: 'Systeem ID',
firmwareVersion: 'Firmware versie: ',
copyright: 'Copyright © 2017 Mark van Renswoude',
loading: 'Een ogenblik geduld, bezig met laden van configuratie...',
rebootPending: 'Het systeem wordt opnieuw opgestart, ververse deze pagina nadien',
applyButton: 'Opslaan',
applyButtonSaving: 'Bezig met opslaan...',
deviceTime: 'Tijd: ',
wifiStatus: {
accesspoint: {
title: 'AP: ',
disabled: 'Uitgeschakeld'
},
stationmode: {
title: 'WiFi: ',
disabled: 'Uitgeschakeld',
idle: 'Slaapstand',
noSSID: 'SSID niet gevonden',
scanCompleted: 'Scan afgerond',
connectFailed: 'Kan geen verbinding maken',
connectionLost: 'Verbinding verloren',
disconnected: 'Niet verbonden'
}
},
status: {
tabTitle: 'Status',
title: 'Huidige status',
allStepsTrue: 'Alle treden dezelfde intensiteit',
allStepsFalse: 'Treden individueel instellen'
},
triggers: {
tabTitle: 'Triggers',
timeTitle: 'Tijd',
timeInternet: 'Let op dat voor tijd triggers een internetverbinding vereist is.',
timeNoData: 'Nog geen tijd triggers geconfigureerd',
timeEnabled: 'Tijd triggers inschakelen',
timeTransitionTime: 'Transitie tijd in milliseconden',
timeAdd: 'Toevoegen',
timeDelete: 'Verwijderen',
timeTriggerEnabled: 'Actief',
timeFixedTime: 'Vaste tijd',
timeSunrise: 'Zonsopkomst',
timeSunset: 'Zonsondergang',
timeTime: 'Tijd in minuten',
timeMonday: 'Maandag',
timeTuesday: 'Dinsdag',
timeWednesday: 'Woensdag',
timeThursday: 'Donderdag',
timeFriday: 'Vrijdag',
timeSaturday: 'Zaterdag',
timeSunday: 'Zondag',
motionTitle: 'Beweging',
motionNoData: 'Nog geen beweging triggers geconfigureerd',
motionEnabled: 'Beweging triggers inschakelen',
motionEnabledDuringTimeTrigger: 'Ook inschakelen als er al een tijd trigger actief is',
motionEnabledDuringDay: 'Ook overdag inschakelen (tussen zonsopgang en zonsondergang)',
motionTransitionTime: 'Transitie tijd in milliseconden',
motionDelay: 'Tijd aan in milliseconden',
motionTriggerEnabled: 'Actief',
motionAdd: 'Toevoegen',
motionDelete: 'Verwijderen',
motionPin: 'GPIO pin (actief hoog)',
motionDirection: 'Animatie',
motionDirectionNonDirectional: 'Geen (alle treden gelijktijdig)',
motionDirectionTopDown: 'Boven naar beneden',
motionDirectionBottomUp: 'Beneden naar boven'
},
connection: {
tabTitle: 'Verbinding',
title: 'Verbinding configuratie',
accesspoint: 'Access point inschakelen',
accesspointHint: 'Maakt het mogelijk om een directe connectie vanaf een apparaat naar deze Trap module te maken om de module te configureren. De Trap module is te benaderen via http://192.168.1.4/ nadat je connectie hebt gemaakt. Schakel deze optie uit na het configureren, aangezien deze niet beveiligd is. Je kunt deze optie ook inschakelen door op de Access point knop te drukken totdat de LED aan gaat.',
stationmode: 'Verbinding met WiFi maken',
stationmodeHint: 'Verbind deze Trap module aan je eigen WiFi router. Vul hieronder het SSID en wachtwoord in, en configureer eventuel de overige opties.',
ssid: 'SSID',
password: 'Wachtwoord',
dhcp: 'Gebruik DHCP',
dhcpHint: 'Automatisch een IP adres toewijzen aan deze Trap module. Waarschijnlijk wil je deze optie aan laten, tenzij je weet waar je mee bezig bent.',
ipaddress: 'IP adres',
subnetmask: 'Subnet masker',
gateway: 'Gateway',
hostname: 'Hostnaam',
hostnamePlaceholder: 'Standaard: mac adres'
},
system: {
tabTitle: 'Systeem',
ntpTitle: 'Tijd synchronisatie (NTP)',
pinsTitle: 'Hardware aansluitingen',
mapsTitle: 'Google Maps API',
firmwareTitle: 'Firmware bijwerken',
calibrateTitle: 'Kalibratie',
calibrateButton: 'Kalibreer treden',
calibrateHint: 'Gebruik onderstaande knop om het aantal treden in te stellen, en om de helderheid van elke trede aan te passen',
ntpServer: 'NTP server',
ntpInterval: 'Ververs interval (in minuten)',
ntpLat: 'Breedtegraad',
ntpLng: 'Lengtegraad',
ntpLocation: 'Breedtegraad / lengtegraad ophalen op basis van locatie',
ntpLocationSearch: 'Zoeken',
pinLEDAP: 'Access Point status LED pin (+3.3v)',
pinLEDSTA: 'WiFi status LED pin (+3.3v)',
pinAPButton: 'Access Point inschakelen knop pin (actief laag)',
pinPWMDriverSDA: 'PCA9685 PWM driver SDA pin (data)',
pinPWMDriverSCL: 'PCA9685 PWM driver SCL pin (klok)',
pwmAddress: 'PCA9685 PWM driver I²C address',
pwmFrequency: 'PCA9685 PWM driver frequency',
mapsAPIKey: 'Google Maps API key',
mapsAPIKeyhint: 'Aangeraden bij gebruik van de tijd triggers. Wordt gebruikt om de huidige tijdzone te bepalen. Werkt ook zonder API key, maar Google beperkt dan sterk de requests. Registreer een gratis API key op http://console.developers.google.com/ en activeer het voor gebruik met de Maps API.'
},
error: {
loadStatus: 'Kan systeemstatus niet ophalen',
loadConnection: 'Kan verbinding instellingen niet ophalen',
loadSystem: 'Kan systeem instellingen niet ophalen',
loadTimeTriggers: 'Kan tijd trigger instellingen niet ophalen',
loadMotionTriggers: 'Kan beweging trigger instellingen niet ophalen',
applyConnection: 'Kan verbinding instellingen niet opslaan',
applySystem: 'Kan systeem instellingen niet opslaan',
updateWiFiStatus: 'Kan WiFi status niet ophalen',
uploadFirmware: 'Fout tijdens bijwerken van firmware',
updateSteps: 'Kan trap instellingen niet opslaan',
searchLocation: 'Kan locatie coordinaten niet bepalen',
applyTimeTriggers: 'Kan tijd trigger instellingen niet opslaan',
applyMotionTriggers: 'Kan beweging trigger instellingen niet opslaan',
loadSteps: 'Kan kalibratie instellingen niet ophalen',
updateCalibration: 'Kan kalibratie instellingen niet opslaan',
resetError: 'Het systeem is onverwachts herstart. De laatste status is:',
resetReason: {
0: 'Normaal opgestart',
1: 'Reageert niet, herstart door hardware watchdog',
2: 'Onafgehandelde fout',
3: 'Reageert niet, herstart door software watchdog',
4: 'Herstart verzoek door systeem',
5: 'Wakker geworden uit diepe slaap',
6: 'Systeem gereset'
},
stackTrace: 'Een stack trace is beschikbaar. Stuur het naar de dichtsbijzijnde ontwikkelaar en/of verwijder het van deze Trap module om dit bericht te verbergen.',
stackTraceDownload: 'Downloaden',
stackTraceDelete: 'Verwijderen',
stackTraceDeleteError: 'Kan stack trace niet verwijderen'
},
calibration: {
title: 'Kalibratie wizard',
backButton: 'Terug',
count: 'Aantal treden',
nextButton: 'Volgende',
applyButton: 'Voltooien',
allStepsValue: 'Intensiteit voor alle treden',
ranges: 'Min / max waarden per trede',
useCurve: 'Gebruik logaritmische curve voor intensiteit (aangeraden voor LEDs)'
}
}
}

333
web/logo.ai Normal file
View File

@ -0,0 +1,333 @@
%PDF-1.5 %âãÏÓ
1 0 obj <</Metadata 2 0 R/OCProperties<</D<</ON[5 0 R 21 0 R]/Order 22 0 R/RBGroups[]>>/OCGs[5 0 R 21 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <</Length 9212/Subtype/XML/Type/Metadata>>stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/">
<xmp:CreatorTool>Adobe Illustrator CS6 (Windows)</xmp:CreatorTool>
<xmp:CreateDate>2017-12-30T16:13+01:00</xmp:CreateDate>
<xmp:MetadataDate>2017-12-30T16:17:16+01:00</xmp:MetadataDate>
<xmp:ModifyDate>2017-12-30T16:17:16+01:00</xmp:ModifyDate>
<xmp:Thumbnails>
<rdf:Alt>
<rdf:li rdf:parseType="Resource">
<xmpGImg:width>248</xmpGImg:width>
<xmpGImg:height>256</xmpGImg:height>
<xmpGImg:format>JPEG</xmpGImg:format>
<xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA&#xA;AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK&#xA;DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f&#xA;Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAD4AwER&#xA;AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA&#xA;AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB&#xA;UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE&#xA;1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ&#xA;qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy&#xA;obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp&#xA;0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo&#xA;+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q==</xmpGImg:image>
</rdf:li>
</rdf:Alt>
</xmp:Thumbnails>
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/">
<xmpTPg:NPages>1</xmpTPg:NPages>
<xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency>
<xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
<xmpTPg:MaxPageSize rdf:parseType="Resource">
<stDim:w>512.000000</stDim:w>
<stDim:h>512.000000</stDim:h>
<stDim:unit>Points</stDim:unit>
</xmpTPg:MaxPageSize>
<xmpTPg:SwatchGroups>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<xmpG:groupName>Default Swatch Group</xmpG:groupName>
<xmpG:groupType>0</xmpG:groupType>
</rdf:li>
</rdf:Seq>
</xmpTPg:SwatchGroups>
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:format>application/pdf</dc:format>
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/">
<illustrator:Type>Document</illustrator:Type>
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#">
<xmpMM:DocumentID>xmp.did:89DEF0EC73EDE7119CCADB31B93B2005</xmpMM:DocumentID>
<xmpMM:InstanceID>uuid:b03a2483-4f32-4465-b083-e0550ab05b88</xmpMM:InstanceID>
<xmpMM:OriginalDocumentID>xmp.did:89DEF0EC73EDE7119CCADB31B93B2005</xmpMM:OriginalDocumentID>
<xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
<xmpMM:DerivedFrom rdf:parseType="Resource"/>
<xmpMM:History>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<stEvt:action>saved</stEvt:action>
<stEvt:instanceID>xmp.iid:89DEF0EC73EDE7119CCADB31B93B2005</stEvt:instanceID>
<stEvt:when>2017-12-30T16:13:01+01:00</stEvt:when>
<stEvt:softwareAgent>Adobe Illustrator CS6 (Windows)</stEvt:softwareAgent>
<stEvt:changed>/</stEvt:changed>
</rdf:li>
</rdf:Seq>
</xmpMM:History>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream endobj 3 0 obj <</Count 1/Kids[7 0 R]/Type/Pages>> endobj 7 0 obj <</ArtBox[72.1367 53.875 458.887 458.125]/BleedBox[0.0 0.0 512.0 512.0]/Contents 23 0 R/LastModified(D:20171230161716+02'00')/MediaBox[0.0 0.0 512.0 512.0]/Parent 3 0 R/PieceInfo<</Illustrator 24 0 R>>/Resources<</ExtGState<</GS0 25 0 R>>/Properties<</MC0 21 0 R>>>>/Thumb 26 0 R/TrimBox[0.0 0.0 512.0 512.0]/Type/Page>> endobj 23 0 obj <</Filter/FlateDecode/Length 177>>stream
H‰”Á
Â0 †ïyŠ¼Àº&M¶õj•<6A>0dîàˆxqÂôàëÛ
º¦ ¡¥)ßßÐr°ì‚ÅÕ: L`Q‰_«HÛí¼aª} *†±Ù"”í`ñ|‡)¶6VdÄáºFfÂãévnØ8'ñ|<7C>úEÞÿàûÿó]•ûD yšó¿L9{¼7\Q<>âr ¥g¹…1ÄŠñ) V5º€¿íM.Wÿ!7]üŠž CÕPt
endstream endobj 26 0 obj <</BitsPerComponent 8/ColorSpace 27 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 64/Length 40/Width 64>>stream
8;Z]L!=]#/!5bE.$"(^o%O_;W!8uZ9(]Y:<E5V~>
endstream endobj 27 0 obj [/Indexed/DeviceRGB 255 28 0 R] endobj 28 0 obj <</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream
8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn
6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1
VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH<
PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O(
l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~>
endstream endobj 21 0 obj <</Intent 29 0 R/Name(Logo)/Type/OCG/Usage 30 0 R>> endobj 29 0 obj [/View/Design] endobj 30 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 16.0)/Subtype/Artwork>>>> endobj 25 0 obj <</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>> endobj 24 0 obj <</LastModified(D:20171230161716+02'00')/Private 31 0 R>> endobj 31 0 obj <</AIMetaData 32 0 R/AIPrivateData1 33 0 R/AIPrivateData2 34 0 R/ContainerVersion 11/CreatorVersion 16/NumBlock 2/RoundtripStreamType 1/RoundtripVersion 16>> endobj 32 0 obj <</Length 964>>stream
%!PS-Adobe-3.0
%%Creator: Adobe Illustrator(R) 16.0
%%AI8_CreatorVersion: 16.0.0
%%For: (PsychoMark) ()
%%Title: (logo.ai)
%%CreationDate: 12/30/2017 4:17 PM
%%Canvassize: 16383
%%BoundingBox: 113 218 501 623
%%HiResBoundingBox: 113.6367 218.375 500.3867 622.625
%%DocumentProcessColors:
%AI5_FileFormat 12.0
%AI12_BuildNumber: 682
%AI3_ColorUsage: Color
%AI7_ImageSettings: 0
%%RGBProcessColor: 0 0 0 ([Registration])
%AI3_Cropmarks: 41.5 164.5 553.5 676.5
%AI3_TemplateBox: 297.5 420.5 297.5 420.5
%AI3_TileBox: -8.5 24.5 603.5 816.5
%AI3_DocumentPreview: None
%AI5_ArtSize: 14400 14400
%AI5_RulerUnits: 2
%AI9_ColorModel: 1
%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0
%AI5_TargetResolution: 800
%AI5_NumLayers: 1
%AI9_OpenToView: -1232 1218 0.5 1789 914 26 0 0 82 117 0 0 0 1 1 1 1 1 0 1
%AI5_OpenViewLayers: 7
%%PageOrigin:0 0
%AI7_GridSettings: 72 8 72 8 1 0 0.8 0.8 0.8 0.9 0.9 0.9
%AI9_Flatten: 1
%AI12_CMSettings: 00.MS
%%EndComments
endstream endobj 33 0 obj <</Length 2175>>stream
%%BoundingBox: 113 218 501 623
%%HiResBoundingBox: 113.6367 218.375 500.3867 622.625
%AI7_Thumbnail: 124 128 8
%%BeginData: 2021 Hex Bytes
%0000330000660000990000CC0033000033330033660033990033CC0033FF
%0066000066330066660066990066CC0066FF009900009933009966009999
%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66
%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333
%3333663333993333CC3333FF3366003366333366663366993366CC3366FF
%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99
%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033
%6600666600996600CC6600FF6633006633336633666633996633CC6633FF
%6666006666336666666666996666CC6666FF669900669933669966669999
%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33
%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF
%9933009933339933669933999933CC9933FF996600996633996666996699
%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33
%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF
%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399
%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933
%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF
%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC
%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699
%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33
%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100
%000011111111220000002200000022222222440000004400000044444444
%550000005500000055555555770000007700000077777777880000008800
%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB
%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF
%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF
%524C45FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFF
%FDFCFFFDFCFFFDFCFFFDF7FFFF
%%EndData
endstream endobj 34 0 obj <</Length 24380>>stream
%AI12_CompressedDataxœÜ½gwò:Ó0z>ïµò(Iè`z ½)Š)¡Æ†]žïo?ÜdãFÙïÙç¹÷ºsg¤Ñhšf¤;c£åÊL¶#Òåw†›?îîr9Üo©˜ýl¨¬VzOÁŸ¬MÁr°U¦°-;$E/¶›zÆ<-Â÷­ úŸñ|û4¤6ƒÕo/ö+<Ymg[÷paãñ<C3A3>÷óÃ=xäõyü„ÇGxÆ@ üi<¡6ÃÍŸCš^ülòGüðÇìö°™,6³ìöoð«×oðy#† á5„|èyyÑ$ii#wÈ
Ãn8ZnüòùÜ!_¾–ߎkr³oPÛ1IÓ¹íjKÑ1ð$S Š‹ ·îAOÑX3¯o<C2AF>=,V“çÃzDq‡">ô»€Þ}¥‡3ÐmôýTÖ৹߃ŽÑ1"Y³”Å1Ö<7F>&9[  4ú²q<C2B2>©ín  ÞxÝA@”ø úÁßP8ä²íÚäz·„EÃ÷EÃàqÀG€¿Øg®)jæŠÀç^ˆ€ð"^ž@òÏùWÌð¼Ý<C2BC>,m2Ô¾ÅÌP @Ì_öQó°"©×ÍbúËçi;!Wà Dq5D4Aÿy…¿lö<E280B9>š{0±ÛÕa<C395>¸.ÂcPþ¸Ôwä¦½í ®º¼>¿LàHo85D½ƒ/„°DÀ3Àpbî?‚ï!q8ÂpÚ`&ëÔb¶ØĸN†%j1¦7ì3D˜?h îöÿ(÷¶¿`ôû=¹áX+÷„1
á~jA¬…Í$·]É ÑJÜûTøž; ôÃÌ[ƒZl à›?ž™gAcuKÔö°«l¦Û?¬Œè<>c°ÐÁäN õÑø4bbCŽ ð<>oöΦŒ“" ÌSð.ú
þ<EFBFBD>Âõ¼Ÿ'§`
˜_ ?ÉÕv‡æn&†·!µÓ¼±n†”=àa×'C@2ºð›.°€kv€Hè%ÔFöHªá~nÈ®ÈÍ„æ¡3_%<25>g~Ô²õÏz´]-è5ÿ…ÿ¬³w{ÚÔ7L7©=7´·Û•@¦ûH˜djϼóŸAÂ7Cþw<C3BE>熫ÕbF wóÅX¾Ìs»º¸hŒ¨.‡QüˆGvüÆ ÇD…É?…e­Ú¦õ×p?žñ2¢†ÔÔXpz§Ít´uXìIa¾¶ë4u ­ùp†-XKnp®\.MÕežA¿ÿgE­ã©n¶mÐ7°Ž¬@$«ý—Íày®Iƒ´i-€ùAò<41>CþÁUlwi?îp<C3AE>ð é…£¡ˆ} |>ÔŽ°oØ׺ø‚—ûöüö>ý€ßþ2x Óáã0LnÀCᘀ®0¢;~ó‡Áz
? aR`ƒÒ¦Nc¸ê˜d†Ñéï·Ô<C2B7>Cèþ¤œÔÂkŒÁÿ³Boxœ§MR MÌñ<C38C>™&gò„—`‚ú¸ß0é¯JØNs†%ðGÜ <Ü<0F>×ÅgHýÃü@<Mr¸2X<32>#'5rºï WÀøNöi¼j…frø¤9Ù‡ž E ÿQ€ŽúƒÀ8dÿA8[ÐaøÌ`Ev,cîI0å40´ÿaœôÊãÑvHM ^°$€ñ;¶nkF?3Xá
aÛy¹42Mö'ÆŽglz¡ MrÕÞ6ìLw[z{<7B>ûØ—‘?€|ýïz…¾d<C2BE>äÆÍõžå¬Ãqî~„ˆ u»Oµg0Rú‚·þ^¯6 <36> ˆEj1:ìIZnbþ¯€¹¬Ýx<AŠäØÃDz÷þÙÿ³ãHi½ßЃ?‡,ÂñÞöOŒ©ÑZ¡áŸ®¥ó©6ÀÓÔC¥Õv¼$'ºÈÄ5½&ãž?ÀÑE*¼z xøtÌ¢Ô1P¼ù5™âr¢xÕ‰¢<E280B0>áhEêZzfúÿsF8](ÄþÔ/`ÛÿÃÃqŽô~»þˆ¿•EcôÂиëP?§þßX7 CÿµþüïZÊôô¯ÿÿÿ@Ó«Åø<C3B8>÷GBÀ5äŒ{ÅAÿµ˜ìçºf<C2BA>mùŸ<C3B9>Ý"àöinN.fs]â…oúŸ·?¤5¾t íÊ®Ñ٣ⷿ´†õ·>)ô_ç´*Žg´Ý# 8ß]ÇðŽ_úÏØHP¶¶jL¢ýÍÿ†AôÜ¢kr?œ
<EFBFBD>‰^Üó„<C3B3>céâ9¬µSv1ùCÜâÝ 'œçJp `=¤—ÒEAï¶{i»ájÁ±l˜[\“ÝÂÍ wö§ñvE }ËT ™Ã~khé= ÷ù¤P3¯×À À°Üçz{ØfL [­íÞÂMjÃîF¡ÝDië¨a7Ü<E28098>+(•° l2änc¸µ,°ÁžnèÝÌïøЯÅÄ@ ãð…|Þ€"4ƒôî »íŒ"ñ¥Õ˜âc^¿—+7õâ}Ðl÷A³1Ö˜_Á7å°LFK —W4• Š¤IêOÒÐ&ÿÞ£<C39E>Žáh±Zì9ÕñFaX2@ÑIiXÛÝ„[çƒ&4à.,oP£¦¤PAbê‚Ônôð' "8~oH¨P‡räjUø{/,Q.|„YW6òï9Þn&'¾T\P´¢âj»¥Pß: zÁP½µpÅ!É¿Æ8Õ5c<°öv'š­“æ<E2809C>‡r<íá°÷„igödfçZÂ0e²-€\Ùqü»ý“¤vp‡Öxc¼ZìÀê<C380>nýß`õÌ€Hâej<65>_>¢W($9]¢D ÃhF:æÅÇ9YÖ!µåëxÛ­5~geuŠ<|´÷|o!Èï“ÀUÿDÒs^~#)Š¡ãÜBôJý°ß!®ñ! ø¨µáfv€©"<22>íŽW<0F>h4 1[ü¦"[<5B>ÊqÊnÿîö¸w7jsh¹`rŽè&! kµÌTŠ‡ÕŠë!›¹žrš<72>7ùeIq8&3ÙJkiÁL'Mˆ<4D>ƒPn )œ_Ð{ŒÇT ÃΈ€Ÿ¶p¡ààöø<E28099>5UÂÔ“ß…Õg½§Ù¶É«² âxQÃún8T®¦åȃ÷7‰F¼*m±î†A¿JK=<3D>E>ˆ¨¯j4@ŒwVƒÓ)MòâQ[¡ÞË_cÏ µ˜<E28093>àSW¦m7eÜó÷)Œª-XÝŠÊÉŠ6<C5A0>ÂÄðp³_€ <¤±)
ª&IICwµØ<C2B5>zOm—¼?Hh´Þc‰“UA˜OJ<10>HD^ ¾aÑ®±¢ÌTyÐàÄœó%4Wûxˆ‰@W<>Ójv ­ö?IÄr²š9ðŸ`<60> ¢~ƒŒå<06>MbñˆÀCI^ö<>5N•3F<33>\ZPc<50>!Ô<ÐÉta="a¾çt±"«¤0<C2A4>2Mßæñ\®)Ã¥¢¶0¡Ø´ÈÕi×ÈaS0»"ƒß+˜¢æ­Ãˆ›âXM¨3„nÄaûñš<E28093>ç×àyÞîÅÏ9QøÑ€±Q$ã!²Úv,ç5ÖÀ+\C%¯nŠa6˜Ïœa5»K,z ç eNÊÁ½Åe ‚˜ƒ&eŽ5)"“ò¨m<C2A8>!-E<>#jÖ$ n<>¢6 Ý<Lþ®!+2K½¶u©y¬M,¯¡Ðh<C390>@-ØZ\ L<>ôbkLÜN<C39C>bL#<23>$c!ª¸QÃf+ø†Å¹05‡ÏVÒX­=B<Òó³¹wÀ覀ñ-íØQ;ˆz¸Zi4£—Ý t)FÒv †C“°”FÓñvµ°õØß‘¾À…-N÷¶aƆ, Å·#˜Zi€TÖIqQª£J‡áèHIœíuÔ[z¿rO<18>¨Ó<Ý4(ßc_Àˆz^ÚMÖàñj£¿_»‰~ðÌž¾ðŠÜôïvÛN¥°ÛÎTbˆU+Ò†øöX8ªØL´ÏD(µZÀâ÷
ÞúZî·;í2M) ¿J?™ÆÌ&GzÀw@4M £ y
X"”ú\@0˜=F5N¹•6å`[Ü£P'mw4ý¨ŠÙjÈ I/f\ÁûԄˆÝ3Q…‰Z"KIOÃ!=Zì×Ã<C397>~Q&ž˜ã±¯(7\£MÆÛ `ƒ= ò« 6åµ âcNˆ‡}²Í©‰øfÀ¦Êy¤-§@ Ï·Ôÿpî†B³pªÊñÌ­ÎŒl#.<2E> 35¥Íhè©ðÐ4þ©1Rz¼[<5B>¥öq£ñæ(h+m´¶2¾Ï¡0J0e«áN9؆jØÍÖK7¹<37>)6ª6£ar¸>^FíIŽ-=/<2F>U°‡õ \g°„£¶@ @«L"¨ä;?ΓV£1µÝiµ<69>ÔZí(,_/Üo )ZmJ…a©­…D­÷ø¸´ãº(¤ØšJz¢£5¦µã=[YÓÍÞ=YiHD¦ÑŽšn7ªâ¶£<C2B6>³äfÃý“T³ŠA“Ñš‰j“N»7älˆ(´ùÛŠ <0A>^Úh@[yÇÈ)ç£uòÌ@»éùØ餵`+r£WÐ?<3F>²ð$ÍÂrÚàï<C3A0>[¼ù<C2BC>º'׌¢¤; hGC®é쨩BCVeb±7¹¹2!=<3D>¦@ªc…AÇ¡Ø¡Uh¹Ý<C2B9>Õ$jA«M?j19¨;è*¼¯aœŒÖ¡JP³éa3Vã¦àùFC —† ŸÚ ï£fš^Æx<C386>[ZÖWwËmx#G†Üø¦çµõVo|Ú ú4ü½5°q<05> Ã&Ú”kÄmHŽ×ÿ¨9ÜXËí~®ép3FªÜJÑÒ"GD‡<µ—ž"ðšÌoÇ(ì)ï§g¸7qO<1D>Þ…}ÇÆâorÕ ©)9ÞãbƒÛÕdBY%j8<6A>&.* gvFU÷B™·j06oá[(Ùáè-1.&t22æ5ØNÞzå‰Öê” H¦fXša¶O•¤¤ˆØˆªxC†©;”ÃÓ˜Me±3‡S#È·ó5ƒ¼°òñž ZF¬C~áQƒ"Ç ¹ öѦÆæ¸çGmæۿʉܸQ.ŒóлáQ(<28>%ûÞ
„óE6*
mâínÏçÇíÈPgá±ÓpXÏÅa»pžìgeZ¹J%Ì“PŽ¡§<C2A1>TýþÃx{°Ä‡]ç£ÿ¾îʦ©Òz<6D>E£ÓjÉ-†nú6ôZ.„̱ôk)ùHÅjŸ§4u‡‹ßSäΘ ÎÿägNâ6ï»íé„sG§éªÏФã5#ŵzÜggå—Z: [¹ÅCrœw»-³#\µI çw±ð{iŸÿùÊÞ]ÎÌz[£3•Ö~îH†Ì‡b>pûýYYÞšü”xÉB» G§áÎËÇg¦<67>sw”±âíb_éIJø•ŽÑîµ#ï¼;­¥É Aô*~ê‡üôë-œ]¥WÝØ4;ßçæáw¯ˆ"ß÷ù±·öN¤,o Ði:ןõ·àÓýo¾2©³®ÈÏm¦å2o˜Nt‡“@ý±:Æ…qðÅšñDæÎïÈ>;¿éœåµ˜#ödçÑ<<3C><>ÇÃ%ü´p¦µ9ƒÚKx†ajqû'ÙÕ]Ê⢟‡L­uÿ `KÇçð°œP¼ó•ÎlƵãá)î ¯?á°‡žú3Ô¸âu,c^ä8ÿHwåÂ2üæ'&±EÎ3“ì}z°ºœdvn¬™1ôjwé\%a~+8£AX^<5E>¯|„ÌÉpnÛw$:“<>˜odþBp“;0¤dÈn†Óòz ½l ©’Ù¥-äb´Þ/óSÞ3LÜ<17>Žw
¢ Á}5hˆ©@_Ébý”x+T™ö9gá<C3A1>æëù*€ƒ»„#™,8}ùÔì<C394>ôö<C3B4>ˆO~žûh6ùxõl<C3B5>CZeù.| ]ðZš°@¿<05>Ùü;OÒ©@è=ô3δó?ŽüÔSý- ÛlhôúmÜu_3õ\¶‘Ÿ¶¿é߯<Â%èµ AßC“÷ÂÀëèdÝL½˜ÿyä?!O|º¾›sS»Ð0ù7'[a¤õ»®fê5{µ˜·Mª u8Z3K²À~ç~q¤:Ã_fHÉPd˜Ž·÷¦Lûq8œ„¸%¸ÉèRFT ¬ zî¥Þ'w3ßw"•'ŠŸi?âƒÄw¢˜<bw:²Ûh_:_bââ“ËMÃ>©9}@t£ÁIõX+e<06>^Ä7 Ç.þ]´N«î h¿ûl·ýÓ1EB‡f”,Zïw¶Ü<Ô\5wQàW°z[$lZ… dÔ4X]ë{08³-7èp|üÚÌ„ß}oÒyh”WpS©àrŽ¢rÓ]’Õ@“i?M@Ø$£ùl­·”ë/jŠµ+õÂS°x
>ÂW
<³Ï¾Q´5Vñb>øîs$Kß°_×xZ,äƒálÈUï Ùãö–;N„5ïùÞÙ³?ûÉ:»Út¶™ö¼{ `T<€]Á¹}öï]á<>Ls:·€×¾î³!GmΊN[~j)‡@·sF$:Ý2'цêWÛ_LϺPÞçG<C3A7>ù[æõnLÛÝgší<C5A1>UägåJ é&èˆFh°}$Ù¥u·(&J^+&êß›ö!N [0Ñí%F‡õá÷MÐ<§<E28099>¡­ûð t<>l5ˆ|;âz
ä÷ØÌz«<>Lëû1Ç><3E>f¾Ó‰rÎ š|w€<¨Ýg½ï‡A¦uh„§¨1”ià—ušÇî™)꧗HÕsp¸:4<13>û<EFBFBD>4Qµ® àÏ$KT½“ óé±÷ŸºIðÛ0<C39B>ÿæ<C3BF> ÞtȾYµÎrÜ?à“-Ú`^b_ç00€h»"h—jO?y¦ ÛxÇ¢á;[ယu^
ë>ÿÆãrg»Ãô öA<01>@h˜"<22>°[¶lù<E280B9>…†Æ…ÐÀ×¥Tâ;-ÅZK°Ôð˜Ñ|¤°×÷õhõš>285~k?Í<11>!é»å™ƒÒÏÄ…hij%?¿§N†d&4>c‰£è*Ûþ#ÉJD¦<>ð•aÄ>õq'”F£gZИOHö©è+ä 47
ì¡ÂïÆ>.<2E>_Žé… 4rôb†ˆÁQ5l,KWÄ Ø¨Y
D“¼Ä"äaˆ»ƒ­VfÀIV<08>ÁÚM2ëF˜2ñÔ2äƒ ™ö€rK†]™ð5¸pVE¸~ع<C398> N%·&Ó ªw™ÁaœÆϾð³g¹³˜M ;`†hhMCì¶P<C2B6>û„†ÂŒÕò)¶ í™v§º)¦-døS®?¢™ О³Zñû©+ín¨Örp€ëØ3ùeúÍü:Ë-úƒta¼ü½gtfc÷ ew·EoÈÝ<C388>ÞšØãA]ÍÏ\¶c6*¶OAé‰ü1ÂœîX¬ÑÜüÐ|ÏW«¿Y|HÙ™inòõt«O<C2AB> q»J¯½b4äZÄž‰Õ áú.n¥O=&ÛpT;»ÂØÙödCNŸ s-½ÙÑ,OÚ eÁäBÔã!*ІµÎ>#uÌ»Ã- à¿B8º*n§ŒÙv ¯¹¶ž§ægv ¾_â¼èð\<5C>u¡ó¢ÃsáÌAžQ]ppŒc-ò<>ûKÁƒ‰¬ÜT™<54>×»%k6u³ÈÂH%€ú_çx ó¸ÊŸùj6dl£ìKúÞ}¹ûÚ#S™x,yÃ2¤Â]ÄýÉPÕ<50>¯ü„‡å ã‰ÇNAfSwVµv¦þZl)ãBO”|†¤ÉPÈίƒRvš} ß ܃㮒õ|iyG€á<E282AC>1øäÜ—v¦Q4ꈞÅpk
>LÂÂ
R¦µJ;Àë³_︙ªûÒ¯Æ(ëζí$ ×2 ð½/͹ù§1z<p,ðåvC)<29>]>9€£Ò˜ø…ƒ~C?»ô=±ÉzEÀ©zyÒ¡l1QøtóPÂÅþ—Ý\,lpyúî¢ëÃ_Î[ÏG$<®=f_é†ÏÖ[?³]=ôî2/Ï­tɹo?~–ÌÛ{"¼<ŒDmç^•è:IIPsËSýÚ¨ž3iŠš¿bOo)„!N¤âßÐÌÓh®¯ï²ålà<6C>%¼ÎWÚ(š‡<C5A1>~&¯ºøíÀ¯{a¼µ3M™ËE»Í¢ë 8@Îɨ«f±8°çp¬þtbÔߤß^÷±¹£µÚ0úƒØ“ÕG¡Ž²+ÿHÙ¿YN;æc!É4åW²š¾eì JD1­•ìâ”êËë \ÆÀ[ÜNs z×0*
¿9 HÑ î‹(snû½¥\u…šÄ¼ã3”¶^cŸ©ö¼f.ªÏ“üdur,h5w>Eäse3£œ\[cÒÍ ˜hÛÛC0F~ß“,©Ê OvånzÐÖÙ" ƒóù5òUôï>@÷gÆôo¶oåUØœç> ƒC·w€Ñ
ÔA³ÂÈó¾[6H~ ŸUáµ4 ÂoO»XŸ3Mà <>,ô룰XâPöLm…±ƒø̬Ñ
ß"Œ}Àâ÷›Ó‰Ôë}¾²^|dƒ<64>®;óboÌ2Ͷ£×Í]é+œ]z'nÀ0
æÁ“¯|ÔìÐi¢&@V'ZrÝ<72>´ïo[Å<>ù¾—ŸeW[gþÑj³"“G<E2809C>ȼ˜Ý-gDMñémk.ƒ5Ôj“.8ì¡)®ïà|íÙ~ºs»ßó¨k`šgÆоV„Na6Øv»DÏ,F¢òQJ°0x•âºO?Óilayy¶ž3SÌ÷Êv¹Á‰ÚMükûñ©Öªc?<3F>ãuI“8Ù·-QÜ-ö</
Îí»ˆÉ 
¿Jë½ÝÛýðÂ戗Ì4Ð-OsƼI…º¥Üs®“0»÷A?€IýÛ3<>þÅÌFá~ƒgd¢ý™ypƒ…Z …Š¿åD~U
2;}³1?m}os Ómðw žÙ$+[9FBsy¯¾ï¢OS<4F>7S<37>ªéxv‡ålßÖ4加„NgÂ_“çü4úí)¼“û{ I£?”¥ l<>~/³<>×í<C397>hbá­(ôõSè>èVª±Jò!ä—ãè2ShgÂÑ&²'ùé •4#5ØþÍÅ\ækVÌ¥gÍò탷‰zù&ûOºÓ® ‹‰üë<0E>ËfS:öFÎÒ5@¡L4ŸÍŽæ ðRc®w¬Œý'†’(­žÖŽd¸T†úæ÷=Þãgˤf<C2A4>†!þÀ#?÷éδ}À€'C<>‡¼»9q¥cÏ?KÌrÚ!Sq“c·nøuûñÁ­ˆìèCÑXVž9ÙÿæÈMpŠ”ë‡|¹OÓyç¨XHØnï^Gö~³ëz2xÌ4/& Î–ïÈ€Š{­î€ ×öä§m
øƒÄt<EFBFBD>
ýõ]º+«iÊÜßç§[Ê-÷Q Gg6¸9Ô-æ¦)¿°Œ8ÿZ a¾eL¯âm÷gAøËy¾”¼#<23>Ý 1Ûqç<71>æáÆÖÊä : ŸPZtód¢‰ƒCðùm"¿Ýj©ø0J?™º7ëÞxj™N$wf|QrÖø4ñ`„$hç{(ù(d®ê ^êj¿èkË*)S±¶)Œƒ€Úå¶ÉÐË’ ·žŠ`z<>Ì63µ* Ô¯F”`X<>QEÌbO?Эí{£®%+â~vn”·'ö`9·7—*¾Öú&
ã̧˻_®±s ½ÃM¥^z…¶9“Ãñðøþˆ¨¡¯ì*œ¾Ež»!;ŠŽfÌ5MPüXñXÅSÙFdW«Ý·ÄÏÇ8 Så£ÂˆHXpûžbdVCîi˜áfinkû¼³þ;á ¨å<C2A8>ï¶Hß 4÷…±)ô<#U,Dše©ò[nÖ<6E>;Ÿ®ïï<C3AF>wÿL$«à Èï‚M8D£bhÄß
ŽºÅhØw©µK><3E>õX½¨n9Ô <0A>ý-<2D>Xm¸<6D>.…ÖR^€p-ÿæž Ño¤`Iú=*J(:ᨬ/¶ô`ž= s,ÁÚº\«â`å <01>Z¥ÃÝY!)Ñ{¸‘î*Z]<5D>œ÷uMÇÉØýŒºiÃQ'×ȼD:5&Å¢ó<C2A2>‰Œv3LC ñûþ2,#pKħ
â­=ïÁÞà÷˜çÀ(ǯ7ÚÆx«XLϳ·í‰6<>gŒ(½„'ÀCo>`¤$ž2-gÌÍ̾¡A<ZLƒLÄå6AS1$a2 Ÿ6d6°œLB<4C>ׯN$œÊ•só<73>‰ º`¦tâÅßÏW« ¿ï,} ÚiÅá}!ÿziøÅAøœ¿c ôß—@ Ow™æ*ø&ÉÃnš¶uMùê£p¿9»““®\@ã=Üà9Õ iî!? †L çásJþÚ ÀM²ÿÅ1Ë”¼¬âÅ'žX™F³>GãÞñ<C39E>›ò(ôä+nM¶ hªïnû.!"LÆí˜ohodW.o»Ô}ïnÀÜì³Bü<42>iòÜŠß_‡9Ìà° çɵq¶h2ð 'СæÌŒL=®70û¯ÖÌf4£y9'Ù÷­VÄò6Dp;À®í {w@Ľ„§,§½eÚ¯á¶@¾è2ýSƒ_¿‰Âœ<C382>Òßo@gí:C$žwX Ñ+kI´óö…gœpöªÙLÄY<C384>ðîp4hÕòvõ¹ bM¿°±²ãL½Z†û®³¤„OÔ§ÐäG|ø¶ý<C2B6>.®§ðsW,D·B¿;²ÇÕ“iÿz­ü°Â¦VüT^´íãÛÂ8d fŸl¶ExSyq ‡]Æ~¶ÀSѸúö½àêEfÙÀß…±À¢ïœeÚyÚú­SÀƒ¶ï+á#ä¡ØàºÅ–ù¯EÏŒ±¹ÿ@­•`ø_róôÄ•¯¬ro3€ð½|šÖj6ç¦ñîÜb$EÐC¼ŒX@5Xµ AALŠP¿Cc¢<An.˜òäIû‰‰8>+ ¿œ™MÊØ(~[¿|pQ|æ<1F>SBè <>Wêåf(Ö}<1D>Ñ<EFBFBD>EÖv ÇÊ<C387>Gˆ¡ýg>ñ8(fþÒ•µŒ…'G¬>‡¬¥6]Ôëýî6úZø¸<C3B8>ÎÞ ' úV†1}w?Óž<C393>÷°—ÿ')d¢Šsëòè€9<E282AC>õ¢Øj† jG}>¹lz˜øÙÈ™û´òÛ¿6ÌYÃèz¦Ìh«ZwÀ½[Òû7r<04><>QIÙ01¥
J­Ì‰kݦkx-Z‰׶IÎô6EÃÍŠ
iÛÂKº*¢Bžãôe®-Ÿ„îÆM­²HQë¶z}Ž¨mV+‰ZÜéÅf©Þ6ÛryÖ:Àò•ëêm}°mk?ÜL†”Z=×´M w;¾Ööœ¤I¼l?' l½¯<C2BD>Í”¥ ÍÉ<C38D><C389>þ ×ÈpƒßGh€¹ö†! 2i¹S݆W<1A>ÅÀþÙ »xi»1<C2BB>|P3àfÃÅâcˆœ€ŒuˆgØo!ˆ1iX ªý¡a5üžvF¶Z0gMèÃx»WÙäQ<C3A4>ª†Á¶“½ÛNô ÚpØ,áÕ:nu ÁS<53>Ìá²+<2B>-ÀW`(<28>î˜#û|1<>|Yêi»ÙŽçÔvM
Bç(;]¶þç„cÑÔ8Ž9š <f.k„äƒÂ²±ð<>$\IŒI<C592>-Qc[ÀN¨¡<>×»‡7Ü“íùa=Ú +Z—p‰ñê‚?4P¾ôIá½£™ø‚#=áµ—Ã#¥/¨‰^9)>RR™ÔðŸ3¨"~íˆ(êUüòBU|õtJÞ:…žðÕÈ)·x±NT‰
_V¡K@^yê¨jqæ¨ h àÅ'
eÃRóã<EFBFBD>Ôªuåµ:— : œ¿¶Ô²¦QöªøbV³hJñÕæÙ½ ¯ªüNbB1é±ÝÆNÑ=»ÈP†*`0sW¡hUÿ©”X!€ž“0Ö<>ÎC@©z<10>ŒŠÍé}Q£îUj(dÆÔv4Ü3wÞêPʬ"®lÆÀÀ@GBbÚXíEV™ÀÛ“Þö0_Øã'±w4*He^Ö}†Ž†r0ò ~<Æy/W& 'éBï BöÂÄ7D<37>'Ì¢ÌñÞèqáekø'ù öX™££Ž½aìâEÉ<45><EFBFBD>õ˜ËaY"÷ðæÄ“øOòUlŽdð;ìHuFÂãxr9Ró½/1?ÿà…ÿ„Äý©ö>ŸFK˲¹ù0„û`Iþ©ÏñÐ Í<>6ùÁèòXàF¸Ñ\&Œ¶ç÷¨Ñ9_€gßS·ÑqˆµŒÎ§nÞè"ž|„ç¡gEøƒÆœí%@ûè'лü2<C3BC>ª'ýÙˆ?z­ßQVŠ¡O‰ò€„•|eœ»çÇt5J'#åÄ›»¸}t
Ôç;/öÚŇÌÃØkÏ„7,ÿíiƒCÛ,h|0:;ýŠÑqEŒŽ±»Ç<15>¿ŒÎÁ$h´-+£mŸ]ÀÁÙðÁœMø) zò™AIFË·?Â<>àÞmü²ä}–¤)/iåöÐ/e.÷Á×ÒŠ˜Ü÷òÜ@j4EÅéõ¹ŠT O ÅÐU xó®ëO¿X­ÍäÞˆQ&<26>úÊÙÓn-rp$KÆ{a2ÖÀ¶ÙÚ(aíS_ƒyKM¤ôðüµïY¬ýÐ{C kÙ¼yÞå±&Œ´¹ÓCi˲Ã}y°$c¹<63>G9¬Žø8<C3B8>2;ïÏA V„!ôÞ‰b6T—Åj*þDn7þxC+QœŽËŠXï}<7D>^<5E>Yž2à ô&D©DõäÇZü}õåÚ#8¯<38>íѼZÖ1kÃbAX4ìÔú©ŽgŽ°ÎÄSûA}å; ˆÕvÌP<C38C>¯ÀÞýêǺ+?Âì1bk虈•°Ž©~ÄÛ•Çú\°C‡'+@# ¦Í»lQ k9åÛxßå±l=G|õú,ÕT b|j«/Y¬!ó]0L)`í ˆâ¶Ý°Þ :cqéöá¾FÎ_e±^"ű޻Œnv^ ŸË¢hñ˜à.(mI;iˆØ~4Üò`Í-ÙðNŠµVyþb±ö\VÉXÃA·û]À
9 î{¨­_ÂòX+¦C¤ÖGd±ÖWdY+@Suÿîœòà |8ˆVí÷ <20>µêï׊ŔQk»²¨Xá܈·žBío%¬y¢Cô£òXk.s{2°Çå°BaÓé?ЊÃíXÉÁN ëñæJe°•CXépß<70>í¾"ÖUÀú"¾†ß<39>ÓLϯeìîÙ)Kä~¿;Pĺ%SÖ™Ö'â»PË#¬ˆÓ¤«öÕkêî_*rX)êÙef±Žü6ÉâqDí?ƒuhÚ—¬ÈäHcÔÁŸ# bçÖúƒõ·_è¦ÖJªybÛž“źŒÚ%X簾)Â
Ðø²=oE,+šDáù£±ºŽåⳋÓ<¹½”Â…Â<E280A6>Áš²œV(¡=tpn™8X å¿<C3A5>åªb¹H:«çkèÎôÐ+—ÖG£«Ñ—­ÏV47±·d¸?Û‡ÂÁšz­ÕÄê<1D>ùg¾EúŽ(7røSß~môÅ6#ÉSAIû÷Fÿd½“=`'$•žR€#ž
O“£<E2809C>¨˜Gšúý<C3BA>¨¥ý>ôôXÞ¿§ˆZ3Pzš!j“j=hdäˆ'ã[DéõQWšJOD+·ÜKžb͇“huÌF…×?ÜD;ýdUz$^<5E>¿nù§ #×ÍÁfK­2Ñy5>0OÅ+>}$:3WJéi<C3A9><78>fx¢7x&ÞB…œÒë âÝþRzúC|ußí§Ñ>ÃÄ×ÏÔ©ðúg”è¿ÆýJOÓÄ 4ˆ*<ÀÍV¯Û^ *4Z½Ñoß“ÂÓٽ¶¼ÕÓ£2ÑÆ[ïóÒ·Px}bóö~ª÷òOƒýM;žØ¿+ÍG7ŒÛ#²µ¬ÀÓ±ÎÝåty*•‚¾ýʘ<þÈ? Ø>Q™oö¢Ôàˈ¯ØÓœËÞd<C39E>B Ý’;ë¤íÈnégF%vw˜óVø$³œè”øoH$RTÊ{—öì-¹b;î!4ÿ^H¸ÁoD1—u<E28094>s¹¬§ê¯µvìkßw+ØCmö[SýqÍøC¯»Õ‡ D=O<4F>ø™Ýò‡€¨<E282AC>&xÇÖìY<Œl`)š
ÀŠ5D¢2ù, ç ՟ĶDÌa Îíw ãVk ×UÆj*N<¬P­ñˆ‰b;û¢€ÍõÅÓP ë·
Ö’+*¶&÷÷f+EÇ4<>Õ/Â)ºóZœÃZZ‰(l²âX­[€F@L¾½nE¬ˆç°<02>øŸòX½Æ<>‹ˆ¼ö)bE®…"VèWL¬p4"Ĥ2ÖÈóKW+4R) Ð@;e 4Ü—£©5Çœ,~ô‰e÷˜z;$lئq] Ÿ5}íl ¦ŒGP ‰ãI¢E\<wÞòcWÁ°™E†!#g8â
k?íu¾z<C2BE>üŸnnXCƒ<43>'íð…õíØ@M,v—lÜoa¡¥ßJã2
â ÞÝ¡?pJß$Þ‹¡Î÷(½Ë"jÅ!|L±¡©Òk|½¿cÿ »yÁ<05>õ@ã<>¼·ðE—ؘ ;f¬ÓÉÂûÇù´eÄxœpÆø0'¢!Nú¬gNæï CƒˆŠz·ø&
}rœÌ–¤^,”ˆ¹¸ á:<08>þ°#D®»ìFa„Œö<$ÿ§‰ûH2sìï'­9<1C>ÅvŸq™$ñR†µø™>wQt<51>dÜq"½”9")jl/Ïó¥ÞN<C39E>¿æbZ™¿ˆ©×ÔÓA}uÒ#[P_[u¯ 8!Gznn´é%Bîíê9w" üøxˆæ„<>”0|§ERÈv<C388>xSqyöœ{ű=¢ýaɇBµ2ä+|v¤Ë—ãòË“ñŒd—òVk:ÇÌ >>Ñà\fõÁ1Ȇû>¹³%vòd.5ÖиUÐO¸î™qÉó|¿@<40>ôòM}Hh4ÇS&á ¤3øNøåÙ½T½—ò"N³•÷\ƒ6eÅ GCî_=,ñ Í8@²ÐF´"4EP¸/@ïîhŽ|¿:F-·úÄèQF¬**:8X€øääÅ“ìÞh<,ú¾Y´Mågƒ÷
³
ÿ°Ýg¢ßÇ F(–‡"Pάõ‰q<°¾9ûFLsî…aÖ_ä¬3a†Y¢‰§%ð{4-Ë¥®ôÔ¥<C394>H­<48>qui%M%kMŠMIa†KË,O±¦Ò=É“wxk)ë0ª0“Ã+·E é5jÑëadUïrØA·|Ù÷è£|·x3OÉÖ÷ɬ¦=…Ô¡LI—·ªÖ†YË#À§QÃÖ;aL<03> ÈŽ(fnN†&V0ÚÃEç´©å·{=¢i s³¾_<C2BE>hév2ÑØH³Ù'%Þ­/Ûí+Ê#©å,˜Í¬è÷wV+5[W^g—áîNY]¦áÖ·¬´4]kyáwÚ¦03LñßÆ«µ¤íê¸TF£(!N˜ª<CB9C>¬W#ô„s<Ô;£%4{‚¶ðºßÅdQ¨' CKüÇد¼ÎÚ'iõ'ç<¢­<51>BáqÄF|öì–=ú©y0,èÇ
ˆ-/zc<E(-”ŸŠÄ8…šÝõioÖ+
$é<Gk¯rQÀ(‚ŸŠ~Q >½Ã"7Ât´É‰„ײ¤œØ
Š@̼)k~ïµgŠW!=÷aV§¾©‚ë<¢Òç¯"G°AHýôR]ìrôBPPú˜^'.v½Ä+ÝÍ©5Ñb_?Š»‚§òßÆL&]ÁÕ ÃúQÅ<äâ™<C3A2>Çìæe"
Zí@.RÒ1¯åˆÊúÑŸê´Ÿ‡„<E280A1>f,Äý%. „°Rà él/TË⸞hªb¯;H$Ò7<C392>6ÁÓÂ%
±<EFBFBD>GIlG…¡Íò! ±™<C2B1>Š5ö3ÛŸz½3JèÏgBªZÚÇó°«žc<>˜µR¯^õ°¡ÔJVÒ9í©Õa%ïªb½xæºI½>Ø.aw†¡wU©BÔÆÆÙ=öb+BlHÂhô é.p¦(À9­Ï€0ÍûÄ%*+(?3H¬ù4ôBÃYº2ÊÏP-ئ.ø”¿Y×Ú£ÆVŸrø¡Û:swW©±Æw£ôh@•ð„ÆGiÕ×<C395>Ž(-€¦¾
õIh¸x¼—†ñáÌÙ¬(
;…Ú€ì'uG~‡r\e\¸es —Žî° 0êºAs_ Vy¢q±ÍvìvvÛoO`Â@“xfg«Ø1Ñ®Î<E28098>BEjNî}zv²¸(‡Ä<E280A1>—“nÀµ Ú$Ò üÓaÞ™ÑÆŠ†yÑ}½Xº¡¹çIö½št dqê€vtS0 `@Û±€ûúökH<6B>ã„
eNÓ¤Gœ¨Caò¡ãz$‰vw<Š¾?rØ·ŽŽ—Ž"a2“æPÛSbw á<1F>`¸ˆ£ŒÊã– tŠìÒœ1ðžÉЕѕÏ*ŠÙ:-¶nG<6E>¸¼ö<{O¸B¬SKèvô&TÈIA
”z;u§[‡™<E280A1>fNœ0¥,l4)þŠP$Ú“tiV…w»Õ7ò5éÍiâ3S×#v^<5E>[PŽx€ØtqÉ!Õ<>CSM‡Ï£á{3Na÷íÖÿ§r„`ªëÖ<C3AB>šªõ/§•"·Ú™Ö?>¯^Sð*jmhzºŽZ€.×FLñÅÓÅÉI¨;Ê‘=Q¶<51>6 ¯¢~TWŽrÂÆkr<6B>®U”cÌŠ²»¥ú1f=ÑÉPH4Ì<34>ž|ìù¸€Fj<6A>Ú¡(<28>bVe—Q-OéÊTÚ¾ęF°Ìvò:WìZäêá5É+&ZH}6Å»¸ª.˜ÐŽûSnw
»ª1oåŒ=¥>al!Žuj¯397Ηíö\5%IÆЇVtãzjj
O<EFBFBD>ƒ2M&;NJlw.<2E>´ÈÄ1­á<éÍÆ jssÒö„¦ÁË'S|Ùžéþ»Cšèˆ3tæôBSXö#Z.ÓCƒs†fþ%ê“r4YºÈ¸¹DŒðå1‡ÇÛ²\ÍΪÛKÖç¥Ug8È'‡B1zîÛÉFÕò4×)çs¨ÖòÝ`EK•óáZǵ|7ø1—”ó©×òÝ¥—•óaXejùðDåÊùÔkùnðÒÅKÊùÔkùÄÔå|êµ|7xéâ%å|êµ|7âÒÅóËùÔkùXµvy9Ÿz-bèk”ó©×ò±£9­œOœ­R€ÇÇø-<-ƒO¹ìWGŸôˆ´rÅŸ¶ê;<E280BA>—îœ<>ƾKÄ ËIùë*;…yq@ê RqõmGe>Ç©l"§L­P5åOv§PšV—Â<08>*VtñéáQ ëÂÔ W™>aùi*ÝÒ
t©ôi'±¡O­ß;AìôáÁfA÷ë{õüÖñÐbR¸dƒOd@¡ñ]Þƒû4K'ŸÎ£Yvwj~‰\\Ý<E28093>i>­Ó<C2AD>Ò*»ÓŸ_¢,:mdRLÎòRCë-Ó“à•ù¶³E˜o4
1õɯï LÑ]÷:4 LÚ“![ôz$¡‹ê•'Ú`ØÒ§ ETmâX#4<34>âˆJ ¦ I¹ FNYÕáåqºêÑ"{-㔚Âþᤂ•šBA&W3º°—r<E28094>©X„#5³µ2ïKj™÷õ&‘֘ö¦P+ó^M¡lRú ^BM¡$ê©ÅL…24<32>
½c<éŽòüÉÃÔ>ÆàhÊ{çM£þçD¢ù/<2F>3‡eŠ¨˜,<2C>ò]fnY*)Z–½¾Ò=¹¡Í<>?ˆÖ¹WSVÏÊrþ \έpŸÒ(>ìj¾'<27>`ÜŸC I¸H\\~º•È¢U²§0K¸q[>ÚXQqÔËþd×­D
hRDw B1ª®U°w¤ïì8¤€dHÇYªbL­„Àš§¤•º_ôl/Gõ¬ÒKla*04P¦'8l¸9¨Q«§!€”˲Ž’“ζÓ~*Šk_Xó¼Ð$•îu+—â„qÚOå´X<C2B4>JxðÁ‘ï©“´kôD}ºá9•íÖIÑ•>¡$ëp•Zôæ†?qL_·NÞHÝ(qÜøa/<2F>ÞÀ²¨K¢7§­/ŽÞøocÖ[9OO$ltz<15>§Go”ãëÇ£7`p«Jʈþz8<7A>ѵs P=Ü¥ÑX çÇ<C387>çæ%=êˆÞ°ëF³VP9z£«:ˆ·l <20>N*RM¼ðH é®ÒK£4V—!]U̹¸9¥B2ö¢1ŠžÃqF—?õÖQ̪ãسª<C2B3>¥«˜A¬kp6½\ŠâiòŒZÕ™ ¡UÆ“!ıÎ3Kü´%.ã{j”øé©hU]ÁL!¦JœNOÚ F ¥°©„¡•V²$LtCü¦Ì˜öÖÅÙ•yo<>+λvežzÚõÕ*ó49í:•yLtð¨8ït@ê•y'$Ã^R™‡ÛÐXqÞµ+óŽ<C3B3>B½‰'Uæ©¥ZqFè*ónð <0B>dÖã•*óØÌ{iqÞµ+óææÚ•yÞšÒ¾P©³½Fá?TyùëºS/(¯N«SGê%€æ»†4oéØô5 ð„Ë×ÊQò›Å<08>ØÅÖìŽl̺ø…]Œ¸öÄvyNJ±V<àX6ó^WŠ5°Ò“véz,|æÔiÈæêX<C3AA>½»åc<C3A5>SÖ/úS¡-¿Ò¤C¦)¦B[~u¸ª˜ê9¹l‰“Ù­s¥£Ž ­³pô¸¸ЉëQ^­u®²;ŠÊ<11>Ùè4ü´ÑIC:$1áQÂüíd·G#®ƒj*žÏ¬—†oW-}»jìÛ•Šd?¯P$ë5¹®S$ ]§Hº¼HB¹J,,£Ó>èZš;¨XFŠÊQ°ºò—Äé<PV¯ÇuqŒ6X]ÞŹBrEy"µ¦'WH<L½Þ=67ŠþŠò°åÉÖåý+Eyçûž'å)ï¯Z”Ç&Yh”k^\”wƒN†í«ë<16>܈V”QŒqÉ}ÕW✠íÓïoøÛãd&YzpòÙG±Dû•ÝB:9ð ;¶—µûαlzÒ#”µw&nKåa9<61>Ž= µ“‡¡`÷ x£T«]u-é“ä<s1[ài×êé®5ŸEg<45>È Ä/”·6oÃFçü§o“‡•Åðâõ<C3A2>Ñ^<5E>{<7B>Î|ó ^¼Þ‚¯·<C2AF>öv&?5`ÓœÑU Owf•ÓÃv‰wšO‰¯Ðc*ÂpßSTv×ð8¥Ep´åv_1+.»³ŽnKÅb¿€íS­ì®ïRÄŠn"WÀ2£Ú¬åb¿¾ZÙ<5A>1¨ŒÞDÎcõËÜS¨\—0~©\+÷nbX%WèÁÛ¹oýs{ú;S*Eë©•ÝÍ Öñ=…_eåb¿»¥§3RÂ:TÁZ"bb«SZÛ8{4+b5~šC¯Jû™Š•{ÉÔÂuëBøÑ'®(ð0ÑÕ®n&%ÄJMMõ£<0E>Ôa°ÄO‡£LT!žÆ-bðºÓ*ѱj1gíüÞcã¶q¿æçKã°gÝ0wzíãTòå;½Ò²gQ+gt)i^Qs¼&¼ìz=qŸÒÿk]¯'7}zÃv éâL,@ü×¾mE”;¨r·ôZ=nÖSá<>ö]xZ­èá<>ÜipgWÅêºcE/á5®Ÿ8aÝÓZàS©ð8©ò­|SxµÂ>9ïãÔ˜<C394>ŽÂ>9/A>lwQaŸ\UŸ¢ }~aŸ\ _³°O®ª­›ëöɱ¿·v½Â>åp÷U ûäÒS0 }­Â>ÍLÈëöÉíó(ìâ^RØ'žW¦ªOecåÜÂ>…]Ükö©T¬\³°Oï]öaùþ|UŸüöêE…}rʉ‰@]µ°O®OøÖ÷•
ûäªú$]×(ì“C~Ý\¯°O·Y|ÅÂ>¹ª>¹Š• ûÎ&Úi…}ZD»RaŸ\Uß©DÓQØ'WÕw£PéuAaŸœMz#­ú¾¼°ONö`JúZ…}r-ŒïyÕÂ>1€{M÷ÌÂ>¹¹VÞX9»°Ÿ%ngFMßœYاà{*RäÌÂ>Œ¼<>y£\PvnaŸÜ<C5B8>n°““”ìUØ­ }D&_ FYóšv‡Z¡׉Ÿ
òq7êüê+M¹!19®qŸÉq¥[ü2WøÉšúH¥y0ƪLRù²<ÍR}}|Y€Ôº³×®oúæÊêW>Á_íÊ=­»zeûtœ2»¥gaëêç…ê6*¤
<EFBFBD> l]&ÿmté»L<C2BB>R—é8±I3Sð=%÷ÿ<C3B7>Y<'„™óltÚë:/ÿã8M}
.¾ü<C2BE>‰rhÝÿ§³¦ïG„_tùf§©Üÿ§¿TJáò¿Sƒ<53>g^þ'„”Þÿwb½Ôñå7GžËÝÿwF PØ5ö\<5C>‡bR®JñÜõj<vÕëœIŽæ&örq5QU=¿CH¸Ô¬é“Ëp<5»NüÅ·U3<><33>`4¯íSÎ Ñ]‡nìÓH Ó—¤©ÖÇЊ÷<C5A0>‰öOlÖcwŠ4ŸÈ[;iÃFõu×—©¥Äƒ®Ôc駤Z!<21>à:©VèlË9 Ð\O•žBÌ+ì¤ ("SVŽt:E*'ŽA@g-Å#(Àϯ›“+íqhʇ$ê¹¼^ZíûCZ<43>ª}HíÝ(ýh?¤®ûeì>™$rÍ®K°aÊLš­ ¢¦Ž¹Á<C2B9>MµÍ)`ŧlw˜ã|Õ÷=u sGù(gƼ^õrÇ×ë]îø*)¡8W
t¶'÷
nº‹îC€¢ž>z
 +:ï†o±©d#<23> ŠL Å%›ÅÀ&;^<5E>à7™Šs÷gÜû'_ýwæzÄ@]ñ®o­{ÿtšê—ÞûÇ™êŠWÿ<57>Xú£pïß©.î™÷þɨ5ñÕ—Œë(fsÚe}RhÚ÷þIĪվgÝû§ûøöÒçïÅõe<C3B5>€Õ§qò†þj_¯IÖµäú´j_¯é´ûååƒ)<29>椎ºÍBL®ô^ðΤ+ø¥‘Ñ…]Ví 0Úóòjß7Õ³íÒH{žR}”µ£ëAe#­¥:ÎÚ<C38E>ed¯êM6 &—gF¯²O,¤ê¢½*eµ&_K¥V-7RŽö±œ¦ÛÁÐæ:r¥Þ½ÂòЖ:§ŽBª.rñ¯ä{(u?ÿÈ÷TPR1«[%cRÓb”†@·Tk©NµGÌÆ­ øùÇÏÁB¹ v¼x÷ŒºÛœ[9•O
73ïU*Ó.9Uë˜hW» sD [¹Š»Qz-›œ;¬C<C2AC>Þè¼@°;ÐQªânº§ïj—a>©ž2¢»îVXÆÉàÓF!; 5š¢|{XÅ >F{°Ô° ÿ¤<C3BF>Ž±ûæP<C3A6>¡Û_ÞòÓw/!ûIT#GÑÞ;JÀŠZ|5<>ÉÊWæ%L÷Š•yÔáÛ­\Ñ$|3éM|áj:™[ùjµwµ*įĿ‘"NŸ[ŠXï½<C3AF>ƒ±Ö‰b<E280B0>@CGž³-l¸â9Ú<'•*ó"åäïíš«¤2φÕÞð·ÇaD~ð)b%ŠQâYkÈ|Û\ûúrX8\•òGS(c-9¿;ŠX-¤5<Ç]Ò’@·ÖšE+E¿&<26>rX¦1ýåj¦ŒrøÑ'v2l¹÷ÉZO»þa½ …¦!óo¿Ð­k ÎYîc',ÑùÌHÌQ,ɺ>ŠÀ”²Îâ5ŸŽìLÖa—ÙQúÈ_åÚ…<tÆŠM/­l$•[ùDæ°ìµnz»¥7áóx¯L¤ÀäÕÏÒkç%AàitÇ<74>>òš7À` —ê¤:%qK«<ήè€<C3A8>V§” z#9ƒXW<>˜¦œ5G¨™¯·ØäêëF k§éëò=§®¸r<>ã<\±.s¸'sÚ5<<3C>ú9»z#·=çþX˜˜h…ÏŽzŠ<7A>ÞMðÂu¶‰z.óâi…kB ë¹e|£t°™¾D½AP6¤ªX¨q©¾zDýukZ^MA5L_<MœŠâ=Þù~ÕG­ Wô¤G~“žÉÐQ¥?4=arë8<C3AB>çăº½Tþç97L῾Ì2²“Þ«#<23>I*)7̱9šuvÀ¤Sк
Ú° ÉIIݤ;¡ -r<>¨“K*½FG1á5Ft*×Ù)Å ÃTPGQÒSë±9”ìJÊž/ »¶TóÖŸ£9TÌZF(ÍR!ÝAÙAS,¡½áï[;ÚÉŒ¯F4Íj¢“ˆv­óQгˆ¦RwTÅ,Þ,>³$Q¯Ùˆ×ßœQ(^žÊõˆ72ÇœP(î„r=â<>ä>éKõÖ#rŠ@ÑGRqÚEƒr{k'”$*ˆ½£zD9‡ý„DmU£”ýpRI¢I<>ê%Ô©%‰ºÜCéöªxH'U.iÔ#*{ÒõWùq<C3B9>uY!.þÅË
q7ꨶñz—j…®tY¡ ´åÇJeÚ¿tY!R=÷b@½—òa»÷²BÄi°[ÿîe…xæý¿xY¡|øv”½vÙÕLw´NµÂo;äÓà.½ð<C2BD>cUùÛOͶS¼ððœS­Î¸ðPqp×8ÕJ¸ðð¢ªÞ\áT+tááNµÒs᡾zÏ/<$ô<>cè³.<LªÞvˆ9—]xxbAÙ¹JÙB|ÛáQ̹ªîFõ~Ï.<T¿íP%ÖyÚ…‡êCbLõ+\x¨™ {<7B> Õ/ceÚåª/cÞT—@<40>z᡺æ“Úig_x(J:ºíPÖ¿9çÂCõ¼,,íú² u”\ãÂCõÛy}sé…‡'ÞSxî…‡b(ÒÛ<0E>Äg^x¨œ®Æ¢¹Î…‡êû1¬L»üÂCÕê&WýZÕjÊ·âÁEªïó [ધx+FnO½ðP© ߊ¸°ä#") QrAª]xx¸äÂCžÝdo;<ŠÙœ{á¡:”Å{
//¹Àn;¼¬”äUÑC—ì6ëKíÕqá¡zí ¾¿Ï»ðPýäÁñ¸ðÂC¾ðK¶bû/ZÒ_u|á¡™v<E284A2> Õo;¼ÜTg/<¼B9¶ž õ¸¸W¸ð<C2B8>ƒ"ïq©µS/<Ô¼¢PEØœqá¡Z¡Çç/ë{^~á¡,“ñ·^&ÓÞô?ø¾çEª?XöÃe
—+ >5:¨xáá鵸g]x( Åê+'\xx~F—Ì…‡çêß°_x(Z<>2h®pá!WU'Û!+Ó.¿ðP=‰Š1n¯p᡺wÏíj…Wò·žëMzááù¾çI*ÉCæ¶Ãk¥(æÔo;¼‘­ö…‡ê·²&ÇåªéÊ+é3.<T¿íPËNÓ_¤«zÛ!„¼èÂÃs,3.<TœM´$ÞúVƒ¦qá¡®¼ÎË/<T×(¤*UäþØv¿)ë
VM¥‘ðá`β!¦¯1<C2AF>4" ~S÷êùþâ"J¦”ãª<C3A3>&¨‘€hŸÖ±Á‰€êø:¬¡i“cI
@IdŽ¢ÍEÚ³O<f¼·Ï4åQXÕUv´CÊx÷é¸7¨ÑÒ/Ï<>Œ#žü<C5BE>:ñfÏÑ^,·D¡ðã!
Q¬¾d "(n5¢ôuµÊ󀨭§3¢¾"çDëÉ%Ú•E‡x¥çS¢CìWD§ÿp Þ\+ñî|µ_#S<>è÷»SbÐôï‰o×B|?Y_(Š*x(ús§ÞM ¡ƒˆ•¶„ï¡<C3AF>ÐÜÃjSk1)×ËÏÑiºóõ>3Úïïº<C3AF>ÛH|•½k´J<C2B4>÷óå<C3B3>Éõ<[Í«ñ]%½¯<C2BD>~ºù„<C3B9>+J4œ»Àó7šTÐ3ÅvûŽ¸%'àçÆVVœ°sƒJ^i¸ÖŒ®l/€]<5D>ÉÞ°ËJè…ˆ‰Vz‰E¿Äw¢o£¨g—CîÑX¶®#á·¤‰b¶š%ŠÓñ#Qª?¯is?4Å´Faò·àxˆ<žâÀxÃTH¾…Nx€î?$<uÒ)1ÂÄËHt•á·u-Di±Á ÚH°˜úß{£-ñ0º<,Î<11>ÑiK§áo<C3A1>éÖ)£+ãŸ<18>OݲÑþkcþFXÖfîõ9 ½õÚsÞ8@“[ =^8´A:^3RhŠßS|jí¼ÎÙ, >u~ûOx&7zÝ]ï Âêö ¯PBZÙO?¤¼Q¶1§_³nÊö—˜Õ¿ºØ¯µ ȃ}a¼ü<C2BC>ž`…Ȭ·5:S}{ûò`eû™ Û„h ܃ˆ{6¼Í=pÏr.á<>/ûþšâT<Â`“Í2܃º—Гv·"<¥;ú ¢<>?ãØK9§ÐG]ª¸Í<>v Û><00>Ùæû¢àAÓ¾úS¯Fðµ¶óÈvD,Ð(:a+;0ÁÉ,,P.F€l­E€pªƒùmÔ=0"íDº|}EpíÌy~ÏSkéï¶ýÍ 3|êÜkÂÓ«¹p¢}sh¾Áì‡Þ]yÏ0qm,¯ÅD!ðƒÉOFºº/¸w˹¸œ6®E9ùùA,p <0A>œˆE]´=F^¢R1m!ÛùÊäÁÈùž`h]/ÇÆmŸ#ï¼;­åJÅg[ Ì,“õ†~èCœÉFu¤’Ð5¼__=,÷<>º„wT«ØÐÜŒ>¼ŒEìõ}ÉhÅ·Ÿ†~îÓ$€`°ó»l"<22>.¤ÑÁ×®‡}wùApŸúÏ<>€F­%…幜ø±g}£PØ>ª¾ï§ß;v4Ëu<36>Äɾmš‡Šõì*bŽ`a<>dñ°BŠ˜{d+ó…‰r>³ïºy¬ceö}Â[ò¦­àÓÐË}šø„v@g7Á÷?IwàhæÁ}§h·%ãáŽËôTü~º‹"Eë¿Ðaâ±ä‚ùú
9
<EFBFBD>n9Síf <0A>ε˜$?MÊB÷4Û¿Š¨VºðBæ<02><>o]£íùÝÊ
ÑC ÝÊ\5Õ6Ú«¤Ñµyù„5*w´¿ãáyè1,žŒ­=Öyò“m2ü…¯G2 ½¬nðu°ç´l<C2B4>鶿Pt:XÑ ïr‹ÎÞÖ <0A>[|–´ñ Í>4)<29>èÀ!à™&*žEwïf_ <67> i Ó
<EFBFBD>raªó\=føì  SG¢- e¹NW<4E>H~C Ž¦êq$Ã<>rî{™™ˆDl<44> ‚£* bT½p4üRô²¿!0A³:xölçGhcªß‚•ù ÞüƈS$D<>÷“]™6EoÙUôB^24Ë©&z/vIàñ/m0̤)ƒïÈÚKͱ¯ÆS4S…`®:ø7Í~?›ŸÑ/±ûm>7“ÎJá-K6ÓÓ¶÷<õµŠQs0YŒÞ6'¹¯Gs9~˜ƒß :`<60>ÛKœiµL >ðÜÂÉu3ÁûûµHòÝÞ¦c…ßϘ?—dÚ¹þg¡ÜXxÓ¿O&_Y/èb~åͲ&D ¿àÌ;ú™=d3HX¬7 Ì,âc¬fºàX¼Ï3íÎ'£Ô2Ó:çáßr³V-îÂûÄ4GË  ‰˜…w·ýÊAÃ#:Œ^}ÌǨß|y·;ñV쿼éø㲩>`&-Œ9jr$÷F‰£ˆ óÁe÷¸fÏ£(
ò+dŽÕš€àä"»´Z_Ô<00>êT<y&êxõ6sÕó¥ÒÃ2ÓÎûO&¼Ï—íy+:¨~MNÛ[Ü<>rÖ"ö[ÑI6Pt²<Wø,þòÆpÜŸjïÓ¹EñžJw,Ýßô¶LõŠgÙœ©Ï5@„Àm~úB7Ñ¢¤<C2A2>_<1¹ÛäÁòŒ½s}8‰æz Ð`4·Ïè;äûøòãzJnôg
½£¿€Ýš“F/7t†2æù@Y;Þ[S«™šH¸ö‰üaXª™Úªg;UÍ”ƒF
HÒ|+[ð•ÓÑBdì,PýX?þ^Û$K«N÷š´‡ÞÍÑ E¶ÃeË^ÂøŒSx¼ì/8»\.»´l>óî°…F“Y@粿DÎA¢i<C2A2>ùb9ÇX6ZcV0`·Ý-ZP(Ô’|Ø r¨™àýµfš‰<C5A1>l\•dÍ!µv¡”×±æ šK¥¼µ~§ šýtÔŒSx}SF<53>Z»È”¥5»õ­MîS|ÌnŒ¾Ñ5ðKF Ãv2KìBN;²âš³E
´ Ýj¡Ä}:yQ8àÉ¡3ó²F§¯øÌ|µÙ³>£ë³Ð‚^` :€UðÀEÁZ‰±<E280B0>&N£+âê<C3AA>B6N1ž!ïM NcÃlæ„2ö4²§ Ò¸˜¾¶ÛAÐùM¶ÞHóÁàw¦ÿ±æ+¡G0#VS:*ºù@ò¯0 žùÝcÃ|¨tàúQwÈt§úPóÐ6¾Cõ¹œy…“$6Æûù\¬¾ñÀžŽÎywóÊM6#½– Ä*±,N6ªP0å“R§tGR@Ÿ>×mTÉœÙú>æzG}¡Z;É€RЬ—áQ·W1*ÔQÃ<1A>“݈Ӆíuv£½¿ÚñÎU ýùnÔI<<3C>ô<EFBFBD>´ gj9•X<E280A2>œÃîÛßã+횈cF=üè2™ð¢3ßôA%Ë»5<C2BB>ôÍ*P4:“Gø atþîÞ˜mÇ‹)5JÝhûžFØpä`âgÎwý·ô͹¢È^ÆT¿¾O«Sß\Šº0ü.,Ó_Ï7Àß„Gè<47> m·Îÿ»úæTUÇØZcV06<>Û¼zà†Ë~¸Šø†DÕqœv<C593>¢ÉOïwfh\<5C>ƹ¡sùrÆÕE•xA8e<38><65>Éél&äùa3}¨esÂ:;s±ip×,j2í“RÇ€qNÓ³ÎÎ5@sÒ:;sè8§éYg:ÜÛëü
A!:Èe¿¢7sY7á+¼e{ÏçŽHgÆ/ÏÅ|Ê|Ì8·1Kþ½¸ÿÊ”CßÏéi{/P_£z<åíDòæÉ¡<C389> þb;î ÁBhŠã…— ­=€æÚ<C3A6>z9CzÒWŽh覜¸9#Z™µœÄý÷=ida³ÞÚ5#rCÇÓyþ'‡s²š3×FnÏÞ28¶µÏˆ§<CB86>ÊãQÄËwöqWÃÓøW#·Š.ž8žæ½ žf½ýpÃØY~mœäËÈ-wNIKW¼×Bîsóð©s±ÿ:ØtÄàܱ¼˜”&1ÏRÕ»âRMûg¦ýúÓ×%çÐ'>w0V`[¤ò˜¤Õ¦8X®ìÅÌjø£Ï<C2A3> G6¦ÈZÁENOå95·0~Ø4ƒ)§3œ?¹ì÷•8Ê´ðe»Ã½ Iíý7gŸÛ<C5B8>ü—gM=¼ÿ×fM=@ó/Ïþ ÛD—Ì>šz>£ë_ýSv
/˜}4õŒ°ù7gM8
ÿ«³¯Ï¸U<C2B8>}ÆðÚŽ“³p ͤŽ³D³ŠÆàÞâ …ê¸ùE5GõuØ<75>ëU˜”ô4¦¸ïƒÛ"† ýfÊeLü^UGtKÀ&€ÚóÞ¹(H`8r·©` ú¬Ñž_úÍdJ¦Jœ»µãã‡o*ï(ÏÆôH`t»<74> caÅŠ* ‚¥`e2û
Ÿq—ÿ>þážYÞé-õÝL%#ú ÌaÇ<61>Ô=€ÙvŸ4—8ØK=(4Bxñ¿Ñ—ÀŽø¸þ3éM<E28099>„ÜàtOpnO¿Ù‡ØHüê¶YS_ «<C2A0>ó·cÃÍÍÁ(äöôÇ=·<m]øÀÌí°;â«×gœ¡zÎ K´¬­êànî)¹M?¯h}1×^Áüy¾;+<2B>ôÌE­ òt ¦0tøÄépø¨ótèŠéàà§vnÿ ü#Ž1/F„Üû¤ÏÁífÛ…)M:Øo˜î¤r¿øªbÎ<…D`±²×´hÓÁ]z¬þp}ÉÁwðê`64äX¼kÜz
¨lïC ƒutÛüT`#ä]Âø-á&ŽÓTaØÔah°s!Õ ¡´,µ•as^Ú —xyžíc h7¨zU†çÒ<C3A7>"ÑykyOâMèÁpÚN_çbv Ɔ> Æ<>Ó©CïŒ8]r¢‰8[ÇŒøÓ/V+ `˜ªKƒ<4B>ÎKYËI(à\\m^ÔT\éŒeãôq0Bfa<66>ѦR5'Âå± ín7þxƒ»†oí·“r‰§©<C2A7>Ës†ršGQN`w<>,— ‡]÷Ú÷œË%§yˆK¢,€tsšÇwI'Nóøµ—]9ðð”s00¨dÐ/ê„OCqúF<C3BA>>ç¥<C3A7>p‰•ô93âsë` €FmF|zL9!Æ .WÒ>¯ø«Xøü‚Ç$'¥iÂ|pJŠ¥íTì½@õÝï™r蹜ΠKŠ†˜Òkþ
L..ófe|Q™O¡K°•FurÃÂu—.ß <q¢Â+T”9*Às#×ÆŸz51ÕT\<5C><04>ÿ=ØÐW`ʦ·àkÌÁ…†Ø¶`ÚÅ|ý¢ITdñß&ßlEV?=ãêh«XW½NbóÍ?°ãrw#ô€á´ªöê<C3B6>ð/¹ñ³‡)ÿ€À¸n+sîÁ3S<33>ë-ÕLCþ7¼ÇJ°úî1ÿÌ…?ØÅ Ë<{¸Z¶gE¼åP òË3³ñŽúw<ðÓd´ó²ëæÅÅ;/·Œ~cÂ
Þq(½È«ðŽËuô•…;~ÿð²E—½¢ƒ/aejÎ|¶Pl­íÆ·àW­»3jë§ãDçVðíù(G“9HrD€âQ„Â…t"ðp•ÇL'¯q*”| <gÂ_+±½·Œ\!ꇯ,þÊg;L}ß0ÞðåËP°¼1 íË¿W¼ §çÇõ ûiõêc?>úL-FÁ6Ü°Ðú
¾¬·| 20ïiòÝ ï Ûú«Á¼>84Yà߶.Ï_ïxYíw|òÃ? úMv4#l4ßoÆÊjfWî™=ÓOkùjÅØb1°bÚŠŽáÏòºÇá¶'²þ»C…„å
,â/³ºï}¯¦Jý>þd?å‡ ¾<>Ÿm×Yg¤Ý©çÚ_ÅüÊ4Î4ÛŸÂÈ•~ƒâ)ÁD9æ\Ùî~bBL<­8Š™ž{6ŽÂ,ž§ ûZ<C3BB>´3„´<´ì¨>ä"ñc<>·ïéO½†ŒL!‡8-e+SJÒK¨ÖÊv¶|Þkòøò®¶|E© Î~˹ái ]žUmèhäIùÊ‚«Ä†çŒq‡<71>ždÀ
+/ÝÄÄj<C384>qž14+Å“Ôc¯³æ€YÕÅ<C395>
,Oþ1±%@û¢¯¹<C2AF>.WL§DtÁÄ( ¿ÜìKi\OpF¬„.]ÌÜŒVB—j®äJ-ˆH-¸ º\,/eþ0[ y}Oó°"©:µ˜-6çÍñ›?<™Š×ûº™lI¶É¿÷ùíø°&7{CÌàÉ´r•J$˜'ÇÛ _€sþæeŸå6c?BE”Jˆø0ŸFK˲¹ù0ÌO‰^RšhhÙµas™Ýƒ»ƒ¹ƒp®e´i'üÚcb;LøTr ³ÈP4×nΓTöPp<ÕÞ$KÑïvéæ bV=K *­ü{>5<>§¼wÖ¶¾oå¾*«|áãÛ1DEÂÇÇÀÔ¢JÆJ³áh$ÕÙV×Ðw
áŸ8<Ø¢atyîû2ÕÙw`!AîÓŸŽe@Ò
7¼]2BÉòÐcf¶;@ÝÅ®–à' ¿ò:Öi³Ÿ¬L!=XŒvñbDkÓɧ/Lsn´xÝü¸ýííÓŸêì²Ü2
/Äë +ÃÆì<C386>d¢Á¶€GpÏðEæû:ôÓB96¶ú0ü¥²{l8j°ÜÄ'^`ØÁ²Hn“d#çJuŠ3{
EéÕ‡
ãñCÀê†çV°•å<E280A2>ŠKðƒàAJÌiÔÜÌé–‡.°'M®t/ø3<C3B8> \°n6̹$>Û­ÛÆ4ü Ž^ÝÍ¡9í i ½pJB¯éƒ“1ÎùJ`;N¯±\æõ>þ<>‡«»L;³ycŽÂÈ~Øû(ó Y
ã.¯àÚ~žÉ¾ñI՜؉Ì<>£¦9ì¢ð™·1Ÿ†¦s ¦PÄ»¿ŒR¡Ñqè;¿£Y­Ö4Z¾ºa¸Û f.bAø³\øá(Ë¡Úÿ7‰³§ÌÞƒühF\<5C>þI'”ðJŒSqXN{-<2D>3åþüÐóŒ<C3B3>ùTø|t0Ÿ°žïg~ô›/Ûs$Ý ½¯Ë«üôŽ†~P:‘ꘓ,€Ý ­Ò•5Hw÷Èþ Ì~J &~X”Ïi‡%x!šb»pþîêò‡ñÇÉØ1\ü@¯)s¦Ø&úM™3íÄÐ'˜2gÚ1ÜÞµ^SæL;† ˆ©A…˜£óJ}øÄ ,ˆüöÏ?(Ëm+Þžó/ܶ"å<>lMRT"ÕDåÓ阳%/<2F>æžÉ•¢_evQÍø6ì»ïQ²…Jõ¿ ûr à`ïËø ´,Èl@º0Þ³/Y°ì€ŸÜüoþ707Î<òÆJ1au¿§ÎŸyAÒÌe_ØÉd‰øºÉï^ö˜ÔnãNº{ WÜ%»—èŒuŽh'oÜA:H7%ÔãhL<68>_J üòtèãt@›Ê,Þq"€îàD€g숉À14N‡aâY ƒÿcå<63>`‰`yºOßÊæ3XôìG¨1Cú ¸…Ëf³b»ÙòÌPj õ©ÄPʘD™ÖæèŒXuLºœ:ŒÞNÇÊPÀ<>X~É@êËžæyÙ@ÐÙ<C390>¬è<w seÖÒ%QZõÂPT<1D>¯ú,ÑN]ç<>Þö$AGsãsw™°i ¤ÒŠ'šîiŒ(ùNàÞ%‡AÒå|$ó½ÃÑèÈòpæ"»áîbI6¶Fmb¨­ÅehzkQ~ê<>%Dë}òÔÃúK¿Ê¢#s{\Ò•pš4±hô{™êR.8M/·öæÚ\rÔ )ѲŒrÂ@69j"NÓ=ê\$pZï`ºlí¿Í:”´<E2809D>þ`-<2D>yK”À8ºH!%="µˆ¡Ñ‰¹‚â:Áê5bŒôel1Úì%}¦ýê@òJzÇz­Ð|œÆ¬Q,riý°`Àµ ª|àò»ËÂvZ:y!!8ÁyŠQÊë„(µ(åuB”hnÔ¢”× Q"4jQÊë„(Ñn¯Z”ò:!ÊîÌU¥(åuB”\\@1Jy<4A>%@£¥¼Nˆmø«E)¯¢D9†jQÊë„(9) ¥¼NˆºÔ¢”:C”Yá j6#ƒ‘<16>@ GmÌQ¸CÓ#š%תó 7uÝPjY1©ÅË679èµ¢ëÁŸ þa;“Œ14µ˜]ˆÈà_îœð´ Õ,ÀSÄßIg‡âδ '˜hêô5ö!{?Ë´ŸþÄ6ð¹]üœ×i™{Ød žsÝÔ¤ªƒï4L´«ìÐñâ(l—ý’ :ûaKÐàOƕ츰[L(Âl³{K¯Ö2.{ÎniõÏn6xæ"@!¡ˆ`.ì¸|;6|¸{ ÷9á©ïp7sPØLð<4C>Lðóø©Eî;Ô$8ȳŦ6ü‡¤nþð˜ÿðüŽ¼¾ˆÁ /Aøkmt󇵶<C2B5>mm†Ä9ðd¨}~1Þ/¶›!õ<>!†~ë>Õ^+yCÌÀ6<C380>¬ ´¿ÛÐöéöqpóaÈÀ?Ý¿àß&üC°øá]ôÐËþÖýöñ|ú1n_Ðð—ÁKž _„arÞ(áŽúÂaƒ?t ë›?a¿Ûòr¿Ônþh)u<>í#ðÿÎÝ ˆ)f˜ÁÌ06Õ34´4„Šc†‰<E280A0>ž9ÈVSK¨ƦÆz&0"Ì011Ó3‡‡!Ô D’ê#˜;,-Eˆ0ÃÇ šâ€ œÞtA¥°j@bzjHQbf(¦'–¥*$æåå—$–¤¥ÒRRŠ3òËA" Mp ÀäìïÆËM˜Ù
endstream endobj 5 0 obj <</Intent 14 0 R/Name(Logo)/Type/OCG/Usage 15 0 R>> endobj 14 0 obj [/View/Design] endobj 15 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 16.0)/Subtype/Artwork>>>> endobj 22 0 obj [21 0 R] endobj 35 0 obj <</CreationDate(D:20171230161300+01'00')/Creator(Adobe Illustrator CS6 \(Windows\))/ModDate(D:20171230161716+01'00')>> endobj xref
0 36
0000000004 65535 f
0000000016 00000 n
0000000159 00000 n
0000009448 00000 n
0000000000 00000 f
0000039045 00000 n
0000000000 00000 f
0000009499 00000 n
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000039112 00000 n
0000039143 00000 n
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000010825 00000 n
0000039228 00000 n
0000009835 00000 n
0000011122 00000 n
0000011009 00000 n
0000010082 00000 n
0000010263 00000 n
0000010311 00000 n
0000010893 00000 n
0000010924 00000 n
0000011196 00000 n
0000011370 00000 n
0000012385 00000 n
0000014612 00000 n
0000039253 00000 n
trailer
<</Size 36/Root 1 0 R/Info 35 0 R/ID[<95FCB4CF9C1B59448ABEC1DF1DD64C33><716E2DB4294F6749B3E5ADF7E13AF65C>]>>
startxref
39388
%%EOF

BIN
web/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

View File

@ -1,31 +0,0 @@
{
"name": "stairs",
"version": "1.0.0",
"description": "Stairs lighting project",
"main": "app.js",
"author": "Mark van Renswoude",
"license": "ISC",
"dependencies": {
"buffer-reader": "^0.1.0",
"express": "^4.15.2",
"md5-file": "^3.1.1",
"semver-utils": "^1.1.1"
},
"devDependencies": {
"@types/crossroads": "0.0.29",
"@types/hasher": "0.0.29",
"@types/jquery": "^2.0.41",
"@types/knockout": "^3.4.40",
"@types/nprogress": "0.0.29",
"@types/requirejs": "^2.1.29",
"gulp": "^3.9.1",
"gulp-clean-css": "^3.0.4",
"gulp-concat": "^2.6.1",
"gulp-debounced-watch": "^1.0.4",
"gulp-plumber": "^1.1.0",
"gulp-sass": "^3.1.0",
"gulp-typescript": "^3.1.6",
"gulp-uglify": "^2.1.2",
"typescript": "^2.2.2"
}
}

View File

@ -1,24 +0,0 @@
module.exports =
{
Command:
{
Error: 0x00,
Ping: 0x01,
Reply: 0x02,
GetMode: 0x03,
SetMode: 0x04,
GetRange: 0x05,
SetRange: 0x06,
UpdateFirmware: 0xFF
},
Mode:
{
Static: 0x01,
Custom: 0x02,
Alternate: 0x03,
Slide: 0x04
//ADC: 0x05
}
};

706
web/site.scss Normal file
View File

@ -0,0 +1,706 @@
@import "variables.scss";
html
{
box-sizing: border-box;
font-size: 62.5%;
}
*, *:before, *:after
{
box-sizing: inherit;
}
body
{
background-color: black;
color: white;
font-family: 'Verdana', 'Arial', sans-serif;
font-size: 1.3em;
font-weight: 300;
letter-spacing: .01em;
line-height: 1.3;
padding-bottom: 3rem;
@media #{$mediumScreen}
{
padding-top: 3rem;
}
}
a
{
text-decoration: none;
}
/*
Hide VueJS container until the template has been processed
*/
[v-cloak]
{
display: none;
}
#container
{
background: $containerBackground;
margin-top: 2rem;
padding: 1rem;
box-shadow: 0 0 50px $containerShadowColor;
border: solid 1px black;
@media #{$mediumScreen}
{
width: 768px;
margin-left: auto;
margin-right: auto;
}
}
.header
{
position: relative;
img
{
float: left;
margin-right: 1rem;
}
.wifistatus
{
@media #{$smallScreen}
{
clear: both;
margin-top: 3rem;
}
@media #{$mediumScreen}
{
position: absolute;
right: 0;
top: 0;
}
.indicator
{
display: inline-block;
width: 1rem;
height: 1rem;
border-radius: 50%;
margin-right: 0.5rem;
&[data-status=connected] { background-color: #339966; }
&[data-status=disconnected] { border: solid 1px #808080; }
&[data-status=connecting] { background-color: #ff9933; }
&[data-status=error] { background-color: #cc0000; }
}
}
}
%outset
{
border: 1px solid #111111;
border-radius: 3px;
box-shadow: inset 0 1px rgba(255,255,255,0.1), inset 0 -1px 3px rgba(0,0,0,0.3), inset 0 0 0 1px rgba(255,255,255,0.08), 0 1px 2px rgba(0,0,0,0.15);
}
%inset
{
border: 1px solid #111111;
border-color: black #111111 #111111;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.25),0 1px rgba(255,255,255,0.08);
}
button, input
{
font-family: 'Verdana', 'Arial', sans-serif;
}
button, .button, input[type=submit]
{
@extend %outset;
display: inline-block;
padding: 0 12px;
color: $buttonTextColor;
background: $buttonBackground;
cursor: pointer;
line-height: 3rem;
&:hover, &:focus, &.focus
{
color: $buttonHoverTextColor;
background: $buttonHoverBackground;
outline: none
}
&:active, &.active
{
@extend %inset;
color: $buttonActiveTextColor;
background: $buttonActiveBackground;
}
}
input[type=submit], .button-primary
{
background: $buttonPrimaryBackground;
&:hover, &:focus, &.focus
{
background: $buttonPrimaryHoverBackground;
}
}
a.button
{
text-decoration: none
}
.navigation
{
clear: both;
margin-top: 3rem;
}
.tabs
{
&>.button
{
margin-left: -1px;
border-radius: 0;
&:first-child
{
margin-left: 0;
border-radius: 3px 0 0 3px
}
&:last-child
{
border-radius: 0 3px 3px 0
}
&:focus
{
position: relative;
z-index: 1
}
}
}
.version
{
color: $versionTextColor;
font-size: 8pt;
text-align: center;
margin-top: 2rem;
}
.notificationContainer
{
position: fixed;
top: 2rem;
z-index: 666;
@media #{$mediumScreen}
{
width: 512px;
left: 50%;
}
}
.notification
{
@extend %outset;
background: $notificationBackground;
/* border: solid 1px $notificationBorderColor;*/
box-shadow: 0 0 10px black;
color: white;
cursor: pointer;
padding: .5em;
margin-bottom: 2rem;
position: relative;
@media #{$mediumScreen}
{
left: -50%;
}
.message
{
white-space: pre;
}
&.error
{
background: $notificationErrorBackground;
}
}
.check, .radio
{
display: inline-block;
cursor: pointer;
user-select: none;
white-space: nowrap;
.control
{
@extend %outset;
background: $checkRadioBackground;
display: inline-block;
width: 16px;
height: 16px;
position: relative;
}
.label
{
display: inline-block;
margin-left: .5em;
vertical-align: top;
}
&.checked
{
.control
{
background: $checkRadioSelectedBackground;
.inner
{
}
}
}
&.disabled
{
cursor: not-allowed;
}
}
.radio
{
.control, .control .inner
{
border-radius: 50%;
}
.control .inner
{
color: black;
position: absolute;
top: 4px;
left: 4px;
width: 6px;
height: 6px;
}
&.checked .control .inner
{
background: #cccccc;
box-shadow: 0 1px rgba(0,0,0,0.5);
}
}
.check
{
.control .inner
{
position: absolute;
top: 5px;
left: 4px;
width: 6px;
height: 3px;
}
&.checked .control .inner
{
border: solid rgba(255,255,255,0.8);
border-width: 0 0 2px 2px;
transform: rotate(-45deg);
box-shadow: -1px 0 rgba(0,0,0,0.2), 0 1px rgba(0,0,0,0.5)
}
}
.form-control
{
margin-top: 1em;
}
input[type=text], input[type=number], input[type=password], textarea
{
@extend %inset;
background: $inputBackground;
color: $inputTextColor;
padding: .5em;
width: 100%;
}
select
{
@extend %outset;
background: $selectBackground;
color: $inputTextColor;
padding: .5em;
}
input[type=range]
{
margin-top: 1rem;
margin-bottom: 1rem;
}
h1
{
font-size: 2rem;
margin: 0;
}
h2
{
color: #c0c0c0;
font-size: 1.2rem;
margin: 0;
}
h3
{
@extend %outset;
color: $sectionHeaderTextColor;
background: $sectionHeaderBackground;
font-size: 1.2rem;
padding: .5rem;
}
h4
{
font-size: 1.4rem;
}
input[disabled]
{
cursor: not-allowed;
color: $inputDisabledTextColor;
background: $inputDisabledBackground;
}
label
{
display: block;
margin-top: .5em;
margin-bottom: .5em;
}
.label-inline
{
margin-right: 2rem;
}
@media #{$mediumScreen}
{
.horizontal
{
clear: both;
label
{
display: inline-block;
}
input[type=text], input[type=number], input[type=password], textarea
{
display: inline-block;
float: right;
width: 50%;
}
&:after
{
clear: both;
}
}
}
.hint
{
display: block;
font-size: 8pt;
color: #808080;
margin-bottom: 1.5rem;
}
.loading
{
margin-top: 3rem;
text-align: center;
}
.suboptions
{
margin-left: 5rem;
}
.buttons
{
clear: both;
text-align: center;
margin-top: 1rem;
}
.sliders
{
margin-top: 2rem;
}
.step
{
margin-left: 3rem;
margin-right: 3rem;
position: relative;
.slidercontainer
{
margin-right: 4em;
}
.value
{
position: absolute;
right: 0;
top: .1rem;
color: $sliderValueColor;
}
}
.slidercontainer
{
margin-top: 1rem;
}
.slider
{
-webkit-appearance: none;
width: 100%;
height: $sliderBarSize;
border-radius: $sliderBarSize / 2;
background: $sliderBarColor;
outline: none;
&::-webkit-slider-thumb
{
-webkit-appearance: none;
appearance: none;
width: $sliderThumbSize;
height: $sliderThumbSize;
border-radius: 50%;
background: $sliderThumbColor;
cursor: pointer;
}
&::-moz-range-thumb
{
width: $sliderThumbSize;
height: $sliderThumbSize;
border-radius: 50%;
background: $sliderThumbColor;
cursor: pointer;
}
}
.warning
{
@extend %outset;
background: #973a38;
padding: .5em;
margin-bottom: 2rem;
margin-top: 1rem;
}
.nodata
{
color: #808080;
text-align: center;
}
.clear
{
clear: both;
}
.panel
{
margin-bottom: 2rem;
padding: 0;
.panel-header
{
@extend %outset;
border-radius: 3px 3px 0 0;
border-bottom-width: 0;
padding: .5em;
label {
font-size: 1em;
}
background: $panelHeaderBackground;
color: $panelHeaderTextColor;
.actions
{
float: right;
}
a, .label
{
color: $panelHeaderLinkColor;
}
}
.panel-body
{
@extend %outset;
border-radius: 0 0 3px 3px;
background: $panelBodyBackground;
padding: 2rem;
}
&.active
{
.panel-header
{
background: $panelActiveHeaderBackground;
color: $panelActiveHeaderTextColor;
}
}
}
.inline
{
display: inline-block;
width: auto;
}
.weekdays
{
margin-top: 1rem;
.label
{
width: 8em;
}
}
.fade-enter-active, .fade-leave-active
{
transition: opacity .5s;
}
.fade-enter, .fade-leave-to
{
opacity: 0;
}
.range
{
clear: both;
.start
{
position: relative;
display: inline-block;
width: 49%;
.slidercontainer
{
margin-right: 4em;
}
.value
{
position: absolute;
right: 0;
top: 1.5rem;
color: $sliderValueColor;
}
}
.end
{
position: relative;
display: inline-block;
float: right;
width: 50%;
.slidercontainer
{
margin-left: 4em;
}
.value
{
position: absolute;
left: 0;
top: 1.5rem;
color: $sliderValueColor;
}
}
&:after
{
clear: both;
}
}
.resetReason
{
margin-left: 2em;
}

View File

@ -1 +0,0 @@
var bowerBase="../../bower_components/",config={baseUrl:"assets/dist/",shim:{bootstrap:{deps:["jquery","tether"],exports:"Bootstrap"}},paths:{crossroads:bowerBase+"crossroads/dist/crossroads.min",hasher:bowerBase+"hasher/dist/js/hasher.min",jquery:bowerBase+"jquery/dist/jquery.min",knockout:bowerBase+"knockout/dist/knockout",signals:bowerBase+"js-signals/dist/signals.min",text:bowerBase+"text/text",bootstrap:bowerBase+"bootstrap/dist/js/bootstrap.min",tether:bowerBase+"tether/dist/js/tether.min",nprogress:bowerBase+"nprogress/nprogress"}};config.wrapShim=!0,requirejs.config(config),window.Tether=require(["tether"],function(){require(["bootstrap","index"])});

View File

@ -1 +0,0 @@
body{background-color:#fff}#page{margin-top:16px}.row .header{font-weight:700;padding-bottom:8px}

View File

@ -1,26 +0,0 @@
<form>
<div class="form-group row">
<label for="host" class="col-sm-2 col-form-label">Host</label>
<div class="col-sm-10">
<input type="text" class="form-control" data-bind="value: Host" id="host" />
</div>
</div>
<div class="form-group row">
<label for="port" class="col-sm-2 col-form-label">Port</label>
<div class="col-sm-10">
<input type="number" class="form-control" data-bind="value: Port" min="1" max="65536" id="port" />
</div>
</div>
<div class="form-group row">
<label for="path" class="col-sm-2 col-form-label">Path</label>
<div class="col-sm-10">
<input type="text" class="form-control" data-bind="value: Path" id="path" />
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-primary" data-bind="click: updateFirmware">Check for update</button>
</div>
</div>
</form>

View File

@ -1 +0,0 @@
define(["require","exports","text!./page-firmware.html","knockout"],function(t,e,o,r){"use strict";return{viewModel:function(){function t(){this.Host=r.observable(location.hostname),this.Port=r.observable(location.port),this.Path=r.observable("/updateFirmware"),this.updateFirmware=function(){}}return t}(),template:o}});

View File

@ -1 +0,0 @@
Hello world!

View File

@ -1 +0,0 @@
define(["require","exports","text!./page-home.html"],function(e,t,n){"use strict";return{viewModel:function(){function e(){}return e.prototype.dispose=function(){},e}(),template:n}});

View File

@ -1,71 +0,0 @@
<form>
<div class="form-group">
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="mode" data-bind="checkedValue: StairsMode.Static, checked: Stairs.Mode.Current">
Static
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="mode" data-bind="checkedValue: StairsMode.Custom, checked: Stairs.Mode.Current">
Custom
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="mode" data-bind="checkedValue: StairsMode.Alternate, checked: Stairs.Mode.Current">
Alternating
</label>
</div>
</div>
</form>
<div data-bind="visible: Stairs.Mode.Current() == StairsMode.Static" style="display: none">
<div class="form-group row">
<div class="col-sm-2">Brightness</div>
<div class="col-sm-5"><input type="number" class="form-control" min="0" max="4095" data-bind="value: Stairs.Mode.Static.Brightness" /></div>
<div class="col-sm-5"><input type="range" class="form-control" min="0" max="4095" data-bind="value: Stairs.Mode.Static.Brightness, valueUpdate: 'input'" /></div>
</div>
</div>
<div data-bind="visible: Stairs.Mode.Current() == StairsMode.Custom" style="display: none">
<form>
<div class="row">
<div class="col-sm-2 header">Step</div>
<div class="col-sm-10 header">Value</div>
</div>
<div data-bind="foreach: Stairs.Mode.Custom.Brightness">
<div class="form-group row">
<div class="col-sm-2" data-bind="text: $parent.Stairs.Mode.Custom.Brightness().length - $index()"></div>
<div class="col-sm-5"><input type="number" class="form-control" min="0" max="4095" data-bind="value: $data.value" /></div>
<div class="col-sm-5"><input type="range" class="form-control" min="0" max="4095" data-bind="value: $data.value, valueUpdate: 'input'" /></div>
</div>
</div>
</form>
</div>
<div data-bind="visible: Stairs.Mode.Current() == StairsMode.Alternate" style="display: none">
<div class="form-group row">
<div class="col-sm-2">Interval</div>
<div class="col-sm-5"><input type="number" class="form-control" data-bind="value: Stairs.Mode.Alternate.Interval" /></div>
<div class="col-sm-5"></div>
</div>
<div class="form-group row">
<div class="col-sm-2">Brightness</div>
<div class="col-sm-5"><input type="number" class="form-control" min="0" max="4095" data-bind="value: Stairs.Mode.Alternate.Brightness" /></div>
<div class="col-sm-5"><input type="range" class="form-control" min="0" max="4095" data-bind="value: Stairs.Mode.Alternate.Brightness, valueUpdate: 'input'" /></div>
</div>
</div>
<!--
<div data-bind="visible: Stairs.Mode.Current() == StairsMode.Slide" style="display: none">
<div class="parameter">
Brightness: <input type="range" min="0" max="4095" data-bind="value: slide.brightness, valueUpdate: 'input'" /> <input type="number" min="0" max="4095" data-bind="value: slide.brightness" />
</div>
</div>
-->

View File

@ -1 +0,0 @@
define(["require","exports","text!./page-mode.html","stairs"],function(t,e,i,r){"use strict";return{viewModel:function(){function t(){this.StairsMode=r.StairsMode,this.Stairs=r.Stairs.instance()}return t}(),template:i}});

View File

@ -1,23 +0,0 @@
<form>
<div class="form-check">
<label class="form-check-label">
<input type="checkbox" class="form-check-input" data-bind="checked: Stairs.Range.UseScaling"> Use scaling
</label>
</div>
<div class="row">
<div class="col-sm-2 header">Step</div>
<div class="col-sm-5 header">Start (min. 0)</div>
<div class="col-sm-5 header">End (max. 4095)</div>
</div>
<div data-bind="foreach: Stairs.Range.Values">
<div class="form-group row">
<div class="col-sm-2" data-bind="text: $parent.Stairs.Range.Values().length - $index()"></div>
<div class="col-sm-5"><input type="number" class="form-control" data-bind="value: Start"></div>
<div class="col-sm-5"><input type="number" class="form-control" data-bind="value: End"></div>
</div>
</div>
<button class="btn btn-warning" data-bind="click: resetRanges">Reset</button>
</form>

Some files were not shown because too many files have changed in this diff Show More