Browse Source

Merge pull request #4 from xoseperez/dev

Merging back from Xoseperez
ota
ColinShorts 6 years ago
committed by GitHub
parent
commit
07107bdf51
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 25238 additions and 5900 deletions
  1. +2
    -2
      .github/stale.yml
  2. +1
    -0
      .gitignore
  3. +25
    -2
      .travis.yml
  4. +82
    -0
      CHANGELOG.md
  5. +49
    -37
      README.md
  6. +120
    -49
      code/build.sh
  7. +6
    -5
      code/eagle.flash.1m0m1s.ld
  8. +19
    -0
      code/eagle.flash.1m0m2s.ld
  9. +21
    -0
      code/eagle.flash.4m1m4s.ld
  10. +20
    -0
      code/eagle.flash.4m3m4e.ld
  11. +19
    -0
      code/eagle.flash.512k0m1s.ld
  12. +76
    -53
      code/espurna/api.ino
  13. +26
    -13
      code/espurna/button.ino
  14. +1
    -1
      code/espurna/config/all.h
  15. +16
    -3
      code/espurna/config/arduino.h
  16. +30
    -0
      code/espurna/config/defaults.h
  17. +56
    -0
      code/espurna/config/dependencies.h
  18. +120
    -9
      code/espurna/config/general.h
  19. +354
    -17
      code/espurna/config/hardware.h
  20. +206
    -10
      code/espurna/config/progmem.h
  21. +108
    -54
      code/espurna/config/prototypes.h
  22. +152
    -27
      code/espurna/config/sensors.h
  23. +24
    -3
      code/espurna/config/types.h
  24. +1
    -2
      code/espurna/config/version.h
  25. BIN
      code/espurna/data/index.all.html.gz
  26. BIN
      code/espurna/data/index.html.gz
  27. BIN
      code/espurna/data/index.light.html.gz
  28. BIN
      code/espurna/data/index.rfbridge.html.gz
  29. BIN
      code/espurna/data/index.rfm69.html.gz
  30. BIN
      code/espurna/data/index.sensor.html.gz
  31. BIN
      code/espurna/data/index.small.html.gz
  32. +44
    -34
      code/espurna/debug.ino
  33. +86
    -0
      code/espurna/eeprom.ino
  34. +16
    -4
      code/espurna/espurna.ino
  35. +636
    -0
      code/espurna/fs_math.c
  36. +16
    -6
      code/espurna/homeassistant.ino
  37. +2
    -2
      code/espurna/i2c.ino
  38. +58
    -56
      code/espurna/ir.ino
  39. +34
    -28
      code/espurna/led.ino
  40. +65
    -0
      code/espurna/libs/RFM69Wrap.h
  41. +116
    -0
      code/espurna/libs/fs_math.h
  42. +22
    -10
      code/espurna/libs/pwm.h
  43. +22
    -25
      code/espurna/light.ino
  44. +86
    -3
      code/espurna/migrate.ino
  45. +22
    -11
      code/espurna/mqtt.ino
  46. +19
    -3
      code/espurna/ota.ino
  47. +1
    -1
      code/espurna/pwm.c
  48. +174
    -52
      code/espurna/relay.ino
  49. +35
    -10
      code/espurna/rfbridge.ino
  50. +280
    -0
      code/espurna/rfm69.ino
  51. +148
    -86
      code/espurna/sensor.ino
  52. +34
    -2
      code/espurna/sensors/AnalogSensor.h
  53. +6
    -6
      code/espurna/sensors/BaseSensor.h
  54. +1
    -1
      code/espurna/sensors/DHTSensor.h
  55. +18
    -11
      code/espurna/sensors/EmonSensor.h
  56. +29
    -12
      code/espurna/sensors/EventSensor.h
  57. +298
    -0
      code/espurna/sensors/GeigerSensor.h
  58. +2
    -2
      code/espurna/sensors/HLW8012Sensor.h
  59. +125
    -0
      code/espurna/sensors/NTCSensor.h
  60. +38
    -11
      code/espurna/sensors/PMSX003Sensor.h
  61. +12
    -6
      code/espurna/sensors/PZEM004TSensor.h
  62. +39
    -32
      code/espurna/sensors/SonarSensor.h
  63. +4
    -1
      code/espurna/sensors/V9261FSensor.h
  64. +67
    -45
      code/espurna/settings.ino
  65. +1
    -1
      code/espurna/ssdp.ino
  66. +3048
    -0
      code/espurna/static/index.all.html.gz.h
  67. +0
    -3248
      code/espurna/static/index.html.gz.h
  68. +2964
    -0
      code/espurna/static/index.light.html.gz.h
  69. +2563
    -0
      code/espurna/static/index.rfbridge.html.gz.h
  70. +4033
    -0
      code/espurna/static/index.rfm69.html.gz.h
  71. +2603
    -0
      code/espurna/static/index.sensor.html.gz.h
  72. +2519
    -0
      code/espurna/static/index.small.html.gz.h
  73. +5
    -7
      code/espurna/system.ino
  74. +1
    -1
      code/espurna/telnet.ino
  75. +83
    -175
      code/espurna/utils.ino
  76. +107
    -18
      code/espurna/web.ino
  77. +214
    -52
      code/espurna/wifi.ino
  78. +69
    -13
      code/espurna/ws.ino
  79. +36
    -10
      code/extra_scripts.py
  80. +122
    -52
      code/gulpfile.js
  81. +131
    -2
      code/html/custom.css
  82. +265
    -60
      code/html/custom.js
  83. +165
    -18
      code/html/index.html
  84. +0
    -120
      code/html/vendor/checkboxes.css
  85. +0
    -366
      code/html/vendor/checkboxes.js
  86. +460
    -0
      code/html/vendor/datatables-1.10.16.css
  87. +178
    -0
      code/html/vendor/datatables-1.10.16.min.js
  88. BIN
      code/html/vendor/images/border-off.png
  89. BIN
      code/html/vendor/images/border-on.png
  90. BIN
      code/html/vendor/images/handle-center.png
  91. BIN
      code/html/vendor/images/handle-left.png
  92. BIN
      code/html/vendor/images/handle-right.png
  93. BIN
      code/html/vendor/images/label-off.png
  94. BIN
      code/html/vendor/images/label-on.png
  95. BIN
      code/html/vendor/images/sort_asc.png
  96. BIN
      code/html/vendor/images/sort_both.png
  97. BIN
      code/html/vendor/images/sort_desc.png
  98. +2
    -4
      code/memanalyzer.py
  99. +42
    -16
      code/ota.py
  100. +270
    -7
      code/package-lock.json

code/.github/stale.yml → .github/stale.yml View File

@ -1,9 +1,9 @@
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 120
daysUntilStale: 60
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 30
daysUntilClose: 7
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:

+ 1
- 0
.gitignore View File

@ -14,3 +14,4 @@ custom.h
.python
.env
.DS_Store
.vscode

+ 25
- 2
.travis.yml View File

@ -2,14 +2,36 @@ language: python
python:
- '2.7'
sudo: false
conditions: v1
cache:
directories:
- "~/.npm"
- "~/.platformio"
- "$TRAVIS_BUILD_DIR/code/.piolibdeps"
- "$TRAVIS_BUILD_DIR/code/espurna/node_modules"
install:
- pip install -U platformio
- cd code ; npm install --only=dev ; cd ..
env:
global:
- BUILDER_TOTAL_THREADS=4
script:
- cd code && ./build.sh && cd ..
- cd code && ./build.sh -p && cd ..
stages:
- name: Test
- name: Release
if: tag IS present AND tag =~ ^\d+\.\d+\.\d+$
jobs:
include:
- stage: Test
script: cd code && ./build.sh travis01
- script: cd code && ./build.sh travis02
- script: cd code && ./build.sh travis03
- stage: Release
env: BUILDER_THREAD=0
- env: BUILDER_THREAD=1
- env: BUILDER_THREAD=2
- env: BUILDER_THREAD=3
before_deploy:
- mv firmware/*/espurna-*.bin firmware/
deploy:
@ -20,8 +42,9 @@ deploy:
file: firmware/espurna-*.bin
skip_cleanup: true
on:
all_branches: true
tags: true
repo: xoseperez/espurna
condition: $TRAVIS_BUILD_STAGE_NAME = Release
notifications:
pushover:
api_key:


+ 82
- 0
CHANGELOG.md View File

@ -3,6 +3,88 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [1.13.1] 2018-07-10
### Fixed
- Build issues with Arduino IDE ([#975](https://github.com/xoseperez/espurna/issues/975))
- Right web interface image for with RF Bridge
- Full web interface image if light and sensor together ([#981](https://github.com/xoseperez/espurna/issues/981))
- Some devices still not using DOUT flash mode
- Crash on loading malformed configuration file
- Mismatch between memory size and layout size for some boards (this might require reflashing)
- Wrong settings report after factory reset
- Memory leak in JustWifi library
- New buttons not rendering right in Safari ([#1028](https://github.com/xoseperez/espurna/issues/1028))
### Added
- Support for RFM69GW board (see http://tinkerman.cat/rfm69-wifi-gateway/)
- Support for Sonoff IFAN02
- Support for NTC sensors ([#1001](https://github.com/xoseperez/espurna/issues/1001))
- Support for single-pin latched relays ([#1039](https://github.com/xoseperez/espurna/issues/1039))
- Check binary flash mode in web upgrade
- Sampling to AnalogSensor
- Parallel builds in Travis (thanks to @lobradov)
### Changed
- Reworked platformio.ini, build.sh files (thanks to @gn0st1c and @mcspr)
## [1.13.0] 2018-06-22
### Fixed
- Fixed PZEM004T compilation issues, working when using hardware serial ([#837](https://github.com/xoseperez/espurna/issues/837))
- Fixed per channel state on/off for lights ([#830](https://github.com/xoseperez/espurna/issues/830))
- Fixed overflow in CSE7766 energy calculation ([#856](https://github.com/xoseperez/espurna/issues/856))
- Fixed On MQTT disconnect in web UI ([#845](https://github.com/xoseperez/espurna/issues/845))
- Check valid hostnames ([#874](https://github.com/xoseperez/espurna/issues/874), [#879](https://github.com/xoseperez/espurna/issues/879))
- Fix Sonoff POW R2 configuration
- Fixed InfluxDB sensor by id ([#882](https://github.com/xoseperez/espurna/issues/882))
- Fix build when disabling WEB_SUPPORT ([#923](https://github.com/xoseperez/espurna/issues/923))
- Fix calibration error in EmonSensor ([#876](https://github.com/xoseperez/espurna/issues/876))
- Fix telnet and web debug responsiveness ([#896](https://github.com/xoseperez/espurna/issues/896))
- Use double quotes in JSON for non-numeric values ([#929](https://github.com/xoseperez/espurna/issues/929))
- Support connections over HTTPS via proxy ([#937](https://github.com/xoseperez/espurna/issues/937))
### Added
- EEPROM sector rotation using EEPROM_Rotate library
- Code filtering when building web UI images
- Added pulsing a relay via MQTT and REST API ([#896](https://github.com/xoseperez/espurna/issues/896), [#902](https://github.com/xoseperez/espurna/issues/902))
- Support for WPS (not available in pre-built binaries)
- Support for Smart Config (not available in pre-built binaries)
- Support for CCT lights (thanks to @Skaronator)
- Allow RELAYx_DELAY_ON/OFF also for none GPIO relay types (thanks to @zafrirron)
- Added relay status to Domoticz on MQTT connection ([#872](https://github.com/xoseperez/espurna/issues/872))
- Added configurable UART-to-MQTT terminator
- Added telnet link to web UI
- Reload terminal command to force all modules to reload settings from config ([#816](https://github.com/xoseperez/espurna/issues/816))
- Added security headers to each HTTP response (thanks to @ITNerdBox)
- Customized GET terminal command (thanks to @mcspr)
- More RC codes supported on TX for RF Bridge (thanks to @wildwiz)
- Support for BL0937 power monitoring chip with unmodified HLW8012 library ([#737](https://github.com/xoseperez/espurna/issues/737))
- Enable CORS
- Support for Allnet ESP8266 UP Relay (thanks to @bajo)
- Support for Tonbux Mosquito Killer (thanks to @gn0st1c)
- Support for Neo Coolcam NAS-WR01W WiFi Smart Power Plug
- Support for TYWE3S-based Estink WiFi Power Strip (thanks to @sandman, [#852](https://github.com/xoseperez/espurna/issues/852))
- Support for Pilotak ESP DIN V1
- Support for DIY Geiger counter (thanks to @Trickx)
- Support for HomeCube / Blitzwolf BW-SHP2
* Support for Vanzavanzu Smart Wifi Plug Mini
- Support for Bruno Horta's OnOfre board
### Changed
- Updated PlatformIO to use Core 3.5.3
- Updated to JustWifi 2.0
- CSS optimizations ([#870](https://github.com/xoseperez/espurna/issues/870), [#871](https://github.com/xoseperez/espurna/issues/871))
- Several changes in OTA Manager
- Better memory layout info
- MQTT keep alive time increased to 300s
- Using ticket-based authentication for WS
- Refactor module and sensor listings ([#896](https://github.com/xoseperez/espurna/issues/896))
- Using alternative math methods to save ~8Kb with lights
- Simpligying mired/kelvin methods
- Changed web UI checkboxes with pure CSS versions
### Deprecated
- {identifier} place-holder in MQTT base topic
## [1.12.6] 2018-05-02
### Fixed
- Check NTP_SUPPORT for sensors (thanks to @mcspr)


+ 49
- 37
README.md View File

@ -3,8 +3,8 @@
ESPurna ("spark" in Catalan) is a custom firmware for ESP8285/ESP8266 based smart switches, lights and sensors.
It uses the Arduino Core for ESP8266 framework and a number of 3rd party libraries.
[![version](https://img.shields.io/badge/version-1.12.7a-brightgreen.svg)](CHANGELOG.md)
[![branch](https://img.shields.io/badge/branch-dev-orange.svg)](https://github.org/xoseperez/espurna/tree/dev/)
[![version](https://img.shields.io/badge/version-1.13.2b-brightgreen.svg)](CHANGELOG.md)
[![branch](https://img.shields.io/badge/branch-dev-orange.svg)](https://github.com/xoseperez/espurna/tree/dev/)
[![travis](https://travis-ci.org/xoseperez/espurna.svg?branch=dev)](https://travis-ci.org/xoseperez/espurna)
[![codacy](https://img.shields.io/codacy/grade/c9496e25cf07434cba786b462cb15f49/dev.svg)](https://www.codacy.com/app/xoseperez/espurna/dashboard)
[![license](https://img.shields.io/github/license/xoseperez/espurna.svg)](LICENSE)
@ -30,6 +30,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Scans for strongest network if more than one defined (also available in web UI)
* Handles correctly multiple AP with the same SSID
* Defaults to AP mode (also available after double clicking the main button)
* Support for **WPS** and **Smart Config** (not available in default builds)
* Network visibility
* Supports mDNS (service reporting and metadata) both server mode and client mode (.local name resolution)
* Supports NetBIOS, LLMNR and Netbios (when built with Arduino Core >= 2.4.0) and SSDP (experimental)
@ -40,6 +41,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Support for **relay synchronization** (all equal, only one ON, one and only on ON)
* Support for **MQTT groups** to sync switches between devices
* Support for **delayed ON/OFF**
* Support for **latched relays**
* **MQTT** enabled
* **SSL/TLS support** (not on regular builds, see [#64](https://github.com/xoseperez/espurna/issues/64))
* Switch on/off and toggle relays, group topics (sync relays between different devices)
@ -68,6 +70,8 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Environment
* **DHT11 / DHT22 / DHT21 / AM2301 / Itead's SI7021**
* **BMP280** and **BME280** temperature, humidity (BME280) and pressure sensor by Bosch
* **TMP35** and **TMP36** analog temperature sensors
* **NTC** temperature sensors
* **SI7021** temperature and humidity sensor
* **SHT3X** temperature and humidity sensor over I2C (Wemos shield)
* **AM2320** temperature and humidity sensor over I2C
@ -77,17 +81,21 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* **PMSX003/PMS5003T/ST** dust sensors
* **BH1750** luminosity sensor
* **GUVAS12SD** UV sensor
* **GEIGER COUNTER** by RH Electronics
* Power monitoring
* **HLW8012** using the [HLW8012 Library](https://bitbucket.org/xoseperez/hlw8012) (Sonoff POW)
* Non-invasive **current sensor** using **internal ADC** or **ADC121** or **ADS1115**
* **ECH1560** power monitor chip
* **CSE7766** and **CSE7759B** power monitor chips
* **HJL-01** and **BL0937** power monitor chips
* Non-invasive **current sensor** using **internal ADC** or **ADC712** or **ADC121** or **ADS1115**
* **V9261F** power monitor chip
* **PZEM0004T** power monitor board
* Raw analog and digital sensors
* Simple pulse counter
* Support for (almost) any UART based sensor via the **UART-to-MQTT module**
* Support for different units (Fahrenheit or Celsius, Watts or Kilowatts, Joules or kWh)
* Support for LED lights
* MY92XX-based light bulbs and PWM LED strips (dimmers) up to 5 channels (RGB, cold white and warm white, for instance)
* Support for CCT lights
* RGB and HSV color codes supported
* Manage channels individually
* Temperature color supported (in [mired](https://en.wikipedia.org/wiki/Mired) and [kelvin](https://en.wikipedia.org/wiki/Color_temperature)) via MQTT / REST API
@ -97,7 +105,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Option to have separate switches for each channel
* Support for simple 433MHz RF receivers
* Support for UART-to-MQTT bidirectional bridge
* Fast asynchronous **HTTP Server**
* Fast asynchronous **HTTP Server** and cool **Web User Interface**
* Configurable port
* Basic authentication
* Web-based configuration
@ -108,7 +116,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Upgrade firmware from the web interface
* Works great behind a [secured reverse proxy](http://tinkerman.cat/secure-remote-access-to-your-iot-devices/)
* **REST API** (enable/disable from web interface)
* GET and PUT relay status
* GET and PUT relay status (including pulses)
* Change light color (for supported hardware)
* GET sensor data (power, current, voltage, temperature and humidity) depending on the available hardware
* Works great behind a secured reverse proxy
@ -137,6 +145,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Long click (>1 second) to reboot device (only main button)
* Extra long click (>10 seconds) to go back to factory settings (only main button)
* Specific definitions for touch button devices (ESPurna Switch, Sonoff Touch & T1)
* Configuration stored in different sectors to prevent data loosing and corruption
## Notices
@ -199,55 +208,58 @@ Here is the list of supported hardware. For more information please refer to the
||||
|---|---|---|
|![Tinkerman Espurna H](images/devices/tinkerman-espurna-h.jpg)|||
|**Tinkerman ESPurna H**|||
|![Itead Sonoff RF Bridge](images/devices/itead-sonoff-rfbridge.jpg)|||
|**Itead Sonoff RF Bridge**|||
|![Itead Sonoff Basic](images/devices/itead-sonoff-basic.jpg)|![Itead Sonoff RF](images/devices/itead-sonoff-rf.jpg)|![Itead Sonoff Dual/Dual R2](images/devices/itead-sonoff-dual.jpg)|
|**Itead Sonoff Basic**|**Itead Sonoff RF**|**Itead Sonoff Dual/Dual R2**|
|![Itead Sonoff POW](images/devices/itead-sonoff-pow.jpg)|![Itead Sonoff TH10/TH16](images/devices/itead-sonoff-th.jpg)|![Electrodragon WiFi IOT](images/devices/electrodragon-wifi-iot.jpg)|
|**Itead Sonoff POW**|**Itead Sonoff TH10/TH16**|**Electrodragon WiFi IOT**|
|![Itead Sonoff 4CH](images/devices/itead-sonoff-4ch.jpg)|![Itead Sonoff 4CH Pro](images/devices/itead-sonoff-4ch-pro.jpg)|![OpenEnergyMonitor WiFi MQTT Relay / Thermostat](images/devices/openenergymonitor-mqtt-relay.jpg)|
|**Itead Sonoff 4CH**|**Itead Sonoff 4CH Pro**|**OpenEnergyMonitor WiFi MQTT Relay / Thermostat**|
|![Itead S20](images/devices/itead-s20.jpg)|![WorkChoice EcoPlug](images/devices/workchoice-ecoplug.jpg)|![Power meters based on V9261F and ECH1560](images/devices/generic-v9261f.jpg)|
|**Itead S20**|**WorkChoice EcoPlug**|**Power meters based on V9261F and ECH1560**|
|![Tinkerman Espurna H](images/devices/tinkerman-espurna-h.jpg)|![Tinkerman RFM69GW](images/devices/tinkerman-rfm69gw.jpg)||
|**Tinkerman ESPurna H**|**Tinkerman RFM69GW**||
|![Itead Sonoff RF Bridge](images/devices/itead-sonoff-rfbridge.jpg)|![Itead Sonoff RF](images/devices/itead-sonoff-rf.jpg)|![Itead Sonoff 4CH](images/devices/itead-sonoff-4ch.jpg)|
|**Itead Sonoff RF Bridge**|**Itead Sonoff RF**|**Itead Sonoff 4CH**|
|![Itead Sonoff 4CH Pro](images/devices/itead-sonoff-4ch-pro.jpg)|||
|**Itead Sonoff 4CH Pro**|||
|![Itead Sonoff S31](images/devices/itead-sonoff-s31.jpg)|![BlitzWolf BW-SPP2](images/devices/blitzwolf-bw-shp2.jpg)|![Power meters based on V9261F](images/devices/generic-v9261f.jpg)|
|**Itead Sonoff S31**|**Blitzwolf BW-SHP2<br />(also by HomeCube, Coosa, Goosund)**|**Power meters based on V9261F**|
|![Itead Sonoff POW](images/devices/itead-sonoff-pow.jpg)|![Itead Sonoff POW](images/devices/itead-sonoff-pow-r2.jpg)|![Vanzavanzu Smart WiFi Plug Mini](images/devices/vanzavanzu-smart-wifi-plug-mini.jpg)|
|**Itead Sonoff POW**|**Itead Sonoff POW R2**|**Vanzavanzu Smart WiFi Plug Mini**|
|![Itead Sonoff Basic](images/devices/itead-sonoff-basic.jpg)|![Itead Sonoff Dual/Dual R2](images/devices/itead-sonoff-dual.jpg)|![Itead Sonoff TH10/TH16](images/devices/itead-sonoff-th.jpg)|
|**Itead Sonoff Basic**|**Itead Sonoff Dual/Dual R2**|**Itead Sonoff TH10/TH16**|
|![Electrodragon WiFi IOT](images/devices/electrodragon-wifi-iot.jpg)|![OpenEnergyMonitor WiFi MQTT Relay / Thermostat](images/devices/openenergymonitor-mqtt-relay.jpg)||
|**Electrodragon WiFi IOT**|**OpenEnergyMonitor WiFi MQTT Relay / Thermostat**||
|![Itead S20](images/devices/itead-s20.jpg)|![WorkChoice EcoPlug](images/devices/workchoice-ecoplug.jpg)|![Neo Coolcam NAS WR01W](images/devices/neo-coolcam-wifi.jpg)|
|**Itead S20**|**WorkChoice EcoPlug**|**Neo Coolcam NAS WR01W**|
|![Schuko Wifi Plug](images/devices/schuko-wifi-plug.jpg)|![KMC 70011](images/devices/kmc-70011.jpg)|![Xenon SM-PW702U](images/devices/xenon-sm-pw702u.jpg)|
|**Schuko Wifi Plug**|**KMC 70011**|**Xenon SM-PW702U**|
|![Maxcio W-US002S](images/devices/maxcio-w-us002s.jpg)|![HEYGO HY02](images/devices/heygo-hy02.jpg)|![YiDian XS-SSA05](images/devices/yidian-xs-ssa05.jpg)|
|**Maxcio W-US002S**|**HEYGO HY02**|**YiDian XS-SSA05**|
|![WiOn 50055](images/devices/wion-50055.jpg)|![LINGAN SWA1](images/devices/lingan-swa1.jpg)|![Tonbux PowerStrip02](images/devices/tonbux-powerstrip02.jpg)|
|**WiOn 50055**|**LINGAN SWA1**|**Tonbux PowerStrip02**
|![WiOn 50055](images/devices/wion-50055.jpg)|![LINGAN SWA1](images/devices/lingan-swa1.jpg)||
|**WiOn 50055**|**LINGAN SWA1**||
|![Tonbux PowerStrip02](images/devices/tonbux-powerstrip02.jpg)|![ForNorm Power Strip](images/devices/fornorm-power-strip.jpg)|![Zhilde ZLD-EU55-W](images/devices/zhilde-zld-eu55-w.jpg)|
|**Tonbux PowerStrip02**|**Fornorm Power Strip**|**Zhilde ZLD-EU55-W**|
|![Itead Sonoff Touch](images/devices/itead-sonoff-touch.jpg)|![Itead Sonoff T1](images/devices/itead-sonoff-t1.jpg)|![YJZK 2-gang switch](images/devices/yjzk-2gang-switch.jpg)|
|**Itead Sonoff Touch**|**Itead Sonoff T1**|**YJZK 2-gang switch**|
|![Itead Slampher](images/devices/itead-slampher.jpg)|||
|**Itead Slampher**|||
|![Itead Sonoff B1](images/devices/itead-sonoff-b1.jpg)|![AI-Thinker Wifi Light / Noduino OpenLight](images/devices/aithinker-ai-light.jpg)|![Authometion LYT8266](images/devices/authometion-lyt8266.jpg)|
|**Itead Sonoff B1**|**AI-Thinker Wifi Light / Noduino OpenLight**|**Authometion LYT8266**|
|![Arilux E27](images/devices/arilux-e27.jpg)|![Itead Sonoff LED](images/devices/itead-sonoff-led.jpg)|![Itead BN-SZ01](images/devices/itead-bn-sz01.jpg)|
|**Arilux E27**|**Itead Sonoff LED**|**Itead BN-SZ01**|
|![Itead Slampher](images/devices/itead-slampher.jpg)|![Arilux E27](images/devices/arilux-e27.jpg)|![Itead Sonoff B1](images/devices/itead-sonoff-b1.jpg)|
|**Itead Slampher**|**Arilux E27**|**Itead Sonoff B1**|
|![AI-Thinker Wifi Light / Noduino OpenLight](images/devices/aithinker-ai-light.jpg)|![Authometion LYT8266](images/devices/authometion-lyt8266.jpg)||
|**AI-Thinker Wifi Light / Noduino OpenLight**|**Authometion LYT8266**||
|![Itead Sonoff LED](images/devices/itead-sonoff-led.jpg)|![Itead BN-SZ01](images/devices/itead-bn-sz01.jpg)|![InterMitTech QuinLED 2.6](images/devices/intermittech-quinled-2.6.jpg)|
|**Itead Sonoff LED**|**Itead BN-SZ01**|**InterMitTech QuinLED 2.6**|
|![Arilux AL-LC01 (RGB)](images/devices/arilux-al-lc01.jpg)|![Arilux AL-LC02 (RGBW)](images/devices/arilux-al-lc02.jpg)|![Arilux AL-LC06 (RGBWWCW)](images/devices/arilux-al-lc06.jpg)|
|**Arilux AL-LC01 (RGB)**|**Arilux AL-LC02 (RGBW)**|**Arilux AL-LC06 (RGBWWCW)**|
|![Arilux AL-LC11 (RGBWWW) & RF](images/devices/arilux-al-lc11.jpg)|![MagicHome LED Controller (1.0 and 2.x)](images/devices/magichome-led-controller.jpg)|![Huacanxing H801/802](images/devices/huacanxing-h801.jpg)|
|**Arilux AL-LC11 (RGBWWW) & RF**|**MagicHome LED Controller (1.0/2.x)**|**Huacanxing H801/802**|
|![InterMitTech QuinLED 2.6](images/devices/intermittech-quinled-2.6.jpg)||
|**InterMitTech QuinLED 2.6**||
|![Itead Sonoff SV](images/devices/itead-sonoff-sv.jpg)|![Itead 1CH Inching](images/devices/itead-1ch-inching.jpg)|![Itead Motor Clockwise/Anticlockwise](images/devices/itead-motor.jpg)|
|**Itead Sonoff SV**|**Itead 1CH Inching**|**Itead Motor Clockwise/Anticlockwise**|
|![Jan Goedeke Wifi Relay (NO/NC)](images/devices/jangoe-wifi-relay.jpg)|![Jorge García Wifi + Relays Board Kit](images/devices/jorgegarcia-wifi-relays.jpg)|![EXS Wifi Relay v3.1](images/devices/exs-wifi-relay-v31.jpg)|
|**Jan Goedeke Wifi Relay (NO/NC)**|**Jorge García Wifi + Relays Board Kit**|**EXS Wifi Relay v3.1**|
|![ManCaveMade ESP-Live](images/devices/mancavemade-esp-live.jpg)|![Wemos D1 Mini Relay Shield](images/devices/wemos-d1-mini-relayshield.jpg)|![Witty Cloud](images/devices/witty-cloud.jpg)|
|**ManCaveMade ESP-Live**|**Wemos D1 Mini Relay Shield**|**Witty Cloud**|
|![IKE ESPike](images/devices/ike-espike.jpg)||![Arniex Swifitch](images/devices/arniex-swifitch.jpg)|
|**IKE ESPike**|**STM_RELAY**|**Arniex Swifitch**|
|![IKE ESPike](images/devices/ike-espike.jpg)|![Pilotak ESP DIN](images/devices/pilotak-esp-din.jpg)|![Arniex Swifitch](images/devices/arniex-swifitch.jpg)|
|**IKE ESPike**|**Pilotak ESP DIN|**Arniex Swifitch**|
|![Heltec Touch Relay](images/devices/heltec-touch-relay.jpg)|![Generic Relay v4.0](images/devices/generic-relay-40.jpg)|![Generic RGBLed v1.0](images/devices/generic-rgbled-10.jpg)|
|**Heltec Touch Relay**|**Generic Relay v4.0**|**Generic RGBLed v1.0**|
|![Generic DHT11 v1.0](images/devices/generic-dht11-10.jpg)|![Generic DS18B20 v1.0](images/devices/generic-ds18b20-10.jpg)||
|**Generic DHT11 v1.0**|**Generic DS18B20 v1.0**||
|![Tonbux Mosquito Killer](images/devices/tonbux-mosquito-killer.jpg)|||
|**Tonbux Mosquito Killer**|||
**Other supported boards:**
IteadStudio Sonoff S31, IteadStudio Sonoff POW R2, Zhilde ZLD-EU55-W, Luani HVIO
|![Generic DHT11 v1.0](images/devices/generic-dht11-10.jpg)|![Generic DS18B20 v1.0](images/devices/generic-ds18b20-10.jpg)|![Bruno Horta's OnOfre](images/devices/bh-onofre.jpg)|
|**Generic DHT11 v1.0**|**Generic DS18B20 v1.0**|**Bruno Horta's OnOfre**|
|![Allnet ESP8266-UP-Relay](images/devices/allnet-esp8266-up-relay.jpg)|![RH Electronics Geiger Counter](images/devices/generic-geiger-diy.png)|![Luani HVIO](images/devices/luani-hvio.jpg)|
|**Allnet ESP8266-UP-Relay**|**RH Electronics Geiger Counter**|**Luani HVIO**|
|![Tonbux Mosquito Killer](images/devices/tonbux-mosquito-killer.jpg)|![Itead Sonoff IFAN02](images/devices/itead-sonoff-ifan02.jpg)||
|**Tonbux Mosquito Killer**|**Itead Sonoff IFAN02**|||
**Other supported boards (beta):**
KMC 4 Outlet, Gosund WS1, Smart Dual Plug, MakerFocus Intelligent Module LM33 for Lamps


+ 120
- 49
code/build.sh View File

@ -1,73 +1,144 @@
#!/bin/bash
set -e
# Welcome
echo "--------------------------------------------------------------"
echo "ESPURNA FIRMWARE BUILDER"
# Utility
is_git() {
command -v git >/dev/null 2>&1 || return 1
command git rev-parse >/dev/null 2>&1 || return 1
return 0
}
# Script settings
version=$(grep APP_VERSION espurna/config/version.h | awk '{print $3}' | sed 's/"//g')
if is_git; then
git_revision=$(git rev-parse --short HEAD)
git_version=${version}-${git_revision}
else
git_revision=
git_version=$version
fi
par_build=false
par_thread=${BUILDER_THREAD:-0}
par_total_threads=${BUILDER_TOTAL_THREADS:-4}
if [ ${par_thread} -ne ${par_thread} -o \
${par_total_threads} -ne ${par_total_threads} ]; then
echo "Parallel threads should be a number."
exit
fi
if [ ${par_thread} -ge ${par_total_threads} ]; then
echo "Current thread is greater than total threads. Doesn't make sense"
exit
fi
# Available environments
travis=$(grep env: platformio.ini | grep travis | sed 's/\[env://' | sed 's/\]/ /' | sort)
available=$(grep env: platformio.ini | grep -v ota | grep -v ssl | grep -v travis | sed 's/\[env://' | sed 's/\]/ /' | sort)
list_envs() {
grep env: platformio.ini | sed 's/\[env:\(.*\)\]/\1/g'
}
# Parameters
environments=$@
if [ "$environments" == "list" ]; then
travis=$(list_envs | grep travis | sort)
available=$(list_envs | grep -Ev -- '-ota$|-ssl$|^travis' | sort)
# Build tools settings
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DAPP_REVISION='\"$git_revision\"'"
# Functions
print_available() {
echo "--------------------------------------------------------------"
echo "Available environments:"
for environment in $available; do
echo "* $environment"
done
exit
fi
}
# Environments to build
if [ $# -eq 0 ]; then
environments=$available
print_environments() {
echo "--------------------------------------------------------------"
echo "Current environments:"
for environment in $environments; do
echo "* $environment"
done
}
# Hook to build travis test envs
if [[ "${TRAVIS_BRANCH}" != "" ]]; then
re='^[0-9]+\.[0-9]+\.[0-9]+$'
if ! [[ ${TRAVIS_BRANCH} =~ $re ]]; then
environments=$travis
fi
set_default_environments() {
# Hook to build in parallel when using travis
if [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Release" ]] && ${par_build}; then
environments=$(echo ${available} | \
awk -v par_thread=${par_thread} -v par_total_threads=${par_total_threads} \
'{ for (i = 1; i <= NF; i++) if (++j % par_total_threads == par_thread ) print $i; }')
return
fi
fi
# Only build travisN
if [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]]; then
environments=$travis
return
fi
# Get current version
version=$(grep APP_VERSION espurna/config/version.h | awk '{print $3}' | sed 's/"//g')
echo "Building for version $version"
# Fallback to all available environments
environments=$available
}
# Create output folder
mkdir -p firmware
build_webui() {
# Build system uses gulpscript.js to build web interface
if [ ! -e node_modules/gulp/bin/gulp.js ]; then
echo "--------------------------------------------------------------"
echo "Installing dependencies..."
npm install --only=dev
fi
if [ ! -e node_modules/gulp/bin/gulp.js ]; then
# Recreate web interface (espurna/data/index.html.*.gz.h)
echo "--------------------------------------------------------------"
echo "Installing dependencies..."
npm install --only=dev
fi
echo "Building web interface..."
node node_modules/gulp/bin/gulp.js || exit
}
echo "--------------------------------------------------------------"
echo "Get revision..."
revision=$(git rev-parse HEAD)
revision=${revision:0:7}
cp espurna/config/version.h espurna/config/version.h.original
sed -i -e "s/APP_REVISION \".*\"/APP_REVISION \"$revision\"/g" espurna/config/version.h
build_environments() {
echo "--------------------------------------------------------------"
echo "Building firmware images..."
mkdir -p ../firmware/espurna-$version
# Recreate web interface
echo "--------------------------------------------------------------"
echo "Building web interface..."
node node_modules/gulp/bin/gulp.js || exit
for environment in $environments; do
echo -n "* espurna-$version-$environment.bin --- "
platformio run --silent --environment $environment || exit 1
stat -c %s .pioenvs/$environment/firmware.bin
[[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]] || \
mv .pioenvs/$environment/firmware.bin ../firmware/espurna-$version/espurna-$version-$environment.bin
done
echo "--------------------------------------------------------------"
}
# Build all the required firmware images
echo "--------------------------------------------------------------"
echo "Building firmware images..."
mkdir -p ../firmware/espurna-$version
for environment in $environments; do
echo "* espurna-$version-$environment.bin"
platformio run --silent --environment $environment || exit 1
mv .pioenvs/$environment/firmware.bin ../firmware/espurna-$version/espurna-$version-$environment.bin
# Parameters
while getopts "lp" opt; do
case $opt in
l)
print_available
exit
;;
p)
par_build=true
;;
esac
done
shift $((OPTIND-1))
# Welcome
echo "--------------------------------------------------------------"
echo "ESPURNA FIRMWARE BUILDER"
echo "Building for version ${git_version}"
# Environments to build
environments=$@
if [ $# -eq 0 ]; then
set_default_environments
fi
if ${CI:-false}; then
print_environments
fi
mv espurna/config/version.h.original espurna/config/version.h
build_webui
build_environments

code/esp8266.flash.1m0.ld → code/eagle.flash.1m0m1s.ld View File

@ -1,6 +1,7 @@
/* Flash Split for 1M chips, no SPIFFS */
/* Flash Split for 1M chips */
/* sketch 999KB */
/* eeprom 20KB */
/* eeprom 4KB */
/* reserved 16KB */
MEMORY
{
@ -12,7 +13,7 @@ MEMORY
PROVIDE ( _SPIFFS_start = 0x402FB000 );
PROVIDE ( _SPIFFS_end = 0x402FB000 );
PROVIDE ( _SPIFFS_page = 0 );
PROVIDE ( _SPIFFS_block = 0 );
PROVIDE ( _SPIFFS_page = 0x0 );
PROVIDE ( _SPIFFS_block = 0x0 );
INCLUDE "esp8266.flash.common.ld"
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 19
- 0
code/eagle.flash.1m0m2s.ld View File

@ -0,0 +1,19 @@
/* Flash Split for 1M chips, no SPIFFS */
/* sketch 995KB */
/* eeprom 8KB */
/* reserved 16KB */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xf8ff0
}
PROVIDE ( _SPIFFS_start = 0x402FA000 );
PROVIDE ( _SPIFFS_end = 0x402FA000 );
PROVIDE ( _SPIFFS_page = 0 );
PROVIDE ( _SPIFFS_block = 0 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 21
- 0
code/eagle.flash.4m1m4s.ld View File

@ -0,0 +1,21 @@
/* Flash Split for 4M chips */
/* sketch 1019KB */
/* empty/ota? 2048KB */
/* spiffs 992KB */
/* eeprom 16KB */
/* reserved 16KB */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xfeff0
}
PROVIDE ( _SPIFFS_start = 0x40500000 );
PROVIDE ( _SPIFFS_end = 0x405F8000 );
PROVIDE ( _SPIFFS_page = 0x100 );
PROVIDE ( _SPIFFS_block = 0x2000 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 20
- 0
code/eagle.flash.4m3m4e.ld View File

@ -0,0 +1,20 @@
/* Flash Split for 4M chips */
/* sketch 1019KB */
/* spiffs 3040KB */
/* eeprom 16KB */
/* reserved 16KB */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xfeff0
}
PROVIDE ( _SPIFFS_start = 0x40300000 );
PROVIDE ( _SPIFFS_end = 0x405F8000 );
PROVIDE ( _SPIFFS_page = 0x100 );
PROVIDE ( _SPIFFS_block = 0x2000 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 19
- 0
code/eagle.flash.512k0m1s.ld View File

@ -0,0 +1,19 @@
/* Flash Split for 512K chips */
/* sketch 487KB */
/* eeprom 4KB */
/* reserved 16KB */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0x79ff0
}
PROVIDE ( _SPIFFS_start = 0x4027B000 );
PROVIDE ( _SPIFFS_end = 0x4027B000 );
PROVIDE ( _SPIFFS_page = 0x0 );
PROVIDE ( _SPIFFS_block = 0x0 );
INCLUDE "../ld/eagle.app.v6.common.ld"

+ 76
- 53
code/espurna/api.ino View File

@ -6,7 +6,7 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if WEB_SUPPORT
#if API_SUPPORT
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
@ -27,6 +27,7 @@ bool _apiWebSocketOnReceive(const char * key, JsonVariant& value) {
}
void _apiWebSocketOnSend(JsonObject& root) {
root["apiVisible"] = 1;
root["apiEnabled"] = getSetting("apiEnabled", API_ENABLED).toInt() == 1;
root["apiKey"] = getSetting("apiKey");
root["apiRealTime"] = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1;
@ -70,48 +71,6 @@ bool _asJson(AsyncWebServerRequest *request) {
return asJson;
}
ArRequestHandlerFunction _bindAPI(unsigned int apiID) {
return [apiID](AsyncWebServerRequest *request) {
webLog(request);
if (!_authAPI(request)) return;
web_api_t api = _apis[apiID];
// Check if its a PUT
if (api.putFn != NULL) {
if (request->hasParam("value", request->method() == HTTP_PUT)) {
AsyncWebParameter* p = request->getParam("value", request->method() == HTTP_PUT);
(api.putFn)((p->value()).c_str());
}
}
// Get response from callback
char value[API_BUFFER_SIZE] = {0};
(api.getFn)(value, API_BUFFER_SIZE);
// The response will be a 404 NOT FOUND if the resource is not available
if (0 == value[0]) {
DEBUG_MSG_P(PSTR("[API] Sending 404 response\n"));
request->send(404);
return;
}
DEBUG_MSG_P(PSTR("[API] Sending response '%s'\n"), value);
// Format response according to the Accept header
if (_asJson(request)) {
char buffer[64];
snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": %s }"), api.key, value);
request->send(200, "application/json", buffer);
} else {
request->send(200, "text/plain", value);
}
};
}
void _onAPIs(AsyncWebServerRequest *request) {
webLog(request);
@ -130,6 +89,7 @@ void _onAPIs(AsyncWebServerRequest *request) {
root[_apis[i].key] = String(buffer);
}
root.printTo(output);
jsonBuffer.clear();
request->send(200, "application/json", output);
} else {
@ -167,31 +127,94 @@ void _onRPC(AsyncWebServerRequest *request) {
}
bool _apiRequestCallback(AsyncWebServerRequest *request) {
String url = request->url();
// Main API entry point
if (url.equals("/api") || url.equals("/apis")) {
_onAPIs(request);
return true;
}
// Main RPC entry point
if (url.equals("/rpc")) {
_onRPC(request);
return true;
}
// Not API request
if (!url.startsWith("/api/")) return false;
for (unsigned char i=0; i < _apis.size(); i++) {
// Search API url
web_api_t api = _apis[i];
if (!url.endsWith(api.key)) continue;
// Log and check credentials
webLog(request);
if (!_authAPI(request)) return false;
// Check if its a PUT
if (api.putFn != NULL) {
if (request->hasParam("value", request->method() == HTTP_PUT)) {
AsyncWebParameter* p = request->getParam("value", request->method() == HTTP_PUT);
(api.putFn)((p->value()).c_str());
}
}
// Get response from callback
char value[API_BUFFER_SIZE] = {0};
(api.getFn)(value, API_BUFFER_SIZE);
// The response will be a 404 NOT FOUND if the resource is not available
if (0 == value[0]) {
DEBUG_MSG_P(PSTR("[API] Sending 404 response\n"));
request->send(404);
return false;
}
DEBUG_MSG_P(PSTR("[API] Sending response '%s'\n"), value);
// Format response according to the Accept header
if (_asJson(request)) {
char buffer[64];
if (isNumber(value)) {
snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": %s }"), api.key, value);
} else {
snprintf_P(buffer, sizeof(buffer), PSTR("{ \"%s\": \"%s\" }"), api.key, value);
}
request->send(200, "application/json", buffer);
} else {
request->send(200, "text/plain", value);
}
return true;
}
return false;
}
// -----------------------------------------------------------------------------
void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn) {
// Store it
web_api_t api;
char buffer[40];
snprintf_P(buffer, sizeof(buffer), PSTR("/api/%s"), key);
api.key = strdup(key);
api.getFn = getFn;
api.putFn = putFn;
_apis.push_back(api);
// Bind call
unsigned int methods = HTTP_GET;
if (putFn != NULL) methods += HTTP_PUT;
webServer()->on(buffer, methods, _bindAPI(_apis.size() - 1));
}
void apiSetup() {
webServer()->on("/apis", HTTP_GET, _onAPIs);
webServer()->on("/rpc", HTTP_GET, _onRPC);
wsOnSendRegister(_apiWebSocketOnSend);
wsOnReceiveRegister(_apiWebSocketOnReceive);
webRequestRegister(_apiRequestCallback);
}
#endif // WEB_SUPPORT
#endif // API_SUPPORT

+ 26
- 13
code/espurna/button.ino View File

@ -10,6 +10,8 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
// BUTTON
// -----------------------------------------------------------------------------
#if BUTTON_SUPPORT
#include <DebounceEvent.h>
#include <vector>
@ -60,16 +62,18 @@ unsigned char buttonAction(unsigned char id, unsigned char event) {
if (event == BUTTON_EVENT_DBLCLICK) return (actions >> 8) & 0x0F;
if (event == BUTTON_EVENT_LNGCLICK) return (actions >> 12) & 0x0F;
if (event == BUTTON_EVENT_LNGLNGCLICK) return (actions >> 16) & 0x0F;
if (event == BUTTON_EVENT_TRIPLECLICK) return (actions >> 20) & 0x0F;
return BUTTON_MODE_NONE;
}
unsigned long buttonStore(unsigned long pressed, unsigned long click, unsigned long dblclick, unsigned long lngclick, unsigned long lnglngclick) {
unsigned long buttonStore(unsigned long pressed, unsigned long click, unsigned long dblclick, unsigned long lngclick, unsigned long lnglngclick, unsigned long tripleclick) {
unsigned int value;
value = pressed;
value += click << 4;
value += dblclick << 8;
value += lngclick << 12;
value += lnglngclick << 16;
value += tripleclick << 20;
return value;
}
@ -77,12 +81,13 @@ uint8_t mapEvent(uint8_t event, uint8_t count, uint16_t length) {
if (event == EVENT_PRESSED) return BUTTON_EVENT_PRESSED;
if (event == EVENT_CHANGED) return BUTTON_EVENT_CLICK;
if (event == EVENT_RELEASED) {
if (count == 1) {
if (1 == count) {
if (length > BUTTON_LNGLNGCLICK_DELAY) return BUTTON_EVENT_LNGLNGCLICK;
if (length > BUTTON_LNGCLICK_DELAY) return BUTTON_EVENT_LNGCLICK;
return BUTTON_EVENT_CLICK;
}
if (count == 2) return BUTTON_EVENT_DBLCLICK;
if (2 == count) return BUTTON_EVENT_DBLCLICK;
if (3 == count) return BUTTON_EVENT_TRIPLECLICK;
}
return BUTTON_EVENT_NONE;
}
@ -113,7 +118,13 @@ void buttonEvent(unsigned int id, unsigned char event) {
relayStatus(_buttons[id].relayID - 1, false);
}
}
if (action == BUTTON_MODE_AP) createAP();
if (action == BUTTON_MODE_AP) wifiStartAP();
#if defined(JUSTWIFI_ENABLE_WPS)
if (action == BUTTON_MODE_WPS) wifiStartWPS();
#endif // defined(JUSTWIFI_ENABLE_WPS)
#if defined(JUSTWIFI_ENABLE_SMARTCONFIG)
if (action == BUTTON_MODE_SMART_CONFIG) wifiStartSmartConfig();
#endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG)
if (action == BUTTON_MODE_RESET) {
deferredReset(100, CUSTOM_RESET_HARDWARE);
}
@ -129,7 +140,7 @@ void buttonSetup() {
#ifdef ITEAD_SONOFF_DUAL
unsigned int actions = buttonStore(BUTTON_MODE_NONE, BUTTON_MODE_TOGGLE, BUTTON_MODE_NONE, BUTTON_MODE_NONE, BUTTON_MODE_NONE);
unsigned int actions = buttonStore(BUTTON_MODE_NONE, BUTTON_MODE_TOGGLE, BUTTON_MODE_NONE, BUTTON_MODE_NONE, BUTTON_MODE_NONE, BUTTON_MODE_NONE);
_buttons.push_back({new DebounceEvent(0, BUTTON_PUSHBUTTON), actions, 1});
_buttons.push_back({new DebounceEvent(0, BUTTON_PUSHBUTTON), actions, 2});
_buttons.push_back({new DebounceEvent(0, BUTTON_PUSHBUTTON), actions, BUTTON3_RELAY});
@ -140,49 +151,49 @@ void buttonSetup() {
#if BUTTON1_PIN != GPIO_NONE
{
unsigned int actions = buttonStore(BUTTON1_PRESS, BUTTON1_CLICK, BUTTON1_DBLCLICK, BUTTON1_LNGCLICK, BUTTON1_LNGLNGCLICK);
unsigned int actions = buttonStore(BUTTON1_PRESS, BUTTON1_CLICK, BUTTON1_DBLCLICK, BUTTON1_LNGCLICK, BUTTON1_LNGLNGCLICK, BUTTON1_TRIPLECLICK);
_buttons.push_back({new DebounceEvent(BUTTON1_PIN, BUTTON1_MODE, BUTTON_DEBOUNCE_DELAY, btnDelay), actions, BUTTON1_RELAY});
}
#endif
#if BUTTON2_PIN != GPIO_NONE
{
unsigned int actions = buttonStore(BUTTON2_PRESS, BUTTON2_CLICK, BUTTON2_DBLCLICK, BUTTON2_LNGCLICK, BUTTON2_LNGLNGCLICK);
unsigned int actions = buttonStore(BUTTON2_PRESS, BUTTON2_CLICK, BUTTON2_DBLCLICK, BUTTON2_LNGCLICK, BUTTON2_LNGLNGCLICK, BUTTON2_TRIPLECLICK);
_buttons.push_back({new DebounceEvent(BUTTON2_PIN, BUTTON2_MODE, BUTTON_DEBOUNCE_DELAY, btnDelay), actions, BUTTON2_RELAY});
}
#endif
#if BUTTON3_PIN != GPIO_NONE
{
unsigned int actions = buttonStore(BUTTON3_PRESS, BUTTON3_CLICK, BUTTON3_DBLCLICK, BUTTON3_LNGCLICK, BUTTON3_LNGLNGCLICK);
unsigned int actions = buttonStore(BUTTON3_PRESS, BUTTON3_CLICK, BUTTON3_DBLCLICK, BUTTON3_LNGCLICK, BUTTON3_LNGLNGCLICK, BUTTON3_TRIPLECLICK);
_buttons.push_back({new DebounceEvent(BUTTON3_PIN, BUTTON3_MODE, BUTTON_DEBOUNCE_DELAY, btnDelay), actions, BUTTON3_RELAY});
}
#endif
#if BUTTON4_PIN != GPIO_NONE
{
unsigned int actions = buttonStore(BUTTON4_PRESS, BUTTON4_CLICK, BUTTON4_DBLCLICK, BUTTON4_LNGCLICK, BUTTON4_LNGLNGCLICK);
unsigned int actions = buttonStore(BUTTON4_PRESS, BUTTON4_CLICK, BUTTON4_DBLCLICK, BUTTON4_LNGCLICK, BUTTON4_LNGLNGCLICK, BUTTON4_TRIPLECLICK);
_buttons.push_back({new DebounceEvent(BUTTON4_PIN, BUTTON4_MODE, BUTTON_DEBOUNCE_DELAY, btnDelay), actions, BUTTON4_RELAY});
}
#endif
#if BUTTON5_PIN != GPIO_NONE
{
unsigned int actions = buttonStore(BUTTON5_PRESS, BUTTON5_CLICK, BUTTON5_DBLCLICK, BUTTON5_LNGCLICK, BUTTON5_LNGLNGCLICK);
unsigned int actions = buttonStore(BUTTON5_PRESS, BUTTON5_CLICK, BUTTON5_DBLCLICK, BUTTON5_LNGCLICK, BUTTON5_LNGLNGCLICK, BUTTON5_TRIPLECLICK);
_buttons.push_back({new DebounceEvent(BUTTON5_PIN, BUTTON5_MODE, BUTTON_DEBOUNCE_DELAY, btnDelay), actions, BUTTON5_RELAY});
}
#endif
#if BUTTON6_PIN != GPIO_NONE
{
unsigned int actions = buttonStore(BUTTON6_PRESS, BUTTON6_CLICK, BUTTON6_DBLCLICK, BUTTON6_LNGCLICK, BUTTON6_LNGLNGCLICK);
unsigned int actions = buttonStore(BUTTON6_PRESS, BUTTON6_CLICK, BUTTON6_DBLCLICK, BUTTON6_LNGCLICK, BUTTON6_LNGLNGCLICK, BUTTON6_TRIPLECLICK);
_buttons.push_back({new DebounceEvent(BUTTON6_PIN, BUTTON6_MODE, BUTTON_DEBOUNCE_DELAY, btnDelay), actions, BUTTON6_RELAY});
}
#endif
#if BUTTON7_PIN != GPIO_NONE
{
unsigned int actions = buttonStore(BUTTON7_PRESS, BUTTON7_CLICK, BUTTON7_DBLCLICK, BUTTON7_LNGCLICK, BUTTON7_LNGLNGCLICK);
unsigned int actions = buttonStore(BUTTON7_PRESS, BUTTON7_CLICK, BUTTON7_DBLCLICK, BUTTON7_LNGCLICK, BUTTON7_LNGLNGCLICK, BUTTON7_TRIPLECLICK);
_buttons.push_back({new DebounceEvent(BUTTON7_PIN, BUTTON7_MODE, BUTTON_DEBOUNCE_DELAY, btnDelay), actions, BUTTON7_RELAY});
}
#endif
#if BUTTON8_PIN != GPIO_NONE
{
unsigned int actions = buttonStore(BUTTON8_PRESS, BUTTON8_CLICK, BUTTON8_DBLCLICK, BUTTON8_LNGCLICK, BUTTON8_LNGLNGCLICK);
unsigned int actions = buttonStore(BUTTON8_PRESS, BUTTON8_CLICK, BUTTON8_DBLCLICK, BUTTON8_LNGCLICK, BUTTON8_LNGLNGCLICK, BUTTON8_TRIPLECLICK);
_buttons.push_back({new DebounceEvent(BUTTON8_PIN, BUTTON8_MODE, BUTTON_DEBOUNCE_DELAY, btnDelay), actions, BUTTON8_RELAY});
}
#endif
@ -257,3 +268,5 @@ void buttonLoop() {
#endif
}
#endif // BUTTON_SUPPORT

+ 1
- 1
code/espurna/config/all.h View File

@ -29,10 +29,10 @@
#include "hardware.h"
#include "defaults.h"
#include "general.h"
#include "dependencies.h"
#include "prototypes.h"
#include "sensors.h"
#include "progmem.h"
#include "dependencies.h"
#include "debug.h"
#ifdef USE_CORE_VERSION_H


+ 16
- 3
code/espurna/config/arduino.h View File

@ -8,9 +8,11 @@
//--------------------------------------------------------------------------------
//#define NODEMCU_LOLIN
//#define WEMOS_D1_MINI
//#define WEMOS_D1_MINI_RELAYSHIELD
//#define TINKERMAN_ESPURNA_H06
//#define TINKERMAN_ESPURNA_H08
//#define TINKERMAN_RFM69GW
//#define ITEAD_SONOFF_BASIC
//#define ITEAD_SONOFF_RF
//#define ITEAD_SONOFF_TH
@ -82,16 +84,22 @@
//#define LUANI_HVIO
//#define ALLNET_4DUINO_IOT_WLAN_RELAIS
//#define TONBUX_MOSQUITO_KILLER
//#define NEO_COOLCAM_POWER_PLUG_WIFI
//#define NEO_COOLCAM_NAS_WR01W
//#define ESTINK_WIFI_POWER_STRIP
//#define PILOTAK_ESP_DIN_V1
//#define BLITZWOLF_BWSHP2
//#define BH_ONOFRE
//#define ITEAD_SONOFF_IFAN02
//#define GENERIC_AG_L4
//--------------------------------------------------------------------------------
// Features (values below are non-default values)
//--------------------------------------------------------------------------------
//#define ALEXA_SUPPORT 0
//#define API_SUPPORT 0
//#define BROKER_SUPPORT 0
//#define BUTTON_SUPPORT 0
//#define DEBUG_SERIAL_SUPPORT 0
//#define DEBUG_TELNET_SUPPORT 0
//#define DEBUG_UDP_SUPPORT 1
@ -101,15 +109,18 @@
//#define I2C_SUPPORT 1
//#define INFLUXDB_SUPPORT 1
//#define IR_SUPPORT 1
//#define LED_SUPPORT 0
//#define LLMNR_SUPPORT 1 // Only with Arduino Core 2.4.0
//#define MDNS_SERVER_SUPPORT 0
//#define MDNS_CLIENT_SUPPORT 1
//#define MDNS_SERVER_SUPPORT 0
//#define MQTT_SUPPORT 0
//#define NETBIOS_SUPPORT 1 // Only with Arduino Core 2.4.0
//#define NOFUSS_SUPPORT 1
//#define NTP_SUPPORT 0
//#define RFM69_SUPPORT 1
//#define RF_SUPPORT 1
//#define SCHEDULER_SUPPORT 0
//#define SENSOR_SUPPORT 1
//#define SPIFFS_SUPPORT 1
//#define SSDP_SUPPORT 1
//#define TELNET_SUPPORT 0
@ -135,13 +146,15 @@
//#define EMON_ADS1X15_SUPPORT 1
//#define EMON_ANALOG_SUPPORT 1
//#define EVENTS_SUPPORT 1
//#define GEIGER_SUPPORT 1
//#define GUVAS12SD_SUPPORT 1
//#define HCSR04_SUPPORT 1
//#define HLW8012_SUPPORT 1
//#define MHZ19_SUPPORT 1
//#define NTC_SUPPORT 1
//#define PMSX003_SUPPORT 1
//#define PZEM004T_SUPPORT 1
//#define SHT3X_I2C_SUPPORT 1
//#define SI7021_SUPPORT 1
//#define SONAR_SUPPORT 1
//#define TMP3X_SUPPORT 1
//#define V9261F_SUPPORT 1

+ 30
- 0
code/espurna/config/defaults.h View File

@ -108,6 +108,31 @@
#define BUTTON8_DBLCLICK BUTTON_MODE_NONE
#endif
#ifndef BUTTON1_TRIPLECLICK
#define BUTTON1_TRIPLECLICK BUTTON_MODE_SMART_CONFIG
#endif
#ifndef BUTTON2_TRIPLECLICK
#define BUTTON2_TRIPLECLICK BUTTON_MODE_NONE
#endif
#ifndef BUTTON3_TRIPLECLICK
#define BUTTON3_TRIPLECLICK BUTTON_MODE_NONE
#endif
#ifndef BUTTON4_TRIPLECLICK
#define BUTTON4_TRIPLECLICK BUTTON_MODE_NONE
#endif
#ifndef BUTTON5_TRIPLECLICK
#define BUTTON5_TRIPLECLICK BUTTON_MODE_NONE
#endif
#ifndef BUTTON6_TRIPLECLICK
#define BUTTON6_TRIPLECLICK BUTTON_MODE_NONE
#endif
#ifndef BUTTON7_TRIPLECLICK
#define BUTTON7_TRIPLECLICK BUTTON_MODE_NONE
#endif
#ifndef BUTTON8_TRIPLECLICK
#define BUTTON8_TRIPLECLICK BUTTON_MODE_NONE
#endif
#ifndef BUTTON1_LNGCLICK
#define BUTTON1_LNGCLICK BUTTON_MODE_RESET
#endif
@ -413,3 +438,8 @@
#ifndef LIGHT_PROVIDER
#define LIGHT_PROVIDER LIGHT_PROVIDER_NONE
#endif
// App revision, populated by the build script
#ifndef APP_REVISION
#define APP_REVISION ""
#endif

+ 56
- 0
code/espurna/config/dependencies.h View File

@ -15,6 +15,11 @@
#define DEBUG_WEB_SUPPORT 0
#endif
#if not WEB_SUPPORT
#undef API_SUPPORT
#define API_SUPPORT 0 // API support requires web support
#endif
#if not WEB_SUPPORT
#undef SSDP_SUPPORT
#define SSDP_SUPPORT 0 // SSDP support requires web support
@ -48,3 +53,54 @@
#undef NTP_SUPPORT
#define NTP_SUPPORT 1 // Scheduler needs NTP
#endif
// -----------------------------------------------------------------------------
// WEB UI IMAGE
// -----------------------------------------------------------------------------
#define WEBUI_IMAGE_SMALL 0
#define WEBUI_IMAGE_LIGHT 1
#define WEBUI_IMAGE_SENSOR 2
#define WEBUI_IMAGE_RFBRIDGE 4
#define WEBUI_IMAGE_RFM69 8
#define WEBUI_IMAGE_FULL 15
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
#ifdef WEBUI_IMAGE
#undef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
#else
#define WEBUI_IMAGE WEBUI_IMAGE_LIGHT
#endif
#endif
#if SENSOR_SUPPORT == 1
#ifndef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_SENSOR
#else
#undef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
#endif
#endif
#if defined(ITEAD_SONOFF_RFBRIDGE)
#ifndef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_RFBRIDGE
#else
#undef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
#endif
#endif
#if RFM69_SUPPORT == 1
#ifndef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_RFM69
#else
#undef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
#endif
#endif
#ifndef WEBUI_IMAGE
#define WEBUI_IMAGE WEBUI_IMAGE_SMALL
#endif

+ 120
- 9
code/espurna/config/general.h View File

@ -142,12 +142,17 @@
//------------------------------------------------------------------------------
#define EEPROM_SIZE 4096 // EEPROM size in bytes
//#define EEPROM_RORATE_SECTORS 2 // Number of sectors to use for EEPROM rotation
// If not defined the firmware will use a number based
// on the number of available sectors
#define EEPROM_RELAY_STATUS 0 // Address for the relay status (1 byte)
#define EEPROM_ENERGY_COUNT 1 // Address for the energy counter (4 bytes)
#define EEPROM_CUSTOM_RESET 5 // Address for the reset reason (1 byte)
#define EEPROM_CRASH_COUNTER 6 // Address for the crash counter (1 byte)
#define EEPROM_MESSAGE_ID 7 // Address for the MQTT message id (4 bytes)
#define EEPROM_DATA_END 11 // End of custom EEPROM data block
#define EEPROM_ROTATE_DATA 11 // Reserved for the EEPROM_ROTATE library (3 bytes)
#define EEPROM_DATA_END 14 // End of custom EEPROM data block
//------------------------------------------------------------------------------
// HEARTBEAT
@ -196,6 +201,10 @@
// BUTTON
//------------------------------------------------------------------------------
#ifndef BUTTON_SUPPORT
#define BUTTON_SUPPORT 1
#endif
#ifndef BUTTON_DEBOUNCE_DELAY
#define BUTTON_DEBOUNCE_DELAY 50 // Debounce delay (ms)
#endif
@ -212,6 +221,14 @@
#define BUTTON_LNGLNGCLICK_DELAY 10000 // Time in ms holding the button down to get a long-long click
#endif
//------------------------------------------------------------------------------
// LED
//------------------------------------------------------------------------------
#ifndef LED_SUPPORT
#define LED_SUPPORT 1
#endif
//------------------------------------------------------------------------------
// RELAY
//------------------------------------------------------------------------------
@ -256,6 +273,14 @@
#define RELAY_SAVE_DELAY 1000
#endif
// Configure the MQTT payload for ON/OFF
#ifndef RELAY_MQTT_ON
#define RELAY_MQTT_ON "1"
#endif
#ifndef RELAY_MQTT_OFF
#define RELAY_MQTT_OFF "0"
#endif
// -----------------------------------------------------------------------------
// WIFI
// -----------------------------------------------------------------------------
@ -276,9 +301,6 @@
#define WIFI_AP_CAPTIVE 1 // Captive portal enabled when in AP mode
#endif
#ifndef WIFI_AP_MODE
#define WIFI_AP_MODE AP_MODE_ALONE
#endif
#ifndef WIFI_SLEEP_MODE
#define WIFI_SLEEP_MODE WIFI_NONE_SLEEP // WIFI_NONE_SLEEP, WIFI_LIGHT_SLEEP or WIFI_MODEM_SLEEP
@ -375,6 +397,15 @@
#define WEB_PORT 80 // HTTP port
#endif
// Defining a WEB_REMOTE_DOMAIN will enable Cross-Origin Resource Sharing (CORS)
// so you will be able to login to this device from another domain. This will allow
// you to manage all ESPurna devices in your local network from a unique installation
// of the web UI. This installation could be in a local server (a Raspberry Pi, for instance)
// or in the Internet. Since the WebUI is just one compressed file with HTML, CSS and JS
// there are no special requirements. Any static web server will do (NGinx, Apache, Lighttpd,...).
// The only requirement is that the resource must be available under this domain.
#define WEB_REMOTE_DOMAIN "http://tinkerman.cat"
// -----------------------------------------------------------------------------
// WEBSOCKETS
// -----------------------------------------------------------------------------
@ -400,6 +431,10 @@
// API
// -----------------------------------------------------------------------------
#ifndef API_SUPPORT
#define API_SUPPORT 1 // API (REST & RPC) support built in
#endif
// This will only be enabled if WEB_SUPPORT is 1 (this is the default value)
#ifndef API_ENABLED
#define API_ENABLED 0 // Do not enable API by default
@ -682,6 +717,9 @@
#define MQTT_TOPIC_UARTOUT "uartout"
#define MQTT_TOPIC_LOADAVG "loadavg"
#define MQTT_TOPIC_BOARD "board"
#define MQTT_TOPIC_PULSE "pulse"
#define MQTT_TOPIC_SPEED "speed"
#define MQTT_TOPIC_IR "ir"
// Light module
#define MQTT_TOPIC_CHANNEL "channel"
@ -759,7 +797,7 @@
#endif
#if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
#define LIGHT_MAX_PWM 10000 // 5000 * 200ns => 1 kHz
#define LIGHT_MAX_PWM 10000 // 10000 * 200ns => 2 kHz
#endif
#endif // LIGHT_MAX_PWM
@ -854,6 +892,14 @@
#define HOMEASSISTANT_ENABLED 0 // Integration not enabled by default
#define HOMEASSISTANT_PREFIX "homeassistant" // Default MQTT prefix
#ifndef HOMEASSISTANT_PAYLOAD_ON
#define HOMEASSISTANT_PAYLOAD_ON "1" // Payload for ON and available messages
#endif
#ifndef HOMEASSISTANT_PAYLOAD_OFF
#define HOMEASSISTANT_PAYLOAD_OFF "0" // Payload for OFF and unavailable messages
#endif
// -----------------------------------------------------------------------------
// INFLUXDB
// -----------------------------------------------------------------------------
@ -1009,7 +1055,6 @@
#define RF_RECEIVE_DELAY 500 // Interval between recieving in ms (avoid debouncing)
#endif
#ifndef RF_RAW_SUPPORT
#define RF_RAW_SUPPORT 0 // RF raw codes require a specific firmware for the EFM8BB1
// https://github.com/rhx/RF-Bridge-EFM8BB1
@ -1024,8 +1069,8 @@
#define IR_SUPPORT 0 // Do not build with IR support by default (10.25Kb)
#endif
#ifndef IR_PIN
#define IR_PIN 4 // IR LED
#ifndef IR_RECEIVER_PIN
#define IR_RECEIVER_PIN 4 // IR LED
#endif
// 24 Buttons Set of the IR Remote
@ -1033,6 +1078,10 @@
#define IR_BUTTON_SET 1 // IR button set to use (see below)
#endif
#ifndef IR_DEBOUNCE
#define IR_DEBOUNCE 500 // IR debounce time in milliseconds
#endif
//Remote Buttons SET 1 (for the original Remote shipped with the controller)
#if IR_SUPPORT
#if IR_BUTTON_SET == 1
@ -1055,7 +1104,7 @@
#define IR_BUTTON_COUNT 24
const unsigned long IR_BUTTON[IR_BUTTON_COUNT][3] PROGMEM = {
const uint32_t IR_BUTTON[IR_BUTTON_COUNT][3] PROGMEM = {
{ 0xFF906F, IR_BUTTON_MODE_BRIGHTER, 1 },
{ 0xFFB847, IR_BUTTON_MODE_BRIGHTER, 0 },
@ -1199,3 +1248,65 @@
#define RF_DEBOUNCE 500
#define RF_LEARN_TIMEOUT 60000
//--------------------------------------------------------------------------------
// Custom RFM69 to MQTT bridge
// Check http://tinkerman.cat/rfm69-wifi-gateway/
// Enable support by passing RFM69_SUPPORT=1 build flag
//--------------------------------------------------------------------------------
#ifndef RFM69_SUPPORT
#define RFM69_SUPPORT 0
#endif
#ifndef RFM69_MAX_TOPICS
#define RFM69_MAX_TOPICS 50
#endif
#ifndef RFM69_DEFAULT_TOPIC
#define RFM69_DEFAULT_TOPIC "/rfm69gw/{node}/{key}"
#endif
#ifndef RFM69_NODE_ID
#define RFM69_NODE_ID 2
#endif
#ifndef RFM69_GATEWAY_ID
#define RFM69_GATEWAY_ID 2
#endif
#ifndef RFM69_NETWORK_ID
#define RFM69_NETWORK_ID 164
#endif
#ifndef RFM69_PROMISCUOUS
#define RFM69_PROMISCUOUS 1
#endif
#ifndef RFM69_PROMISCUOUS_SENDS
#define RFM69_PROMISCUOUS_SENDS 0
#endif
#ifndef RFM69_FREQUENCY
#define RFM69_FREQUENCY RF69_868MHZ
#endif
#ifndef RFM69_ENCRYPTKEY
#define RFM69_ENCRYPTKEY "fibonacci0123456"
#endif
#ifndef RFM69_CS_PIN
#define RFM69_CS_PIN SS
#endif
#ifndef RFM69_IRQ_PIN
#define RFM69_IRQ_PIN 5
#endif
#ifndef RFM69_RESET_PIN
#define RFM69_RESET_PIN 7
#endif
#ifndef RFM69_IS_RFM69HW
#define RFM69_IS_RFM69HW 0
#endif

+ 354
- 17
code/espurna/config/hardware.h View File

@ -15,7 +15,7 @@
// RELAY#_TYPE: Relay can be RELAY_TYPE_NORMAL, RELAY_TYPE_INVERSE, RELAY_TYPE_LATCHED or RELAY_TYPE_LATCHED_INVERSE
// LED#_PIN: GPIO for the n-th LED (1-based, up to 8 LEDs)
// LED#_PIN_INVERSE: LED has inversed logic (lit when pulled down)
// LED#_MODE: Check general.h for LED_MODE_%
// LED#_MODE: Check types.h for LED_MODE_%
// LED#_RELAY: Linked relay (1-based)
//
// Besides, other hardware specific information should be stated here
@ -37,9 +37,11 @@
// Disable non-core modules
#define ALEXA_SUPPORT 0
#define BROKER_SUPPORT 0
#define BUTTON_SUPPORT 0
#define DOMOTICZ_SUPPORT 0
#define HOMEASSISTANT_SUPPORT 0
#define I2C_SUPPORT 0
#define MDNS_SERVER_SUPPORT 0
#define MQTT_SUPPORT 0
#define NTP_SUPPORT 0
#define SCHEDULER_SUPPORT 0
@ -47,6 +49,14 @@
#define THINGSPEAK_SUPPORT 0
#define WEB_SUPPORT 0
// Extra light-weight image
//#define DEBUG_SERIAL_SUPPORT 0
//#define DEBUG_TELNET_SUPPORT 0
//#define DEBUG_WEB_SUPPORT 0
//#define LED_SUPPORT 0
//#define TELNET_SUPPORT 0
//#define TERMINAL_SUPPORT 0
// -----------------------------------------------------------------------------
// Development boards
// -----------------------------------------------------------------------------
@ -70,6 +80,32 @@
#define LED1_PIN 2
#define LED1_PIN_INVERSE 1
#elif defined(NODEMCU_BASIC)
// Info
// Generic NodeMCU Board without any buttons or relays connected.
#define MANUFACTURER "NODEMCU"
#define DEVICE "BASIC"
#elif defined(WEMOS_D1_MINI)
// Info
#define MANUFACTURER "WEMOS"
#define DEVICE "D1_MINI"
// Buttons
// No buttons on the D1 MINI alone, but defining it without adding a button doen't create problems
#define BUTTON1_PIN 0 // Connect a pushbutton between D3 and GND,
// it's the same as using a Wemos one button shield
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// LEDs
#define LED1_PIN 2
#define LED1_PIN_INVERSE 1
#define I2C_SDA_PIN 4 // D2
#define I2C_SCL_PIN 5 // D1
#elif defined(WEMOS_D1_MINI_RELAYSHIELD)
// Info
@ -190,6 +226,30 @@
#define RELAY1_PIN 12
#define RELAY1_TYPE RELAY_TYPE_INVERSE
// Check http://tinkerman.cat/rfm69-wifi-gateway/
#elif defined(TINKERMAN_RFM69GW)
// Info
#define MANUFACTURER "TINKERMAN"
#define DEVICE "RFM69GW"
// Buttons
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
// RFM69GW
#define RFM69_SUPPORT 1
// Disable non-core modules
#define ALEXA_SUPPORT 0
#define DOMOTICZ_SUPPORT 0
#define HOMEASSISTANT_SUPPORT 0
#define I2C_SUPPORT 0
#define SCHEDULER_SUPPORT 0
#define SENSOR_SUPPORT 0
#define THINGSPEAK_SUPPORT 0
// -----------------------------------------------------------------------------
// Itead Studio boards
// -----------------------------------------------------------------------------
@ -661,7 +721,7 @@
// the port and remove UART noise on serial line
#if not RFB_DIRECT
#define SERIAL_BAUDRATE 19200
#define DEBUG_SERIAL_SUPPORT 0
//#define DEBUG_SERIAL_SUPPORT 0
#endif
#elif defined(ITEAD_SONOFF_B1)
@ -840,6 +900,36 @@
#define CSE7766_SUPPORT 1
#define CSE7766_PIN 1
#elif defined(ITEAD_SONOFF_IFAN02)
// Info
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_IFAN02"
// These are virtual buttons triggered by the remote
#define BUTTON1_PIN 0
#define BUTTON2_PIN 9
#define BUTTON3_PIN 10
#define BUTTON4_PIN 14
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON3_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON4_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
// Relays
#define RELAY1_PIN 12
#define RELAY2_PIN 5
#define RELAY3_PIN 4
#define RELAY4_PIN 15
#define RELAY1_TYPE RELAY_TYPE_NORMAL
#define RELAY2_TYPE RELAY_TYPE_NORMAL
#define RELAY3_TYPE RELAY_TYPE_NORMAL
#define RELAY4_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 1
// -----------------------------------------------------------------------------
// YJZK
// -----------------------------------------------------------------------------
@ -978,7 +1068,7 @@
// IR
#define IR_SUPPORT 1
#define IR_PIN 4
#define IR_RECEIVER_PIN 4
#define IR_BUTTON_SET 1
#elif defined(MAGICHOME_LED_CONTROLLER_20)
@ -1007,7 +1097,7 @@
// IR
#define IR_SUPPORT 1
#define IR_PIN 4
#define IR_RECEIVER_PIN 4
#define IR_BUTTON_SET 1
// -----------------------------------------------------------------------------
@ -1510,7 +1600,7 @@
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 0
#define LED1_PIN_INVERSE 1
// HLW8012
#ifndef HLW8012_SUPPORT
@ -1821,7 +1911,7 @@
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON3_PIN 14
#define BUTTON3_RELAY 2
#define BUTTON3_RELAY 3
#define BUTTON3_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define RELAY1_PIN 4
@ -2104,13 +2194,19 @@
// Buttons
#define BUTTON1_PIN 12
#define BUTTON1_RELAY 1
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_MODE BUTTON_SWITCH | BUTTON_DEFAULT_HIGH //Hardware Pullup
#define BUTTON1_PRESS BUTTON_MODE_NONE
#define BUTTON1_CLICK BUTTON_MODE_TOGGLE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_NONE
#define BUTTON2_PIN 13
#define BUTTON2_RELAY 2
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_MODE BUTTON_SWITCH | BUTTON_DEFAULT_HIGH //Hardware Pullup
#define BUTTON2_CLICK BUTTON_MODE_TOGGLE
// Relays
#define RELAY1_PIN 4
@ -2158,15 +2254,16 @@
#define LED4_RELAY 1
// -----------------------------------------------------------------------------
// NEO Coolcam Power Plug
// NEO Coolcam NAS-WR01W Wifi Smart Power Plug
// https://es.aliexpress.com/item/-/32854589733.html?spm=a219c.12010608.0.0.6d084e68xX0y5N
// https://www.fasttech.com/product/9649426-neo-coolcam-nas-wr01w-wifi-smart-power-plug-eu
// -----------------------------------------------------------------------------
#elif defined(NEO_COOLCAM_POWER_PLUG_WIFI)
#elif defined(NEO_COOLCAM_NAS_WR01W)
// Info
#define MANUFACTURER "NEO_COOLCAM"
#define DEVICE "POWER_PLUG_WIFI"
#define DEVICE "NAS_WR01W"
// Buttons
#define BUTTON1_PIN 13
@ -2234,6 +2331,222 @@
#define LED3_RELAY 3
#define LED4_RELAY 4
// -----------------------------------------------------------------------------
// Bruno Horta's OnOfre
// https://www.bhonofre.pt/
// https://github.com/brunohorta82/BH_OnOfre/
// -----------------------------------------------------------------------------
#elif defined(BH_ONOFRE)
// Info
#define MANUFACTURER "BH"
#define DEVICE "ONOFRE"
// Buttons
#define BUTTON1_PIN 12
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP
#define BUTTON1_RELAY 1
#define BUTTON2_PIN 13
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH | BUTTON_SET_PULLUP
#define BUTTON2_RELAY 2
// Relays
#define RELAY1_PIN 4
#define RELAY1_TYPE RELAY_TYPE_NORMAL
#define RELAY2_PIN 5
#define RELAY2_TYPE RELAY_TYPE_NORMAL
// -----------------------------------------------------------------------------
// Several boards under different names uing a power chip labelled BL0937 or HJL-01
// * Blitzwolf (https://www.amazon.es/Inteligente-Temporización-Dispositivos-Cualquier-BlitzWolf/dp/B07BMQP142)
// * HomeCube (https://www.amazon.de/Steckdose-Homecube-intelligente-Verbrauchsanzeige-funktioniert/dp/B076Q2LKHG)
// * Coosa (https://www.amazon.com/COOSA-Monitoring-Function-Campatible-Assiatant/dp/B0788W9TDR)
// * Goosund (http://www.gosund.com/?m=content&c=index&a=show&catid=6&id=5)
// * Ablue (https://www.amazon.de/Intelligente-Steckdose-Ablue-Funktioniert-Assistant/dp/B076DRFRZC)
// -----------------------------------------------------------------------------
#elif defined(BLITZWOLF_BWSHP2)
// Info
#define MANUFACTURER "BLITZWOLF"
#define DEVICE "BWSHP2"
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 15
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 2
#define LED1_PIN_INVERSE 1
#define LED2_PIN 0
#define LED2_PIN_INVERSE 1
#define LED2_MODE LED_MODE_FINDME
#define LED2_RELAY 1
// HJL01 / BL0937
#ifndef HLW8012_SUPPORT
#define HLW8012_SUPPORT 1
#endif
#define HLW8012_SEL_PIN 12
#define HLW8012_CF1_PIN 14
#define HLW8012_CF_PIN 5
#define HLW8012_SEL_CURRENT LOW
#define HLW8012_CURRENT_RATIO 25740
#define HLW8012_VOLTAGE_RATIO 313400
#define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING
// ----------------------------------------------------------------------------------------
// Homecube 16A is similar but some pins differ and it also has RGB LEDs
// https://www.amazon.de/gp/product/B07D7RVF56/ref=oh_aui_detailpage_o00_s01?ie=UTF8&psc=1
// ----------------------------------------------------------------------------------------
#elif defined(HOMECUBE_16A)
// Info
#define MANUFACTURER "HOMECUBE"
#define DEVICE "16A"
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 15
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
//LED Pin 4 - ESP8266 onboard LED
//Red LED: 0
//Green LED: 12
//Blue LED: 2
// Blue
#define LED1_PIN 2
#define LED1_PIN_INVERSE 0
// Green
#define LED2_PIN 12
#define LED2_PIN_INVERSE 1
#define LED2_MODE LED_MODE_RELAY
// Red
#define LED3_PIN 0
#define LED3_PIN_INVERSE 0
#define LED2_MODE LED_MODE_OFF
// HJL01 / BL0937
#ifndef HLW8012_SUPPORT
#define HLW8012_SUPPORT 1
#endif
#define HLW8012_SEL_PIN 16
#define HLW8012_CF1_PIN 14
#define HLW8012_CF_PIN 5
#define HLW8012_SEL_CURRENT LOW
#define HLW8012_CURRENT_RATIO 25740
#define HLW8012_VOLTAGE_RATIO 313400
#define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING
// -----------------------------------------------------------------------------
// VANZAVANZU Smart Outlet Socket (based on BL0937 or HJL-01)
// https://www.amazon.com/Smart-Plug-Wifi-Mini-VANZAVANZU/dp/B078PHD6S5
// -----------------------------------------------------------------------------
#elif defined(VANZAVANZU_SMART_WIFI_PLUG_MINI)
// Info
#define MANUFACTURER "VANZAVANZU"
#define DEVICE "SMART_WIFI_PLUG_MINI"
// Buttons
#define BUTTON1_PIN 13
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// Relays
#define RELAY1_PIN 15
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// LEDs
#define LED1_PIN 2
#define LED1_PIN_INVERSE 1
#define LED2_PIN 0
#define LED2_PIN_INVERSE 1
#define LED2_MODE LED_MODE_FINDME
#define LED2_RELAY 1
// Disable UART noise
#define DEBUG_SERIAL_SUPPORT 0
// HJL01 / BL0937
#ifndef HLW8012_SUPPORT
#define HLW8012_SUPPORT 1
#endif
#define HLW8012_SEL_PIN 3
#define HLW8012_CF1_PIN 14
#define HLW8012_CF_PIN 5
#define HLW8012_SEL_CURRENT LOW
#define HLW8012_CURRENT_RATIO 25740
#define HLW8012_VOLTAGE_RATIO 313400
#define HLW8012_POWER_RATIO 3414290
#define HLW8012_INTERRUPT_ON FALLING
#elif defined(GENERIC_AG_L4)
// Info
#define MANUFACTURER "GENERIC"
#define DEVICE "AG_L4"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// button 1: "power" button
#define BUTTON1_PIN 4
#define BUTTON1_RELAY 1
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_SET_PULLUP | BUTTON_DEFAULT_HIGH
#define BUTTON1_PRESS BUTTON_MODE_TOGGLE
#define BUTTON1_CLICK BUTTON_MODE_NONE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_RESET
// button 2: "wifi" button
#define BUTTON2_PIN 2
#define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_PRESS BUTTON_MODE_TOGGLE
#define BUTTON2_CLICK BUTTON_MODE_NONE
#define BUTTON2_DBLCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGCLICK BUTTON_MODE_NONE
#define BUTTON2_LNGLNGCLICK BUTTON_MODE_NONE
// LEDs
#define LED1_PIN 5 // red status led
#define LED1_PIN_INVERSE 0
#define LED2_PIN 16 // master light power
#define LED2_PIN_INVERSE 1
#define LED2_MODE LED_MODE_RELAY
// Light
#define LIGHT_CHANNELS 3
#define LIGHT_CH1_PIN 14 // RED
#define LIGHT_CH2_PIN 13 // GREEN
#define LIGHT_CH3_PIN 12 // BLUE
#define LIGHT_CH1_INVERSE 0
#define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0
// -----------------------------------------------------------------------------
// TEST boards (do not use!!)
// -----------------------------------------------------------------------------
@ -2276,6 +2589,8 @@
#define EMON_ADS1X15_SUPPORT 1
#define SHT3X_I2C_SUPPORT 1
#define SI7021_SUPPORT 1
#define PMSX003_SUPPORT 1
#define SENSEAIR_SUPPORT 1
// A bit of lights - pin 5
@ -2314,6 +2629,11 @@
#define MANUFACTURER "TravisCI"
#define DEVICE "Virtual board 02"
// Some buttons - pin 0
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// A bit of CSE7766 - pin 1
#ifndef CSE7766_SUPPORT
#define CSE7766_SUPPORT 1
@ -2329,7 +2649,7 @@
// IR - pin 4
#define IR_SUPPORT 1
#define IR_PIN 4
#define IR_RECEIVER_PIN 4
#define IR_BUTTON_SET 1
// A bit of DHT - pin 5
@ -2345,10 +2665,10 @@
#define EVENTS_SUPPORT 1
#define EVENTS_PIN 6
// HC-RS04
#define HCSR04_SUPPORT 1
#define HCSR04_TRIGGER 7
#define HCSR04_ECHO 8
// Sonar
#define SONAR_SUPPORT 1
#define SONAR_TRIGGER 7
#define SONAR_ECHO 8
// MHZ19
#define MHZ19_SUPPORT 1
@ -2356,7 +2676,7 @@
#define MHZ19_TX_PIN 10
// PZEM004T
#define PZEM004T_SUPPORT 0 // not working?
#define PZEM004T_SUPPORT 1
#define PZEM004T_RX_PIN 11
#define PZEM004T_TX_PIN 12
@ -2368,12 +2688,24 @@
#define GUVAS12SD_SUPPORT 1
#define GUVAS12SD_PIN 14
// Test non-default modules
#define MDNS_CLIENT_SUPPORT 1
#define NOFUSS_SUPPORT 1
#define UART_MQTT_SUPPORT 1
#define INFLUXDB_SUPPORT 1
#define IR_SUPPORT 1
#elif defined(TRAVIS03)
// Relay provider light/my92XX
#define MANUFACTURER "TravisCI"
#define DEVICE "Virtual board 03"
// Some buttons - pin 0
#define BUTTON1_PIN 0
#define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
// MY9231 Light - pins 1,2
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
@ -2391,6 +2723,11 @@
#define EMON_ANALOG_SUPPORT 1
#endif
// Test non-default modules
#define LLMNR_SUPPORT 1
#define NETBIOS_SUPPORT 1
#define SSDP_SUPPORT 1
#endif
// -----------------------------------------------------------------------------


+ 206
- 10
code/espurna/config/progmem.h View File

@ -23,19 +23,206 @@ PROGMEM const char* const custom_reset_string[] = {
custom_reset_factory
};
//--------------------------------------------------------------------------------
// Capabilities
//--------------------------------------------------------------------------------
PROGMEM const char espurna_modules[] =
#if ALEXA_SUPPORT
"ALEXA "
#endif
#if API_SUPPORT
"API "
#endif
#if BROKER_SUPPORT
"BROKER "
#endif
#if BUTTON_SUPPORT
"BUTTON "
#endif
#if DEBUG_SERIAL_SUPPORT
"DEBUG_SERIAL "
#endif
#if DEBUG_TELNET_SUPPORT
"DEBUG_TELNET "
#endif
#if DEBUG_UDP_SUPPORT
"DEBUG_UDP "
#endif
#if DEBUG_WEB_SUPPORT
"DEBUG_WEB "
#endif
#if DOMOTICZ_SUPPORT
"DOMOTICZ "
#endif
#if HOMEASSISTANT_SUPPORT
"HOMEASSISTANT "
#endif
#if I2C_SUPPORT
"I2C "
#endif
#if INFLUXDB_SUPPORT
"INFLUXDB "
#endif
#if IR_SUPPORT
"IR "
#endif
#if LED_SUPPORT
"LED "
#endif
#if LLMNR_SUPPORT
"LLMNR "
#endif
#if MDNS_CLIENT_SUPPORT
"MDNS_CLIENT "
#endif
#if MDNS_SERVER_SUPPORT
"MDNS_SERVER "
#endif
#if MQTT_SUPPORT
"MQTT "
#endif
#if NETBIOS_SUPPORT
"NETBIOS "
#endif
#if NOFUSS_SUPPORT
"NOFUSS "
#endif
#if NTP_SUPPORT
"NTP "
#endif
#if RFM69_SUPPORT
"RFM69 "
#endif
#if RF_SUPPORT
"RF "
#endif
#if SCHEDULER_SUPPORT
"SCHEDULER "
#endif
#if SENSOR_SUPPORT
"SENSOR "
#endif
#if SPIFFS_SUPPORT
"SPIFFS "
#endif
#if SSDP_SUPPORT
"SSDP "
#endif
#if TELNET_SUPPORT
"TELNET "
#endif
#if TERMINAL_SUPPORT
"TERMINAL "
#endif
#if THINGSPEAK_SUPPORT
"THINGSPEAK "
#endif
#if UART_MQTT_SUPPORT
"UART_MQTT "
#endif
#if WEB_SUPPORT
"WEB "
#endif
"";
//--------------------------------------------------------------------------------
// Sensors
//--------------------------------------------------------------------------------
#if SENSOR_SUPPORT
PROGMEM const char espurna_sensors[] =
#if AM2320_SUPPORT
"AM2320_I2C "
#endif
#if ANALOG_SUPPORT
"ANALOG "
#endif
#if BH1750_SUPPORT
"BH1750 "
#endif
#if BMX280_SUPPORT
"BMX280 "
#endif
#if CSE7766_SUPPORT
"CSE7766 "
#endif
#if DALLAS_SUPPORT
"DALLAS "
#endif
#if DHT_SUPPORT
"DHTXX "
#endif
#if DIGITAL_SUPPORT
"DIGITAL "
#endif
#if ECH1560_SUPPORT
"ECH1560 "
#endif
#if EMON_ADC121_SUPPORT
"EMON_ADC121 "
#endif
#if EMON_ADS1X15_SUPPORT
"EMON_ADX1X15 "
#endif
#if EMON_ANALOG_SUPPORT
"EMON_ANALOG "
#endif
#if EVENTS_SUPPORT
"EVENTS "
#endif
#if GEIGER_SUPPORT
"GEIGER "
#endif
#if GUVAS12SD_SUPPORT
"GUVAS12SD "
#endif
#if HLW8012_SUPPORT
"HLW8012 "
#endif
#if MHZ19_SUPPORT
"MHZ19 "
#endif
#if NTC_SUPPORT
"NTC "
#endif
#if PMSX003_SUPPORT
"PMSX003 "
#endif
#if PZEM004T_SUPPORT
"PZEM004T "
#endif
#if SENSEAIR_SUPPORT
"SENSEAIR "
#endif
#if SHT3X_I2C_SUPPORT
"SHT3X_I2C "
#endif
#if SI7021_SUPPORT
"SI7021 "
#endif
#if SONAR_SUPPORT
"SONAR "
#endif
#if TMP3X_SUPPORT
"TMP3X "
#endif
#if V9261F_SUPPORT
"V9261F "
#endif
"";
PROGMEM const unsigned char magnitude_decimals[] = {
0,
1, 0, 2,
3, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
0, 0, 0,
0, 0, 3, 3
1, 0, 2, // THP
3, 0, 0, 0, 0, 0, 0, 0, // Power decimals
0, 0, 0, // analog, digital, event
0, 0, 0, // PM
0, 0, 3, 3, 0,
4, 4, // Geiger Counter decimals
0
};
PROGMEM const char magnitude_unknown_topic[] = "unknown";
@ -52,7 +239,7 @@ PROGMEM const char magnitude_energy_topic[] = "energy";
PROGMEM const char magnitude_energy_delta_topic[] = "energy_delta";
PROGMEM const char magnitude_analog_topic[] = "analog";
PROGMEM const char magnitude_digital_topic[] = "digital";
PROGMEM const char magnitude_events_topic[] = "events";
PROGMEM const char magnitude_event_topic[] = "event";
PROGMEM const char magnitude_pm1dot0_topic[] = "pm1dot0";
PROGMEM const char magnitude_pm2dot5_topic[] = "pm2dot5";
PROGMEM const char magnitude_pm10_topic[] = "pm10";
@ -61,16 +248,21 @@ PROGMEM const char magnitude_lux_topic[] = "lux";
PROGMEM const char magnitude_uv_topic[] = "uv";
PROGMEM const char magnitude_distance_topic[] = "distance";
PROGMEM const char magnitude_hcho_topic[] = "hcho";
PROGMEM const char magnitude_geiger_cpm_topic[] = "ldr_cpm"; // local dose rate [Counts per minute]
PROGMEM const char magnitude_geiger_sv_topic[] = "ldr_uSvh"; // local dose rate [µSievert per hour]
PROGMEM const char magnitude_count_topic[] = "count";
PROGMEM const char* const magnitude_topics[] = {
magnitude_unknown_topic, magnitude_temperature_topic, magnitude_humidity_topic,
magnitude_pressure_topic, magnitude_current_topic, magnitude_voltage_topic,
magnitude_active_power_topic, magnitude_apparent_power_topic, magnitude_reactive_power_topic,
magnitude_power_factor_topic, magnitude_energy_topic, magnitude_energy_delta_topic,
magnitude_analog_topic, magnitude_digital_topic, magnitude_events_topic,
magnitude_analog_topic, magnitude_digital_topic, magnitude_event_topic,
magnitude_pm1dot0_topic, magnitude_pm2dot5_topic, magnitude_pm10_topic,
magnitude_co2_topic, magnitude_lux_topic, magnitude_uv_topic,
magnitude_distance_topic, magnitude_hcho_topic
magnitude_distance_topic, magnitude_hcho_topic,
magnitude_geiger_cpm_topic, magnitude_geiger_sv_topic,
magnitude_count_topic
};
PROGMEM const char magnitude_empty[] = "";
@ -90,6 +282,9 @@ PROGMEM const char magnitude_lux[] = "lux";
PROGMEM const char magnitude_uv[] = "uv";
PROGMEM const char magnitude_distance[] = "m";
PROGMEM const char magnitude_mgm3[] = "mg/m³";
PROGMEM const char magnitude_geiger_cpm[] = "cpm"; // Counts per Minute: Unit of local dose rate (Geiger counting)
PROGMEM const char magnitude_geiger_sv[] = "µSv/h"; // µSievert per hour: 2nd unit of local dose rate (Geiger counting)
PROGMEM const char* const magnitude_units[] = {
magnitude_empty, magnitude_celsius, magnitude_percentage,
@ -99,8 +294,9 @@ PROGMEM const char* const magnitude_units[] = {
magnitude_empty, magnitude_empty, magnitude_empty,
magnitude_ugm3, magnitude_ugm3, magnitude_ugm3,
magnitude_ppm, magnitude_lux, magnitude_uv,
magnitude_distance, magnitude_mgm3
magnitude_distance, magnitude_mgm3,
magnitude_geiger_cpm, magnitude_geiger_sv, // Geiger counter units
magnitude_empty
};
#endif

+ 108
- 54
code/espurna/config/prototypes.h View File

@ -7,66 +7,51 @@ extern "C" {
#include "user_interface.h"
}
// -----------------------------------------------------------------------------
// WebServer
// -----------------------------------------------------------------------------
#include <ESPAsyncWebServer.h>
AsyncWebServer * webServer();
// -----------------------------------------------------------------------------
// API
// -----------------------------------------------------------------------------
typedef std::function<void(char *, size_t)> api_get_callback_f;
typedef std::function<void(const char *)> api_put_callback_f;
void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn = NULL);
#if WEB_SUPPORT
typedef std::function<void(char *, size_t)> api_get_callback_f;
typedef std::function<void(const char *)> api_put_callback_f;
void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn = NULL);
#else
#define api_get_callback_f void *
#define api_put_callback_f void *
#endif
// -----------------------------------------------------------------------------
// WebSockets
// Broker
// -----------------------------------------------------------------------------
typedef std::function<void(JsonObject&)> ws_on_send_callback_f;
void wsOnSendRegister(ws_on_send_callback_f callback);
void wsSend(ws_on_send_callback_f sender);
typedef std::function<void(uint32_t, const char *, JsonObject&)> ws_on_action_callback_f;
void wsOnActionRegister(ws_on_action_callback_f callback);
typedef std::function<void(void)> ws_on_after_parse_callback_f;
void wsOnAfterParseRegister(ws_on_after_parse_callback_f callback);
typedef std::function<bool(const char *, JsonVariant&)> ws_on_receive_callback_f;
void wsOnReceiveRegister(ws_on_receive_callback_f callback);
#if BROKER_SUPPORT
void brokerRegister(void (*)(const char *, unsigned char, const char *));
#endif
// -----------------------------------------------------------------------------
// WIFI
// Debug
// -----------------------------------------------------------------------------
#include "JustWifi.h"
typedef std::function<void(justwifi_messages_t code, char * parameter)> wifi_callback_f;
void wifiRegister(wifi_callback_f callback);
void debugSend(const char * format, ...);
void debugSend_P(PGM_P format, ...);
// -----------------------------------------------------------------------------
// MQTT
// Domoticz
// -----------------------------------------------------------------------------
typedef std::function<void(unsigned int, const char *, const char *)> mqtt_callback_f;
void mqttRegister(mqtt_callback_f callback);
String mqttMagnitude(char * topic);
#if DOMOTICZ_SUPPORT
template<typename T> void domoticzSend(const char * key, T value);
template<typename T> void domoticzSend(const char * key, T nvalue, const char * svalue);
#endif
// -----------------------------------------------------------------------------
// Broker
// EEPROM_ROTATE
// -----------------------------------------------------------------------------
void brokerRegister(void (*)(const char *, unsigned char, const char *));
#include <EEPROM_Rotate.h>
EEPROM_Rotate EEPROMr;
// -----------------------------------------------------------------------------
// Settings
// GPIO
// -----------------------------------------------------------------------------
#include <Embedis.h>
template<typename T> bool setSetting(const String& key, T value);
template<typename T> bool setSetting(const String& key, unsigned int index, T value);
template<typename T> String getSetting(const String& key, T defaultValue);
template<typename T> String getSetting(const String& key, unsigned int index, T defaultValue);
void settingsGetJson(JsonObject& data);
bool settingsRestoreJson(JsonObject& data);
void settingsRegisterCommand(const String& name, void (*call)(Embedis*));
void settingsInject(void *data, size_t len);
bool gpioValid(unsigned char gpio);
bool gpioGetLock(unsigned char gpio);
bool gpioReleaseLock(unsigned char gpio);
// -----------------------------------------------------------------------------
// I2C
@ -94,23 +79,47 @@ int16_t i2c_read_int16_le(uint8_t address, uint8_t reg);
void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len);
// -----------------------------------------------------------------------------
// GPIO
// MQTT
// -----------------------------------------------------------------------------
bool gpioValid(unsigned char gpio);
bool gpioGetLock(unsigned char gpio);
bool gpioReleaseLock(unsigned char gpio);
#if MQTT_SUPPORT
typedef std::function<void(unsigned int, const char *, const char *)> mqtt_callback_f;
void mqttRegister(mqtt_callback_f callback);
String mqttMagnitude(char * topic);
#else
#define mqtt_callback_f void *
#endif
// -----------------------------------------------------------------------------
// Debug
// OTA
// -----------------------------------------------------------------------------
void debugSend(const char * format, ...);
void debugSend_P(PGM_P format, ...);
#include "ESPAsyncTCP.h"
// -----------------------------------------------------------------------------
// Domoticz
// RFM69
// -----------------------------------------------------------------------------
typedef struct {
unsigned long messageID;
unsigned char packetID;
unsigned char senderID;
unsigned char targetID;
char * key;
char * value;
int16_t rssi;
} packet_t;
// -----------------------------------------------------------------------------
// Settings
// -----------------------------------------------------------------------------
template<typename T> void domoticzSend(const char * key, T value);
template<typename T> void domoticzSend(const char * key, T nvalue, const char * svalue);
#include <Embedis.h>
template<typename T> bool setSetting(const String& key, T value);
template<typename T> bool setSetting(const String& key, unsigned int index, T value);
template<typename T> String getSetting(const String& key, T defaultValue);
template<typename T> String getSetting(const String& key, unsigned int index, T defaultValue);
void settingsGetJson(JsonObject& data);
bool settingsRestoreJson(JsonObject& data);
void settingsRegisterCommand(const String& name, void (*call)(Embedis*));
void settingsInject(void *data, size_t len);
Stream & settingsSerial();
// -----------------------------------------------------------------------------
// Utils
@ -118,5 +127,50 @@ template<typename T> void domoticzSend(const char * key, T nvalue, const char *
char * ltrim(char * s);
void nice_delay(unsigned long ms);
#define ARRAYINIT(type, name, ...) \
type name[] = {__VA_ARGS__};
#define ARRAYINIT(type, name, ...) type name[] = {__VA_ARGS__};
// -----------------------------------------------------------------------------
// WebServer
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
#include <ESPAsyncWebServer.h>
AsyncWebServer * webServer();
#else
#define AsyncWebServerRequest void
#define ArRequestHandlerFunction void
#define AsyncWebSocketClient void
#define AsyncWebSocket void
#define AwsEventType void *
#endif
typedef std::function<bool(AsyncWebServerRequest *request)> web_request_callback_f;
void webRequestRegister(web_request_callback_f callback);
// -----------------------------------------------------------------------------
// WebSockets
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
typedef std::function<void(JsonObject&)> ws_on_send_callback_f;
void wsOnSendRegister(ws_on_send_callback_f callback);
void wsSend(ws_on_send_callback_f sender);
typedef std::function<void(uint32_t, const char *, JsonObject&)> ws_on_action_callback_f;
void wsOnActionRegister(ws_on_action_callback_f callback);
typedef std::function<void(void)> ws_on_after_parse_callback_f;
void wsOnAfterParseRegister(ws_on_after_parse_callback_f callback);
typedef std::function<bool(const char *, JsonVariant&)> ws_on_receive_callback_f;
void wsOnReceiveRegister(ws_on_receive_callback_f callback);
#else
#define ws_on_send_callback_f void *
#define ws_on_action_callback_f void *
#define ws_on_after_parse_callback_f void *
#define ws_on_receive_callback_f void *
#endif
// -----------------------------------------------------------------------------
// WIFI
// -----------------------------------------------------------------------------
#include "JustWifi.h"
typedef std::function<void(justwifi_messages_t code, char * parameter)> wifi_callback_f;
void wifiRegister(wifi_callback_f callback);

+ 152
- 27
code/espurna/config/sensors.h View File

@ -79,6 +79,14 @@
#define ANALOG_SUPPORT 0
#endif
#ifndef ANALOG_SAMPLES
#define ANALOG_SAMPLES 10 // Number of samples
#endif
#ifndef ANALOG_DELAY
#define ANALOG_DELAY 0 // Delay between samples in micros
#endif
//------------------------------------------------------------------------------
// BH1750
// Enable support by passing BH1750_SUPPORT=1 build flag
@ -273,6 +281,11 @@
#define EVENTS_SUPPORT 0 // Do not build with counter support by default
#endif
#ifndef EVENTS_TRIGGER
#define EVENTS_TRIGGER 1 // 1 to trigger callback on events,
// 0 to only count them and report periodically
#endif
#ifndef EVENTS_PIN
#define EVENTS_PIN 2 // GPIO to monitor
#endif
@ -285,36 +298,48 @@
#define EVENTS_INTERRUPT_MODE RISING // RISING, FALLING, BOTH
#endif
#define EVENTS_DEBOUNCE 50 // Do not register events within less than 10 millis
#define EVENTS_DEBOUNCE 50 // Do not register events within less than 50 millis
//------------------------------------------------------------------------------
// GUVAS12SD UV Sensor (analog)
// Enable support by passing GUVAS12SD_SUPPORT=1 build flag
// Geiger sensor
// Enable support by passing GEIGER_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef GUVAS12SD_SUPPORT
#define GUVAS12SD_SUPPORT 0
#ifndef GEIGER_SUPPORT
#define GEIGER_SUPPORT 0 // Do not build with geiger support by default
#endif
#ifndef GUVAS12SD_PIN
#define GUVAS12SD_PIN 14
#ifndef GEIGER_PIN
#define GEIGER_PIN D1 // GPIO to monitor "D1" => "GPIO5"
#endif
//------------------------------------------------------------------------------
// HC-SR04
// Enable support by passing HCSR04_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef GEIGER_PIN_MODE
#define GEIGER_PIN_MODE INPUT // INPUT, INPUT_PULLUP
#endif
#ifndef HCSR04_SUPPORT
#define HCSR04_SUPPORT 0
#ifndef GEIGER_INTERRUPT_MODE
#define GEIGER_INTERRUPT_MODE RISING // RISING, FALLING, BOTH
#endif
#ifndef HCSR04_TRIGGER
#define HCSR04_TRIGGER 12 // GPIO for the trigger pin (output)
#define GEIGER_DEBOUNCE 25 // Do not register events within less than 25 millis.
// Value derived here: Debounce time 25ms, because https://github.com/Trickx/espurna/wiki/Geiger-counter
#define GEIGER_CPM2SIEVERT 240 // CPM to µSievert per hour conversion factor
// Typically the literature uses the invers, but I find an integer type more convienient.
#define GEIGER_REPORT_SIEVERTS 1 // Enabler for local dose rate reports in µSv/h
#define GEIGER_REPORT_CPM 1 // Enabler for local dose rate reports in counts per minute
//------------------------------------------------------------------------------
// GUVAS12SD UV Sensor (analog)
// Enable support by passing GUVAS12SD_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef GUVAS12SD_SUPPORT
#define GUVAS12SD_SUPPORT 0
#endif
#ifndef HCSR04_ECHO
#define HCSR04_ECHO 14 // GPIO for the echo pin (input)
#ifndef GUVAS12SD_PIN
#define GUVAS12SD_PIN 14
#endif
//------------------------------------------------------------------------------
@ -354,7 +379,27 @@
#define HLW8012_VOLTAGE_R_DOWN ( 1000 ) // Downstream voltage resistor
#endif
#ifndef HLW8012_CURRENT_RATIO
#define HLW8012_CURRENT_RATIO 0 // Set to 0 to use factory defaults
#endif
#ifndef HLW8012_VOLTAGE_RATIO
#define HLW8012_VOLTAGE_RATIO 0 // Set to 0 to use factory defaults
#endif
#ifndef HLW8012_POWER_RATIO
#define HLW8012_POWER_RATIO 0 // Set to 0 to use factory defaults
#endif
#ifndef HLW8012_USE_INTERRUPTS
#define HLW8012_USE_INTERRUPTS 1 // Use interrupts to trap HLW8012 signals
#endif
#ifndef HLW8012_INTERRUPT_ON
#define HLW8012_INTERRUPT_ON CHANGE // When to trigger the interrupt
// Use CHANGE for HLW8012
// Use FALLING for BL0937 / HJL0
#endif
//------------------------------------------------------------------------------
// MHZ19 CO2 sensor
@ -373,6 +418,43 @@
#define MHZ19_TX_PIN 15
#endif
//------------------------------------------------------------------------------
// NTC sensor
// Enable support by passing NTC_SUPPORT=1 build flag
//--------------------------------------------------------------------------------
#ifndef NTC_SUPPORT
#define NTC_SUPPORT 0
#endif
#ifndef NTC_SAMPLES
#define NTC_SAMPLES 10 // Number of samples
#endif
#ifndef NTC_DELAY
#define NTC_DELAY 0 // Delay between samples in micros
#endif
#ifndef NTC_R_UP
#define NTC_R_UP 0 // Resistor upstream, set to 0 if none
#endif
#ifndef NTC_R_DOWN
#define NTC_R_DOWN 10000 // Resistor downstream, set to 0 if none
#endif
#ifndef NTC_T0
#define NTC_T0 298.15 // 25 Celsius
#endif
#ifndef NTC_R0
#define NTC_R0 10000 // Resistance at T0
#endif
#ifndef NTC_BETA
#define NTC_BETA 3977 // Beta coeficient
#endif
//------------------------------------------------------------------------------
// SenseAir CO2 sensor
// Enable support by passing SENSEAIR_SUPPORT=1 build flag
@ -410,14 +492,21 @@
#define PMS_SMART_SLEEP 0
#endif
#ifndef PMS_USE_SOFT
#define PMS_USE_SOFT 0 // If PMS_USE_SOFT == 1, DEBUG_SERIAL_SUPPORT must be 0
#endif
#ifndef PMS_RX_PIN
#define PMS_RX_PIN 13
#define PMS_RX_PIN 13 // Software serial RX GPIO (if PMS_USE_SOFT == 1)
#endif
#ifndef PMS_TX_PIN
#define PMS_TX_PIN 15
#define PMS_TX_PIN 15 // Software serial TX GPIO (if PMS_USE_SOFT == 1)
#endif
#ifndef PMS_HW_PORT
#define PMS_HW_PORT Serial // Hardware serial port (if PMS_USE_SOFT == 0)
#endif
//------------------------------------------------------------------------------
// PZEM004T based power monitor
// Enable support by passing PZEM004T_SUPPORT=1 build flag
@ -428,7 +517,7 @@
#endif
#ifndef PZEM004T_USE_SOFT
#define PZEM004T_USE_SOFT 1 // Use software serial
#define PZEM004T_USE_SOFT 0 // Software serial is not working atm, use hardware serial
#endif
#ifndef PZEM004T_RX_PIN
@ -440,12 +529,12 @@
#endif
#ifndef PZEM004T_HW_PORT
#define PZEM004T_HW_PORT Serial1 // Hardware serial port (if PZEM004T_USE_SOFT == 0)
#define PZEM004T_HW_PORT Serial // Hardware serial port (if PZEM004T_USE_SOFT == 0)
#endif
//------------------------------------------------------------------------------
// SHT3X I2C (Wemos) temperature & humidity sensor
// Enable support by passing SHT3X_SUPPORT=1 build flag
// Enable support by passing SHT3X_I2C_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef SHT3X_I2C_SUPPORT
@ -469,6 +558,31 @@
#define SI7021_ADDRESS 0x00 // 0x00 means auto
#endif
//------------------------------------------------------------------------------
// Sonar
// Enable support by passing SONAR_SUPPORT=1 build flag
//------------------------------------------------------------------------------
#ifndef SONAR_SUPPORT
#define SONAR_SUPPORT 0
#endif
#ifndef SONAR_TRIGGER
#define SONAR_TRIGGER 12 // GPIO for the trigger pin (output)
#endif
#ifndef SONAR_ECHO
#define SONAR_ECHO 14 // GPIO for the echo pin (input)
#endif
#ifndef SONAR_MAX_DISTANCE
#define SONAR_MAX_DISTANCE MAX_SENSOR_DISTANCE // Max sensor distance in cm
#endif
#ifndef SONAR_ITERATIONS
#define SONAR_ITERATIONS 5 // Number of iterations to ping for
#endif // error correction.
//------------------------------------------------------------------------------
// TMP3X analog temperature sensor
// Enable support by passing TMP3X_SUPPORT=1 build flag
@ -527,15 +641,17 @@
EMON_ADS1X15_SUPPORT || \
EMON_ANALOG_SUPPORT || \
EVENTS_SUPPORT || \
GEIGER_SUPPORT || \
GUVAS12SD_SUPPORT || \
HCSR04_SUPPORT || \
HLW8012_SUPPORT || \
MHZ19_SUPPORT || \
NTC_SUPPORT || \
SENSEAIR_SUPPORT || \
PMSX003_SUPPORT || \
PZEM004T_SUPPORT || \
SHT3X_I2C_SUPPORT || \
SI7021_SUPPORT || \
SONAR_SUPPORT || \
TMP3X_SUPPORT || \
V9261F_SUPPORT \
)
@ -639,12 +755,12 @@
#include "../sensors/EventSensor.h"
#endif
#if GUVAS12SD_SUPPORT
#include "../sensors/GUVAS12SDSensor.h"
#if GEIGER_SUPPORT
#include "../sensors/GeigerSensor.h" // The main file for geiger counting module
#endif
#if HCSR04_SUPPORT
#include "../sensors/HCSR04Sensor.h"
#if GUVAS12SD_SUPPORT
#include "../sensors/GUVAS12SDSensor.h"
#endif
#if HLW8012_SUPPORT
@ -657,6 +773,11 @@
#include "../sensors/MHZ19Sensor.h"
#endif
#if NTC_SUPPORT
#include "../sensors/AnalogSensor.h"
#include "../sensors/NTCSensor.h"
#endif
#if SENSEAIR_SUPPORT
#include <SoftwareSerial.h>
#include "../sensors/SenseAirSensor.h"
@ -680,6 +801,10 @@
#include "../sensors/SHT3XI2CSensor.h"
#endif
#if SONAR_SUPPORT
#include "../sensors/SonarSensor.h"
#endif
#if TMP3X_SUPPORT
#include "../sensors/TMP3XSensor.h"
#endif


+ 24
- 3
code/espurna/config/types.h View File

@ -3,6 +3,19 @@
// Do not touch this definitions
//------------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// WIFI
// -----------------------------------------------------------------------------
#define WIFI_STATE_AP 1
#define WIFI_STATE_STA 2
#define WIFI_STATE_AP_STA 3
#define WIFI_STATE_WPS 4
#define WIFI_STATE_SMARTCONFIG 8
#define WIFI_AP_ALLWAYS 1
#define WIFI_AP_FALLBACK 2
//------------------------------------------------------------------------------
// BUTTONS
//------------------------------------------------------------------------------
@ -14,6 +27,7 @@
#define BUTTON_EVENT_DBLCLICK 3
#define BUTTON_EVENT_LNGCLICK 4
#define BUTTON_EVENT_LNGLNGCLICK 5
#define BUTTON_EVENT_TRIPLECLICK 6
#define BUTTON_MODE_NONE 0
#define BUTTON_MODE_TOGGLE 1
@ -23,6 +37,8 @@
#define BUTTON_MODE_RESET 5
#define BUTTON_MODE_PULSE 6
#define BUTTON_MODE_FACTORY 7
#define BUTTON_MODE_WPS 8
#define BUTTON_MODE_SMART_CONFIG 9
// Needed for ESP8285 boards under Windows using PlatformIO (?)
#ifndef BUTTON_PUSHBUTTON
@ -249,8 +265,10 @@
#define SENSOR_GUVAS12SD_ID 0x20
#define SENSOR_CSE7766_ID 0x21
#define SENSOR_TMP3X_ID 0x22
#define SENSOR_HCSR04_ID 0x23
#define SENSOR_SONAR_ID 0x23
#define SENSOR_SENSEAIR_ID 0x24
#define SENSOR_GEIGER_ID 0x25
#define SENSOR_NTC_ID 0x26
//--------------------------------------------------------------------------------
// Magnitudes
@ -270,7 +288,7 @@
#define MAGNITUDE_ENERGY_DELTA 11
#define MAGNITUDE_ANALOG 12
#define MAGNITUDE_DIGITAL 13
#define MAGNITUDE_EVENTS 14
#define MAGNITUDE_EVENT 14
#define MAGNITUDE_PM1dot0 15
#define MAGNITUDE_PM2dot5 16
#define MAGNITUDE_PM10 17
@ -279,5 +297,8 @@
#define MAGNITUDE_UV 20
#define MAGNITUDE_DISTANCE 21
#define MAGNITUDE_HCHO 22
#define MAGNITUDE_GEIGER_CPM 23
#define MAGNITUDE_GEIGER_SIEVERT 24
#define MAGNITUDE_COUNT 25
#define MAGNITUDE_MAX 23
#define MAGNITUDE_MAX 26

+ 1
- 2
code/espurna/config/version.h View File

@ -1,6 +1,5 @@
#define APP_NAME "ESPURNA"
#define APP_VERSION "1.12.7a"
#define APP_REVISION "db84006"
#define APP_VERSION "1.13.2b"
#define APP_AUTHOR "xose.perez@gmail.com"
#define APP_WEBSITE "http://tinkerman.cat"
#define CFG_VERSION 3

BIN
code/espurna/data/index.all.html.gz View File


BIN
code/espurna/data/index.html.gz View File


BIN
code/espurna/data/index.light.html.gz View File


BIN
code/espurna/data/index.rfbridge.html.gz View File


BIN
code/espurna/data/index.rfm69.html.gz View File


BIN
code/espurna/data/index.sensor.html.gz View File


BIN
code/espurna/data/index.small.html.gz View File


+ 44
- 34
code/espurna/debug.ino View File

@ -10,7 +10,7 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
#include <stdio.h>
#include <stdarg.h>
#include <EEPROM.h>
#include <EEPROM_Rotate.h>
extern "C" {
#include "user_interface.h"
@ -26,6 +26,8 @@ char _udp_syslog_header[40] = {0};
void _debugSend(char * message) {
bool pause = false;
#if DEBUG_ADD_TIMESTAMP
static bool add_timestamp = true;
char timestamp[10] = {0};
@ -50,6 +52,7 @@ void _debugSend(char * message) {
#endif
_udp_debug.write(message);
_udp_debug.endPacket();
pause = true;
#if SYSTEM_CHECK_ENABLED
}
#endif
@ -60,24 +63,31 @@ void _debugSend(char * message) {
_telnetWrite(timestamp, strlen(timestamp));
#endif
_telnetWrite(message, strlen(message));
pause = true;
#endif
#if DEBUG_WEB_SUPPORT
if (wsConnected() && (getFreeHeap() > 10000)) {
String m = String(message);
m.replace("\"", "&quot;");
m.replace("{", "&#123");
m.replace("}", "&#125");
char buffer[m.length() + 24];
DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(1) + strlen(message) + 17);
JsonObject &root = jsonBuffer.createObject();
#if DEBUG_ADD_TIMESTAMP
snprintf_P(buffer, sizeof(buffer), PSTR("{\"weblog\": \"%s%s\"}"), timestamp, m.c_str());
char buffer[strlen(timestamp) + strlen(message) + 1];
snprintf_P(buffer, sizeof(buffer), "%s%s", timestamp, message);
root.set("weblog", buffer);
#else
snprintf_P(buffer, sizeof(buffer), PSTR("{\"weblog\": \"%s\"}"), m.c_str());
root.set("weblog", message);
#endif
wsSend(buffer);
String out;
root.printTo(out);
jsonBuffer.clear();
wsSend(out.c_str());
pause = true;
}
#endif
if (pause) optimistic_yield(100);
}
// -----------------------------------------------------------------------------
@ -204,31 +214,31 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack
// write crash time to EEPROM
uint32_t crash_time = millis();
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
// write reset info to EEPROM
EEPROM.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason);
EEPROM.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause);
EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason);
EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause);
// write epc1, epc2, epc3, excvaddr and depc to EEPROM
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1);
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2);
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3);
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr);
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc);
// write stack start and end address to EEPROM
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
// write stack trace to EEPROM
int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
for (uint32_t i = stack_start; i < stack_end; i++) {
byte* byteValue = (byte*) i;
EEPROM.write(current_address++, *byteValue);
EEPROMr.write(current_address++, *byteValue);
}
EEPROM.commit();
EEPROMr.commit();
}
@ -237,8 +247,8 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack
*/
void debugClearCrashInfo() {
uint32_t crash_time = 0xFFFFFFFF;
EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
EEPROM.commit();
EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
EEPROMr.commit();
}
/**
@ -247,28 +257,28 @@ void debugClearCrashInfo() {
void debugDumpCrashInfo() {
uint32_t crash_time;
EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
if ((crash_time == 0) || (crash_time == 0xFFFFFFFF)) {
DEBUG_MSG_P(PSTR("[DEBUG] No crash info\n"));
return;
}
DEBUG_MSG_P(PSTR("[DEBUG] Latest crash was at %lu ms after boot\n"), crash_time);
DEBUG_MSG_P(PSTR("[DEBUG] Reason of restart: %u\n"), EEPROM.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON));
DEBUG_MSG_P(PSTR("[DEBUG] Exception cause: %u\n"), EEPROM.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE));
DEBUG_MSG_P(PSTR("[DEBUG] Reason of restart: %u\n"), EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON));
DEBUG_MSG_P(PSTR("[DEBUG] Exception cause: %u\n"), EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE));
uint32_t epc1, epc2, epc3, excvaddr, depc;
EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1);
EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2);
EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3);
EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr);
EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc);
DEBUG_MSG_P(PSTR("[DEBUG] epc1=0x%08x epc2=0x%08x epc3=0x%08x\n"), epc1, epc2, epc3);
DEBUG_MSG_P(PSTR("[DEBUG] excvaddr=0x%08x depc=0x%08x\n"), excvaddr, depc);
uint32_t stack_start, stack_end;
EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
DEBUG_MSG_P(PSTR("[DEBUG] >>>stack>>>\n[DEBUG] "));
int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
int16_t stack_len = stack_end - stack_start;
@ -276,7 +286,7 @@ void debugDumpCrashInfo() {
for (int16_t i = 0; i < stack_len; i += 0x10) {
DEBUG_MSG_P(PSTR("%08x: "), stack_start + i);
for (byte j = 0; j < 4; j++) {
EEPROM.get(current_address, stack_trace);
EEPROMr.get(current_address, stack_trace);
DEBUG_MSG_P(PSTR("%08x "), stack_trace);
current_address += 4;
}


+ 86
- 0
code/espurna/eeprom.ino View File

@ -0,0 +1,86 @@
/*
EEPROM MODULE
*/
#include <EEPROM_Rotate.h>
// -----------------------------------------------------------------------------
void eepromRotate(bool value) {
// Enable/disable EEPROM rotation only if we are using more sectors than the
// reserved by the memory layout
if (EEPROMr.size() > EEPROMr.reserved()) {
if (value) {
DEBUG_MSG_P(PSTR("[EEPROM] Reenabling EEPROM rotation\n"));
} else {
DEBUG_MSG_P(PSTR("[EEPROM] Disabling EEPROM rotation\n"));
}
EEPROMr.rotate(value);
}
}
String eepromSectors() {
String response;
for (uint32_t i = 0; i < EEPROMr.size(); i++) {
if (i > 0) response = response + String(", ");
response = response + String(EEPROMr.base() - i);
}
return response;
}
#if TERMINAL_SUPPORT
void _eepromInitCommands() {
settingsRegisterCommand(F("EEPROM.DUMP"), [](Embedis* e) {
EEPROMr.dump(settingsSerial());
DEBUG_MSG_P(PSTR("\n+OK\n"));
});
settingsRegisterCommand(F("FLASH.DUMP"), [](Embedis* e) {
if (e->argc < 2) {
DEBUG_MSG_P(PSTR("-ERROR: Wrong arguments\n"));
return;
}
uint32_t sector = String(e->argv[1]).toInt();
uint32_t max = ESP.getFlashChipSize() / SPI_FLASH_SEC_SIZE;
if (sector >= max) {
DEBUG_MSG_P(PSTR("-ERROR: Sector out of range\n"));
return;
}
EEPROMr.dump(settingsSerial(), sector);
DEBUG_MSG_P(PSTR("\n+OK\n"));
});
}
#endif
// -----------------------------------------------------------------------------
void eepromSetup() {
#ifdef EEPROM_ROTATE_SECTORS
EEPROMr.size(EEPROM_ROTATE_SECTORS);
#else
// If the memory layout has more than one sector reserved use those,
// otherwise calculate pool size based on memory size.
if (EEPROMr.size() == 1) {
if (EEPROMr.last() > 1000) { // 4Mb boards
EEPROMr.size(4);
} else if (EEPROMr.last() > 250) { // 1Mb boards
EEPROMr.size(2);
}
}
#endif
EEPROMr.offset(EEPROM_ROTATE_DATA);
EEPROMr.begin(EEPROM_SIZE);
#if TERMINAL_SUPPORT
_eepromInitCommands();
#endif
}

+ 16
- 4
code/espurna/espurna.ino View File

@ -47,7 +47,10 @@ void setup() {
debugSetup();
#endif
// Init EEPROM, Serial, SPIFFS and system check
// Init EEPROM
eepromSetup();
// Init Serial, SPIFFS and system check
systemSetup();
// Init persistance and terminal features
@ -84,11 +87,13 @@ void setup() {
#if WEB_SUPPORT
webSetup();
wsSetup();
apiSetup();
#if DEBUG_WEB_SUPPORT
debugWebSetup();
#endif
#endif
#if API_SUPPORT
apiSetup();
#endif
// lightSetup must be called before relaySetup
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
@ -96,8 +101,12 @@ void setup() {
#endif
relaySetup();
buttonSetup();
ledSetup();
#if BUTTON_SUPPORT
buttonSetup();
#endif
#if LED_SUPPORT
ledSetup();
#endif
#if MQTT_SUPPORT
mqttSetup();
#endif
@ -137,6 +146,9 @@ void setup() {
#if THINGSPEAK_SUPPORT
tspkSetup();
#endif
#if RFM69_SUPPORT
rfm69Setup();
#endif
#if RF_SUPPORT
rfSetup();
#endif


+ 636
- 0
code/espurna/fs_math.c View File

@ -0,0 +1,636 @@
/**
* This code is available at
* http://www.mindspring.com/~pfilandr/C/fs_math/
* and it is believed to be public domain.
*/
/* BEGIN fs_math.c */
#include "libs/fs_math.h"
#include <float.h>
/*
** pi == (atan(1.0 / 3) + atan(1.0 / 2)) * 4
*/
static double fs_pi(void);
static long double fs_pil(void);
double fs_sqrt(double x)
{
int n;
double a, b;
if (x > 0 && DBL_MAX >= x) {
for (n = 0; x > 2; x /= 4) {
++n;
}
while (0.5 > x) {
--n;
x *= 4;
}
a = x;
b = (1 + x) / 2;
do {
x = b;
b = (a / x + x) / 2;
} while (x > b);
while (n > 0) {
x *= 2;
--n;
}
while (0 > n) {
x /= 2;
++n;
}
} else {
if (x != 0) {
x = DBL_MAX;
}
}
return x;
}
double fs_log(double x)
{
int n;
double a, b, c, epsilon;
static double A, B, C;
static int initialized;
if (x > 0 && DBL_MAX >= x) {
if (!initialized) {
initialized = 1;
A = fs_sqrt(2);
B = A / 2;
C = fs_log(A);
}
for (n = 0; x > A; x /= 2) {
++n;
}
while (B > x) {
--n;
x *= 2;
}
a = (x - 1) / (x + 1);
x = C * n + a;
c = a * a;
n = 1;
epsilon = DBL_EPSILON * x;
if (0 > a) {
if (epsilon > 0) {
epsilon = -epsilon;
}
do {
n += 2;
a *= c;
b = a / n;
x += b;
} while (epsilon > b);
} else {
if (0 > epsilon) {
epsilon = -epsilon;
}
do {
n += 2;
a *= c;
b = a / n;
x += b;
} while (b > epsilon);
}
x *= 2;
} else {
x = -DBL_MAX;
}
return x;
}
double fs_log10(double x)
{
static double log_10;
static int initialized;
if (!initialized) {
initialized = 1;
log_10 = fs_log(10);
}
return x > 0 && DBL_MAX >= x ? fs_log(x) / log_10 : fs_log(x);
}
double fs_exp(double x)
{
unsigned n, square;
double b, e;
static double x_max, x_min, epsilon;
static int initialized;
if (!initialized) {
initialized = 1;
x_max = fs_log(DBL_MAX);
x_min = fs_log(DBL_MIN);
epsilon = DBL_EPSILON / 4;
}
if (x_max >= x && x >= x_min) {
for (square = 0; x > 1; x /= 2) {
++square;
}
while (-1 > x) {
++square;
x /= 2;
}
e = b = n = 1;
do {
b /= n++;
b *= x;
e += b;
b /= n++;
b *= x;
e += b;
} while (b > epsilon);
while (square-- != 0) {
e *= e;
}
} else {
e = x > 0 ? DBL_MAX : 0;
}
return e;
}
double fs_modf(double value, double *iptr)
{
double a, b;
const double c = value;
if (0 > c) {
value = -value;
}
if (DBL_MAX >= value) {
for (*iptr = 0; value >= 1; value -= b) {
a = value / 2;
b = 1;
while (a >= b) {
b *= 2;
}
*iptr += b;
}
} else {
*iptr = value;
value = 0;
}
if (0 > c) {
*iptr = -*iptr;
value = -value;
}
return value;
}
double fs_fmod(double x, double y)
{
double a, b;
const double c = x;
if (0 > c) {
x = -x;
}
if (0 > y) {
y = -y;
}
if (y != 0 && DBL_MAX >= y && DBL_MAX >= x) {
while (x >= y) {
a = x / 2;
b = y;
while (a >= b) {
b *= 2;
}
x -= b;
}
} else {
x = 0;
}
return 0 > c ? -x : x;
}
double fs_pow(double x, double y)
{
double p = 0;
if (0 > x && fs_fmod(y, 1) == 0) {
if (fs_fmod(y, 2) == 0) {
p = fs_exp(fs_log(-x) * y);
} else {
p = -fs_exp(fs_log(-x) * y);
}
} else {
if (x != 0 || 0 >= y) {
p = fs_exp(fs_log( x) * y);
}
}
return p;
}
static double fs_pi(void)
{
unsigned n;
double a, b, epsilon;
static double p;
static int initialized;
if (!initialized) {
initialized = 1;
epsilon = DBL_EPSILON / 4;
n = 1;
a = 3;
do {
a /= 9;
b = a / n;
n += 2;
a /= 9;
b -= a / n;
n += 2;
p += b;
} while (b > epsilon);
epsilon = DBL_EPSILON / 2;
n = 1;
a = 2;
do {
a /= 4;
b = a / n;
n += 2;
a /= 4;
b -= a / n;
n += 2;
p += b;
} while (b > epsilon);
p *= 4;
}
return p;
}
double fs_cos(double x)
{
unsigned n;
int negative, sine;
double a, b, c;
static double pi, two_pi, half_pi, third_pi, epsilon;
static int initialized;
if (0 > x) {
x = -x;
}
if (DBL_MAX >= x) {
if (!initialized) {
initialized = 1;
pi = fs_pi();
two_pi = 2 * pi;
half_pi = pi / 2;
third_pi = pi / 3;
epsilon = DBL_EPSILON / 2;
}
if (x > two_pi) {
x = fs_fmod(x, two_pi);
}
if (x > pi) {
x = two_pi - x;
}
if (x > half_pi) {
x = pi - x;
negative = 1;
} else {
negative = 0;
}
if (x > third_pi) {
x = half_pi - x;
sine = 1;
} else {
sine = 0;
}
c = x * x;
x = n = 0;
a = 1;
do {
b = a;
a *= c;
a /= ++n;
a /= ++n;
b -= a;
a *= c;
a /= ++n;
a /= ++n;
x += b;
} while (b > epsilon);
if (sine) {
x = fs_sqrt((1 - x) * (1 + x));
}
if (negative) {
x = -x;
}
} else {
x = -DBL_MAX;
}
return x;
}
double fs_log2(double x)
{
static double log_2;
static int initialized;
if (!initialized) {
initialized = 1;
log_2 = fs_log(2);
}
return x > 0 && DBL_MAX >= x ? fs_log(x) / log_2 : fs_log(x);
}
double fs_exp2(double x)
{
static double log_2;
static int initialized;
if (!initialized) {
initialized = 1;
log_2 = fs_log(2);
}
return fs_exp(x * log_2);
}
long double fs_powl(long double x, long double y)
{
long double p;
if (0 > x && fs_fmodl(y, 1) == 0) {
if (fs_fmodl(y, 2) == 0) {
p = fs_expl(fs_logl(-x) * y);
} else {
p = -fs_expl(fs_logl(-x) * y);
}
} else {
if (x != 0 || 0 >= y) {
p = fs_expl(fs_logl( x) * y);
} else {
p = 0;
}
}
return p;
}
long double fs_sqrtl(long double x)
{
long int n;
long double a, b;
if (x > 0 && LDBL_MAX >= x) {
for (n = 0; x > 2; x /= 4) {
++n;
}
while (0.5 > x) {
--n;
x *= 4;
}
a = x;
b = (1 + x) / 2;
do {
x = b;
b = (a / x + x) / 2;
} while (x > b);
while (n > 0) {
x *= 2;
--n;
}
while (0 > n) {
x /= 2;
++n;
}
} else {
if (x != 0) {
x = LDBL_MAX;
}
}
return x;
}
long double fs_logl(long double x)
{
long int n;
long double a, b, c, epsilon;
static long double A, B, C;
static int initialized;
if (x > 0 && LDBL_MAX >= x) {
if (!initialized) {
initialized = 1;
B = 1.5;
do {
A = B;
B = 1 / A + A / 2;
} while (A > B);
B /= 2;
C = fs_logl(A);
}
for (n = 0; x > A; x /= 2) {
++n;
}
while (B > x) {
--n;
x *= 2;
}
a = (x - 1) / (x + 1);
x = C * n + a;
c = a * a;
n = 1;
epsilon = LDBL_EPSILON * x;
if (0 > a) {
if (epsilon > 0) {
epsilon = -epsilon;
}
do {
n += 2;
a *= c;
b = a / n;
x += b;
} while (epsilon > b);
} else {
if (0 > epsilon) {
epsilon = -epsilon;
}
do {
n += 2;
a *= c;
b = a / n;
x += b;
} while (b > epsilon);
}
x *= 2;
} else {
x = -LDBL_MAX;
}
return x;
}
long double fs_expl(long double x)
{
long unsigned n, square;
long double b, e;
static long double x_max, x_min, epsilon;
static int initialized;
if (!initialized) {
initialized = 1;
x_max = fs_logl(LDBL_MAX);
x_min = fs_logl(LDBL_MIN);
epsilon = LDBL_EPSILON / 4;
}
if (x_max >= x && x >= x_min) {
for (square = 0; x > 1; x /= 2) {
++square;
}
while (-1 > x) {
++square;
x /= 2;
}
e = b = n = 1;
do {
b /= n++;
b *= x;
e += b;
b /= n++;
b *= x;
e += b;
} while (b > epsilon);
while (square-- != 0) {
e *= e;
}
} else {
e = x > 0 ? LDBL_MAX : 0;
}
return e;
}
static long double fs_pil(void)
{
long unsigned n;
long double a, b, epsilon;
static long double p;
static int initialized;
if (!initialized) {
initialized = 1;
epsilon = LDBL_EPSILON / 4;
n = 1;
a = 3;
do {
a /= 9;
b = a / n;
n += 2;
a /= 9;
b -= a / n;
n += 2;
p += b;
} while (b > epsilon);
epsilon = LDBL_EPSILON / 2;
n = 1;
a = 2;
do {
a /= 4;
b = a / n;
n += 2;
a /= 4;
b -= a / n;
n += 2;
p += b;
} while (b > epsilon);
p *= 4;
}
return p;
}
long double fs_cosl(long double x)
{
long unsigned n;
int negative, sine;
long double a, b, c;
static long double pi, two_pi, half_pi, third_pi, epsilon;
static int initialized;
if (0 > x) {
x = -x;
}
if (LDBL_MAX >= x) {
if (!initialized) {
initialized = 1;
pi = fs_pil();
two_pi = 2 * pi;
half_pi = pi / 2;
third_pi = pi / 3;
epsilon = LDBL_EPSILON / 2;
}
if (x > two_pi) {
x = fs_fmodl(x, two_pi);
}
if (x > pi) {
x = two_pi - x;
}
if (x > half_pi) {
x = pi - x;
negative = 1;
} else {
negative = 0;
}
if (x > third_pi) {
x = half_pi - x;
sine = 1;
} else {
sine = 0;
}
c = x * x;
x = n = 0;
a = 1;
do {
b = a;
a *= c;
a /= ++n;
a /= ++n;
b -= a;
a *= c;
a /= ++n;
a /= ++n;
x += b;
} while (b > epsilon);
if (sine) {
x = fs_sqrtl((1 - x) * (1 + x));
}
if (negative) {
x = -x;
}
} else {
x = -LDBL_MAX;
}
return x;
}
long double fs_fmodl(long double x, long double y)
{
long double a, b;
const long double c = x;
if (0 > c) {
x = -x;
}
if (0 > y) {
y = -y;
}
if (y != 0 && LDBL_MAX >= y && LDBL_MAX >= x) {
while (x >= y) {
a = x / 2;
b = y;
while (a >= b) {
b *= 2;
}
x -= b;
}
} else {
x = 0;
}
return 0 > c ? -x : x;
}
/* END fs_math.c */

+ 16
- 6
code/espurna/homeassistant.ino View File

@ -44,6 +44,7 @@ void _haSendMagnitudes() {
JsonObject& config = jsonBuffer.createObject();
_haSendMagnitude(i, config);
config.printTo(output);
jsonBuffer.clear();
}
mqttSendRaw(topic.c_str(), output.c_str());
@ -72,11 +73,11 @@ void _haSendSwitch(unsigned char i, JsonObject& config) {
if (relayCount()) {
config["state_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, false);
config["command_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, true);
config["payload_on"] = String("1");
config["payload_off"] = String("0");
config["payload_on"] = String(HOMEASSISTANT_PAYLOAD_ON);
config["payload_off"] = String(HOMEASSISTANT_PAYLOAD_OFF);
config["availability_topic"] = mqttTopic(MQTT_TOPIC_STATUS, false);
config["payload_available"] = String("1");
config["payload_not_available"] = String("0");
config["payload_available"] = String(HOMEASSISTANT_PAYLOAD_ON);
config["payload_not_available"] = String(HOMEASSISTANT_PAYLOAD_OFF);
}
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
@ -123,6 +124,7 @@ void _haSendSwitches() {
JsonObject& config = jsonBuffer.createObject();
_haSendSwitch(i, config);
config.printTo(output);
jsonBuffer.clear();
}
mqttSendRaw(topic.c_str(), output.c_str());
@ -163,6 +165,8 @@ String _haGetConfig() {
}
output += "\n";
jsonBuffer.clear();
}
#if SENSOR_SUPPORT
@ -186,6 +190,8 @@ String _haGetConfig() {
}
output += "\n";
jsonBuffer.clear();
}
#endif
@ -255,13 +261,17 @@ void _haInitCommands() {
settingsRegisterCommand(F("HA.SEND"), [](Embedis* e) {
setSetting("haEnabled", "1");
_haConfigure();
wsSend(_haWebSocketOnSend);
#if WEB_SUPPORT
wsSend(_haWebSocketOnSend);
#endif
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("HA.CLEAR"), [](Embedis* e) {
setSetting("haEnabled", "0");
_haConfigure();
wsSend(_haWebSocketOnSend);
#if WEB_SUPPORT
wsSend(_haWebSocketOnSend);
#endif
DEBUG_MSG_P(PSTR("+OK\n"));
});
}


+ 2
- 2
code/espurna/i2c.ino View File

@ -230,7 +230,7 @@ uint16_t i2c_read_uint16(uint8_t address, uint8_t reg) {
void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len) {
Wire.beginTransmission((uint8_t) address);
Wire.requestFrom(address, (uint8_t) len);
for (int i=0; i<len; i++) buffer[i] = Wire.read();
for (size_t i=0; i<len; i++) buffer[i] = Wire.read();
Wire.endTransmission();
}
@ -332,7 +332,7 @@ unsigned char i2cFind(size_t size, unsigned char * addresses) {
unsigned char i2cFindAndLock(size_t size, unsigned char * addresses) {
unsigned char start = 0;
unsigned char address = 0;
while (address = i2cFind(size, addresses, start)) {
while ((address = i2cFind(size, addresses, start))) {
if (i2cGetLock(address)) break;
start++;
}


+ 58
- 56
code/espurna/ir.ino View File

@ -20,83 +20,86 @@ unsigned long _ir_last_toggle = 0;
// PRIVATE
// -----------------------------------------------------------------------------
void _irProcessCode(unsigned long code) {
void _irProcessCode(unsigned long code, unsigned char type) {
static unsigned long last_code;
boolean found = false;
// Check valid code
unsigned long last_code = 0;
unsigned long last_time = 0;
if (code == 0xFFFFFFFF) return;
if (type == 0xFF) return;
if ((last_code == code) && (millis() - last_time < IR_DEBOUNCE)) return;
last_code = code;
DEBUG_MSG_P(PSTR("[IR] Received 0x%08X (%d)\n"), code, type);
// Repeat last valid code
DEBUG_MSG_P(PSTR("[IR] Received 0x%06X\n"), code);
if (code == 0xFFFFFFFF) {
DEBUG_MSG_P(PSTR("[IR] Processing 0x%06X\n"), code);
code = last_code;
}
#if IR_BUTTON_SET > 0
for (unsigned char i = 0; i < IR_BUTTON_COUNT ; i++) {
boolean found = false;
unsigned long button_code = pgm_read_dword(&IR_BUTTON[i][0]);
if (code == button_code) {
for (unsigned char i = 0; i < IR_BUTTON_COUNT ; i++) {
unsigned long button_mode = pgm_read_dword(&IR_BUTTON[i][1]);
unsigned long button_value = pgm_read_dword(&IR_BUTTON[i][2]);
uint32_t button_code = pgm_read_dword(&IR_BUTTON[i][0]);
if (code == button_code) {
if (button_mode == IR_BUTTON_MODE_STATE) {
relayStatus(0, button_value);
}
unsigned long button_mode = pgm_read_dword(&IR_BUTTON[i][1]);
unsigned long button_value = pgm_read_dword(&IR_BUTTON[i][2]);
if (button_mode == IR_BUTTON_MODE_TOGGLE) {
if (button_mode == IR_BUTTON_MODE_STATE) {
relayStatus(0, button_value);
}
if (millis() - _ir_last_toggle > 250){
if (button_mode == IR_BUTTON_MODE_TOGGLE) {
relayToggle(button_value);
_ir_last_toggle = millis();
} else {
DEBUG_MSG_P(PSTR("[IR] Ignoring repeated code\n"));
}
}
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
if (button_mode == IR_BUTTON_MODE_BRIGHTER) {
lightBrightnessStep(button_value ? 1 : -1);
nice_delay(150); //debounce
}
if (button_mode == IR_BUTTON_MODE_BRIGHTER) {
lightBrightnessStep(button_value ? 1 : -1);
nice_delay(150); //debounce
}
if (button_mode == IR_BUTTON_MODE_RGB) {
lightColor(button_value);
}
if (button_mode == IR_BUTTON_MODE_RGB) {
lightColor(button_value);
}
/*
#if LIGHT_PROVIDER == LIGHT_PROVIDER_FASTLED
if (button_mode == IR_BUTTON_MODE_EFFECT) {
_buttonAnimMode(button_value);
/*
#if LIGHT_PROVIDER == LIGHT_PROVIDER_FASTLED
if (button_mode == IR_BUTTON_MODE_EFFECT) {
_buttonAnimMode(button_value);
}
#endif
*/
/*
if (button_mode == IR_BUTTON_MODE_HSV) {
lightColor(button_value);
}
#endif
*/
*/
/*
if (button_mode == IR_BUTTON_MODE_HSV) {
lightColor(button_value);
}
*/
lightUpdate(true, true);
lightUpdate(true, true);
#endif
#endif
found = true;
break;
found = true;
last_code = code;
break;
}
}
}
}
if (!found) {
DEBUG_MSG_P(PSTR("[IR] Ignoring code\n"));
}
if (!found) {
DEBUG_MSG_P(PSTR("[IR] Ignoring code\n"));
}
#endif
}
#if MQTT_SUPPORT
char buffer[16];
snprintf_P(buffer, sizeof(buffer), "0x%08X", code);
mqttSend(MQTT_TOPIC_IR, buffer);
#endif
}
// -----------------------------------------------------------------------------
// PUBLIC API
@ -104,7 +107,7 @@ void _irProcessCode(unsigned long code) {
void irSetup() {
_ir_recv = new IRrecv(IR_PIN);
_ir_recv = new IRrecv(IR_RECEIVER_PIN);
_ir_recv->enableIRIn();
// Register loop
@ -114,8 +117,7 @@ void irSetup() {
void irLoop() {
if (_ir_recv->decode(&_ir_results)) {
unsigned long code = _ir_results.value;
_irProcessCode(code);
_irProcessCode(_ir_results.value, _ir_results.decode_type);
_ir_recv->resume(); // Receive the next value
}
}


+ 34
- 28
code/espurna/led.ino View File

@ -10,6 +10,8 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
// LED
// -----------------------------------------------------------------------------
#if LED_SUPPORT
typedef struct {
unsigned char pin;
bool reverse;
@ -182,16 +184,18 @@ void ledSetup() {
void ledLoop() {
uint8_t wifi_state = wifiState();
for (unsigned char i=0; i<_leds.size(); i++) {
if (_ledMode(i) == LED_MODE_WIFI) {
if (wifiConnected()) {
if (WiFi.getMode() == WIFI_AP) {
_ledBlink(i, 900, 100);
} else {
_ledBlink(i, 4900, 100);
}
if (wifi_state & WIFI_STATE_WPS || wifi_state & WIFI_STATE_SMARTCONFIG) {
_ledBlink(i, 100, 100);
} else if (wifi_state & WIFI_STATE_STA) {
_ledBlink(i, 4900, 100);
} else if (wifi_state & WIFI_STATE_AP) {
_ledBlink(i, 900, 100);
} else {
_ledBlink(i, 500, 500);
}
@ -200,19 +204,19 @@ void ledLoop() {
if (_ledMode(i) == LED_MODE_FINDME_WIFI) {
if (wifiConnected()) {
if (wifi_state & WIFI_STATE_WPS || wifi_state & WIFI_STATE_SMARTCONFIG) {
_ledBlink(i, 100, 100);
} else if (wifi_state & WIFI_STATE_STA) {
if (relayStatus(_leds[i].relay-1)) {
if (WiFi.getMode() == WIFI_AP) {
_ledBlink(i, 900, 100);
} else {
_ledBlink(i, 4900, 100);
}
_ledBlink(i, 4900, 100);
} else {
if (WiFi.getMode() == WIFI_AP) {
_ledBlink(i, 100, 900);
} else {
_ledBlink(i, 100, 4900);
}
_ledBlink(i, 100, 4900);
}
} else if (wifi_state & WIFI_STATE_AP) {
if (relayStatus(_leds[i].relay-1)) {
_ledBlink(i, 900, 100);
} else {
_ledBlink(i, 100, 900);
}
} else {
_ledBlink(i, 500, 500);
@ -222,19 +226,19 @@ void ledLoop() {
if (_ledMode(i) == LED_MODE_RELAY_WIFI) {
if (wifiConnected()) {
if (wifi_state & WIFI_STATE_WPS || wifi_state & WIFI_STATE_SMARTCONFIG) {
_ledBlink(i, 100, 100);
} else if (wifi_state & WIFI_STATE_STA) {
if (relayStatus(_leds[i].relay-1)) {
_ledBlink(i, 100, 4900);
} else {
_ledBlink(i, 4900, 100);
}
} else if (wifi_state & WIFI_STATE_AP) {
if (relayStatus(_leds[i].relay-1)) {
if (WiFi.getMode() == WIFI_AP) {
_ledBlink(i, 100, 900);
} else {
_ledBlink(i, 100, 4900);
}
_ledBlink(i, 100, 900);
} else {
if (WiFi.getMode() == WIFI_AP) {
_ledBlink(i, 900, 100);
} else {
_ledBlink(i, 4900, 100);
}
_ledBlink(i, 900, 100);
}
} else {
_ledBlink(i, 500, 500);
@ -288,3 +292,5 @@ void ledLoop() {
_led_update = false;
}
#endif // LED_SUPPORT

+ 65
- 0
code/espurna/libs/RFM69Wrap.h View File

@ -0,0 +1,65 @@
/*
RFM69Wrap
RFM69 by Felix Ruso (http://LowPowerLab.com/contact) wrapper for ESP8266
Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <RFM69.h>
#include <RFM69_ATC.h>
#include <SPI.h>
class RFM69Wrap: public RFM69_ATC {
public:
RFM69Wrap(uint8_t slaveSelectPin=RF69_SPI_CS, uint8_t interruptPin=RF69_IRQ_PIN, bool isRFM69HW=false, uint8_t interruptNum=0):
RFM69_ATC(slaveSelectPin, interruptPin, isRFM69HW, interruptNum) {};
protected:
// overriding SPI_CLOCK for ESP8266
void select() {
noInterrupts();
#if defined (SPCR) && defined (SPSR)
// save current SPI settings
_SPCR = SPCR;
_SPSR = SPSR;
#endif
// set RFM69 SPI settings
SPI.setDataMode(SPI_MODE0);
SPI.setBitOrder(MSBFIRST);
#if defined(__arm__)
SPI.setClockDivider(SPI_CLOCK_DIV16);
#elif defined(ARDUINO_ARCH_ESP8266)
SPI.setClockDivider(SPI_CLOCK_DIV2); // speeding it up for the ESP8266
#else
SPI.setClockDivider(SPI_CLOCK_DIV4);
#endif
digitalWrite(_slaveSelectPin, LOW);
}
};

+ 116
- 0
code/espurna/libs/fs_math.h View File

@ -0,0 +1,116 @@
/**
* This code is available at
* http://www.mindspring.com/~pfilandr/C/fs_math/
* and it is believed to be public domain.
*/
/* BEGIN fs_math.h */
/*
** Portable freestanding code.
*/
#ifndef H_FS_MATH_H
#define H_FS_MATH_H
double fs_sqrt(double x);
double fs_log(double x);
double fs_log10(double x);
/*
** exp(x) = 1 + x + x^2/2! + x^3/3! + ...
*/
double fs_exp(double x);
double fs_modf(double value, double *iptr);
double fs_fmod(double x, double y);
double fs_pow(double x, double y);
double fs_cos(double x);
/*
** C99
*/
double fs_log2(double x);
double fs_exp2(double x);
long double fs_powl(long double x, long double y);
long double fs_sqrtl(long double x);
long double fs_logl(long double x);
long double fs_expl(long double x);
long double fs_cosl(long double x);
long double fs_fmodl(long double x, long double y);
#endif
/* END fs_math.h */
#if 0
/*
> > Anybody know where I can get some source code for a
> > reasonably fast double
> > precision square root algorithm in C.
> > I'm looking for one that is not IEEE
> > compliant as I am running on a Z/OS mainframe.
> >
> > I would love to use the standard library but
> > unfortunatly I'm using a
> > stripped down version of C that looses the the runtime library
> > (we have to write our own).
>
> long double Ssqrt(long double x)
> {
> long double a, b;
> size_t c;
size_t is a bug here.
c needs to be a signed type:
long c;
> if (x > 0) {
> c = 0;
> while (x > 4) {
> x /= 4;
> ++c;
> }
> while (1.0 / 4 > x) {
> x *= 4;
> --c;
> }
> a = x;
> b = ((4 > a) + a) / 2;
Not a bug, but should be:
b = (1 + a) / 2;
> do {
> x = b;
> b = (a / x + x) / 2;
> } while (x > b);
> if (c > 0) {
The above line is why c needs to be a signed type,
otherwise the decremented values of c, are greater than zero,
and the function won't work if the initial value of x
is less than 0.25
> while (c--) {
> x *= 2;
> }
> } else {
> while (c++) {
> x /= 2;
> }
> }
> }
> return x;
> }
>
> >
> > That algorithm was actually 4 times slower
> > then the one below, and more
> > code. It was accurate though.
> >
>
> Sorry Pete, I wasn't looking very carefully.
> When I converted your function
> to double precision it's was much quicker, the best I've seen yet.
*/
#endif

+ 22
- 10
code/espurna/libs/pwm.h View File

@ -1,15 +1,28 @@
/*
* Copyright (C) 2016 Stefan Brüns <stefan.bruens@rwth-aachen.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef __PWM_H__
#define __PWM_H__
/*pwm.h: function and macro definition of PWM API , driver level */
/*user_light.h: user interface for light API, user level*/
/*user_light_adj: API for color changing and lighting effects, user level*/
/*NOTE!! : DO NOT CHANGE THIS FILE*/
/*SUPPORT UP TO 8 PWM CHANNEL*/
//#define PWM_CHANNEL_NUM_MAX 8
/*SUPPORT UP TO 8 PWM CHANNEL*/
#ifndef PWM_CHANNEL_NUM_MAX
#define PWM_CHANNEL_NUM_MAX 8
#endif
struct pwm_param {
uint32 period;
@ -17,7 +30,6 @@ struct pwm_param {
uint32 duty[PWM_CHANNEL_NUM_MAX]; //PWM_CHANNEL<=8
};
/* pwm_init should be called only once, for now */
void pwm_init(uint32 period, uint32 *duty,uint32 pwm_channel_num,uint32 (*pin_info_list)[3]);
void pwm_start(void);


+ 22
- 25
code/espurna/light.ino View File

@ -12,6 +12,10 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
#include <ArduinoJson.h>
#include <vector>
extern "C" {
#include "libs/fs_math.h"
}
#if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER
#define PWM_CHANNEL_NUM_MAX LIGHT_CHANNELS
extern "C" {
@ -275,53 +279,39 @@ void _fromHSV(const char * hsv) {
// Thanks to Sacha Telgenhof for sharing this code in his AiLight library
// https://github.com/stelgenhof/AiLight
void _fromKelvin(unsigned long kelvin, bool setMireds) {
void _fromKelvin(unsigned long kelvin) {
if (!_light_has_color) return;
if (setMireds) {
_light_mireds = constrain(round(1000000UL / kelvin), LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS);
}
_light_mireds = constrain(round(1000000UL / kelvin), LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS);
if (_light_use_cct) {
_setRGBInputValue(LIGHT_MAX_VALUE, LIGHT_MAX_VALUE, LIGHT_MAX_VALUE);
return;
}
// Calculate colors
kelvin /= 100;
unsigned int red = (kelvin <= 66)
? LIGHT_MAX_VALUE
: 329.698727446 * pow((kelvin - 60), -0.1332047592);
: 329.698727446 * fs_pow((double) (kelvin - 60), -0.1332047592);
unsigned int green = (kelvin <= 66)
? 99.4708025861 * log(kelvin) - 161.1195681661
: 288.1221695283 * pow(kelvin, -0.0755148492);
? 99.4708025861 * fs_log(kelvin) - 161.1195681661
: 288.1221695283 * fs_pow((double) kelvin, -0.0755148492);
unsigned int blue = (kelvin >= 66)
? LIGHT_MAX_VALUE
: ((kelvin <= 19)
? 0
: 138.5177312231 * log(kelvin - 10) - 305.0447927307);
: 138.5177312231 * fs_log(kelvin - 10) - 305.0447927307);
_setRGBInputValue(red, green, blue);
}
void _fromKelvin(unsigned long kelvin) {
_fromKelvin(kelvin, true);
}
// Color temperature is measured in mireds (kelvin = 1e6/mired)
void _fromMireds(unsigned long mireds) {
if (!_light_has_color) return;
_light_mireds = mireds = constrain(mireds, LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS);
if (_light_use_cct) {
_setRGBInputValue(LIGHT_MAX_VALUE, LIGHT_MAX_VALUE, LIGHT_MAX_VALUE);
return;
}
unsigned long kelvin = constrain(1000000UL / mireds, 1000, 40000) / 100;
_fromKelvin(kelvin, false);
unsigned long kelvin = constrain(1000000UL / mireds, 1000, 40000);
_fromKelvin(kelvin);
}
// -----------------------------------------------------------------------------
@ -836,6 +826,10 @@ void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
}
}
#endif
#if API_SUPPORT
void _lightAPISetup() {
// API entry points (protected with apikey)
if (_light_has_color) {
@ -906,7 +900,7 @@ void _lightAPISetup() {
}
}
#endif // WEB_SUPPORT
#endif // API_SUPPORT
#if TERMINAL_SUPPORT
@ -1079,7 +1073,6 @@ void lightSetup() {
_lightColorRestore();
#if WEB_SUPPORT
_lightAPISetup();
wsOnSendRegister(_lightWebSocketOnSend);
wsOnActionRegister(_lightWebSocketOnAction);
wsOnReceiveRegister(_lightWebSocketOnReceive);
@ -1091,6 +1084,10 @@ void lightSetup() {
});
#endif
#if API_SUPPORT
_lightAPISetup();
#endif
#if MQTT_SUPPORT
mqttRegister(_lightMQTTCallback);
#endif


+ 86
- 3
code/espurna/migrate.ino View File

@ -953,7 +953,7 @@ void migrate() {
setSetting("relayGPIO", 0, 5);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
#elif defined(NEO_COOLCAM_POWER_PLUG_WIFI)
#elif defined(NEO_COOLCAM_NAS_WR01W)
setSetting("board", 75);
setSetting("ledGPIO", 0, 4);
@ -962,7 +962,7 @@ void migrate() {
setSetting("btnRelay", 0, 0);
setSetting("relayGPIO", 0, 12);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
#elif defined(PILOTAK_ESP_DIN_V1)
setSetting("board", 76);
@ -977,7 +977,7 @@ void migrate() {
#elif defined(ESTINK_WIFI_POWER_STRIP)
setSetting("board", 76);
setSetting("board", 77);
setSetting("btnGPIO", 0, 16);
setSetting("btnRelay", 0, 3);
setSetting("ledGPIO", 0, 0);
@ -1004,6 +1004,89 @@ void migrate() {
setSetting("relayType", 2, RELAY_TYPE_NORMAL);
setSetting("relayType", 3, RELAY_TYPE_NORMAL);
#elif defined(BH_ONOFRE)
setSetting("board", 78);
setSetting("btnGPIO", 0, 12);
setSetting("btnGPIO", 0, 13);
setSetting("btnRelay", 0, 0);
setSetting("btnRelay", 0, 1);
setSetting("relayGPIO", 0, 4);
setSetting("relayGPIO", 1, 5);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
setSetting("relayType", 1, RELAY_TYPE_NORMAL);
#elif defined(BLITZWOLF_BWSHP2)
setSetting("board", 79);
setSetting("ledGPIO", 0, 2);
setSetting("ledLogic", 0, 1);
setSetting("ledGPIO", 1, 0);
setSetting("ledLogic", 1, 1);
setSetting("ledMode", 1, LED_MODE_FINDME);
setSetting("ledRelay", 1, 0);
setSetting("btnGPIO", 0, 13);
setSetting("btnRelay", 0, 0);
setSetting("relayGPIO", 0, 15);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
setSetting("selGPIO", 12);
setSetting("cf1GPIO", 14);
setSetting("cfGPIO", 5);
setSetting("pwrRatioC", 25740);
setSetting("pwrRatioV", 313400);
setSetting("pwrRatioP", 3414290);
setSetting("hlwSelC", LOW);
setSetting("hlwIntM", FALLING);
#elif defined(TINKERMAN_RFM69GW)
setSetting("board", 80);
setSetting("btnGPIO", 0, 0);
#elif defined(ITEAD_SONOFF_IFAN02)
setSetting("board", 81);
setSetting("btnGPIO", 0, 0);
setSetting("btnGPIO", 1, 9);
setSetting("btnGPIO", 2, 10);
setSetting("btnGPIO", 3, 14);
setSetting("ledGPIO", 1, 13);
setSetting("ledLogic", 1, 1);
setSetting("relayGPIO", 0, 12);
setSetting("relayGPIO", 1, 5);
setSetting("relayGPIO", 2, 4);
setSetting("relayGPIO", 3, 15);
setSetting("relayType", 0, RELAY_TYPE_NORMAL);
setSetting("relayType", 1, RELAY_TYPE_NORMAL);
setSetting("relayType", 2, RELAY_TYPE_NORMAL);
setSetting("relayType", 3, RELAY_TYPE_NORMAL);
#elif defined(GENERIC_AG_L4)
setSetting("board", 82);
setSetting("btnGPIO", 0, 4);
setSetting("btnGPIO", 1, 2);
setSetting("btnRelay", 0, 0);
setSetting("ledGPIO", 0, 5);
setSetting("ledGPIO", 1, 16);
setSetting("ledLogic", 0, 0);
setSetting("ledLogic", 1, 1);
setSetting("relayProvider", RELAY_PROVIDER_LIGHT);
setSetting("lightProvider", LIGHT_PROVIDER_DIMMER);
setSetting("chGPIO", 0, 14);
setSetting("chGPIO", 1, 13);
setSetting("chGPIO", 2, 12);
setSetting("chLogic", 0, 0);
setSetting("chLogic", 1, 0);
setSetting("chLogic", 2, 0);
setSetting("relays", 1);
#else
// Allow users to define new settings without migration config


+ 22
- 11
code/espurna/mqtt.ino View File

@ -8,7 +8,7 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
#if MQTT_SUPPORT
#include <EEPROM.h>
#include <EEPROM_Rotate.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ArduinoJson.h>
@ -218,7 +218,6 @@ void _mqttConfigure() {
if (_mqtt_topic.endsWith("/")) _mqtt_topic.remove(_mqtt_topic.length()-1);
// Placeholders
_mqtt_topic.replace("{identifier}", getSetting("hostname"));
_mqtt_topic.replace("{hostname}", getSetting("hostname"));
_mqtt_topic.replace("{magnitude}", "#");
if (_mqtt_topic.indexOf("#") == -1) _mqtt_topic = _mqtt_topic + "/#";
@ -250,6 +249,14 @@ void _mqttConfigure() {
}
void _mqttBackwards() {
String mqttTopic = getSetting("mqttTopic", MQTT_TOPIC);
if (mqttTopic.indexOf("{identifier}") > 0) {
mqttTopic.replace("{identifier}", "{hostname}");
setSetting("mqttTopic", mqttTopic);
}
}
unsigned long _mqttNextMessageId() {
static unsigned long id = 0;
@ -258,7 +265,7 @@ unsigned long _mqttNextMessageId() {
if (id == 0) {
// read id from EEPROM and shift it
id = EEPROM.read(EEPROM_MESSAGE_ID);
id = EEPROMr.read(EEPROM_MESSAGE_ID);
if (id == 0xFF) {
// There was nothing in EEPROM,
@ -267,9 +274,9 @@ unsigned long _mqttNextMessageId() {
} else {
id = (id << 8) + EEPROM.read(EEPROM_MESSAGE_ID + 1);
id = (id << 8) + EEPROM.read(EEPROM_MESSAGE_ID + 2);
id = (id << 8) + EEPROM.read(EEPROM_MESSAGE_ID + 3);
id = (id << 8) + EEPROMr.read(EEPROM_MESSAGE_ID + 1);
id = (id << 8) + EEPROMr.read(EEPROM_MESSAGE_ID + 2);
id = (id << 8) + EEPROMr.read(EEPROM_MESSAGE_ID + 3);
// Calculate next block and start from there
id = MQTT_MESSAGE_ID_SHIFT * (1 + (id / MQTT_MESSAGE_ID_SHIFT));
@ -280,11 +287,11 @@ unsigned long _mqttNextMessageId() {
// Save to EEPROM every MQTT_MESSAGE_ID_SHIFT
if (id % MQTT_MESSAGE_ID_SHIFT == 0) {
EEPROM.write(EEPROM_MESSAGE_ID + 0, (id >> 24) & 0xFF);
EEPROM.write(EEPROM_MESSAGE_ID + 1, (id >> 16) & 0xFF);
EEPROM.write(EEPROM_MESSAGE_ID + 2, (id >> 8) & 0xFF);
EEPROM.write(EEPROM_MESSAGE_ID + 3, (id >> 0) & 0xFF);
EEPROM.commit();
EEPROMr.write(EEPROM_MESSAGE_ID + 0, (id >> 24) & 0xFF);
EEPROMr.write(EEPROM_MESSAGE_ID + 1, (id >> 16) & 0xFF);
EEPROMr.write(EEPROM_MESSAGE_ID + 2, (id >> 8) & 0xFF);
EEPROMr.write(EEPROM_MESSAGE_ID + 3, (id >> 0) & 0xFF);
EEPROMr.commit();
}
id++;
@ -606,6 +613,8 @@ void mqttFlush() {
// Send
String output;
root.printTo(output);
jsonBuffer.clear();
mqttSendRaw(_mqtt_topic_json.c_str(), output.c_str(), false);
// Clear queue
@ -741,6 +750,8 @@ void mqttReset() {
void mqttSetup() {
_mqttBackwards();
DEBUG_MSG_P(PSTR("[MQTT] Async %s, SSL %s, Autoconnect %s\n"),
MQTT_USE_ASYNC ? "ENABLED" : "DISABLED",
ASYNC_TCP_SSL_ENABLED ? "ENABLED" : "DISABLED",


+ 19
- 3
code/espurna/ota.ino View File

@ -70,6 +70,7 @@ void _otaFrom(const char * host, unsigned int port, const char * url) {
#ifdef DEBUG_PORT
Update.printError(DEBUG_PORT);
#endif
eepromRotate(true);
}
DEBUG_MSG_P(PSTR("[OTA] Disconnected\n"));
@ -133,6 +134,9 @@ void _otaFrom(const char * host, unsigned int port, const char * url) {
}
#endif
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false);
DEBUG_MSG_P(PSTR("[OTA] Downloading %s\n"), _ota_url);
char buffer[strlen_P(OTA_REQUEST_TEMPLATE) + strlen(_ota_url) + strlen(_ota_host)];
snprintf_P(buffer, sizeof(buffer), OTA_REQUEST_TEMPLATE, _ota_url, _ota_host);
@ -140,7 +144,6 @@ void _otaFrom(const char * host, unsigned int port, const char * url) {
}, NULL);
#if ASYNC_TCP_SSL_ENABLED
bool connected = _ota_client->connect(host, port, 443 == port);
#else
@ -214,10 +217,16 @@ void otaSetup() {
// -------------------------------------------------------------------------
ArduinoOTA.onStart([]() {
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false);
DEBUG_MSG_P(PSTR("[OTA] Start\n"));
#if WEB_SUPPORT
wsSend_P(PSTR("{\"message\": 2}"));
#endif
});
ArduinoOTA.onEnd([]() {
@ -229,8 +238,14 @@ void otaSetup() {
deferredReset(100, CUSTOM_RESET_OTA);
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), (progress / (total / 100)));
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
static unsigned int _progOld;
unsigned int _prog = (progress / (total / 100));
if (_prog != _progOld) {
DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), _prog);
_progOld = _prog;
}
});
ArduinoOTA.onError([](ota_error_t error) {
@ -242,6 +257,7 @@ void otaSetup() {
else if (error == OTA_RECEIVE_ERROR) DEBUG_MSG_P(PSTR("Receive Failed\n"));
else if (error == OTA_END_ERROR) DEBUG_MSG_P(PSTR("End Failed\n"));
#endif
eepromRotate(true);
});
ArduinoOTA.begin();


+ 1
- 1
code/espurna/pwm.c View File

@ -43,9 +43,9 @@
#endif
#include <c_types.h>
#include <pwm.h>
#include <eagle_soc.h>
#include <ets_sys.h>
#include "libs/pwm.h"
// from SDK hw_timer.c
#define TIMER1_DIVIDE_BY_16 0x0004


+ 174
- 52
code/espurna/relay.ino View File

@ -6,7 +6,7 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include <EEPROM.h>
#include <EEPROM_Rotate.h>
#include <Ticker.h>
#include <ArduinoJson.h>
#include <vector>
@ -23,7 +23,6 @@ typedef struct {
unsigned long delay_off; // Delay to turn relay OFF
unsigned char pulse; // RELAY_PULSE_NONE, RELAY_PULSE_OFF or RELAY_PULSE_ON
unsigned long pulse_ms; // Pulse length in millis
unsigned long pulse_start; // Current pulse start (millis), 0 means no pulse
// Status variables
@ -35,6 +34,10 @@ typedef struct {
bool report; // Whether to report to own topic
bool group_report; // Whether to report to group topic
// Helping objects
Ticker pulseTicker; // Holds the pulse back timer
} relay_t;
std::vector<relay_t> _relays;
bool _relayRecursive = false;
@ -96,7 +99,7 @@ void _relayProviderStatus(unsigned char id, bool status) {
if (_relays.size() == lightChannels()) {
lightState(id, status);
lightState(true);
} else if (_relays.size() == lightChannels() + 1) {
} else if (_relays.size() == (lightChannels() + 1u)) {
if (id == 0) {
lightState(status);
} else {
@ -118,15 +121,15 @@ void _relayProviderStatus(unsigned char id, bool status) {
} else if (_relays[id].type == RELAY_TYPE_LATCHED || _relays[id].type == RELAY_TYPE_LATCHED_INVERSE) {
bool pulse = RELAY_TYPE_LATCHED ? HIGH : LOW;
digitalWrite(_relays[id].pin, !pulse);
digitalWrite(_relays[id].reset_pin, !pulse);
if (status) {
if (GPIO_NONE != _relays[id].reset_pin) digitalWrite(_relays[id].reset_pin, !pulse);
if (status || (GPIO_NONE == _relays[id].reset_pin)) {
digitalWrite(_relays[id].pin, pulse);
} else {
digitalWrite(_relays[id].reset_pin, pulse);
}
nice_delay(RELAY_LATCHING_PULSE);
digitalWrite(_relays[id].pin, !pulse);
digitalWrite(_relays[id].reset_pin, !pulse);
if (GPIO_NONE != _relays[id].reset_pin) digitalWrite(_relays[id].reset_pin, !pulse);
}
#endif
@ -191,7 +194,9 @@ void _relayProcess(bool mode) {
#endif
// Flag relay-based LEDs to update status
ledUpdate(true);
#if LED_SUPPORT
ledUpdate(true);
#endif
_relays[id].report = false;
_relays[id].group_report = false;
@ -200,27 +205,42 @@ void _relayProcess(bool mode) {
}
/**
* Walks the relay vector check if any relay has to pulse back
*/
void _relayPulseCheck() {
unsigned long current_time = millis();
for (unsigned char id = 0; id < _relays.size(); id++) {
if (_relays[id].pulse_start > 0) {
if (current_time - _relays[id].pulse_start > _relays[id].pulse_ms) {
_relays[id].pulse_start = 0;
relayToggle(id);
}
#if defined(ITEAD_SONOFF_IFAN02)
unsigned char _relay_ifan02_speeds[] = {0, 1, 3, 5};
unsigned char getSpeed() {
unsigned char speed =
(_relays[1].target_status ? 1 : 0) +
(_relays[2].target_status ? 2 : 0) +
(_relays[3].target_status ? 4 : 0);
for (unsigned char i=0; i<4; i++) {
if (_relay_ifan02_speeds[i] == speed) return i;
}
return 0;
}
void setSpeed(unsigned char speed) {
if ((0 <= speed) & (speed <= 3)) {
if (getSpeed() == speed) return;
unsigned char states = _relay_ifan02_speeds[speed];
for (unsigned char i=0; i<3; i++) {
relayStatus(i+1, states & 1 == 1);
states >>= 1;
}
}
}
#endif
// -----------------------------------------------------------------------------
// RELAY
// -----------------------------------------------------------------------------
void relayPulse(unsigned char id) {
_relays[id].pulseTicker.detach();
byte mode = _relays[id].pulse;
if (mode == RELAY_PULSE_NONE) return;
unsigned long ms = _relays[id].pulse_ms;
@ -229,11 +249,12 @@ void relayPulse(unsigned char id) {
bool status = relayStatus(id);
bool pulseStatus = (mode == RELAY_PULSE_ON);
if (pulseStatus == status) {
_relays[id].pulse_start = 0;
} else {
if (pulseStatus != status) {
DEBUG_MSG_P(PSTR("[RELAY] Scheduling relay #%d back in %lums (pulse)\n"), id, ms);
_relays[id].pulse_start = millis();
_relays[id].pulseTicker.once_ms(ms, relayToggle, id);
// Reconfigure after dynamic pulse
_relays[id].pulse = getSetting("relayPulse", id, RELAY_PULSE_MODE).toInt();
_relays[id].pulse_ms = 1000 * getSetting("relayTime", id, RELAY_PULSE_MODE).toFloat();
}
}
@ -264,8 +285,8 @@ bool relayStatus(unsigned char id, bool status, bool report, bool group_report)
} else {
unsigned int current_time = millis();
unsigned int fw_end = _relays[id].fw_start + 1000 * RELAY_FLOOD_WINDOW;
unsigned long current_time = millis();
unsigned long fw_end = _relays[id].fw_start + 1000 * RELAY_FLOOD_WINDOW;
unsigned long delay = status ? _relays[id].delay_on : _relays[id].delay_off;
_relays[id].fw_count++;
@ -369,9 +390,9 @@ void relaySave() {
if (relayStatus(i)) mask += bit;
bit += bit;
}
EEPROM.write(EEPROM_RELAY_STATUS, mask);
EEPROMr.write(EEPROM_RELAY_STATUS, mask);
DEBUG_MSG_P(PSTR("[RELAY] Saving mask: %d\n"), mask);
EEPROM.commit();
EEPROMr.commit();
}
void relayToggle(unsigned char id, bool report, bool group_report) {
@ -449,7 +470,7 @@ void _relayBoot() {
bool trigger_save = false;
// Get last statuses from EEPROM
unsigned char mask = EEPROM.read(EEPROM_RELAY_STATUS);
unsigned char mask = EEPROMr.read(EEPROM_RELAY_STATUS);
DEBUG_MSG_P(PSTR("[RELAY] Retrieving mask: %d\n"), mask);
// Walk the relays
@ -476,7 +497,6 @@ void _relayBoot() {
}
_relays[i].current_status = !status;
_relays[i].target_status = status;
_relays[i].pulse_start = 0;
#if RELAY_PROVIDER == RELAY_PROVIDER_STM
_relays[i].change_time = millis() + 3000 + 1000 * i;
#else
@ -487,8 +507,8 @@ void _relayBoot() {
// Save if there is any relay in the RELAY_BOOT_TOGGLE mode
if (trigger_save) {
EEPROM.write(EEPROM_RELAY_STATUS, mask);
EEPROM.commit();
EEPROMr.write(EEPROM_RELAY_STATUS, mask);
EEPROMr.commit();
}
_relayRecursive = false;
@ -498,9 +518,13 @@ void _relayBoot() {
void _relayConfigure() {
for (unsigned int i=0; i<_relays.size(); i++) {
pinMode(_relays[i].pin, OUTPUT);
if (_relays[i].type == RELAY_TYPE_LATCHED || _relays[i].type == RELAY_TYPE_LATCHED_INVERSE) {
if (GPIO_NONE != _relays[i].reset_pin) {
pinMode(_relays[i].reset_pin, OUTPUT);
}
if (_relays[i].type == RELAY_TYPE_INVERSE) {
//set to high to block short opening of relay
digitalWrite(_relays[i].pin, HIGH);
}
_relays[i].pulse = getSetting("relayPulse", i, RELAY_PULSE_MODE).toInt();
_relays[i].pulse_ms = 1000 * getSetting("relayTime", i, RELAY_PULSE_MODE).toFloat();
}
@ -604,16 +628,16 @@ void relaySetupWS() {
// REST API
//------------------------------------------------------------------------------
#if WEB_SUPPORT
#if API_SUPPORT
void relaySetupAPI() {
char key[20];
// API entry points (protected with apikey)
for (unsigned int relayID=0; relayID<relayCount(); relayID++) {
char key[15];
snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_RELAY, relayID);
apiRegister(key,
[relayID](char * buffer, size_t len) {
snprintf_P(buffer, len, PSTR("%d"), _relays[relayID].target_status ? 1 : 0);
@ -638,11 +662,45 @@ void relaySetupAPI() {
}
);
snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_PULSE, relayID);
apiRegister(key,
[relayID](char * buffer, size_t len) {
dtostrf((double) _relays[relayID].pulse_ms / 1000, 1-len, 3, buffer);
},
[relayID](const char * payload) {
unsigned long pulse = 1000 * String(payload).toFloat();
if (0 == pulse) return;
if (RELAY_PULSE_NONE != _relays[relayID].pulse) {
DEBUG_MSG_P(PSTR("[RELAY] Overriding relay #%d pulse settings\n"), relayID);
}
_relays[relayID].pulse_ms = pulse;
_relays[relayID].pulse = relayStatus(relayID) ? RELAY_PULSE_ON : RELAY_PULSE_OFF;
relayToggle(relayID, true, false);
}
);
#if defined(ITEAD_SONOFF_IFAN02)
apiRegister(MQTT_TOPIC_SPEED,
[relayID](char * buffer, size_t len) {
snprintf(buffer, len, "%u", getSpeed());
},
[relayID](const char * payload) {
setSpeed(atoi(payload));
}
);
#endif
}
}
#endif // WEB_SUPPORT
#endif // API_SUPPORT
//------------------------------------------------------------------------------
// MQTT
@ -657,7 +715,7 @@ void relayMQTT(unsigned char id) {
// Send state topic
if (_relays[id].report) {
_relays[id].report = false;
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? "1" : "0");
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
}
// Check group topic
@ -667,14 +725,22 @@ void relayMQTT(unsigned char id) {
if (t.length() > 0) {
bool status = relayStatus(id);
if (getSetting("mqttGroupInv", id, 0).toInt() == 1) status = !status;
mqttSendRaw(t.c_str(), status ? "1" : "0");
mqttSendRaw(t.c_str(), status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
}
}
// Send speed for IFAN02
#if defined (ITEAD_SONOFF_IFAN02)
char buffer[5];
snprintf(buffer, sizeof(buffer), "%u", getSpeed());
mqttSend(MQTT_TOPIC_SPEED, buffer);
#endif
}
void relayMQTT() {
for (unsigned int id=0; id < _relays.size(); id++) {
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? "1" : "0");
mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? RELAY_MQTT_ON : RELAY_MQTT_OFF);
}
}
@ -706,9 +772,18 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
#endif
// Subscribe to own /set topic
char buffer[strlen(MQTT_TOPIC_RELAY) + 3];
snprintf_P(buffer, sizeof(buffer), PSTR("%s/+"), MQTT_TOPIC_RELAY);
mqttSubscribe(buffer);
char relay_topic[strlen(MQTT_TOPIC_RELAY) + 3];
snprintf_P(relay_topic, sizeof(relay_topic), PSTR("%s/+"), MQTT_TOPIC_RELAY);
mqttSubscribe(relay_topic);
// Subscribe to pulse topic
char pulse_topic[strlen(MQTT_TOPIC_PULSE) + 3];
snprintf_P(pulse_topic, sizeof(pulse_topic), PSTR("%s/+"), MQTT_TOPIC_PULSE);
mqttSubscribe(pulse_topic);
#if defined(ITEAD_SONOFF_IFAN02)
mqttSubscribe(MQTT_TOPIC_SPEED);
#endif
// Subscribe to group topics
for (unsigned int i=0; i < _relays.size(); i++) {
@ -720,26 +795,53 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
if (type == MQTT_MESSAGE_EVENT) {
// Check relay topic
String t = mqttMagnitude((char *) topic);
if (t.startsWith(MQTT_TOPIC_RELAY)) {
// Get value
unsigned char value = relayParsePayload(payload);
if (value == 0xFF) return;
// magnitude is relay/#/pulse
if (t.startsWith(MQTT_TOPIC_PULSE)) {
unsigned int id = t.substring(strlen(MQTT_TOPIC_PULSE)+1).toInt();
if (id >= relayCount()) {
DEBUG_MSG_P(PSTR("[RELAY] Wrong relayID (%d)\n"), id);
return;
}
unsigned long pulse = 1000 * String(payload).toFloat();
if (0 == pulse) return;
if (RELAY_PULSE_NONE != _relays[id].pulse) {
DEBUG_MSG_P(PSTR("[RELAY] Overriding relay #%d pulse settings\n"), id);
}
_relays[id].pulse_ms = pulse;
_relays[id].pulse = relayStatus(id) ? RELAY_PULSE_ON : RELAY_PULSE_OFF;
relayToggle(id, true, false);
return;
}
// magnitude is relay/#
if (t.startsWith(MQTT_TOPIC_RELAY)) {
// Get relay ID
unsigned int id = t.substring(strlen(MQTT_TOPIC_RELAY)+1).toInt();
if (id >= relayCount()) {
DEBUG_MSG_P(PSTR("[RELAY] Wrong relayID (%d)\n"), id);
} else {
relayStatusWrap(id, value, false);
return;
}
return;
// Get value
unsigned char value = relayParsePayload(payload);
if (value == 0xFF) return;
relayStatusWrap(id, value, false);
return;
}
// Check group topics
for (unsigned int i=0; i < _relays.size(); i++) {
@ -762,6 +864,13 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
}
}
// Itead Sonoff IFAN02
#if defined (ITEAD_SONOFF_IFAN02)
if (t.startsWith(MQTT_TOPIC_SPEED)) {
setSpeed(atoi(payload));
}
#endif
}
if (type == MQTT_DISCONNECT_EVENT) {
@ -811,8 +920,14 @@ void _relayInitCommands() {
settingsRegisterCommand(F("RELAY"), [](Embedis* e) {
if (e->argc < 2) {
DEBUG_MSG_P(PSTR("-ERROR: Wrong arguments\n"));
return;
}
int id = String(e->argv[1]).toInt();
if (id >= relayCount()) {
DEBUG_MSG_P(PSTR("-ERROR: Wrong relayID (%d)\n"), id);
return;
}
if (e->argc > 2) {
int value = String(e->argv[2]).toInt();
if (value == 2) {
@ -822,6 +937,11 @@ void _relayInitCommands() {
}
}
DEBUG_MSG_P(PSTR("Status: %s\n"), _relays[id].target_status ? "true" : "false");
if (_relays[id].pulse != RELAY_PULSE_NONE) {
DEBUG_MSG_P(PSTR("Pulse: %s\n"), (_relays[id].pulse == RELAY_PULSE_ON) ? "ON" : "OFF");
DEBUG_MSG_P(PSTR("Pulse time: %d\n"), _relays[id].pulse_ms);
}
DEBUG_MSG_P(PSTR("+OK\n"));
});
@ -834,7 +954,6 @@ void _relayInitCommands() {
//------------------------------------------------------------------------------
void _relayLoop() {
_relayPulseCheck();
_relayProcess(false);
_relayProcess(true);
}
@ -844,10 +963,11 @@ void relaySetup() {
// Dummy relays for AI Light, Magic Home LED Controller, H801,
// Sonoff Dual and Sonoff RF Bridge
#if DUMMY_RELAY_COUNT > 0
unsigned int _delay_on[8] = {RELAY1_DELAY_ON, RELAY2_DELAY_ON, RELAY3_DELAY_ON, RELAY4_DELAY_ON, RELAY5_DELAY_ON, RELAY6_DELAY_ON, RELAY7_DELAY_ON, RELAY8_DELAY_ON};
unsigned int _delay_off[8] = {RELAY1_DELAY_OFF, RELAY2_DELAY_OFF, RELAY3_DELAY_OFF, RELAY4_DELAY_OFF, RELAY5_DELAY_OFF, RELAY6_DELAY_OFF, RELAY7_DELAY_OFF, RELAY8_DELAY_OFF};
for (unsigned char i=0; i < DUMMY_RELAY_COUNT; i++) {
_relays.push_back((relay_t) {0, RELAY_TYPE_NORMAL,0,_delay_on[i], _delay_off[i]});
_relays.push_back((relay_t) {0, RELAY_TYPE_NORMAL,0,_delay_on[i], _delay_off[i]});
}
#else
@ -887,9 +1007,11 @@ void relaySetup() {
espurnaRegisterLoop(_relayLoop);
#if WEB_SUPPORT
relaySetupAPI();
relaySetupWS();
#endif
#if API_SUPPORT
relaySetupAPI();
#endif
#if MQTT_SUPPORT
relaySetupMQTT();
#endif


+ 35
- 10
code/espurna/rfbridge.ino View File

@ -156,6 +156,9 @@ void _rfbSendRaw(const byte *message, const unsigned char n = RF_MESSAGE_SIZE) {
void _rfbSend(byte * message) {
#if RFB_DIRECT
unsigned int protocol = message[1];
unsigned int timing =
(message[2] << 8) |
(message[3] << 0) ;
unsigned int bitlength = message[4];
unsigned long rf_code =
(message[5] << 24) |
@ -163,7 +166,11 @@ void _rfbSend(byte * message) {
(message[7] << 8) |
(message[8] << 0) ;
_rfModem->setProtocol(protocol);
if (timing > 0) {
_rfModem->setPulseLength(timing);
}
_rfModem->send(rf_code, bitlength);
_rfModem->resetAvailable();
#else
Serial.println();
Serial.write(RF_CODE_START);
@ -202,6 +209,9 @@ void _rfbSend() {
}
void _rfbSend(byte * code, unsigned char times) {
#if RFB_DIRECT
times = 1;
#endif
char buffer[RF_MESSAGE_SIZE];
_rfbToChar(code, buffer);
@ -231,7 +241,7 @@ void _rfbSendRawOnce(byte *code, unsigned char length) {
#endif // RF_RAW_SUPPORT
bool _rfbMatch(char * code, unsigned char& relayID, unsigned char& value) {
bool _rfbMatch(char* code, unsigned char& relayID, unsigned char& value, char* buffer = NULL) {
if (strlen(code) != 18) return false;
@ -247,6 +257,7 @@ bool _rfbMatch(char * code, unsigned char& relayID, unsigned char& value) {
DEBUG_MSG_P(PSTR("[RFBRIDGE] Match ON code for relay %d\n"), i);
value = 1;
found = true;
if (buffer) strcpy(buffer, code_on.c_str());
}
String code_off = rfbRetrieve(i, false);
@ -254,6 +265,7 @@ bool _rfbMatch(char * code, unsigned char& relayID, unsigned char& value) {
DEBUG_MSG_P(PSTR("[RFBRIDGE] Match OFF code for relay %d\n"), i);
if (found) value = 2;
found = true;
if (buffer) strcpy(buffer, code_off.c_str());
}
if (found) {
@ -286,11 +298,12 @@ void _rfbDecode() {
}
if (action == RF_CODE_LEARN_OK || action == RF_CODE_RFIN) {
#if MQTT_SUPPORT
_rfbToChar(&_uartbuf[1], buffer);
mqttSend(MQTT_TOPIC_RFIN, buffer);
#endif
_rfbAck();
_rfbToChar(&_uartbuf[1], buffer);
DEBUG_MSG_P(PSTR("[RFBRIDGE] Received message '%s'\n"), buffer);
}
if (action == RF_CODE_LEARN_OK) {
@ -309,12 +322,16 @@ void _rfbDecode() {
if (action == RF_CODE_RFIN) {
DEBUG_MSG_P(PSTR("[RFBRIDGE] Forward message '%s'\n"), buffer);
// Look for the code
/* Look for the code, possibly replacing the code with the exact learned one on match
* we want to do this on learn too to be sure that the learned code is the same if it
* is equivalent
*/
unsigned char id;
unsigned char status = 0;
if (_rfbMatch(buffer, id, status)) {
unsigned char status;
bool matched = _rfbMatch(buffer, id, status, buffer);
if (matched) {
DEBUG_MSG_P(PSTR("[RFBRIDGE] Matched message '%s'\n"), buffer);
_rfbin = true;
if (status == 2) {
relayToggle(id);
@ -323,6 +340,10 @@ void _rfbDecode() {
}
}
#if MQTT_SUPPORT
mqttSend(MQTT_TOPIC_RFIN, buffer);
#endif
}
}
@ -354,11 +375,14 @@ void _rfbReceive() {
unsigned long rf_code = _rfModem->getReceivedValue();
if ( rf_code > 0) {
DEBUG_MSG_P(PSTR("[RFBRIDGE] Received code: %08X\n"), rf_code);
unsigned int timing = _rfModem->getReceivedDelay();
memset(_uartbuf, 0, sizeof(_uartbuf));
unsigned char *msgbuf = _uartbuf + 1;
_uartbuf[0] = _learning ? RF_CODE_LEARN_OK: RF_CODE_RFIN;
msgbuf[0] = 0xC0;
msgbuf[1] = _rfModem->getReceivedProtocol();
msgbuf[2] = timing >> 8;
msgbuf[3] = timing >> 0;
msgbuf[4] = _rfModem->getReceivedBitlength();
msgbuf[5] = rf_code >> 24;
msgbuf[6] = rf_code >> 16;
@ -594,6 +618,7 @@ void rfbSetup() {
_rfModem = new RCSwitch();
_rfModem->enableReceive(RFB_RX_PIN);
_rfModem->enableTransmit(RFB_TX_PIN);
_rfModem->setRepeatTransmit(6);
DEBUG_MSG_P(PSTR("[RFBRIDGE] RF receiver on GPIO %u\n"), RFB_RX_PIN);
DEBUG_MSG_P(PSTR("[RFBRIDGE] RF transmitter on GPIO %u\n"), RFB_TX_PIN);
#endif


+ 280
- 0
code/espurna/rfm69.ino View File

@ -0,0 +1,280 @@
/*
RFM69 MODULE
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
#if RFM69_SUPPORT
#include "libs/RFM69Wrap.h"
#define RFM69_PACKET_SEPARATOR ':'
// -----------------------------------------------------------------------------
// Locals
// -----------------------------------------------------------------------------
RFM69Wrap * _rfm69_radio;
struct _node_t {
unsigned long count = 0;
unsigned long missing = 0;
unsigned long duplicates = 0;
unsigned char lastPacketID = 0;
};
_node_t _rfm69_node_info[255];
unsigned char _rfm69_node_count;
unsigned long _rfm69_packet_count;
// -----------------------------------------------------------------------------
// WEB
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
void _rfm69WebSocketOnSend(JsonObject& root) {
root["rfm69Visible"] = 1;
root["rfm69Topic"] = getSetting("rfm69Topic", RFM69_DEFAULT_TOPIC);
root["packetCount"] = _rfm69_packet_count;
root["nodeCount"] = _rfm69_node_count;
JsonArray& mappings = root.createNestedArray("mapping");
for (unsigned char i=0; i<RFM69_MAX_TOPICS; i++) {
unsigned char node = getSetting("node", i, 0).toInt();
if (0 == node) break;
JsonObject& mapping = mappings.createNestedObject();
mapping["node"] = node;
mapping["key"] = getSetting("key", i, "");
mapping["topic"] = getSetting("topic", i, "");
}
}
bool _rfm69WebSocketOnReceive(const char * key, JsonVariant& value) {
if (strncmp(key, "rfm69", 5) == 0) return true;
if (strncmp(key, "node", 4) == 0) return true;
if (strncmp(key, "key", 3) == 0) return true;
if (strncmp(key, "topic", 5) == 0) return true;
return false;
}
void _rfm69WebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
if (strcmp(action, "clear-counts") == 0) _rfm69Clear();
}
#endif // WEB_SUPPORT
void _rfm69CleanNodes(unsigned char num) {
// Look for the last defined node
int i = 0;
while (i < num) {
if (getSetting("node", i, 0).toInt() == 0) break;
if (getSetting("key", i, "").length() == 0) break;
if (getSetting("topic", i, "").length() == 0) break;
++i;
}
// Delete all other settings
while (i < WIFI_MAX_NETWORKS) {
delSetting("node", i);
delSetting("key", i);
delSetting("topic", i);
++i;
}
}
void _rfm69Configure() {
_rfm69CleanNodes(RFM69_MAX_TOPICS);
}
// -----------------------------------------------------------------------------
// Radio
// -----------------------------------------------------------------------------
void _rfm69Debug(const char * level, packet_t * data) {
DEBUG_MSG_P(
PSTR("[RFM69] %s: messageID:%05d senderID:%03d targetID:%03d packetID:%03d rssi:%-04d key:%s value:%s\n"),
level,
data->messageID,
data->senderID,
data->targetID,
data->packetID,
data->rssi,
data->key,
data->value
);
}
void _rfm69Process(packet_t * data) {
// Count seen nodes and packets
if (_rfm69_node_info[data->senderID].count == 0) ++_rfm69_node_count;
++_rfm69_packet_count;
// Detect duplicates and missing packets
// packetID==0 means device is not sending packetID info
if (data->packetID > 0) {
if (_rfm69_node_info[data->senderID].count > 0) {
unsigned char gap = data->packetID - _rfm69_node_info[data->senderID].lastPacketID;
if (gap == 0) {
_rfm69_node_info[data->senderID].duplicates = _rfm69_node_info[data->senderID].duplicates + 1;
//_rfm69Debug("DUP", data);
return;
}
if ((gap > 1) && (data->packetID > 1)) {
_rfm69_node_info[data->senderID].missing = _rfm69_node_info[data->senderID].missing + gap - 1;
DEBUG_MSG_P(PSTR("[RFM69] %u missing packets detected\n"), gap - 1);
}
}
}
_rfm69Debug("OK ", data);
_rfm69_node_info[data->senderID].lastPacketID = data->packetID;
_rfm69_node_info[data->senderID].count = _rfm69_node_info[data->senderID].count + 1;
// Send info to websocket clients
{
char buffer[200];
snprintf_P(
buffer,
sizeof(buffer) - 1,
PSTR("{\"nodeCount\": %d, \"packetCount\": %lu, \"packet\": {\"senderID\": %u, \"targetID\": %u, \"packetID\": %u, \"key\": \"%s\", \"value\": \"%s\", \"rssi\": %d, \"duplicates\": %d, \"missing\": %d}}"),
_rfm69_node_count, _rfm69_packet_count,
data->senderID, data->targetID, data->packetID, data->key, data->value, data->rssi,
_rfm69_node_info[data->senderID].duplicates , _rfm69_node_info[data->senderID].missing);
wsSend(buffer);
}
// If we are the target of the message, forward it via MQTT, otherwise quit
if (!RFM69_PROMISCUOUS_SENDS && (RFM69_GATEWAY_ID != data->targetID)) return;
// Try to find a matching mapping
for (unsigned int i=0; i<RFM69_MAX_TOPICS; i++) {
unsigned char node = getSetting("node", i, 0).toInt();
if (0 == node) break;
if ((node == data->senderID) && (getSetting("key", i, "").equals(data->key))) {
mqttSendRaw((char *) getSetting("topic", i, "").c_str(), (char *) String(data->value).c_str());
return;
}
}
// Mapping not found, use default topic
String topic = getSetting("rfm69Topic", RFM69_DEFAULT_TOPIC);
if (topic.length() > 0) {
topic.replace("{node}", String(data->senderID));
topic.replace("{key}", String(data->key));
mqttSendRaw((char *) topic.c_str(), (char *) String(data->value).c_str());
}
}
void _rfm69Loop() {
if (_rfm69_radio->receiveDone()) {
uint8_t senderID = _rfm69_radio->SENDERID;
uint8_t targetID = _rfm69_radio->TARGETID;
int16_t rssi = _rfm69_radio->RSSI;
uint8_t length = _rfm69_radio->DATALEN;
char buffer[length + 1];
strncpy(buffer, (const char *) _rfm69_radio->DATA, length);
buffer[length] = 0;
// Do not send ACKs in promiscuous mode,
// we want to listen without being heard
if (!RFM69_PROMISCUOUS) {
if (_rfm69_radio->ACKRequested()) _rfm69_radio->sendACK();
}
uint8_t parts = 1;
for (uint8_t i=0; i<length; i++) {
if (buffer[i] == RFM69_PACKET_SEPARATOR) ++parts;
}
if (parts > 1) {
char sep[2] = {RFM69_PACKET_SEPARATOR, 0};
uint8_t packetID = 0;
char * key = strtok(buffer, sep);
char * value = strtok(NULL, sep);
if (parts > 2) {
char * packet = strtok(NULL, sep);
packetID = atoi(packet);
}
packet_t message;
message.messageID = ++_rfm69_packet_count;
message.packetID = packetID;
message.senderID = senderID;
message.targetID = targetID;
message.key = key;
message.value = value;
message.rssi = rssi;
_rfm69Process(&message);
}
}
}
void _rfm69Clear() {
for(unsigned int i=0; i<255; i++) {
_rfm69_node_info[i].duplicates = 0;
_rfm69_node_info[i].missing = 0;
}
_rfm69_node_count = 0;
_rfm69_packet_count = 0;
}
// -----------------------------------------------------------------------------
// RFM69
// -----------------------------------------------------------------------------
void rfm69Setup() {
delay(10);
_rfm69Configure();
_rfm69_radio = new RFM69Wrap(RFM69_CS_PIN, RFM69_IRQ_PIN, RFM69_IS_RFM69HW, digitalPinToInterrupt(RFM69_IRQ_PIN));
_rfm69_radio->initialize(RFM69_FREQUENCY, RFM69_NODE_ID, RFM69_NETWORK_ID);
_rfm69_radio->encrypt(RFM69_ENCRYPTKEY);
_rfm69_radio->promiscuous(RFM69_PROMISCUOUS);
_rfm69_radio->enableAutoPower(0);
if (RFM69_IS_RFM69HW) _rfm69_radio->setHighPower();
DEBUG_MSG_P(PSTR("[RFM69] Worning at %u MHz\n"), RFM69_FREQUENCY == RF69_433MHZ ? 433 : RFM69_FREQUENCY == RF69_868MHZ ? 868 : 915);
DEBUG_MSG_P(PSTR("[RFM69] Node %u\n"), RFM69_NODE_ID);
DEBUG_MSG_P(PSTR("[RFM69] Network %u\n"), RFM69_NETWORK_ID);
DEBUG_MSG_P(PSTR("[RFM69] Promiscuous mode %s\n"), RFM69_PROMISCUOUS ? "ON" : "OFF");
#if WEB_SUPPORT
wsOnSendRegister(_rfm69WebSocketOnSend);
wsOnReceiveRegister(_rfm69WebSocketOnReceive);
wsOnAfterParseRegister(_rfm69Configure);
wsOnActionRegister(_rfm69WebSocketOnAction);
#endif
// Register loop
espurnaRegisterLoop(_rfm69Loop);
}
#endif // RFM69_SUPPORT

+ 148
- 86
code/espurna/sensor.ino View File

@ -114,6 +114,8 @@ void _sensorWebSocketSendData(JsonObject& root) {
for (unsigned char i=0; i<_magnitudes.size(); i++) {
sensor_magnitude_t magnitude = _magnitudes[i];
if (magnitude.type == MAGNITUDE_EVENT) continue;
unsigned char decimals = _magnitudeDecimals(magnitude.type);
dtostrf(magnitude.current, 1-sizeof(buffer), decimals, buffer);
@ -183,6 +185,7 @@ void _sensorWebSocketStart(JsonObject& root) {
#if PZEM004T_SUPPORT
if (sensor->getID() == SENSOR_PZEM004T_ID) {
root["pzemVisible"] = 1;
root["pwrVisible"] = 1;
}
#endif
@ -220,6 +223,10 @@ void _sensorWebSocketStart(JsonObject& root) {
}
#endif // WEB_SUPPORT
#if API_SUPPORT
void _sensorAPISetup() {
for (unsigned char magnitude_id=0; magnitude_id<_magnitudes.size(); magnitude_id++) {
@ -239,7 +246,8 @@ void _sensorAPISetup() {
}
}
#endif
#endif // API_SUPPORT
#if TERMINAL_SUPPORT
@ -323,6 +331,8 @@ void _sensorLoad() {
#if ANALOG_SUPPORT
{
AnalogSensor * sensor = new AnalogSensor();
sensor->setSamples(ANALOG_SAMPLES);
sensor->setDelay(ANALOG_DELAY);
_sensors.push_back(sensor);
}
#endif
@ -443,13 +453,26 @@ void _sensorLoad() {
{
EventSensor * sensor = new EventSensor();
sensor->setGPIO(EVENTS_PIN);
sensor->setMode(EVENTS_PIN_MODE);
sensor->setTrigger(EVENTS_TRIGGER);
sensor->setPinMode(EVENTS_PIN_MODE);
sensor->setDebounceTime(EVENTS_DEBOUNCE);
sensor->setInterruptMode(EVENTS_INTERRUPT_MODE);
_sensors.push_back(sensor);
}
#endif
#if GEIGER_SUPPORT
{
GeigerSensor * sensor = new GeigerSensor(); // Create instance of thr Geiger module.
sensor->setGPIO(GEIGER_PIN); // Interrupt pin of the attached geiger counter board.
sensor->setMode(GEIGER_PIN_MODE); // This pin is an input.
sensor->setDebounceTime(GEIGER_DEBOUNCE); // Debounce time 25ms, because https://github.com/Trickx/espurna/wiki/Geiger-counter
sensor->setInterruptMode(GEIGER_INTERRUPT_MODE); // Interrupt triggering: edge detection rising.
sensor->setCPM2SievertFactor(GEIGER_CPM2SIEVERT); // Conversion factor from counts per minute to µSv/h
_sensors.push_back(sensor);
}
#endif
#if GUVAS12SD_SUPPORT
{
GUVAS12SDSensor * sensor = new GUVAS12SDSensor();
@ -458,11 +481,13 @@ void _sensorLoad() {
}
#endif
#if HCSR04_SUPPORT
#if SONAR_SUPPORT
{
HCSR04Sensor * sensor = new HCSR04Sensor();
sensor->setTrigger(HCSR04_TRIGGER);
sensor->setEcho(HCSR04_ECHO);
SonarSensor * sensor = new SonarSensor();
sensor->setEcho(SONAR_ECHO);
sensor->setIterations(SONAR_ITERATIONS);
sensor->setMaxDistance(SONAR_MAX_DISTANCE);
sensor->setTrigger(SONAR_TRIGGER);
_sensors.push_back(sensor);
}
#endif
@ -487,6 +512,20 @@ void _sensorLoad() {
}
#endif
#if NTC_SUPPORT
{
NTCSensor * sensor = new NTCSensor();
sensor->setSamples(NTC_SAMPLES);
sensor->setDelay(NTC_DELAY);
sensor->setUpstreamResistor(NTC_R_UP);
sensor->setDownstreamResistor(NTC_R_DOWN);
sensor->setBeta(NTC_BETA);
sensor->setR0(NTC_R0);
sensor->setT0(NTC_T0);
_sensors.push_back(sensor);
}
#endif
#if SENSEAIR_SUPPORT
{
SenseAirSensor * sensor = new SenseAirSensor();
@ -499,8 +538,12 @@ void _sensorLoad() {
#if PMSX003_SUPPORT
{
PMSX003Sensor * sensor = new PMSX003Sensor();
sensor->setRX(PMS_RX_PIN);
sensor->setTX(PMS_TX_PIN);
#if PMS_USE_SOFT
sensor->setRX(PMS_RX_PIN);
sensor->setTX(PMS_TX_PIN);
#else
sensor->setSerial(& PMS_HW_PORT);
#endif
sensor->setType(PMS_TYPE);
_sensors.push_back(sensor);
}
@ -554,8 +597,17 @@ void _sensorLoad() {
}
void _sensorCallback(unsigned char i, unsigned char type, const char * payload) {
DEBUG_MSG_P(PSTR("[SENSOR] Sensor #%u callback, type %u, payload: '%s'\n"), i, type, payload);
void _sensorCallback(unsigned char i, unsigned char type, double value) {
DEBUG_MSG_P(PSTR("[SENSOR] Sensor #%u callback, type %u, payload: '%s'\n"), i, type, String(value).c_str());
for (unsigned char k=0; k<_magnitudes.size(); k++) {
if ((_sensors[i] == _magnitudes[k].sensor) && (type == _magnitudes[k].type)) {
_sensorReport(k, value);
return;
}
}
}
void _sensorInit() {
@ -592,7 +644,7 @@ void _sensorInit() {
new_magnitude.min_change = 0;
if (type == MAGNITUDE_DIGITAL) {
new_magnitude.filter = new MaxFilter();
} else if (type == MAGNITUDE_EVENTS) {
} else if (type == MAGNITUDE_COUNT || type == MAGNITUDE_GEIGER_CPM|| type == MAGNITUDE_GEIGER_SIEVERT) { // For geiger counting moving average filter is the most appropriate if needed at all.
new_magnitude.filter = new MovingAverageFilter();
} else {
new_magnitude.filter = new MedianFilter();
@ -607,8 +659,8 @@ void _sensorInit() {
}
// Hook callback
_sensors[i]->onEvent([i](unsigned char type, const char * payload) {
_sensorCallback(i, type, payload);
_sensors[i]->onEvent([i](unsigned char type, double value) {
_sensorCallback(i, type, value);
});
// Custom initializations
@ -631,13 +683,13 @@ void _sensorInit() {
double value;
value = getSetting("pwrRatioC", 0).toFloat();
value = getSetting("pwrRatioC", HLW8012_CURRENT_RATIO).toFloat();
if (value > 0) sensor->setCurrentRatio(value);
value = getSetting("pwrRatioV", 0).toFloat();
value = getSetting("pwrRatioV", HLW8012_VOLTAGE_RATIO).toFloat();
if (value > 0) sensor->setVoltageRatio(value);
value = getSetting("pwrRatioP", 0).toFloat();
value = getSetting("pwrRatioP", HLW8012_POWER_RATIO).toFloat();
if (value > 0) sensor->setPowerRatio(value);
}
@ -691,7 +743,7 @@ void _sensorConfigure() {
double value;
EmonAnalogSensor * sensor = (EmonAnalogSensor *) _sensors[i];
if (value = getSetting("pwrExpectedP", 0).toInt()) {
if ((value = getSetting("pwrExpectedP", 0).toInt())) {
sensor->expectedPower(0, value);
setSetting("pwrRatioC", sensor->getCurrentRatio(0));
}
@ -778,17 +830,17 @@ void _sensorConfigure() {
double value;
CSE7766Sensor * sensor = (CSE7766Sensor *) _sensors[i];
if (value = getSetting("pwrExpectedC", 0).toFloat()) {
if ((value = getSetting("pwrExpectedC", 0).toFloat())) {
sensor->expectedCurrent(value);
setSetting("pwrRatioC", sensor->getCurrentRatio());
}
if (value = getSetting("pwrExpectedV", 0).toInt()) {
if ((value = getSetting("pwrExpectedV", 0).toInt())) {
sensor->expectedVoltage(value);
setSetting("pwrRatioV", sensor->getVoltageRatio());
}
if (value = getSetting("pwrExpectedP", 0).toInt()) {
if ((value = getSetting("pwrExpectedP", 0).toInt())) {
sensor->expectedPower(value);
setSetting("pwrRatioP", sensor->getPowerRatio());
}
@ -826,6 +878,72 @@ void _sensorConfigure() {
}
void _sensorReport(unsigned char index, double value) {
sensor_magnitude_t magnitude = _magnitudes[index];
unsigned char decimals = _magnitudeDecimals(magnitude.type);
char buffer[10];
dtostrf(value, 1-sizeof(buffer), decimals, buffer);
#if BROKER_SUPPORT
brokerPublish(magnitudeTopic(magnitude.type).c_str(), magnitude.local, buffer);
#endif
#if MQTT_SUPPORT
mqttSend(magnitudeTopicIndex(index).c_str(), buffer);
#if SENSOR_PUBLISH_ADDRESSES
char topic[32];
snprintf(topic, sizeof(topic), "%s/%s", SENSOR_ADDRESS_TOPIC, magnitudeTopic(magnitude.type).c_str());
if (SENSOR_USE_INDEX || (_counts[magnitude.type] > 1)) {
mqttSend(topic, magnitude.global, magnitude.sensor->address(magnitude.local).c_str());
} else {
mqttSend(topic, magnitude.sensor->address(magnitude.local).c_str());
}
#endif // SENSOR_PUBLISH_ADDRESSES
#endif // MQTT_SUPPORT
#if INFLUXDB_SUPPORT
if (SENSOR_USE_INDEX || (_counts[magnitude.type] > 1)) {
idbSend(magnitudeTopic(magnitude.type).c_str(), magnitude.global, buffer);
} else {
idbSend(magnitudeTopic(magnitude.type).c_str(), buffer);
}
#endif // INFLUXDB_SUPPORT
#if THINGSPEAK_SUPPORT
tspkEnqueueMeasurement(index, buffer);
#endif
#if DOMOTICZ_SUPPORT
{
char key[15];
snprintf_P(key, sizeof(key), PSTR("dczMagnitude%d"), index);
if (magnitude.type == MAGNITUDE_HUMIDITY) {
int status;
if (value > 70) {
status = HUMIDITY_WET;
} else if (value > 45) {
status = HUMIDITY_COMFORTABLE;
} else if (value > 30) {
status = HUMIDITY_NORMAL;
} else {
status = HUMIDITY_DRY;
}
char status_buf[5];
itoa(status, status_buf, 10);
domoticzSend(key, buffer, status_buf);
} else {
domoticzSend(key, 0, buffer);
}
}
#endif // DOMOTICZ_SUPPORT
}
// -----------------------------------------------------------------------------
// Public
// -----------------------------------------------------------------------------
@ -914,19 +1032,20 @@ void sensorSetup() {
// Configure stored values
_sensorConfigure();
// Websockets
#if WEB_SUPPORT
// Websockets
wsOnSendRegister(_sensorWebSocketStart);
wsOnReceiveRegister(_sensorWebSocketOnReceive);
wsOnSendRegister(_sensorWebSocketSendData);
wsOnAfterParseRegister(_sensorConfigure);
#endif
// API
// API
#if API_SUPPORT
_sensorAPISetup();
#endif
// Terminal
#if TERMINAL_SUPPORT
_sensorInitCommands();
#endif
@ -962,7 +1081,6 @@ void sensorLoop() {
double current;
double filtered;
char buffer[64];
// Pre-read hook
_sensorPre();
@ -998,19 +1116,18 @@ void sensorLoop() {
magnitude.filter->add(current);
// Special case
if (magnitude.type == MAGNITUDE_EVENTS) {
if (magnitude.type == MAGNITUDE_COUNT) {
current = magnitude.filter->result();
}
current = _magnitudeProcess(magnitude.type, current);
_magnitudes[i].current = current;
unsigned char decimals = _magnitudeDecimals(magnitude.type);
// Debug
#if SENSOR_DEBUG
{
dtostrf(current, 1-sizeof(buffer), decimals, buffer);
char buffer[64];
dtostrf(current, 1-sizeof(buffer), _magnitudeDecimals(magnitude.type), buffer);
DEBUG_MSG_P(PSTR("[SENSOR] %s - %s: %s%s\n"),
magnitude.sensor->slot(magnitude.local).c_str(),
magnitudeTopic(magnitude.type).c_str(),
@ -1032,63 +1149,8 @@ void sensorLoop() {
if (fabs(filtered - magnitude.reported) >= magnitude.min_change) {
_magnitudes[i].reported = filtered;
dtostrf(filtered, 1-sizeof(buffer), decimals, buffer);
#if BROKER_SUPPORT
brokerPublish(magnitudeTopic(magnitude.type).c_str(), magnitude.local, buffer);
#endif
#if MQTT_SUPPORT
mqttSend(magnitudeTopicIndex(i).c_str(), buffer);
#if SENSOR_PUBLISH_ADDRESSES
char topic[32];
snprintf(topic, sizeof(topic), "%s/%s", SENSOR_ADDRESS_TOPIC, magnitudeTopic(magnitude.type).c_str());
if (SENSOR_USE_INDEX || (_counts[magnitude.type] > 1)) {
mqttSend(topic, magnitude.global, magnitude.sensor->address(magnitude.local).c_str());
} else {
mqttSend(topic, magnitude.sensor->address(magnitude.local).c_str());
}
#endif // SENSOR_PUBLISH_ADDRESSES
#endif // MQTT_SUPPORT
#if INFLUXDB_SUPPORT
if (SENSOR_USE_INDEX || (_counts[magnitude.type] > 1)) {
idbSend(magnitudeTopic(magnitude.type).c_str(), magnitude.global, buffer);
} else {
idbSend(magnitudeTopic(magnitude.type).c_str(), buffer);
}
#endif // INFLUXDB_SUPPORT
#if THINGSPEAK_SUPPORT
tspkEnqueueMeasurement(i, buffer);
#endif
#if DOMOTICZ_SUPPORT
{
char key[15];
snprintf_P(key, sizeof(key), PSTR("dczMagnitude%d"), i);
if (magnitude.type == MAGNITUDE_HUMIDITY) {
int status;
if (filtered > 70) {
status = HUMIDITY_WET;
} else if (filtered > 45) {
status = HUMIDITY_COMFORTABLE;
} else if (filtered > 30) {
status = HUMIDITY_NORMAL;
} else {
status = HUMIDITY_DRY;
}
char status_buf[5];
itoa(status, status_buf, 10);
domoticzSend(key, buffer, status_buf);
} else {
domoticzSend(key, 0, buffer);
}
}
#endif // DOMOTICZ_SUPPORT
_sensorReport(i, filtered);
} // if (fabs(filtered - magnitude.reported) >= magnitude.min_change)
} // if (report_count == 0)


+ 34
- 2
code/espurna/sensors/AnalogSensor.h View File

@ -3,7 +3,7 @@
// Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && ANALOG_SUPPORT
#if SENSOR_SUPPORT && (ANALOG_SUPPORT || NTC_SUPPORT)
#pragma once
@ -27,6 +27,24 @@ class AnalogSensor : public BaseSensor {
_sensor_id = SENSOR_ANALOG_ID;
}
void setSamples(unsigned int samples) {
if (_samples > 0) _samples = samples;
}
void setDelay(unsigned long micros) {
_micros = micros;
}
// ---------------------------------------------------------------------
unsigned int getSamples() {
return _samples;
}
unsigned long getDelay() {
return _micros;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
@ -60,10 +78,24 @@ class AnalogSensor : public BaseSensor {
// Current value for slot # index
double value(unsigned char index) {
if (index == 0) return analogRead(0);
if (index == 0) return _read();
return 0;
}
protected:
unsigned int _read() {
if (1 == _samples) return analogRead(0);
unsigned long sum = 0;
for (unsigned int i=0; i<_samples; i++) {
if (i>0) delayMicroseconds(_micros);
sum += analogRead(0);
}
return sum / _samples;
}
unsigned int _samples = 1;
unsigned long _micros = 0;
};


+ 6
- 6
code/espurna/sensors/BaseSensor.h View File

@ -21,7 +21,7 @@
#define SENSOR_ERROR_CALIBRATION 8 // Calibration error or Not calibrated
#define SENSOR_ERROR_OTHER 99 // Any other error
typedef std::function<void(unsigned char, const char *)> TSensorCallback;
typedef std::function<void(unsigned char, double)> TSensorCallback;
class BaseSensor {
@ -46,19 +46,19 @@ class BaseSensor {
virtual void post() {}
// Descriptive name of the sensor
virtual String description() {}
virtual String description() = 0;
// Address of the sensor (it could be the GPIO or I2C address)
virtual String address(unsigned char index) {}
virtual String address(unsigned char index) = 0;
// Descriptive name of the slot # index
virtual String slot(unsigned char index) {};
virtual String slot(unsigned char index) = 0;
// Type for slot # index
virtual unsigned char type(unsigned char index) {}
virtual unsigned char type(unsigned char index) = 0;
// Current value for slot # index
virtual double value(unsigned char index) {}
virtual double value(unsigned char index) = 0;
// Retrieve current instance configuration
virtual void getConfig(JsonObject& root) {};


+ 1
- 1
code/espurna/sensors/DHTSensor.h View File

@ -221,7 +221,7 @@ class DHTSensor : public BaseSensor {
}
unsigned long _signal(int usTimeOut, bool state) {
unsigned long _signal(unsigned long usTimeOut, bool state) {
unsigned long uSec = 1;
while (digitalRead(_gpio) == state) {
if (++uSec > usTimeOut) return 0;


+ 18
- 11
code/espurna/sensors/EmonSensor.h View File

@ -10,9 +10,11 @@
#undef I2C_SUPPORT
#define I2C_SUPPORT 1 // Explicitly request I2C support.
#include "Arduino.h"
#include "I2CSensor.h"
extern "C" {
#include "libs/fs_math.h"
}
class EmonSensor : public I2CSensor {
@ -43,6 +45,7 @@ class EmonSensor : public I2CSensor {
if (actual == 0) return;
if (expected == actual) return;
_current_ratio[channel] = _current_ratio[channel] * ((double) expected / (double) actual);
calculateFactors(channel);
_dirty = true;
}
@ -70,6 +73,7 @@ class EmonSensor : public I2CSensor {
if (channel >= _channels) return;
if (_current_ratio[channel] == current_ratio) return;
_current_ratio[channel] = current_ratio;
calculateFactors(channel);
_dirty = true;
}
@ -105,8 +109,7 @@ class EmonSensor : public I2CSensor {
for (unsigned char i=0; i<_channels; i++) {
_energy[i] = _current[i] = 0;
_pivot[i] = _adc_counts >> 1;
_current_factor[i] = _current_ratio[i] * _reference / _adc_counts;
_multiplier[i] = calculateMultiplier(_current_factor[i]);
calculateFactors(i);
}
#if SENSOR_DEBUG
@ -143,20 +146,24 @@ class EmonSensor : public I2CSensor {
#endif
}
virtual unsigned int readADC(unsigned char channel) {}
virtual unsigned int readADC(unsigned char channel) = 0;
void calculateFactors(unsigned char channel) {
_current_factor[channel] = _current_ratio[channel] * _reference / _adc_counts;
unsigned int calculateMultiplier(double current_factor) {
unsigned int s = 1;
unsigned int i = 1;
unsigned int m = s * i;
unsigned int multiplier;
while (m * current_factor < 1) {
unsigned int m = 1;
unsigned int multiplier = 1;
while (m * _current_factor[channel] < 1) {
multiplier = m;
i = (i == 1) ? 2 : (i == 2) ? 5 : 1;
if (i == 1) s *= 10;
m = s * i;
}
return multiplier;
_multiplier[channel] = multiplier;
}
double read(unsigned char channel) {
@ -192,7 +199,7 @@ class EmonSensor : public I2CSensor {
}
// Calculate current
double rms = _samples > 0 ? sqrt(sum / _samples) : 0;
double rms = _samples > 0 ? fs_sqrt(sum / _samples) : 0;
double current = _current_factor[channel] * rms;
current = (double) (int(current * _multiplier[channel]) - 1) / _multiplier[channel];
if (current < 0) current = 0;
@ -206,7 +213,7 @@ class EmonSensor : public I2CSensor {
DEBUG_MSG("[EMON] Min value: %d\n", min);
DEBUG_MSG("[EMON] Midpoint value: %d\n", int(_pivot[channel]));
DEBUG_MSG("[EMON] RMS value: %d\n", int(rms));
DEBUG_MSG("[EMON] Current (mA): %d\n", int(current));
DEBUG_MSG("[EMON] Current (mA): %d\n", int(1000 * current));
#endif
// Check timing


+ 29
- 12
code/espurna/sensors/EventSensor.h View File

@ -33,12 +33,16 @@ class EventSensor : public BaseSensor {
_gpio = gpio;
}
void setMode(unsigned char mode) {
_mode = mode;
void setTrigger(bool trigger) {
_trigger = trigger;
}
void setInterruptMode(unsigned char mode) {
_interrupt_mode = mode;
void setPinMode(unsigned char pin_mode) {
_pin_mode = pin_mode;
}
void setInterruptMode(unsigned char interrupt_mode) {
_interrupt_mode = interrupt_mode;
}
void setDebounceTime(unsigned long debounce) {
@ -51,8 +55,12 @@ class EventSensor : public BaseSensor {
return _gpio;
}
unsigned char getMode() {
return _mode;
bool getTrigger() {
return _trigger;
}
unsigned char getPinMode() {
return _pin_mode;
}
unsigned char getInterruptMode() {
@ -70,8 +78,9 @@ class EventSensor : public BaseSensor {
// Initialization method, must be idempotent
// Defined outside the class body
void begin() {
pinMode(_gpio, _mode);
pinMode(_gpio, _pin_mode);
_enableInterrupts(true);
_count = _trigger ? 2 : 1;
_ready = true;
}
@ -94,7 +103,8 @@ class EventSensor : public BaseSensor {
// Type for slot # index
unsigned char type(unsigned char index) {
if (index == 0) return MAGNITUDE_EVENTS;
if (index == 0) return MAGNITUDE_COUNT;
if (index == 1) return MAGNITUDE_EVENT;
return MAGNITUDE_NONE;
}
@ -113,8 +123,14 @@ class EventSensor : public BaseSensor {
(void) gpio;
static unsigned long last = 0;
if (millis() - last > _debounce) {
_events = _events + 1;
last = millis();
_events = _events + 1;
if (_trigger) {
if (_callback) _callback(MAGNITUDE_EVENT, digitalRead(gpio));
}
}
}
@ -148,9 +164,10 @@ class EventSensor : public BaseSensor {
volatile unsigned long _events = 0;
unsigned long _debounce = EVENTS_DEBOUNCE;
unsigned char _gpio;
unsigned char _mode;
unsigned char _interrupt_mode;
unsigned char _gpio = GPIO_NONE;
bool _trigger = false;
unsigned char _pin_mode = INPUT;
unsigned char _interrupt_mode = RISING;
};


+ 298
- 0
code/espurna/sensors/GeigerSensor.h View File

@ -0,0 +1,298 @@
// -----------------------------------------------------------------------------
// Geiger Sensor based on Event Counter Sensor
// Copyright (C) 2018 by Sven Kopetzki <skopetzki at web dot de>
// Documentation: https://github.com/Trickx/espurna/wiki/Geiger-counter
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && GEIGER_SUPPORT
#pragma once
#include "Arduino.h"
#include "BaseSensor.h"
class GeigerSensor : public BaseSensor {
public:
// ---------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------
GeigerSensor() : BaseSensor() {
_count = 2;
_sensor_id = SENSOR_GEIGER_ID;
}
~GeigerSensor() {
_enableInterrupts(false);
}
// ---------------------------------------------------------------------
void setGPIO(unsigned char gpio) {
_gpio = gpio;
}
void setMode(unsigned char mode) {
_mode = mode;
}
void setInterruptMode(unsigned char mode) {
_interrupt_mode = mode;
}
void setDebounceTime(unsigned long debounce) {
_debounce = debounce;
}
void setCPM2SievertFactor(unsigned int cpm2sievert) {
_cpm2sievert = cpm2sievert;
}
// ---------------------------------------------------------------------
unsigned char getGPIO() {
return _gpio;
}
unsigned char getMode() {
return _mode;
}
unsigned char getInterruptMode() {
return _interrupt_mode;
}
unsigned long getDebounceTime() {
return _debounce;
}
unsigned long getCPM2SievertFactor() {
return _cpm2sievert;
}
// ---------------------------------------------------------------------
// Sensors API
// ---------------------------------------------------------------------
// Initialization method, must be idempotent
// Defined outside the class body
void begin() {
pinMode(_gpio, _mode);
_enableInterrupts(true);
_ready = true;
}
// Descriptive name of the sensor
String description() {
char buffer[20];
snprintf(buffer, sizeof(buffer), "µSv/h @ GPIO%d", _gpio);
return String(buffer);
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
char buffer[30];
unsigned char i=0;
#if GEIGER_REPORT_CPM
if (index == i++) {
snprintf(buffer, sizeof(buffer), "Counts per Minute @ GPIO%d", _gpio);
return String(buffer);
}
#endif
#if GEIGER_REPORT_SIEVERTS
if (index == i++) {
snprintf(buffer, sizeof(buffer), "CPM / %d = µSv/h", _cpm2sievert);
return String(buffer);
}
#endif
snprintf(buffer, sizeof(buffer), "Events @ GPIO%d", _gpio);
return String(buffer);
};
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char index) {
return String(_gpio);
}
// Type for slot # index
unsigned char type(unsigned char index) {
unsigned char i=0;
#if GEIGER_REPORT_CPM
if (index == i++) return MAGNITUDE_GEIGER_CPM;
#endif
#if GEIGER_REPORT_SIEVERTS
if (index == i++) return MAGNITUDE_GEIGER_SIEVERT;
#endif
return MAGNITUDE_NONE;
}
// Current value for slot # index
double value(unsigned char index) {
unsigned char i=0;
#if GEIGER_REPORT_CPM
if (index == i++) {
unsigned long _period_begin = _lastreport_cpm;
_lastreport_cpm = millis();
double value = _events * 60000;
value = value / (_lastreport_cpm-_period_begin);
#if SENSOR_DEBUG
char data[128]; char buffer[10];
dtostrf(value, 1-sizeof(buffer), 4, buffer);
snprintf(data, sizeof(data), "Ticks: %u | Interval: %u | CPM: %s", _ticks, (_lastreport_cpm-_period_begin), buffer);
DEBUG_MSG("[GEIGER] %s\n", data);
#endif
_events = 0;
return value;
}
#endif
#if GEIGER_REPORT_SIEVERTS
if (index == i++) {
unsigned long _period_begin = _lastreport_sv;
_lastreport_sv = millis();
double value = _ticks * 60000 / _cpm2sievert;
value = value / (_lastreport_sv-_period_begin);
#if SENSOR_DEBUG
char data[128]; char buffer[10];
dtostrf(value, 1-sizeof(buffer), 4, buffer);
snprintf(data, sizeof(data), "Ticks: %u | Interval: %u | µSievert: %s", _ticks, (_lastreport_sv-_period_begin), buffer);
DEBUG_MSG("[GEIGER] %s\n", data);
#endif
_ticks = 0;
return value;
}
#endif
return 0;
}
// Handle interrupt calls
void handleInterrupt(unsigned char gpio) {
(void) gpio;
static unsigned long last = 0;
if (millis() - last > _debounce) {
_events = _events + 1;
_ticks = _ticks + 1;
last = millis();
}
}
protected:
// ---------------------------------------------------------------------
// Interrupt management
// ---------------------------------------------------------------------
void _attach(GeigerSensor * instance, unsigned char gpio, unsigned char mode);
void _detach(unsigned char gpio);
void _enableInterrupts(bool value) {
static unsigned char _interrupt_gpio = GPIO_NONE;
if (value) {
if (_interrupt_gpio != GPIO_NONE) _detach(_interrupt_gpio);
_attach(this, _gpio, _interrupt_mode);
_interrupt_gpio = _gpio;
} else if (_interrupt_gpio != GPIO_NONE) {
_detach(_interrupt_gpio);
_interrupt_gpio = GPIO_NONE;
}
}
// ---------------------------------------------------------------------
// Protected
// ---------------------------------------------------------------------
volatile unsigned long _events = 0;
volatile unsigned long _ticks = 0;
unsigned long _debounce = GEIGER_DEBOUNCE;
unsigned int _cpm2sievert = GEIGER_CPM2SIEVERT;
unsigned char _gpio;
unsigned char _mode;
unsigned char _interrupt_mode;
// Added for µSievert calculations
unsigned long _lastreport_cpm = millis();
unsigned long _lastreport_sv = _lastreport_cpm;
};
// -----------------------------------------------------------------------------
// Interrupt helpers
// -----------------------------------------------------------------------------
GeigerSensor * _geiger_sensor_instance[10] = {NULL};
void ICACHE_RAM_ATTR _geiger_sensor_isr(unsigned char gpio) {
unsigned char index = gpio > 5 ? gpio-6 : gpio;
if (_geiger_sensor_instance[index]) {
_geiger_sensor_instance[index]->handleInterrupt(gpio);
}
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_0() {
_geiger_sensor_isr(0);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_1() {
_geiger_sensor_isr(1);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_2() {
_geiger_sensor_isr(2);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_3() {
_geiger_sensor_isr(3);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_4() {
_geiger_sensor_isr(4);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_5() {
_geiger_sensor_isr(5);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_12() {
_geiger_sensor_isr(12);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_13() {
_geiger_sensor_isr(13);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_14() {
_geiger_sensor_isr(14);
}
void ICACHE_RAM_ATTR _geiger_sensor_isr_15() {
_geiger_sensor_isr(15);
}
static void (*_geiger_sensor_isr_list[10])() = {
_geiger_sensor_isr_0, _geiger_sensor_isr_1, _geiger_sensor_isr_2,
_geiger_sensor_isr_3, _geiger_sensor_isr_4, _geiger_sensor_isr_5,
_geiger_sensor_isr_12, _geiger_sensor_isr_13, _geiger_sensor_isr_14,
_geiger_sensor_isr_15
};
void GeigerSensor::_attach(GeigerSensor * instance, unsigned char gpio, unsigned char mode) {
if (!gpioValid(gpio)) return;
_detach(gpio);
unsigned char index = gpio > 5 ? gpio-6 : gpio;
_geiger_sensor_instance[index] = instance;
attachInterrupt(gpio, _geiger_sensor_isr_list[index], mode);
#if SENSOR_DEBUG
DEBUG_MSG_P(PSTR("[GEIGER] GPIO%d interrupt attached to %s\n"), gpio, instance->description().c_str());
#endif
}
void GeigerSensor::_detach(unsigned char gpio) {
if (!gpioValid(gpio)) return;
unsigned char index = gpio > 5 ? gpio-6 : gpio;
if (_geiger_sensor_instance[index]) {
detachInterrupt(gpio);
#if SENSOR_DEBUG
DEBUG_MSG_P(PSTR("[GEIGER] GPIO%d interrupt detached from %s\n"), gpio, _geiger_sensor_instance[index]->description().c_str());
#endif
_geiger_sensor_instance[index] = NULL;
}
}
#endif // SENSOR_SUPPORT && GEIGER_SUPPORT

+ 2
- 2
code/espurna/sensors/HLW8012Sensor.h View File

@ -234,13 +234,13 @@ class HLW8012Sensor : public BaseSensor {
if (_interrupt_cf != _cf) {
if (_interrupt_cf != GPIO_NONE) _detach(_interrupt_cf);
_attach(this, _cf, CHANGE);
_attach(this, _cf, HLW8012_INTERRUPT_ON);
_interrupt_cf = _cf;
}
if (_interrupt_cf1 != _cf1) {
if (_interrupt_cf1 != GPIO_NONE) _detach(_interrupt_cf1);
_attach(this, _cf1, CHANGE);
_attach(this, _cf1, HLW8012_INTERRUPT_ON);
_interrupt_cf1 = _cf1;
}


+ 125
- 0
code/espurna/sensors/NTCSensor.h View File

@ -0,0 +1,125 @@
// -----------------------------------------------------------------------------
// NTC Sensor (maps to a NTCSensor)
// Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && NTC_SUPPORT
#pragma once
// Set ADC to TOUT pin
#undef ADC_MODE_VALUE
#define ADC_MODE_VALUE ADC_TOUT
#include "Arduino.h"
#include "AnalogSensor.h"
extern "C" {
#include "../libs/fs_math.h"
}
class NTCSensor : public AnalogSensor {
public:
// ---------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------
NTCSensor(): AnalogSensor() {
_count = 1;
_sensor_id = SENSOR_NTC_ID;
}
void setBeta(unsigned long beta) {
if (beta > 0) _beta = beta;
}
void setUpstreamResistor(unsigned long resistance) {
_resistance_up = resistance;
if (_resistance_up > 0) _resistance_down = 0;
}
void setDownstreamResistor(unsigned long resistance) {
_resistance_down = resistance;
if (_resistance_down > 0) _resistance_up = 0;
}
void setR0(unsigned long resistance) {
if (resistance > 0) _R0 = resistance;
}
void setT0(double temperature) {
if (temperature > 0) _T0 = temperature;
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
// Descriptive name of the sensor
String description() {
return String("NTC @ TOUT");
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
return description();
};
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char index) {
return String("0");
}
// Type for slot # index
unsigned char type(unsigned char index) {
if (index == 0) return MAGNITUDE_TEMPERATURE;
return MAGNITUDE_NONE;
}
// Current value for slot # index
double value(unsigned char index) {
double temperature = 0;
if (index == 0) {
// sampled reading
double read = _read();
// Ru = (1023/c - 1) * Rd
double resistance;
double alpha = (1023.0 / read) - 1;
if (_resistance_down > 0) {
resistance = _resistance_down * alpha;
} else if (0 == alpha) {
resistance = _R0;
} else {
resistance = _resistance_up / alpha;
}
// 1/T = 1/T0 + 1/B * ln(R/R0)
temperature = fs_log(resistance / _R0);
temperature = (1.0 / _T0) + (temperature / _beta);
temperature = 1.0 / temperature - 273.15;
}
return temperature;
}
protected:
unsigned long _beta = NTC_BETA;
unsigned long _resistance_up = NTC_R_UP;
unsigned long _resistance_down = NTC_R_DOWN;
unsigned long _R0 = NTC_R0;
double _T0 = NTC_T0;
};
#endif // SENSOR_SUPPORT && NTC_SUPPORT

+ 38
- 11
code/espurna/sensors/PMSX003Sensor.h View File

@ -12,7 +12,12 @@
#include "Arduino.h"
#include "BaseSensor.h"
#if PMS_USE_SOFT
#include <SoftwareSerial.h>
#endif
// Generic data
#define PMS_BAUD_RATE 9600
// Type of sensor
#define PMS_TYPE_X003 0
@ -46,7 +51,7 @@ const static struct {
class PMSX003 {
protected:
SoftwareSerial *_serial = NULL; // Should initialized by child class
Stream *_serial = NULL; // Should initialized by child class
public:
@ -175,7 +180,13 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
_dirty = true;
}
// Should call setType after constrcutor immediately to enable corresponding slot count
void setSerial(HardwareSerial * serial) {
_soft = false;
_serial = serial;
_dirty = true;
}
// Should call setType after constructor immediately to enable corresponding slot count
void setType(unsigned char type) {
_type = type;
_count = pms_specs[_type].slot_count;
@ -204,30 +215,45 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
if (!_dirty) return;
if (_serial) delete _serial;
if (_soft) {
if (_serial) delete _serial;
_serial = new SoftwareSerial(_pin_rx, _pin_tx, false, 64);
static_cast<SoftwareSerial*>(_serial)->enableIntTx(false);
}
if (_soft) {
static_cast<SoftwareSerial*>(_serial)->begin(PMS_BAUD_RATE);
} else {
static_cast<HardwareSerial*>(_serial)->begin(PMS_BAUD_RATE);
}
_serial = new SoftwareSerial(_pin_rx, _pin_tx, false, 64);
_serial->enableIntTx(false);
_serial->begin(9600);
passiveMode();
_startTime = millis();
_ready = true;
_dirty = false;
}
// Descriptive name of the sensor
String description() {
char buffer[28];
snprintf(buffer, sizeof(buffer), "%s @ SwSerial(%u,%u)", pms_specs[_type].name, _pin_rx, _pin_tx);
if (_soft) {
snprintf(buffer, sizeof(buffer), "%s @ SwSerial(%u,%u)", pms_specs[_type].name, _pin_rx, _pin_tx);
} else {
snprintf(buffer, sizeof(buffer), "%s @ HwSerial", pms_specs[_type].name);
}
return String(buffer);
}
// Descriptive name of the slot # index
String slot(unsigned char index) {
char buffer[36] = {0};
snprintf(buffer, sizeof(buffer), "%d @ %s @ SwSerial(%u,%u)", int(index + 1), pms_specs[_type].name, _pin_rx, _pin_tx);
if (_soft) {
snprintf(buffer, sizeof(buffer), "%d @ %s @ SwSerial(%u,%u)", int(index + 1), pms_specs[_type].name, _pin_rx, _pin_tx);
} else {
snprintf(buffer, sizeof(buffer), "%d @ %s @ HwSerial", int(index + 1), pms_specs[_type].name);
}
return String(buffer);
}
@ -301,7 +327,7 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
#endif
requestRead();
}
// Current value for slot # index
@ -310,6 +336,7 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
}
protected:
bool _soft = true;
unsigned int _pin_rx;
unsigned int _pin_tx;
unsigned long _startTime;
@ -322,4 +349,4 @@ class PMSX003Sensor : public BaseSensor, PMSX003 {
};
#endif // SENSOR_SUPPORT && PMS_SUPPORT
#endif // SENSOR_SUPPORT && PMSX003_SUPPORT

+ 12
- 6
code/espurna/sensors/PZEM004TSensor.h View File

@ -84,7 +84,11 @@ class PZEM004TSensor : public BaseSensor {
// Descriptive name of the sensor
String description() {
char buffer[28];
snprintf(buffer, sizeof(buffer), "PZEM004T @ SwSerial(%u,%u)", _pin_rx, _pin_tx);
if (_serial) {
snprintf(buffer, sizeof(buffer), "PZEM004T @ HwSerial");
} else {
snprintf(buffer, sizeof(buffer), "PZEM004T @ SwSerial(%u,%u)", _pin_rx, _pin_tx);
}
return String(buffer);
}
@ -109,11 +113,13 @@ class PZEM004TSensor : public BaseSensor {
// Current value for slot # index
double value(unsigned char index) {
if (index == 0) return _pzem->current(_ip);
if (index == 1) return _pzem->voltage(_ip);
if (index == 2) return _pzem->power(_ip);
if (index == 3) return _pzem->energy(_ip);
return 0;
double response = 0;
if (index == 0) response = _pzem->current(_ip);
if (index == 1) response = _pzem->voltage(_ip);
if (index == 2) response = _pzem->power(_ip);
if (index == 3) response = _pzem->energy(_ip) * 3600;
if (response < 0) response = 0;
return response;
}
protected:


code/espurna/sensors/HCSR04Sensor.h → code/espurna/sensors/SonarSensor.h View File

@ -1,16 +1,18 @@
// -----------------------------------------------------------------------------
// HC-SR04 Ultrasonic sensor
// HC-SR04, SRF05, SRF06, DYP-ME007, JSN-SR04T & Parallax PING)))
// Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
// Enhancements by Rui Marinho
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && HCSR04_SUPPORT
#if SENSOR_SUPPORT && SONAR_SUPPORT
#pragma once
#include "Arduino.h"
#include "BaseSensor.h"
#include "NewPing.h"
class HCSR04Sensor : public BaseSensor {
class SonarSensor : public BaseSensor {
public:
@ -18,17 +20,30 @@ class HCSR04Sensor : public BaseSensor {
// Public
// ---------------------------------------------------------------------
HCSR04Sensor(): BaseSensor() {
SonarSensor(): BaseSensor() {
_count = 1;
_sensor_id = SENSOR_HCSR04_ID;
_sensor_id = SENSOR_SONAR_ID;
}
// ---------------------------------------------------------------------
// Echo pin.
void setEcho(unsigned char echo) {
_echo = echo;
}
// Number of iterations to ping in order to filter out erroneous readings
// using a digital filter.
void setIterations(unsigned int iterations) {
_iterations = iterations;
}
// Max sensor distance in centimeters.
void setMaxDistance(unsigned int distance) {
_max_distance = distance;
}
// Trigger pin.
void setTrigger(unsigned char trigger) {
_trigger = trigger;
}
@ -43,22 +58,28 @@ class HCSR04Sensor : public BaseSensor {
return _trigger;
}
unsigned int getMaxDistance() {
return _max_distance;
}
unsigned int getIterations() {
return _iterations;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
// Initialization method, must be idempotent
void begin() {
pinMode(_echo, INPUT);
pinMode(_trigger, OUTPUT);
digitalWrite(_trigger, LOW);
_sonar = new NewPing(getTrigger(), getEcho(), getMaxDistance());
_ready = true;
}
// Descriptive name of the sensor
String description() {
char buffer[24];
snprintf(buffer, sizeof(buffer), "HCSR04 @ GPIO(%u, %u)", _trigger, _echo);
char buffer[23];
snprintf(buffer, sizeof(buffer), "Sonar @ GPIO(%u, %u)", _trigger, _echo);
return String(buffer);
}
@ -80,28 +101,11 @@ class HCSR04Sensor : public BaseSensor {
// Current value for slot # index
double value(unsigned char index) {
if (index == 0) {
// Trigger pulse
digitalWrite(_trigger, HIGH);
delayMicroseconds(10);
digitalWrite(_trigger, LOW);
// Wait for echo pulse low-high-low
while ( digitalRead(_echo) == 0 ) yield();
unsigned long start = micros();
while ( digitalRead(_echo) == 1 ) yield();
unsigned long travel_time = micros() - start;
// Assuming a speed of sound of 340m/s
// Dividing by 2 since it is a round trip
return 340.0 * (double) travel_time / 1000000.0 / 2;
if (index != 0) return 0;
if (getIterations() > 0) {
return NewPing::convert_cm(_sonar->ping_median(getIterations())) / 100.0;
}
return 0;
return _sonar->ping_cm() / 100.0;
}
@ -113,7 +117,10 @@ class HCSR04Sensor : public BaseSensor {
unsigned char _trigger;
unsigned char _echo;
unsigned int _max_distance;
unsigned int _iterations;
NewPing * _sonar = NULL;
};
#endif // SENSOR_SUPPORT && HCSR04_SUPPORT
#endif // SENSOR_SUPPORT && SONAR_SUPPORT

+ 4
- 1
code/espurna/sensors/V9261FSensor.h View File

@ -9,6 +9,9 @@
#include "Arduino.h"
#include "BaseSensor.h"
extern "C" {
#include "libs/fs_math.h"
}
#include <SoftwareSerial.h>
@ -203,7 +206,7 @@ class V9261FSensor : public BaseSensor {
if (_voltage < 0) _voltage = 0;
if (_current < 0) _current = 0;
_apparent = sqrt(_reactive * _reactive + _active * _active);
_apparent = fs_sqrt(_reactive * _reactive + _active * _active);
}


+ 67
- 45
code/espurna/settings.ino View File

@ -6,7 +6,7 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include <EEPROM.h>
#include <EEPROM_Rotate.h>
#include <vector>
#include "libs/EmbedisWrap.h"
#include <Stream.h>
@ -30,43 +30,46 @@ bool _settings_save = false;
unsigned long settingsSize() {
unsigned pos = SPI_FLASH_SEC_SIZE - 1;
while (size_t len = EEPROM.read(pos)) {
while (size_t len = EEPROMr.read(pos)) {
if (0xFF == len) break;
pos = pos - len - 2;
}
return SPI_FLASH_SEC_SIZE - pos;
return SPI_FLASH_SEC_SIZE - pos + EEPROM_DATA_END;
}
// -----------------------------------------------------------------------------
unsigned int _settingsKeyCount() {
unsigned int settingsKeyCount() {
unsigned count = 0;
unsigned pos = SPI_FLASH_SEC_SIZE - 1;
while (size_t len = EEPROM.read(pos)) {
while (size_t len = EEPROMr.read(pos)) {
if (0xFF == len) break;
pos = pos - len - 2;
len = EEPROM.read(pos);
len = EEPROMr.read(pos);
pos = pos - len - 2;
count ++;
}
return count;
}
String _settingsKeyName(unsigned int index) {
String settingsKeyName(unsigned int index) {
String s;
unsigned count = 0;
unsigned pos = SPI_FLASH_SEC_SIZE - 1;
while (size_t len = EEPROM.read(pos)) {
while (size_t len = EEPROMr.read(pos)) {
if (0xFF == len) break;
pos = pos - len - 2;
if (count == index) {
s.reserve(len);
for (unsigned char i = 0 ; i < len; i++) {
s += (char) EEPROM.read(pos + i + 1);
s += (char) EEPROMr.read(pos + i + 1);
}
break;
}
count++;
len = EEPROM.read(pos);
len = EEPROMr.read(pos);
pos = pos - len - 2;
}
@ -80,11 +83,11 @@ std::vector<String> _settingsKeys() {
std::vector<String> keys;
//unsigned int size = settingsKeyCount();
unsigned int size = _settingsKeyCount();
unsigned int size = settingsKeyCount();
for (unsigned int i=0; i<size; i++) {
//String key = settingsKeyName(i);
String key = _settingsKeyName(i);
String key = settingsKeyName(i);
bool inserted = false;
for (unsigned char j=0; j<keys.size(); j++) {
@ -151,32 +154,21 @@ void _settingsKeysCommand() {
DEBUG_MSG_P(PSTR("Current settings:\n"));
for (unsigned int i=0; i<keys.size(); i++) {
String value = getSetting(keys[i]);
DEBUG_MSG_P(PSTR("> %s => %s\n"), (keys[i]).c_str(), value.c_str());
DEBUG_MSG_P(PSTR("> %s => \"%s\"\n"), (keys[i]).c_str(), value.c_str());
}
unsigned long freeEEPROM = SPI_FLASH_SEC_SIZE - settingsSize();
DEBUG_MSG_P(PSTR("Number of keys: %d\n"), keys.size());
DEBUG_MSG_P(PSTR("Current EEPROM sector: %u\n"), EEPROMr.current());
DEBUG_MSG_P(PSTR("Free EEPROM: %d bytes (%d%%)\n"), freeEEPROM, 100 * freeEEPROM / SPI_FLASH_SEC_SIZE);
}
void _settingsFactoryResetCommand() {
for (unsigned int i = 0; i < SPI_FLASH_SEC_SIZE; i++) {
EEPROM.write(i, 0xFF);
}
EEPROM.commit();
}
void _settingsDumpCommand(bool ascii) {
for (unsigned int i = 0; i < SPI_FLASH_SEC_SIZE; i++) {
if (i % 16 == 0) DEBUG_MSG_P(PSTR("\n[%04X] "), i);
byte c = EEPROM.read(i);
if (ascii && 32 <= c && c <= 126) {
DEBUG_MSG_P(PSTR(" %c "), c);
} else {
DEBUG_MSG_P(PSTR("%02X "), c);
}
EEPROMr.write(i, 0xFF);
}
EEPROMr.commit();
}
void _settingsInitCommands() {
@ -194,13 +186,6 @@ void _settingsInitCommands() {
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("EEPROM.DUMP"), [](Embedis* e) {
bool ascii = false;
if (e->argc == 2) ascii = String(e->argv[1]).toInt() == 1;
_settingsDumpCommand(ascii);
DEBUG_MSG_P(PSTR("\n+OK\n"));
});
settingsRegisterCommand(F("ERASE.CONFIG"), [](Embedis* e) {
DEBUG_MSG_P(PSTR("+OK\n"));
resetReason(CUSTOM_RESET_TERMINAL);
@ -257,7 +242,7 @@ void _settingsInitCommands() {
settingsRegisterCommand(F("INFO"), [](Embedis* e) {
info();
wifiStatus();
wifiDebug();
//StreamString s;
//WiFi.printDiag(s);
//DEBUG_MSG(s.c_str());
@ -269,18 +254,40 @@ void _settingsInitCommands() {
DEBUG_MSG_P(PSTR("+OK\n"));
});
settingsRegisterCommand(F("RELOAD"), [](Embedis* e) {
wsReload();
settingsRegisterCommand(F("GET"), [](Embedis* e) {
if (e->argc < 2) {
DEBUG_MSG_P(PSTR("-ERROR: Wrong arguments\n"));
return;
}
for (unsigned char i = 1; i < e->argc; i++) {
String key = String(e->argv[i]);
String value;
if (!Embedis::get(key, value)) {
DEBUG_MSG_P(PSTR("> %s =>\n"), key.c_str());
continue;
}
DEBUG_MSG_P(PSTR("> %s => \"%s\"\n"), key.c_str(), value.c_str());
}
DEBUG_MSG_P(PSTR("+OK\n"));
});
#if WEB_SUPPORT
settingsRegisterCommand(F("RELOAD"), [](Embedis* e) {
wsReload();
DEBUG_MSG_P(PSTR("+OK\n"));
});
#endif
settingsRegisterCommand(F("RESET"), [](Embedis* e) {
DEBUG_MSG_P(PSTR("+OK\n"));
deferredReset(100, CUSTOM_RESET_TERMINAL);
});
settingsRegisterCommand(F("RESET.SAFE"), [](Embedis* e) {
EEPROM.write(EEPROM_CRASH_COUNTER, SYSTEM_CHECK_MAX);
EEPROMr.write(EEPROM_CRASH_COUNTER, SYSTEM_CHECK_MAX);
DEBUG_MSG_P(PSTR("+OK\n"));
deferredReset(100, CUSTOM_RESET_TERMINAL);
});
@ -358,6 +365,10 @@ void settingsInject(void *data, size_t len) {
_serial.inject((char *) data, len);
}
Stream & settingsSerial() {
return (Stream &) _serial;
}
size_t settingsMaxSize() {
size_t size = EEPROM_SIZE;
if (size > SPI_FLASH_SEC_SIZE) size = SPI_FLASH_SEC_SIZE;
@ -367,19 +378,30 @@ size_t settingsMaxSize() {
bool settingsRestoreJson(JsonObject& data) {
// Check this is an ESPurna configuration file (must have "app":"ESPURNA")
const char* app = data["app"];
if (strcmp(app, APP_NAME) != 0) return false;
if (!app || strcmp(app, APP_NAME) != 0) {
DEBUG_MSG_P(PSTR("[SETTING] Wrong or missing 'app' key\n"));
return false;
}
for (unsigned int i = EEPROM_DATA_END; i < SPI_FLASH_SEC_SIZE; i++) {
EEPROM.write(i, 0xFF);
// Clear settings
bool is_backup = data["backup"];
if (is_backup) {
for (unsigned int i = EEPROM_DATA_END; i < SPI_FLASH_SEC_SIZE; i++) {
EEPROMr.write(i, 0xFF);
}
}
// Dump settings to memory buffer
for (auto element : data) {
if (strcmp(element.key, "app") == 0) continue;
if (strcmp(element.key, "version") == 0) continue;
if (strcmp(element.key, "backup") == 0) continue;
setSetting(element.key, element.value.as<char*>());
}
// Persist to EEPROM
saveSettings();
DEBUG_MSG_P(PSTR("[SETTINGS] Settings restored successfully\n"));
@ -410,7 +432,7 @@ void settingsRegisterCommand(const String& name, void (*call)(Embedis*)) {
void settingsSetup() {
EEPROM.begin(SPI_FLASH_SEC_SIZE);
EEPROMr.begin(SPI_FLASH_SEC_SIZE);
_serial.callback([](uint8_t ch) {
#if TELNET_SUPPORT
@ -423,8 +445,8 @@ void settingsSetup() {
Embedis::dictionary( F("EEPROM"),
SPI_FLASH_SEC_SIZE,
[](size_t pos) -> char { return EEPROM.read(pos); },
[](size_t pos, char value) { EEPROM.write(pos, value); },
[](size_t pos) -> char { return EEPROMr.read(pos); },
[](size_t pos, char value) { EEPROMr.write(pos, value); },
#if SETTINGS_AUTOSAVE
[]() { _settings_save = true; }
#else
@ -448,7 +470,7 @@ void settingsSetup() {
void settingsLoop() {
if (_settings_save) {
EEPROM.commit();
EEPROMr.commit();
_settings_save = false;
}


+ 1
- 1
code/espurna/ssdp.ino View File

@ -46,7 +46,7 @@ void ssdpSetup() {
char response[strlen_P(_ssdp_template) + 100];
snprintf_P(response, sizeof(response), _ssdp_template,
WiFi.localIP().toString().c_str(), // ip
ip.toString().c_str(), // ip
webPort(), // port
SSDP_DEVICE_TYPE, // device type
getSetting("hostname").c_str(), // friendlyName


+ 3048
- 0
code/espurna/static/index.all.html.gz.h
File diff suppressed because it is too large
View File


+ 0
- 3248
code/espurna/static/index.html.gz.h
File diff suppressed because it is too large
View File


+ 2964
- 0
code/espurna/static/index.light.html.gz.h
File diff suppressed because it is too large
View File


+ 2563
- 0
code/espurna/static/index.rfbridge.html.gz.h
File diff suppressed because it is too large
View File


+ 4033
- 0
code/espurna/static/index.rfm69.html.gz.h
File diff suppressed because it is too large
View File


+ 2603
- 0
code/espurna/static/index.sensor.html.gz.h
File diff suppressed because it is too large
View File


+ 2519
- 0
code/espurna/static/index.small.html.gz.h
File diff suppressed because it is too large
View File


+ 5
- 7
code/espurna/system.ino View File

@ -6,7 +6,7 @@ Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include <EEPROM.h>
#include <EEPROM_Rotate.h>
// -----------------------------------------------------------------------------
@ -30,7 +30,7 @@ unsigned short int _load_average = 100;
bool _systemStable = true;
void systemCheck(bool stable) {
unsigned char value = EEPROM.read(EEPROM_CRASH_COUNTER);
unsigned char value = EEPROMr.read(EEPROM_CRASH_COUNTER);
if (stable) {
value = 0;
DEBUG_MSG_P(PSTR("[MAIN] System OK\n"));
@ -41,8 +41,8 @@ void systemCheck(bool stable) {
DEBUG_MSG_P(PSTR("[MAIN] System UNSTABLE\n"));
}
}
EEPROM.write(EEPROM_CRASH_COUNTER, value);
EEPROM.commit();
EEPROMr.write(EEPROM_CRASH_COUNTER, value);
EEPROMr.commit();
}
bool systemCheck() {
@ -140,7 +140,7 @@ void _systemSetupSpecificHardware() {
// These devices use the hardware UART
// to communicate to secondary microcontrollers
#if defined(ITEAD_SONOFF_RFBRIDGE) || defined(ITEAD_SONOFF_DUAL) || defined(STM_RELAY)
#if defined(ITEAD_SONOFF_RFBRIDGE) || defined(ITEAD_SONOFF_DUAL) || (RELAY_PROVIDER == RELAY_PROVIDER_STM)
Serial.begin(SERIAL_BAUDRATE);
#endif
@ -148,8 +148,6 @@ void _systemSetupSpecificHardware() {
void systemSetup() {
EEPROM.begin(EEPROM_SIZE);
#if SPIFFS_SUPPORT
SPIFFS.begin();
#endif


+ 1
- 1
code/espurna/telnet.ino View File

@ -138,7 +138,7 @@ void _telnetNewClient(AsyncClient *client) {
// If there is no terminal support automatically dump info and crash data
#if TERMINAL_SUPPORT == 0
info();
wifiStatus();
wifiDebug();
debugDumpCrashInfo();
debugClearCrashInfo();
#endif


+ 83
- 175
code/espurna/utils.ino View File

@ -64,6 +64,16 @@ unsigned int getFreeHeap() {
return ESP.getFreeHeap();
}
String getEspurnaModules() {
return FPSTR(espurna_modules);
}
#if SENSOR_SUPPORT
String getEspurnaSensors() {
return FPSTR(espurna_sensors);
}
#endif
String buildTime() {
const char time_now[] = __TIME__; // hh:mm:ss
@ -211,14 +221,50 @@ void heartbeat() {
#endif /// HEARTBEAT_ENABLED
unsigned int sectors(size_t size) {
// -----------------------------------------------------------------------------
// INFO
// -----------------------------------------------------------------------------
extern "C" uint32_t _SPIFFS_start;
extern "C" uint32_t _SPIFFS_end;
unsigned int info_bytes2sectors(size_t size) {
return (int) (size + SPI_FLASH_SEC_SIZE - 1) / SPI_FLASH_SEC_SIZE;
}
unsigned long info_ota_space() {
return (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
}
unsigned long info_filesystem_space() {
return ((uint32_t)&_SPIFFS_end - (uint32_t)&_SPIFFS_start);
}
unsigned long info_eeprom_space() {
return EEPROMr.reserved() * SPI_FLASH_SEC_SIZE;
}
void _info_print_memory_layout_line(const char * name, unsigned long bytes, bool reset) {
static unsigned long index = 0;
if (reset) index = 0;
if (0 == bytes) return;
unsigned int _sectors = info_bytes2sectors(bytes);
DEBUG_MSG_P(PSTR("[INIT] %-20s: %8lu bytes / %4d sectors (%4d to %4d)\n"), name, bytes, _sectors, index, index + _sectors - 1);
index += _sectors;
}
void _info_print_memory_layout_line(const char * name, unsigned long bytes) {
_info_print_memory_layout_line(name, bytes, false);
}
void info() {
DEBUG_MSG_P(PSTR("\n\n"));
DEBUG_MSG_P(PSTR("[INIT] %s %s\n"), (char *) APP_NAME, (char *) APP_VERSION);
if (strlen(APP_REVISION) > 0) {
DEBUG_MSG_P(PSTR("[INIT] %s %s (%s)\n"), (char *) APP_NAME, (char *) APP_VERSION, (char *) APP_REVISION);
} else {
DEBUG_MSG_P(PSTR("[INIT] %s %s\n"), (char *) APP_NAME, (char *) APP_VERSION);
}
DEBUG_MSG_P(PSTR("[INIT] %s\n"), (char *) APP_AUTHOR);
DEBUG_MSG_P(PSTR("[INIT] %s\n\n"), (char *) APP_WEBSITE);
DEBUG_MSG_P(PSTR("[INIT] CPU chip ID: 0x%06X\n"), ESP.getChipId());
@ -235,13 +281,18 @@ void info() {
DEBUG_MSG_P(PSTR("[INIT] Flash speed: %u Hz\n"), ESP.getFlashChipSpeed());
DEBUG_MSG_P(PSTR("[INIT] Flash mode: %s\n"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN");
DEBUG_MSG_P(PSTR("\n"));
DEBUG_MSG_P(PSTR("[INIT] Flash sector size: %8u bytes\n"), SPI_FLASH_SEC_SIZE);
DEBUG_MSG_P(PSTR("[INIT] Flash size (CHIP): %8u bytes\n"), ESP.getFlashChipRealSize());
DEBUG_MSG_P(PSTR("[INIT] Flash size (SDK): %8u bytes / %4d sectors\n"), ESP.getFlashChipSize(), sectors(ESP.getFlashChipSize()));
DEBUG_MSG_P(PSTR("[INIT] Firmware size: %8u bytes / %4d sectors\n"), ESP.getSketchSize(), sectors(ESP.getSketchSize()));
DEBUG_MSG_P(PSTR("[INIT] Max OTA size: %8u bytes / %4d sectors\n"), maxSketchSpace(), sectors(maxSketchSpace()));
DEBUG_MSG_P(PSTR("[INIT] EEPROM size: %8u bytes / %4d sectors\n"), settingsMaxSize(), sectors(settingsMaxSize()));
DEBUG_MSG_P(PSTR("[INIT] Empty space: %8u bytes / 4 sectors\n"), 4 * SPI_FLASH_SEC_SIZE);
_info_print_memory_layout_line("Flash size (CHIP)", ESP.getFlashChipRealSize(), true);
_info_print_memory_layout_line("Flash size (SDK)", ESP.getFlashChipSize(), true);
_info_print_memory_layout_line("Reserved", 1 * SPI_FLASH_SEC_SIZE, true);
_info_print_memory_layout_line("Firmware size", ESP.getSketchSize());
_info_print_memory_layout_line("Max OTA size", info_ota_space());
_info_print_memory_layout_line("SPIFFS size", info_filesystem_space());
_info_print_memory_layout_line("EEPROM size", info_eeprom_space());
_info_print_memory_layout_line("Reserved", 4 * SPI_FLASH_SEC_SIZE);
DEBUG_MSG_P(PSTR("\n"));
DEBUG_MSG_P(PSTR("[INIT] EEPROM sectors: %s\n"), (char *) eepromSectors().c_str());
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
@ -265,171 +316,12 @@ void info() {
// -------------------------------------------------------------------------
DEBUG_MSG_P(PSTR("[INIT] BOARD: %s\n"), getBoardName().c_str());
DEBUG_MSG_P(PSTR("[INIT] SUPPORT:"));
#if ALEXA_SUPPORT
DEBUG_MSG_P(PSTR(" ALEXA"));
#endif
#if BROKER_SUPPORT
DEBUG_MSG_P(PSTR(" BROKER"));
#endif
#if DEBUG_SERIAL_SUPPORT
DEBUG_MSG_P(PSTR(" DEBUG_SERIAL"));
#endif
#if DEBUG_TELNET_SUPPORT
DEBUG_MSG_P(PSTR(" DEBUG_TELNET"));
#endif
#if DEBUG_UDP_SUPPORT
DEBUG_MSG_P(PSTR(" DEBUG_UDP"));
#endif
#if DEBUG_WEB_SUPPORT
DEBUG_MSG_P(PSTR(" DEBUG_WEB"));
#endif
#if DOMOTICZ_SUPPORT
DEBUG_MSG_P(PSTR(" DOMOTICZ"));
#endif
#if HOMEASSISTANT_SUPPORT
DEBUG_MSG_P(PSTR(" HOMEASSISTANT"));
#endif
#if I2C_SUPPORT
DEBUG_MSG_P(PSTR(" I2C"));
#endif
#if INFLUXDB_SUPPORT
DEBUG_MSG_P(PSTR(" INFLUXDB"));
#endif
#if LLMNR_SUPPORT
DEBUG_MSG_P(PSTR(" LLMNR"));
#endif
#if MDNS_SERVER_SUPPORT
DEBUG_MSG_P(PSTR(" MDNS_SERVER"));
#endif
#if MDNS_CLIENT_SUPPORT
DEBUG_MSG_P(PSTR(" MDNS_CLIENT"));
#endif
#if MQTT_SUPPORT
DEBUG_MSG_P(PSTR(" MQTT"));
#endif
#if NETBIOS_SUPPORT
DEBUG_MSG_P(PSTR(" NETBIOS"));
#endif
#if NOFUSS_SUPPORT
DEBUG_MSG_P(PSTR(" NOFUSS"));
#endif
#if NTP_SUPPORT
DEBUG_MSG_P(PSTR(" NTP"));
#endif
#if RF_SUPPORT
DEBUG_MSG_P(PSTR(" RF"));
#endif
#if SCHEDULER_SUPPORT
DEBUG_MSG_P(PSTR(" SCHEDULER"));
#endif
DEBUG_MSG_P(PSTR("[INIT] SUPPORT: %s\n"), getEspurnaModules().c_str());
#if SENSOR_SUPPORT
DEBUG_MSG_P(PSTR(" SENSOR"));
#endif
#if SPIFFS_SUPPORT
DEBUG_MSG_P(PSTR(" SPIFFS"));
#endif
#if SSDP_SUPPORT
DEBUG_MSG_P(PSTR(" SSDP"));
#endif
#if TELNET_SUPPORT
DEBUG_MSG_P(PSTR(" TELNET"));
#endif
#if TERMINAL_SUPPORT
DEBUG_MSG_P(PSTR(" TERMINAL"));
#endif
#if THINGSPEAK_SUPPORT
DEBUG_MSG_P(PSTR(" THINGSPEAK"));
#endif
#if UART_MQTT_SUPPORT
DEBUG_MSG_P(PSTR(" UART_MQTT"));
#endif
#if WEB_SUPPORT
DEBUG_MSG_P(PSTR(" WEB"));
#endif
#if SENSOR_SUPPORT
DEBUG_MSG_P(PSTR("\n"));
DEBUG_MSG_P(PSTR("[INIT] SENSORS:"));
#if AM2320_SUPPORT
DEBUG_MSG_P(PSTR(" AM2320_I2C"));
#endif
#if ANALOG_SUPPORT
DEBUG_MSG_P(PSTR(" ANALOG"));
#endif
#if BH1750_SUPPORT
DEBUG_MSG_P(PSTR(" BH1750"));
#endif
#if BMX280_SUPPORT
DEBUG_MSG_P(PSTR(" BMX280"));
#endif
#if CSE7766_SUPPORT
DEBUG_MSG_P(PSTR(" CSE7766"));
#endif
#if DALLAS_SUPPORT
DEBUG_MSG_P(PSTR(" DALLAS"));
#endif
#if DHT_SUPPORT
DEBUG_MSG_P(PSTR(" DHTXX"));
#endif
#if DIGITAL_SUPPORT
DEBUG_MSG_P(PSTR(" DIGITAL"));
#endif
#if ECH1560_SUPPORT
DEBUG_MSG_P(PSTR(" ECH1560"));
#endif
#if EMON_ADC121_SUPPORT
DEBUG_MSG_P(PSTR(" EMON_ADC121"));
#endif
#if EMON_ADS1X15_SUPPORT
DEBUG_MSG_P(PSTR(" EMON_ADX1X15"));
#endif
#if EMON_ANALOG_SUPPORT
DEBUG_MSG_P(PSTR(" EMON_ANALOG"));
#endif
#if EVENTS_SUPPORT
DEBUG_MSG_P(PSTR(" EVENTS"));
#endif
#if GUVAS12SD_SUPPORT
DEBUG_MSG_P(PSTR(" GUVAS12SD"));
#endif
#if HCSR04_SUPPORT
DEBUG_MSG_P(PSTR(" HCSR04"));
#endif
#if HLW8012_SUPPORT
DEBUG_MSG_P(PSTR(" HLW8012"));
#endif
#if MHZ19_SUPPORT
DEBUG_MSG_P(PSTR(" MHZ19"));
#endif
#if PMSX003_SUPPORT
DEBUG_MSG_P(PSTR(" PMSX003"));
#endif
#if PZEM004T_SUPPORT
DEBUG_MSG_P(PSTR(" PZEM004T"));
#endif
#if SENSEAIR_SUPPORT
DEBUG_MSG_P(PSTR(" SENSEAIR"));
#endif
#if SHT3X_I2C_SUPPORT
DEBUG_MSG_P(PSTR(" SHT3X_I2C"));
#endif
#if SI7021_SUPPORT
DEBUG_MSG_P(PSTR(" SI7021"));
#endif
#if TMP3X_SUPPORT
DEBUG_MSG_P(PSTR(" TMP3X"));
#endif
#if V9261F_SUPPORT
DEBUG_MSG_P(PSTR(" V9261F"));
#endif
DEBUG_MSG_P(PSTR("[INIT] SENSORS: %s\n"), getEspurnaSensors().c_str());
#endif // SENSOR_SUPPORT
DEBUG_MSG_P(PSTR("\n\n"));
DEBUG_MSG_P(PSTR("[INIT] WEBUI IMAGE CODE: %u\n"), WEBUI_IMAGE);
DEBUG_MSG_P(PSTR("\n"));
// -------------------------------------------------------------------------
@ -508,7 +400,7 @@ bool sslFingerPrintChar(const char * fingerprint, char * destination) {
unsigned char resetReason() {
static unsigned char status = 255;
if (status == 255) {
status = EEPROM.read(EEPROM_CUSTOM_RESET);
status = EEPROMr.read(EEPROM_CUSTOM_RESET);
if (status > 0) resetReason(0);
if (status > CUSTOM_RESET_MAX) status = 0;
}
@ -516,8 +408,8 @@ unsigned char resetReason() {
}
void resetReason(unsigned char reason) {
EEPROM.write(EEPROM_CUSTOM_RESET, reason);
EEPROM.commit();
EEPROMr.write(EEPROM_CUSTOM_RESET, reason);
EEPROMr.commit();
}
void reset(unsigned char reason) {
@ -552,3 +444,19 @@ void nice_delay(unsigned long ms) {
int __get_adc_mode() {
return (int) (ADC_MODE_VALUE);
}
bool isNumber(const char * s) {
unsigned char len = strlen(s);
bool decimal = false;
for (unsigned char i=0; i<len; i++) {
if (s[i] == '-') {
if (i>0) return false;
} else if (s[i] == '.') {
if (decimal) return false;
decimal = true;
} else if (!isdigit(s[i])) {
return false;
}
}
return true;
}

+ 107
- 18
code/espurna/web.ino View File

@ -16,7 +16,21 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
#include <ArduinoJson.h>
#if WEB_EMBEDDED
#include "static/index.html.gz.h"
#if WEBUI_IMAGE == WEBUI_IMAGE_SMALL
#include "static/index.small.html.gz.h"
#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHT
#include "static/index.light.html.gz.h"
#elif WEBUI_IMAGE == WEBUI_IMAGE_SENSOR
#include "static/index.sensor.html.gz.h"
#elif WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE
#include "static/index.rfbridge.html.gz.h"
#elif WEBUI_IMAGE == WEBUI_IMAGE_RFM69
#include "static/index.rfm69.html.gz.h"
#elif WEBUI_IMAGE == WEBUI_IMAGE_FULL
#include "static/index.all.html.gz.h"
#endif
#endif // WEB_EMBEDDED
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
@ -31,6 +45,8 @@ char _last_modified[50];
std::vector<uint8_t> * _webConfigBuffer;
bool _webConfigSuccess = false;
std::vector<web_request_callback_f> _web_request_callbacks;
// -----------------------------------------------------------------------------
// HOOKS
// -----------------------------------------------------------------------------
@ -40,10 +56,9 @@ void _onReset(AsyncWebServerRequest *request) {
request->send(200);
}
void _onGetConfig(AsyncWebServerRequest *request) {
void _onDiscover(AsyncWebServerRequest *request) {
webLog(request);
if (!_authenticate(request)) return request->requestAuthentication(getSetting("hostname").c_str());
AsyncResponseStream *response = request->beginResponseStream("text/json");
@ -51,12 +66,45 @@ void _onGetConfig(AsyncWebServerRequest *request) {
JsonObject &root = jsonBuffer.createObject();
root["app"] = APP_NAME;
root["version"] = APP_VERSION;
settingsGetJson(root);
root.prettyPrintTo(*response);
root["hostname"] = getSetting("hostname");
root["device"] = getBoardName();
root.printTo(*response);
request->send(response);
}
void _onGetConfig(AsyncWebServerRequest *request) {
webLog(request);
if (!webAuthenticate(request)) {
return request->requestAuthentication(getSetting("hostname").c_str());
}
AsyncResponseStream *response = request->beginResponseStream("text/json");
char buffer[100];
snprintf_P(buffer, sizeof(buffer), PSTR("attachment; filename=\"%s-backup.json\""), (char *) getSetting("hostname").c_str());
response->addHeader("Content-Disposition", buffer);
response->addHeader("X-XSS-Protection", "1; mode=block");
response->addHeader("X-Content-Type-Options", "nosniff");
response->addHeader("X-Frame-Options", "deny");
response->printf("{\n\"app\": \"%s\"", APP_NAME);
response->printf(",\n\"version\": \"%s\"", APP_VERSION);
response->printf(",\n\"backup\": \"1\"");
#if NTP_SUPPORT
response->printf(",\n\"timestamp\": \"%s\"", ntpDateTime().c_str());
#endif
// Write the keys line by line (not sorted)
unsigned long count = settingsKeyCount();
for (unsigned int i=0; i<count; i++) {
String key = settingsKeyName(i);
String value = getSetting(key);
response->printf(",\n\"%s\": \"%s\"", key.c_str(), value.c_str());
}
response->printf("\n}");
request->send(response);
@ -64,7 +112,9 @@ void _onGetConfig(AsyncWebServerRequest *request) {
void _onPostConfig(AsyncWebServerRequest *request) {
webLog(request);
if (!_authenticate(request)) return request->requestAuthentication(getSetting("hostname").c_str());
if (!webAuthenticate(request)) {
return request->requestAuthentication(getSetting("hostname").c_str());
}
request->send(_webConfigSuccess ? 200 : 400);
}
@ -112,7 +162,9 @@ void _onPostConfigData(AsyncWebServerRequest *request, String filename, size_t i
void _onHome(AsyncWebServerRequest *request) {
webLog(request);
if (!_authenticate(request)) return request->requestAuthentication(getSetting("hostname").c_str());
if (!webAuthenticate(request)) {
return request->requestAuthentication(getSetting("hostname").c_str());
}
if (request->header("If-Modified-Since").equals(_last_modified)) {
@ -130,12 +182,12 @@ void _onHome(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginChunkedResponse("text/html", [max](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
// Get the chunk based on the index and maxLen
size_t len = index_html_gz_len - index;
size_t len = webui_image_len - index;
if (len > maxLen) len = maxLen;
if (len > max) len = max;
if (len > 0) memcpy_P(buffer, index_html_gz + index, len);
if (len > 0) memcpy_P(buffer, webui_image + index, len);
DEBUG_MSG_P(PSTR("[WEB] Sending %d%%%% (max chunk size: %4d)\r"), int(100 * index / index_html_gz_len), max);
DEBUG_MSG_P(PSTR("[WEB] Sending %d%%%% (max chunk size: %4d)\r"), int(100 * index / webui_image_len), max);
if (len == 0) DEBUG_MSG_P(PSTR("\n"));
// Return the actual length of the chunk (0 for end of file)
@ -145,12 +197,15 @@ void _onHome(AsyncWebServerRequest *request) {
#else
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_html_gz, index_html_gz_len);
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", webui_image, webui_image_len);
#endif
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Last-Modified", _last_modified);
response->addHeader("X-XSS-Protection", "1; mode=block");
response->addHeader("X-Content-Type-Options", "nosniff");
response->addHeader("X-Frame-Options", "deny");
request->send(response);
}
@ -212,7 +267,9 @@ int _onCertificate(void * arg, const char *filename, uint8_t **buf) {
void _onUpgrade(AsyncWebServerRequest *request) {
webLog(request);
if (!_authenticate(request)) return request->requestAuthentication(getSetting("hostname").c_str());
if (!webAuthenticate(request)) {
return request->requestAuthentication(getSetting("hostname").c_str());
}
char buffer[10];
if (!Update.hasError()) {
@ -223,7 +280,12 @@ void _onUpgrade(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", buffer);
response->addHeader("Connection", "close");
if (!Update.hasError()) {
response->addHeader("X-XSS-Protection", "1; mode=block");
response->addHeader("X-Content-Type-Options", "nosniff");
response->addHeader("X-Frame-Options", "deny");
if (Update.hasError()) {
eepromRotate(true);
} else {
deferredReset(100, CUSTOM_RESET_UPGRADE);
}
request->send(response);
@ -231,7 +293,12 @@ void _onUpgrade(AsyncWebServerRequest *request) {
}
void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!index) {
// Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
eepromRotate(false);
DEBUG_MSG_P(PSTR("[UPGRADE] Start: %s\n"), filename.c_str());
Update.runAsync(true);
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
@ -239,7 +306,9 @@ void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t inde
Update.printError(DEBUG_PORT);
#endif
}
}
if (!Update.hasError()) {
if (Update.write(data, len) != len) {
#ifdef DEBUG_PORT
@ -247,6 +316,7 @@ void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t inde
#endif
}
}
if (final) {
if (Update.end(true)){
DEBUG_MSG_P(PSTR("[UPGRADE] Success: %u bytes\n"), index + len);
@ -260,9 +330,22 @@ void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t inde
}
}
void _onRequest(AsyncWebServerRequest *request){
// Send request to subscribers
for (unsigned char i = 0; i < _web_request_callbacks.size(); i++) {
bool response = (_web_request_callbacks[i])(request);
if (response) return;
}
// No subscriber handled the request, return a 404
request->send(404);
}
// -----------------------------------------------------------------------------
bool _authenticate(AsyncWebServerRequest *request) {
bool webAuthenticate(AsyncWebServerRequest *request) {
#if USE_PASSWORD
String password = getSetting("adminPass", ADMIN_PASS);
char httpPassword[password.length() + 1];
@ -279,6 +362,10 @@ AsyncWebServer * webServer() {
return _server;
}
void webRequestRegister(web_request_callback_f callback) {
_web_request_callbacks.push_back(callback);
}
unsigned int webPort() {
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
return 443;
@ -307,10 +394,13 @@ void webSetup() {
#if WEB_EMBEDDED
_server->on("/index.html", HTTP_GET, _onHome);
#endif
// Other entry points
_server->on("/reset", HTTP_GET, _onReset);
_server->on("/config", HTTP_GET, _onGetConfig);
_server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigData);
_server->on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeData);
_server->on("/discover", HTTP_GET, _onDiscover);
// Serve static files
#if SPIFFS_SUPPORT
@ -322,10 +412,8 @@ void webSetup() {
});
#endif
// 404
_server->onNotFound([](AsyncWebServerRequest *request){
request->send(404);
});
// Handle other requests, including 404
_server->onNotFound(_onRequest);
// Run server
#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED
@ -334,6 +422,7 @@ void webSetup() {
#else
_server->begin();
#endif
DEBUG_MSG_P(PSTR("[WEBSERVER] Webserver running on port %u\n"), port);
}


+ 214
- 52
code/espurna/wifi.ino View File

@ -10,11 +10,26 @@ Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
#include <Ticker.h>
uint32_t _wifi_scan_client_id = 0;
bool _wifi_wps_running = false;
bool _wifi_smartconfig_running = false;
uint8_t _wifi_ap_mode = WIFI_AP_FALLBACK;
// -----------------------------------------------------------------------------
// PRIVATE
// -----------------------------------------------------------------------------
void _wifiCheckAP() {
if ((WIFI_AP_FALLBACK == _wifi_ap_mode) &&
(jw.connected()) &&
((WiFi.getMode() & WIFI_AP) > 0) &&
(WiFi.softAPgetStationNum() == 0)
) {
jw.enableAP(false);
}
}
void _wifiConfigure() {
jw.setHostname(getSetting("hostname").c_str());
@ -25,9 +40,11 @@ void _wifiConfigure() {
#endif
jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
wifiReconnectCheck();
jw.setAPMode(WIFI_AP_MODE);
jw.enableAPFallback(true);
jw.cleanNetworks();
_wifi_ap_mode = getSetting("apmode", WIFI_AP_FALLBACK).toInt();
// If system is flagged unstable we do not init wifi networks
#if SYSTEM_CHECK_ENABLED
if (!systemCheck()) return;
@ -56,7 +73,7 @@ void _wifiConfigure() {
}
}
jw.scanNetworks(getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1);
jw.enableScan(getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1);
}
@ -196,6 +213,47 @@ void _wifiInject() {
}
}
void _wifiCallback(justwifi_messages_t code, char * parameter) {
if (MESSAGE_WPS_START == code) {
_wifi_wps_running = true;
}
if (MESSAGE_SMARTCONFIG_START == code) {
_wifi_smartconfig_running = true;
}
if (MESSAGE_WPS_ERROR == code || MESSAGE_SMARTCONFIG_ERROR == code) {
_wifi_wps_running = false;
_wifi_smartconfig_running = false;
}
if (MESSAGE_WPS_SUCCESS == code || MESSAGE_SMARTCONFIG_SUCCESS == code) {
String ssid = WiFi.SSID();
String pass = WiFi.psk();
// Look for the same SSID
uint8_t count = 0;
while (count < WIFI_MAX_NETWORKS) {
if (!hasSetting("ssid", count)) break;
if (ssid.equals(getSetting("ssid", count, ""))) break;
count++;
}
// If we have reached the max we overwrite the first one
if (WIFI_MAX_NETWORKS == count) count = 0;
setSetting("ssid", count, ssid);
setSetting("pass", count, pass);
_wifi_wps_running = false;
_wifi_smartconfig_running = false;
}
}
#if WIFI_AP_CAPTIVE
#include "DNSServer.h"
@ -221,7 +279,9 @@ void _wifiCaptivePortal(justwifi_messages_t code, char * parameter) {
#if DEBUG_SUPPORT
void _wifiDebug(justwifi_messages_t code, char * parameter) {
void _wifiDebugCallback(justwifi_messages_t code, char * parameter) {
// -------------------------------------------------------------------------
if (code == MESSAGE_SCANNING) {
DEBUG_MSG_P(PSTR("[WIFI] Scanning\n"));
@ -243,6 +303,8 @@ void _wifiDebug(justwifi_messages_t code, char * parameter) {
DEBUG_MSG_P(PSTR("[WIFI] %s\n"), parameter);
}
// -------------------------------------------------------------------------
if (code == MESSAGE_CONNECTING) {
DEBUG_MSG_P(PSTR("[WIFI] Connecting to %s\n"), parameter);
}
@ -256,25 +318,59 @@ void _wifiDebug(justwifi_messages_t code, char * parameter) {
}
if (code == MESSAGE_CONNECTED) {
wifiStatus();
}
if (code == MESSAGE_ACCESSPOINT_CREATED) {
wifiStatus();
wifiDebug(WIFI_STA);
}
if (code == MESSAGE_DISCONNECTED) {
DEBUG_MSG_P(PSTR("[WIFI] Disconnected\n"));
}
// -------------------------------------------------------------------------
if (code == MESSAGE_ACCESSPOINT_CREATING) {
DEBUG_MSG_P(PSTR("[WIFI] Creating access point\n"));
}
if (code == MESSAGE_ACCESSPOINT_CREATED) {
wifiDebug(WIFI_AP);
}
if (code == MESSAGE_ACCESSPOINT_FAILED) {
DEBUG_MSG_P(PSTR("[WIFI] Could not create access point\n"));
}
if (code == MESSAGE_ACCESSPOINT_DESTROYED) {
DEBUG_MSG_P(PSTR("[WIFI] Access point destroyed\n"));
}
// -------------------------------------------------------------------------
if (code == MESSAGE_WPS_START) {
DEBUG_MSG_P(PSTR("[WIFI] WPS started\n"));
}
if (code == MESSAGE_WPS_SUCCESS) {
DEBUG_MSG_P(PSTR("[WIFI] WPS succeded!\n"));
}
if (code == MESSAGE_WPS_ERROR) {
DEBUG_MSG_P(PSTR("[WIFI] WPS failed\n"));
}
// ------------------------------------------------------------------------
if (code == MESSAGE_SMARTCONFIG_START) {
DEBUG_MSG_P(PSTR("[WIFI] Smart Config started\n"));
}
if (code == MESSAGE_SMARTCONFIG_SUCCESS) {
DEBUG_MSG_P(PSTR("[WIFI] Smart Config succeded!\n"));
}
if (code == MESSAGE_SMARTCONFIG_ERROR) {
DEBUG_MSG_P(PSTR("[WIFI] Smart Config failed\n"));
}
}
#endif // DEBUG_SUPPORT
@ -294,10 +390,24 @@ void _wifiInitCommands() {
});
settingsRegisterCommand(F("WIFI.AP"), [](Embedis* e) {
createAP();
wifiStartAP();
DEBUG_MSG_P(PSTR("+OK\n"));
});
#if defined(JUSTWIFI_ENABLE_WPS)
settingsRegisterCommand(F("WIFI.WPS"), [](Embedis* e) {
wifiStartWPS();
DEBUG_MSG_P(PSTR("+OK\n"));
});
#endif // defined(JUSTWIFI_ENABLE_WPS)
#if defined(JUSTWIFI_ENABLE_SMARTCONFIG)
settingsRegisterCommand(F("WIFI.SMARTCONFIG"), [](Embedis* e) {
wifiStartSmartConfig();
DEBUG_MSG_P(PSTR("+OK\n"));
});
#endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG)
settingsRegisterCommand(F("WIFI.SCAN"), [](Embedis* e) {
_wifiScan();
DEBUG_MSG_P(PSTR("+OK\n"));
@ -346,6 +456,59 @@ void _wifiWebSocketOnAction(uint32_t client_id, const char * action, JsonObject&
#endif
// -----------------------------------------------------------------------------
// INFO
// -----------------------------------------------------------------------------
void wifiDebug(WiFiMode_t modes) {
bool footer = false;
if (((modes & WIFI_STA) > 0) && ((WiFi.getMode() & WIFI_STA) > 0)) {
uint8_t * bssid = WiFi.BSSID();
DEBUG_MSG_P(PSTR("[WIFI] ------------------------------------- MODE STA\n"));
DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), WiFi.SSID().c_str());
DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.localIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.macAddress().c_str());
DEBUG_MSG_P(PSTR("[WIFI] GW %s\n"), WiFi.gatewayIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] DNS %s\n"), WiFi.dnsIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MASK %s\n"), WiFi.subnetMask().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] HOST http://%s.local\n"), WiFi.hostname().c_str());
DEBUG_MSG_P(PSTR("[WIFI] BSSID %02X:%02X:%02X:%02X:%02X:%02X\n"),
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], bssid[6]
);
DEBUG_MSG_P(PSTR("[WIFI] CH %d\n"), WiFi.channel());
DEBUG_MSG_P(PSTR("[WIFI] RSSI %d\n"), WiFi.RSSI());
footer = true;
}
if (((modes & WIFI_AP) > 0) && ((WiFi.getMode() & WIFI_AP) > 0)) {
DEBUG_MSG_P(PSTR("[WIFI] -------------------------------------- MODE AP\n"));
DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), getSetting("hostname").c_str());
DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), getSetting("adminPass", ADMIN_PASS).c_str());
DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str());
footer = true;
}
if (WiFi.getMode() == 0) {
DEBUG_MSG_P(PSTR("[WIFI] ------------------------------------- MODE OFF\n"));
DEBUG_MSG_P(PSTR("[WIFI] No connection\n"));
footer = true;
}
if (footer) {
DEBUG_MSG_P(PSTR("[WIFI] ----------------------------------------------\n"));
}
}
void wifiDebug() {
wifiDebug(WIFI_AP_STA);
}
// -----------------------------------------------------------------------------
// API
// -----------------------------------------------------------------------------
@ -372,11 +535,30 @@ void wifiDisconnect() {
jw.disconnect();
}
bool createAP() {
jw.disconnect();
jw.resetReconnectTimeout();
return jw.createAP();
void wifiStartAP(bool only) {
if (only) {
jw.enableSTA(false);
jw.disconnect();
jw.resetReconnectTimeout();
}
jw.enableAP(true);
}
void wifiStartAP() {
wifiStartAP(true);
}
#if defined(JUSTWIFI_ENABLE_WPS)
void wifiStartWPS() {
jw.startWPS();
}
#endif // defined(JUSTWIFI_ENABLE_WPS)
#if defined(JUSTWIFI_ENABLE_SMARTCONFIG)
void wifiStartSmartConfig() {
jw.startSmartConfig();
}
#endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG)
void wifiReconnectCheck() {
bool connected = false;
@ -389,44 +571,13 @@ void wifiReconnectCheck() {
jw.setReconnectTimeout(connected ? 0 : WIFI_RECONNECT_INTERVAL);
}
void wifiStatus() {
if (WiFi.getMode() == WIFI_AP_STA) {
DEBUG_MSG_P(PSTR("[WIFI] MODE AP + STA --------------------------------\n"));
} else if (WiFi.getMode() == WIFI_AP) {
DEBUG_MSG_P(PSTR("[WIFI] MODE AP --------------------------------------\n"));
} else if (WiFi.getMode() == WIFI_STA) {
DEBUG_MSG_P(PSTR("[WIFI] MODE STA -------------------------------------\n"));
} else {
DEBUG_MSG_P(PSTR("[WIFI] MODE OFF -------------------------------------\n"));
DEBUG_MSG_P(PSTR("[WIFI] No connection\n"));
}
if ((WiFi.getMode() & WIFI_AP) == WIFI_AP) {
DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), jw.getAPSSID().c_str());
DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), getSetting("adminPass", ADMIN_PASS).c_str());
DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str());
}
if ((WiFi.getMode() & WIFI_STA) == WIFI_STA) {
uint8_t * bssid = WiFi.BSSID();
DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), WiFi.SSID().c_str());
DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.localIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.macAddress().c_str());
DEBUG_MSG_P(PSTR("[WIFI] GW %s\n"), WiFi.gatewayIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] DNS %s\n"), WiFi.dnsIP().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] MASK %s\n"), WiFi.subnetMask().toString().c_str());
DEBUG_MSG_P(PSTR("[WIFI] HOST http://%s.local\n"), WiFi.hostname().c_str());
DEBUG_MSG_P(PSTR("[WIFI] BSSID %02X:%02X:%02X:%02X:%02X:%02X\n"),
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], bssid[6]
);
DEBUG_MSG_P(PSTR("[WIFI] CH %d\n"), WiFi.channel());
DEBUG_MSG_P(PSTR("[WIFI] RSSI %d\n"), WiFi.RSSI());
}
DEBUG_MSG_P(PSTR("[WIFI] ----------------------------------------------\n"));
uint8_t wifiState() {
uint8_t state = 0;
if (jw.connected()) state += WIFI_STATE_STA;
if (jw.connectable()) state += WIFI_STATE_AP;
if (_wifi_wps_running) state += WIFI_STATE_WPS;
if (_wifi_smartconfig_running) state += WIFI_STATE_SMARTCONFIG;
return state;
}
void wifiRegister(wifi_callback_f callback) {
@ -445,11 +596,12 @@ void wifiSetup() {
_wifiConfigure();
// Message callbacks
wifiRegister(_wifiCallback);
#if WIFI_AP_CAPTIVE
wifiRegister(_wifiCaptivePortal);
#endif
#if DEBUG_SUPPORT
wifiRegister(_wifiDebug);
wifiRegister(_wifiDebugCallback);
#endif
#if WEB_SUPPORT
@ -470,17 +622,27 @@ void wifiSetup() {
void wifiLoop() {
// Main wifi loop
jw.loop();
// Process captrive portal DNS queries if in AP mode only
#if WIFI_AP_CAPTIVE
if ((WiFi.getMode() & WIFI_AP) == WIFI_AP) {
_wifi_dnsServer.processNextRequest();
}
#endif
// Do we have a pending scan?
if (_wifi_scan_client_id > 0) {
_wifiScan(_wifi_scan_client_id);
_wifi_scan_client_id = 0;
}
// Check if we should disable AP
static unsigned long last = 0;
if (millis() - last > 60000) {
last = millis();
_wifiCheckAP();
}
}

+ 69
- 13
code/espurna/ws.ino View File

@ -27,6 +27,57 @@ std::vector<ws_on_receive_callback_f> _ws_on_receive_callbacks;
// Private methods
// -----------------------------------------------------------------------------
typedef struct {
IPAddress ip;
unsigned long timestamp = 0;
} ws_ticket_t;
ws_ticket_t _ticket[WS_BUFFER_SIZE];
void _onAuth(AsyncWebServerRequest *request) {
webLog(request);
if (!webAuthenticate(request)) return request->requestAuthentication();
IPAddress ip = request->client()->remoteIP();
unsigned long now = millis();
unsigned short index;
for (index = 0; index < WS_BUFFER_SIZE; index++) {
if (_ticket[index].ip == ip) break;
if (_ticket[index].timestamp == 0) break;
if (now - _ticket[index].timestamp > WS_TIMEOUT) break;
}
if (index == WS_BUFFER_SIZE) {
request->send(429);
} else {
_ticket[index].ip = ip;
_ticket[index].timestamp = now;
request->send(200, "text/plain", "OK");
}
}
bool _wsAuth(AsyncWebSocketClient * client) {
IPAddress ip = client->remoteIP();
unsigned long now = millis();
unsigned short index = 0;
for (index = 0; index < WS_BUFFER_SIZE; index++) {
if ((_ticket[index].ip == ip) && (now - _ticket[index].timestamp < WS_TIMEOUT)) break;
}
if (index == WS_BUFFER_SIZE) {
DEBUG_MSG_P(PSTR("[WEBSOCKET] Validation check failed\n"));
wsSend_P(client->id(), PSTR("{\"message\": 10}"));
return false;
}
return true;
}
// -----------------------------------------------------------------------------
#if MQTT_SUPPORT
void _wsMQTTCallback(unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) wsSend_P(PSTR("{\"mqttStatus\": true}"));
@ -315,6 +366,11 @@ void _wsStart(uint32_t client_id) {
void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
if (type == WS_EVT_CONNECT) {
#ifndef NOWSAUTH
if (!_wsAuth(client)) return;
#endif
IPAddress ip = client->remoteIP();
DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n"), client->id(), ip[0], ip[1], ip[2], ip[3], server->url());
_wsStart(client->id());
@ -384,6 +440,7 @@ void wsSend(ws_on_send_callback_f callback) {
callback(root);
String output;
root.printTo(output);
jsonBuffer.clear();
_ws.textAll((char *) output.c_str());
}
}
@ -408,6 +465,7 @@ void wsSend(uint32_t client_id, ws_on_send_callback_f callback) {
callback(root);
String output;
root.printTo(output);
jsonBuffer.clear();
_ws.text(client_id, (char *) output.c_str());
}
@ -421,17 +479,6 @@ void wsSend_P(uint32_t client_id, PGM_P payload) {
_ws.text(client_id, buffer);
}
void wsConfigure() {
#if USE_PASSWORD
bool auth = getSetting("wsAuth", WS_AUTHENTICATION).toInt() == 1;
if (auth) {
_ws.setAuthentication(WEB_USERNAME, (const char *) getSetting("adminPass", ADMIN_PASS).c_str());
} else {
_ws.setAuthentication("", "");
}
#endif
}
// This method being public makes
// _ws_on_after_parse_callbacks strange here,
// it should belong somewhere else.
@ -442,15 +489,24 @@ void wsReload() {
}
void wsSetup() {
_ws.onEvent(_wsEvent);
wsConfigure();
webServer()->addHandler(&_ws);
// CORS
#ifdef WEB_REMOTE_DOMAIN
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", WEB_REMOTE_DOMAIN);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
#endif
webServer()->on("/auth", HTTP_GET, _onAuth);
#if MQTT_SUPPORT
mqttRegister(_wsMQTTCallback);
#endif
wsOnSendRegister(_wsOnStart);
wsOnReceiveRegister(_wsOnReceive);
wsOnAfterParseRegister(wsConfigure);
espurnaRegisterLoop(_wsLoop);
}


+ 36
- 10
code/extra_scripts.py View File

@ -1,9 +1,16 @@
#!/usr/bin/env python
from subprocess import call
from __future__ import print_function
import os
import time
import sys
from subprocess import call
import click
from platformio import util
import distutils.spawn
Import("env")
Import("env", "projenv")
# ------------------------------------------------------------------------------
# Utils
@ -30,6 +37,17 @@ class Color(object):
def clr(color, text):
return color + str(text) + '\x1b[0m'
def print_warning(message, color=Color.LIGHT_YELLOW):
print(clr(color, message), file=sys.stderr)
def print_filler(fill, color=Color.WHITE, err=False):
width, _ = click.get_terminal_size()
if len(fill) > 1:
fill = fill[0]
out = sys.stderr if err else sys.stdout
print(clr(color, fill * width), file=out)
# ------------------------------------------------------------------------------
# Callbacks
# ------------------------------------------------------------------------------
@ -51,18 +69,26 @@ def cpp_check(source, target, env):
print("Finished cppcheck...\n")
def check_size(source, target, env):
time.sleep(2)
size = target[0].get_size()
print clr(Color.LIGHT_BLUE, "Binary size: %s bytes" % size)
#if size > 512000:
# print clr(Color.LIGHT_RED, "File too large for OTA!")
# Exit(1)
(binary,) = target
path = binary.get_abspath()
size = os.stat(path).st_size
print(clr(Color.LIGHT_BLUE, "Binary size: {} bytes".format(size)))
# Warn 1MB variants about exceeding OTA size limit
flash_size = int(env.BoardConfig().get("upload.maximum_size", 0))
if (flash_size == 1048576) and (size >= 512000):
print_filler("*", color=Color.LIGHT_YELLOW, err=True)
print_warning("File is too large for OTA! Here you can find instructions on how to flash it:")
print_warning("https://github.com/xoseperez/espurna/wiki/TwoStepUpdates", color=Color.LIGHT_CYAN)
print_filler("*", color=Color.LIGHT_YELLOW, err=True)
# ------------------------------------------------------------------------------
# Hooks
# ------------------------------------------------------------------------------
# Always show warnings for project code
projenv.ProcessUnFlags("-w")
remove_float_support()
#env.AddPreAction("buildprog", cpp_check)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", check_size)

+ 122
- 52
code/gulpfile.js View File

@ -19,65 +19,84 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*eslint quotes: ["error", "single"]*/
/*eslint quotes: ['error', 'single']*/
/*eslint-env es6*/
// -----------------------------------------------------------------------------
// File system builder
// Dependencies
// -----------------------------------------------------------------------------
const fs = require('fs');
const gulp = require('gulp');
const runSequence = require('run-sequence');
const through = require('through2');
const htmlmin = require('gulp-htmlmin');
const uglify = require('gulp-uglify');
const gzip = require('gulp-gzip');
const inline = require('gulp-inline');
const inlineImages = require('gulp-css-base64');
const favicon = require('gulp-base64-favicon');
const crass = require('gulp-crass');
const htmllint = require('gulp-htmllint');
const log = require('fancy-log');
const csslint = require('gulp-csslint');
const crass = require('gulp-crass');
const rename = require('gulp-rename');
const replace = require('gulp-replace');
const remover = require('gulp-remove-code');
const gzip = require('gulp-gzip');
const path = require('path');
// -----------------------------------------------------------------------------
// Configuration
// -----------------------------------------------------------------------------
const htmlFolder = 'html/';
const configFolder = 'espurna/config/';
const dataFolder = 'espurna/data/';
const staticFolder = 'espurna/static/';
var toHeader = function(filename) {
var source = dataFolder + filename;
var destination = staticFolder + filename + '.h';
var safename = filename.split('.').join('_');
// -----------------------------------------------------------------------------
// Methods
// -----------------------------------------------------------------------------
var wstream = fs.createWriteStream(destination);
wstream.on('error', function (err) {
log.error(err);
});
var toHeader = function(name, debug) {
var data = fs.readFileSync(source);
return through.obj(function (source, encoding, callback) {
wstream.write('#define ' + safename + '_len ' + data.length + '\n');
wstream.write('const uint8_t ' + safename + '[] PROGMEM = {');
var parts = source.path.split(path.sep);
var filename = parts[parts.length - 1];
var safename = name || filename.split('.').join('_');
for (var i=0; i<data.length; i++) {
if (0 === (i % 20)) {
wstream.write('\n');
// Generate output
var output = '';
output += '#define ' + safename + '_len ' + source.contents.length + '\n';
output += 'const uint8_t ' + safename + '[] PROGMEM = {';
for (var i=0; i<source.contents.length; i++) {
if (i > 0) output += ',';
if (0 === (i % 20)) output += '\n';
output += '0x' + ('00' + source.contents[i].toString(16)).slice(-2);
}
wstream.write('0x' + ('00' + data[i].toString(16)).slice(-2));
if (i < (data.length - 1)) {
wstream.write(',');
output += '\n};';
// clone the contents
var destination = source.clone();
destination.path = source.path + '.h';
destination.contents = Buffer.from(output);
if (debug) {
console.info('Image ' + filename + ' \tsize: ' + source.contents.length + ' bytes');
}
}
wstream.write('\n};');
wstream.end();
callback(null, destination);
});
};
var htmllintReporter = function(filepath, issues) {
if (issues.length > 0) {
issues.forEach(function (issue) {
log.info(
if (issues.length > 0) {
issues.forEach(function (issue) {
console.info(
'[gulp-htmllint] ' +
filepath + ' [' +
issue.line + ',' +
@ -85,28 +104,24 @@ var htmllintReporter = function(filepath, issues) {
'(' + issue.code + ') ' +
issue.msg
);
});
process.exitCode = 1;
}
});
process.exitCode = 1;
}
};
gulp.task('build_certs', function() {
toHeader('server.cer');
toHeader('server.key');
});
gulp.task('csslint', function() {
gulp.src('html/*.css').
pipe(csslint({ids: false})).
pipe(csslint.formatter());
});
var buildWebUI = function(module) {
gulp.task('buildfs_embeded', ['buildfs_inline'], function() {
toHeader('index.html.gz');
});
var modules = {'light': false, 'sensor': false, 'rfbridge': false, 'rfm69': false};
if ('all' === module) {
modules['light'] = true;
modules['sensor'] = true;
modules['rfbridge'] = false; // we will never be adding this except when building RFBRIDGE
modules['rfm69'] = false; // we will never be adding this except when building RFM69GW
} else if ('small' !== module) {
modules[module] = true;
}
gulp.task('buildfs_inline', function() {
return gulp.src('html/*.html').
return gulp.src(htmlFolder + '*.html').
pipe(htmllint({
'failOnError': true,
'rules': {
@ -116,11 +131,12 @@ gulp.task('buildfs_inline', function() {
}, htmllintReporter)).
pipe(favicon()).
pipe(inline({
base: 'html/',
js: [uglify],
base: htmlFolder,
js: [],
css: [crass, inlineImages],
disabledTypes: ['svg', 'img']
})).
pipe(remover(modules)).
pipe(htmlmin({
collapseWhitespace: true,
removeComments: true,
@ -129,8 +145,62 @@ gulp.task('buildfs_inline', function() {
})).
pipe(replace('pure-', 'p-')).
pipe(gzip()).
pipe(gulp.dest(dataFolder));
pipe(rename('index.' + module + '.html.gz')).
pipe(gulp.dest(dataFolder)).
pipe(toHeader('webui_image', true)).
pipe(gulp.dest(staticFolder));
};
// -----------------------------------------------------------------------------
// Tasks
// -----------------------------------------------------------------------------
gulp.task('certs', function() {
gulp.src(dataFolder + 'server.*').
pipe(toHeader(debug=false)).
pipe(gulp.dest(staticFolder));
});
gulp.task('csslint', function() {
gulp.src(htmlFolder + '*.css').
pipe(csslint({ids: false})).
pipe(csslint.formatter());
});
gulp.task('webui_small', function() {
return buildWebUI('small');
});
gulp.task('webui_sensor', function() {
return buildWebUI('sensor');
});
gulp.task('webui_light', function() {
return buildWebUI('light');
});
gulp.task('webui_rfbridge', function() {
return buildWebUI('rfbridge');
});
gulp.task('webui_rfm69', function() {
return buildWebUI('rfm69');
});
gulp.task('webui_all', function() {
return buildWebUI('all');
});
gulp.task('webui', function(cb) {
runSequence([
'webui_small',
'webui_sensor',
'webui_light',
'webui_rfbridge',
'webui_rfm69',
'webui_all'
], cb);
});
gulp.task('default', ['buildfs_embeded']);
gulp.task('default', ['webui']);

+ 131
- 2
code/html/custom.css View File

@ -147,8 +147,8 @@ div.state {
color: white;
letter-spacing: 0;
margin-bottom: 10px;
padding: 8px 8px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
padding: 8px 8px;
}
.main-buttons {
@ -158,15 +158,22 @@ div.state {
.main-buttons button {
width: 100px;
}
.button-del-schedule {
margin-top: 15px;
}
.button-reboot,
.button-reconnect,
.button-ha-del,
.button-rfb-forget,
.button-del-network,
.button-del-mapping,
.button-del-schedule,
.button-dbg-clear,
.button-upgrade,
.button-clear-filters,
.button-clear-messages,
.button-clear-counts,
.button-settings-factory {
background: rgb(192, 0, 0); /* redish */
}
@ -174,8 +181,9 @@ div.state {
.button-update,
.button-update-password,
.button-add-network,
.button-rfb-learn,
.button-add-mapping,
.button-upgrade-browse,
.button-rfb-learn,
.button-ha-add,
.button-ha-config,
.button-settings-backup,
@ -199,6 +207,9 @@ div.state {
}
.button-upgrade-browse,
.button-clear-filters,
.button-clear-messages,
.button-clear-counts,
.button-dbgcmd,
.button-ha-add,
.button-apikey,
@ -221,6 +232,104 @@ span.slider {
margin-top: 7px;
}
/* -----------------------------------------------------------------------------
Checkboxes
-------------------------------------------------------------------------- */
.toggleWrapper {
overflow: hidden;
width: auto;
height: 30px;
margin: 0px 0px 10px 0px;
padding: 0px;
border-radius: 4px;
box-shadow: inset 1px 1px #CCC;
}
.toggleWrapper input {
position: absolute;
left: -99em;
}
label[for].toggle {
margin: 0px;
padding: 0px;
}
.toggle {
letter-spacing:normal;
cursor: pointer;
display: inline-block;
position: relative;
width: 130px;
height: 100%;
background: #e9e9e9;
color: #a9a9a9;
border-radius: 4px;
-webkit-transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.toggle:before,
.toggle:after {
position: absolute;
line-height: 30px;
font-size: .7em;
z-index: 2;
-webkit-transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.toggle:before {
content: "NO";
left: 20px;
}
input[name="relay"] + .toggle:before {
content: "OFF";
}
.toggle:after{
content: "YES";
right: 20px;
}
input[name="relay"] + .toggle:after {
content: "ON";
}
.toggle__handler {
display: inline-block;
position: relative;
z-index: 1;
background: #c00000;
width: 50%;
height: 100%;
border-radius: 4px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
top: 0px;
left: 0px;
-webkit-transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
-webkit-transform: translateX(0px);
transform: translateX(0px);
}
input:checked + .toggle:after {
color: #fff;
}
input:checked + .toggle:before {
color: #a9a9a9;
}
input + .toggle:before {
color: #fff;
}
input:checked + .toggle .toggle__handler {
width: 50%;
background: #00c000;
-webkit-transform: translateX(65px);
transform: translateX(65px);
border-color: #000;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
input[disabled] + .toggle .toggle__handler {
background: #ccc;
}
/* -----------------------------------------------------------------------------
Loading
-------------------------------------------------------------------------- */
@ -311,6 +420,26 @@ span.slider {
padding: 10px;
}
/* -----------------------------------------------------------------------------
Table
-------------------------------------------------------------------------- */
.right {
text-align: right;
}
table.dataTable.display tbody td {
text-align: center;
}
#packets_filter {
display: none;
}
.filtered {
color: rgb(202, 60, 60);
}
/* -----------------------------------------------------------------------------
Logs
-------------------------------------------------------------------------- */


+ 265
- 60
code/html/custom.js View File

@ -18,6 +18,11 @@ var useCCT = false;
var now = 0;
var ago = 0;
<!-- removeIf(!rfm69)-->
var packets;
var filters = [];
<!-- endRemoveIf(!rfm69)-->
// -----------------------------------------------------------------------------
// Messages
// -----------------------------------------------------------------------------
@ -34,13 +39,14 @@ function initMessages() {
messages[10] = "Session expired, please reload page...";
}
<!-- removeIf(!sensor)-->
function sensorName(id) {
var names = [
"DHT", "Dallas", "Emon Analog", "Emon ADC121", "Emon ADS1X15",
"HLW8012", "V9261F", "ECH1560", "Analog", "Digital",
"Events", "PMSX003", "BMX280", "MHZ19", "SI7021",
"SHT3X I2C", "BH1750", "PZEM004T", "AM2320 I2C", "GUVAS12SD",
"TMP3X", "HC-SR04", "SenseAir"
"TMP3X", "Sonar", "SenseAir", "GeigerTicks", "GeigerCPM"
];
if (1 <= id && id <= names.length) {
return names[id - 1];
@ -53,8 +59,10 @@ function magnitudeType(type) {
"Temperature", "Humidity", "Pressure",
"Current", "Voltage", "Active Power", "Apparent Power",
"Reactive Power", "Power Factor", "Energy", "Energy (delta)",
"Analog", "Digital", "Events",
"PM1.0", "PM2.5", "PM10", "CO2", "Lux", "UV", "Distance" , "HCHO"
"Analog", "Digital", "Event",
"PM1.0", "PM2.5", "PM10", "CO2", "Lux", "UV", "Distance" , "HCHO",
"Local Dose Rate", "Local Dose Rate",
"Count"
];
if (1 <= type && type <= types.length) {
return types[type - 1];
@ -72,6 +80,7 @@ function magnitudeError(error) {
}
return "Error " + error;
}
<!-- endRemoveIf(!sensor)-->
// -----------------------------------------------------------------------------
// Utils
@ -168,8 +177,13 @@ function validateForm(form) {
var re_hostname = new RegExp('^(?!-)[A-Za-z0-9-]{0,31}[A-Za-z0-9]$');
var hostname = $("input[name='hostname']", form).val();
if (!re_hostname.test(hostname)) {
var hostname = $("input[name='hostname']", form);
var hasChanged = hostname.attr("hasChanged") || 0;
if (0 === hasChanged) {
return true;
}
if (!re_hostname.test(hostname.val())) {
alert("Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'), the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen.");
return false;
}
@ -203,7 +217,8 @@ function addValue(data, name, value) {
"dczRelayIdx", "dczMagnitude",
"tspkRelay", "tspkMagnitude",
"ledMode",
"adminPass"
"adminPass",
"node", "key", "topic"
];
if (name in data) {
@ -306,11 +321,17 @@ function checkFirmware(file, callback) {
reader.onloadend = function(evt) {
if (FileReader.DONE === evt.target.readyState) {
callback(0xE9 === evt.target.result.charCodeAt(0));
if (0xE9 !== evt.target.result.charCodeAt(0)) callback(false);
if (0x03 !== evt.target.result.charCodeAt(2)) {
var response = window.confirm("Binary image is not using DOUT flash mode. This might cause resets in some devices. Press OK to continue.");
callback(response);
} else {
callback(true);
}
}
};
var blob = file.slice(0, 1);
var blob = file.slice(0, 3);
reader.readAsBinaryString(blob);
}
@ -451,12 +472,8 @@ function doUpdate() {
// Empty special fields
$(".pwrExpected").val(0);
$("input[name='pwrResetCalibration']").
prop("checked", false).
iphoneStyle("refresh");
$("input[name='pwrResetE']").
prop("checked", false).
iphoneStyle("refresh");
$("input[name='pwrResetCalibration']").prop("checked", false);
$("input[name='pwrResetE']").prop("checked", false);
// Change handling
numChanged = 0;
@ -510,7 +527,7 @@ function onFileUpload(event) {
if (data) {
sendAction("restore", data);
} else {
alert(messages[4]);
window.alert(messages[4]);
}
};
reader.readAsText(inputFile);
@ -533,13 +550,12 @@ function doFactoryReset() {
if (response === false) {
return false;
}
websock.send(JSON.stringify({"action": "factory_reset"}));
sendAction("factory_reset", {});
doReload(5000);
return false;
}
function doToggle(element, value) {
var id = parseInt(element.attr("data"), 10);
function doToggle(id, value) {
sendAction("relay", {id: id, status: value ? 1 : 0 });
return false;
}
@ -570,6 +586,56 @@ function doDebugClear() {
return false;
}
<!-- removeIf(!rfm69)-->
function doClearCounts() {
sendAction("clear-counts", {});
return false;
}
function doClearMessages() {
packets.clear().draw(false);
return false;
}
function doFilter(e) {
var index = packets.cell(this).index();
if (index == 'undefined') return;
var c = index.column;
var column = packets.column(c);
if (filters[c]) {
filters[c] = false;
column.search("");
$(column.header()).removeClass("filtered");
} else {
filters[c] = true;
var data = packets.row(this).data();
if (e.which == 1) {
column.search('^' + data[c] + '$', true, false );
} else {
column.search('^((?!(' + data[c] + ')).)*$', true, false );
}
$(column.header()).addClass("filtered");
}
column.draw();
return false;
}
function doClearFilters() {
for (var i = 0; i < packets.columns()[0].length; i++) {
if (filters[i]) {
filters[i] = false;
var column = packets.column(i);
column.search("");
$(column.header()).removeClass("filtered");
column.draw();
}
}
return false;
}
<!-- endRemoveIf(!rfm69)-->
// -----------------------------------------------------------------------------
// Visualization
// -----------------------------------------------------------------------------
@ -583,10 +649,7 @@ function toggleMenu() {
function showPanel() {
$(".panel").hide();
if ($("#layout").hasClass("active")) { toggleMenu(); }
$("#" + $(this).attr("data")).show().
find("input[type='checkbox']").
iphoneStyle("calculateDimensions").
iphoneStyle("refresh");
$("#" + $(this).attr("data")).show();
}
// -----------------------------------------------------------------------------
@ -608,6 +671,7 @@ function createRelayList(data, container, template_name) {
}
<!-- removeIf(!sensor)-->
function createMagnitudeList(data, container, template_name) {
var current = $("#" + container + " > div").length;
@ -624,6 +688,29 @@ function createMagnitudeList(data, container, template_name) {
}
}
<!-- endRemoveIf(!sensor)-->
// -----------------------------------------------------------------------------
// RFM69
// -----------------------------------------------------------------------------
<!-- removeIf(!rfm69)-->
function addMapping() {
var template = $("#nodeTemplate .pure-g")[0];
var line = $(template).clone();
var tabindex = $("#mapping > div").length * 3 + 50;
$(line).find("input").each(function() {
$(this).attr("tabindex", tabindex++);
});
$(line).find("button").on('click', delMapping);
line.appendTo("#mapping");
}
function delMapping() {
var parent = $(this).parent().parent();
$(parent).remove();
}
<!-- endRemoveIf(!rfm69)-->
// -----------------------------------------------------------------------------
// Wifi
@ -665,6 +752,7 @@ function addNetwork() {
// -----------------------------------------------------------------------------
// Relays scheduler
// -----------------------------------------------------------------------------
function delSchedule() {
var parent = $(this).parents(".pure-g");
$(parent).remove();
@ -676,6 +764,7 @@ function moreSchedule() {
}
function addSchedule(event) {
var numSchedules = $("#schedules > div").length;
if (numSchedules >= maxSchedules) {
alert("Max number of schedules reached");
@ -697,14 +786,15 @@ function addSchedule(event) {
});
$(line).find(".button-del-schedule").on("click", delSchedule);
$(line).find(".button-more-schedule").on("click", moreSchedule);
$(line).find("input[name='schUTC']").prop("id", "schUTC" + (numSchedules + 1))
.next().prop("for", "schUTC" + (numSchedules + 1));
$(line).find("input[name='schEnabled']").prop("id", "schEnabled" + (numSchedules + 1))
.next().prop("for", "schEnabled" + (numSchedules + 1));
line.appendTo("#schedules");
$(line).find("input[type='checkbox']").
prop("checked", false).
iphoneStyle("calculateDimensions").
iphoneStyle("refresh");
$(line).find("input[type='checkbox']").prop("checked", false);
return line;
}
// -----------------------------------------------------------------------------
@ -722,15 +812,15 @@ function initRelays(data) {
// Add relay fields
var line = $(template).clone();
$(".id", line).html(i);
$("input", line).attr("data", i);
$(":checkbox", line).prop('checked', data[i]).attr("data", i)
.prop("id", "relay" + i)
.on("change", function (event) {
var id = parseInt($(event.target).attr("data"), 10);
var status = $(event.target).prop("checked");
doToggle(id, status);
});
$("label.toggle", line).prop("for", "relay" + i)
line.appendTo("#relays");
$("input[type='checkbox']", line).iphoneStyle({
onChange: doToggle,
resizeContainer: true,
resizeHandle: true,
checkedLabel: "ON",
uncheckedLabel: "OFF"
});
// Populate the relay SELECTs
$("select.isrelay").append(
@ -738,6 +828,17 @@ function initRelays(data) {
}
}
function createCheckboxes() {
$("input[type='checkbox']").each(function() {
if($(this).prop("name"))$(this).prop("id", $(this).prop("name"));
$(this).parent().addClass("toggleWrapper");
$(this).after('<label for="' + $(this).prop("name") + '" class="toggle"><span class="toggle__handler"></span></label>')
});
}
@ -767,6 +868,7 @@ function initRelayConfig(data) {
// Sensors & Magnitudes
// -----------------------------------------------------------------------------
<!-- removeIf(!sensor)-->
function initMagnitudes(data) {
// check if already initialized
@ -785,11 +887,14 @@ function initMagnitudes(data) {
}
}
<!-- endRemoveIf(!sensor)-->
// -----------------------------------------------------------------------------
// Lights
// -----------------------------------------------------------------------------
<!-- removeIf(!light)-->
function initColorRGB() {
// check if already initialized
@ -908,11 +1013,14 @@ function initChannels(num) {
}
}
<!-- endRemoveIf(!light)-->
// -----------------------------------------------------------------------------
// RFBridge
// -----------------------------------------------------------------------------
<!-- removeIf(!rfbridge)-->
function rfbLearn() {
var parent = $(this).parents(".pure-g");
var input = $("input", parent);
@ -951,6 +1059,7 @@ function addRfbNode() {
return line;
}
<!-- endRemoveIf(!rfbridge)-->
// -----------------------------------------------------------------------------
// Processing
@ -999,6 +1108,8 @@ function processData(data) {
// RFBridge
// ---------------------------------------------------------------------
<!-- removeIf(!rfbridge)-->
if ("rfbCount" === key) {
for (i=0; i<data.rfbCount; i++) { addRfbNode(); }
return;
@ -1016,11 +1127,58 @@ function processData(data) {
}
return;
}
<!-- endRemoveIf(!rfbridge)-->
// ---------------------------------------------------------------------
// RFM69
// ---------------------------------------------------------------------
<!-- removeIf(!rfm69)-->
if (key == "packet") {
var packet = data.packet;
var d = new Date();
packets.row.add([
d.toLocaleTimeString('en-US', { hour12: false }),
packet.senderID,
packet.packetID,
packet.targetID,
packet.key,
packet.value,
packet.rssi,
packet.duplicates,
packet.missing,
]).draw(false);
return;
}
if (key == "mapping") {
for (var i in data.mapping) {
// add a new row
addMapping();
// get group
var line = $("#mapping .pure-g")[i];
// fill in the blanks
var mapping = data.mapping[i];
Object.keys(mapping).forEach(function(key) {
var id = "input[name=" + key + "]";
if ($(id, line).length) $(id, line).val(mapping[key]).attr("original", mapping[key]);
});
}
return;
}
<!-- endRemoveIf(!rfm69)-->
// ---------------------------------------------------------------------
// Lights
// ---------------------------------------------------------------------
<!-- removeIf(!light)-->
if ("rgb" === key) {
initColorRGB();
$("input[name='color']").wheelColorPicker("setValue", value, true);
@ -1071,10 +1229,14 @@ function processData(data) {
useCCT = value;
}
<!-- endRemoveIf(!light)-->
// ---------------------------------------------------------------------
// Sensors & Magnitudes
// ---------------------------------------------------------------------
<!-- removeIf(!sensor)-->
if ("magnitudes" === key) {
initMagnitudes(value);
for (i in value) {
@ -1090,6 +1252,8 @@ function processData(data) {
return;
}
<!-- endRemoveIf(!sensor)-->
// ---------------------------------------------------------------------
// WiFi
// ---------------------------------------------------------------------
@ -1141,9 +1305,7 @@ function processData(data) {
var sch_value = schedule[key];
$("input[name='" + key + "']", sch_line).val(sch_value);
$("select[name='" + key + "']", sch_line).prop("value", sch_value);
$("input[type='checkbox'][name='" + key + "']", sch_line).
prop("checked", sch_value).
iphoneStyle("refresh");
$("input[type='checkbox'][name='" + key + "']", sch_line).prop("checked", sch_value);
});
}
return;
@ -1156,12 +1318,7 @@ function processData(data) {
if ("relayStatus" === key) {
initRelays(value);
for (i in value) {
// Set the status for each relay
$("input.relayStatus[data='" + i + "']").
prop("checked", value[i]).
iphoneStyle("refresh");
$("input[name='relay'][data='" + i + "']").prop("checked", value[i]);
}
return;
}
@ -1183,10 +1340,12 @@ function processData(data) {
}
// Domoticz - Magnitudes
<!-- removeIf(!sensor)-->
if ("dczMagnitudes" === key) {
createMagnitudeList(value, "dczMagnitudes", "dczMagnitudeTemplate");
return;
}
<!-- endRemoveIf(!sensor)-->
// ---------------------------------------------------------------------
// Thingspeak
@ -1199,10 +1358,12 @@ function processData(data) {
}
// Thingspeak - Magnitudes
<!-- removeIf(!sensor)-->
if ("tspkMagnitudes" === key) {
createMagnitudeList(value, "tspkMagnitudes", "tspkMagnitudeTemplate");
return;
}
<!-- endRemoveIf(!sensor)-->
// ---------------------------------------------------------------------
// General
@ -1216,7 +1377,7 @@ function processData(data) {
// Web log
if ("weblog" === key) {
$("#weblog").append(value);
$("#weblog").append(new Text(value));
$("#weblog").scrollTop($("#weblog")[0].scrollHeight - $("#weblog").height());
return;
}
@ -1231,7 +1392,7 @@ function processData(data) {
if ("deviceip" === key) {
var a_href = $("span[name='" + key + "']").parent();
a_href.attr("href", "http://" + value);
a_href.attr("href", "//" + value);
a_href.next().attr("href", "telnet://" + value);
}
@ -1272,9 +1433,7 @@ function processData(data) {
var input = $("input[name='" + key + "']");
if (input.length > 0) {
if (input.attr("type") === "checkbox") {
input.
prop("checked", value).
iphoneStyle("refresh");
input.prop("checked", value);
} else if (input.attr("type") === "radio") {
input.val([value]);
} else {
@ -1351,27 +1510,44 @@ function hasChanged() {
function initUrls(root) {
var paths = ["ws", "upgrade", "config"];
var paths = ["ws", "upgrade", "config", "auth"];
urls["root"] = root;
paths.forEach(function(path) {
urls[path] = new URL(path, root);
urls[path].protocol = root.protocol;
});
urls.ws.protocol = "ws";
if (root.protocol == "https:") {
urls.ws.protocol = "wss:";
} else {
urls.ws.protocol = "ws:";
}
}
function connectToURL(url) {
initUrls(url);
if (websock) { websock.close(); }
websock = new WebSocket(urls.ws.href);
websock.onmessage = function(evt) {
var data = getJson(evt.data.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"));
if (data) {
processData(data);
}
};
$.ajax({
'method': 'GET',
'crossDomain': true,
'url': urls.auth.href,
'xhrFields': { 'withCredentials': true }
}).done(function(data) {
if (websock) { websock.close(); }
websock = new WebSocket(urls.ws.href);
websock.onmessage = function(evt) {
var data = getJson(evt.data.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"));
if (data) {
processData(data);
}
};
}).fail(function() {
// Nothing to do, reload page and retry
});
}
function connect(host) {
@ -1385,10 +1561,16 @@ function connectToCurrentURL() {
connectToURL(new URL(window.location));
}
function getParameterByName(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
$(function() {
initMessages();
loadTimeZones();
createCheckboxes();
setInterval(function() { keepTime(); }, 1000);
$("#menuLink").on("click", toggleMenu);
@ -1423,13 +1605,36 @@ $(function() {
$(".more", addNetwork()).toggle();
});
$(".button-add-switch-schedule").on("click", { schType: 1 }, addSchedule);
<!-- removeIf(!light)-->
$(".button-add-light-schedule").on("click", { schType: 2 }, addSchedule);
<!-- endRemoveIf(!light)-->
<!-- removeIf(!rfm69)-->
$(".button-add-mapping").on('click', addMapping);
$(".button-del-mapping").on('click', delMapping);
$(".button-clear-counts").on('click', doClearCounts);
$(".button-clear-messages").on('click', doClearMessages);
$(".button-clear-filters").on('click', doClearFilters);
$('#packets tbody').on('mousedown', 'td', doFilter);
packets = $('#packets').DataTable({
"paging": false
});
for (var i = 0; i < packets.columns()[0].length; i++) {
filters[i] = false;
}
<!-- endRemoveIf(!rfm69)-->
$(document).on("change", "input", hasChanged);
$(document).on("change", "select", hasChanged);
// don't autoconnect when opening from filesystem
if (window.location.protocol === "file:") { return; }
connectToCurrentURL();
// Check host param in query string
if (host = getParameterByName('host')) {
connect(host);
} else {
connectToCurrentURL();
}
});

+ 165
- 18
code/html/index.html View File

@ -11,8 +11,12 @@
<link rel="stylesheet" href="vendor/pure-1.0.0.min.css" />
<link rel="stylesheet" href="vendor/pure-grids-responsive-1.0.0.min.css" />
<link rel="stylesheet" href="vendor/side-menu.css" />
<link rel="stylesheet" href="vendor/checkboxes.css" />
<!-- removeIf(!light) -->
<link rel="stylesheet" href="vendor/jquery.wheelcolorpicker-3.0.3.css" />
<!-- endRemoveIf(!light) -->
<!-- removeIf(!rfm69) -->
<link rel="stylesheet" href="vendor/datatables-1.10.16.css" />
<!-- endRemoveIf(!rfm69) -->
<link rel="stylesheet" href="custom.css" />
<!-- endbuild -->
@ -98,9 +102,21 @@
<a href="#" class="pure-menu-link" data="panel-idb">INFLUXDB</a>
</li>
<!-- removeIf(!light) -->
<li class="pure-menu-item module module-color">
<a href="#" class="pure-menu-link" data="panel-color">LIGHTS</a>
</li>
<!-- endRemoveIf(!light) -->
<!-- removeIf(!rfm69) -->
<li class="pure-menu-item module module-rfm69">
<a href="#" class="pure-menu-link" data="panel-mapping">MAPPING</a>
</li>
<li class="pure-menu-item module module-rfm69">
<a href="#" class="pure-menu-link" data="panel-messages">MESSAGES</a>
</li>
<!-- endRemoveIf(!rfm69) -->
<li class="pure-menu-item module module-mqtt">
<a href="#" class="pure-menu-link" data="panel-mqtt">MQTT</a>
@ -110,17 +126,21 @@
<a href="#" class="pure-menu-link" data="panel-ntp">NTP</a>
</li>
<!-- removeIf(!rfbridge) -->
<li class="pure-menu-item module module-rfb">
<a href="#" class="pure-menu-link" data="panel-rfb">RF</a>
</li>
<!-- endRemoveIf(!rfbridge) -->
<li class="pure-menu-item module module-sch">
<a href="#" class="pure-menu-link" data="panel-schedule">SCHEDULE</a>
</li>
<!-- removeIf(!sensor) -->
<li class="pure-menu-item module module-sensors">
<a href="#" class="pure-menu-link" data="panel-sensors">SENSORS</a>
</li>
<!-- endRemoveIf(!sensor) -->
<li class="pure-menu-item module module-relay">
<a href="#" class="pure-menu-link" data="panel-relay">SWITCHES</a>
@ -177,13 +197,27 @@
<div id="relays"></div>
<!-- removeIf(!light) -->
<div id="colors"></div>
<div id="cct"></div>
<div id="channels"></div>
<!-- endRemoveIf(!light) -->
<!-- removeIf(!sensor) -->
<div id="magnitudes"></div>
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!rfm69) -->
<div class="pure-g module module-rfm69">
<label class="pure-u-1 pure-u-lg-1-4">Packet count</label>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="text" name="packetCount" readonly /></div>
</div>
<div class="pure-g module module-rfm69">
<label class="pure-u-1 pure-u-lg-1-4">Node count</label>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="text" name="nodeCount" readonly /></div>
</div>
<!-- endRemoveIf(!rfm69) -->
<div class="pure-u-1 pure-u-lg-1-2 state">
@ -211,10 +245,8 @@
<div class="pure-u-1-2">Firmware version</div>
<div class="pure-u-11-24"><span class="right" name="app_version"></span></div>
<!--
<div class="pure-u-1-2">Firmware revision</div>
<div class="pure-u-11-24"><span class="right" name="app_revision"></span></div>
-->
<div class="pure-u-1-2">Firmware build date</div>
<div class="pure-u-11-24"><span class="right" name="app_build"></span></div>
@ -343,7 +375,7 @@
<div class="pure-g module module-alexa">
<label class="pure-u-1 pure-u-lg-1-4">Alexa integration</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="alexaEnabled" tabindex="13" /></div>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="alexaEnabled" /></div>
</div>
</fieldset>
@ -382,6 +414,7 @@
</div>
</div>
<!-- removeIf(!light) -->
<div class="panel" id="panel-color">
<div class="header">
@ -467,6 +500,7 @@
</fieldset>
</div>
</div>
<!-- endRemoveIf(!light) -->
<div class="panel" id="panel-admin">
@ -516,12 +550,12 @@
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="wsAuth" /></div>
</div>
<div class="pure-g">
<div class="pure-g module module-api">
<label class="pure-u-1 pure-u-lg-1-4">Enable HTTP API</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="apiEnabled" /></div>
</div>
<div class="pure-g">
<div class="pure-g module module-api">
<label class="pure-u-1 pure-u-lg-1-4">Real time API</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="apiRealTime" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
@ -532,7 +566,7 @@
</div>
</div>
<div class="pure-g">
<div class="pure-g module module-api">
<label class="pure-u-1 pure-u-lg-1-4">HTTP API Key</label>
<input name="apiKey" class="pure-u-3-4 pure-u-lg-1-2" type="text" tabindex="14" />
<div class=" pure-u-1-4 pure-u-lg-1-4"><button class="pure-button button-apikey pure-u-23-24">Auto</button></div>
@ -603,7 +637,6 @@
ESPurna will scan for visible WiFi SSIDs and try to connect to networks defined below in order of <strong>signal strength</strong>, even if multiple AP share the same SSID.
When disabled, ESPurna will try to connect to the networks in the same order they are listed below.
Disable this option if you are <strong>connecting to a single access point</strong> (or router) or to a <strong>hidden SSID</strong>.
</thead>
</div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<button class="pure-button button-wifi-scan" type="button">Scan now</button>
@ -638,13 +671,88 @@
<div id="schedules"></div>
<button type="button" class="pure-button button-add-switch-schedule module module-relay">Add switch schedule</button>
<!-- removeIf(!light) -->
<button type="button" class="pure-button button-add-light-schedule module module-color">Add channel schedule</button>
<!-- endRemoveIf(!light) -->
</fieldset>
</div>
</div>
<!-- removeIf(!rfm69) -->
<div class="panel" id="panel-mapping">
<div class="header">
<h1>MAPPING</h1>
<h2>
Configure the map between nodeID/key and MQTT topic. Messages from the given nodeID with the given key will be forwarded to the specified topic.
You can also configure a default topic using {nodeid} and {key} as placeholders, if the default topic is empty messages without defined map will be discarded.
</h2>
</div>
<div class="page">
<fieldset>
<legend>Default topic</legend>
<div class="pure-g">
<input name="rfm69Topic" type="text" class="pure-u-23-24" value="" size="8" tabindex="41" placeholder="Default MQTT Topic (use {nodeid} and {key} as placeholders)">
</div>
<legend>Specific topics</legend>
<div id="mapping"></div>
<button type="button" class="pure-button button-add-mapping">Add</button>
</fieldset>
</div>
</div>
<div class="panel" id="panel-messages">
<div class="header">
<h1>MESSAGES</h1>
<h2>
Messages being received. Previous messages are not displayed.
You have to keep the page open in order to keep receiving them.
You can filter/unfilter by clicking on the values.
Left click on a value to show only rows that match that value, middle click to show all rows but those matching that value.
Filtered colums have red headers.
</h2>
</div>
<div class="page">
<table id="packets" class="display" cellspacing="0">
<thead>
<tr>
<th>Timestamp</th>
<th>SenderID</th>
<th>PacketID</th>
<th>TargetID</th>
<th>Key</th>
<th>Value</th>
<th>RSSI</th>
<th>Duplicates</th>
<th>Missing</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button class="pure-button button-clear-filters">Clear filters</button>
<button class="pure-button button-clear-messages">Clear messages</button>
<button class="pure-button button-clear-counts">Clear counts</button>
</div>
</div>
<!-- endRemoveIf(!rfm69) -->
<div class="panel" id="panel-mqtt">
@ -735,11 +843,13 @@
<div class="pure-u-1 pure-u-lg-3-4 hint">
This is the root topic for this device. The {hostname} and {mac} placeholders will be replaced by the device hostname and MAC address.<br />
- <strong>&lt;root&gt;/relay/#/set</strong> Send a 0 or a 1 as a payload to this topic to switch it on or off. You can also send a 2 to toggle its current state. Replace # with the switch ID (starting from 0). If the board has only one switch it will be 0.<br />
<!-- removeIf(!light) -->
<span class="module module-color">- <strong>&lt;root&gt;/rgb/set</strong> Set the color using this topic, your can either send an "#RRGGBB" value or "RRR,GGG,BBB" (0-255 each).<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/hsv/set</strong> Set the color using hue (0-360), saturation (0-100) and value (0-100) values, comma separated.<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/brightness/set</strong> Set the brighness (0-255).<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/channel/#/set</strong> Set the value for a single color channel (0-255). Replace # with the channel ID (starting from 0 and up to 4 for RGBWC lights).<br /></span>
<span class="module module-color">- <strong>&lt;root&gt;/mired/set</strong> Set the temperature color in mired.<br /></span>
<!-- endRemoveIf(!light) -->
- <strong>&lt;root&gt;/status</strong> The device will report a 1 to this topic every few minutes. Upon MQTT disconnecting this will be set to 0.<br />
- Other values reported (depending on the build) are: <strong>firmware</strong> and <strong>version</strong>, <strong>hostname</strong>, <strong>IP</strong>, <strong>MAC</strong>, signal strenth (<strong>RSSI</strong>), <strong>uptime</strong> (in seconds), <strong>free heap</strong> and <strong>power supply</strong>.
</div>
@ -747,7 +857,8 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Use JSON payload</label>
<div class="pure-u-1 pure-u-lg-3-4"><input type="checkbox" name="mqttUseJson" tabindex="32" /></div>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="mqttUseJson" tabindex="32" /></div>
<div class="pure-u-1 pure-u-lg-1-2"></div>
<div class="pure-u-1 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
All messages (except the device status) will be included in a JSON payload along with the timestamp and hostname
@ -844,7 +955,9 @@
<div id="dczRelays"></div>
<!-- removeIf(!sensor) -->
<div id="dczMagnitudes"></div>
<!-- endRemoveIf(!sensor) -->
</fieldset>
</div>
@ -872,7 +985,9 @@
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
Home Assistant auto-discovery feature. Enable and save to add the device to your HA console.
<!-- removeIf(!light) -->
When using a colour light you might want to disable CSS style so Home Assistant can parse the color.
<!-- endRemoveIf(!light) -->
</div>
</div>
@ -903,8 +1018,6 @@
</div>
<div class="panel" id="panel-thingspeak">
<div class="header">
@ -938,7 +1051,9 @@
<div id="tspkRelays"></div>
<!-- removeIf(!sensor) -->
<div id="tspkMagnitudes"></div>
<!-- endRemoveIf(!sensor) -->
</fieldset>
</div>
@ -1025,6 +1140,7 @@
</div>
<!-- removeIf(!sensor) -->
<div class="panel" id="panel-sensors">
<div class="header">
@ -1075,7 +1191,7 @@
</select>
</div>
<div class="pure-g module module-hlw module-cse module-emon">
<div class="pure-g module module-hlw module-cse module-emon module-pzem">
<label class="pure-u-1 pure-u-lg-1-4">Energy units</label>
<select name="energyUnits" tabindex="16" class="pure-u-1 pure-u-lg-1-4">
<option value="0">Joules (J)</option>
@ -1161,7 +1277,9 @@
</div>
</div>
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!rfbridge) -->
<div class="panel" id="panel-rfb">
<div class="header">
@ -1182,6 +1300,7 @@
</fieldset>
</div>
</div>
<!-- endRemoveIf(!rfbridge) -->
</form>
@ -1191,6 +1310,7 @@
<!-- Templates -->
<!-- removeIf(!rfbridge) -->
<div id="rfbNodeTemplate" class="template">
<legend>Switch #<span></span></legend>
@ -1212,6 +1332,7 @@
</div>
</div>
<!-- endRemoveIf(!rfbridge) -->
<div id="networkTemplate" class="template">
@ -1269,6 +1390,7 @@
<label class="pure-u-1 pure-u-lg-1-4">Use UTC time</label>
<div class="pure-u-1 pure-u-lg-3-4"><input type="checkbox" name="schUTC" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<label class="pure-u-1 pure-u-lg-1-4">And weekday is one of</label>
<div class="pure-u-2-5 pure-u-lg-1-5">
<input class="pure-u-23-24 pure-u-lg-23-24" name="schWDs" type="text" maxlength="15" tabindex="0" value="1,2,3,4,5,6,7" />
@ -1281,7 +1403,7 @@
<label class="pure-u-1 pure-u-lg-1-4">Enabled</label>
<div class="pure-u-1 pure-u-lg-3-4"><input type="checkbox" name="schEnabled" /></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-1-2"></div>
<button class="pure-button button-del-schedule" type="button">Delete schedule</button>
</div>
</div>
@ -1299,6 +1421,7 @@
<input type="hidden" name="schType" value="1">
</div>
<!-- removeIf(!light) -->
<div id="lightActionTemplate" class="template">
<label class="pure-u-1 pure-u-lg-1-4">Brightness</label>
<div class="pure-u-1 pure-u-lg-1-5">
@ -1307,11 +1430,12 @@
<select class="pure-u-1 pure-u-lg-1-5 islight" name="schSwitch"></select>
<input type="hidden" name="schType" value="2">
</div>
<!-- endRemoveIf(!light) -->
<div id="relayTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Switch #<span class="id"></span></label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" class="relayStatus pure-u-1 pure-u-lg-1-4" data="0" /></div>
<div><input name="relay" type="checkbox" on="ON" off="OFF" /></div>
</div>
</div>
@ -1336,7 +1460,7 @@
</div>
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-1-4"><label>Pulse time (s)</label></div>
<div class="pure-u-1 pure-u-lg-1-4"><input name="relayTime" class="pure-u-1" type="number" min="0" step="0.1" max="86400" /></div>
<div class="pure-u-1 pure-u-lg-1-4"><input name="relayTime" class="pure-u-1" type="number" min="0" step="0.1" max="3600" /></div>
</div>
<div class="pure-g module module-mqtt">
<div class="pure-u-1 pure-u-lg-1-4"><label>MQTT group</label></div>
@ -1366,6 +1490,7 @@
</div>
</div>
<!-- removeIf(!sensor) -->
<div id="dczMagnitudeTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Magnitude</label>
@ -1373,6 +1498,7 @@
<div class="pure-u-1 pure-u-lg-1-2 hint center"></div>
</div>
</div>
<!-- endRemoveIf(!sensor) -->
<div id="tspkRelayTemplate" class="template">
<div class="pure-g">
@ -1381,6 +1507,7 @@
</div>
</div>
<!-- removeIf(!sensor) -->
<div id="tspkMagnitudeTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Magnitude</label>
@ -1388,7 +1515,9 @@
<div class="pure-u-1 pure-u-lg-1-2 hint center"></div>
</div>
</div>
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!light) -->
<div id="colorRGBTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Color</label>
@ -1424,7 +1553,9 @@
<span class="slider mireds pure-u-lg-1-4"></span>
</div>
</div>
<!-- endRemoveIf(!light) -->
<!-- removeIf(!sensor) -->
<div id="magnitudeTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4"></label>
@ -1434,6 +1565,18 @@
<div class="pure-u-1 pure-u-lg-1-2 hint center"></div>
</div>
</div>
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!rfm69) -->
<div id="nodeTemplate" class="template">
<div class="pure-g">
<div class="pure-u-md-1-6 pure-u-1-2"><input name="node" type="text" class="pure-u-11-12" value="" size="8" tabindex="0" placeholder="Node ID"></div>
<div class="pure-u-md-1-6 pure-u-1-2"><input name="key" type="text" class="pure-u-11-12" value="" size="8" tabindex="0" placeholder="Key"></div>
<div class="pure-u-md-1-2 pure-u-3-4"><input name="topic" type="text" class="pure-md-11-12 pure-u-23-24" value="" size="8" tabindex="0" placeholder="MQTT Topic"></div>
<div class="pure-u-md-1-6 pure-u-1-4"><button type="button" class="pure-button button-del-mapping pure-u-5-6 pure-u-md-5-6">Del</button></div>
</div>
</div>
<!-- endRemoveIf(!rfm69) -->
<iframe id="downloader"></iframe>
<input id="uploader" type="file" />
@ -1443,8 +1586,12 @@
<!-- build:js script.js -->
<script src="vendor/jquery-3.2.1.min.js"></script>
<script src="custom.js"></script>
<script src="vendor/checkboxes.js"></script>
<!-- removeIf(!light) -->
<script src="vendor/jquery.wheelcolorpicker-3.0.3.min.js"></script>
<!-- endRemoveIf(!light) -->
<!-- removeIf(!rfm69) -->
<script src="vendor/datatables-1.10.16.min.js"></script>
<!-- endRemoveIf(!rfm69) -->
<!-- endbuild -->
</html>

+ 0
- 120
code/html/vendor/checkboxes.css View File

@ -1,120 +0,0 @@
.iPhoneCheckContainer {
-webkit-transform:translate3d(0,0,0);
position: relative;
height: 30px;
cursor: pointer;
overflow: hidden;
margin: 5px 0 10px 0;
}
.iPhoneCheckContainer input {
position: absolute;
top: 5px;
left: 30px;
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
opacity: 0;
}
.iPhoneCheckContainer label {
white-space: nowrap;
font-size: 17px;
line-height: 17px;
font-weight: bold;
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
cursor: pointer;
display: block;
height: 27px;
position: absolute;
width: auto;
top: 0;
padding-top: 5px;
overflow: hidden;
}
.iPhoneCheckContainer, .iPhoneCheckContainer label {
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
}
.iPhoneCheckDisabled {
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50);
opacity: 0.5;
}
div.iPhoneCheckBorderOn {
position: absolute;
left: 0;
width: 4px;
height: 100%;
background-image: url('images/border-on.png');
background-repeat: no-repeat;
}
label.iPhoneCheckLabelOn {
color: white;
background-image: url('images/label-on.png');
background-repeat: repeat-x;
text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.6);
left: 0;
padding-top: 5px;
margin-left: 4px;
margin-top: 0px;
}
label.iPhoneCheckLabelOn span {
padding-left: 4px;
}
label.iPhoneCheckLabelOff {
color: #8b8b8b;
background-image: url('images/label-off.png');
background-repeat: repeat-x;
text-shadow: 0px 0px 2px rgba(255, 255, 255, 0.6);
text-align: right;
margin-right: 4px;
margin-top: 0px;
right: 0;
}
label.iPhoneCheckLabelOff span {
padding-right: 4px;
}
div.iPhoneCheckBorderOff {
position: absolute;
width: 4px;
height: 100%;
background-image: url('images/border-off.png');
background-repeat: no-repeat;
}
.iPhoneCheckHandle {
display: block;
height: 27px;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
width: 0;
background-image: url('images/handle-left.png');
background-repeat: no-repeat;
padding-left: 4px;
}
.iPhoneCheckHandleCenter {
position: absolute;
width: 100%;
height: 100%;
background-image: url('images/handle-center.png');
background-repeat: repeat-x;
}
.iPhoneCheckHandleRight {
position: absolute;
height: 100%;
width: 4px;
right: 0px;
background-image: url('images/handle-right.png');
background-repeat: no-repeat;
}

+ 0
- 366
code/html/vendor/checkboxes.js View File

@ -1,366 +0,0 @@
// Generated by CoffeeScript 1.6.2
/*eslint quotes: ["error", "double"]*/
/*eslint-env es6*/
(function() {
var iOSCheckbox, matched, userAgent,
__slice = [].slice;
if ($.browser == null) {
userAgent = navigator.userAgent || "";
jQuery.uaMatch = function(ua) {
var match;
ua = ua.toLowerCase();
match = /(chrome)[ \/]([\w.]+)/.exec(ua) || /(webkit)[ \/]([\w.]+)/.exec(ua) || /(opera)(?:.*version)?[ \/]([\w.]+)/.exec(ua) || /(msie) ([\w.]+)/.exec(ua) || ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+))?/.exec(ua) || [];
return {
browser: match[1] || "",
version: match[2] || "0"
};
};
matched = jQuery.uaMatch(userAgent);
jQuery.browser = {};
if (matched.browser) {
jQuery.browser[matched.browser] = true;
jQuery.browser.version = matched.version;
}
if (jQuery.browser.webkit) {
jQuery.browser.safari = true;
}
}
iOSCheckbox = (function() {
function iOSCheckbox(elem, options) {
var key, opts, value;
this.elem = $(elem);
opts = $.extend({}, iOSCheckbox.defaults, options);
for (key in opts) {
if ({}.hasOwnProperty.call(opts, key)) {
value = opts[key];
this[key] = value;
}
}
this.elem.data(this.dataName, this);
this.wrapCheckboxWithDivs();
this.attachEvents();
this.disableTextSelection();
this.calculateDimensions();
}
iOSCheckbox.prototype.calculateDimensions = function() {
if (this.resizeHandle) {
this.optionallyResize("handle");
}
if (this.resizeContainer) {
this.optionallyResize("container");
}
return this.initialPosition();
};
iOSCheckbox.prototype.isDisabled = function() {
return this.elem.is(":disabled");
};
iOSCheckbox.prototype.wrapCheckboxWithDivs = function() {
this.elem.wrap("<div class='" + this.containerClass + "' />");
this.container = this.elem.parent();
this.offLabel = $("<label class='" + this.labelOffClass + "'>\n <span>" + this.uncheckedLabel + "</span>\n</label>").appendTo(this.container);
this.offSpan = this.offLabel.children("span");
this.onLabel = $("<label class='" + this.labelOnClass + "'>\n <span>" + this.checkedLabel + "</span>\n</label>").appendTo(this.container);
this.onBorder = $("<div class='iPhoneCheckBorderOn'</div>").appendTo(this.container);
this.offBorder = $("<div class='iPhoneCheckBorderOff'</div>").appendTo(this.container);
this.onSpan = this.onLabel.children("span");
this.handle = $("<div class='" + this.handleClass + "'></div>").appendTo(this.container);
this.handleCenter = $("<div class='" + this.handleCenterClass + "'></div>").appendTo(this.handle);
this.handleRight = $("<div class='" + this.handleRightClass + "'></div>").appendTo(this.handle);
return true;
};
iOSCheckbox.prototype.disableTextSelection = function() {
if ($.browser.msie) {
return $([this.handle, this.offLabel, this.onLabel, this.container]).attr("unselectable", "on");
}
};
iOSCheckbox.prototype._getDimension = function(elem, dimension) {
if ($.fn.actual != null) {
return elem.actual(dimension);
} else {
return elem[dimension]();
}
};
iOSCheckbox.prototype.optionallyResize = function(mode) {
var newWidth, offLabelWidth, offSpan, onLabelWidth, onSpan;
onSpan = this.onLabel.find("span");
onLabelWidth = this._getDimension(onSpan, "width");
onLabelWidth += parseInt(onSpan.css("padding-left"), 10);
offSpan = this.offLabel.find("span");
offLabelWidth = this._getDimension(offSpan, "width");
offLabelWidth += parseInt(offSpan.css("padding-right"), 10);
if (mode === "container") {
newWidth = onLabelWidth > offLabelWidth ? onLabelWidth : offLabelWidth;
newWidth += this._getDimension(this.handle, "width") + this.handleMargin;
return this.container.css({
width: newWidth
});
} else {
newWidth = onLabelWidth > offLabelWidth ? onLabelWidth : offLabelWidth;
this.handleCenter.css({
width: newWidth + 4
});
return this.handle.css({
width: newWidth + 7
});
}
};
iOSCheckbox.prototype.onMouseDown = function(event) {
var x;
event.preventDefault();
if (this.isDisabled()) {
return;
}
x = event.pageX || event.originalEvent.changedTouches[0].pageX;
iOSCheckbox.currentlyClicking = this.handle;
iOSCheckbox.dragStartPosition = x;
return iOSCheckbox.handleLeftOffset = parseInt(this.handle.css("left"), 10) || 0;
};
iOSCheckbox.prototype.onDragMove = function(event, x) {
var newWidth, p;
if (iOSCheckbox.currentlyClicking !== this.handle) {
return;
}
p = (x + iOSCheckbox.handleLeftOffset - iOSCheckbox.dragStartPosition) / this.rightSide;
if (p < 0) {
p = 0;
}
if (p > 1) {
p = 1;
}
newWidth = p * this.rightSide;
this.handle.css({
left: newWidth
});
this.onLabel.css({
width: newWidth + this.handleRadius
});
this.offSpan.css({
marginRight: -newWidth
});
return this.onSpan.css({
marginLeft: -(1 - p) * this.rightSide
});
};
iOSCheckbox.prototype.onDragEnd = function(event, x) {
var p;
if (iOSCheckbox.currentlyClicking !== this.handle) {
return;
}
if (this.isDisabled()) {
return;
}
if (iOSCheckbox.dragging) {
p = (x - iOSCheckbox.dragStartPosition) / this.rightSide;
this.elem.prop("checked", p >= 0.5).change();
} else {
this.elem.prop("checked", !this.elem.prop("checked")).change();
}
iOSCheckbox.currentlyClicking = null;
iOSCheckbox.dragging = null;
if (typeof this.onChange === "function") {
this.onChange(this.elem, this.elem.prop("checked"));
}
return this.didChange();
};
iOSCheckbox.prototype.refresh = function() {
return this.didChange();
};
iOSCheckbox.prototype.didChange = function() {
var newLeft;
if (this.isDisabled()) {
this.container.addClass(this.disabledClass);
return false;
} else {
this.container.removeClass(this.disabledClass);
}
newLeft = this.elem.prop("checked") ? this.rightSide + 2 : 0;
this.handle.animate({
left: newLeft
}, this.duration);
this.onLabel.animate({
width: newLeft + this.handleRadius
}, this.duration);
this.offSpan.animate({
marginRight: - newLeft
}, this.duration);
return this.onSpan.animate({
marginLeft: newLeft - this.rightSide
}, this.duration);
};
iOSCheckbox.prototype.attachEvents = function() {
var localMouseMove, localMouseUp, self;
self = this;
localMouseMove = function(event) {
return self.onGlobalMove.apply(self, arguments);
};
localMouseUp = function(event) {
self.onGlobalUp.apply(self, arguments);
$(document).unbind("mousemove touchmove", localMouseMove);
return $(document).unbind("mouseup touchend", localMouseUp);
};
this.elem.change(function() {
return self.refresh();
});
return this.container.bind("mousedown touchstart", function(event) {
self.onMouseDown.apply(self, arguments);
$(document).bind("mousemove touchmove", localMouseMove);
return $(document).bind("mouseup touchend", localMouseUp);
});
};
iOSCheckbox.prototype.initialPosition = function() {
var containerWidth, offset;
containerWidth = this._getDimension(this.container, "width");
this.offLabel.css({
width: containerWidth - this.containerRadius - 4
});
this.offBorder.css({
left: containerWidth - 4
});
offset = this.containerRadius + 1;
if ($.browser.msie && $.browser.version < 7) {
offset -= 3;
}
this.rightSide = containerWidth - this._getDimension(this.handle, "width") - offset;
if (this.elem.is(":checked")) {
this.handle.css({
left: this.rightSide
});
this.onLabel.css({
width: this.rightSide + this.handleRadius
});
this.offSpan.css({
marginRight: -this.rightSide,
});
} else {
this.onLabel.css({
width: 0
});
this.onSpan.css({
marginLeft: -this.rightSide
});
}
if (this.isDisabled()) {
return this.container.addClass(this.disabledClass);
}
};
iOSCheckbox.prototype.onGlobalMove = function(event) {
var x;
if (!(!this.isDisabled() && iOSCheckbox.currentlyClicking)) {
return;
}
event.preventDefault();
x = event.pageX || event.originalEvent.changedTouches[0].pageX;
if (!iOSCheckbox.dragging && (Math.abs(iOSCheckbox.dragStartPosition - x) > this.dragThreshold)) {
iOSCheckbox.dragging = true;
}
return this.onDragMove(event, x);
};
iOSCheckbox.prototype.onGlobalUp = function(event) {
var x;
if (!iOSCheckbox.currentlyClicking) {
return;
}
event.preventDefault();
x = event.pageX || event.originalEvent.changedTouches[0].pageX;
this.onDragEnd(event, x);
return false;
};
iOSCheckbox.defaults = {
duration: 200,
checkedLabel: "ON",
uncheckedLabel: "OFF",
resizeHandle: true,
resizeContainer: true,
disabledClass: "iPhoneCheckDisabled",
containerClass: "iPhoneCheckContainer",
labelOnClass: "iPhoneCheckLabelOn",
labelOffClass: "iPhoneCheckLabelOff",
handleClass: "iPhoneCheckHandle",
handleCenterClass: "iPhoneCheckHandleCenter",
handleRightClass: "iPhoneCheckHandleRight",
dragThreshold: 5,
handleMargin: 15,
handleRadius: 4,
containerRadius: 5,
dataName: "iphoneStyle",
onChange: function() {}
};
return iOSCheckbox;
})();
$.iphoneStyle = this.iOSCheckbox = iOSCheckbox;
$.fn.iphoneStyle = function() {
var args, checkbox, dataName, existingControl, method, params, _i, _len, _ref, _ref1, _ref2, _ref3;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
dataName = (_ref = (_ref1 = args[0]) != null ? _ref1.dataName : void 0) != null ? _ref : iOSCheckbox.defaults.dataName;
_ref2 = this.filter(":checkbox");
for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
checkbox = _ref2[_i];
existingControl = $(checkbox).data(dataName);
if (existingControl != null) {
method = args[0], params = 2 <= args.length ? __slice.call(args, 1) : [];
if ((_ref3 = existingControl[method]) != null) {
_ref3.apply(existingControl, params);
}
} else {
new iOSCheckbox(checkbox, args[0]);
}
}
return this;
};
$.fn.iOSCheckbox = function(options) {
var opts;
if (options == null) {
options = {};
}
opts = $.extend({}, options, {
resizeHandle: false,
disabledClass: "iOSCheckDisabled",
containerClass: "iOSCheckContainer",
labelOnClass: "iOSCheckLabelOn",
labelOffClass: "iOSCheckLabelOff",
handleClass: "iOSCheckHandle",
handleCenterClass: "iOSCheckHandleCenter",
handleRightClass: "iOSCheckHandleRight",
dataName: "iOSCheckbox"
});
return this.iphoneStyle(opts);
};
}).call(this);

+ 460
- 0
code/html/vendor/datatables-1.10.16.css View File

@ -0,0 +1,460 @@
/*
* This combined file was created by the DataTables downloader builder:
* https://datatables.net/download
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#dt/dt-1.10.16
*
* Included libraries:
* DataTables 1.10.16
*/
/*
* Table styles
*/
table.dataTable {
width: 100%;
margin: 0 auto;
clear: both;
border-collapse: separate;
border-spacing: 0;
/*
* Header and footer styles
*/
/*
* Body styles
*/
}
table.dataTable thead th,
table.dataTable tfoot th {
font-weight: bold;
}
table.dataTable thead th,
table.dataTable thead td {
padding: 10px 18px;
border-bottom: 1px solid #111;
}
table.dataTable thead th:active,
table.dataTable thead td:active {
outline: none;
}
table.dataTable tfoot th,
table.dataTable tfoot td {
padding: 10px 18px 6px 18px;
border-top: 1px solid #111;
}
table.dataTable thead .sorting,
table.dataTable thead .sorting_asc,
table.dataTable thead .sorting_desc,
table.dataTable thead .sorting_asc_disabled,
table.dataTable thead .sorting_desc_disabled {
cursor: pointer;
*cursor: hand;
background-repeat: no-repeat;
background-position: center right;
}
table.dataTable thead .sorting {
background-image: url("images/sort_both.png");
}
table.dataTable thead .sorting_asc {
background-image: url("images/sort_asc.png");
}
table.dataTable thead .sorting_desc {
background-image: url("images/sort_desc.png");
}
table.dataTable thead .sorting_asc_disabled {
background-image: url("images/sort_asc_disabled.png");
}
table.dataTable thead .sorting_desc_disabled {
background-image: url("images/sort_desc_disabled.png");
}
table.dataTable tbody tr {
background-color: #ffffff;
}
table.dataTable tbody tr.selected {
background-color: #B0BED9;
}
table.dataTable tbody th,
table.dataTable tbody td {
padding: 8px 10px;
}
table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
border-top: 1px solid #ddd;
}
table.dataTable.row-border tbody tr:first-child th,
table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
table.dataTable.display tbody tr:first-child td {
border-top: none;
}
table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
border-top: 1px solid #ddd;
border-right: 1px solid #ddd;
}
table.dataTable.cell-border tbody tr th:first-child,
table.dataTable.cell-border tbody tr td:first-child {
border-left: 1px solid #ddd;
}
table.dataTable.cell-border tbody tr:first-child th,
table.dataTable.cell-border tbody tr:first-child td {
border-top: none;
}
table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
background-color: #f9f9f9;
}
table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
background-color: #acbad4;
}
table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
background-color: #f6f6f6;
}
table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
background-color: #aab7d1;
}
table.dataTable.order-column tbody tr > .sorting_1,
table.dataTable.order-column tbody tr > .sorting_2,
table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
table.dataTable.display tbody tr > .sorting_2,
table.dataTable.display tbody tr > .sorting_3 {
background-color: #fafafa;
}
table.dataTable.order-column tbody tr.selected > .sorting_1,
table.dataTable.order-column tbody tr.selected > .sorting_2,
table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
table.dataTable.display tbody tr.selected > .sorting_2,
table.dataTable.display tbody tr.selected > .sorting_3 {
background-color: #acbad5;
}
table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
background-color: #f1f1f1;
}
table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
background-color: #f3f3f3;
}
table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
background-color: whitesmoke;
}
table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
background-color: #a6b4cd;
}
table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
background-color: #a8b5cf;
}
table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
background-color: #a9b7d1;
}
table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
background-color: #fafafa;
}
table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
background-color: #fcfcfc;
}
table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
background-color: #fefefe;
}
table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
background-color: #acbad5;
}
table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
background-color: #aebcd6;
}
table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
background-color: #afbdd8;
}
table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
background-color: #eaeaea;
}
table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
background-color: #ececec;
}
table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
background-color: #efefef;
}
table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
background-color: #a2aec7;
}
table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
background-color: #a3b0c9;
}
table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
background-color: #a5b2cb;
}
table.dataTable.no-footer {
border-bottom: 1px solid #111;
}
table.dataTable.nowrap th, table.dataTable.nowrap td {
white-space: nowrap;
}
table.dataTable.compact thead th,
table.dataTable.compact thead td {
padding: 4px 17px 4px 4px;
}
table.dataTable.compact tfoot th,
table.dataTable.compact tfoot td {
padding: 4px;
}
table.dataTable.compact tbody th,
table.dataTable.compact tbody td {
padding: 4px;
}
table.dataTable th.dt-left,
table.dataTable td.dt-left {
text-align: left;
}
table.dataTable th.dt-center,
table.dataTable td.dt-center,
table.dataTable td.dataTables_empty {
text-align: center;
}
table.dataTable th.dt-right,
table.dataTable td.dt-right {
text-align: right;
}
table.dataTable th.dt-justify,
table.dataTable td.dt-justify {
text-align: justify;
}
table.dataTable th.dt-nowrap,
table.dataTable td.dt-nowrap {
white-space: nowrap;
}
table.dataTable thead th.dt-head-left,
table.dataTable thead td.dt-head-left,
table.dataTable tfoot th.dt-head-left,
table.dataTable tfoot td.dt-head-left {
text-align: left;
}
table.dataTable thead th.dt-head-center,
table.dataTable thead td.dt-head-center,
table.dataTable tfoot th.dt-head-center,
table.dataTable tfoot td.dt-head-center {
text-align: center;
}
table.dataTable thead th.dt-head-right,
table.dataTable thead td.dt-head-right,
table.dataTable tfoot th.dt-head-right,
table.dataTable tfoot td.dt-head-right {
text-align: right;
}
table.dataTable thead th.dt-head-justify,
table.dataTable thead td.dt-head-justify,
table.dataTable tfoot th.dt-head-justify,
table.dataTable tfoot td.dt-head-justify {
text-align: justify;
}
table.dataTable thead th.dt-head-nowrap,
table.dataTable thead td.dt-head-nowrap,
table.dataTable tfoot th.dt-head-nowrap,
table.dataTable tfoot td.dt-head-nowrap {
white-space: nowrap;
}
table.dataTable tbody th.dt-body-left,
table.dataTable tbody td.dt-body-left {
text-align: left;
}
table.dataTable tbody th.dt-body-center,
table.dataTable tbody td.dt-body-center {
text-align: center;
}
table.dataTable tbody th.dt-body-right,
table.dataTable tbody td.dt-body-right {
text-align: right;
}
table.dataTable tbody th.dt-body-justify,
table.dataTable tbody td.dt-body-justify {
text-align: justify;
}
table.dataTable tbody th.dt-body-nowrap,
table.dataTable tbody td.dt-body-nowrap {
white-space: nowrap;
}
table.dataTable,
table.dataTable th,
table.dataTable td {
box-sizing: content-box;
}
/*
* Control feature layout
*/
.dataTables_wrapper {
position: relative;
clear: both;
*zoom: 1;
zoom: 1;
}
.dataTables_wrapper .dataTables_length {
float: left;
}
.dataTables_wrapper .dataTables_filter {
float: right;
text-align: right;
}
.dataTables_wrapper .dataTables_filter input {
margin-left: 0.5em;
}
.dataTables_wrapper .dataTables_info {
clear: both;
float: left;
padding-top: 0.755em;
}
.dataTables_wrapper .dataTables_paginate {
float: right;
text-align: right;
padding-top: 0.25em;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
box-sizing: border-box;
display: inline-block;
min-width: 1.5em;
padding: 0.5em 1em;
margin-left: 2px;
text-align: center;
text-decoration: none !important;
cursor: pointer;
*cursor: hand;
color: #333 !important;
border: 1px solid transparent;
border-radius: 2px;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
color: #333 !important;
border: 1px solid #979797;
background-color: white;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%);
/* IE10+ */
background: -o-linear-gradient(top, white 0%, #dcdcdc 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, white 0%, #dcdcdc 100%);
/* W3C */
}
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
cursor: default;
color: #666 !important;
border: 1px solid transparent;
background: transparent;
box-shadow: none;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
color: white !important;
border: 1px solid #111;
background-color: #585858;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #585858 0%, #111 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, #585858 0%, #111 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, #585858 0%, #111 100%);
/* IE10+ */
background: -o-linear-gradient(top, #585858 0%, #111 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #585858 0%, #111 100%);
/* W3C */
}
.dataTables_wrapper .dataTables_paginate .paginate_button:active {
outline: none;
background-color: #2b2b2b;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
/* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Chrome10+,Safari5.1+ */
background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* FF3.6+ */
background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* IE10+ */
background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Opera 11.10+ */
background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);
/* W3C */
box-shadow: inset 0 0 3px #111;
}
.dataTables_wrapper .dataTables_paginate .ellipsis {
padding: 0 1em;
}
.dataTables_wrapper .dataTables_processing {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 40px;
margin-left: -50%;
margin-top: -25px;
padding-top: 20px;
text-align: center;
font-size: 1.2em;
background-color: white;
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
}
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_processing,
.dataTables_wrapper .dataTables_paginate {
color: #333;
}
.dataTables_wrapper .dataTables_scroll {
clear: both;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
*margin-top: -1px;
-webkit-overflow-scrolling: touch;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td {
vertical-align: middle;
}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing,
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing {
height: 0;
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
}
.dataTables_wrapper.no-footer .dataTables_scrollBody {
border-bottom: 1px solid #111;
}
.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,
.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
border-bottom: none;
}
.dataTables_wrapper:after {
visibility: hidden;
display: block;
content: "";
clear: both;
height: 0;
}
@media screen and (max-width: 767px) {
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_paginate {
float: none;
text-align: center;
}
.dataTables_wrapper .dataTables_paginate {
margin-top: 0.5em;
}
}
@media screen and (max-width: 640px) {
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter {
float: none;
text-align: center;
}
.dataTables_wrapper .dataTables_filter {
margin-top: 0.5em;
}
}

+ 178
- 0
code/html/vendor/datatables-1.10.16.min.js View File

@ -0,0 +1,178 @@
/*
* This combined file was created by the DataTables downloader builder:
* https://datatables.net/download
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#dt/dt-1.10.16
*
* Included libraries:
* DataTables 1.10.16
*/
/*!
DataTables 1.10.16
©2008-2017 SpryMedia Ltd - datatables.net/license
*/
(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,G){E||(E=window);G||(G="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(G,E,E.document)}:h(jQuery,window,document)})(function(h,E,G,k){function X(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),
d[c]=e,"o"===b[1]&&X(a[e])});a._hungarianMap=d}function I(a,b,c){a._hungarianMap||X(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),I(a[d],b[d],c)):b[d]=b[e]})}function Ca(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&F(a,a,"sZeroRecords","sLoadingRecords");
a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&cb(a)}function db(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX=
a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&I(m.models.oSearch,a[b])}function eb(a){A(a,"orderable","bSortable");A(a,"orderData","aDataSort");A(a,"orderSequence","asSorting");A(a,"orderDataType","sortDataType");var b=a.aDataSort;"number"===typeof b&&!h.isArray(b)&&(a.aDataSort=[b])}function fb(a){if(!m.__browser){var b={};m.__browser=b;var c=h("<div/>").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1,overflow:"hidden"}).append(h("<div/>").css({position:"absolute",
top:1,left:1,width:100,overflow:"scroll"}).append(h("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function gb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==
e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Da(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:G.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);ja(a,d,h(b).data())}function ja(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=
e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(eb(c),I(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=Q(g),i=b.mRender?Q(b.mRender):null,c=function(a){return"string"===
typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return R(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=
d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Y(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ea(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;(""!==b.sY||""!==b.sX)&&ka(a);r(a,null,"column-sizing",[a])}function Z(a,b){var c=la(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function $(a,b){var c=
la(a,"bVisible"),c=h.inArray(b,c);return-1!==c?c:null}function aa(a){var b=0;h.each(a.aoColumns,function(a,d){d.bVisible&&"none"!==h(d.nTh).css("display")&&b++});return b}function la(a,b){var c=[];h.map(a.aoColumns,function(a,e){a[b]&&c.push(e)});return c}function Fa(a){var b=a.aoColumns,c=a.aoData,d=m.ext.type.detect,e,f,g,j,i,h,l,q,t;e=0;for(f=b.length;e<f;e++)if(l=b[e],t=[],!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){g=0;for(j=d.length;g<j;g++){i=0;for(h=c.length;i<h;i++){t[i]===
k&&(t[i]=B(a,i,e,"type"));q=d[g](t[i],a);if(!q&&g!==d.length-1)break;if("html"===q)break}if(q){l.sType=q;break}}l.sType||(l.sType="string")}}function hb(a,b,c,d){var e,f,g,j,i,n,l=a.aoColumns;if(b)for(e=b.length-1;0<=e;e--){n=b[e];var q=n.targets!==k?n.targets:n.aTargets;h.isArray(q)||(q=[q]);f=0;for(g=q.length;f<g;f++)if("number"===typeof q[f]&&0<=q[f]){for(;l.length<=q[f];)Da(a);d(q[f],n)}else if("number"===typeof q[f]&&0>q[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;j<
i;j++)("_all"==q[f]||h(l[j].nTh).hasClass(q[f]))&&d(j,n)}}if(c){e=0;for(a=c.length;e<a;e++)d(e,c[e])}}function M(a,b,c,d){var e=a.aoData.length,f=h.extend(!0,{},m.models.oRow,{src:c?"dom":"data",idx:e});f._aData=b;a.aoData.push(f);for(var g=a.aoColumns,j=0,i=g.length;j<i;j++)g[j].sType=null;a.aiDisplayMaster.push(e);b=a.rowIdFn(b);b!==k&&(a.aIds[b]=f);(c||!a.oFeatures.bDeferRender)&&Ga(a,e,c,d);return e}function ma(a,b){var c;b instanceof h||(b=h(b));return b.map(function(b,e){c=Ha(a,e);return M(a,
c.data,e,c.cells)})}function B(a,b,c,d){var e=a.iDraw,f=a.aoColumns[c],g=a.aoData[b]._aData,j=f.sDefaultContent,i=f.fnGetData(g,d,{settings:a,row:b,col:c});if(i===k)return a.iDrawError!=e&&null===j&&(J(a,0,"Requested unknown parameter "+("function"==typeof f.mData?"{function}":"'"+f.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=e),j;if((i===g||null===i)&&null!==j&&d!==k)i=j;else if("function"===typeof i)return i.call(g);return null===i&&"display"==d?"":i}function ib(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,
d,{settings:a,row:b,col:c})}function Ia(a){return h.map(a.match(/(\\.|[^\.])+/g)||[""],function(a){return a.replace(/\\\./g,".")})}function Q(a){if(h.isPlainObject(a)){var b={};h.each(a,function(a,c){c&&(b[a]=Q(c))});return function(a,c,f,g){var j=b[c]||b._;return j!==k?j(a,c,f,g):a}}if(null===a)return function(a){return a};if("function"===typeof a)return function(b,c,f,g){return a(b,c,f,g)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var c=function(a,
b,f){var g,j;if(""!==f){j=Ia(f);for(var i=0,n=j.length;i<n;i++){f=j[i].match(ba);g=j[i].match(U);if(f){j[i]=j[i].replace(ba,"");""!==j[i]&&(a=a[j[i]]);g=[];j.splice(0,i+1);j=j.join(".");if(h.isArray(a)){i=0;for(n=a.length;i<n;i++)g.push(c(a[i],b,j))}a=f[0].substring(1,f[0].length-1);a=""===a?g:g.join(a);break}else if(g){j[i]=j[i].replace(U,"");a=a[j[i]]();continue}if(null===a||a[j[i]]===k)return k;a=a[j[i]]}}return a};return function(b,e){return c(b,e,a)}}return function(b){return b[a]}}function R(a){if(h.isPlainObject(a))return R(a._);
if(null===a)return function(){};if("function"===typeof a)return function(b,d,e){a(b,"set",d,e)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var b=function(a,d,e){var e=Ia(e),f;f=e[e.length-1];for(var g,j,i=0,n=e.length-1;i<n;i++){g=e[i].match(ba);j=e[i].match(U);if(g){e[i]=e[i].replace(ba,"");a[e[i]]=[];f=e.slice();f.splice(0,i+1);g=f.join(".");if(h.isArray(d)){j=0;for(n=d.length;j<n;j++)f={},b(f,d[j],g),a[e[i]].push(f)}else a[e[i]]=d;return}j&&(e[i]=e[i].replace(U,
""),a=a[e[i]](d));if(null===a[e[i]]||a[e[i]]===k)a[e[i]]={};a=a[e[i]]}if(f.match(U))a[f.replace(U,"")](d);else a[f.replace(ba,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Ja(a){return D(a.aoData,"_aData")}function na(a){a.aoData.length=0;a.aiDisplayMaster.length=0;a.aiDisplay.length=0;a.aIds={}}function oa(a,b,c){for(var d=-1,e=0,f=a.length;e<f;e++)a[e]==b?d=e:a[e]>b&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function ca(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);
c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ha(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c<f;c++)g(j[c],c)}}e._aSortData=null;e._aFilterData=null;g=a.aoColumns;if(d!==k)g[d].sType=null;else{c=0;for(f=g.length;c<f;c++)g[c].sType=null;Ka(a,e)}}function Ha(a,b,c,d){var e=[],f=b.firstChild,g,j,i=0,n,l=a.aoColumns,q=a._rowReadObject,d=d!==k?d:q?{}:[],t=function(a,b){if("string"===typeof a){var c=a.indexOf("@");
-1!==c&&(c=a.substring(c+1),R(a)(d,b.getAttribute(c)))}},m=function(a){if(c===k||c===i)j=l[i],n=h.trim(a.innerHTML),j&&j._bAttrSrc?(R(j.mData._)(d,n),t(j.mData.sort,a),t(j.mData.type,a),t(j.mData.filter,a)):q?(j._setter||(j._setter=R(j.mData)),j._setter(d,n)):d[i]=n;i++};if(f)for(;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)m(f),e.push(f);f=f.nextSibling}else{e=b.anCells;f=0;for(g=e.length;f<g;f++)m(e[f])}if(b=b.firstChild?b:b.nTr)(b=b.getAttribute("id"))&&R(a.rowId)(d,b);return{data:d,cells:e}}
function Ga(a,b,c,d){var e=a.aoData[b],f=e._aData,g=[],j,i,n,l,q;if(null===e.nTr){j=c||G.createElement("tr");e.nTr=j;e.anCells=g;j._DT_RowIndex=b;Ka(a,e);l=0;for(q=a.aoColumns.length;l<q;l++){n=a.aoColumns[l];i=c?d[l]:G.createElement(n.sCellType);i._DT_CellIndex={row:b,column:l};g.push(i);if((!c||n.mRender||n.mData!==l)&&(!h.isPlainObject(n.mData)||n.mData._!==l+".display"))i.innerHTML=B(a,b,l,"display");n.sClass&&(i.className+=" "+n.sClass);n.bVisible&&!c?j.appendChild(i):!n.bVisible&&c&&i.parentNode.removeChild(i);
n.fnCreatedCell&&n.fnCreatedCell.call(a.oInstance,i,B(a,b,l),f,b,l)}r(a,"aoRowCreatedCallback",null,[j,f,b])}e.nTr.setAttribute("role","row")}function Ka(a,b){var c=b.nTr,d=b._aData;if(c){var e=a.rowIdFn(d);e&&(c.id=e);d.DT_RowClass&&(e=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?qa(b.__rowc.concat(e)):e,h(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&h(c).attr(d.DT_RowAttr);d.DT_RowData&&h(c).data(d.DT_RowData)}}function jb(a){var b,c,d,e,f,g=a.nTHead,j=a.nTFoot,i=0===
h("th, td",g).length,n=a.oClasses,l=a.aoColumns;i&&(e=h("<tr/>").appendTo(g));b=0;for(c=l.length;b<c;b++)f=l[b],d=h(f.nTh).addClass(f.sClass),i&&d.appendTo(e),a.oFeatures.bSort&&(d.addClass(f.sSortingClass),!1!==f.bSortable&&(d.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),La(a,f.nTh,b))),f.sTitle!=d[0].innerHTML&&d.html(f.sTitle),Ma(a,"header")(a,d,f,n);i&&da(a.aoHeader,g);h(g).find(">tr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH);
if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b<c;b++)f=l[b],f.nTf=a[b].cell,f.sClass&&h(f.nTf).addClass(f.sClass)}}function ea(a,b,c){var d,e,f,g=[],j=[],i=a.aoColumns.length,n;if(b){c===k&&(c=!1);d=0;for(e=b.length;d<e;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=i-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);j.push([])}d=0;for(e=g.length;d<e;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(n=i=1,j[d][f]===k){a.appendChild(g[d][f].cell);
for(j[d][f]=1;g[d+i]!==k&&g[d][f].cell==g[d+i][f].cell;)j[d+i][f]=1,i++;for(;g[d][f+n]!==k&&g[d][f].cell==g[d][f+n].cell;){for(c=0;c<i;c++)j[d+c][f+n]=1;n++}h(g[d][f].cell).attr("rowspan",i).attr("colspan",n)}}}}function N(a){var b=r(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))C(a,!1);else{var b=[],c=0,d=a.asStripeClasses,e=d.length,f=a.oLanguage,g=a.iInitDisplayStart,j="ssp"==y(a),i=a.aiDisplay;a.bDrawing=!0;g!==k&&-1!==g&&(a._iDisplayStart=j?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=
-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!kb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j<f;j++){var l=i[j],q=a.aoData[l];null===q.nTr&&Ga(a,l);l=q.nTr;if(0!==e){var t=d[c%e];q._sRowStripe!=t&&(h(l).removeClass(q._sRowStripe).addClass(t),q._sRowStripe=t)}r(a,"aoRowCallback",null,[l,q._aData,c,j]);b.push(l);c++}}else c=f.sZeroRecords,1==a.iDraw&&"ajax"==y(a)?c=f.sLoadingRecords:
f.sEmptyTable&&0===a.fnRecordsTotal()&&(c=f.sEmptyTable),b[0]=h("<tr/>",{"class":e?d[0]:""}).append(h("<td />",{valign:"top",colSpan:aa(a),"class":a.oClasses.sRowEmpty}).html(c))[0];r(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ja(a),g,n,i]);r(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ja(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));r(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function S(a,b){var c=a.oFeatures,d=c.bFilter;
c.bSort&&lb(a);d?fa(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;N(a);a._drawHold=!1}function mb(a){var b=a.oClasses,c=h(a.nTable),c=h("<div/>").insertBefore(c),d=a.oFeatures,e=h("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,k=0;k<f.length;k++){g=null;j=f[k];if("<"==j){i=h("<div/>")[0];
n=f[k+1];if("'"==n||'"'==n){l="";for(q=2;f[k+q]!=n;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=nb(a);else if("f"==j&&d.bFilter)g=ob(a);else if("r"==j&&d.bProcessing)g=pb(a);else if("t"==j)g=qb(a);else if("i"==j&&d.bInfo)g=rb(a);else if("p"==
j&&d.bPaginate)g=sb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q<n;q++)if(j==i[q].cFeature){g=i[q].fnInit(a);break}}g&&(i=a.aanFeatures,i[j]||(i[j]=[]),i[j].push(g),e.append(g))}c.replaceWith(e);a.nHolding=null}function da(a,b){var c=h(b).children("tr"),d,e,f,g,j,i,n,l,q,k;a.splice(0,a.length);f=0;for(i=c.length;f<i;f++)a.push([]);f=0;for(i=c.length;f<i;f++){d=c[f];for(e=d.firstChild;e;){if("TD"==e.nodeName.toUpperCase()||"TH"==e.nodeName.toUpperCase()){l=1*e.getAttribute("colspan");
q=1*e.getAttribute("rowspan");l=!l||0===l||1===l?1:l;q=!q||0===q||1===q?1:q;g=0;for(j=a[f];j[g];)g++;n=g;k=1===l?!0:!1;for(j=0;j<l;j++)for(g=0;g<q;g++)a[f+g][n+j]={cell:e,unique:k},a[f+g].nTr=d}e=e.nextSibling}}}function ra(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],da(c,b)));for(var b=0,e=c.length;b<e;b++)for(var f=0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function sa(a,b,c){r(a,"aoServerParams","serverParams",[b]);if(b&&h.isArray(b)){var d={},
e=/(.*?)\[\]$/;h.each(b,function(a,b){var c=b.name.match(e);c?(c=c[0],d[c]||(d[c]=[]),d[c].push(b.value)):d[b.name]=b.value});b=d}var f,g=a.ajax,j=a.oInstance,i=function(b){r(a,null,"xhr",[a,b,a.jqXHR]);c(b)};if(h.isPlainObject(g)&&g.data){f=g.data;var n=h.isFunction(f)?f(b,a):f,b=h.isFunction(f)&&n?n:h.extend(!0,b,n);delete g.data}n={data:b,success:function(b){var c=b.error||b.sError;c&&J(a,0,c);a.json=b;i(b)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(b,c){var d=r(a,null,"xhr",
[a,null,a.jqXHR]);-1===h.inArray(!0,d)&&("parsererror"==c?J(a,0,"Invalid JSON response",1):4===b.readyState&&J(a,0,"Ajax error",7));C(a,!1)}};a.oAjaxData=b;r(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(j,a.sAjaxSource,h.map(b,function(a,b){return{name:b,value:a}}),i,a):a.sAjaxSource||"string"===typeof g?a.jqXHR=h.ajax(h.extend(n,{url:g||a.sAjaxSource})):h.isFunction(g)?a.jqXHR=g.call(j,b,i,a):(a.jqXHR=h.ajax(h.extend(n,g)),g.data=f)}function kb(a){return a.bAjaxDataGet?(a.iDraw++,C(a,
!0),sa(a,tb(a),function(b){ub(a,b)}),!1):!0}function tb(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,e=a.oPreviousSearch,f=a.aoPreSearchCols,g,j=[],i,n,l,k=V(a);g=a._iDisplayStart;i=!1!==d.bPaginate?a._iDisplayLength:-1;var t=function(a,b){j.push({name:a,value:b})};t("sEcho",a.iDraw);t("iColumns",c);t("sColumns",D(b,"sName").join(","));t("iDisplayStart",g);t("iDisplayLength",i);var pa={draw:a.iDraw,columns:[],order:[],start:g,length:i,search:{value:e.sSearch,regex:e.bRegex}};for(g=0;g<c;g++)n=b[g],
l=f[g],i="function"==typeof n.mData?"function":n.mData,pa.columns.push({data:i,name:n.sName,searchable:n.bSearchable,orderable:n.bSortable,search:{value:l.sSearch,regex:l.bRegex}}),t("mDataProp_"+g,i),d.bFilter&&(t("sSearch_"+g,l.sSearch),t("bRegex_"+g,l.bRegex),t("bSearchable_"+g,n.bSearchable)),d.bSort&&t("bSortable_"+g,n.bSortable);d.bFilter&&(t("sSearch",e.sSearch),t("bRegex",e.bRegex));d.bSort&&(h.each(k,function(a,b){pa.order.push({column:b.col,dir:b.dir});t("iSortCol_"+a,b.col);t("sSortDir_"+
a,b.dir)}),t("iSortingCols",k.length));b=m.ext.legacy.ajax;return null===b?a.sAjaxSource?j:pa:b?j:pa}function ub(a,b){var c=ta(a,b),d=b.sEcho!==k?b.sEcho:b.draw,e=b.iTotalRecords!==k?b.iTotalRecords:b.recordsTotal,f=b.iTotalDisplayRecords!==k?b.iTotalDisplayRecords:b.recordsFiltered;if(d){if(1*d<a.iDraw)return;a.iDraw=1*d}na(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(f,10);d=0;for(e=c.length;d<e;d++)M(a,c[d]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;N(a);a._bInitComplete||
ua(a,b);a.bAjaxDataGet=!0;C(a,!1)}function ta(a,b){var c=h.isPlainObject(a.ajax)&&a.ajax.dataSrc!==k?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===c?b.aaData||b[c]:""!==c?Q(c)(b):b}function ob(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,e=a.oPreviousSearch,f=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("<div/>",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("<label/>").append(j)),f=function(){var b=!this.value?
"":this.value;b!=e.sSearch&&(fa(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,N(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",g?Na(f,g):f).on("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==G.activeElement&&i.val(e.sSearch)}catch(d){}});
return b[0]}function fa(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Fa(a);if("ssp"!=y(a)){vb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<e.length;b++)wb(a,e[b].sSearch,b,e[b].bEscapeRegex!==k?!e[b].bEscapeRegex:e[b].bRegex,e[b].bSmart,e[b].bCaseInsensitive);xb(a)}else f(b);a.bFiltered=!0;r(a,null,"search",[a])}function xb(a){for(var b=
m.ext.search,c=a.aiDisplay,d,e,f=0,g=b.length;f<g;f++){for(var j=[],i=0,n=c.length;i<n;i++)e=c[i],d=a.aoData[e],b[f](a,d._aFilterData,e,d._aData,i)&&j.push(e);c.length=0;h.merge(c,j)}}function wb(a,b,c,d,e,f){if(""!==b){for(var g=[],j=a.aiDisplay,d=Oa(b,d,e,f),e=0;e<j.length;e++)b=a.aoData[j[e]]._aFilterData[c],d.test(b)&&g.push(j[e]);a.aiDisplay=g}}function vb(a,b,c,d,e,f){var d=Oa(b,d,e,f),f=a.oPreviousSearch.sSearch,g=a.aiDisplayMaster,j,e=[];0!==m.ext.search.length&&(c=!0);j=yb(a);if(0>=b.length)a.aiDisplay=
g.slice();else{if(j||c||f.length>b.length||0!==b.indexOf(f)||a.bSorted)a.aiDisplay=g.slice();b=a.aiDisplay;for(c=0;c<b.length;c++)d.test(a.aoData[b[c]]._sFilterRow)&&e.push(b[c]);a.aiDisplay=e}}function Oa(a,b,c,d){a=b?a:Pa(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function yb(a){var b=a.aoColumns,c,d,e,f,g,j,i,h,l=m.ext.type.search;c=!1;
d=0;for(f=a.aoData.length;d<f;d++)if(h=a.aoData[d],!h._aFilterData){j=[];e=0;for(g=b.length;e<g;e++)c=b[e],c.bSearchable?(i=B(a,d,e,"filter"),l[c.sType]&&(i=l[c.sType](i)),null===i&&(i=""),"string"!==typeof i&&i.toString&&(i=i.toString())):i="",i.indexOf&&-1!==i.indexOf("&")&&(va.innerHTML=i,i=Wb?va.textContent:va.innerText),i.replace&&(i=i.replace(/[\r\n]/g,"")),j.push(i);h._aFilterData=j;h._sFilterRow=j.join(" ");c=!0}return c}function zb(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,
caseInsensitive:a.bCaseInsensitive}}function Ab(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function rb(a){var b=a.sTableId,c=a.aanFeatures.i,d=h("<div/>",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Bb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Bb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+
1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Cb(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Cb(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,
f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ga(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){mb(a);jb(a);ea(a,a.aoHeader);ea(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Ea(a);b=0;for(c=e.length;b<c;b++)f=e[b],f.sWidth&&(f.nTh.style.width=v(f.sWidth));r(a,null,"preInit",[a]);S(a);e=y(a);if("ssp"!=e||g)"ajax"==e?sa(a,[],function(c){var f=ta(a,c);for(b=0;b<f.length;b++)M(a,f[b]);a.iInitDisplayStart=
d;S(a);C(a,!1);ua(a,c)},a):(C(a,!1),ua(a))}else setTimeout(function(){ga(a)},200)}function ua(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&Y(a);r(a,null,"plugin-init",[a,b]);r(a,"aoInitComplete","init",[a,b])}function Qa(a,b){var c=parseInt(b,10);a._iDisplayLength=c;Ra(a);r(a,null,"length",[a,c])}function nb(a){for(var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,e=h.isArray(d[0]),f=e?d[0]:d,d=e?d[1]:d,e=h("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,j=f.length;g<j;g++)e[0][g]=
new Option("number"===typeof d[g]?a.fnFormatNumber(d[g]):d[g],f[g]);var i=h("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Qa(a,h(this).val());N(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function sb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){N(a)},
b=h("<div/>").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;l<k;l++)Ma(a,"pageButton")(a,f.p[l],l,h,b,i)}else c.fnUpdate(a,e)},sName:"pagination"}));return b}function Sa(a,b,c){var d=a._iDisplayStart,e=a._iDisplayLength,f=a.fnRecordsDisplay();0===f||-1===
e?d=0:"number"===typeof b?(d=b*e,d>f&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e<f&&(d+=e):"last"==b?d=Math.floor((f-1)/e)*e:J(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(r(a,null,"page",[a]),c&&N(a));return b}function pb(a){return h("<div/>",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",
b?"block":"none");r(a,null,"processing",[a,b])}function qb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("<div/>",{"class":f.sScrollWrapper}).append(h("<div/>",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",
{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("<div/>",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("<div/>",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",
0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:ka,sName:"scrolling"});return i[0]}function ka(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"),
j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),m=t.children("table"),o=h(a.nTHead),p=h(a.nTable),s=p[0],r=s.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,T=x.bScrollOversize,Xb=D(a.aoColumns,"nTh"),O,K,P,w,Ta=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};K=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==K&&a.scrollBarVis!==k)a.scrollBarVis=K,Y(a);else{a.scrollBarVis=K;p.children("thead, tfoot").remove();
u&&(P=u.clone().prependTo(p),O=u.find("tr"),P=P.find("tr"));w=o.clone().prependTo(p);o=o.find("tr");K=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ra(a,w),function(b,c){B=Z(a,b);c.style.width=a.aoColumns[B].sWidth});u&&H(function(a){a.style.width=""},P);f=p.outerWidth();if(""===c){r.width="100%";if(T&&(p.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(p.outerWidth()-b);f=p.outerWidth()}else""!==d&&(r.width=
v(d),f=p.outerWidth());H(C,K);H(function(a){z.push(a.innerHTML);Ta.push(v(h(a).css("width")))},K);H(function(a,b){if(h.inArray(a,Xb)!==-1)a.style.width=Ta[b]},o);h(K).height(0);u&&(H(C,P),H(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},P),H(function(a,b){a.style.width=y[b]},O),h(P).height(0));H(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+z[b]+"</div>";a.style.width=Ta[b]},K);u&&H(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+
A[b]+"</div>";a.style.width=y[b]},P);if(p.outerWidth()<f){O=j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(T&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=v(O-b);(""===c||""!==d)&&J(a,1,"Possible column misalignment",6)}else O="100%";q.width=v(O);g.width=v(O);u&&(a.nScrollFoot.style.width=v(O));!e&&T&&(q.height=v(s.offsetHeight+b));c=p.outerWidth();n[0].style.width=v(c);i.width=v(c);d=p.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+
(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(m[0].style.width=v(c),t[0].style.width=v(c),t[0].style[e]=d?b+"px":"0px");p.children("colgroup").insertBefore(p.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function H(a,b,c){for(var d=0,e=0,f=b.length,g,j;e<f;){g=b[e].firstChild;for(j=c?c[e].firstChild:null;g;)1===g.nodeType&&(c?a(g,j,d):a(g,d),d++),g=g.nextSibling,j=c?j.nextSibling:null;e++}}function Ea(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,
e=d.sY,f=d.sX,g=d.sXInner,j=c.length,i=la(a,"bVisible"),n=h("th",a.nTHead),l=b.getAttribute("width"),k=b.parentNode,t=!1,m,o,p=a.oBrowser,d=p.bScrollOversize;(m=b.style.width)&&-1!==m.indexOf("%")&&(l=m);for(m=0;m<i.length;m++)o=c[i[m]],null!==o.sWidth&&(o.sWidth=Db(o.sWidthOrig,k),t=!0);if(d||!t&&!f&&!e&&j==aa(a)&&j==n.length)for(m=0;m<j;m++)i=Z(a,m),null!==i&&(c[i].sWidth=v(n.eq(m).width()));else{j=h(b).clone().css("visibility","hidden").removeAttr("id");j.find("tbody tr").remove();var s=h("<tr/>").appendTo(j.find("tbody"));
j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=ra(a,j.find("thead")[0]);for(m=0;m<i.length;m++)o=c[i[m]],n[m].style.width=null!==o.sWidthOrig&&""!==o.sWidthOrig?v(o.sWidthOrig):"",o.sWidthOrig&&f&&h(n[m]).append(h("<div/>").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m<i.length;m++)t=i[m],o=c[t],h(Eb(a,t)).clone(!1).append(o.sContentPadding).appendTo(s);h("[name]",
j).removeAttr("name");o=h("<div/>").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()<k.clientWidth&&l&&j.width(k.clientWidth)):e?j.width(k.clientWidth):l&&j.width(l);for(m=e=0;m<i.length;m++)k=h(n[m]),g=k.outerWidth()-k.width(),k=p.bBounding?Math.ceil(n[m].getBoundingClientRect().width):k.outerWidth(),e+=k,c[i[m]].sWidth=v(k-g);b.style.width=v(e);o.remove()}l&&(b.style.width=
v(l));if((l||f)&&!a._reszEvt)b=function(){h(E).on("resize.DT-"+a.sInstance,Na(function(){Y(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0}function Db(a,b){if(!a)return 0;var c=h("<div/>").css("width",v(a)).appendTo(b||G.body),d=c[0].offsetWidth;c.remove();return d}function Eb(a,b){var c=Fb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("<td/>").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Fb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;f<g;f++)c=B(a,f,b,"display")+"",c=c.replace(Yb,
""),c=c.replace(/&nbsp;/g," "),c.length>d&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function V(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;a<n.length;a++){i=n[a][0];f=e[i].aDataSort;b=0;for(c=f.length;b<c;b++)g=f[b],j=e[g].sType||
"string",n[a]._idx===k&&(n[a]._idx=h.inArray(n[a][1],e[g].asSorting)),d.push({src:i,col:g,dir:n[a][1],index:n[a]._idx,type:j,formatter:m.ext.type.order[j+"-pre"]})}return d}function lb(a){var b,c,d=[],e=m.ext.type.order,f=a.aoData,g=0,j,i=a.aiDisplayMaster,h;Fa(a);h=V(a);b=0;for(c=h.length;b<c;b++)j=h[b],j.formatter&&g++,Gb(a,j.col);if("ssp"!=y(a)&&0!==h.length){b=0;for(c=i.length;b<c;b++)d[i[b]]=b;g===h.length?i.sort(function(a,b){var c,e,g,j,i=h.length,k=f[a]._aSortData,m=f[b]._aSortData;for(g=
0;g<i;g++)if(j=h[g],c=k[j.col],e=m[j.col],c=c<e?-1:c>e?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return c<e?-1:c>e?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,o=f[b]._aSortData;for(j=0;j<k;j++)if(i=h[j],c=m[i.col],g=o[i.col],i=e[i.type+"-"+i.dir]||e["string-"+i.dir],c=i(c,g),0!==c)return c;c=d[a];g=d[b];return c<g?-1:c>g?1:0})}a.bSorted=!0}function Hb(a){for(var b,c,d=a.aoColumns,e=V(a),a=a.oLanguage.oAria,f=0,g=d.length;f<g;f++){c=d[f];var j=c.asSorting;b=c.sTitle.replace(/<.*?>/g,
"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0<e.length&&e[0].col==f?(i.setAttribute("aria-sort","asc"==e[0].dir?"ascending":"descending"),c=j[e[0].index+1]||j[0]):c=j[0],b+="asc"===c?a.sSortAscending:a.sSortDescending);i.setAttribute("aria-label",b)}}function Ua(a,b,c,d){var e=a.aaSorting,f=a.aoColumns[b].asSorting,g=function(a,b){var c=a._idx;c===k&&(c=h.inArray(a[1],f));return c+1<f.length?c+1:b?null:0};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=h.inArray(b,
D(e,"0")),-1!==c?(b=g(e[c],!0),null===b&&1===e.length&&(b=0),null===b?e.splice(c,1):(e[c][1]=f[b],e[c]._idx=b)):(e.push([b,f[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=f[b],e[0]._idx=b):(e.length=0,e.push([b,f[0]]),e[0]._idx=0);S(a);"function"==typeof d&&d(a)}function La(a,b,c,d){var e=a.aoColumns[c];Va(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(C(a,!0),setTimeout(function(){Ua(a,c,b.shiftKey,d);"ssp"!==y(a)&&C(a,!1)},0)):Ua(a,c,b.shiftKey,d))})}
function wa(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=V(a),e=a.oFeatures,f,g;if(e.bSort&&e.bSortClasses){e=0;for(f=b.length;e<f;e++)g=b[e].src,h(D(a.aoData,"anCells",g)).removeClass(c+(2>e?e+1:3));e=0;for(f=d.length;e<f;e++)g=d[e].src,h(D(a.aoData,"anCells",g)).addClass(c+(2>e?e+1:3))}a.aLastSort=d}function Gb(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,$(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j<i;j++)if(c=a.aoData[j],
c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)f=d?e[j]:B(a,j,b,"sort"),c._aSortData[b]=g?g(f):f}function xa(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:h.extend(!0,[],a.aaSorting),search:zb(a.oPreviousSearch),columns:h.map(a.aoColumns,function(b,d){return{visible:b.bVisible,search:zb(a.aoPreSearchCols[d])}})};r(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,
b)}}function Ib(a,b,c){var d,e,f=a.aoColumns,b=function(b){if(b&&b.time){var g=r(a,"aoStateLoadParams","stateLoadParams",[a,b]);if(-1===h.inArray(!1,g)&&(g=a.iStateDuration,!(0<g&&b.time<+new Date-1E3*g)&&!(b.columns&&f.length!==b.columns.length))){a.oLoadedState=h.extend(!0,{},b);b.start!==k&&(a._iDisplayStart=b.start,a.iInitDisplayStart=b.start);b.length!==k&&(a._iDisplayLength=b.length);b.order!==k&&(a.aaSorting=[],h.each(b.order,function(b,c){a.aaSorting.push(c[0]>=f.length?[0,c[1]]:c)}));b.search!==
k&&h.extend(a.oPreviousSearch,Ab(b.search));if(b.columns){d=0;for(e=b.columns.length;d<e;d++)g=b.columns[d],g.visible!==k&&(f[d].bVisible=g.visible),g.search!==k&&h.extend(a.aoPreSearchCols[d],Ab(g.search))}r(a,"aoStateLoaded","stateLoaded",[a,b])}}c()};if(a.oFeatures.bStateSave){var g=a.fnStateLoadCallback.call(a.oInstance,a,b);g!==k&&b(g)}else c()}function ya(a){var b=m.settings,a=h.inArray(a,D(b,"nTable"));return-1!==a?b[a]:null}function J(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+
" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+d);if(b)E.console&&console.log&&console.log(c);else if(b=m.ext,b=b.sErrMode||b.errMode,a&&r(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function F(a,b,c,d){h.isArray(c)?h.each(c,function(c,d){h.isArray(d)?F(a,b,d[0],d[1]):F(a,b,d)}):(d===k&&(d=c),b[c]!==k&&(a[d]=b[c]))}function Jb(a,b,c){var d,e;for(e in b)b.hasOwnProperty(e)&&(d=b[e],
h.isPlainObject(d)?(h.isPlainObject(a[e])||(a[e]={}),h.extend(!0,a[e],d)):a[e]=c&&"data"!==e&&"aaData"!==e&&h.isArray(d)?d.slice():d);return a}function Va(a,b,c){h(a).on("click.DT",b,function(b){a.blur();c(b)}).on("keypress.DT",b,function(a){13===a.which&&(a.preventDefault(),c(a))}).on("selectstart.DT",function(){return!1})}function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function r(a,b,c,d){var e=[];b&&(e=h.map(a[b].slice().reverse(),function(b){return b.fn.apply(a.oInstance,d)}));null!==c&&(b=h.Event(c+
".dt"),h(a.nTable).trigger(b,d),e.push(b.result));return e}function Ra(a){var b=a._iDisplayStart,c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Ma(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ha(a,b){var c=[],c=Kb.numbers_length,d=Math.floor(c/2);b<=c?c=W(0,b):a<=d?(c=W(0,
c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=W(b-(c-2),b):(c=W(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function cb(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Wa)},"html-num":function(b){return za(b,a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Wa)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Lb(a){return function(){var b=
[ya(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new s(ya(this[x.iApiIndex])):new s(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=
function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ka(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};
this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();
return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return ya(this[x.iApiIndex])};
this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in m.ext.internal)e&&(this[e]=Lb(e));this.each(function(){var e={},g=1<d?Jb(e,a,!0):
a,j=0,i,e=this.getAttribute("id"),n=!1,l=m.defaults,q=h(this);if("table"!=this.nodeName.toLowerCase())J(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{db(l);eb(l.column);I(l,l,!0);I(l.column,l.column,!0);I(l,h.extend(g,q.data()));var t=m.settings,j=0;for(i=t.length;j<i;j++){var o=t[j];if(o.nTable==this||o.nTHead.parentNode==this||o.nTFoot&&o.nTFoot.parentNode==this){var s=g.bRetrieve!==k?g.bRetrieve:l.bRetrieve;if(c||s)return o.oInstance;if(g.bDestroy!==k?g.bDestroy:l.bDestroy){o.oInstance.fnDestroy();
break}else{J(o,0,"Cannot reinitialise DataTable",3);return}}if(o.sTableId==this.id){t.splice(j,1);break}}if(null===e||""===e)this.id=e="DataTables_Table_"+m.ext._unique++;var p=h.extend(!0,{},m.models.oSettings,{sDestroyWidth:q[0].style.width,sInstance:e,sTableId:e});p.nTable=this;p.oApi=b.internal;p.oInit=g;t.push(p);p.oInstance=1===b.length?b:q.dataTable();db(g);g.oLanguage&&Ca(g.oLanguage);g.aLengthMenu&&!g.iDisplayLength&&(g.iDisplayLength=h.isArray(g.aLengthMenu[0])?g.aLengthMenu[0][0]:g.aLengthMenu[0]);
g=Jb(h.extend(!0,{},l),g);F(p.oFeatures,g,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));F(p,g,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],
["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"]]);F(p.oScroll,g,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);F(p.oLanguage,g,"fnInfoCallback");z(p,"aoDrawCallback",g.fnDrawCallback,"user");z(p,"aoServerParams",g.fnServerParams,"user");z(p,"aoStateSaveParams",g.fnStateSaveParams,"user");z(p,"aoStateLoadParams",g.fnStateLoadParams,"user");z(p,"aoStateLoaded",g.fnStateLoaded,"user");z(p,"aoRowCallback",
g.fnRowCallback,"user");z(p,"aoRowCreatedCallback",g.fnCreatedRow,"user");z(p,"aoHeaderCallback",g.fnHeaderCallback,"user");z(p,"aoFooterCallback",g.fnFooterCallback,"user");z(p,"aoInitComplete",g.fnInitComplete,"user");z(p,"aoPreDrawCallback",g.fnPreDrawCallback,"user");p.rowIdFn=Q(g.rowId);fb(p);var u=p.oClasses;h.extend(u,m.ext.classes,g.oClasses);q.addClass(u.sTable);p.iInitDisplayStart===k&&(p.iInitDisplayStart=g.iDisplayStart,p._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(p.bDeferLoading=
!0,e=h.isArray(g.iDeferLoading),p._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,p._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var v=p.oLanguage;h.extend(!0,v,g.oLanguage);v.sUrl&&(h.ajax({dataType:"json",url:v.sUrl,success:function(a){Ca(a);I(l.oLanguage,a);h.extend(true,v,a);ga(p)},error:function(){ga(p)}}),n=!0);null===g.asStripeClasses&&(p.asStripeClasses=[u.sStripeOdd,u.sStripeEven]);var e=p.asStripeClasses,x=q.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(e,function(a){return x.hasClass(a)}))&&
(h("tbody tr",this).removeClass(e.join(" ")),p.asDestroyStripes=e.slice());e=[];t=this.getElementsByTagName("thead");0!==t.length&&(da(p.aoHeader,t[0]),e=ra(p));if(null===g.aoColumns){t=[];j=0;for(i=e.length;j<i;j++)t.push(null)}else t=g.aoColumns;j=0;for(i=t.length;j<i;j++)Da(p,e?e[j]:null);hb(p,g.aoColumnDefs,t,function(a,b){ja(p,a,b)});if(x.length){var w=function(a,b){return a.getAttribute("data-"+b)!==null?b:null};h(x[0]).children("th, td").each(function(a,b){var c=p.aoColumns[a];if(c.mData===
a){var d=w(b,"sort")||w(b,"order"),e=w(b,"filter")||w(b,"search");if(d!==null||e!==null){c.mData={_:a+".display",sort:d!==null?a+".@data-"+d:k,type:d!==null?a+".@data-"+d:k,filter:e!==null?a+".@data-"+e:k};ja(p,a)}}})}var T=p.oFeatures,e=function(){if(g.aaSorting===k){var a=p.aaSorting;j=0;for(i=a.length;j<i;j++)a[j][1]=p.aoColumns[j].asSorting[0]}wa(p);T.bSort&&z(p,"aoDrawCallback",function(){if(p.bSorted){var a=V(p),b={};h.each(a,function(a,c){b[c.src]=c.dir});r(p,null,"order",[p,a,b]);Hb(p)}});
z(p,"aoDrawCallback",function(){(p.bSorted||y(p)==="ssp"||T.bDeferRender)&&wa(p)},"sc");var a=q.children("caption").each(function(){this._captionSide=h(this).css("caption-side")}),b=q.children("thead");b.length===0&&(b=h("<thead/>").appendTo(q));p.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("<tbody/>").appendTo(q));p.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(p.oScroll.sX!==""||p.oScroll.sY!==""))b=h("<tfoot/>").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter);
else if(b.length>0){p.nTFoot=b[0];da(p.aoFooter,p.nTFoot)}if(g.aaData)for(j=0;j<g.aaData.length;j++)M(p,g.aaData[j]);else(p.bDeferLoading||y(p)=="dom")&&ma(p,h(p.nTBody).children("tr"));p.aiDisplay=p.aiDisplayMaster.slice();p.bInitialised=true;n===false&&ga(p)};g.bStateSave?(T.bStateSave=!0,z(p,"aoDrawCallback",xa,"state_save"),Ib(p,g,e)):e()}});b=null;return this},x,s,o,u,Xa={},Mb=/[\r\n]/g,Aa=/<.*?>/g,Zb=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,$b=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)",
"g"),Wa=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,L=function(a){return!a||!0===a||"-"===a?!0:!1},Nb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Ob=function(a,b){Xa[b]||(Xa[b]=RegExp(Pa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Xa[b],"."):a},Ya=function(a,b,c){var d="string"===typeof a;if(L(a))return!0;b&&d&&(a=Ob(a,b));c&&d&&(a=a.replace(Wa,""));return!isNaN(parseFloat(a))&&isFinite(a)},Pb=function(a,b,c){return L(a)?!0:!(L(a)||"string"===
typeof a)?null:Ya(a.replace(Aa,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;e<f;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;e<f;e++)a[e]&&d.push(a[e][b]);return d},ia=function(a,b,c,d){var e=[],f=0,g=b.length;if(d!==k)for(;f<g;f++)a[b[f]][c]&&e.push(a[b[f]][c][d]);else for(;f<g;f++)e.push(a[b[f]][c]);return e},W=function(a,b){var c=[],d;b===k?(b=0,d=a):(d=b,b=a);for(var e=b;e<d;e++)c.push(e);return c},Qb=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);
return b},qa=function(a){var b;a:{if(!(2>a.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d<e;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();b=[];var e=a.length,f,g=0,d=0;a:for(;d<e;d++){c=a[d];for(f=0;f<g;f++)if(b[f]===c)continue a;b.push(c);g++}return b};m.util={throttle:function(a,b){var c=b!==k?b:200,d,e;return function(){var b=this,g=+new Date,j=arguments;d&&g<d+c?(clearTimeout(e),e=setTimeout(function(){d=k;a.apply(b,j)},c)):(d=g,a.apply(b,j))}},escapeRegex:function(a){return a.replace($b,
"\\$1")}};var A=function(a,b,c){a[b]!==k&&(a[c]=a[b])},ba=/\[.*?\]$/,U=/\(\)$/,Pa=m.util.escapeRegex,va=h("<div>")[0],Wb=va.textContent!==k,Yb=/<.*?>/g,Na=m.util.throttle,Rb=[],w=Array.prototype,ac=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof
h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};s=function(a,b){if(!(this instanceof s))return new s(a,b);var c=[],d=function(a){(a=ac(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;e<f;e++)d(a[e]);else d(a);this.context=qa(c);b&&h.merge(this,b);this.selector={rows:null,cols:null,opts:null};s.extend(this,this,Rb)};m.Api=s;h.extend(s.prototype,{any:function(){return 0!==this.count()},concat:w.concat,context:[],count:function(){return this.flatten().length},
each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=this.context;return b.length>a?new s(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);return new s(this.context,b)},flatten:function(){var a=[];return new s(this.context,a.concat.apply(a,this.toArray()))},join:w.join,indexOf:w.indexOf||function(a,b){for(var c=b||0,
d=this.length;c<d;c++)if(this[c]===a)return c;return-1},iterator:function(a,b,c,d){var e=[],f,g,j,h,n,l=this.context,m,o,u=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);g=0;for(j=l.length;g<j;g++){var r=new s(l[g]);if("table"===b)f=c.call(r,l[g],g),f!==k&&e.push(f);else if("columns"===b||"rows"===b)f=c.call(r,l[g],this[g],g),f!==k&&e.push(f);else if("column"===b||"column-rows"===b||"row"===b||"cell"===b){o=this[g];"column-rows"===b&&(m=Ba(l[g],u.opts));h=0;for(n=o.length;h<n;h++)f=o[h],f=
"cell"===b?c.call(r,l[g],f.row,f.column,g,h):c.call(r,l[g],f,g,h,m),f!==k&&e.push(f)}}return e.length||d?(a=new s(l,a?e.concat.apply([],e):e),b=a.selector,b.rows=u.rows,b.cols=u.cols,b.opts=u.opts,a):this},lastIndexOf:w.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,map:function(a){var b=[];if(w.map)b=w.map.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new s(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},
pop:w.pop,push:w.push,reduce:w.reduce||function(a,b){return gb(this,a,b,0,this.length,1)},reduceRight:w.reduceRight||function(a,b){return gb(this,a,b,this.length-1,-1,-1)},reverse:w.reverse,selector:null,shift:w.shift,slice:function(){return new s(this.context,this)},sort:w.sort,splice:w.splice,toArray:function(){return w.slice.call(this)},to$:function(){return h(this)},toJQuery:function(){return h(this)},unique:function(){return new s(this.context,qa(this))},unshift:w.unshift});s.extend=function(a,
b,c){if(c.length&&b&&(b instanceof s||b.__dt_wrapper)){var d,e,f,g=function(a,b,c){return function(){var d=b.apply(a,arguments);s.extend(d,d,c.methodExt);return d}};d=0;for(e=c.length;d<e;d++)f=c[d],b[f.name]="function"===typeof f.val?g(a,f.val,f):h.isPlainObject(f.val)?{}:f.val,b[f.name].__dt_wrapper=!0,s.extend(a,b[f.name],f.propExt)}};s.register=o=function(a,b){if(h.isArray(a))for(var c=0,d=a.length;c<d;c++)s.register(a[c],b);else for(var e=a.split("."),f=Rb,g,j,c=0,d=e.length;c<d;c++){g=(j=-1!==
e[c].indexOf("()"))?e[c].replace("()",""):e[c];var i;a:{i=0;for(var n=f.length;i<n;i++)if(f[i].name===g){i=f[i];break a}i=null}i||(i={name:g,val:{},methodExt:[],propExt:[]},f.push(i));c===d-1?i.val=b:f=j?i.methodExt:i.propExt}};s.registerPlural=u=function(a,b,c){s.register(a,c);s.register(b,function(){var a=c.apply(this,arguments);return a===this?this:a instanceof s?a.length?h.isArray(a[0])?new s(a.context,a[0]):a[0]:k:a})};o("tables()",function(a){var b;if(a){b=s;var c=this.context;if("number"===
typeof a)a=[c[a]];else var d=h.map(c,function(a){return a.nTable}),a=h(d).filter(a).map(function(){var a=h.inArray(this,d);return c[a]}).toArray();b=new b(a)}else b=this;return b});o("table()",function(a){var a=this.tables(a),b=a.context;return b.length?new s(b[0]):a});u("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});u("tables().body()","table().body()",function(){return this.iterator("table",function(a){return a.nTBody},1)});u("tables().header()",
"table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});u("tables().footer()","table().footer()",function(){return this.iterator("table",function(a){return a.nTFoot},1)});u("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});o("draw()",function(a){return this.iterator("table",function(b){"page"===a?N(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),S(b,!1===a))})});o("page()",function(a){return a===
k?this.page.info().page:this.iterator("table",function(b){Sa(b,a)})});o("page.info()",function(){if(0===this.context.length)return k;var a=this.context[0],b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),e=-1===c;return{page:e?0:Math.floor(b/c),pages:e?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,serverSide:"ssp"===y(a)}});o("page.len()",function(a){return a===k?0!==this.context.length?this.context[0]._iDisplayLength:
k:this.iterator("table",function(b){Qa(b,a)})});var Sb=function(a,b,c){if(c){var d=new s(a);d.one("draw",function(){c(d.ajax.json())})}if("ssp"==y(a))S(a,b);else{C(a,!0);var e=a.jqXHR;e&&4!==e.readyState&&e.abort();sa(a,[],function(c){na(a);for(var c=ta(a,c),d=0,e=c.length;d<e;d++)M(a,c[d]);S(a,b);C(a,!1)})}};o("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});o("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});o("ajax.reload()",function(a,
b){return this.iterator("table",function(c){Sb(c,!1===b,a)})});o("ajax.url()",function(a){var b=this.context;if(a===k){if(0===b.length)return k;b=b[0];return b.ajax?h.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(b){h.isPlainObject(b.ajax)?b.ajax.url=a:b.ajax=a})});o("ajax.url().load()",function(a,b){return this.iterator("table",function(c){Sb(c,!1===b,a)})});var Za=function(a,b,c,d,e){var f=[],g,j,i,n,l,m;i=typeof b;if(!b||"string"===i||"function"===
i||b.length===k)b=[b];i=0;for(n=b.length;i<n;i++){j=b[i]&&b[i].split&&!b[i].match(/[\[\(:]/)?b[i].split(","):[b[i]];l=0;for(m=j.length;l<m;l++)(g=c("string"===typeof j[l]?h.trim(j[l]):j[l]))&&g.length&&(f=f.concat(g))}a=x.selector[a];if(a.length){i=0;for(n=a.length;i<n;i++)f=a[i](d,e,f)}return qa(f)},$a=function(a){a||(a={});a.filter&&a.search===k&&(a.search=a.filter);return h.extend({search:"none",order:"current",page:"all"},a)},ab=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=
a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Ba=function(a,b){var c,d,e,f=[],g=a.aiDisplay;c=a.aiDisplayMaster;var j=b.search;d=b.order;e=b.page;if("ssp"==y(a))return"removed"===j?[]:W(0,c.length);if("current"==e){c=a._iDisplayStart;for(d=a.fnDisplayEnd();c<d;c++)f.push(g[c])}else if("current"==d||"applied"==d)f="none"==j?c.slice():"applied"==j?g.slice():h.map(c,function(a){return-1===h.inArray(a,g)?a:null});else if("index"==d||"original"==d){c=0;for(d=a.aoData.length;c<
d;c++)"none"==j?f.push(c):(e=h.inArray(c,g),(-1===e&&"removed"==j||0<=e&&"applied"==j)&&f.push(c))}return f};o("rows()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=$a(b),c=this.iterator("table",function(c){var e=b,f;return Za("row",a,function(a){var b=Nb(a);if(b!==null&&!e)return[b];f||(f=Ba(c,e));if(b!==null&&h.inArray(b,f)!==-1)return[b];if(a===null||a===k||a==="")return f;if(typeof a==="function")return h.map(f,function(b){var e=c.aoData[b];return a(b,e._aData,e.nTr)?b:null});
b=Qb(ia(c.aoData,f,"nTr"));if(a.nodeName){if(a._DT_RowIndex!==k)return[a._DT_RowIndex];if(a._DT_CellIndex)return[a._DT_CellIndex.row];b=h(a).closest("*[data-dt-row]");return b.length?[b.data("dt-row")]:[]}if(typeof a==="string"&&a.charAt(0)==="#"){var i=c.aIds[a.replace(/^#/,"")];if(i!==k)return[i.idx]}return h(b).filter(a).map(function(){return this._DT_RowIndex}).toArray()},c,e)},1);c.selector.rows=a;c.selector.opts=b;return c});o("rows().nodes()",function(){return this.iterator("row",function(a,
b){return a.aoData[b].nTr||k},1)});o("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return ia(a.aoData,b,"_aData")},1)});u("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){var d=b.aoData[c];return"search"===a?d._aFilterData:d._aSortData},1)});u("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){ca(b,c,a)})});u("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,
b){return b},1)});u("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,e=c.length;d<e;d++)for(var f=0,g=this[d].length;f<g;f++){var h=c[d].rowIdFn(c[d].aoData[this[d][f]]._aData);b.push((!0===a?"#":"")+h)}return new s(c,b)});u("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var e=b.aoData,f=e[c],g,h,i,n,l;e.splice(c,1);g=0;for(h=e.length;g<h;g++)if(i=e[g],l=i.anCells,null!==i.nTr&&(i.nTr._DT_RowIndex=g),null!==l){i=0;for(n=l.length;i<
n;i++)l[i]._DT_CellIndex.row=g}oa(b.aiDisplayMaster,c);oa(b.aiDisplay,c);oa(a[d],c,!1);0<b._iRecordsDisplay&&b._iRecordsDisplay--;Ra(b);c=b.rowIdFn(f._aData);c!==k&&delete b.aIds[c]});this.iterator("table",function(a){for(var c=0,d=a.aoData.length;c<d;c++)a.aoData[c].idx=c});return this});o("rows.add()",function(a){var b=this.iterator("table",function(b){var c,f,g,h=[];f=0;for(g=a.length;f<g;f++)c=a[f],c.nodeName&&"TR"===c.nodeName.toUpperCase()?h.push(ma(b,c)[0]):h.push(M(b,c));return h},1),c=this.rows(-1);
c.pop();h.merge(c,b);return c});o("row()",function(a,b){return ab(this.rows(a,b))});o("row().data()",function(a){var b=this.context;if(a===k)return b.length&&this.length?b[0].aoData[this[0]]._aData:k;b[0].aoData[this[0]]._aData=a;ca(b[0],this[0],"data");return this});o("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});o("row.add()",function(a){a instanceof h&&a.length&&(a=a[0]);var b=this.iterator("table",function(b){return a.nodeName&&
"TR"===a.nodeName.toUpperCase()?ma(b,a)[0]:M(b,a)});return this.row(b[0])});var bb=function(a,b){var c=a.context;if(c.length&&(c=c[0].aoData[b!==k?b:a[0]])&&c._details)c._details.remove(),c._detailsShow=k,c._details=k},Tb=function(a,b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];if(d._details){(d._detailsShow=b)?d._details.insertAfter(d.nTr):d._details.detach();var e=c[0],f=new s(e),g=e.aoData;f.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<D(g,
"_details").length&&(f.on("draw.dt.DT_details",function(a,b){e===b&&f.rows({page:"current"}).eq(0).each(function(a){a=g[a];a._detailsShow&&a._details.insertAfter(a.nTr)})}),f.on("column-visibility.dt.DT_details",function(a,b){if(e===b)for(var c,d=aa(b),f=0,h=g.length;f<h;f++)c=g[f],c._details&&c._details.children("td[colspan]").attr("colspan",d)}),f.on("destroy.dt.DT_details",function(a,b){if(e===b)for(var c=0,d=g.length;c<d;c++)g[c]._details&&bb(f,c)}))}}};o("row().child()",function(a,b){var c=this.context;
if(a===k)return c.length&&this.length?c[0].aoData[this[0]]._details:k;if(!0===a)this.child.show();else if(!1===a)bb(this);else if(c.length&&this.length){var d=c[0],c=c[0].aoData[this[0]],e=[],f=function(a,b){if(h.isArray(a)||a instanceof h)for(var c=0,k=a.length;c<k;c++)f(a[c],b);else a.nodeName&&"tr"===a.nodeName.toLowerCase()?e.push(a):(c=h("<tr><td/></tr>").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=aa(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e);c._detailsShow&&
c._details.insertAfter(c.nTr)}return this});o(["row().child.show()","row().child().show()"],function(){Tb(this,!0);return this});o(["row().child.hide()","row().child().hide()"],function(){Tb(this,!1);return this});o(["row().child.remove()","row().child().remove()"],function(){bb(this);return this});o("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var bc=/^([^:]+):(name|visIdx|visible)$/,Ub=function(a,b,c,d,e){for(var c=
[],d=0,f=e.length;d<f;d++)c.push(B(a,e[d],b));return c};o("columns()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=$a(b),c=this.iterator("table",function(c){var e=a,f=b,g=c.aoColumns,j=D(g,"sName"),i=D(g,"nTh");return Za("column",e,function(a){var b=Nb(a);if(a==="")return W(g.length);if(b!==null)return[b>=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Ub(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(bc):"";if(k)switch(k[2]){case "visIdx":case "visible":b=
parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[Z(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()",
"column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Ub,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});u("columns().cache()","column().cache()",
function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ia(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ia(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData,i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m=
h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(n=j.length;i<n;i++)l=j[i].nTr,f=j[i].anCells,l&&l.insertBefore(f[c],f[m]||null)}else h(D(b.aoData,"anCells",c)).detach();g.bVisible=a;ea(b,b.aoHeader);ea(b,b.aoFooter);xa(b)}});a!==k&&(this.iterator("column",function(c,e){r(c,null,"column-visibility",[c,e,a,b])}),(b===k||b)&&this.columns.adjust());return c});u("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===a?$(b,c):c},1)});o("columns.adjust()",
function(){return this.iterator("table",function(a){Y(a)},1)});o("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===a||"toData"===a)return Z(c,b);if("fromData"===a||"toVisible"===a)return $(c,b)}});o("column()",function(a,b){return ab(this.columns(a,b))});o("cells()",function(a,b,c){h.isPlainObject(a)&&(a.row===k?(c=a,a=null):(c=b,b=null));h.isPlainObject(b)&&(c=b,b=null);if(null===b||b===k)return this.iterator("table",function(b){var d=a,e=$a(c),f=
b.aoData,g=Ba(b,e),j=Qb(ia(f,g,"anCells")),i=h([].concat.apply([],j)),l,n=b.aoColumns.length,m,o,u,s,r,v;return Za("cell",d,function(a){var c=typeof a==="function";if(a===null||a===k||c){m=[];o=0;for(u=g.length;o<u;o++){l=g[o];for(s=0;s<n;s++){r={row:l,column:s};if(c){v=f[l];a(r,B(b,l,s),v.anCells?v.anCells[s]:null)&&m.push(r)}else m.push(r)}}return m}if(h.isPlainObject(a))return[a];c=i.filter(a).map(function(a,b){return{row:b._DT_CellIndex.row,column:b._DT_CellIndex.column}}).toArray();if(c.length||
!a.nodeName)return c;v=h(a).closest("*[data-dt-row]");return v.length?[{row:v.data("dt-row"),column:v.data("dt-column")}]:[]},b,e)});var d=this.columns(b,c),e=this.rows(a,c),f,g,j,i,n,l=this.iterator("table",function(a,b){f=[];g=0;for(j=e[b].length;g<j;g++){i=0;for(n=d[b].length;i<n;i++)f.push({row:e[b][g],column:d[b][i]})}return f},1);h.extend(l.selector,{cols:b,rows:a,opts:c});return l});u("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b])&&
a.anCells?a.anCells[c]:k},1)});o("cells().data()",function(){return this.iterator("cell",function(a,b,c){return B(a,b,c)},1)});u("cells().cache()","cell().cache()",function(a){a="search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});u("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return B(b,c,d,a)},1)});u("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,
b,c){return{row:b,column:c,columnVisible:$(a,c)}},1)});u("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(b,c,d){ca(b,c,a,d)})});o("cell()",function(a,b,c){return ab(this.cells(a,b,c))});o("cell().data()",function(a){var b=this.context,c=this[0];if(a===k)return b.length&&c.length?B(b[0],c[0].row,c[0].column):k;ib(b[0],c[0].row,c[0].column,a);ca(b[0],c[0].row,"data",c[0].column);return this});o("order()",function(a,b){var c=this.context;if(a===k)return 0!==
c.length?c[0].aaSorting:k;"number"===typeof a?a=[[a,b]]:a.length&&!h.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));return this.iterator("table",function(b){b.aaSorting=a.slice()})});o("order.listener()",function(a,b,c){return this.iterator("table",function(d){La(d,a,b,c)})});o("order.fixed()",function(a){if(!a){var b=this.context,b=b.length?b[0].aaSortingFixed:k;return h.isArray(b)?{pre:b}:b}return this.iterator("table",function(b){b.aaSortingFixed=h.extend(!0,{},a)})});o(["columns().order()",
"column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var e=[];h.each(b[d],function(b,c){e.push([c,a])});c.aaSorting=e})});o("search()",function(a,b,c,d){var e=this.context;return a===k?0!==e.length?e[0].oPreviousSearch.sSearch:k:this.iterator("table",function(e){e.oFeatures.bFilter&&fa(e,h.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});u("columns().search()","column().search()",function(a,
b,c,d){return this.iterator("column",function(e,f){var g=e.aoPreSearchCols;if(a===k)return g[f].sSearch;e.oFeatures.bFilter&&(h.extend(g[f],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),fa(e,e.oPreviousSearch,1))})});o("state()",function(){return this.context.length?this.context[0].oSavedState:null});o("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});o("state.loaded()",function(){return this.context.length?
this.context[0].oLoadedState:null});o("state.save()",function(){return this.iterator("table",function(a){xa(a)})});m.versionCheck=m.fnVersionCheck=function(a){for(var b=m.version.split("."),a=a.split("."),c,d,e=0,f=a.length;e<f;e++)if(c=parseInt(b[e],10)||0,d=parseInt(a[e],10)||0,c!==d)return c>d;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof m.Api)return!0;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?
h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new s(c):c};m.camelToHungarian=I;o("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){o(b+"()",function(){var a=Array.prototype.slice.call(arguments);
a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});o("clear()",function(){return this.iterator("table",function(a){na(a)})});o("settings()",function(){return new s(this.context,this.context)});o("init()",function(){var a=this.context;return a.length?a[0].oInit:null});o("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});o("destroy()",function(a){a=a||
!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),o;b.bDestroying=!0;r(b,"aoDestroyCallback","destroy",[b]);a||(new s(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));
b.aaSorting=[];b.aaSortingFixed=[];wa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable),(o=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%o])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,
1)})});h.each(["column","row","cell"],function(a,b){o(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,n){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,n)})})});o("i18n()",function(a,b,c){var d=this.context[0],a=Q(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.16";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow=
{nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,
sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,
this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+
"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",
sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};
X(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};X(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,
bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],
aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,
aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,
b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=x={buttons:{},classes:{},build:"dt/dt-1.10.16",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},
order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",
sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",
sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Kb=m.ext.pager;h.extend(Kb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ha(a,
b)]},simple_numbers:function(a,b){return["previous",ha(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ha(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ha(a,b),"last"]},_numbers:ha,numbers_length:7});h.extend(!0,m.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},n,l,m=0,o=function(b,d){var k,s,u,r,v=function(b){Sa(a,b.data.action,true)};k=0;for(s=d.length;k<s;k++){r=d[k];if(h.isArray(r)){u=
h("<"+(r.DT_el||"div")+"/>").appendTo(b);o(u,r)}else{n=null;l="";switch(r){case "ellipsis":b.append('<span class="ellipsis">&#x2026;</span>');break;case "first":n=j.sFirst;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":n=j.sPrevious;l=r+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":n=j.sNext;l=r+(e<f-1?"":" "+g.sPageButtonDisabled);break;case "last":n=j.sLast;l=r+(e<f-1?"":" "+g.sPageButtonDisabled);break;default:n=r+1;l=e===r?g.sPageButtonActive:""}if(n!==null){u=h("<a>",{"class":g.sPageButton+
" "+l,"aria-controls":a.sTableId,"aria-label":i[r],"data-dt-idx":m,tabindex:a.iTabIndex,id:c===0&&typeof r==="string"?a.sTableId+"_"+r:null}).html(n).appendTo(b);Va(u,{action:r},v);m++}}}},s;try{s=h(b).find(G.activeElement).data("dt-idx")}catch(u){}o(h(b).empty(),d);s!==k&&h(b).find("[data-dt-idx="+s+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!Zb.test(a))return null;var b=Date.parse(a);
return null!==b&&!isNaN(b)||L(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return Ya(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Pb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Pb(a,c,!0)?"html-num-fmt"+c:null},function(a){return L(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return L(a)?a:"string"===typeof a?a.replace(Mb," ").replace(Aa,""):""},string:function(a){return L(a)?
a:"string"===typeof a?a.replace(Mb," "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Ob(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){return Date.parse(a)||-Infinity},"html-pre":function(a){return L(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return L(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a<
b?-1:a>b?1:0},"string-desc":function(a,b){return a<b?1:a>b?-1:0}});cb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);
h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Vb=function(a){return"string"===typeof a?a.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,
"&quot;"):a};m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Vb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Vb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Lb,_fnBuildAjax:sa,_fnAjaxUpdate:kb,_fnAjaxParameters:tb,_fnAjaxUpdateDraw:ub,
_fnAjaxDataSrc:ta,_fnAddColumn:Da,_fnColumnOptions:ja,_fnAdjustColumnSizing:Y,_fnVisibleToColumnIndex:Z,_fnColumnIndexToVisible:$,_fnVisbleColumns:aa,_fnGetColumns:la,_fnColumnTypes:Fa,_fnApplyColumnDefs:hb,_fnHungarianMap:X,_fnCamelToHungarian:I,_fnLanguageCompat:Ca,_fnBrowserDetect:fb,_fnAddData:M,_fnAddTr:ma,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:ib,
_fnSplitObjNotation:Ia,_fnGetObjectDataFn:Q,_fnSetObjectDataFn:R,_fnGetDataMaster:Ja,_fnClearTable:na,_fnDeleteIndex:oa,_fnInvalidate:ca,_fnGetRowElements:Ha,_fnCreateTr:Ga,_fnBuildHead:jb,_fnDrawHead:ea,_fnDraw:N,_fnReDraw:S,_fnAddOptionsHtml:mb,_fnDetectHeader:da,_fnGetUniqueThs:ra,_fnFeatureHtmlFilter:ob,_fnFilterComplete:fa,_fnFilterCustom:xb,_fnFilterColumn:wb,_fnFilter:vb,_fnFilterCreateSearch:Oa,_fnEscapeRegex:Pa,_fnFilterData:yb,_fnFeatureHtmlInfo:rb,_fnUpdateInfo:Bb,_fnInfoMacros:Cb,_fnInitialise:ga,
_fnInitComplete:ua,_fnLengthChange:Qa,_fnFeatureHtmlLength:nb,_fnFeatureHtmlPaginate:sb,_fnPageChange:Sa,_fnFeatureHtmlProcessing:pb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:qb,_fnScrollDraw:ka,_fnApplyToChildren:H,_fnCalculateColumnWidths:Ea,_fnThrottle:Na,_fnConvertToWidth:Db,_fnGetWidestNode:Eb,_fnGetMaxLenString:Fb,_fnStringToCss:v,_fnSortFlatten:V,_fnSort:lb,_fnSortAria:Hb,_fnSortListener:Ua,_fnSortAttachListener:La,_fnSortingClasses:wa,_fnSortData:Gb,_fnSaveState:xa,_fnLoadState:Ib,_fnSettingsFromNode:ya,
_fnLog:J,_fnMap:F,_fnBindAction:Va,_fnCallbackReg:z,_fnCallbackFire:r,_fnLengthOverflow:Ra,_fnRenderer:Ma,_fnDataSource:y,_fnRowAttributes:Ka,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable});

BIN
code/html/vendor/images/border-off.png View File

Before After
Width: 4  |  Height: 27  |  Size: 308 B

BIN
code/html/vendor/images/border-on.png View File

Before After
Width: 4  |  Height: 27  |  Size: 302 B

BIN
code/html/vendor/images/handle-center.png View File

Before After
Width: 4  |  Height: 27  |  Size: 190 B

BIN
code/html/vendor/images/handle-left.png View File

Before After
Width: 4  |  Height: 27  |  Size: 264 B

BIN
code/html/vendor/images/handle-right.png View File

Before After
Width: 4  |  Height: 27  |  Size: 264 B

BIN
code/html/vendor/images/label-off.png View File

Before After
Width: 4  |  Height: 27  |  Size: 211 B

BIN
code/html/vendor/images/label-on.png View File

Before After
Width: 4  |  Height: 27  |  Size: 214 B

BIN
code/html/vendor/images/sort_asc.png View File

Before After
Width: 19  |  Height: 19  |  Size: 160 B

BIN
code/html/vendor/images/sort_both.png View File

Before After
Width: 19  |  Height: 19  |  Size: 201 B

BIN
code/html/vendor/images/sort_desc.png View File

Before After
Width: 19  |  Height: 19  |  Size: 158 B

+ 2
- 4
code/memanalyzer.py View File

@ -55,7 +55,7 @@ def file_size(file):
def analyse_memory(elf_file):
command = "%s -t '%s' " % (objdump_binary, elf_file)
command = "%s -t '%s'" % (objdump_binary, elf_file)
response = subprocess.check_output(shlex.split(command))
if isinstance(response, bytes):
response = response.decode('utf-8')
@ -102,7 +102,7 @@ def run(env_, modules_):
flags = ""
for item in modules_.items():
flags += "-D%s_SUPPORT=%d " % item
command = "export ESPURNA_BOARD=\"WEMOS_D1_MINI_RELAYSHIELD\"; export ESPURNA_FLAGS=\"%s\"; platformio run --silent --environment %s" % (flags, env_)
command = "ESPURNA_BOARD=\"WEMOS_D1_MINI_RELAYSHIELD\" ESPURNA_FLAGS=\"%s\" platformio run --silent --environment %s 2>/dev/null" % (flags, env_)
subprocess.check_call(command, shell=True)
@ -251,6 +251,4 @@ try:
except:
raise
subprocess.check_call("export ESPURNA_BOARD=\"\"; export ESPURNA_FLAGS=\"\"", shell=True)
print("\n")

+ 42
- 16
code/ota.py View File

@ -8,6 +8,7 @@
# -------------------------------------------------------------------------------
from __future__ import print_function
import shutil
import argparse
import re
import socket
@ -27,7 +28,7 @@ except NameError:
DISCOVER_TIMEOUT = 2
description = "ESPurna OTA Manager v0.2"
description = "ESPurna OTA Manager v0.3"
devices = []
discover_last = 0
@ -111,6 +112,11 @@ def get_boards():
boards.append(m.group(1))
return sorted(boards)
def get_device_size(device):
if device.get('mem_size', 0) == device.get('sdk_size', 0):
return int(device.get('mem_size', 0)) / 1024
return 0
def get_empty_board():
"""
Returns the empty structure of a board to flash
@ -128,9 +134,26 @@ def get_board_by_index(index):
board['hostname'] = device.get('hostname')
board['board'] = device.get('target_board', '')
board['ip'] = device.get('ip', '')
board['size'] = int(device.get('mem_size', 0) if device.get('mem_size', 0) == device.get('sdk_size', 0) else 0) / 1024
board['size'] = get_device_size(device)
return board
def get_board_by_mac(mac):
"""
Returns the required data to flash a given board
"""
hostname = hostname.lower()
for device in devices:
if device.get('mac', '').lower() == mac:
board = {}
board['hostname'] = device.get('hostname')
board['board'] = device.get('device')
board['ip'] = device.get('ip')
board['size'] = get_device_size(device)
if not board['board'] or not board['ip'] or board['size'] == 0:
return None
return board
return None
def get_board_by_hostname(hostname):
"""
Returns the required data to flash a given board
@ -141,13 +164,9 @@ def get_board_by_hostname(hostname):
board = {}
board['hostname'] = device.get('hostname')
board['board'] = device.get('target_board')
if not board['board']:
return None
board['ip'] = device.get('ip')
if not board['ip']:
return None
board['size'] = int(device.get('sdk_size', 0)) / 1024
if board['size'] == 0:
board['size'] = get_device_size(device)
if not board['board'] or not board['ip'] or board['size'] == 0:
return None
return board
return None
@ -201,13 +220,20 @@ def input_board():
return board
def boardname(board):
return board.get('hostname', board['ip'])
def store(device, env):
source = ".pioenvs/%s/firmware.elf" % env
destination = ".pioenvs/elfs/%s.elf" % boardname(device).lower()
shutil.move(source, destination)
def run(device, env):
print("Building and flashing image over-the-air...")
command = "export ESPURNA_IP=\"%s\"; export ESPURNA_BOARD=\"%s\"; export ESPURNA_AUTH=\"%s\"; export ESPURNA_FLAGS=\"%s\"; platformio run --silent --environment %s -t upload"
command = "ESPURNA_IP=\"%s\" ESPURNA_BOARD=\"%s\" ESPURNA_AUTH=\"%s\" ESPURNA_FLAGS=\"%s\" platformio run --silent --environment %s -t upload"
command = command % (device['ip'], device['board'], device['auth'], device['flags'], env)
subprocess.check_call(command, shell=True)
store(device, env)
# -------------------------------------------------------------------------------
@ -287,12 +313,12 @@ if __name__ == '__main__':
# Summary
print()
print("HOST = %s" % board.get('hostname', board['ip']))
print("IP = %s" % board['ip'])
print("BOARD = %s" % board['board'])
print("AUTH = %s" % board['auth'])
print("FLAGS = %s" % board['flags'])
print("ENV = %s" % env)
print("HOST = %s" % boardname(board))
print("IP = %s" % board['ip'])
print("BOARD = %s" % board['board'])
print("AUTH = %s" % board['auth'])
print("FLAGS = %s" % board['flags'])
print("ENV = %s" % env)
response = True
if args.yes == 0:


+ 270
- 7
code/package-lock.json View File

@ -48,6 +48,15 @@
"ansi-wrap": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz"
}
},
"ansi-cyan": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz",
"integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=",
"dev": true,
"requires": {
"ansi-wrap": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz"
}
},
"ansi-gray": {
"version": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
"integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=",
@ -56,6 +65,15 @@
"ansi-wrap": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz"
}
},
"ansi-red": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz",
"integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=",
"dev": true,
"requires": {
"ansi-wrap": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz"
}
},
"ansi-regex": {
"version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
@ -841,6 +859,16 @@
"clone": "https://registry.npmjs.org/clone/-/clone-1.0.3.tgz"
}
},
"define-properties": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz",
"integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=",
"dev": true,
"requires": {
"foreach": "2.0.5",
"object-keys": "1.0.11"
}
},
"define-property": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
@ -981,6 +1009,30 @@
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
"dev": true
},
"es-abstract": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz",
"integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==",
"dev": true,
"requires": {
"es-to-primitive": "1.1.1",
"function-bind": "1.1.1",
"has": "1.0.3",
"is-callable": "1.1.3",
"is-regex": "1.0.4"
}
},
"es-to-primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz",
"integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=",
"dev": true,
"requires": {
"is-callable": "1.1.3",
"is-date-object": "1.0.1",
"is-symbol": "1.0.1"
}
},
"escape-string-regexp": {
"version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
@ -999,11 +1051,19 @@
"requires": {
"duplexer": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
"from": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
"map-stream": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"map-stream": "0.1.0",
"pause-stream": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"split": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
"stream-combiner": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
"through": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
},
"dependencies": {
"map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
"dev": true
}
}
},
"expand-brackets": {
@ -1273,6 +1333,12 @@
"for-in": "1.0.2"
}
},
"foreach": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
"dev": true
},
"forever-agent": {
"version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
@ -1307,6 +1373,12 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"gaze": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz",
@ -1579,7 +1651,7 @@
},
"gulp-csslint": {
"version": "https://registry.npmjs.org/gulp-csslint/-/gulp-csslint-1.0.1.tgz",
"integrity": "sha1-ESqQj3rvmO/Ce3vQCAHxOne+y5M=",
"integrity": "sha512-Rec56+RpCGg7feK3d/S45oqgxyLV3end0ed+UjWFv6YziQae2Bp4DNSDobwEvJdfCAsOhOSExEEB+jcfMx430w==",
"dev": true,
"requires": {
"csslint": "https://registry.npmjs.org/csslint/-/csslint-1.0.5.tgz",
@ -1602,7 +1674,7 @@
},
"plugin-error": {
"version": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz",
"integrity": "sha1-dwFr2JGdCsN3/c3QMiMolTyleBw=",
"integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==",
"dev": true,
"requires": {
"ansi-colors": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
@ -1658,8 +1730,9 @@
}
},
"gulp-htmllint": {
"version": "https://registry.npmjs.org/gulp-htmllint/-/gulp-htmllint-0.0.14.tgz",
"integrity": "sha1-fobrpgVmjfsVstLVAZX+VqdVhaI=",
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/gulp-htmllint/-/gulp-htmllint-0.0.14.tgz",
"integrity": "sha512-D5FfR2G4SUwxPd/ypE5glZSXXddl3bnGz9JlY3gS5e8AXSvYJco+baIWtpT8fq5aqyr5IELf2I+VgCe6b+yq1A==",
"dev": true,
"requires": {
"gulp-util": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz",
@ -1720,6 +1793,66 @@
"through2": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz"
}
},
"gulp-remove-code": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gulp-remove-code/-/gulp-remove-code-3.0.2.tgz",
"integrity": "sha512-Ptrwce+Nq8B+zPMomMzjfkEpldBpbHDOtE4Py0jyxh3eE7scfzEjxAs0XojwFw4xBkLWuNdqetZhPk7zsuyxAw==",
"dev": true,
"requires": {
"bufferstreams": "2.0.1",
"escape-string-regexp": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"object.entries": "1.0.4",
"plugin-error": "1.0.1",
"through2": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz"
},
"dependencies": {
"bufferstreams": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-2.0.1.tgz",
"integrity": "sha512-ZswyIoBfFb3cVDsnZLLj2IDJ/0ppYdil/v2EGlZXvoefO689FokEmFEldhN5dV7R2QBxFneqTJOMIpfqhj+n0g==",
"dev": true,
"requires": {
"readable-stream": "2.3.6"
}
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"isarray": "1.0.0",
"process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"string_decoder": "1.1.1",
"util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz"
}
}
}
},
"gulp-rename": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.3.0.tgz",
"integrity": "sha512-nEuZB7/9i0IZ8AXORTizl2QLP9tcC9uWc/s329zElBLJw1CfOhmMXBxwVlCRKjDyrWuhVP0uBKl61KeQ32TiCg==",
"dev": true
},
"gulp-replace": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.0.0.tgz",
@ -1847,6 +1980,15 @@
"har-schema": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "1.1.1"
}
},
"has-ansi": {
"version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
@ -2103,6 +2245,12 @@
"integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=",
"dev": true
},
"is-callable": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz",
"integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=",
"dev": true
},
"is-data-descriptor": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
@ -2112,6 +2260,12 @@
"kind-of": "6.0.2"
}
},
"is-date-object": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
"dev": true
},
"is-descriptor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
@ -2218,6 +2372,15 @@
"isobject": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
}
},
"is-regex": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
"integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
"dev": true,
"requires": {
"has": "1.0.3"
}
},
"is-relative": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
@ -2227,6 +2390,12 @@
"is-unc-path": "1.0.0"
}
},
"is-symbol": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz",
"integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
"dev": true
},
"is-typedarray": {
"version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
@ -2601,8 +2770,9 @@
"dev": true
},
"map-stream": {
"version": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz",
"integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=",
"dev": true
},
"map-visit": {
@ -2846,6 +3016,12 @@
}
}
},
"object-keys": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz",
"integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=",
"dev": true
},
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
@ -2867,6 +3043,18 @@
"isobject": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
}
},
"object.entries": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz",
"integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=",
"dev": true,
"requires": {
"define-properties": "1.1.2",
"es-abstract": "1.12.0",
"function-bind": "1.1.1",
"has": "1.0.3"
}
},
"object.map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
@ -3038,6 +3226,18 @@
"pinkie": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
}
},
"plugin-error": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz",
"integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==",
"dev": true,
"requires": {
"ansi-colors": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
"arr-diff": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
"arr-union": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
"extend-shallow": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz"
}
},
"posix-character-classes": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@ -3321,6 +3521,69 @@
"glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz"
}
},
"run-sequence": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-2.2.1.tgz",
"integrity": "sha512-qkzZnQWMZjcKbh3CNly2srtrkaO/2H/SI5f2eliMCapdRD3UhMrwjfOAZJAnZ2H8Ju4aBzFZkBGXUqFs9V0yxw==",
"dev": true,
"requires": {
"chalk": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"fancy-log": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz",
"plugin-error": "0.1.2"
},
"dependencies": {
"arr-diff": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
"integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=",
"dev": true,
"requires": {
"arr-flatten": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
"array-slice": "0.2.3"
}
},
"arr-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
"integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=",
"dev": true
},
"array-slice": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
"integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
"dev": true
},
"extend-shallow": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
"integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=",
"dev": true,
"requires": {
"kind-of": "1.1.0"
}
},
"kind-of": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
"integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=",
"dev": true
},
"plugin-error": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
"integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=",
"dev": true,
"requires": {
"ansi-cyan": "0.1.1",
"ansi-red": "0.1.1",
"arr-diff": "1.1.0",
"arr-union": "2.1.0",
"extend-shallow": "1.1.4"
}
}
}
},
"safe-buffer": {
"version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=",


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

Loading…
Cancel
Save