Merge branch 'release/2.0'
This commit is contained in:
commit
fb00c0baa2
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,9 +1,6 @@
|
|||||||
.pioenvs
|
.pioenvs
|
||||||
.piolibdeps
|
.piolibdeps
|
||||||
src/credentials.h
|
bin
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
web/node_modules/
|
node_modules
|
||||||
web/update/
|
src/secret.h
|
||||||
src/version.h
|
|
||||||
web/version.js
|
|
||||||
web/static/bower_components/
|
|
325
API.md
Normal file
325
API.md
Normal 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
61
DEVELOPING.md
Normal 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
17
README.md
Normal 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)
|
@ -5,18 +5,21 @@
|
|||||||
"path": ".",
|
"path": ".",
|
||||||
"file_exclude_patterns": ["*.sublime-project"]
|
"file_exclude_patterns": ["*.sublime-project"]
|
||||||
}
|
}
|
||||||
]/*,
|
],
|
||||||
"build_systems":
|
"completions":[
|
||||||
[
|
["t", "{{ \\$t('${1:}') }}"]
|
||||||
{
|
],
|
||||||
"name": "PlatformIO - Build",
|
"settings": {
|
||||||
"cmd": ["platformio", "run"],
|
"todoreview": {
|
||||||
"working_dir": "$project_path"
|
"exclude_folders": [
|
||||||
},
|
"*.pioenvs*",
|
||||||
{
|
"*.piolibdeps*",
|
||||||
"name": "PlatformIO - Upload",
|
"*bin*",
|
||||||
"cmd": ["platformio", "run", "--target", "upload"],
|
"*node_modules*"
|
||||||
"working_dir": "$project_path"
|
],
|
||||||
|
"patterns": {
|
||||||
|
"TODO": "TODO[\\s]*(?P<todo>.*)$",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]*/
|
}
|
||||||
}
|
}
|
5
WIRING.md
Normal file
5
WIRING.md
Normal 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
|
@ -1,3 +0,0 @@
|
|||||||
& .\updateversion.ps1
|
|
||||||
& platformio run
|
|
||||||
Copy-Item .\.pioenvs\esp12e\firmware.bin .\web\update\
|
|
@ -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
256
devserver.js
Normal 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')
|
||||||
|
});
|
BIN
docs/ESP8266-12F WiFi Module.fzpz
Normal file
BIN
docs/ESP8266-12F WiFi Module.fzpz
Normal file
Binary file not shown.
BIN
docs/PCA9685 16x12-bit PWM Breakout.fzpz
Normal file
BIN
docs/PCA9685 16x12-bit PWM Breakout.fzpz
Normal file
Binary file not shown.
BIN
docs/PIR sensor.fzpz
Normal file
BIN
docs/PIR sensor.fzpz
Normal file
Binary file not shown.
4889
docs/Pinout.ai
Normal file
4889
docs/Pinout.ai
Normal file
File diff suppressed because one or more lines are too long
BIN
docs/Wiring.fzz
Normal file
BIN
docs/Wiring.fzz
Normal file
Binary file not shown.
BIN
docs/Wiring.png
Normal file
BIN
docs/Wiring.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 229 KiB |
170
docs/protocol.md
170
docs/protocol.md
@ -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
168
gulp-cppstringify.js
Normal 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
295
gulpfile.js
Normal 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
3
hosted/timezone.php
Normal 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
39
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
9
platformio-buildflags.bat
Normal file
9
platformio-buildflags.bat
Normal 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
|
@ -9,7 +9,15 @@
|
|||||||
; http://docs.platformio.org/page/projectconf.html
|
; http://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
[env:esp12e]
|
[env:esp12e]
|
||||||
platform = espressif8266_stage
|
platform = https://github.com/platformio/platform-espressif8266.git#feature/stage
|
||||||
board = esp12e
|
board = esp12e
|
||||||
framework = arduino
|
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
105
src/assets/css.h
Normal 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
181
src/assets/html.h
Normal 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
2384
src/assets/js.h
Normal file
File diff suppressed because it is too large
Load Diff
13
src/assets/version.h
Normal file
13
src/assets/version.h
Normal 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 = ;
|
||||||
|
const char VersionBranch[] = "master";
|
||||||
|
const char VersionSemVer[] = "2.0.0";
|
||||||
|
const char VersionFullSemVer[] = "2.0.0";
|
||||||
|
const char VersionCommitDate[] = "2018-02-17";
|
||||||
|
|
||||||
|
#endif
|
44
src/charproperties.cpp
Normal file
44
src/charproperties.cpp
Normal 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
15
src/charproperties.h
Normal 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
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright 2017 (c) Mark van Renswoude
|
* Copyright 2017 (c) Mark van Renswoude
|
||||||
*/
|
*/
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "PCA9685.h"
|
#include "./PCA9685.h"
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
|
|
||||||
#include <Arduino.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)
|
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;
|
uint8_t result = 0;
|
||||||
|
|
||||||
Wire.beginTransmission(this->address);
|
Wire.beginTransmission(this->mAddress);
|
||||||
Wire.write(registerAddress);
|
Wire.write(registerAddress);
|
||||||
Wire.endTransmission();
|
Wire.endTransmission();
|
||||||
|
|
||||||
Wire.requestFrom(this->address, (uint8_t)1);
|
Wire.requestFrom(this->mAddress, (uint8_t)1);
|
||||||
if (Wire.available())
|
if (Wire.available())
|
||||||
result = Wire.read();
|
result = Wire.read();
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ uint8_t PCA9685::read(uint8_t registerAddress)
|
|||||||
|
|
||||||
void PCA9685::write(uint8_t registerAddress, uint8_t value)
|
void PCA9685::write(uint8_t registerAddress, uint8_t value)
|
||||||
{
|
{
|
||||||
Wire.beginTransmission(this->address);
|
Wire.beginTransmission(this->mAddress);
|
||||||
Wire.write(registerAddress);
|
Wire.write(registerAddress);
|
||||||
Wire.write(value);
|
Wire.write(value);
|
||||||
Wire.endTransmission();
|
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)
|
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(PCA9685::RegisterLED0OnL + (4 * pin));
|
||||||
this->write(on);
|
this->write(on);
|
||||||
this->write(on >> 8);
|
this->write(on >> 8);
|
||||||
@ -124,7 +124,7 @@ void PCA9685::setAll(uint16_t value)
|
|||||||
|
|
||||||
void PCA9685::setAll(uint16_t on, uint16_t off)
|
void PCA9685::setAll(uint16_t on, uint16_t off)
|
||||||
{
|
{
|
||||||
Wire.beginTransmission(this->address);
|
Wire.beginTransmission(this->mAddress);
|
||||||
this->write(PCA9685::RegisterAllLEDOnL);
|
this->write(PCA9685::RegisterAllLEDOnL);
|
||||||
this->write(on);
|
this->write(on);
|
||||||
this->write(on >> 8);
|
this->write(on >> 8);
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
class PCA9685
|
class PCA9685
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
uint8_t address;
|
uint8_t mAddress;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint8_t read(uint8_t registerAddress);
|
uint8_t read(uint8_t registerAddress);
|
||||||
|
99
src/config.h
99
src/config.h
@ -1,56 +1,67 @@
|
|||||||
#ifndef __Config
|
#ifndef __config
|
||||||
#define __Config
|
#define __config
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <stdint.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
|
//#define SerialDebug
|
||||||
|
|
||||||
|
// Enables the crash API methods to cause crashes, you probably never
|
||||||
// The name of this device on the network
|
// want to leave this on unless you're debugging the exception handler
|
||||||
static const char* WiFiHostname = "Stairs";
|
//#define EnableCrashAPI
|
||||||
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef SerialDebug
|
#ifdef SerialDebug
|
||||||
#define _d(msg) Serial.print(msg)
|
static const uint32_t SerialDebugBaudrate = 115200;
|
||||||
#define _dln(msg) Serial.println(msg)
|
static const uint32_t SerialDebugStartupDelay = 2000;
|
||||||
#else
|
#endif
|
||||||
#define _d(msg) do { } while (0)
|
|
||||||
#define _dln(msg) do { } while (0)
|
|
||||||
|
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
|
||||||
|
|
||||||
#endif
|
#endif
|
@ -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
18
src/debug.cpp
Normal 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
24
src/debug.h
Normal 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
35
src/global.cpp
Normal 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
49
src/global.h
Normal 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
|
412
src/main.cpp
412
src/main.cpp
@ -1,365 +1,153 @@
|
|||||||
/*
|
/*
|
||||||
* Stairs lighting
|
* Stairs lighting
|
||||||
* Copyright 2017 (c) Mark van Renswoude
|
* Copyright 2017 (c) Mark van Renswoude
|
||||||
|
*
|
||||||
|
* https://git.x2software.net/pub/Stairs
|
||||||
*/
|
*/
|
||||||
#include <stdint.h>
|
#include <Arduino.h>
|
||||||
#include <Stream.h>
|
|
||||||
#include <ESP8266WiFi.h>
|
#include <ESP8266WiFi.h>
|
||||||
#include <WiFiUdp.h>
|
#include <WiFiUDP.h>
|
||||||
#include <ESP8266httpUpdate.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <ESPAsyncTCP.h>
|
||||||
|
#include <TimeLib.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <EspSaveCrash.h>
|
||||||
|
|
||||||
#include "config.h"
|
extern "C" {
|
||||||
#include "protocol.h"
|
#include <user_interface.h>
|
||||||
#include "components\PCA9685.h"
|
}
|
||||||
//#include "modes\adc.h"
|
|
||||||
#include "modes\alternate.h"
|
#include "./config.h"
|
||||||
#include "modes\custom.h"
|
#include "./debug.h"
|
||||||
#include "modes\slide.h"
|
#include "./global.h"
|
||||||
#include "modes\static.h"
|
#include "./components/PCA9685.h"
|
||||||
#include "stairs.h"
|
#include "./settings/connection.h"
|
||||||
#include "version.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;
|
ADC_MODE(ADC_VCC);
|
||||||
Stairs* stairs;
|
|
||||||
WiFiUDP udpServer;
|
|
||||||
uint8_t currentModeIdentifier;
|
|
||||||
IMode* currentMode;
|
|
||||||
|
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
void checkRequest();
|
void handleNotFound(AsyncWebServerRequest* request);
|
||||||
void handleRequest(uint8_t* packet);
|
|
||||||
IMode* createMode(uint8_t identifier);
|
|
||||||
void setCurrentMode(IMode *mode, uint8_t identifier);
|
|
||||||
void handleCurrentMode();
|
|
||||||
|
|
||||||
|
|
||||||
|
AsyncWebServer server(80);
|
||||||
|
PCA9685* pwmDriver;
|
||||||
|
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
#ifdef SerialDebug
|
_dinit();
|
||||||
Serial.begin(115200);
|
|
||||||
delay(5000);
|
currentTime = millis();
|
||||||
#endif
|
|
||||||
|
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");
|
_dln("Setup :: initializing PCA9685");
|
||||||
_d("Version: ");
|
|
||||||
_dln(FirmwareVersion);
|
|
||||||
|
|
||||||
pwmDriver = new PCA9685();
|
pwmDriver = new PCA9685();
|
||||||
pwmDriver->setAddress(PWMDriverAddress, PinSDA, PinSCL);
|
pwmDriver->setAddress(systemSettings->pwmDriverAddress(), systemSettings->pinPWMDriverSDA(), systemSettings->pinPWMDriverSCL());
|
||||||
pwmDriver->setPWMFrequency(PWMDriverPWMFrequency);
|
pwmDriver->setPWMFrequency(systemSettings->pwmDriverFrequency());
|
||||||
|
pwmDriver->setAll(0);
|
||||||
_dln("Initializing Stairs");
|
|
||||||
|
|
||||||
|
_dln("Setup :: initializing Stairs");
|
||||||
stairs = new Stairs();
|
stairs = new Stairs();
|
||||||
stairs->init(pwmDriver);
|
stairs->init(pwmDriver);
|
||||||
|
|
||||||
_dln("Initializing WiFi");
|
|
||||||
WiFi.mode(WIFI_STA);
|
|
||||||
WiFi.hostname(WiFiHostname);
|
|
||||||
WiFi.begin(WiFiSSID, WiFiPassword);
|
|
||||||
|
|
||||||
|
_dln("Setup :: starting initialization sequence");
|
||||||
_dln("Starting initialization sequence");
|
stairs->set(0, 255);
|
||||||
stairs->setAll(IStairs::Off);
|
|
||||||
stairs->set(0, IStairs::On);
|
|
||||||
delay(300);
|
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 - 1, 0);
|
||||||
stairs->set(step, IStairs::On);
|
stairs->set(step, 255);
|
||||||
delay(300);
|
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
|
_dln("Setup :: registering routes");
|
||||||
uint16_t brightness = 0;
|
registerStaticRoutes(&server);
|
||||||
uint16_t speed = 16;
|
registerAPIRoutes(&server);
|
||||||
|
registerSettingsRoutes(&server);
|
||||||
|
registerFirmwareRoutes(&server);
|
||||||
|
|
||||||
while (WiFi.status() != WL_CONNECTED)
|
_dln("Setup :: starting HTTP server");
|
||||||
{
|
server.onNotFound(handleNotFound);
|
||||||
brightness += speed;
|
server.begin();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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()
|
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();
|
currentTime = millis();
|
||||||
|
updateDebugStatus();
|
||||||
checkRequest();
|
|
||||||
handleCurrentMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void checkRequest()
|
if (connectionSettingsChanged)
|
||||||
{
|
|
||||||
int packetSize = udpServer.parsePacket();
|
|
||||||
if (packetSize)
|
|
||||||
{
|
{
|
||||||
_dln("Handling incoming packet");
|
_dln("Loop :: connection settings changed");
|
||||||
|
initWiFi();
|
||||||
memset(packet, 0, sizeof(packet));
|
connectionSettingsChanged = false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastUpdateCheck = currentTime;
|
|
||||||
|
|
||||||
switch (OTAUpdateEnabled)
|
updateWiFi();
|
||||||
{
|
updateLED();
|
||||||
case 1:
|
updateNTPClient();
|
||||||
_dln("Checking for update (fixed)");
|
checkTriggers();
|
||||||
result = ESPhttpUpdate.update(OTAUpdateFixedHost, OTAUpdateFixedPort, OTAUpdateFixedPath, FirmwareVersion);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
stairs->tick();
|
||||||
{
|
|
||||||
_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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void handleRequest(uint8_t* packet)
|
void handleNotFound(AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
_d("Handling request: ");
|
_d("HTTP :: not found: "); _dln(request->url());
|
||||||
_dln(*packet);
|
request->send(404);
|
||||||
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
77
src/main.debug.h
Normal file
77
src/main.debug.h
Normal 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
70
src/main.led.h
Normal 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
399
src/main.triggers.h
Normal 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×tamp=" +
|
||||||
|
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
121
src/main.wifi.h
Normal 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");
|
||||||
|
}
|
29
src/mode.h
29
src/mode.h
@ -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
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
||||||
{
|
|
||||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
|
||||||
}
|
|
@ -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
|
|
@ -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
190
src/server/api.cpp
Normal 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
13
src/server/api.h
Normal 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
74
src/server/firmware.cpp
Normal 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
13
src/server/firmware.h
Normal 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
245
src/server/settings.cpp
Normal 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
13
src/server/settings.h
Normal 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
10
src/server/shared.cpp
Normal 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
15
src/server/shared.h
Normal 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
29
src/server/static.cpp
Normal 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
14
src/server/static.h
Normal 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
|
72
src/settings/abstractjson.cpp
Normal file
72
src/settings/abstractjson.cpp
Normal 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);
|
||||||
|
}
|
28
src/settings/abstractjson.h
Normal file
28
src/settings/abstractjson.h
Normal 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
|
93
src/settings/connection.cpp
Normal file
93
src/settings/connection.cpp
Normal 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
74
src/settings/connection.h
Normal 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
91
src/settings/steps.cpp
Normal 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
57
src/settings/steps.h
Normal 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
115
src/settings/system.cpp
Normal 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
90
src/settings/system.h
Normal 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
|
86
src/settings/triggers/motion.cpp
Normal file
86
src/settings/triggers/motion.cpp
Normal 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;
|
||||||
|
}
|
71
src/settings/triggers/motion.h
Normal file
71
src/settings/triggers/motion.h
Normal 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
|
196
src/settings/triggers/time.cpp
Normal file
196
src/settings/triggers/time.cpp
Normal 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;
|
||||||
|
}
|
80
src/settings/triggers/time.h
Normal file
80
src/settings/triggers/time.h
Normal 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
|
306
src/stairs.cpp
306
src/stairs.cpp
@ -1,149 +1,221 @@
|
|||||||
|
#include "./stairs.h"
|
||||||
#include <Math.h>
|
#include <Math.h>
|
||||||
#include <FS.h>
|
#include <FS.h>
|
||||||
#include "stairs.h"
|
#include "./debug.h"
|
||||||
|
#include "./global.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const static float factorBase = log10(2) / log10(PCA9685::On);
|
const static float CurveFactor = log10(2) / log10(4095);
|
||||||
|
const static float LinearFactor = 4095.0f / 255.0f;
|
||||||
|
|
||||||
struct Header
|
|
||||||
{
|
|
||||||
uint8_t version;
|
|
||||||
uint8_t rangeCount;
|
|
||||||
bool useScaling;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
void Stairs::init(PCA9685* pwmDriver)
|
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;
|
// Re-apply all values in case the PWM value changed
|
||||||
this->ranges[i].end = IStairs::On;
|
for (uint8_t step = 0; step < stepsSettings->count(); step++)
|
||||||
|
applyCurrentValue(step);
|
||||||
|
|
||||||
|
stepsSettingsChanged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->pwmDriver = pwmDriver;
|
|
||||||
|
|
||||||
_dln("Loading range configuration");
|
if (!mTick) return;
|
||||||
SPIFFS.begin();
|
|
||||||
this->readRange();
|
uint32_t elapsedTime = mLastTransitionTime != 0 ? currentTime - mLastTransitionTime : 0;
|
||||||
}
|
if (!elapsedTime) return;
|
||||||
|
|
||||||
|
|
||||||
uint8_t Stairs::getCount()
|
mLastTransitionTime = currentTime;
|
||||||
{
|
mTick = false;
|
||||||
return StepCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void Stairs::set(uint8_t step, uint16_t brightness)
|
for (uint8_t step = 0; step < stepsSettings->count(); step++)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_dln("Full on/off, returning input");
|
Step* stepState = &mStep[step];
|
||||||
return brightness;
|
|
||||||
|
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)
|
if (!mTick)
|
||||||
{
|
mLastTransitionTime = 0;
|
||||||
_dln("Step out of bounds, returning input");
|
}
|
||||||
return brightness;
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
mStep[step].startValue = mStep[step].currentValue;
|
||||||
float factor = ((range->end - range->start) + 1) * factorBase;
|
mStep[step].startTime = startTime;
|
||||||
brightness = pow(2, (brightness / factor)) - 1 + range->start;
|
mStep[step].remainingTime = transitionTime;
|
||||||
|
|
||||||
|
if (!mLastTransitionTime)
|
||||||
|
mLastTransitionTime = currentTime;
|
||||||
|
|
||||||
|
mTick = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_dln("Not using scaling");
|
mStep[step].currentValue = brightness;
|
||||||
if (brightness < range->start) brightness = range->start;
|
applyCurrentValue(step);
|
||||||
if (brightness > range->end) brightness = range->end;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
uint16_t pwmValue;
|
||||||
return brightness;
|
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)
|
//_d("Stairs :: Output: "); _dln(pwmValue);
|
||||||
{
|
return pwmValue;
|
||||||
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();
|
|
||||||
}
|
}
|
44
src/stairs.h
44
src/stairs.h
@ -1,41 +1,45 @@
|
|||||||
#ifndef __Stairs
|
#ifndef __Stairs
|
||||||
#define __Stairs
|
#define __Stairs
|
||||||
|
|
||||||
#include "components/PCA9685.h"
|
#include "./components/PCA9685.h"
|
||||||
#include "modes/base.h"
|
#include "./config.h"
|
||||||
#include "config.h"
|
#include "./settings/steps.h"
|
||||||
|
|
||||||
|
struct Step
|
||||||
struct Range
|
|
||||||
{
|
{
|
||||||
uint16_t start;
|
uint8_t currentValue;
|
||||||
uint16_t end;
|
uint8_t startValue;
|
||||||
|
uint8_t targetValue;
|
||||||
|
int16_t startTime;
|
||||||
|
uint16_t remainingTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class Stairs : public IStairs
|
class Stairs
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
PCA9685* pwmDriver;
|
PCA9685* mPWMDriver;
|
||||||
|
Step mStep[MaxStepCount];
|
||||||
|
|
||||||
bool useScaling;
|
uint32_t mLastTransitionTime;
|
||||||
Range ranges[StepCount];
|
bool mTick = false;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void readRange();
|
uint8_t ease(uint8_t startValue, uint8_t targetValue, uint16_t transitionTime, uint16_t elapsedTime);
|
||||||
void writeRange();
|
inline void updateCurrentValue(Step* stepState);
|
||||||
|
inline void applyCurrentValue(uint8_t step);
|
||||||
uint16_t getPWMValue(uint8_t step, uint16_t brightness);
|
uint16_t getPWMValue(uint8_t step, uint8_t brightness);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void init(PCA9685* pwmDriver);
|
void init(PCA9685* pwmDriver);
|
||||||
|
void tick();
|
||||||
|
|
||||||
uint8_t getCount();
|
bool inTransition() { return mTick; }
|
||||||
void set(uint8_t step, uint16_t brightness);
|
|
||||||
void setAll(uint16_t brightness);
|
|
||||||
|
|
||||||
void getRange(Stream* stream);
|
uint8_t get(uint8_t step, bool target = true);
|
||||||
void setRange(uint8_t* data);
|
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
|
#endif
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||||||
& .\updateversion.ps1
|
|
||||||
& platformio run --target upload
|
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"directory": "static/bower_components"
|
|
||||||
}
|
|
1186
web/app.js
1186
web/app.js
File diff suppressed because it is too large
Load Diff
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
342
web/client.js
342
web/client.js
@ -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
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
1
web/dist/bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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
444
web/index.html
Normal 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">« {{ $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
367
web/lang.js
Normal 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
333
web/logo.ai
Normal 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
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAD4AwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F
XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
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Þÿàûÿó]•ûDyšó¿L9{¼7\Q<>âr
¥g¹…1ÄŠñ)–4ãV5º€¿íM.Wÿ!7]üŠž |