Compare commits
107
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bb358c961 | ||
|
|
12a8391cd7 | ||
|
|
b6dc04920a | ||
|
|
aca1736201 | ||
|
|
fdf986a61e | ||
|
|
5c0822b704 | ||
|
|
fad4f2c707 | ||
|
|
84530f76fd | ||
|
|
fa208858d9 | ||
|
|
6d452ecbc0 | ||
|
|
23fbddb055 | ||
|
|
fe2a209e44 | ||
|
|
82905a8cdd | ||
|
|
3751904cb4 | ||
|
|
bcf7625285 | ||
|
|
4a3e79f02e | ||
|
|
68e1073858 | ||
|
|
f73d45404f | ||
|
|
ecc7c46b8d | ||
|
|
0dd5937707 | ||
|
|
547080acf5 | ||
|
|
d3c699aefa | ||
|
|
a8d19cd6e1 | ||
|
|
38d48ab0e4 | ||
|
|
51bef05465 | ||
|
|
4eef69516e | ||
|
|
9175193b67 | ||
|
|
65118fbc42 | ||
|
|
076f0e9dfd | ||
|
|
571e969bc4 | ||
|
|
8e15f87cd3 | ||
|
|
dd9e1538c8 | ||
|
|
001e275131 | ||
|
|
196021bef5 | ||
|
|
63b9616677 | ||
|
|
d4c9a6d582 | ||
|
|
5fe66fdaef | ||
|
|
6445dc0fb8 | ||
|
|
7a20cf4b04 | ||
|
|
bbf77c6b1e | ||
|
|
b805d1b183 | ||
|
|
07b1ea3a5c | ||
|
|
3b0410f560 | ||
|
|
8f19b990ff | ||
|
|
519ac0e3bd | ||
|
|
651843fb06 | ||
|
|
fcbbdce118 | ||
|
|
6f8683ba9d | ||
|
|
710b8a2cdc | ||
|
|
b989784fb9 | ||
|
|
94489618ca | ||
|
|
82d8f07eea | ||
|
|
20041dd483 | ||
|
|
4f9174d362 | ||
|
|
68ecc05712 | ||
|
|
5fad39ee0e | ||
|
|
01f513c97b | ||
|
|
3bfbea92d8 | ||
|
|
d818624287 | ||
|
|
d92388d11f | ||
|
|
37df309127 | ||
|
|
be8a124803 | ||
|
|
104236dd0c | ||
|
|
e1dd004cf5 | ||
|
|
b5ec78ab41 | ||
|
|
fff9d9bc61 | ||
|
|
ef47c771ef | ||
|
|
9f442259e9 | ||
|
|
8e5a3195b9 | ||
|
|
cc4729eb6b | ||
|
|
f7c4b0d70a | ||
|
|
566068f7cd | ||
|
|
5c15a7d4cb | ||
|
|
b9a4770ff2 | ||
|
|
e471a57578 | ||
|
|
6e05900b5a | ||
|
|
15f6d78128 | ||
|
|
b32f7d1228 | ||
|
|
45fef23bad | ||
|
|
e20e6b7d3e | ||
|
|
0531b599fe | ||
|
|
a5751eec79 | ||
|
|
e02b8571f6 | ||
|
|
303a8d3877 | ||
|
|
6d00474315 | ||
|
|
46fb4c7615 | ||
|
|
48c93ed043 | ||
|
|
2d1f049444 | ||
|
|
c313f6eb72 | ||
|
|
cccdc9cedb | ||
|
|
429979c6d1 | ||
|
|
e28a541fe9 | ||
|
|
0f2b8c6564 | ||
|
|
5f682c303f | ||
|
|
dcbb42f5ef | ||
|
|
235ef8c39d | ||
|
|
4d59c66354 | ||
|
|
25fa963752 | ||
|
|
7695e6196c | ||
|
|
b5c29eaa38 | ||
|
|
f29e38160a | ||
|
|
842d7becf0 | ||
|
|
417421ae31 | ||
|
|
393db24175 | ||
|
|
815f11591c | ||
|
|
fcf0e8b7d3 | ||
|
|
8ba724e682 |
@@ -1,3 +1,5 @@
|
|||||||
.pio
|
.pio
|
||||||
.pioenvs
|
.pioenvs
|
||||||
.piolibdeps
|
.piolibdeps
|
||||||
|
.vscode
|
||||||
|
include/config.h
|
||||||
Vendored
-226
@@ -1,226 +0,0 @@
|
|||||||
{
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "!!! WARNING !!! AUTO-GENERATED FILE, PLEASE DO NOT MODIFY IT AND USE https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Mac",
|
|
||||||
"macFrameworkPath": [],
|
|
||||||
"includePath": [
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/include",
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/src",
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/lib/pubsubclient/src",
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/lib/esp8266FTPServer",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SD/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SPIFFS/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/FS/src",
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/.pio/libdeps/esp32/MFRC522_ID63/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SPI/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/config",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/app_trace",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/app_update",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/asio",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/bootloader_support",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/bt",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/coap",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/console",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/driver",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp-tls",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp32",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_adc_cal",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_event",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_http_client",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_http_server",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_https_ota",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_ringbuf",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/ethernet",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/expat",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/fatfs",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/freemodbus",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/freertos",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/heap",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/idf_test",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/jsmn",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/json",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/libsodium",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/log",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/lwip",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mbedtls",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mdns",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/micro-ecc",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mqtt",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/newlib",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/nghttp",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/nvs_flash",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/openssl",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/protobuf-c",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/protocomm",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/pthread",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/sdmmc",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/smartconfig_ack",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/soc",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/spi_flash",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/spiffs",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/tcp_transport",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/tcpip_adapter",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/ulp",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/vfs",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wear_levelling",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wifi_provisioning",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wpa_supplicant",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/xtensa-debug-module",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp32-camera",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp-face",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/fb_gfx",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/cores/esp32",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/variants/esp32",
|
|
||||||
"/Users/fabian/.platformio/lib/Adafruit MCP23017 Arduino Library_ID334",
|
|
||||||
"/Users/fabian/.platformio/lib/FastLED_ID126",
|
|
||||||
"/Users/fabian/.platformio/lib/PubSubClient_ID89/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/ArduinoOTA/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/AsyncUDP/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/AzureIoT/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/BLE/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/BluetoothSerial/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/DNSServer/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/EEPROM/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/ESP32/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/ESPmDNS/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/FFat/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/HTTPClient/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/HTTPUpdate/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/NetBIOS/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/Preferences/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SD_MMC/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SimpleBLE/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/Ticker/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/Update/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/WebServer/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/WiFiClientSecure/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/Wire/src",
|
|
||||||
"/Users/fabian/.platformio/packages/tool-unity",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"browse": {
|
|
||||||
"limitSymbolsToIncludedHeaders": true,
|
|
||||||
"path": [
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/include",
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/src",
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/lib/pubsubclient/src",
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/lib/esp8266FTPServer",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SD/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SPIFFS/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/FS/src",
|
|
||||||
"/Users/fabian/Documents/PlatformIO/Projects/esmp3/.pio/libdeps/esp32/MFRC522_ID63/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SPI/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/config",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/app_trace",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/app_update",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/asio",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/bootloader_support",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/bt",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/coap",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/console",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/driver",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp-tls",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp32",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_adc_cal",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_event",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_http_client",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_http_server",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_https_ota",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_ringbuf",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/ethernet",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/expat",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/fatfs",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/freemodbus",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/freertos",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/heap",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/idf_test",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/jsmn",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/json",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/libsodium",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/log",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/lwip",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mbedtls",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mdns",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/micro-ecc",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mqtt",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/newlib",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/nghttp",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/nvs_flash",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/openssl",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/protobuf-c",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/protocomm",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/pthread",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/sdmmc",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/smartconfig_ack",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/soc",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/spi_flash",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/spiffs",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/tcp_transport",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/tcpip_adapter",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/ulp",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/vfs",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wear_levelling",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wifi_provisioning",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wpa_supplicant",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/xtensa-debug-module",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp32-camera",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp-face",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/fb_gfx",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/cores/esp32",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/variants/esp32",
|
|
||||||
"/Users/fabian/.platformio/lib/Adafruit MCP23017 Arduino Library_ID334",
|
|
||||||
"/Users/fabian/.platformio/lib/FastLED_ID126",
|
|
||||||
"/Users/fabian/.platformio/lib/PubSubClient_ID89/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/ArduinoOTA/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/AsyncUDP/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/AzureIoT/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/BLE/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/BluetoothSerial/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/DNSServer/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/EEPROM/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/ESP32/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/ESPmDNS/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/FFat/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/HTTPClient/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/HTTPUpdate/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/NetBIOS/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/Preferences/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SD_MMC/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/SimpleBLE/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/Ticker/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/Update/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/WebServer/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/WiFiClientSecure/src",
|
|
||||||
"/Users/fabian/.platformio/packages/framework-arduinoespressif32/libraries/Wire/src",
|
|
||||||
"/Users/fabian/.platformio/packages/tool-unity",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"defines": [
|
|
||||||
"PLATFORMIO=40100",
|
|
||||||
"ARDUINO_ESP32_DEV",
|
|
||||||
"VERSION=\"0.1-12-ged788a9-dirty\"",
|
|
||||||
"ESP32",
|
|
||||||
"ESP_PLATFORM",
|
|
||||||
"F_CPU=240000000L",
|
|
||||||
"HAVE_CONFIG_H",
|
|
||||||
"MBEDTLS_CONFIG_FILE=\"mbedtls/esp_config.h\"",
|
|
||||||
"ARDUINO=10805",
|
|
||||||
"ARDUINO_ARCH_ESP32",
|
|
||||||
"ARDUINO_VARIANT=\"esp32\"",
|
|
||||||
"ARDUINO_BOARD=\"Espressif ESP-WROVER-KIT\"",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"intelliSenseMode": "clang-x64",
|
|
||||||
"cStandard": "c99",
|
|
||||||
"cppStandard": "c++11",
|
|
||||||
"compilerPath": "\"/Users/fabian/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc\" -mlongcalls"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version": 4
|
|
||||||
}
|
|
||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
|
||||||
// for the documentation about the extensions.json format
|
|
||||||
"recommendations": [
|
|
||||||
"platformio.platformio-ide"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Vendored
-32
@@ -1,32 +0,0 @@
|
|||||||
// AUTOMATICALLY GENERATED FILE. PLEASE DO NOT MODIFY IT MANUALLY
|
|
||||||
|
|
||||||
// PIO Unified Debugger
|
|
||||||
//
|
|
||||||
// Documentation: https://docs.platformio.org/page/plus/debugging.html
|
|
||||||
// Configuration: https://docs.platformio.org/page/projectconf/section_env_debug.html
|
|
||||||
|
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "platformio-debug",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "PIO Debug",
|
|
||||||
"executable": "/Users/fabian/Documents/PlatformIO/Projects/esmp3/.pio/build/esp32/firmware.elf",
|
|
||||||
"toolchainBinDir": "/Users/fabian/.platformio/packages/toolchain-xtensa32/bin",
|
|
||||||
"preLaunchTask": {
|
|
||||||
"type": "PlatformIO",
|
|
||||||
"task": "Pre-Debug"
|
|
||||||
},
|
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "platformio-debug",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "PIO Debug (skip Pre-Debug)",
|
|
||||||
"executable": "/Users/fabian/Documents/PlatformIO/Projects/esmp3/.pio/build/esp32/firmware.elf",
|
|
||||||
"toolchainBinDir": "/Users/fabian/.platformio/packages/toolchain-xtensa32/bin",
|
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Vendored
-54
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"files.associations": {
|
|
||||||
"__debug": "cpp",
|
|
||||||
"__functional_base": "cpp",
|
|
||||||
"__node_handle": "cpp",
|
|
||||||
"__nullptr": "cpp",
|
|
||||||
"__tree": "cpp",
|
|
||||||
"__tuple": "cpp",
|
|
||||||
"algorithm": "cpp",
|
|
||||||
"atomic": "cpp",
|
|
||||||
"cstddef": "cpp",
|
|
||||||
"cstdint": "cpp",
|
|
||||||
"cstdio": "cpp",
|
|
||||||
"cstdlib": "cpp",
|
|
||||||
"cstring": "cpp",
|
|
||||||
"exception": "cpp",
|
|
||||||
"functional": "cpp",
|
|
||||||
"initializer_list": "cpp",
|
|
||||||
"iosfwd": "cpp",
|
|
||||||
"iterator": "cpp",
|
|
||||||
"limits": "cpp",
|
|
||||||
"memory": "cpp",
|
|
||||||
"new": "cpp",
|
|
||||||
"optional": "cpp",
|
|
||||||
"stdexcept": "cpp",
|
|
||||||
"tuple": "cpp",
|
|
||||||
"type_traits": "cpp",
|
|
||||||
"typeinfo": "cpp",
|
|
||||||
"utility": "cpp",
|
|
||||||
"array": "cpp",
|
|
||||||
"*.tcc": "cpp",
|
|
||||||
"cctype": "cpp",
|
|
||||||
"clocale": "cpp",
|
|
||||||
"cmath": "cpp",
|
|
||||||
"cstdarg": "cpp",
|
|
||||||
"ctime": "cpp",
|
|
||||||
"cwchar": "cpp",
|
|
||||||
"cwctype": "cpp",
|
|
||||||
"deque": "cpp",
|
|
||||||
"list": "cpp",
|
|
||||||
"unordered_map": "cpp",
|
|
||||||
"unordered_set": "cpp",
|
|
||||||
"vector": "cpp",
|
|
||||||
"fstream": "cpp",
|
|
||||||
"iomanip": "cpp",
|
|
||||||
"istream": "cpp",
|
|
||||||
"ostream": "cpp",
|
|
||||||
"numeric": "cpp",
|
|
||||||
"sstream": "cpp",
|
|
||||||
"streambuf": "cpp",
|
|
||||||
"system_error": "cpp",
|
|
||||||
"cinttypes": "cpp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,94 @@
|
|||||||
# Ports
|
# ESMP3
|
||||||
|
|
||||||
|
## What you need
|
||||||
|
Please note: This list is a "things I used", neither
|
||||||
|
"these are the best things for this stuff" nor "you
|
||||||
|
can only use these things". But please be aware that
|
||||||
|
using other stuff may lead to you having to make
|
||||||
|
more or less easy modifications.
|
||||||
|
|
||||||
|
Prizes are more or less the cheapest I could find on
|
||||||
|
Aliexpress.
|
||||||
|
|
||||||
|
| What? | For what? | Price (approx) |
|
||||||
|
|-------|-----------|----------------|
|
||||||
|
| ESP-32-WROOM-32D | Controlling everything | 4€ |
|
||||||
|
| WS1053B on a PCB with SD card slot | Play the MP3 files; provide an SD card slot | 5€ |
|
||||||
|
| MFRC522 | RFID reader | 1€ |
|
||||||
|
| 5V Amplifier(s) (e.g. 2x PAM-8302) | Single-channel Amplifier | 2€ |
|
||||||
|
| Speaker(s) matching your amp (e.g. 2pcs 4 Ohm 5W) | Enabling you to hear the sounds | 4€ |
|
||||||
|
| RFID tags (ISO14443A) - e.g. 10 cards | You can also get Keyfobs or stickers | 4€ |
|
||||||
|
| 4 buttons | Prev/Next track, Volume up/down | 1€ |
|
||||||
|
|
||||||
|
You'all also need an SD card, some breadboard(s), jumper cables and a soldering iron.
|
||||||
|
Also, some kind of box for the finished player.
|
||||||
|
|
||||||
|
## How to connect
|
||||||
|
|
||||||
|
Schematics coming soon...ish...
|
||||||
|
|
||||||
|
## How to install
|
||||||
|
|
||||||
|
Format your SD card with FAT32 and put files on it: Every album has
|
||||||
|
to go into its own folder in the root of the SD card. Folders and files
|
||||||
|
should not contain special characters (meaning stuff like äöüß). Spaces
|
||||||
|
and dashes an alike are okay. Put the SD card into the SD card slot.
|
||||||
|
|
||||||
|
Copy `include/config.sample.h` to `include/config.h`. Modify it to at
|
||||||
|
least contain the correct login details for your WiFi.
|
||||||
|
|
||||||
|
The code then should compile in PlatformIO without errors. Upload it
|
||||||
|
to your ESP32. After that, upload static files using PlatformIO's task
|
||||||
|
"Upload file system image".
|
||||||
|
|
||||||
|
The serial console in PlatformIO should give you more or less useful
|
||||||
|
messages about what's going on. There will also be a line saying
|
||||||
|
"WiFi connected. IP address: xxx.xxx.xxx.xxx" when the connection to
|
||||||
|
your WiFi succeeded.
|
||||||
|
|
||||||
|
In your browser, enter "http://xxx.xxx.xxx.xxx/" (using the IP address)
|
||||||
|
from above. From there you can define mappings between RFID tag IDs and
|
||||||
|
folders on the SD card.
|
||||||
|
|
||||||
|
## RFID-folder-mappings
|
||||||
|
|
||||||
|
### Via webinterface
|
||||||
|
|
||||||
|
To create a new mapping between an RFID tag and an folder, you can use
|
||||||
|
the web interface. Click the button with the cogs icon. After putting
|
||||||
|
your rfid tag on the reader (and possibly removing it again), its ID
|
||||||
|
will be shown in the dialog. Click the button with the arrows behind
|
||||||
|
the ID to start the mapping mode.
|
||||||
|
|
||||||
|
The dialog showing all folders with media files will be shown. Click the
|
||||||
|
button with the arrows behind the correct folder, to create the mapping.
|
||||||
|
|
||||||
|
### Manually
|
||||||
|
|
||||||
|
Mapping are stored on the SD card in the file `/_mapping.txt`. Every
|
||||||
|
mapping goes on its own line. Lines should be separated by \n (Unix-
|
||||||
|
style line endings); the last line should also end with a newline.
|
||||||
|
|
||||||
|
Format of a line is `<RFID id>=<folder>`. RFID id is the UID of an
|
||||||
|
RFID tag, expressed as 8 lowercase characters with leading 0 (if
|
||||||
|
necessary). Folder is the foldername to play; starting with a slash and
|
||||||
|
ending without one.
|
||||||
|
|
||||||
|
A valid `_mapping.txt` could look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
1a2b3c4d=/Christmas Music Vol. 17
|
||||||
|
003aab7f=/Let it go
|
||||||
|
b691a22c=/Frozen Audiobook
|
||||||
|
22cb6ae9=/Let it go
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
(Yes, more than one tag can map to a folder.)
|
||||||
|
|
||||||
|
## Technical details
|
||||||
|
|
||||||
|
### Ports
|
||||||
|
|
||||||
| Device | Port | Connected to |
|
| Device | Port | Connected to |
|
||||||
| ------ | ---- | ------------ |
|
| ------ | ---- | ------------ |
|
||||||
@@ -22,3 +112,63 @@
|
|||||||
| BTN_VOL_DOWN | | 32 |
|
| BTN_VOL_DOWN | | 32 |
|
||||||
|
|
||||||
Buttons pull to GND if pushed -> Internal Pull-Up needed!
|
Buttons pull to GND if pushed -> Internal Pull-Up needed!
|
||||||
|
|
||||||
|
### RFID tags
|
||||||
|
The mapping of rfid tags to files uses the ID of the
|
||||||
|
tag. A file called `_mapping.txt` in the root folder of
|
||||||
|
the SD card defines the mappings between RFID tag ids and
|
||||||
|
folders to play.
|
||||||
|
|
||||||
|
The easiest way to create this file is to use the mapping
|
||||||
|
functionality of the webinterface.
|
||||||
|
|
||||||
|
#### Special modes
|
||||||
|
You can also save data on the tags to further manipulate
|
||||||
|
the system. Position of the data is irrelevant, the whole
|
||||||
|
tag will be searched.
|
||||||
|
|
||||||
|
Using `[random]` will play the files in a random order.
|
||||||
|
`[random:2]` will randomize everything except the first 2
|
||||||
|
files. This can be useful for having the favorite song of
|
||||||
|
your kids playing, but after that getting a bit of randomness.
|
||||||
|
|
||||||
|
Using `[lock]` will turn this key into a key for the locking
|
||||||
|
mode. Scanning the tag enables locking mode. The next album
|
||||||
|
started will keep running until the end. Removing the tag
|
||||||
|
will be deactivated, as are the buttons for prev and next
|
||||||
|
track. You can disable locking mode by again scanning the
|
||||||
|
lock tag again.
|
||||||
|
|
||||||
|
`[advent]` is used for christmas time. An album with this tag
|
||||||
|
will only play in December. On December 1st, only track 1
|
||||||
|
will play. On December 2nd, track 2 followed by track 1. On
|
||||||
|
December 3rd, tracks 3, 1 and 2. From December 24th on, track
|
||||||
|
24 followed by tracks 1-23. So your kid will get the "daily track"
|
||||||
|
first, followed by all previous tags in the right order.
|
||||||
|
|
||||||
|
#### API
|
||||||
|
You can send commands to ESMP3 using three different ways:
|
||||||
|
* Through a websocket connection to `ws://<IP>/ws`.
|
||||||
|
* Through the serial console using an USB cable.
|
||||||
|
* Via HTTP POST request to `http://<IP>/cmd`, having the
|
||||||
|
command in the variable `cmd`.
|
||||||
|
|
||||||
|
Supported commands are:
|
||||||
|
| Command | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| `play <PATH>` | Starts playing the given path. Path may be a path on the
|
||||||
|
sd card or a http(s) URL of a webstream (direct links to mp3/4/ogg streams,
|
||||||
|
PLS files, M3U files or podcast XML feeds are supported). |
|
||||||
|
| `play` | Continues playing the previously played thing. |
|
||||||
|
| `stop` | Stops playing. |
|
||||||
|
| `volume=<X>` | Sets the volume to X (0-255). |
|
||||||
|
| `track_prev` | Starts the previous track, if available. |
|
||||||
|
| `track_next` | Starts the next track, if available. |
|
||||||
|
| `track=<X>` | Starts playing track no. X of the currently playing album. |
|
||||||
|
| `reset_vs1053` | Resets the VS1053 audio chip. |
|
||||||
|
| `reboot` | Reboots ESMP3. |
|
||||||
|
| `add_mapping=<ID>=<PATH>` | Adds a mapping between RFID card <ID> and path
|
||||||
|
<PATH>. See `play` for valid path formats. |
|
||||||
|
| `update` | Runs an update check. |
|
||||||
|
| `debug=<0|1>` | Enables / disables debug messages. This value is persisted across reboots. |
|
||||||
|
| `trace=<0|1>` | Enables / disables tracing messages. This value is also persisted across reboots. |
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
VERSION=1
|
||||||
|
IMAGE_PATH=https://files.schle.nz/esmp3/firmware.bin
|
||||||
|
IMAGE_MD5=00000000000000000000000000000000
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo -n "-DVERSION=\\\"`git describe --tags --dirty`-`date +%Y%m%d-%H%M%S`\\\""
|
echo -n "-DVERSION=\\\"`git describe --tags --dirty`\\\""
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#set -x
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if ! git diff-index --quiet HEAD ; then
|
||||||
|
echo "Git isn't clean. Cant deploy."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
branch_name=$(git symbolic-ref -q HEAD)
|
||||||
|
branch_name=${branch_name##refs/heads/}
|
||||||
|
branch_name=${branch_name:-HEAD}
|
||||||
|
|
||||||
|
if [ "$branch_name" != "master" ]; then
|
||||||
|
echo "We are not on master branch. Can't deploy."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
echo "Last tags:"
|
||||||
|
vers=`git tag --sort=-version:refname | head -n 5`
|
||||||
|
while read version; do
|
||||||
|
echo " $version"
|
||||||
|
done <<< "$vers"
|
||||||
|
|
||||||
|
read -p "Version to generate: " VERSION
|
||||||
|
|
||||||
|
OTA_VERSION=`grep "VERSION=" bin/update.manifest | cut -d"=" -f2`
|
||||||
|
OTA_VERSION=$(( $OTA_VERSION + 1 ))
|
||||||
|
|
||||||
|
sed -i.bak "s/#define OTA_VERSION .*/#define OTA_VERSION $OTA_VERSION/" include/config.h include/config.sample.h
|
||||||
|
rm include/config.h.bak include/config.sample.h.bak
|
||||||
|
|
||||||
|
PLATFORMIO_BUILD_FLAGS='-DVERSION=\"$VERSION\"' pio run -e deploy -t buildprog || exit 1
|
||||||
|
|
||||||
|
cp .pio/build/deploy/firmware.bin bin/firmware.bin || exit 1
|
||||||
|
|
||||||
|
sed -i.bak "s/VERSION=.*/VERSION=$OTA_VERSION/" bin/update.manifest
|
||||||
|
MD5=`md5sum --binary bin/firmware.bin | cut -d" " -f1`
|
||||||
|
sed -i.bak "s/IMAGE_MD5=.*/IMAGE_MD5=$MD5/" bin/update.manifest
|
||||||
|
rm bin/update.manifest.bak
|
||||||
|
|
||||||
|
echo; echo; echo; echo; echo
|
||||||
|
echo "Please check the git diff, if everything looks okay:"
|
||||||
|
git diff
|
||||||
|
|
||||||
|
read -p "Press ENTER to continue, Ctrl-C to abort. " foo
|
||||||
|
|
||||||
|
git add bin/firmware.bin bin/update.manifest
|
||||||
|
git commit -m "Deploying version $VERSION."
|
||||||
|
git tag -a -m "Deploying version $VERSION" $VERSION
|
||||||
|
git push --follow-tags
|
||||||
@@ -1,15 +1,34 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
#define SHOW_DEBUG
|
// This is a simple number indicating the version for the HTTP Updater.
|
||||||
#define SHOW_TRACE
|
#define OTA_VERSION 1
|
||||||
#define FTP_DEBUG
|
// Comment out to prevent automatic updates.
|
||||||
|
#define OTA_UPDATE_URL "https://files.schle.nz/esmp3/update.manifest"
|
||||||
|
#define OTA_CHECK_INTERVAL 12*60*60*1000 // 12 hours
|
||||||
|
|
||||||
#define WIFI_SSID "Schlenz"
|
#define SHOW_DEBUG
|
||||||
#define WIFI_PASS "1410WischlingenPanda"
|
//#define SHOW_TRACE
|
||||||
|
#define FTP_DEBUG
|
||||||
|
#define DELAY_AFTER_DEBUG_AND_TRACE 0
|
||||||
|
|
||||||
|
// Here you can define WiFi data to use. But actually, the better way to do
|
||||||
|
// this is by using /_wifi.txt on the sd card.
|
||||||
|
//#define WIFI_SSID "---CHANGEME---"
|
||||||
|
//#define WIFI_PASS "---CHANGEME---"
|
||||||
|
|
||||||
#define VS1053_SLEEP_DELAY 5000
|
#define VS1053_SLEEP_DELAY 5000
|
||||||
#define MQTT_REPORT_INTERVAL 10000
|
#define POSITION_SEND_INTERVAL 5000
|
||||||
|
|
||||||
|
#define DEBOUNCE_MILLIS 200
|
||||||
|
#define VOLUME_DEFAULT 230
|
||||||
|
#define VOLUME_MIN 190
|
||||||
|
#define VOLUME_MAX 255
|
||||||
|
#define VOLUME_STEP 0x08
|
||||||
|
|
||||||
|
#define RFID_SCAN_INTERVAL 100
|
||||||
|
|
||||||
|
#define NUM_BUTTONS 4
|
||||||
|
|
||||||
#define PIN_SD_CS(x) (digitalWrite(16, x))
|
#define PIN_SD_CS(x) (digitalWrite(16, x))
|
||||||
#define PIN_SD_CS_SETUP() (pinMode(16, OUTPUT))
|
#define PIN_SD_CS_SETUP() (pinMode(16, OUTPUT))
|
||||||
@@ -48,33 +67,18 @@
|
|||||||
#define BTN_NEXT_SETUP() (pinMode(33, INPUT_PULLUP))
|
#define BTN_NEXT_SETUP() (pinMode(33, INPUT_PULLUP))
|
||||||
|
|
||||||
|
|
||||||
#define MCP_I2C_ADDR 7
|
|
||||||
|
|
||||||
#define NUM_BUTTONS 4
|
|
||||||
|
|
||||||
#define DEBOUNCE_MILLIS 200
|
|
||||||
#define VOLUME_DEFAULT 230
|
|
||||||
#define VOLUME_MIN 190
|
|
||||||
#define VOLUME_MAX 255
|
|
||||||
#define VOLUME_STEP 0x08
|
|
||||||
|
|
||||||
#define MCP_SPI_SETTING_DELAY 1
|
|
||||||
|
|
||||||
#define RFID_SCAN_INTERVAL 100
|
|
||||||
|
|
||||||
|
|
||||||
// Other definitions
|
// Other definitions
|
||||||
#define INFO(x, ...) Serial.printf(x, ##__VA_ARGS__)
|
#define INFO(x, ...) Serial.printf(x, ##__VA_ARGS__)
|
||||||
#define ERROR(x, ...) Serial.printf(x, ##__VA_ARGS__)
|
#define ERROR(x, ...) Serial.printf(x, ##__VA_ARGS__)
|
||||||
|
|
||||||
#ifdef SHOW_DEBUG
|
#ifdef SHOW_DEBUG
|
||||||
#define DEBUG(x, ...) Serial.printf(x, ##__VA_ARGS__)
|
#define DEBUG(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);}
|
||||||
#else
|
#else
|
||||||
#define DEBUG(x, ...) while(0) {}
|
#define DEBUG(x, ...) while(0) {}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SHOW_TRACE
|
#ifdef SHOW_TRACE
|
||||||
#define TRACE(x, ...) Serial.printf(x, ##__VA_ARGS__)
|
#define TRACE(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);}
|
||||||
#else
|
#else
|
||||||
#define TRACE(x, ...) while(0) {}
|
#define TRACE(x, ...) while(0) {}
|
||||||
#endif
|
#endif
|
||||||
+33
-12
@@ -1,16 +1,26 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
|
class Controller;
|
||||||
|
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "mqtt_client.h"
|
#include "playlist.h"
|
||||||
|
#include "playlist_manager.h"
|
||||||
|
#include "http_server.h"
|
||||||
|
|
||||||
|
#undef DEPRECATED
|
||||||
#include <MFRC522.h>
|
#include <MFRC522.h>
|
||||||
|
|
||||||
|
enum ControllerState { NORMAL, LOCKING, LOCKED };
|
||||||
|
|
||||||
class Controller {
|
class Controller {
|
||||||
private:
|
private:
|
||||||
MFRC522* _rfid;
|
MFRC522* _rfid;
|
||||||
SPIMaster* _spi;
|
HTTPServer* _http_server;
|
||||||
MQTTClient* _mqtt_client;
|
ControllerState _state = NORMAL;
|
||||||
bool _rfid_enabled = true;
|
bool _rfid_enabled = true;
|
||||||
void _check_rfid();
|
void _check_rfid();
|
||||||
void _check_serial();
|
void _check_serial();
|
||||||
@@ -19,22 +29,33 @@ private:
|
|||||||
uint32_t _get_rfid_card_uid();
|
uint32_t _get_rfid_card_uid();
|
||||||
String _read_rfid_data();
|
String _read_rfid_data();
|
||||||
bool _rfid_present = false;
|
bool _rfid_present = false;
|
||||||
uint32_t _last_rfid_card_uid = 0;
|
String _last_rfid_uid = "";
|
||||||
uint8_t _no_rfid_card_count = 0;
|
String _last_rfid_data = "";
|
||||||
Player* _player;
|
|
||||||
unsigned long _last_rfid_scan_at = 0;
|
unsigned long _last_rfid_scan_at = 0;
|
||||||
|
unsigned long _last_position_info_at = 0;
|
||||||
|
unsigned long _last_update_check_at = 0;
|
||||||
|
unsigned long _last_wifi_try_at = 0;
|
||||||
String _serial_buffer = String();
|
String _serial_buffer = String();
|
||||||
void _execute_serial_command(String cmd);
|
String _cmd_queue = "";
|
||||||
void _execute_command_ls(String path);
|
void _execute_command_ls(String path);
|
||||||
void _execute_command_ids();
|
void _execute_command_ids();
|
||||||
void _execute_command_help();
|
void _execute_command_help();
|
||||||
unsigned long _button_last_pressed_at[NUM_BUTTONS];
|
unsigned long _button_last_pressed_at[NUM_BUTTONS];
|
||||||
bool _check_button(uint8_t btn);
|
bool _check_button(uint8_t btn);
|
||||||
unsigned long _last_mqtt_report_at = 0;
|
|
||||||
void _send_mqtt_report();
|
|
||||||
public:
|
public:
|
||||||
Controller(Player* p, SPIMaster* s);
|
Controller(Player* p, PlaylistManager* pm);
|
||||||
void set_mqtt_client(MQTTClient* m);
|
PlaylistManager* pm;
|
||||||
String get_status_json();
|
Player* player;
|
||||||
|
void register_http_server(HTTPServer* h);
|
||||||
void loop();
|
void loop();
|
||||||
|
void send_controller_status();
|
||||||
|
void send_player_status();
|
||||||
|
void send_playlist_manager_status();
|
||||||
|
void send_position();
|
||||||
|
void inform_new_client(AsyncWebSocketClient* client);
|
||||||
|
String json();
|
||||||
|
bool process_message(String m);
|
||||||
|
void queue_command(String cmd);
|
||||||
|
void update_playlist_manager();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <SD.h>
|
||||||
|
#include "config.h"
|
||||||
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
|
class DataSource {
|
||||||
|
private:
|
||||||
|
public:
|
||||||
|
DataSource() {};
|
||||||
|
virtual ~DataSource() {};
|
||||||
|
virtual size_t read(uint8_t* buf, size_t len) = 0;
|
||||||
|
virtual int read() = 0;
|
||||||
|
virtual size_t position() = 0;
|
||||||
|
virtual void seek(size_t position) = 0;
|
||||||
|
virtual size_t size() = 0;
|
||||||
|
virtual void close() = 0;
|
||||||
|
virtual bool usable() = 0;
|
||||||
|
virtual int peek(int offset) = 0;
|
||||||
|
void skip_id3_tag();
|
||||||
|
};
|
||||||
|
|
||||||
|
class SDDataSource : public DataSource {
|
||||||
|
private:
|
||||||
|
File _file;
|
||||||
|
public:
|
||||||
|
SDDataSource(String file);
|
||||||
|
~SDDataSource();
|
||||||
|
size_t read(uint8_t* buf, size_t len);
|
||||||
|
int read();
|
||||||
|
size_t position();
|
||||||
|
void seek(size_t position);
|
||||||
|
size_t size();
|
||||||
|
void close();
|
||||||
|
bool usable();
|
||||||
|
int peek(int offset=0);
|
||||||
|
};
|
||||||
|
|
||||||
|
class HTTPSDataSource : public DataSource {
|
||||||
|
private:
|
||||||
|
WiFiClient* _stream = NULL;
|
||||||
|
HTTPClientWrapper* _http = NULL;
|
||||||
|
uint32_t _position;
|
||||||
|
String _url;
|
||||||
|
void _init(String url, uint32_t offset);
|
||||||
|
public:
|
||||||
|
HTTPSDataSource(String url, uint32_t offset=0);
|
||||||
|
~HTTPSDataSource();
|
||||||
|
size_t read(uint8_t* buf, size_t len);
|
||||||
|
int read();
|
||||||
|
size_t position();
|
||||||
|
void seek(size_t position);
|
||||||
|
size_t size();
|
||||||
|
void close();
|
||||||
|
bool usable();
|
||||||
|
int peek(int offset=0);
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
class HTTPClientWrapper {
|
||||||
|
private:
|
||||||
|
HTTPClient* _http;
|
||||||
|
uint8_t* _buffer;
|
||||||
|
uint16_t _buffer_size;
|
||||||
|
uint16_t _buffer_length;
|
||||||
|
uint16_t _buffer_position;
|
||||||
|
uint32_t _chunk_length;
|
||||||
|
|
||||||
|
bool _connected = false;
|
||||||
|
String _content_type;
|
||||||
|
uint32_t _length;
|
||||||
|
bool _request(String method, String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||||
|
WiFiClient* _stream;
|
||||||
|
bool _is_chunked;
|
||||||
|
void _read_next_chunk_header(bool first);
|
||||||
|
uint16_t _fill_buffer();
|
||||||
|
|
||||||
|
public:
|
||||||
|
HTTPClientWrapper();
|
||||||
|
~HTTPClientWrapper();
|
||||||
|
bool get(String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||||
|
bool head(String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||||
|
String getContentType();
|
||||||
|
String getString();
|
||||||
|
int read();
|
||||||
|
uint32_t read(uint8_t* dst, uint32_t len);
|
||||||
|
void close();
|
||||||
|
uint32_t getSize();
|
||||||
|
String readUntil(String sep);
|
||||||
|
String readLine();
|
||||||
|
int peek(int offset=0);
|
||||||
|
};
|
||||||
+14
-14
@@ -1,29 +1,29 @@
|
|||||||
/*
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <ESP8266WebServer.h>
|
|
||||||
|
class HTTPServer;
|
||||||
|
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include <SD.h>
|
#include <AsyncTCP.h>
|
||||||
#include <WiFiClient.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
#include <ESP8266WiFi.h>
|
|
||||||
#include <ESP8266mDNS.h>
|
|
||||||
|
|
||||||
class HTTPServer {
|
class HTTPServer {
|
||||||
private:
|
private:
|
||||||
ESP8266WebServer* _http_server;
|
AsyncWebServer* _server;
|
||||||
void _handle_upload();
|
|
||||||
|
Player* _player;
|
||||||
|
Controller* _controller;
|
||||||
|
void _handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
|
||||||
uint16_t _chunk_length;
|
uint16_t _chunk_length;
|
||||||
uint8_t* _chunk;
|
uint8_t* _chunk;
|
||||||
|
File _upload_file;
|
||||||
uint32_t _file_size;
|
uint32_t _file_size;
|
||||||
uint32_t _file_size_done;
|
uint32_t _file_size_done;
|
||||||
bool _need_header;
|
bool _need_header;
|
||||||
uint32_t _upload_position;
|
uint32_t _upload_position;
|
||||||
void _handle_index();
|
void _onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len);
|
||||||
void _handle_status();
|
void _handle_index(AsyncWebServerRequest* req);
|
||||||
Player* _player;
|
|
||||||
Controller* _controller;
|
|
||||||
public:
|
public:
|
||||||
HTTPServer(Player* p, Controller* c);
|
HTTPServer(Player* p, Controller* c);
|
||||||
void loop();
|
AsyncWebSocket* ws;
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
void wifi_connect();
|
||||||
|
|
||||||
|
extern bool debug_enabled;
|
||||||
|
extern bool trace_enabled;
|
||||||
|
extern Preferences prefs;
|
||||||
|
|
||||||
|
extern const uint8_t file_index_html_start[] asm("_binary_src_webinterface_index_html_gz_start");
|
||||||
|
extern const size_t file_index_html_size asm("_binary_src_webinterface_index_html_gz_size");
|
||||||
|
extern const uint8_t file_timezones_json_start[] asm("_binary_src_webinterface_timezones_json_gz_start");
|
||||||
|
extern const size_t file_timezones_json_size asm("_binary_src_webinterface_timezones_json_gz_size");
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
#include <PubSubClient.h>
|
|
||||||
#include <WiFiClient.h>
|
|
||||||
|
|
||||||
class MQTTClient {
|
|
||||||
private:
|
|
||||||
WiFiClient* _wifi_client;
|
|
||||||
PubSubClient* _mqtt;
|
|
||||||
unsigned long _last_reconnect_attempt;
|
|
||||||
void _reconnect();
|
|
||||||
public:
|
|
||||||
MQTTClient();
|
|
||||||
void loop();
|
|
||||||
void publish_status(String s);
|
|
||||||
void publish_rfid_uid(uint32_t uid);
|
|
||||||
};
|
|
||||||
+18
-35
@@ -2,9 +2,13 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <list>
|
|
||||||
#include <map>
|
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
|
#include "playlist.h"
|
||||||
|
#include "data_sources.h"
|
||||||
|
|
||||||
|
class Player;
|
||||||
|
|
||||||
|
#include "controller.h"
|
||||||
|
|
||||||
#define SCI_MODE 0x00
|
#define SCI_MODE 0x00
|
||||||
#define SCI_STATUS 0x01
|
#define SCI_STATUS 0x01
|
||||||
@@ -38,15 +42,8 @@
|
|||||||
class Player {
|
class Player {
|
||||||
private:
|
private:
|
||||||
enum state { uninitialized, idle, playing, stopping,
|
enum state { uninitialized, idle, playing, stopping,
|
||||||
system_sound_while_playing, system_sound_while_stopped,
|
|
||||||
sleeping, recording };
|
sleeping, recording };
|
||||||
struct album_state {
|
|
||||||
uint8_t index;
|
|
||||||
uint32_t position;
|
|
||||||
};
|
|
||||||
void _check_system_sound(String filename);
|
|
||||||
void _reset();
|
void _reset();
|
||||||
void _init();
|
|
||||||
void _wait();
|
void _wait();
|
||||||
uint16_t _read_control_register(uint8_t address, bool do_wait=true);
|
uint16_t _read_control_register(uint8_t address, bool do_wait=true);
|
||||||
void _write_control_register(uint8_t address, uint16_t value, bool do_wait=true);
|
void _write_control_register(uint8_t address, uint16_t value, bool do_wait=true);
|
||||||
@@ -59,11 +56,7 @@ private:
|
|||||||
void _flush_and_cancel();
|
void _flush_and_cancel();
|
||||||
int8_t _get_endbyte();
|
int8_t _get_endbyte();
|
||||||
void _flush(uint count, int8_t fill_byte);
|
void _flush(uint count, int8_t fill_byte);
|
||||||
void _set_last_track(const char* album, uint8_t track, uint32_t position);
|
|
||||||
std::map<String, album_state> _last_tracks;
|
|
||||||
String _random_album();
|
|
||||||
void _play_file(String filename, uint32_t offset);
|
void _play_file(String filename, uint32_t offset);
|
||||||
uint32_t _id3_tag_offset(File f);
|
|
||||||
void _finish_playing();
|
void _finish_playing();
|
||||||
void _finish_stopping(bool turn_speaker_off);
|
void _finish_stopping(bool turn_speaker_off);
|
||||||
void _mute();
|
void _mute();
|
||||||
@@ -74,48 +67,38 @@ private:
|
|||||||
void _patch_adpcm();
|
void _patch_adpcm();
|
||||||
void _speaker_off();
|
void _speaker_off();
|
||||||
void _speaker_on();
|
void _speaker_on();
|
||||||
void _fill_id_to_folder_map();
|
|
||||||
String _foldername_for_id(String id);
|
|
||||||
|
|
||||||
SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0);
|
SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0);
|
||||||
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||||
SPISettings* _spi_settings = &_spi_settings_slow;
|
SPISettings* _spi_settings = &_spi_settings_slow;
|
||||||
|
|
||||||
std::list<String> _files_in_dir(String dir);
|
DataSource* _file;
|
||||||
String _find_album_dir(String album);
|
uint32_t _file_size = 0;
|
||||||
File _file;
|
|
||||||
uint8_t _buffer[32];
|
uint8_t _buffer[32];
|
||||||
String _playing_album;
|
uint32_t _current_play_position = 0;
|
||||||
uint8_t _playing_index;
|
Playlist* _current_playlist = NULL;
|
||||||
uint8_t _playing_album_songs;
|
|
||||||
uint32_t _current_play_position;
|
|
||||||
uint _refills;
|
uint _refills;
|
||||||
uint8_t _volume;
|
uint8_t _volume;
|
||||||
uint16_t _stop_delay;
|
uint16_t _stop_delay;
|
||||||
uint32_t _skip_to;
|
uint32_t _skip_to;
|
||||||
SPIMaster* _spi;
|
SPIMaster* _spi;
|
||||||
|
Controller* _controller;
|
||||||
unsigned long _stopped_at;
|
unsigned long _stopped_at;
|
||||||
public:
|
public:
|
||||||
std::map<String, String> id_to_folder_map;
|
|
||||||
|
|
||||||
Player(SPIMaster* s);
|
Player(SPIMaster* s);
|
||||||
|
void init();
|
||||||
|
void register_controller(Controller* c);
|
||||||
void vol_up();
|
void vol_up();
|
||||||
void vol_down();
|
void vol_down();
|
||||||
void track_next();
|
void track_next();
|
||||||
void track_prev();
|
void track_prev();
|
||||||
|
void set_track(uint8_t track);
|
||||||
bool is_playing();
|
bool is_playing();
|
||||||
|
bool play();
|
||||||
bool play_id(String id);
|
bool play(Playlist* p);
|
||||||
bool play_album(String album);
|
|
||||||
void play_random_album();
|
|
||||||
bool play_song(String album, uint8_t song_index, uint32_t offset=0);
|
|
||||||
void play_system_sound(String filename);
|
|
||||||
void stop(bool turn_speaker_off=true);
|
void stop(bool turn_speaker_off=true);
|
||||||
bool loop();
|
bool loop();
|
||||||
void set_volume(uint8_t vol, bool save = true);
|
void set_volume(uint8_t vol, bool save = true);
|
||||||
std::list<String> ls(String path, bool withFiles=true, bool withDirs=true, bool withHidden=false);
|
String position_json();
|
||||||
String album() { return _playing_album; }
|
String json();
|
||||||
uint8_t track() { return _playing_index; }
|
|
||||||
uint32_t position() { return _current_play_position; }
|
|
||||||
uint8_t volume() { return _volume; }
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "main.h"
|
||||||
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
|
enum PlaylistPersistence {
|
||||||
|
PERSIST_NONE,
|
||||||
|
PERSIST_TEMPORARY,
|
||||||
|
PERSIST_PERMANENTLY
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlaylistEntry {
|
||||||
|
String filename;
|
||||||
|
String title;
|
||||||
|
String id;
|
||||||
|
|
||||||
|
bool operator<(PlaylistEntry p) { return title < p.title; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class Playlist {
|
||||||
|
private:
|
||||||
|
uint32_t _position = 0;
|
||||||
|
uint32_t _current_track = 0;
|
||||||
|
bool _started = false;
|
||||||
|
bool _shuffled = false;
|
||||||
|
std::vector<PlaylistEntry> _files;
|
||||||
|
String _title = "";
|
||||||
|
String _path;
|
||||||
|
void _add_path(String path);
|
||||||
|
void _examine_http_url(String url);
|
||||||
|
void _parse_rss(HTTPClientWrapper* http);
|
||||||
|
void _parse_m3u(HTTPClientWrapper* http);
|
||||||
|
void _parse_pls(HTTPClientWrapper* http);
|
||||||
|
public:
|
||||||
|
PlaylistPersistence persistence = PERSIST_TEMPORARY;
|
||||||
|
Playlist(String path);
|
||||||
|
void start();
|
||||||
|
uint16_t get_file_count();
|
||||||
|
bool has_track_next();
|
||||||
|
bool has_track_prev();
|
||||||
|
bool track_next();
|
||||||
|
bool track_prev();
|
||||||
|
void track_restart();
|
||||||
|
bool set_track(uint8_t track);
|
||||||
|
void set_track_by_id(String id);
|
||||||
|
void reset();
|
||||||
|
String path();
|
||||||
|
bool is_empty();
|
||||||
|
bool get_current_file(String* dst);
|
||||||
|
String get_current_track_id();
|
||||||
|
uint32_t get_position();
|
||||||
|
void set_position(uint32_t p);
|
||||||
|
void shuffle(uint8_t random_offset=0);
|
||||||
|
void advent_shuffle(uint8_t day);
|
||||||
|
bool is_fresh();
|
||||||
|
void dump();
|
||||||
|
void json(JsonObject json);
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
#include "playlist.h"
|
||||||
|
|
||||||
|
class PlaylistManager {
|
||||||
|
private:
|
||||||
|
std::map<String, String> _map;
|
||||||
|
std::map<String, Playlist*> _playlists;
|
||||||
|
std::vector<String> _unmapped_folders;
|
||||||
|
void _check_for_special_chars(String s);
|
||||||
|
void _save_mapping();
|
||||||
|
public:
|
||||||
|
PlaylistManager();
|
||||||
|
Playlist* get_playlist_for_id(String id);
|
||||||
|
Playlist* get_playlist_for_folder(String folder);
|
||||||
|
void dump_ids();
|
||||||
|
void scan_files();
|
||||||
|
String json();
|
||||||
|
bool add_mapping(String id, String folder);
|
||||||
|
String create_mapping_txt();
|
||||||
|
void persist(Playlist* p);
|
||||||
|
};
|
||||||
+37
-10
@@ -6,7 +6,9 @@
|
|||||||
|
|
||||||
class SPIMaster {
|
class SPIMaster {
|
||||||
public:
|
public:
|
||||||
SPIMaster() {
|
static uint8_t state;
|
||||||
|
|
||||||
|
static void init() {
|
||||||
PIN_SD_CS_SETUP();
|
PIN_SD_CS_SETUP();
|
||||||
PIN_VS1053_XCS_SETUP();
|
PIN_VS1053_XCS_SETUP();
|
||||||
PIN_VS1053_XDCS_SETUP();
|
PIN_VS1053_XDCS_SETUP();
|
||||||
@@ -14,30 +16,55 @@ public:
|
|||||||
disable();
|
disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
void select_sd(bool enabled=true) {
|
static void select_sd(bool enabled=true) {
|
||||||
PIN_SD_CS(enabled ? LOW : HIGH);
|
PIN_SD_CS(enabled ? LOW : HIGH);
|
||||||
delayMicroseconds(MCP_SPI_SETTING_DELAY);
|
if (enabled) {
|
||||||
|
state |= 1;
|
||||||
|
} else {
|
||||||
|
state &= ~1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void select_vs1053_xcs(bool enabled=true) {
|
static void select_vs1053_xcs(bool enabled=true) {
|
||||||
PIN_VS1053_XCS(enabled ? LOW : HIGH);
|
PIN_VS1053_XCS(enabled ? LOW : HIGH);
|
||||||
delayMicroseconds(MCP_SPI_SETTING_DELAY);
|
if (enabled) {
|
||||||
|
state |= 2;
|
||||||
|
} else {
|
||||||
|
state &= ~2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void select_vs1053_xdcs(bool enabled=true) {
|
static void select_vs1053_xdcs(bool enabled=true) {
|
||||||
PIN_VS1053_XDCS(enabled ? LOW : HIGH);
|
PIN_VS1053_XDCS(enabled ? LOW : HIGH);
|
||||||
delayMicroseconds(MCP_SPI_SETTING_DELAY);
|
if (enabled) {
|
||||||
|
state |= 4;
|
||||||
|
} else {
|
||||||
|
state &= ~4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void select_rc522(bool enabled=true) {
|
static void select_rc522(bool enabled=true) {
|
||||||
PIN_RC522_CS(enabled ? LOW : HIGH);
|
PIN_RC522_CS(enabled ? LOW : HIGH);
|
||||||
delayMicroseconds(MCP_SPI_SETTING_DELAY);
|
if (enabled) {
|
||||||
|
state |= 8;
|
||||||
|
} else {
|
||||||
|
state &= ~8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void disable() {
|
static void set_state(uint8_t s) {
|
||||||
|
disable();
|
||||||
|
if (s & 1) select_sd();
|
||||||
|
if (s & 2) select_vs1053_xcs();
|
||||||
|
if (s & 4) select_vs1053_xdcs();
|
||||||
|
if (s & 8) select_rc522();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disable() {
|
||||||
PIN_SD_CS(HIGH);
|
PIN_SD_CS(HIGH);
|
||||||
PIN_VS1053_XCS(HIGH);
|
PIN_VS1053_XCS(HIGH);
|
||||||
PIN_VS1053_XDCS(HIGH);
|
PIN_VS1053_XDCS(HIGH);
|
||||||
PIN_RC522_CS(HIGH);
|
PIN_RC522_CS(HIGH);
|
||||||
|
state = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
|
class Updater {
|
||||||
|
public:
|
||||||
|
static void run();
|
||||||
|
static bool do_update(int cmd, String url, String expected_md5);
|
||||||
|
static bool read_line(String* dst, HTTPClientWrapper* http, String expected_key);
|
||||||
|
};
|
||||||
@@ -1,988 +0,0 @@
|
|||||||
/*
|
|
||||||
* FTP Serveur for ESP8266
|
|
||||||
* based on FTP Serveur for Arduino Due and Ethernet shield (W5100) or WIZ820io (W5200)
|
|
||||||
* based on Jean-Michel Gallego's work
|
|
||||||
* modified to work with esp8266 SPIFFS by David Paiva david@nailbuster.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ESP8266FtpServer.h"
|
|
||||||
#ifdef ESP8266
|
|
||||||
#include <ESP8266WiFi.h>
|
|
||||||
#elif defined ESP32
|
|
||||||
#include <WiFi.h>
|
|
||||||
#include "SPIFFS.h"
|
|
||||||
#endif
|
|
||||||
#include <SD.h>
|
|
||||||
#include <WiFiClient.h>
|
|
||||||
#include <FS.h>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
WiFiServer ftpServer( FTP_CTRL_PORT );
|
|
||||||
WiFiServer dataServer( FTP_DATA_PORT_PASV );
|
|
||||||
|
|
||||||
void FtpServer::begin(String uname, String pword)
|
|
||||||
{
|
|
||||||
// Tells the ftp server to begin listening for incoming connection
|
|
||||||
_FTP_USER=uname;
|
|
||||||
_FTP_PASS = pword;
|
|
||||||
|
|
||||||
ftpServer.begin();
|
|
||||||
delay(10);
|
|
||||||
dataServer.begin();
|
|
||||||
delay(10);
|
|
||||||
millisTimeOut = (uint32_t)FTP_TIME_OUT * 60 * 1000;
|
|
||||||
millisDelay = 0;
|
|
||||||
cmdStatus = 0;
|
|
||||||
iniVariables();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FtpServer::iniVariables()
|
|
||||||
{
|
|
||||||
// Default for data port
|
|
||||||
dataPort = FTP_DATA_PORT_PASV;
|
|
||||||
|
|
||||||
// Default Data connection is Active
|
|
||||||
dataPassiveConn = true;
|
|
||||||
|
|
||||||
// Set the root directory
|
|
||||||
strcpy( cwdName, "/" );
|
|
||||||
|
|
||||||
rnfrCmd = false;
|
|
||||||
transferStatus = 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void FtpServer::handleFTP()
|
|
||||||
{
|
|
||||||
if((int32_t) ( millisDelay - millis() ) > 0 )
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (ftpServer.hasClient()) {
|
|
||||||
client.stop();
|
|
||||||
client = ftpServer.available();
|
|
||||||
}
|
|
||||||
|
|
||||||
if( cmdStatus == 0 )
|
|
||||||
{
|
|
||||||
if( client.connected())
|
|
||||||
disconnectClient();
|
|
||||||
cmdStatus = 1;
|
|
||||||
}
|
|
||||||
else if( cmdStatus == 1 ) // Ftp server waiting for connection
|
|
||||||
{
|
|
||||||
abortTransfer();
|
|
||||||
iniVariables();
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println("Ftp server waiting for connection on port "+ String(FTP_CTRL_PORT));
|
|
||||||
#endif
|
|
||||||
cmdStatus = 2;
|
|
||||||
}
|
|
||||||
else if( cmdStatus == 2 ) // Ftp server idle
|
|
||||||
{
|
|
||||||
|
|
||||||
if( client.connected() ) // A client connected
|
|
||||||
{
|
|
||||||
clientConnected();
|
|
||||||
millisEndConnection = millis() + 10 * 1000 ; // wait client id during 10 s.
|
|
||||||
cmdStatus = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if( readChar() > 0 ) // got response
|
|
||||||
{
|
|
||||||
if( cmdStatus == 3 ) // Ftp server waiting for user identity
|
|
||||||
if( userIdentity() )
|
|
||||||
cmdStatus = 4;
|
|
||||||
else
|
|
||||||
cmdStatus = 0;
|
|
||||||
else if( cmdStatus == 4 ) // Ftp server waiting for user registration
|
|
||||||
if( userPassword() )
|
|
||||||
{
|
|
||||||
cmdStatus = 5;
|
|
||||||
millisEndConnection = millis() + millisTimeOut;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
cmdStatus = 0;
|
|
||||||
else if( cmdStatus == 5 ) // Ftp server waiting for user command
|
|
||||||
if( ! processCommand())
|
|
||||||
cmdStatus = 0;
|
|
||||||
else
|
|
||||||
millisEndConnection = millis() + millisTimeOut;
|
|
||||||
}
|
|
||||||
else if (!client.connected() || !client)
|
|
||||||
{
|
|
||||||
cmdStatus = 1;
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println("client disconnected");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
if( transferStatus == 1 ) // Retrieve data
|
|
||||||
{
|
|
||||||
if( ! doRetrieve())
|
|
||||||
transferStatus = 0;
|
|
||||||
}
|
|
||||||
else if( transferStatus == 2 ) // Store data
|
|
||||||
{
|
|
||||||
if( ! doStore())
|
|
||||||
transferStatus = 0;
|
|
||||||
}
|
|
||||||
else if( cmdStatus > 2 && ! ((int32_t) ( millisEndConnection - millis() ) > 0 ))
|
|
||||||
{
|
|
||||||
client.println("530 Timeout");
|
|
||||||
millisDelay = millis() + 200; // delay of 200 ms
|
|
||||||
cmdStatus = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FtpServer::clientConnected()
|
|
||||||
{
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println("Client connected!");
|
|
||||||
#endif
|
|
||||||
client.println( "220--- Welcome to FTP for ESP8266/ESP32 ---");
|
|
||||||
client.println( "220--- By David Paiva ---");
|
|
||||||
client.println( "220 -- Version "+ String(FTP_SERVER_VERSION) +" --");
|
|
||||||
iCL = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FtpServer::disconnectClient()
|
|
||||||
{
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println(" Disconnecting client");
|
|
||||||
#endif
|
|
||||||
abortTransfer();
|
|
||||||
client.println("221 Goodbye");
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean FtpServer::userIdentity()
|
|
||||||
{
|
|
||||||
if( strcmp( command, "USER" ))
|
|
||||||
client.println( "500 Syntax error");
|
|
||||||
if( strcmp( parameters, _FTP_USER.c_str() ))
|
|
||||||
client.println( "530 user not found");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.println( "331 OK. Password required");
|
|
||||||
strcpy( cwdName, "/" );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
millisDelay = millis() + 100; // delay of 100 ms
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean FtpServer::userPassword()
|
|
||||||
{
|
|
||||||
if( strcmp( command, "PASS" ))
|
|
||||||
client.println( "500 Syntax error");
|
|
||||||
else if( strcmp( parameters, _FTP_PASS.c_str() ))
|
|
||||||
client.println( "530 ");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println( "OK. Waiting for commands.");
|
|
||||||
#endif
|
|
||||||
client.println( "230 OK.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
millisDelay = millis() + 100; // delay of 100 ms
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean FtpServer::processCommand()
|
|
||||||
{
|
|
||||||
///////////////////////////////////////
|
|
||||||
// //
|
|
||||||
// ACCESS CONTROL COMMANDS //
|
|
||||||
// //
|
|
||||||
///////////////////////////////////////
|
|
||||||
|
|
||||||
//
|
|
||||||
// CDUP - Change to Parent Directory
|
|
||||||
//
|
|
||||||
if( ! strcmp( command, "CDUP" ))
|
|
||||||
{
|
|
||||||
char* pos = strrchr(cwdName, '/');
|
|
||||||
if (pos > cwdName) cwdName[pos-cwdName]='\0';
|
|
||||||
else if (pos==cwdName) cwdName[1]='\0';
|
|
||||||
client.println("250 Ok. Current directory is " + String(cwdName));
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// CWD - Change Working Directory
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "CWD" ))
|
|
||||||
{
|
|
||||||
char path[ FTP_CWD_SIZE ];
|
|
||||||
|
|
||||||
if( strcmp( parameters, "." ) == 0 ) // 'CWD .' is the same as PWD command
|
|
||||||
client.println( "257 \"" + String(cwdName) + "\" is your current directory");
|
|
||||||
else if (strcmp(parameters, "..")==0) {
|
|
||||||
char* pos = strrchr(cwdName, '/');
|
|
||||||
if (pos > cwdName) cwdName[pos-cwdName]='\0';
|
|
||||||
else if (pos==cwdName) cwdName[1]='\0';
|
|
||||||
client.println( "250 Ok. Current directory is " + String(cwdName) );
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
if (makePath(path)) {
|
|
||||||
if (SD.exists(path)) {
|
|
||||||
File f = SD.open(path);
|
|
||||||
if (f.isDirectory()) {
|
|
||||||
strcpy(cwdName, path);
|
|
||||||
client.println( "250 Ok. Current directory is " + String(cwdName) );
|
|
||||||
} else {
|
|
||||||
client.println("450 Path is not a directory.");
|
|
||||||
}
|
|
||||||
f.close();
|
|
||||||
} else {
|
|
||||||
client.println("450 path does not exist");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
client.println( "450 Invalid path");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// PWD - Print Directory
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "PWD" ))
|
|
||||||
client.println( "257 \"" + String(cwdName) + "\" is your current directory");
|
|
||||||
//
|
|
||||||
// QUIT
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "QUIT" ))
|
|
||||||
{
|
|
||||||
disconnectClient();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////
|
|
||||||
// //
|
|
||||||
// TRANSFER PARAMETER COMMANDS //
|
|
||||||
// //
|
|
||||||
///////////////////////////////////////
|
|
||||||
|
|
||||||
//
|
|
||||||
// MODE - Transfer Mode
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "MODE" ))
|
|
||||||
{
|
|
||||||
if( ! strcmp( parameters, "S" ))
|
|
||||||
client.println( "200 S Ok");
|
|
||||||
// else if( ! strcmp( parameters, "B" ))
|
|
||||||
// client.println( "200 B Ok\r\n";
|
|
||||||
else
|
|
||||||
client.println( "504 Only S(tream) is suported");
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// PASV - Passive Connection management
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "PASV" ))
|
|
||||||
{
|
|
||||||
if (data.connected()) data.stop();
|
|
||||||
//dataServer.begin();
|
|
||||||
//dataIp = Ethernet.localIP();
|
|
||||||
dataIp = client.localIP();
|
|
||||||
dataPort = FTP_DATA_PORT_PASV;
|
|
||||||
//data.connect( dataIp, dataPort );
|
|
||||||
//data = dataServer.available();
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println("Connection management set to passive");
|
|
||||||
Serial.println( "Data port set to " + String(dataPort));
|
|
||||||
#endif
|
|
||||||
client.println( "227 Entering Passive Mode ("+ String(dataIp[0]) + "," + String(dataIp[1])+","+ String(dataIp[2])+","+ String(dataIp[3])+","+String( dataPort >> 8 ) +","+String ( dataPort & 255 )+").");
|
|
||||||
dataPassiveConn = true;
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// PORT - Data Port
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "PORT" ))
|
|
||||||
{
|
|
||||||
if (data) data.stop();
|
|
||||||
// get IP of data client
|
|
||||||
dataIp[ 0 ] = atoi( parameters );
|
|
||||||
char * p = strchr( parameters, ',' );
|
|
||||||
for( uint8_t i = 1; i < 4; i ++ )
|
|
||||||
{
|
|
||||||
dataIp[ i ] = atoi( ++ p );
|
|
||||||
p = strchr( p, ',' );
|
|
||||||
}
|
|
||||||
// get port of data client
|
|
||||||
dataPort = 256 * atoi( ++ p );
|
|
||||||
p = strchr( p, ',' );
|
|
||||||
dataPort += atoi( ++ p );
|
|
||||||
if( p == NULL )
|
|
||||||
client.println( "501 Can't interpret parameters");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
client.println("200 PORT command successful");
|
|
||||||
dataPassiveConn = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// STRU - File Structure
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "STRU" ))
|
|
||||||
{
|
|
||||||
if( ! strcmp( parameters, "F" ))
|
|
||||||
client.println( "200 F Ok");
|
|
||||||
// else if( ! strcmp( parameters, "R" ))
|
|
||||||
// client.println( "200 B Ok\r\n";
|
|
||||||
else
|
|
||||||
client.println( "504 Only F(ile) is suported");
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// TYPE - Data Type
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "TYPE" ))
|
|
||||||
{
|
|
||||||
if( ! strcmp( parameters, "A" ))
|
|
||||||
client.println( "200 TYPE is now ASII");
|
|
||||||
else if( ! strcmp( parameters, "I" ))
|
|
||||||
client.println( "200 TYPE is now 8-bit binary");
|
|
||||||
else
|
|
||||||
client.println( "504 Unknow TYPE");
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////
|
|
||||||
// //
|
|
||||||
// FTP SERVICE COMMANDS //
|
|
||||||
// //
|
|
||||||
///////////////////////////////////////
|
|
||||||
|
|
||||||
//
|
|
||||||
// ABOR - Abort
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "ABOR" ))
|
|
||||||
{
|
|
||||||
abortTransfer();
|
|
||||||
client.println( "226 Data connection closed");
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// DELE - Delete a File
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "DELE" ))
|
|
||||||
{
|
|
||||||
char path[ FTP_CWD_SIZE ];
|
|
||||||
if( strlen( parameters ) == 0 )
|
|
||||||
client.println( "501 No file name");
|
|
||||||
else if( makePath( path ))
|
|
||||||
{
|
|
||||||
if( ! SD.exists( path ))
|
|
||||||
client.println( "550 File " + String(parameters) + " not found");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if( SD.remove( path ))
|
|
||||||
client.println( "250 Deleted " + String(parameters) );
|
|
||||||
else
|
|
||||||
client.println( "450 Can't delete " + String(parameters));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// LIST - List
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "LIST" ))
|
|
||||||
{
|
|
||||||
if( ! dataConnect())
|
|
||||||
client.println( "425 No data connection");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.println( "150 Accepted data connection");
|
|
||||||
uint16_t nm = 0;
|
|
||||||
File root = SD.open(cwdName);
|
|
||||||
if(!root){
|
|
||||||
client.println( "550 Can't open directory " + String(cwdName) );
|
|
||||||
// return;
|
|
||||||
} else {
|
|
||||||
// if(!root.isDirectory()){
|
|
||||||
// Serial.println("Not a directory");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
File file = root.openNextFile();
|
|
||||||
while(file){
|
|
||||||
data.printf("%s %13d Jan 01 00:00 %s\r\n", file.isDirectory() ? "drwxr-xr-x 1 owner group" : "-rw-r--r-- 1 owner group", file.size(), file.name());
|
|
||||||
file = root.openNextFile();
|
|
||||||
}
|
|
||||||
client.println( "226 " + String(nm) + " matches total");
|
|
||||||
}
|
|
||||||
data.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// MLSD - Listing for Machine Processing (see RFC 3659)
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "MLSD" ))
|
|
||||||
{
|
|
||||||
if( ! dataConnect())
|
|
||||||
client.println( "425 No data connection MLSD");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.println( "150 Accepted data connection");
|
|
||||||
uint16_t nm = 0;
|
|
||||||
File root = SD.open(cwdName);
|
|
||||||
// if(!root){
|
|
||||||
// client.println( "550 Can't open directory " + String(cwdName) );
|
|
||||||
// // return;
|
|
||||||
// } else {
|
|
||||||
// if(!root.isDirectory()){
|
|
||||||
// Serial.println("Not a directory");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
File file = root.openNextFile();
|
|
||||||
while(file){
|
|
||||||
if(file.isDirectory()){
|
|
||||||
// if(levels){
|
|
||||||
// listDir(fs, file.name(), levels -1);
|
|
||||||
// }
|
|
||||||
data.println(String("Type=dir;perm=cdelmp;modify=20000101160656; ") + file.name());
|
|
||||||
} else {
|
|
||||||
String fn, fs;
|
|
||||||
fn = file.name();
|
|
||||||
fs = String(file.size());
|
|
||||||
data.println( "Type=file;perm=drw;Size=" + fs + ";modify=20000101160656;" +" " + fn);
|
|
||||||
nm ++;
|
|
||||||
}
|
|
||||||
file = root.openNextFile();
|
|
||||||
}
|
|
||||||
client.println( "226-options: -a -l");
|
|
||||||
client.println( "226 " + String(nm) + " matches total");
|
|
||||||
// }
|
|
||||||
data.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// NLST - Name List
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "NLST" ))
|
|
||||||
{
|
|
||||||
if( ! dataConnect())
|
|
||||||
client.println( "425 No data connection");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.println( "150 Accepted data connection");
|
|
||||||
uint16_t nm = 0;
|
|
||||||
File root = SD.open(cwdName);
|
|
||||||
if(!root){
|
|
||||||
client.println( "550 Can't open directory " + String(cwdName) );
|
|
||||||
} else {
|
|
||||||
|
|
||||||
File file = root.openNextFile();
|
|
||||||
while(file){
|
|
||||||
data.println( file.name());
|
|
||||||
nm ++;
|
|
||||||
file = root.openNextFile();
|
|
||||||
}
|
|
||||||
client.println( "226 " + String(nm) + " matches total");
|
|
||||||
}
|
|
||||||
data.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// NOOP
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "NOOP" ))
|
|
||||||
{
|
|
||||||
// dataPort = 0;
|
|
||||||
client.println( "200 Zzz...");
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// RETR - Retrieve
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "RETR" ))
|
|
||||||
{
|
|
||||||
char path[ FTP_CWD_SIZE ];
|
|
||||||
if( strlen( parameters ) == 0 )
|
|
||||||
client.println( "501 No file name");
|
|
||||||
else if( makePath( path ))
|
|
||||||
{
|
|
||||||
file = SD.open(path, FILE_READ);
|
|
||||||
if( !file)
|
|
||||||
client.println( "550 File " +String(parameters)+ " not found");
|
|
||||||
else if( !file )
|
|
||||||
client.println( "450 Can't open " +String(parameters));
|
|
||||||
else if( ! dataConnect())
|
|
||||||
client.println( "425 No data connection");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println("Sending " + String(parameters));
|
|
||||||
#endif
|
|
||||||
client.println( "150-Connected to port "+ String(dataPort));
|
|
||||||
client.println( "150 " + String(file.size()) + " bytes to download");
|
|
||||||
millisBeginTrans = millis();
|
|
||||||
bytesTransfered = 0;
|
|
||||||
transferStatus = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// STOR - Store
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "STOR" ))
|
|
||||||
{
|
|
||||||
char path[ FTP_CWD_SIZE ];
|
|
||||||
if( strlen( parameters ) == 0 )
|
|
||||||
client.println( "501 No file name");
|
|
||||||
else if( makePath( path ))
|
|
||||||
{
|
|
||||||
file = SD.open(path, FILE_WRITE);
|
|
||||||
if( !file)
|
|
||||||
client.println( "451 Can't open/create " +String(parameters) );
|
|
||||||
else if( ! dataConnect())
|
|
||||||
{
|
|
||||||
client.println( "425 No data connection");
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println( "Receiving " +String(parameters));
|
|
||||||
#endif
|
|
||||||
client.println( "150 Connected to port " + String(dataPort));
|
|
||||||
millisBeginTrans = millis();
|
|
||||||
bytesTransfered = 0;
|
|
||||||
transferStatus = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// MKD - Make Directory
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "MKD" ))
|
|
||||||
{
|
|
||||||
char path[ FTP_CWD_SIZE ];
|
|
||||||
if( strlen( parameters ) == 0 )
|
|
||||||
client.println( "501 No file name");
|
|
||||||
else if( makePath( path )) {
|
|
||||||
if (SD.exists(path)) {
|
|
||||||
client.println("550 Directory already exists");
|
|
||||||
} else {
|
|
||||||
SD.mkdir(path);
|
|
||||||
client.println("257 Directory created");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
client.println( "550 Can't create \"" + String(parameters) + "\" - malformed path?"); //not support on espyet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// RMD - Remove a Directory
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "RMD" ))
|
|
||||||
{
|
|
||||||
client.println( "501 Can't delete \"" +String(parameters));
|
|
||||||
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// RNFR - Rename From
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "RNFR" ))
|
|
||||||
{
|
|
||||||
client.println("500 Rename not supported");
|
|
||||||
/*buf[ 0 ] = 0;
|
|
||||||
if( strlen( parameters ) == 0 )
|
|
||||||
client.println( "501 No file name");
|
|
||||||
else if( makePath( buf ))
|
|
||||||
{
|
|
||||||
if( ! SD.exists( buf ))
|
|
||||||
client.println( "550 File " +String(parameters)+ " not found");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println("Renaming " + String(buf));
|
|
||||||
#endif
|
|
||||||
client.println( "350 RNFR accepted - file exists, ready for destination");
|
|
||||||
rnfrCmd = true;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// RNTO - Rename To
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "RNTO" ))
|
|
||||||
{
|
|
||||||
client.println("500 Rename not supported");
|
|
||||||
/*char path[ FTP_CWD_SIZE ];
|
|
||||||
char dir[ FTP_FIL_SIZE ];
|
|
||||||
if( strlen( buf ) == 0 || ! rnfrCmd )
|
|
||||||
client.println( "503 Need RNFR before RNTO");
|
|
||||||
else if( strlen( parameters ) == 0 )
|
|
||||||
client.println( "501 No file name");
|
|
||||||
else
|
|
||||||
|
|
||||||
else if( makePath( path ))
|
|
||||||
{
|
|
||||||
if( SD.exists( path ))
|
|
||||||
client.println( "553 " +String(parameters)+ " already exists");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println("Renaming " + String(buf) + " to " + String(path));
|
|
||||||
#endif
|
|
||||||
if( SD.rename( buf, path ))
|
|
||||||
client.println( "250 File successfully renamed or moved");
|
|
||||||
else
|
|
||||||
client.println( "451 Rename/move failure");
|
|
||||||
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
rnfrCmd = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////
|
|
||||||
// //
|
|
||||||
// EXTENSIONS COMMANDS (RFC 3659) //
|
|
||||||
// //
|
|
||||||
///////////////////////////////////////
|
|
||||||
|
|
||||||
//
|
|
||||||
// FEAT - New Features
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "FEAT" ))
|
|
||||||
{
|
|
||||||
client.println( "211-Extensions suported:");
|
|
||||||
client.println( " MLSD");
|
|
||||||
client.println( "211 End.");
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// MDTM - File Modification Time (see RFC 3659)
|
|
||||||
//
|
|
||||||
else if (!strcmp(command, "MDTM"))
|
|
||||||
{
|
|
||||||
client.println("550 Unable to retrieve time");
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// SIZE - Size of the file
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "SIZE" ))
|
|
||||||
{
|
|
||||||
char path[ FTP_CWD_SIZE ];
|
|
||||||
if( strlen( parameters ) == 0 )
|
|
||||||
client.println( "501 No file name");
|
|
||||||
else if( makePath( path ))
|
|
||||||
{
|
|
||||||
file = SD.open(path, FILE_READ);
|
|
||||||
if(!file)
|
|
||||||
client.println( "450 Can't open " +String(parameters) );
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.println( "213 " + String(file.size()));
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// SITE - System command
|
|
||||||
//
|
|
||||||
else if( ! strcmp( command, "SITE" ))
|
|
||||||
{
|
|
||||||
client.println( "500 Unknow SITE command " +String(parameters) );
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// Unrecognized commands ...
|
|
||||||
//
|
|
||||||
else
|
|
||||||
client.println( "500 Unknow command");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean FtpServer::dataConnect()
|
|
||||||
{
|
|
||||||
unsigned long startTime = millis();
|
|
||||||
//wait 5 seconds for a data connection
|
|
||||||
if (!data.connected())
|
|
||||||
{
|
|
||||||
while (!dataServer.hasClient() && millis() - startTime < 10000)
|
|
||||||
{
|
|
||||||
//delay(100);
|
|
||||||
yield();
|
|
||||||
}
|
|
||||||
if (dataServer.hasClient()) {
|
|
||||||
data.stop();
|
|
||||||
data = dataServer.available();
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println("ftpdataserver client....");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.connected();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean FtpServer::doRetrieve()
|
|
||||||
{
|
|
||||||
if (data.connected())
|
|
||||||
{
|
|
||||||
int16_t nb = file.readBytes(buf, FTP_BUF_SIZE);
|
|
||||||
if (nb > 0)
|
|
||||||
{
|
|
||||||
data.write((uint8_t*)buf, nb);
|
|
||||||
bytesTransfered += nb;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closeTransfer();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean FtpServer::doStore()
|
|
||||||
{
|
|
||||||
// Avoid blocking by never reading more bytes than are available
|
|
||||||
int navail = data.available();
|
|
||||||
|
|
||||||
if (navail > 0)
|
|
||||||
{
|
|
||||||
// And be sure not to overflow buf.
|
|
||||||
if (navail > FTP_BUF_SIZE) navail = FTP_BUF_SIZE;
|
|
||||||
int16_t nb = data.read((uint8_t*) buf, navail );
|
|
||||||
// int16_t nb = data.readBytes((uint8_t*) buf, FTP_BUF_SIZE );
|
|
||||||
if( nb > 0 )
|
|
||||||
{
|
|
||||||
// Serial.println( millis() << " " << nb << endl;
|
|
||||||
file.write((uint8_t*) buf, nb );
|
|
||||||
bytesTransfered += nb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( !data.connected() && (navail <= 0) )
|
|
||||||
{
|
|
||||||
closeTransfer();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FtpServer::closeTransfer()
|
|
||||||
{
|
|
||||||
uint32_t deltaT = (int32_t) ( millis() - millisBeginTrans );
|
|
||||||
if( deltaT > 0 && bytesTransfered > 0 )
|
|
||||||
{
|
|
||||||
client.println( "226-File successfully transferred");
|
|
||||||
client.println( "226 " + String(deltaT) + " ms, "+ String(bytesTransfered / deltaT) + " kbytes/s");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
client.println( "226 File successfully transferred");
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
data.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FtpServer::abortTransfer()
|
|
||||||
{
|
|
||||||
if( transferStatus > 0 )
|
|
||||||
{
|
|
||||||
file.close();
|
|
||||||
data.stop();
|
|
||||||
client.println( "426 Transfer aborted" );
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.println( "Transfer aborted!") ;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
transferStatus = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read a char from client connected to ftp server
|
|
||||||
//
|
|
||||||
// update cmdLine and command buffers, iCL and parameters pointers
|
|
||||||
//
|
|
||||||
// return:
|
|
||||||
// -2 if buffer cmdLine is full
|
|
||||||
// -1 if line not completed
|
|
||||||
// 0 if empty line received
|
|
||||||
// length of cmdLine (positive) if no empty line received
|
|
||||||
|
|
||||||
int8_t FtpServer::readChar()
|
|
||||||
{
|
|
||||||
int8_t rc = -1;
|
|
||||||
|
|
||||||
if( client.available())
|
|
||||||
{
|
|
||||||
char c = client.read();
|
|
||||||
// char c;
|
|
||||||
// client.readBytes((uint8_t*) c, 1);
|
|
||||||
#ifdef FTP_DEBUG
|
|
||||||
Serial.print( c);
|
|
||||||
#endif
|
|
||||||
if( c == '\\' )
|
|
||||||
c = '/';
|
|
||||||
if( c != '\r' )
|
|
||||||
if( c != '\n' )
|
|
||||||
{
|
|
||||||
if( iCL < FTP_CMD_SIZE )
|
|
||||||
cmdLine[ iCL ++ ] = c;
|
|
||||||
else
|
|
||||||
rc = -2; // Line too long
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cmdLine[ iCL ] = 0;
|
|
||||||
command[ 0 ] = 0;
|
|
||||||
parameters = NULL;
|
|
||||||
// empty line?
|
|
||||||
if( iCL == 0 )
|
|
||||||
rc = 0;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
rc = iCL;
|
|
||||||
// search for space between command and parameters
|
|
||||||
parameters = strchr( cmdLine, ' ' );
|
|
||||||
if( parameters != NULL )
|
|
||||||
{
|
|
||||||
if( parameters - cmdLine > 4 )
|
|
||||||
rc = -2; // Syntax error
|
|
||||||
else
|
|
||||||
{
|
|
||||||
strncpy( command, cmdLine, parameters - cmdLine );
|
|
||||||
command[ parameters - cmdLine ] = 0;
|
|
||||||
|
|
||||||
while( * ( ++ parameters ) == ' ' )
|
|
||||||
;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if( strlen( cmdLine ) > 4 )
|
|
||||||
rc = -2; // Syntax error.
|
|
||||||
else
|
|
||||||
strcpy( command, cmdLine );
|
|
||||||
iCL = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( rc > 0 )
|
|
||||||
for( uint8_t i = 0 ; i < strlen( command ); i ++ )
|
|
||||||
command[ i ] = toupper( command[ i ] );
|
|
||||||
if( rc == -2 )
|
|
||||||
{
|
|
||||||
iCL = 0;
|
|
||||||
client.println( "500 Syntax error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make complete path/name from cwdName and parameters
|
|
||||||
//
|
|
||||||
// 3 possible cases: parameters can be absolute path, relative path or only the name
|
|
||||||
//
|
|
||||||
// parameters:
|
|
||||||
// fullName : where to store the path/name
|
|
||||||
//
|
|
||||||
// return:
|
|
||||||
// true, if done
|
|
||||||
|
|
||||||
boolean FtpServer::makePath( char * fullName )
|
|
||||||
{
|
|
||||||
return makePath( fullName, parameters );
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean FtpServer::makePath( char * fullName, char * param )
|
|
||||||
{
|
|
||||||
if( param == NULL )
|
|
||||||
param = parameters;
|
|
||||||
|
|
||||||
// Root or empty?
|
|
||||||
if( strcmp( param, "/" ) == 0 || strlen( param ) == 0 )
|
|
||||||
{
|
|
||||||
strcpy( fullName, "/" );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// If relative path, concatenate with current dir
|
|
||||||
if( param[0] != '/' )
|
|
||||||
{
|
|
||||||
strcpy( fullName, cwdName );
|
|
||||||
if( fullName[ strlen( fullName ) - 1 ] != '/' )
|
|
||||||
strncat( fullName, "/", FTP_CWD_SIZE );
|
|
||||||
strncat( fullName, param, FTP_CWD_SIZE );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
strcpy( fullName, param );
|
|
||||||
// If ends with '/', remove it
|
|
||||||
uint16_t strl = strlen( fullName ) - 1;
|
|
||||||
if( fullName[ strl ] == '/' && strl > 1 )
|
|
||||||
fullName[ strl ] = 0;
|
|
||||||
if( strlen( fullName ) < FTP_CWD_SIZE )
|
|
||||||
return true;
|
|
||||||
|
|
||||||
client.println( "500 Command line too long");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate year, month, day, hour, minute and second
|
|
||||||
// from first parameter sent by MDTM command (YYYYMMDDHHMMSS)
|
|
||||||
//
|
|
||||||
// parameters:
|
|
||||||
// pyear, pmonth, pday, phour, pminute and psecond: pointer of
|
|
||||||
// variables where to store data
|
|
||||||
//
|
|
||||||
// return:
|
|
||||||
// 0 if parameter is not YYYYMMDDHHMMSS
|
|
||||||
// length of parameter + space
|
|
||||||
|
|
||||||
uint8_t FtpServer::getDateTime( uint16_t * pyear, uint8_t * pmonth, uint8_t * pday,
|
|
||||||
uint8_t * phour, uint8_t * pminute, uint8_t * psecond )
|
|
||||||
{
|
|
||||||
char dt[ 15 ];
|
|
||||||
|
|
||||||
// Date/time are expressed as a 14 digits long string
|
|
||||||
// terminated by a space and followed by name of file
|
|
||||||
if( strlen( parameters ) < 15 || parameters[ 14 ] != ' ' )
|
|
||||||
return 0;
|
|
||||||
for( uint8_t i = 0; i < 14; i++ )
|
|
||||||
if( ! isdigit( parameters[ i ]))
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
strncpy( dt, parameters, 14 );
|
|
||||||
dt[ 14 ] = 0;
|
|
||||||
* psecond = atoi( dt + 12 );
|
|
||||||
dt[ 12 ] = 0;
|
|
||||||
* pminute = atoi( dt + 10 );
|
|
||||||
dt[ 10 ] = 0;
|
|
||||||
* phour = atoi( dt + 8 );
|
|
||||||
dt[ 8 ] = 0;
|
|
||||||
* pday = atoi( dt + 6 );
|
|
||||||
dt[ 6 ] = 0 ;
|
|
||||||
* pmonth = atoi( dt + 4 );
|
|
||||||
dt[ 4 ] = 0 ;
|
|
||||||
* pyear = atoi( dt );
|
|
||||||
return 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create string YYYYMMDDHHMMSS from date and time
|
|
||||||
//
|
|
||||||
// parameters:
|
|
||||||
// date, time
|
|
||||||
// tstr: where to store the string. Must be at least 15 characters long
|
|
||||||
//
|
|
||||||
// return:
|
|
||||||
// pointer to tstr
|
|
||||||
|
|
||||||
char * FtpServer::makeDateTimeStr( char * tstr, uint16_t date, uint16_t time )
|
|
||||||
{
|
|
||||||
sprintf( tstr, "%04u%02u%02u%02u%02u%02u",
|
|
||||||
(( date & 0xFE00 ) >> 9 ) + 1980, ( date & 0x01E0 ) >> 5, date & 0x001F,
|
|
||||||
( time & 0xF800 ) >> 11, ( time & 0x07E0 ) >> 5, ( time & 0x001F ) << 1 );
|
|
||||||
return tstr;
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,108 +0,0 @@
|
|||||||
|
|
||||||
/*
|
|
||||||
* FTP SERVER FOR ESP8266
|
|
||||||
* based on FTP Serveur for Arduino Due and Ethernet shield (W5100) or WIZ820io (W5200)
|
|
||||||
* based on Jean-Michel Gallego's work
|
|
||||||
* modified to work with esp8266 SPIFFS by David Paiva (david@nailbuster.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** **
|
|
||||||
** DEFINITIONS FOR FTP SERVER **
|
|
||||||
** **
|
|
||||||
*******************************************************************************/
|
|
||||||
|
|
||||||
// Uncomment to print debugging info to console attached to ESP8266
|
|
||||||
//#define FTP_DEBUG
|
|
||||||
|
|
||||||
#ifndef FTP_SERVERESP_H
|
|
||||||
#define FTP_SERVERESP_H
|
|
||||||
|
|
||||||
#define FTP_DEBUG
|
|
||||||
|
|
||||||
//#include "Streaming.h"
|
|
||||||
#include <FS.h>
|
|
||||||
#include <WiFiClient.h>
|
|
||||||
|
|
||||||
#define FTP_SERVER_VERSION "FTP-2017-10-18"
|
|
||||||
|
|
||||||
#define FTP_CTRL_PORT 21 // Command port on wich server is listening
|
|
||||||
#define FTP_DATA_PORT_PASV 50009 // Data port in passive mode
|
|
||||||
|
|
||||||
#define FTP_TIME_OUT 5 // Disconnect client after 5 minutes of inactivity
|
|
||||||
#define FTP_CMD_SIZE 255 + 8 // max size of a command
|
|
||||||
#define FTP_CWD_SIZE 255 + 8 // max size of a directory name
|
|
||||||
#define FTP_FIL_SIZE 255 // max size of a file name
|
|
||||||
//#define FTP_BUF_SIZE 1024 //512 // size of file buffer for read/write
|
|
||||||
#define FTP_BUF_SIZE 2*1460 //512 // size of file buffer for read/write
|
|
||||||
|
|
||||||
class FtpServer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void begin(String uname, String pword);
|
|
||||||
void handleFTP();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void iniVariables();
|
|
||||||
void clientConnected();
|
|
||||||
void disconnectClient();
|
|
||||||
boolean userIdentity();
|
|
||||||
boolean userPassword();
|
|
||||||
boolean processCommand();
|
|
||||||
boolean dataConnect();
|
|
||||||
boolean doRetrieve();
|
|
||||||
boolean doStore();
|
|
||||||
void closeTransfer();
|
|
||||||
void abortTransfer();
|
|
||||||
boolean makePath( char * fullname );
|
|
||||||
boolean makePath( char * fullName, char * param );
|
|
||||||
uint8_t getDateTime( uint16_t * pyear, uint8_t * pmonth, uint8_t * pday,
|
|
||||||
uint8_t * phour, uint8_t * pminute, uint8_t * second );
|
|
||||||
char * makeDateTimeStr( char * tstr, uint16_t date, uint16_t time );
|
|
||||||
int8_t readChar();
|
|
||||||
|
|
||||||
IPAddress dataIp; // IP address of client for data
|
|
||||||
WiFiClient client;
|
|
||||||
WiFiClient data;
|
|
||||||
|
|
||||||
File file;
|
|
||||||
|
|
||||||
boolean dataPassiveConn;
|
|
||||||
uint16_t dataPort;
|
|
||||||
char buf[ FTP_BUF_SIZE ]; // data buffer for transfers
|
|
||||||
char cmdLine[ FTP_CMD_SIZE ]; // where to store incoming char from client
|
|
||||||
char cwdName[ FTP_CWD_SIZE ]; // name of current directory
|
|
||||||
char command[ 5 ]; // command sent by client
|
|
||||||
boolean rnfrCmd; // previous command was RNFR
|
|
||||||
char * parameters; // point to begin of parameters sent by client
|
|
||||||
uint16_t iCL; // pointer to cmdLine next incoming char
|
|
||||||
int8_t cmdStatus, // status of ftp command connexion
|
|
||||||
transferStatus; // status of ftp data transfer
|
|
||||||
uint32_t millisTimeOut, // disconnect after 5 min of inactivity
|
|
||||||
millisDelay,
|
|
||||||
millisEndConnection, //
|
|
||||||
millisBeginTrans, // store time of beginning of a transaction
|
|
||||||
bytesTransfered; //
|
|
||||||
String _FTP_USER;
|
|
||||||
String _FTP_PASS;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // FTP_SERVERESP_H
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,504 +0,0 @@
|
|||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
|
||||||
Version 2.1, February 1999
|
|
||||||
|
|
||||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
(This is the first released version of the Lesser GPL. It also counts
|
|
||||||
as the successor of the GNU Library Public License, version 2, hence
|
|
||||||
the version number 2.1.)
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
Licenses are intended to guarantee your freedom to share and change
|
|
||||||
free software--to make sure the software is free for all its users.
|
|
||||||
|
|
||||||
This license, the Lesser General Public License, applies to some
|
|
||||||
specially designated software packages--typically libraries--of the
|
|
||||||
Free Software Foundation and other authors who decide to use it. You
|
|
||||||
can use it too, but we suggest you first think carefully about whether
|
|
||||||
this license or the ordinary General Public License is the better
|
|
||||||
strategy to use in any particular case, based on the explanations below.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom of use,
|
|
||||||
not price. Our General Public Licenses are designed to make sure that
|
|
||||||
you have the freedom to distribute copies of free software (and charge
|
|
||||||
for this service if you wish); that you receive source code or can get
|
|
||||||
it if you want it; that you can change the software and use pieces of
|
|
||||||
it in new free programs; and that you are informed that you can do
|
|
||||||
these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
distributors to deny you these rights or to ask you to surrender these
|
|
||||||
rights. These restrictions translate to certain responsibilities for
|
|
||||||
you if you distribute copies of the library or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of the library, whether gratis
|
|
||||||
or for a fee, you must give the recipients all the rights that we gave
|
|
||||||
you. You must make sure that they, too, receive or can get the source
|
|
||||||
code. If you link other code with the library, you must provide
|
|
||||||
complete object files to the recipients, so that they can relink them
|
|
||||||
with the library after making changes to the library and recompiling
|
|
||||||
it. And you must show them these terms so they know their rights.
|
|
||||||
|
|
||||||
We protect your rights with a two-step method: (1) we copyright the
|
|
||||||
library, and (2) we offer you this license, which gives you legal
|
|
||||||
permission to copy, distribute and/or modify the library.
|
|
||||||
|
|
||||||
To protect each distributor, we want to make it very clear that
|
|
||||||
there is no warranty for the free library. Also, if the library is
|
|
||||||
modified by someone else and passed on, the recipients should know
|
|
||||||
that what they have is not the original version, so that the original
|
|
||||||
author's reputation will not be affected by problems that might be
|
|
||||||
introduced by others.
|
|
||||||
|
|
||||||
Finally, software patents pose a constant threat to the existence of
|
|
||||||
any free program. We wish to make sure that a company cannot
|
|
||||||
effectively restrict the users of a free program by obtaining a
|
|
||||||
restrictive license from a patent holder. Therefore, we insist that
|
|
||||||
any patent license obtained for a version of the library must be
|
|
||||||
consistent with the full freedom of use specified in this license.
|
|
||||||
|
|
||||||
Most GNU software, including some libraries, is covered by the
|
|
||||||
ordinary GNU General Public License. This license, the GNU Lesser
|
|
||||||
General Public License, applies to certain designated libraries, and
|
|
||||||
is quite different from the ordinary General Public License. We use
|
|
||||||
this license for certain libraries in order to permit linking those
|
|
||||||
libraries into non-free programs.
|
|
||||||
|
|
||||||
When a program is linked with a library, whether statically or using
|
|
||||||
a shared library, the combination of the two is legally speaking a
|
|
||||||
combined work, a derivative of the original library. The ordinary
|
|
||||||
General Public License therefore permits such linking only if the
|
|
||||||
entire combination fits its criteria of freedom. The Lesser General
|
|
||||||
Public License permits more lax criteria for linking other code with
|
|
||||||
the library.
|
|
||||||
|
|
||||||
We call this license the "Lesser" General Public License because it
|
|
||||||
does Less to protect the user's freedom than the ordinary General
|
|
||||||
Public License. It also provides other free software developers Less
|
|
||||||
of an advantage over competing non-free programs. These disadvantages
|
|
||||||
are the reason we use the ordinary General Public License for many
|
|
||||||
libraries. However, the Lesser license provides advantages in certain
|
|
||||||
special circumstances.
|
|
||||||
|
|
||||||
For example, on rare occasions, there may be a special need to
|
|
||||||
encourage the widest possible use of a certain library, so that it becomes
|
|
||||||
a de-facto standard. To achieve this, non-free programs must be
|
|
||||||
allowed to use the library. A more frequent case is that a free
|
|
||||||
library does the same job as widely used non-free libraries. In this
|
|
||||||
case, there is little to gain by limiting the free library to free
|
|
||||||
software only, so we use the Lesser General Public License.
|
|
||||||
|
|
||||||
In other cases, permission to use a particular library in non-free
|
|
||||||
programs enables a greater number of people to use a large body of
|
|
||||||
free software. For example, permission to use the GNU C Library in
|
|
||||||
non-free programs enables many more people to use the whole GNU
|
|
||||||
operating system, as well as its variant, the GNU/Linux operating
|
|
||||||
system.
|
|
||||||
|
|
||||||
Although the Lesser General Public License is Less protective of the
|
|
||||||
users' freedom, it does ensure that the user of a program that is
|
|
||||||
linked with the Library has the freedom and the wherewithal to run
|
|
||||||
that program using a modified version of the Library.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow. Pay close attention to the difference between a
|
|
||||||
"work based on the library" and a "work that uses the library". The
|
|
||||||
former contains code derived from the library, whereas the latter must
|
|
||||||
be combined with the library in order to run.
|
|
||||||
|
|
||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License Agreement applies to any software library or other
|
|
||||||
program which contains a notice placed by the copyright holder or
|
|
||||||
other authorized party saying it may be distributed under the terms of
|
|
||||||
this Lesser General Public License (also called "this License").
|
|
||||||
Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
A "library" means a collection of software functions and/or data
|
|
||||||
prepared so as to be conveniently linked with application programs
|
|
||||||
(which use some of those functions and data) to form executables.
|
|
||||||
|
|
||||||
The "Library", below, refers to any such software library or work
|
|
||||||
which has been distributed under these terms. A "work based on the
|
|
||||||
Library" means either the Library or any derivative work under
|
|
||||||
copyright law: that is to say, a work containing the Library or a
|
|
||||||
portion of it, either verbatim or with modifications and/or translated
|
|
||||||
straightforwardly into another language. (Hereinafter, translation is
|
|
||||||
included without limitation in the term "modification".)
|
|
||||||
|
|
||||||
"Source code" for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For a library, complete source code means
|
|
||||||
all the source code for all modules it contains, plus any associated
|
|
||||||
interface definition files, plus the scripts used to control compilation
|
|
||||||
and installation of the library.
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running a program using the Library is not restricted, and output from
|
|
||||||
such a program is covered only if its contents constitute a work based
|
|
||||||
on the Library (independent of the use of the Library in a tool for
|
|
||||||
writing it). Whether that is true depends on what the Library does
|
|
||||||
and what the program that uses the Library does.
|
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Library's
|
|
||||||
complete source code as you receive it, in any medium, provided that
|
|
||||||
you conspicuously and appropriately publish on each copy an
|
|
||||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
|
||||||
all the notices that refer to this License and to the absence of any
|
|
||||||
warranty; and distribute a copy of this License along with the
|
|
||||||
Library.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy,
|
|
||||||
and you may at your option offer warranty protection in exchange for a
|
|
||||||
fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Library or any portion
|
|
||||||
of it, thus forming a work based on the Library, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The modified work must itself be a software library.
|
|
||||||
|
|
||||||
b) You must cause the files modified to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
c) You must cause the whole of the work to be licensed at no
|
|
||||||
charge to all third parties under the terms of this License.
|
|
||||||
|
|
||||||
d) If a facility in the modified Library refers to a function or a
|
|
||||||
table of data to be supplied by an application program that uses
|
|
||||||
the facility, other than as an argument passed when the facility
|
|
||||||
is invoked, then you must make a good faith effort to ensure that,
|
|
||||||
in the event an application does not supply such function or
|
|
||||||
table, the facility still operates, and performs whatever part of
|
|
||||||
its purpose remains meaningful.
|
|
||||||
|
|
||||||
(For example, a function in a library to compute square roots has
|
|
||||||
a purpose that is entirely well-defined independent of the
|
|
||||||
application. Therefore, Subsection 2d requires that any
|
|
||||||
application-supplied function or table used by this function must
|
|
||||||
be optional: if the application does not supply it, the square
|
|
||||||
root function must still compute square roots.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Library,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Library, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote
|
|
||||||
it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Library.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Library
|
|
||||||
with the Library (or with a work based on the Library) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
|
||||||
License instead of this License to a given copy of the Library. To do
|
|
||||||
this, you must alter all the notices that refer to this License, so
|
|
||||||
that they refer to the ordinary GNU General Public License, version 2,
|
|
||||||
instead of to this License. (If a newer version than version 2 of the
|
|
||||||
ordinary GNU General Public License has appeared, then you can specify
|
|
||||||
that version instead if you wish.) Do not make any other change in
|
|
||||||
these notices.
|
|
||||||
|
|
||||||
Once this change is made in a given copy, it is irreversible for
|
|
||||||
that copy, so the ordinary GNU General Public License applies to all
|
|
||||||
subsequent copies and derivative works made from that copy.
|
|
||||||
|
|
||||||
This option is useful when you wish to copy part of the code of
|
|
||||||
the Library into a program that is not a library.
|
|
||||||
|
|
||||||
4. You may copy and distribute the Library (or a portion or
|
|
||||||
derivative of it, under Section 2) in object code or executable form
|
|
||||||
under the terms of Sections 1 and 2 above provided that you accompany
|
|
||||||
it with the complete corresponding machine-readable source code, which
|
|
||||||
must be distributed under the terms of Sections 1 and 2 above on a
|
|
||||||
medium customarily used for software interchange.
|
|
||||||
|
|
||||||
If distribution of object code is made by offering access to copy
|
|
||||||
from a designated place, then offering equivalent access to copy the
|
|
||||||
source code from the same place satisfies the requirement to
|
|
||||||
distribute the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
5. A program that contains no derivative of any portion of the
|
|
||||||
Library, but is designed to work with the Library by being compiled or
|
|
||||||
linked with it, is called a "work that uses the Library". Such a
|
|
||||||
work, in isolation, is not a derivative work of the Library, and
|
|
||||||
therefore falls outside the scope of this License.
|
|
||||||
|
|
||||||
However, linking a "work that uses the Library" with the Library
|
|
||||||
creates an executable that is a derivative of the Library (because it
|
|
||||||
contains portions of the Library), rather than a "work that uses the
|
|
||||||
library". The executable is therefore covered by this License.
|
|
||||||
Section 6 states terms for distribution of such executables.
|
|
||||||
|
|
||||||
When a "work that uses the Library" uses material from a header file
|
|
||||||
that is part of the Library, the object code for the work may be a
|
|
||||||
derivative work of the Library even though the source code is not.
|
|
||||||
Whether this is true is especially significant if the work can be
|
|
||||||
linked without the Library, or if the work is itself a library. The
|
|
||||||
threshold for this to be true is not precisely defined by law.
|
|
||||||
|
|
||||||
If such an object file uses only numerical parameters, data
|
|
||||||
structure layouts and accessors, and small macros and small inline
|
|
||||||
functions (ten lines or less in length), then the use of the object
|
|
||||||
file is unrestricted, regardless of whether it is legally a derivative
|
|
||||||
work. (Executables containing this object code plus portions of the
|
|
||||||
Library will still fall under Section 6.)
|
|
||||||
|
|
||||||
Otherwise, if the work is a derivative of the Library, you may
|
|
||||||
distribute the object code for the work under the terms of Section 6.
|
|
||||||
Any executables containing that work also fall under Section 6,
|
|
||||||
whether or not they are linked directly with the Library itself.
|
|
||||||
|
|
||||||
6. As an exception to the Sections above, you may also combine or
|
|
||||||
link a "work that uses the Library" with the Library to produce a
|
|
||||||
work containing portions of the Library, and distribute that work
|
|
||||||
under terms of your choice, provided that the terms permit
|
|
||||||
modification of the work for the customer's own use and reverse
|
|
||||||
engineering for debugging such modifications.
|
|
||||||
|
|
||||||
You must give prominent notice with each copy of the work that the
|
|
||||||
Library is used in it and that the Library and its use are covered by
|
|
||||||
this License. You must supply a copy of this License. If the work
|
|
||||||
during execution displays copyright notices, you must include the
|
|
||||||
copyright notice for the Library among them, as well as a reference
|
|
||||||
directing the user to the copy of this License. Also, you must do one
|
|
||||||
of these things:
|
|
||||||
|
|
||||||
a) Accompany the work with the complete corresponding
|
|
||||||
machine-readable source code for the Library including whatever
|
|
||||||
changes were used in the work (which must be distributed under
|
|
||||||
Sections 1 and 2 above); and, if the work is an executable linked
|
|
||||||
with the Library, with the complete machine-readable "work that
|
|
||||||
uses the Library", as object code and/or source code, so that the
|
|
||||||
user can modify the Library and then relink to produce a modified
|
|
||||||
executable containing the modified Library. (It is understood
|
|
||||||
that the user who changes the contents of definitions files in the
|
|
||||||
Library will not necessarily be able to recompile the application
|
|
||||||
to use the modified definitions.)
|
|
||||||
|
|
||||||
b) Use a suitable shared library mechanism for linking with the
|
|
||||||
Library. A suitable mechanism is one that (1) uses at run time a
|
|
||||||
copy of the library already present on the user's computer system,
|
|
||||||
rather than copying library functions into the executable, and (2)
|
|
||||||
will operate properly with a modified version of the library, if
|
|
||||||
the user installs one, as long as the modified version is
|
|
||||||
interface-compatible with the version that the work was made with.
|
|
||||||
|
|
||||||
c) Accompany the work with a written offer, valid for at
|
|
||||||
least three years, to give the same user the materials
|
|
||||||
specified in Subsection 6a, above, for a charge no more
|
|
||||||
than the cost of performing this distribution.
|
|
||||||
|
|
||||||
d) If distribution of the work is made by offering access to copy
|
|
||||||
from a designated place, offer equivalent access to copy the above
|
|
||||||
specified materials from the same place.
|
|
||||||
|
|
||||||
e) Verify that the user has already received a copy of these
|
|
||||||
materials or that you have already sent this user a copy.
|
|
||||||
|
|
||||||
For an executable, the required form of the "work that uses the
|
|
||||||
Library" must include any data and utility programs needed for
|
|
||||||
reproducing the executable from it. However, as a special exception,
|
|
||||||
the materials to be distributed need not include anything that is
|
|
||||||
normally distributed (in either source or binary form) with the major
|
|
||||||
components (compiler, kernel, and so on) of the operating system on
|
|
||||||
which the executable runs, unless that component itself accompanies
|
|
||||||
the executable.
|
|
||||||
|
|
||||||
It may happen that this requirement contradicts the license
|
|
||||||
restrictions of other proprietary libraries that do not normally
|
|
||||||
accompany the operating system. Such a contradiction means you cannot
|
|
||||||
use both them and the Library together in an executable that you
|
|
||||||
distribute.
|
|
||||||
|
|
||||||
7. You may place library facilities that are a work based on the
|
|
||||||
Library side-by-side in a single library together with other library
|
|
||||||
facilities not covered by this License, and distribute such a combined
|
|
||||||
library, provided that the separate distribution of the work based on
|
|
||||||
the Library and of the other library facilities is otherwise
|
|
||||||
permitted, and provided that you do these two things:
|
|
||||||
|
|
||||||
a) Accompany the combined library with a copy of the same work
|
|
||||||
based on the Library, uncombined with any other library
|
|
||||||
facilities. This must be distributed under the terms of the
|
|
||||||
Sections above.
|
|
||||||
|
|
||||||
b) Give prominent notice with the combined library of the fact
|
|
||||||
that part of it is a work based on the Library, and explaining
|
|
||||||
where to find the accompanying uncombined form of the same work.
|
|
||||||
|
|
||||||
8. You may not copy, modify, sublicense, link with, or distribute
|
|
||||||
the Library except as expressly provided under this License. Any
|
|
||||||
attempt otherwise to copy, modify, sublicense, link with, or
|
|
||||||
distribute the Library is void, and will automatically terminate your
|
|
||||||
rights under this License. However, parties who have received copies,
|
|
||||||
or rights, from you under this License will not have their licenses
|
|
||||||
terminated so long as such parties remain in full compliance.
|
|
||||||
|
|
||||||
9. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Library or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Library (or any work based on the
|
|
||||||
Library), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Library or works based on it.
|
|
||||||
|
|
||||||
10. Each time you redistribute the Library (or any work based on the
|
|
||||||
Library), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute, link with or modify the Library
|
|
||||||
subject to these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties with
|
|
||||||
this License.
|
|
||||||
|
|
||||||
11. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Library at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Library by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Library.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under any
|
|
||||||
particular circumstance, the balance of the section is intended to apply,
|
|
||||||
and the section as a whole is intended to apply in other circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
12. If the distribution and/or use of the Library is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Library under this License may add
|
|
||||||
an explicit geographical distribution limitation excluding those countries,
|
|
||||||
so that distribution is permitted only in or among countries not thus
|
|
||||||
excluded. In such case, this License incorporates the limitation as if
|
|
||||||
written in the body of this License.
|
|
||||||
|
|
||||||
13. The Free Software Foundation may publish revised and/or new
|
|
||||||
versions of the Lesser General Public License from time to time.
|
|
||||||
Such new versions will be similar in spirit to the present version,
|
|
||||||
but may differ in detail to address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Library
|
|
||||||
specifies a version number of this License which applies to it and
|
|
||||||
"any later version", you have the option of following the terms and
|
|
||||||
conditions either of that version or of any later version published by
|
|
||||||
the Free Software Foundation. If the Library does not specify a
|
|
||||||
license version number, you may choose any version ever published by
|
|
||||||
the Free Software Foundation.
|
|
||||||
|
|
||||||
14. If you wish to incorporate parts of the Library into other free
|
|
||||||
programs whose distribution conditions are incompatible with these,
|
|
||||||
write to the author to ask for permission. For software which is
|
|
||||||
copyrighted by the Free Software Foundation, write to the Free
|
|
||||||
Software Foundation; we sometimes make exceptions for this. Our
|
|
||||||
decision will be guided by the two goals of preserving the free status
|
|
||||||
of all derivatives of our free software and of promoting the sharing
|
|
||||||
and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
|
||||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
|
||||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
|
||||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
|
||||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
|
||||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
|
||||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
|
||||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
|
||||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
|
||||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
|
||||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
|
||||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
|
||||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
|
||||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
|
||||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
|
||||||
DAMAGES.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Libraries
|
|
||||||
|
|
||||||
If you develop a new library, and you want it to be of the greatest
|
|
||||||
possible use to the public, we recommend making it free software that
|
|
||||||
everyone can redistribute and change. You can do so by permitting
|
|
||||||
redistribution under these terms (or, alternatively, under the terms of the
|
|
||||||
ordinary General Public License).
|
|
||||||
|
|
||||||
To apply these terms, attach the following notices to the library. It is
|
|
||||||
safest to attach them to the start of each source file to most effectively
|
|
||||||
convey the exclusion of warranty; and each file should have at least the
|
|
||||||
"copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
{description}
|
|
||||||
Copyright (C) {year} {fullname}
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library 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
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
|
||||||
USA
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
|
||||||
library `Frob' (a library for tweaking knobs) written by James Random
|
|
||||||
Hacker.
|
|
||||||
|
|
||||||
{signature of Ty Coon}, 1 April 1990
|
|
||||||
Ty Coon, President of Vice
|
|
||||||
|
|
||||||
That's all there is to it!
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# esp8266FTPServer
|
|
||||||
Simple FTP Server for using esp8266/esp32 SPIFFs
|
|
||||||
|
|
||||||
Now should support esp32!!!
|
|
||||||
|
|
||||||
I've modified a FTP server from arduino/wifi shield to work with esp8266....
|
|
||||||
|
|
||||||
This allows you to FTP into your esp8266 and access/modify the spiffs folder/data...it only allows one ftp connection at a time....very simple for now...
|
|
||||||
|
|
||||||
I've tested it with Filezilla, and the basics work (upload/download/rename/delete). There's no create/modify directory support(no directory support in SPIFFS yet).
|
|
||||||
|
|
||||||
You need to setup Filezilla(or other client) to only allow 1 connection..
|
|
||||||
To force FileZilla to use the primary connection for data transfers:
|
|
||||||
Go to File/Site Manager then select your site.
|
|
||||||
In Transfer Settings, check "Limit number of simultaneous connections" and set the maximum to 1
|
|
||||||
|
|
||||||
only supports Passive ftp mode....
|
|
||||||
|
|
||||||
It does NOT support any encryption, so you'll have to disable any form of encryption...
|
|
||||||
|
|
||||||
feel free to try it out (sample provided)....unzip into your arduino library directory (and restart arduino ide).
|
|
||||||
|
|
||||||
|
|
||||||
this is the original project on github I worked from: https://github.com/gallegojm/Arduino-Ftp-Server/tree/master/FtpServer
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "esp8266FTPServer",
|
|
||||||
"description": "Simple FTP Server for using esp8266 SPIFFs",
|
|
||||||
"keywords": "esp8266, ftp, spiffs",
|
|
||||||
"authors":
|
|
||||||
{
|
|
||||||
"name": "David Paiva",
|
|
||||||
"email": "david@paivahome.com",
|
|
||||||
"url": "http://nailbuster.com/"
|
|
||||||
},
|
|
||||||
"repository":
|
|
||||||
{
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/nailbuster/esp8266FTPServer"
|
|
||||||
},
|
|
||||||
"url": "http://nailbuster.com/",
|
|
||||||
"frameworks": "Arduino",
|
|
||||||
"platforms": "*"
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
name=ESP8266FtpServer
|
|
||||||
version=1.0.1
|
|
||||||
author=
|
|
||||||
maintainer=david@paivahome.com
|
|
||||||
sentence=Very Simple FTP server for SPIFFS on esp8266
|
|
||||||
paragraph=Very Simple FTP server for SPIFFS on esp8266
|
|
||||||
category=Communication
|
|
||||||
url=
|
|
||||||
architectures=esp8266,esp32,arduino-esp32
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
tests/bin
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: cpp
|
|
||||||
compiler:
|
|
||||||
- g++
|
|
||||||
script: cd tests && make && make test
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
2.7
|
|
||||||
* Fix remaining-length handling to prevent buffer overrun
|
|
||||||
* Add large-payload API - beginPublish/write/publish/endPublish
|
|
||||||
* Add yield call to improve reliability on ESP
|
|
||||||
* Add Clean Session flag to connect options
|
|
||||||
* Add ESP32 support for functional callback signature
|
|
||||||
* Various other fixes
|
|
||||||
|
|
||||||
2.4
|
|
||||||
* Add MQTT_SOCKET_TIMEOUT to prevent it blocking indefinitely
|
|
||||||
whilst waiting for inbound data
|
|
||||||
* Fixed return code when publishing >256 bytes
|
|
||||||
|
|
||||||
2.3
|
|
||||||
* Add publish(topic,payload,retained) function
|
|
||||||
|
|
||||||
2.2
|
|
||||||
* Change code layout to match Arduino Library reqs
|
|
||||||
|
|
||||||
2.1
|
|
||||||
* Add MAX_TRANSFER_SIZE def to chunk messages if needed
|
|
||||||
* Reject topic/payloads that exceed MQTT_MAX_PACKET_SIZE
|
|
||||||
|
|
||||||
2.0
|
|
||||||
* Add (and default to) MQTT 3.1.1 support
|
|
||||||
* Fix PROGMEM handling for Intel Galileo/ESP8266
|
|
||||||
* Add overloaded constructors for convenience
|
|
||||||
* Add chainable setters for server/callback/client/stream
|
|
||||||
* Add state function to return connack return code
|
|
||||||
|
|
||||||
1.9
|
|
||||||
* Do not split MQTT packets over multiple calls to _client->write()
|
|
||||||
* API change: All constructors now require an instance of Client
|
|
||||||
to be passed in.
|
|
||||||
* Fixed example to match 1.8 api changes - dpslwk
|
|
||||||
* Added username/password support - WilHall
|
|
||||||
* Added publish_P - publishes messages from PROGMEM - jobytaffey
|
|
||||||
|
|
||||||
1.8
|
|
||||||
* KeepAlive interval is configurable in PubSubClient.h
|
|
||||||
* Maximum packet size is configurable in PubSubClient.h
|
|
||||||
* API change: Return boolean rather than int from various functions
|
|
||||||
* API change: Length parameter in message callback changed
|
|
||||||
from int to unsigned int
|
|
||||||
* Various internal tidy-ups around types
|
|
||||||
1.7
|
|
||||||
* Improved keepalive handling
|
|
||||||
* Updated to the Arduino-1.0 API
|
|
||||||
1.6
|
|
||||||
* Added the ability to publish a retained message
|
|
||||||
|
|
||||||
1.5
|
|
||||||
* Added default constructor
|
|
||||||
* Fixed compile error when used with arduino-0021 or later
|
|
||||||
|
|
||||||
1.4
|
|
||||||
* Fixed connection lost handling
|
|
||||||
|
|
||||||
1.3
|
|
||||||
* Fixed packet reading bug in PubSubClient.readPacket
|
|
||||||
|
|
||||||
1.2
|
|
||||||
* Fixed compile error when used with arduino-0016 or later
|
|
||||||
|
|
||||||
|
|
||||||
1.1
|
|
||||||
* Reduced size of library
|
|
||||||
* Added support for Will messages
|
|
||||||
* Clarified licensing - see LICENSE.txt
|
|
||||||
|
|
||||||
|
|
||||||
1.0
|
|
||||||
* Only Quality of Service (QOS) 0 messaging is supported
|
|
||||||
* The maximum message size, including header, is 128 bytes
|
|
||||||
* The keepalive interval is set to 30 seconds
|
|
||||||
* No support for Will messages
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
Copyright (c) 2008-2015 Nicholas O'Leary
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Arduino Client for MQTT
|
|
||||||
|
|
||||||
This library provides a client for doing simple publish/subscribe messaging with
|
|
||||||
a server that supports MQTT.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
The library comes with a number of example sketches. See File > Examples > PubSubClient
|
|
||||||
within the Arduino application.
|
|
||||||
|
|
||||||
Full API documentation is available here: https://pubsubclient.knolleary.net
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
- It can only publish QoS 0 messages. It can subscribe at QoS 0 or QoS 1.
|
|
||||||
- The maximum message size, including header, is **128 bytes** by default. This
|
|
||||||
is configurable via `MQTT_MAX_PACKET_SIZE` in `PubSubClient.h`.
|
|
||||||
- The keepalive interval is set to 15 seconds by default. This is configurable
|
|
||||||
via `MQTT_KEEPALIVE` in `PubSubClient.h`.
|
|
||||||
- The client uses MQTT 3.1.1 by default. It can be changed to use MQTT 3.1 by
|
|
||||||
changing value of `MQTT_VERSION` in `PubSubClient.h`.
|
|
||||||
|
|
||||||
|
|
||||||
## Compatible Hardware
|
|
||||||
|
|
||||||
The library uses the Arduino Ethernet Client api for interacting with the
|
|
||||||
underlying network hardware. This means it Just Works with a growing number of
|
|
||||||
boards and shields, including:
|
|
||||||
|
|
||||||
- Arduino Ethernet
|
|
||||||
- Arduino Ethernet Shield
|
|
||||||
- Arduino YUN – use the included `YunClient` in place of `EthernetClient`, and
|
|
||||||
be sure to do a `Bridge.begin()` first
|
|
||||||
- Arduino WiFi Shield - if you want to send packets > 90 bytes with this shield,
|
|
||||||
enable the `MQTT_MAX_TRANSFER_SIZE` define in `PubSubClient.h`.
|
|
||||||
- Sparkfun WiFly Shield – [library](https://github.com/dpslwk/WiFly)
|
|
||||||
- TI CC3000 WiFi - [library](https://github.com/sparkfun/SFE_CC3000_Library)
|
|
||||||
- Intel Galileo/Edison
|
|
||||||
- ESP8266
|
|
||||||
- ESP32
|
|
||||||
|
|
||||||
The library cannot currently be used with hardware based on the ENC28J60 chip –
|
|
||||||
such as the Nanode or the Nuelectronics Ethernet Shield. For those, there is an
|
|
||||||
[alternative library](https://github.com/njh/NanodeMQTT) available.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This code is released under the MIT License.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
Basic MQTT example with Authentication
|
|
||||||
|
|
||||||
- connects to an MQTT server, providing username
|
|
||||||
and password
|
|
||||||
- publishes "hello world" to the topic "outTopic"
|
|
||||||
- subscribes to the topic "inTopic"
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <Ethernet.h>
|
|
||||||
#include <PubSubClient.h>
|
|
||||||
|
|
||||||
// Update these with values suitable for your network.
|
|
||||||
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
|
|
||||||
IPAddress ip(172, 16, 0, 100);
|
|
||||||
IPAddress server(172, 16, 0, 2);
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
// handle message arrived
|
|
||||||
}
|
|
||||||
|
|
||||||
EthernetClient ethClient;
|
|
||||||
PubSubClient client(server, 1883, callback, ethClient);
|
|
||||||
|
|
||||||
void setup()
|
|
||||||
{
|
|
||||||
Ethernet.begin(mac, ip);
|
|
||||||
// Note - the default maximum packet size is 128 bytes. If the
|
|
||||||
// combined length of clientId, username and password exceed this,
|
|
||||||
// you will need to increase the value of MQTT_MAX_PACKET_SIZE in
|
|
||||||
// PubSubClient.h
|
|
||||||
|
|
||||||
if (client.connect("arduinoClient", "testuser", "testpass")) {
|
|
||||||
client.publish("outTopic","hello world");
|
|
||||||
client.subscribe("inTopic");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop()
|
|
||||||
{
|
|
||||||
client.loop();
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
Basic MQTT example
|
|
||||||
|
|
||||||
This sketch demonstrates the basic capabilities of the library.
|
|
||||||
It connects to an MQTT server then:
|
|
||||||
- publishes "hello world" to the topic "outTopic"
|
|
||||||
- subscribes to the topic "inTopic", printing out any messages
|
|
||||||
it receives. NB - it assumes the received payloads are strings not binary
|
|
||||||
|
|
||||||
It will reconnect to the server if the connection is lost using a blocking
|
|
||||||
reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to
|
|
||||||
achieve the same result without blocking the main loop.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <Ethernet.h>
|
|
||||||
#include <PubSubClient.h>
|
|
||||||
|
|
||||||
// Update these with values suitable for your network.
|
|
||||||
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
|
|
||||||
IPAddress ip(172, 16, 0, 100);
|
|
||||||
IPAddress server(172, 16, 0, 2);
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
Serial.print("Message arrived [");
|
|
||||||
Serial.print(topic);
|
|
||||||
Serial.print("] ");
|
|
||||||
for (int i=0;i<length;i++) {
|
|
||||||
Serial.print((char)payload[i]);
|
|
||||||
}
|
|
||||||
Serial.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
EthernetClient ethClient;
|
|
||||||
PubSubClient client(ethClient);
|
|
||||||
|
|
||||||
void reconnect() {
|
|
||||||
// Loop until we're reconnected
|
|
||||||
while (!client.connected()) {
|
|
||||||
Serial.print("Attempting MQTT connection...");
|
|
||||||
// Attempt to connect
|
|
||||||
if (client.connect("arduinoClient")) {
|
|
||||||
Serial.println("connected");
|
|
||||||
// Once connected, publish an announcement...
|
|
||||||
client.publish("outTopic","hello world");
|
|
||||||
// ... and resubscribe
|
|
||||||
client.subscribe("inTopic");
|
|
||||||
} else {
|
|
||||||
Serial.print("failed, rc=");
|
|
||||||
Serial.print(client.state());
|
|
||||||
Serial.println(" try again in 5 seconds");
|
|
||||||
// Wait 5 seconds before retrying
|
|
||||||
delay(5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup()
|
|
||||||
{
|
|
||||||
Serial.begin(57600);
|
|
||||||
|
|
||||||
client.setServer(server, 1883);
|
|
||||||
client.setCallback(callback);
|
|
||||||
|
|
||||||
Ethernet.begin(mac, ip);
|
|
||||||
// Allow the hardware to sort itself out
|
|
||||||
delay(1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop()
|
|
||||||
{
|
|
||||||
if (!client.connected()) {
|
|
||||||
reconnect();
|
|
||||||
}
|
|
||||||
client.loop();
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
/*
|
|
||||||
Basic ESP8266 MQTT example
|
|
||||||
|
|
||||||
This sketch demonstrates the capabilities of the pubsub library in combination
|
|
||||||
with the ESP8266 board/library.
|
|
||||||
|
|
||||||
It connects to an MQTT server then:
|
|
||||||
- publishes "hello world" to the topic "outTopic" every two seconds
|
|
||||||
- subscribes to the topic "inTopic", printing out any messages
|
|
||||||
it receives. NB - it assumes the received payloads are strings not binary
|
|
||||||
- If the first character of the topic "inTopic" is an 1, switch ON the ESP Led,
|
|
||||||
else switch it off
|
|
||||||
|
|
||||||
It will reconnect to the server if the connection is lost using a blocking
|
|
||||||
reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to
|
|
||||||
achieve the same result without blocking the main loop.
|
|
||||||
|
|
||||||
To install the ESP8266 board, (using Arduino 1.6.4+):
|
|
||||||
- Add the following 3rd party board manager under "File -> Preferences -> Additional Boards Manager URLs":
|
|
||||||
http://arduino.esp8266.com/stable/package_esp8266com_index.json
|
|
||||||
- Open the "Tools -> Board -> Board Manager" and click install for the ESP8266"
|
|
||||||
- Select your ESP8266 in "Tools -> Board"
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <ESP8266WiFi.h>
|
|
||||||
#include <PubSubClient.h>
|
|
||||||
|
|
||||||
// Update these with values suitable for your network.
|
|
||||||
|
|
||||||
const char* ssid = "........";
|
|
||||||
const char* password = "........";
|
|
||||||
const char* mqtt_server = "broker.mqtt-dashboard.com";
|
|
||||||
|
|
||||||
WiFiClient espClient;
|
|
||||||
PubSubClient client(espClient);
|
|
||||||
long lastMsg = 0;
|
|
||||||
char msg[50];
|
|
||||||
int value = 0;
|
|
||||||
|
|
||||||
void setup_wifi() {
|
|
||||||
|
|
||||||
delay(10);
|
|
||||||
// We start by connecting to a WiFi network
|
|
||||||
Serial.println();
|
|
||||||
Serial.print("Connecting to ");
|
|
||||||
Serial.println(ssid);
|
|
||||||
|
|
||||||
WiFi.begin(ssid, password);
|
|
||||||
|
|
||||||
while (WiFi.status() != WL_CONNECTED) {
|
|
||||||
delay(500);
|
|
||||||
Serial.print(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
randomSeed(micros());
|
|
||||||
|
|
||||||
Serial.println("");
|
|
||||||
Serial.println("WiFi connected");
|
|
||||||
Serial.println("IP address: ");
|
|
||||||
Serial.println(WiFi.localIP());
|
|
||||||
}
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
Serial.print("Message arrived [");
|
|
||||||
Serial.print(topic);
|
|
||||||
Serial.print("] ");
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
Serial.print((char)payload[i]);
|
|
||||||
}
|
|
||||||
Serial.println();
|
|
||||||
|
|
||||||
// Switch on the LED if an 1 was received as first character
|
|
||||||
if ((char)payload[0] == '1') {
|
|
||||||
digitalWrite(BUILTIN_LED, LOW); // Turn the LED on (Note that LOW is the voltage level
|
|
||||||
// but actually the LED is on; this is because
|
|
||||||
// it is active low on the ESP-01)
|
|
||||||
} else {
|
|
||||||
digitalWrite(BUILTIN_LED, HIGH); // Turn the LED off by making the voltage HIGH
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void reconnect() {
|
|
||||||
// Loop until we're reconnected
|
|
||||||
while (!client.connected()) {
|
|
||||||
Serial.print("Attempting MQTT connection...");
|
|
||||||
// Create a random client ID
|
|
||||||
String clientId = "ESP8266Client-";
|
|
||||||
clientId += String(random(0xffff), HEX);
|
|
||||||
// Attempt to connect
|
|
||||||
if (client.connect(clientId.c_str())) {
|
|
||||||
Serial.println("connected");
|
|
||||||
// Once connected, publish an announcement...
|
|
||||||
client.publish("outTopic", "hello world");
|
|
||||||
// ... and resubscribe
|
|
||||||
client.subscribe("inTopic");
|
|
||||||
} else {
|
|
||||||
Serial.print("failed, rc=");
|
|
||||||
Serial.print(client.state());
|
|
||||||
Serial.println(" try again in 5 seconds");
|
|
||||||
// Wait 5 seconds before retrying
|
|
||||||
delay(5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output
|
|
||||||
Serial.begin(115200);
|
|
||||||
setup_wifi();
|
|
||||||
client.setServer(mqtt_server, 1883);
|
|
||||||
client.setCallback(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
|
|
||||||
if (!client.connected()) {
|
|
||||||
reconnect();
|
|
||||||
}
|
|
||||||
client.loop();
|
|
||||||
|
|
||||||
long now = millis();
|
|
||||||
if (now - lastMsg > 2000) {
|
|
||||||
lastMsg = now;
|
|
||||||
++value;
|
|
||||||
snprintf (msg, 50, "hello world #%ld", value);
|
|
||||||
Serial.print("Publish message: ");
|
|
||||||
Serial.println(msg);
|
|
||||||
client.publish("outTopic", msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
/*
|
|
||||||
Long message ESP8266 MQTT example
|
|
||||||
|
|
||||||
This sketch demonstrates sending arbitrarily large messages in combination
|
|
||||||
with the ESP8266 board/library.
|
|
||||||
|
|
||||||
It connects to an MQTT server then:
|
|
||||||
- publishes "hello world" to the topic "outTopic"
|
|
||||||
- subscribes to the topic "greenBottles/#", printing out any messages
|
|
||||||
it receives. NB - it assumes the received payloads are strings not binary
|
|
||||||
- If the sub-topic is a number, it publishes a "greenBottles/lyrics" message
|
|
||||||
with a payload consisting of the lyrics to "10 green bottles", replacing
|
|
||||||
10 with the number given in the sub-topic.
|
|
||||||
|
|
||||||
It will reconnect to the server if the connection is lost using a blocking
|
|
||||||
reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to
|
|
||||||
achieve the same result without blocking the main loop.
|
|
||||||
|
|
||||||
To install the ESP8266 board, (using Arduino 1.6.4+):
|
|
||||||
- Add the following 3rd party board manager under "File -> Preferences -> Additional Boards Manager URLs":
|
|
||||||
http://arduino.esp8266.com/stable/package_esp8266com_index.json
|
|
||||||
- Open the "Tools -> Board -> Board Manager" and click install for the ESP8266"
|
|
||||||
- Select your ESP8266 in "Tools -> Board"
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <ESP8266WiFi.h>
|
|
||||||
#include <PubSubClient.h>
|
|
||||||
|
|
||||||
// Update these with values suitable for your network.
|
|
||||||
|
|
||||||
const char* ssid = "........";
|
|
||||||
const char* password = "........";
|
|
||||||
const char* mqtt_server = "broker.mqtt-dashboard.com";
|
|
||||||
|
|
||||||
WiFiClient espClient;
|
|
||||||
PubSubClient client(espClient);
|
|
||||||
long lastMsg = 0;
|
|
||||||
char msg[50];
|
|
||||||
int value = 0;
|
|
||||||
|
|
||||||
void setup_wifi() {
|
|
||||||
|
|
||||||
delay(10);
|
|
||||||
// We start by connecting to a WiFi network
|
|
||||||
Serial.println();
|
|
||||||
Serial.print("Connecting to ");
|
|
||||||
Serial.println(ssid);
|
|
||||||
|
|
||||||
WiFi.begin(ssid, password);
|
|
||||||
|
|
||||||
while (WiFi.status() != WL_CONNECTED) {
|
|
||||||
delay(500);
|
|
||||||
Serial.print(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
randomSeed(micros());
|
|
||||||
|
|
||||||
Serial.println("");
|
|
||||||
Serial.println("WiFi connected");
|
|
||||||
Serial.println("IP address: ");
|
|
||||||
Serial.println(WiFi.localIP());
|
|
||||||
}
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
Serial.print("Message arrived [");
|
|
||||||
Serial.print(topic);
|
|
||||||
Serial.print("] ");
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
Serial.print((char)payload[i]);
|
|
||||||
}
|
|
||||||
Serial.println();
|
|
||||||
|
|
||||||
// Find out how many bottles we should generate lyrics for
|
|
||||||
String topicStr(topic);
|
|
||||||
int bottleCount = 0; // assume no bottles unless we correctly parse a value from the topic
|
|
||||||
if (topicStr.indexOf('/') >= 0) {
|
|
||||||
// The topic includes a '/', we'll try to read the number of bottles from just after that
|
|
||||||
topicStr.remove(0, topicStr.indexOf('/')+1);
|
|
||||||
// Now see if there's a number of bottles after the '/'
|
|
||||||
bottleCount = topicStr.toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bottleCount > 0) {
|
|
||||||
// Work out how big our resulting message will be
|
|
||||||
int msgLen = 0;
|
|
||||||
for (int i = bottleCount; i > 0; i--) {
|
|
||||||
String numBottles(i);
|
|
||||||
msgLen += 2*numBottles.length();
|
|
||||||
if (i == 1) {
|
|
||||||
msgLen += 2*String(" green bottle, standing on the wall\n").length();
|
|
||||||
} else {
|
|
||||||
msgLen += 2*String(" green bottles, standing on the wall\n").length();
|
|
||||||
}
|
|
||||||
msgLen += String("And if one green bottle should accidentally fall\nThere'll be ").length();
|
|
||||||
switch (i) {
|
|
||||||
case 1:
|
|
||||||
msgLen += String("no green bottles, standing on the wall\n\n").length();
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
msgLen += String("1 green bottle, standing on the wall\n\n").length();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
numBottles = i-1;
|
|
||||||
msgLen += numBottles.length();
|
|
||||||
msgLen += String(" green bottles, standing on the wall\n\n").length();
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we can start to publish the message
|
|
||||||
client.beginPublish("greenBottles/lyrics", msgLen, false);
|
|
||||||
for (int i = bottleCount; i > 0; i--) {
|
|
||||||
for (int j = 0; j < 2; j++) {
|
|
||||||
client.print(i);
|
|
||||||
if (i == 1) {
|
|
||||||
client.print(" green bottle, standing on the wall\n");
|
|
||||||
} else {
|
|
||||||
client.print(" green bottles, standing on the wall\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.print("And if one green bottle should accidentally fall\nThere'll be ");
|
|
||||||
switch (i) {
|
|
||||||
case 1:
|
|
||||||
client.print("no green bottles, standing on the wall\n\n");
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
client.print("1 green bottle, standing on the wall\n\n");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
client.print(i-1);
|
|
||||||
client.print(" green bottles, standing on the wall\n\n");
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Now we're done!
|
|
||||||
client.endPublish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reconnect() {
|
|
||||||
// Loop until we're reconnected
|
|
||||||
while (!client.connected()) {
|
|
||||||
Serial.print("Attempting MQTT connection...");
|
|
||||||
// Create a random client ID
|
|
||||||
String clientId = "ESP8266Client-";
|
|
||||||
clientId += String(random(0xffff), HEX);
|
|
||||||
// Attempt to connect
|
|
||||||
if (client.connect(clientId.c_str())) {
|
|
||||||
Serial.println("connected");
|
|
||||||
// Once connected, publish an announcement...
|
|
||||||
client.publish("outTopic", "hello world");
|
|
||||||
// ... and resubscribe
|
|
||||||
client.subscribe("greenBottles/#");
|
|
||||||
} else {
|
|
||||||
Serial.print("failed, rc=");
|
|
||||||
Serial.print(client.state());
|
|
||||||
Serial.println(" try again in 5 seconds");
|
|
||||||
// Wait 5 seconds before retrying
|
|
||||||
delay(5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output
|
|
||||||
Serial.begin(115200);
|
|
||||||
setup_wifi();
|
|
||||||
client.setServer(mqtt_server, 1883);
|
|
||||||
client.setCallback(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
|
|
||||||
if (!client.connected()) {
|
|
||||||
reconnect();
|
|
||||||
}
|
|
||||||
client.loop();
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
Publishing in the callback
|
|
||||||
|
|
||||||
- connects to an MQTT server
|
|
||||||
- subscribes to the topic "inTopic"
|
|
||||||
- when a message is received, republishes it to "outTopic"
|
|
||||||
|
|
||||||
This example shows how to publish messages within the
|
|
||||||
callback function. The callback function header needs to
|
|
||||||
be declared before the PubSubClient constructor and the
|
|
||||||
actual callback defined afterwards.
|
|
||||||
This ensures the client reference in the callback function
|
|
||||||
is valid.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <Ethernet.h>
|
|
||||||
#include <PubSubClient.h>
|
|
||||||
|
|
||||||
// Update these with values suitable for your network.
|
|
||||||
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
|
|
||||||
IPAddress ip(172, 16, 0, 100);
|
|
||||||
IPAddress server(172, 16, 0, 2);
|
|
||||||
|
|
||||||
// Callback function header
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length);
|
|
||||||
|
|
||||||
EthernetClient ethClient;
|
|
||||||
PubSubClient client(server, 1883, callback, ethClient);
|
|
||||||
|
|
||||||
// Callback function
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
// In order to republish this payload, a copy must be made
|
|
||||||
// as the orignal payload buffer will be overwritten whilst
|
|
||||||
// constructing the PUBLISH packet.
|
|
||||||
|
|
||||||
// Allocate the correct amount of memory for the payload copy
|
|
||||||
byte* p = (byte*)malloc(length);
|
|
||||||
// Copy the payload to the new buffer
|
|
||||||
memcpy(p,payload,length);
|
|
||||||
client.publish("outTopic", p, length);
|
|
||||||
// Free the memory
|
|
||||||
free(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup()
|
|
||||||
{
|
|
||||||
|
|
||||||
Ethernet.begin(mac, ip);
|
|
||||||
if (client.connect("arduinoClient")) {
|
|
||||||
client.publish("outTopic","hello world");
|
|
||||||
client.subscribe("inTopic");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop()
|
|
||||||
{
|
|
||||||
client.loop();
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
Reconnecting MQTT example - non-blocking
|
|
||||||
|
|
||||||
This sketch demonstrates how to keep the client connected
|
|
||||||
using a non-blocking reconnect function. If the client loses
|
|
||||||
its connection, it attempts to reconnect every 5 seconds
|
|
||||||
without blocking the main loop.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <Ethernet.h>
|
|
||||||
#include <PubSubClient.h>
|
|
||||||
|
|
||||||
// Update these with values suitable for your hardware/network.
|
|
||||||
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
|
|
||||||
IPAddress ip(172, 16, 0, 100);
|
|
||||||
IPAddress server(172, 16, 0, 2);
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
// handle message arrived
|
|
||||||
}
|
|
||||||
|
|
||||||
EthernetClient ethClient;
|
|
||||||
PubSubClient client(ethClient);
|
|
||||||
|
|
||||||
long lastReconnectAttempt = 0;
|
|
||||||
|
|
||||||
boolean reconnect() {
|
|
||||||
if (client.connect("arduinoClient")) {
|
|
||||||
// Once connected, publish an announcement...
|
|
||||||
client.publish("outTopic","hello world");
|
|
||||||
// ... and resubscribe
|
|
||||||
client.subscribe("inTopic");
|
|
||||||
}
|
|
||||||
return client.connected();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup()
|
|
||||||
{
|
|
||||||
client.setServer(server, 1883);
|
|
||||||
client.setCallback(callback);
|
|
||||||
|
|
||||||
Ethernet.begin(mac, ip);
|
|
||||||
delay(1500);
|
|
||||||
lastReconnectAttempt = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void loop()
|
|
||||||
{
|
|
||||||
if (!client.connected()) {
|
|
||||||
long now = millis();
|
|
||||||
if (now - lastReconnectAttempt > 5000) {
|
|
||||||
lastReconnectAttempt = now;
|
|
||||||
// Attempt to reconnect
|
|
||||||
if (reconnect()) {
|
|
||||||
lastReconnectAttempt = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Client connected
|
|
||||||
|
|
||||||
client.loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
Example of using a Stream object to store the message payload
|
|
||||||
|
|
||||||
Uses SRAM library: https://github.com/ennui2342/arduino-sram
|
|
||||||
but could use any Stream based class such as SD
|
|
||||||
|
|
||||||
- connects to an MQTT server
|
|
||||||
- publishes "hello world" to the topic "outTopic"
|
|
||||||
- subscribes to the topic "inTopic"
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <Ethernet.h>
|
|
||||||
#include <PubSubClient.h>
|
|
||||||
#include <SRAM.h>
|
|
||||||
|
|
||||||
// Update these with values suitable for your network.
|
|
||||||
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
|
|
||||||
IPAddress ip(172, 16, 0, 100);
|
|
||||||
IPAddress server(172, 16, 0, 2);
|
|
||||||
|
|
||||||
SRAM sram(4, SRAM_1024);
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
sram.seek(1);
|
|
||||||
|
|
||||||
// do something with the message
|
|
||||||
for(uint8_t i=0; i<length; i++) {
|
|
||||||
Serial.write(sram.read());
|
|
||||||
}
|
|
||||||
Serial.println();
|
|
||||||
|
|
||||||
// Reset position for the next message to be stored
|
|
||||||
sram.seek(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
EthernetClient ethClient;
|
|
||||||
PubSubClient client(server, 1883, callback, ethClient, sram);
|
|
||||||
|
|
||||||
void setup()
|
|
||||||
{
|
|
||||||
Ethernet.begin(mac, ip);
|
|
||||||
if (client.connect("arduinoClient")) {
|
|
||||||
client.publish("outTopic","hello world");
|
|
||||||
client.subscribe("inTopic");
|
|
||||||
}
|
|
||||||
|
|
||||||
sram.begin();
|
|
||||||
sram.seek(1);
|
|
||||||
|
|
||||||
Serial.begin(9600);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop()
|
|
||||||
{
|
|
||||||
client.loop();
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#######################################
|
|
||||||
# Syntax Coloring Map For PubSubClient
|
|
||||||
#######################################
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Datatypes (KEYWORD1)
|
|
||||||
#######################################
|
|
||||||
|
|
||||||
PubSubClient KEYWORD1
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Methods and Functions (KEYWORD2)
|
|
||||||
#######################################
|
|
||||||
|
|
||||||
connect KEYWORD2
|
|
||||||
disconnect KEYWORD2
|
|
||||||
publish KEYWORD2
|
|
||||||
publish_P KEYWORD2
|
|
||||||
beginPublish KEYWORD2
|
|
||||||
endPublish KEYWORD2
|
|
||||||
write KEYWORD2
|
|
||||||
subscribe KEYWORD2
|
|
||||||
unsubscribe KEYWORD2
|
|
||||||
loop KEYWORD2
|
|
||||||
connected KEYWORD2
|
|
||||||
setServer KEYWORD2
|
|
||||||
setCallback KEYWORD2
|
|
||||||
setClient KEYWORD2
|
|
||||||
setStream KEYWORD2
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Constants (LITERAL1)
|
|
||||||
#######################################
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "PubSubClient",
|
|
||||||
"keywords": "ethernet, mqtt, m2m, iot",
|
|
||||||
"description": "A client library for MQTT messaging. MQTT is a lightweight messaging protocol ideal for small devices. This library allows you to send and receive MQTT messages. It supports the latest MQTT 3.1.1 protocol and can be configured to use the older MQTT 3.1 if needed. It supports all Arduino Ethernet Client compatible hardware, including the Intel Galileo/Edison, ESP8266 and TI CC3000.",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/knolleary/pubsubclient.git"
|
|
||||||
},
|
|
||||||
"version": "2.7",
|
|
||||||
"exclude": "tests",
|
|
||||||
"examples": "examples/*/*.ino",
|
|
||||||
"frameworks": "arduino",
|
|
||||||
"platforms": [
|
|
||||||
"atmelavr",
|
|
||||||
"espressif"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
name=PubSubClient
|
|
||||||
version=2.7
|
|
||||||
author=Nick O'Leary <nick.oleary@gmail.com>
|
|
||||||
maintainer=Nick O'Leary <nick.oleary@gmail.com>
|
|
||||||
sentence=A client library for MQTT messaging.
|
|
||||||
paragraph=MQTT is a lightweight messaging protocol ideal for small devices. This library allows you to send and receive MQTT messages. It supports the latest MQTT 3.1.1 protocol and can be configured to use the older MQTT 3.1 if needed. It supports all Arduino Ethernet Client compatible hardware, including the Intel Galileo/Edison, ESP8266 and TI CC3000.
|
|
||||||
category=Communication
|
|
||||||
url=http://pubsubclient.knolleary.net
|
|
||||||
architectures=*
|
|
||||||
@@ -1,652 +0,0 @@
|
|||||||
/*
|
|
||||||
PubSubClient.cpp - A simple client for MQTT.
|
|
||||||
Nick O'Leary
|
|
||||||
http://knolleary.net
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "PubSubClient.h"
|
|
||||||
#include "Arduino.h"
|
|
||||||
|
|
||||||
PubSubClient::PubSubClient() {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
this->_client = NULL;
|
|
||||||
this->stream = NULL;
|
|
||||||
setCallback(NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient::PubSubClient(Client& client) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setClient(client);
|
|
||||||
this->stream = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient::PubSubClient(IPAddress addr, uint16_t port, Client& client) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(addr, port);
|
|
||||||
setClient(client);
|
|
||||||
this->stream = NULL;
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(IPAddress addr, uint16_t port, Client& client, Stream& stream) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(addr,port);
|
|
||||||
setClient(client);
|
|
||||||
setStream(stream);
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(IPAddress addr, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(addr, port);
|
|
||||||
setCallback(callback);
|
|
||||||
setClient(client);
|
|
||||||
this->stream = NULL;
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(IPAddress addr, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(addr,port);
|
|
||||||
setCallback(callback);
|
|
||||||
setClient(client);
|
|
||||||
setStream(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, Client& client) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(ip, port);
|
|
||||||
setClient(client);
|
|
||||||
this->stream = NULL;
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, Client& client, Stream& stream) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(ip,port);
|
|
||||||
setClient(client);
|
|
||||||
setStream(stream);
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(ip, port);
|
|
||||||
setCallback(callback);
|
|
||||||
setClient(client);
|
|
||||||
this->stream = NULL;
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(ip,port);
|
|
||||||
setCallback(callback);
|
|
||||||
setClient(client);
|
|
||||||
setStream(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient::PubSubClient(const char* domain, uint16_t port, Client& client) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(domain,port);
|
|
||||||
setClient(client);
|
|
||||||
this->stream = NULL;
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(const char* domain, uint16_t port, Client& client, Stream& stream) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(domain,port);
|
|
||||||
setClient(client);
|
|
||||||
setStream(stream);
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(const char* domain, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(domain,port);
|
|
||||||
setCallback(callback);
|
|
||||||
setClient(client);
|
|
||||||
this->stream = NULL;
|
|
||||||
}
|
|
||||||
PubSubClient::PubSubClient(const char* domain, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) {
|
|
||||||
this->_state = MQTT_DISCONNECTED;
|
|
||||||
setServer(domain,port);
|
|
||||||
setCallback(callback);
|
|
||||||
setClient(client);
|
|
||||||
setStream(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::connect(const char *id) {
|
|
||||||
return connect(id,NULL,NULL,0,0,0,0,1);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::connect(const char *id, const char *user, const char *pass) {
|
|
||||||
return connect(id,user,pass,0,0,0,0,1);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::connect(const char *id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage) {
|
|
||||||
return connect(id,NULL,NULL,willTopic,willQos,willRetain,willMessage,1);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::connect(const char *id, const char *user, const char *pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage) {
|
|
||||||
return connect(id,user,pass,willTopic,willQos,willRetain,willMessage,1);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::connect(const char *id, const char *user, const char *pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage, boolean cleanSession) {
|
|
||||||
if (!connected()) {
|
|
||||||
int result = 0;
|
|
||||||
|
|
||||||
if (domain != NULL) {
|
|
||||||
result = _client->connect(this->domain, this->port);
|
|
||||||
} else {
|
|
||||||
result = _client->connect(this->ip, this->port);
|
|
||||||
}
|
|
||||||
if (result == 1) {
|
|
||||||
nextMsgId = 1;
|
|
||||||
// Leave room in the buffer for header and variable length field
|
|
||||||
uint16_t length = MQTT_MAX_HEADER_SIZE;
|
|
||||||
unsigned int j;
|
|
||||||
|
|
||||||
#if MQTT_VERSION == MQTT_VERSION_3_1
|
|
||||||
uint8_t d[9] = {0x00,0x06,'M','Q','I','s','d','p', MQTT_VERSION};
|
|
||||||
#define MQTT_HEADER_VERSION_LENGTH 9
|
|
||||||
#elif MQTT_VERSION == MQTT_VERSION_3_1_1
|
|
||||||
uint8_t d[7] = {0x00,0x04,'M','Q','T','T',MQTT_VERSION};
|
|
||||||
#define MQTT_HEADER_VERSION_LENGTH 7
|
|
||||||
#endif
|
|
||||||
for (j = 0;j<MQTT_HEADER_VERSION_LENGTH;j++) {
|
|
||||||
buffer[length++] = d[j];
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t v;
|
|
||||||
if (willTopic) {
|
|
||||||
v = 0x04|(willQos<<3)|(willRetain<<5);
|
|
||||||
} else {
|
|
||||||
v = 0x00;
|
|
||||||
}
|
|
||||||
if (cleanSession) {
|
|
||||||
v = v|0x02;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(user != NULL) {
|
|
||||||
v = v|0x80;
|
|
||||||
|
|
||||||
if(pass != NULL) {
|
|
||||||
v = v|(0x80>>1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer[length++] = v;
|
|
||||||
|
|
||||||
buffer[length++] = ((MQTT_KEEPALIVE) >> 8);
|
|
||||||
buffer[length++] = ((MQTT_KEEPALIVE) & 0xFF);
|
|
||||||
|
|
||||||
CHECK_STRING_LENGTH(length,id)
|
|
||||||
length = writeString(id,buffer,length);
|
|
||||||
if (willTopic) {
|
|
||||||
CHECK_STRING_LENGTH(length,willTopic)
|
|
||||||
length = writeString(willTopic,buffer,length);
|
|
||||||
CHECK_STRING_LENGTH(length,willMessage)
|
|
||||||
length = writeString(willMessage,buffer,length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(user != NULL) {
|
|
||||||
CHECK_STRING_LENGTH(length,user)
|
|
||||||
length = writeString(user,buffer,length);
|
|
||||||
if(pass != NULL) {
|
|
||||||
CHECK_STRING_LENGTH(length,pass)
|
|
||||||
length = writeString(pass,buffer,length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write(MQTTCONNECT,buffer,length-MQTT_MAX_HEADER_SIZE);
|
|
||||||
|
|
||||||
lastInActivity = lastOutActivity = millis();
|
|
||||||
|
|
||||||
while (!_client->available()) {
|
|
||||||
unsigned long t = millis();
|
|
||||||
if (t-lastInActivity >= ((int32_t) MQTT_SOCKET_TIMEOUT*1000UL)) {
|
|
||||||
_state = MQTT_CONNECTION_TIMEOUT;
|
|
||||||
_client->stop();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uint8_t llen;
|
|
||||||
uint16_t len = readPacket(&llen);
|
|
||||||
|
|
||||||
if (len == 4) {
|
|
||||||
if (buffer[3] == 0) {
|
|
||||||
lastInActivity = millis();
|
|
||||||
pingOutstanding = false;
|
|
||||||
_state = MQTT_CONNECTED;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
_state = buffer[3];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_client->stop();
|
|
||||||
} else {
|
|
||||||
_state = MQTT_CONNECT_FAILED;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// reads a byte into result
|
|
||||||
boolean PubSubClient::readByte(uint8_t * result) {
|
|
||||||
uint32_t previousMillis = millis();
|
|
||||||
while(!_client->available()) {
|
|
||||||
yield();
|
|
||||||
uint32_t currentMillis = millis();
|
|
||||||
if(currentMillis - previousMillis >= ((int32_t) MQTT_SOCKET_TIMEOUT * 1000)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*result = _client->read();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// reads a byte into result[*index] and increments index
|
|
||||||
boolean PubSubClient::readByte(uint8_t * result, uint16_t * index){
|
|
||||||
uint16_t current_index = *index;
|
|
||||||
uint8_t * write_address = &(result[current_index]);
|
|
||||||
if(readByte(write_address)){
|
|
||||||
*index = current_index + 1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t PubSubClient::readPacket(uint8_t* lengthLength) {
|
|
||||||
uint16_t len = 0;
|
|
||||||
if(!readByte(buffer, &len)) return 0;
|
|
||||||
bool isPublish = (buffer[0]&0xF0) == MQTTPUBLISH;
|
|
||||||
uint32_t multiplier = 1;
|
|
||||||
uint16_t length = 0;
|
|
||||||
uint8_t digit = 0;
|
|
||||||
uint16_t skip = 0;
|
|
||||||
uint8_t start = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
if (len == 5) {
|
|
||||||
// Invalid remaining length encoding - kill the connection
|
|
||||||
_state = MQTT_DISCONNECTED;
|
|
||||||
_client->stop();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if(!readByte(&digit)) return 0;
|
|
||||||
buffer[len++] = digit;
|
|
||||||
length += (digit & 127) * multiplier;
|
|
||||||
multiplier *= 128;
|
|
||||||
} while ((digit & 128) != 0);
|
|
||||||
*lengthLength = len-1;
|
|
||||||
|
|
||||||
if (isPublish) {
|
|
||||||
// Read in topic length to calculate bytes to skip over for Stream writing
|
|
||||||
if(!readByte(buffer, &len)) return 0;
|
|
||||||
if(!readByte(buffer, &len)) return 0;
|
|
||||||
skip = (buffer[*lengthLength+1]<<8)+buffer[*lengthLength+2];
|
|
||||||
start = 2;
|
|
||||||
if (buffer[0]&MQTTQOS1) {
|
|
||||||
// skip message id
|
|
||||||
skip += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint16_t i = start;i<length;i++) {
|
|
||||||
if(!readByte(&digit)) return 0;
|
|
||||||
if (this->stream) {
|
|
||||||
if (isPublish && len-*lengthLength-2>skip) {
|
|
||||||
this->stream->write(digit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (len < MQTT_MAX_PACKET_SIZE) {
|
|
||||||
buffer[len] = digit;
|
|
||||||
}
|
|
||||||
len++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this->stream && len > MQTT_MAX_PACKET_SIZE) {
|
|
||||||
len = 0; // This will cause the packet to be ignored.
|
|
||||||
}
|
|
||||||
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::loop() {
|
|
||||||
if (connected()) {
|
|
||||||
unsigned long t = millis();
|
|
||||||
if ((t - lastInActivity > MQTT_KEEPALIVE*1000UL) || (t - lastOutActivity > MQTT_KEEPALIVE*1000UL)) {
|
|
||||||
if (pingOutstanding) {
|
|
||||||
this->_state = MQTT_CONNECTION_TIMEOUT;
|
|
||||||
_client->stop();
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
buffer[0] = MQTTPINGREQ;
|
|
||||||
buffer[1] = 0;
|
|
||||||
_client->write(buffer,2);
|
|
||||||
lastOutActivity = t;
|
|
||||||
lastInActivity = t;
|
|
||||||
pingOutstanding = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_client->available()) {
|
|
||||||
uint8_t llen;
|
|
||||||
uint16_t len = readPacket(&llen);
|
|
||||||
uint16_t msgId = 0;
|
|
||||||
uint8_t *payload;
|
|
||||||
if (len > 0) {
|
|
||||||
lastInActivity = t;
|
|
||||||
uint8_t type = buffer[0]&0xF0;
|
|
||||||
if (type == MQTTPUBLISH) {
|
|
||||||
if (callback) {
|
|
||||||
uint16_t tl = (buffer[llen+1]<<8)+buffer[llen+2]; /* topic length in bytes */
|
|
||||||
memmove(buffer+llen+2,buffer+llen+3,tl); /* move topic inside buffer 1 byte to front */
|
|
||||||
buffer[llen+2+tl] = 0; /* end the topic as a 'C' string with \x00 */
|
|
||||||
char *topic = (char*) buffer+llen+2;
|
|
||||||
// msgId only present for QOS>0
|
|
||||||
if ((buffer[0]&0x06) == MQTTQOS1) {
|
|
||||||
msgId = (buffer[llen+3+tl]<<8)+buffer[llen+3+tl+1];
|
|
||||||
payload = buffer+llen+3+tl+2;
|
|
||||||
callback(topic,payload,len-llen-3-tl-2);
|
|
||||||
|
|
||||||
buffer[0] = MQTTPUBACK;
|
|
||||||
buffer[1] = 2;
|
|
||||||
buffer[2] = (msgId >> 8);
|
|
||||||
buffer[3] = (msgId & 0xFF);
|
|
||||||
_client->write(buffer,4);
|
|
||||||
lastOutActivity = t;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
payload = buffer+llen+3+tl;
|
|
||||||
callback(topic,payload,len-llen-3-tl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (type == MQTTPINGREQ) {
|
|
||||||
buffer[0] = MQTTPINGRESP;
|
|
||||||
buffer[1] = 0;
|
|
||||||
_client->write(buffer,2);
|
|
||||||
} else if (type == MQTTPINGRESP) {
|
|
||||||
pingOutstanding = false;
|
|
||||||
}
|
|
||||||
} else if (!connected()) {
|
|
||||||
// readPacket has closed the connection
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::publish(const char* topic, const char* payload) {
|
|
||||||
return publish(topic,(const uint8_t*)payload,strlen(payload),false);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::publish(const char* topic, const char* payload, boolean retained) {
|
|
||||||
return publish(topic,(const uint8_t*)payload,strlen(payload),retained);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength) {
|
|
||||||
return publish(topic, payload, plength, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) {
|
|
||||||
if (connected()) {
|
|
||||||
if (MQTT_MAX_PACKET_SIZE < MQTT_MAX_HEADER_SIZE + 2+strlen(topic) + plength) {
|
|
||||||
// Too long
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Leave room in the buffer for header and variable length field
|
|
||||||
uint16_t length = MQTT_MAX_HEADER_SIZE;
|
|
||||||
length = writeString(topic,buffer,length);
|
|
||||||
uint16_t i;
|
|
||||||
for (i=0;i<plength;i++) {
|
|
||||||
buffer[length++] = payload[i];
|
|
||||||
}
|
|
||||||
uint8_t header = MQTTPUBLISH;
|
|
||||||
if (retained) {
|
|
||||||
header |= 1;
|
|
||||||
}
|
|
||||||
return write(header,buffer,length-MQTT_MAX_HEADER_SIZE);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::publish_P(const char* topic, const char* payload, boolean retained) {
|
|
||||||
return publish_P(topic, (const uint8_t*)payload, strlen(payload), retained);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::publish_P(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) {
|
|
||||||
uint8_t llen = 0;
|
|
||||||
uint8_t digit;
|
|
||||||
unsigned int rc = 0;
|
|
||||||
uint16_t tlen;
|
|
||||||
unsigned int pos = 0;
|
|
||||||
unsigned int i;
|
|
||||||
uint8_t header;
|
|
||||||
unsigned int len;
|
|
||||||
|
|
||||||
if (!connected()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
tlen = strlen(topic);
|
|
||||||
|
|
||||||
header = MQTTPUBLISH;
|
|
||||||
if (retained) {
|
|
||||||
header |= 1;
|
|
||||||
}
|
|
||||||
buffer[pos++] = header;
|
|
||||||
len = plength + 2 + tlen;
|
|
||||||
do {
|
|
||||||
digit = len % 128;
|
|
||||||
len = len / 128;
|
|
||||||
if (len > 0) {
|
|
||||||
digit |= 0x80;
|
|
||||||
}
|
|
||||||
buffer[pos++] = digit;
|
|
||||||
llen++;
|
|
||||||
} while(len>0);
|
|
||||||
|
|
||||||
pos = writeString(topic,buffer,pos);
|
|
||||||
|
|
||||||
rc += _client->write(buffer,pos);
|
|
||||||
|
|
||||||
for (i=0;i<plength;i++) {
|
|
||||||
rc += _client->write((char)pgm_read_byte_near(payload + i));
|
|
||||||
}
|
|
||||||
|
|
||||||
lastOutActivity = millis();
|
|
||||||
|
|
||||||
return rc == tlen + 4 + plength;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::beginPublish(const char* topic, unsigned int plength, boolean retained) {
|
|
||||||
if (connected()) {
|
|
||||||
// Send the header and variable length field
|
|
||||||
uint16_t length = MQTT_MAX_HEADER_SIZE;
|
|
||||||
length = writeString(topic,buffer,length);
|
|
||||||
uint8_t header = MQTTPUBLISH;
|
|
||||||
if (retained) {
|
|
||||||
header |= 1;
|
|
||||||
}
|
|
||||||
size_t hlen = buildHeader(header, buffer, plength+length-MQTT_MAX_HEADER_SIZE);
|
|
||||||
uint16_t rc = _client->write(buffer+(MQTT_MAX_HEADER_SIZE-hlen),length-(MQTT_MAX_HEADER_SIZE-hlen));
|
|
||||||
lastOutActivity = millis();
|
|
||||||
return (rc == (length-(MQTT_MAX_HEADER_SIZE-hlen)));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PubSubClient::endPublish() {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t PubSubClient::write(uint8_t data) {
|
|
||||||
lastOutActivity = millis();
|
|
||||||
return _client->write(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t PubSubClient::write(const uint8_t *buffer, size_t size) {
|
|
||||||
lastOutActivity = millis();
|
|
||||||
return _client->write(buffer,size);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t PubSubClient::buildHeader(uint8_t header, uint8_t* buf, uint16_t length) {
|
|
||||||
uint8_t lenBuf[4];
|
|
||||||
uint8_t llen = 0;
|
|
||||||
uint8_t digit;
|
|
||||||
uint8_t pos = 0;
|
|
||||||
uint16_t len = length;
|
|
||||||
do {
|
|
||||||
digit = len % 128;
|
|
||||||
len = len / 128;
|
|
||||||
if (len > 0) {
|
|
||||||
digit |= 0x80;
|
|
||||||
}
|
|
||||||
lenBuf[pos++] = digit;
|
|
||||||
llen++;
|
|
||||||
} while(len>0);
|
|
||||||
|
|
||||||
buf[4-llen] = header;
|
|
||||||
for (int i=0;i<llen;i++) {
|
|
||||||
buf[MQTT_MAX_HEADER_SIZE-llen+i] = lenBuf[i];
|
|
||||||
}
|
|
||||||
return llen+1; // Full header size is variable length bit plus the 1-byte fixed header
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::write(uint8_t header, uint8_t* buf, uint16_t length) {
|
|
||||||
uint16_t rc;
|
|
||||||
uint8_t hlen = buildHeader(header, buf, length);
|
|
||||||
|
|
||||||
#ifdef MQTT_MAX_TRANSFER_SIZE
|
|
||||||
uint8_t* writeBuf = buf+(MQTT_MAX_HEADER_SIZE-hlen);
|
|
||||||
uint16_t bytesRemaining = length+hlen; //Match the length type
|
|
||||||
uint8_t bytesToWrite;
|
|
||||||
boolean result = true;
|
|
||||||
while((bytesRemaining > 0) && result) {
|
|
||||||
bytesToWrite = (bytesRemaining > MQTT_MAX_TRANSFER_SIZE)?MQTT_MAX_TRANSFER_SIZE:bytesRemaining;
|
|
||||||
rc = _client->write(writeBuf,bytesToWrite);
|
|
||||||
result = (rc == bytesToWrite);
|
|
||||||
bytesRemaining -= rc;
|
|
||||||
writeBuf += rc;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
#else
|
|
||||||
rc = _client->write(buf+(MQTT_MAX_HEADER_SIZE-hlen),length+hlen);
|
|
||||||
lastOutActivity = millis();
|
|
||||||
return (rc == hlen+length);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::subscribe(const char* topic) {
|
|
||||||
return subscribe(topic, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::subscribe(const char* topic, uint8_t qos) {
|
|
||||||
if (qos > 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (MQTT_MAX_PACKET_SIZE < 9 + strlen(topic)) {
|
|
||||||
// Too long
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (connected()) {
|
|
||||||
// Leave room in the buffer for header and variable length field
|
|
||||||
uint16_t length = MQTT_MAX_HEADER_SIZE;
|
|
||||||
nextMsgId++;
|
|
||||||
if (nextMsgId == 0) {
|
|
||||||
nextMsgId = 1;
|
|
||||||
}
|
|
||||||
buffer[length++] = (nextMsgId >> 8);
|
|
||||||
buffer[length++] = (nextMsgId & 0xFF);
|
|
||||||
length = writeString((char*)topic, buffer,length);
|
|
||||||
buffer[length++] = qos;
|
|
||||||
return write(MQTTSUBSCRIBE|MQTTQOS1,buffer,length-MQTT_MAX_HEADER_SIZE);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean PubSubClient::unsubscribe(const char* topic) {
|
|
||||||
if (MQTT_MAX_PACKET_SIZE < 9 + strlen(topic)) {
|
|
||||||
// Too long
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (connected()) {
|
|
||||||
uint16_t length = MQTT_MAX_HEADER_SIZE;
|
|
||||||
nextMsgId++;
|
|
||||||
if (nextMsgId == 0) {
|
|
||||||
nextMsgId = 1;
|
|
||||||
}
|
|
||||||
buffer[length++] = (nextMsgId >> 8);
|
|
||||||
buffer[length++] = (nextMsgId & 0xFF);
|
|
||||||
length = writeString(topic, buffer,length);
|
|
||||||
return write(MQTTUNSUBSCRIBE|MQTTQOS1,buffer,length-MQTT_MAX_HEADER_SIZE);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PubSubClient::disconnect() {
|
|
||||||
buffer[0] = MQTTDISCONNECT;
|
|
||||||
buffer[1] = 0;
|
|
||||||
_client->write(buffer,2);
|
|
||||||
_state = MQTT_DISCONNECTED;
|
|
||||||
_client->flush();
|
|
||||||
_client->stop();
|
|
||||||
lastInActivity = lastOutActivity = millis();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t PubSubClient::writeString(const char* string, uint8_t* buf, uint16_t pos) {
|
|
||||||
const char* idp = string;
|
|
||||||
uint16_t i = 0;
|
|
||||||
pos += 2;
|
|
||||||
while (*idp) {
|
|
||||||
buf[pos++] = *idp++;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
buf[pos-i-2] = (i >> 8);
|
|
||||||
buf[pos-i-1] = (i & 0xFF);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
boolean PubSubClient::connected() {
|
|
||||||
boolean rc;
|
|
||||||
if (_client == NULL ) {
|
|
||||||
rc = false;
|
|
||||||
} else {
|
|
||||||
rc = (int)_client->connected();
|
|
||||||
if (!rc) {
|
|
||||||
if (this->_state == MQTT_CONNECTED) {
|
|
||||||
this->_state = MQTT_CONNECTION_LOST;
|
|
||||||
_client->flush();
|
|
||||||
_client->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient& PubSubClient::setServer(uint8_t * ip, uint16_t port) {
|
|
||||||
IPAddress addr(ip[0],ip[1],ip[2],ip[3]);
|
|
||||||
return setServer(addr,port);
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient& PubSubClient::setServer(IPAddress ip, uint16_t port) {
|
|
||||||
this->ip = ip;
|
|
||||||
this->port = port;
|
|
||||||
this->domain = NULL;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient& PubSubClient::setServer(const char * domain, uint16_t port) {
|
|
||||||
this->domain = domain;
|
|
||||||
this->port = port;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient& PubSubClient::setCallback(MQTT_CALLBACK_SIGNATURE) {
|
|
||||||
this->callback = callback;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient& PubSubClient::setClient(Client& client){
|
|
||||||
this->_client = &client;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
PubSubClient& PubSubClient::setStream(Stream& stream){
|
|
||||||
this->stream = &stream;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PubSubClient::state() {
|
|
||||||
return this->_state;
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
/*
|
|
||||||
PubSubClient.h - A simple client for MQTT.
|
|
||||||
Nick O'Leary
|
|
||||||
http://knolleary.net
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef PubSubClient_h
|
|
||||||
#define PubSubClient_h
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include "IPAddress.h"
|
|
||||||
#include "Client.h"
|
|
||||||
#include "Stream.h"
|
|
||||||
|
|
||||||
#define MQTT_VERSION_3_1 3
|
|
||||||
#define MQTT_VERSION_3_1_1 4
|
|
||||||
|
|
||||||
// MQTT_VERSION : Pick the version
|
|
||||||
//#define MQTT_VERSION MQTT_VERSION_3_1
|
|
||||||
#ifndef MQTT_VERSION
|
|
||||||
#define MQTT_VERSION MQTT_VERSION_3_1_1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// MQTT_MAX_PACKET_SIZE : Maximum packet size
|
|
||||||
#ifndef MQTT_MAX_PACKET_SIZE
|
|
||||||
#define MQTT_MAX_PACKET_SIZE 512
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// MQTT_KEEPALIVE : keepAlive interval in Seconds
|
|
||||||
#ifndef MQTT_KEEPALIVE
|
|
||||||
#define MQTT_KEEPALIVE 15
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// MQTT_SOCKET_TIMEOUT: socket timeout interval in Seconds
|
|
||||||
#ifndef MQTT_SOCKET_TIMEOUT
|
|
||||||
#define MQTT_SOCKET_TIMEOUT 15
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// MQTT_MAX_TRANSFER_SIZE : limit how much data is passed to the network client
|
|
||||||
// in each write call. Needed for the Arduino Wifi Shield. Leave undefined to
|
|
||||||
// pass the entire MQTT packet in each write call.
|
|
||||||
//#define MQTT_MAX_TRANSFER_SIZE 80
|
|
||||||
|
|
||||||
// Possible values for client.state()
|
|
||||||
#define MQTT_CONNECTION_TIMEOUT -4
|
|
||||||
#define MQTT_CONNECTION_LOST -3
|
|
||||||
#define MQTT_CONNECT_FAILED -2
|
|
||||||
#define MQTT_DISCONNECTED -1
|
|
||||||
#define MQTT_CONNECTED 0
|
|
||||||
#define MQTT_CONNECT_BAD_PROTOCOL 1
|
|
||||||
#define MQTT_CONNECT_BAD_CLIENT_ID 2
|
|
||||||
#define MQTT_CONNECT_UNAVAILABLE 3
|
|
||||||
#define MQTT_CONNECT_BAD_CREDENTIALS 4
|
|
||||||
#define MQTT_CONNECT_UNAUTHORIZED 5
|
|
||||||
|
|
||||||
#define MQTTCONNECT 1 << 4 // Client request to connect to Server
|
|
||||||
#define MQTTCONNACK 2 << 4 // Connect Acknowledgment
|
|
||||||
#define MQTTPUBLISH 3 << 4 // Publish message
|
|
||||||
#define MQTTPUBACK 4 << 4 // Publish Acknowledgment
|
|
||||||
#define MQTTPUBREC 5 << 4 // Publish Received (assured delivery part 1)
|
|
||||||
#define MQTTPUBREL 6 << 4 // Publish Release (assured delivery part 2)
|
|
||||||
#define MQTTPUBCOMP 7 << 4 // Publish Complete (assured delivery part 3)
|
|
||||||
#define MQTTSUBSCRIBE 8 << 4 // Client Subscribe request
|
|
||||||
#define MQTTSUBACK 9 << 4 // Subscribe Acknowledgment
|
|
||||||
#define MQTTUNSUBSCRIBE 10 << 4 // Client Unsubscribe request
|
|
||||||
#define MQTTUNSUBACK 11 << 4 // Unsubscribe Acknowledgment
|
|
||||||
#define MQTTPINGREQ 12 << 4 // PING Request
|
|
||||||
#define MQTTPINGRESP 13 << 4 // PING Response
|
|
||||||
#define MQTTDISCONNECT 14 << 4 // Client is Disconnecting
|
|
||||||
#define MQTTReserved 15 << 4 // Reserved
|
|
||||||
|
|
||||||
#define MQTTQOS0 (0 << 1)
|
|
||||||
#define MQTTQOS1 (1 << 1)
|
|
||||||
#define MQTTQOS2 (2 << 1)
|
|
||||||
|
|
||||||
// Maximum size of fixed header and variable length size header
|
|
||||||
#define MQTT_MAX_HEADER_SIZE 5
|
|
||||||
|
|
||||||
#if defined(ESP8266) || defined(ESP32)
|
|
||||||
#include <functional>
|
|
||||||
#define MQTT_CALLBACK_SIGNATURE std::function<void(char*, uint8_t*, unsigned int)> callback
|
|
||||||
#else
|
|
||||||
#define MQTT_CALLBACK_SIGNATURE void (*callback)(char*, uint8_t*, unsigned int)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define CHECK_STRING_LENGTH(l,s) if (l+2+strlen(s) > MQTT_MAX_PACKET_SIZE) {_client->stop();return false;}
|
|
||||||
|
|
||||||
class PubSubClient : public Print {
|
|
||||||
private:
|
|
||||||
Client* _client;
|
|
||||||
uint8_t buffer[MQTT_MAX_PACKET_SIZE];
|
|
||||||
uint16_t nextMsgId;
|
|
||||||
unsigned long lastOutActivity;
|
|
||||||
unsigned long lastInActivity;
|
|
||||||
bool pingOutstanding;
|
|
||||||
MQTT_CALLBACK_SIGNATURE;
|
|
||||||
uint16_t readPacket(uint8_t*);
|
|
||||||
boolean readByte(uint8_t * result);
|
|
||||||
boolean readByte(uint8_t * result, uint16_t * index);
|
|
||||||
boolean write(uint8_t header, uint8_t* buf, uint16_t length);
|
|
||||||
uint16_t writeString(const char* string, uint8_t* buf, uint16_t pos);
|
|
||||||
// Build up the header ready to send
|
|
||||||
// Returns the size of the header
|
|
||||||
// Note: the header is built at the end of the first MQTT_MAX_HEADER_SIZE bytes, so will start
|
|
||||||
// (MQTT_MAX_HEADER_SIZE - <returned size>) bytes into the buffer
|
|
||||||
size_t buildHeader(uint8_t header, uint8_t* buf, uint16_t length);
|
|
||||||
IPAddress ip;
|
|
||||||
const char* domain;
|
|
||||||
uint16_t port;
|
|
||||||
Stream* stream;
|
|
||||||
int _state;
|
|
||||||
public:
|
|
||||||
PubSubClient();
|
|
||||||
PubSubClient(Client& client);
|
|
||||||
PubSubClient(IPAddress, uint16_t, Client& client);
|
|
||||||
PubSubClient(IPAddress, uint16_t, Client& client, Stream&);
|
|
||||||
PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
|
|
||||||
PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);
|
|
||||||
PubSubClient(uint8_t *, uint16_t, Client& client);
|
|
||||||
PubSubClient(uint8_t *, uint16_t, Client& client, Stream&);
|
|
||||||
PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
|
|
||||||
PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);
|
|
||||||
PubSubClient(const char*, uint16_t, Client& client);
|
|
||||||
PubSubClient(const char*, uint16_t, Client& client, Stream&);
|
|
||||||
PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
|
|
||||||
PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);
|
|
||||||
|
|
||||||
PubSubClient& setServer(IPAddress ip, uint16_t port);
|
|
||||||
PubSubClient& setServer(uint8_t * ip, uint16_t port);
|
|
||||||
PubSubClient& setServer(const char * domain, uint16_t port);
|
|
||||||
PubSubClient& setCallback(MQTT_CALLBACK_SIGNATURE);
|
|
||||||
PubSubClient& setClient(Client& client);
|
|
||||||
PubSubClient& setStream(Stream& stream);
|
|
||||||
|
|
||||||
boolean connect(const char* id);
|
|
||||||
boolean connect(const char* id, const char* user, const char* pass);
|
|
||||||
boolean connect(const char* id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage);
|
|
||||||
boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage);
|
|
||||||
boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage, boolean cleanSession);
|
|
||||||
void disconnect();
|
|
||||||
boolean publish(const char* topic, const char* payload);
|
|
||||||
boolean publish(const char* topic, const char* payload, boolean retained);
|
|
||||||
boolean publish(const char* topic, const uint8_t * payload, unsigned int plength);
|
|
||||||
boolean publish(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);
|
|
||||||
boolean publish_P(const char* topic, const char* payload, boolean retained);
|
|
||||||
boolean publish_P(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);
|
|
||||||
// Start to publish a message.
|
|
||||||
// This API:
|
|
||||||
// beginPublish(...)
|
|
||||||
// one or more calls to write(...)
|
|
||||||
// endPublish()
|
|
||||||
// Allows for arbitrarily large payloads to be sent without them having to be copied into
|
|
||||||
// a new buffer and held in memory at one time
|
|
||||||
// Returns 1 if the message was started successfully, 0 if there was an error
|
|
||||||
boolean beginPublish(const char* topic, unsigned int plength, boolean retained);
|
|
||||||
// Finish off this publish message (started with beginPublish)
|
|
||||||
// Returns 1 if the packet was sent successfully, 0 if there was an error
|
|
||||||
int endPublish();
|
|
||||||
// Write a single byte of payload (only to be used with beginPublish/endPublish)
|
|
||||||
virtual size_t write(uint8_t);
|
|
||||||
// Write size bytes from buffer into the payload (only to be used with beginPublish/endPublish)
|
|
||||||
// Returns the number of bytes written
|
|
||||||
virtual size_t write(const uint8_t *buffer, size_t size);
|
|
||||||
boolean subscribe(const char* topic);
|
|
||||||
boolean subscribe(const char* topic, uint8_t qos);
|
|
||||||
boolean unsubscribe(const char* topic);
|
|
||||||
boolean loop();
|
|
||||||
boolean connected();
|
|
||||||
int state();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
.build
|
|
||||||
tmpbin
|
|
||||||
logs
|
|
||||||
*.pyc
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
SRC_PATH=./src
|
|
||||||
OUT_PATH=./bin
|
|
||||||
TEST_SRC=$(wildcard ${SRC_PATH}/*_spec.cpp)
|
|
||||||
TEST_BIN= $(TEST_SRC:${SRC_PATH}/%.cpp=${OUT_PATH}/%)
|
|
||||||
VPATH=${SRC_PATH}
|
|
||||||
SHIM_FILES=${SRC_PATH}/lib/*.cpp
|
|
||||||
PSC_FILE=../src/PubSubClient.cpp
|
|
||||||
CC=g++
|
|
||||||
CFLAGS=-I${SRC_PATH}/lib -I../src
|
|
||||||
|
|
||||||
all: $(TEST_BIN)
|
|
||||||
|
|
||||||
${OUT_PATH}/%: ${SRC_PATH}/%.cpp ${PSC_FILE} ${SHIM_FILES}
|
|
||||||
mkdir -p ${OUT_PATH}
|
|
||||||
${CC} ${CFLAGS} $^ -o $@
|
|
||||||
|
|
||||||
clean:
|
|
||||||
@rm -rf ${OUT_PATH}
|
|
||||||
|
|
||||||
test:
|
|
||||||
@bin/connect_spec
|
|
||||||
@bin/publish_spec
|
|
||||||
@bin/receive_spec
|
|
||||||
@bin/subscribe_spec
|
|
||||||
@bin/keepalive_spec
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# Arduino Client for MQTT Test Suite
|
|
||||||
|
|
||||||
This is a regression test suite for the `PubSubClient` library.
|
|
||||||
|
|
||||||
There are two parts:
|
|
||||||
|
|
||||||
- Tests that can be compiled and run on any machine
|
|
||||||
- Tests that build the example sketches using the Arduino IDE
|
|
||||||
|
|
||||||
|
|
||||||
It is a work-in-progress and is subject to complete refactoring as the whim takes
|
|
||||||
me.
|
|
||||||
|
|
||||||
|
|
||||||
## Local tests
|
|
||||||
|
|
||||||
These are a set of executables that can be run to test specific areas of functionality.
|
|
||||||
They do not require a real Arduino to be attached, nor the use of the Arduino IDE.
|
|
||||||
|
|
||||||
The tests include a set of mock files to stub out the parts of the Arduino environment the library
|
|
||||||
depends on.
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
- g++
|
|
||||||
|
|
||||||
### Running
|
|
||||||
|
|
||||||
Build the tests using the provided `Makefile`:
|
|
||||||
|
|
||||||
$ make
|
|
||||||
|
|
||||||
This will create a set of executables in `./bin/`. Run each of these executables to test the corresponding functionality.
|
|
||||||
|
|
||||||
*Note:* the `connect_spec` and `keepalive_spec` tests involve testing keepalive timers so naturally take a few minutes to run through.
|
|
||||||
|
|
||||||
## Arduino tests
|
|
||||||
|
|
||||||
*Note:* INO Tool doesn't currently play nicely with Arduino 1.5. This has broken this test suite.
|
|
||||||
|
|
||||||
Without a suitable arduino plugged in, the test suite will only check the
|
|
||||||
example sketches compile cleanly against the library.
|
|
||||||
|
|
||||||
With an arduino plugged in, each sketch that has a corresponding python
|
|
||||||
test case is built, uploaded and then the tests run.
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
- Python 2.7+
|
|
||||||
- [INO Tool](http://inotool.org/) - this provides command-line build/upload of Arduino sketches
|
|
||||||
|
|
||||||
### Running
|
|
||||||
|
|
||||||
The test suite _does not_ run an MQTT server - it is assumed to be running already.
|
|
||||||
|
|
||||||
$ python testsuite.py
|
|
||||||
|
|
||||||
A summary of activity is printed to the console. More comprehensive logs are written
|
|
||||||
to the `logs` directory.
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
|
|
||||||
For each sketch in the library's `examples` directory, e.g. `mqtt_basic.ino`, the suite looks for a matching test case
|
|
||||||
`testcases/mqtt_basic.py`.
|
|
||||||
|
|
||||||
The test case must follow these conventions:
|
|
||||||
- sub-class `unittest.TestCase`
|
|
||||||
- provide the class methods `setUpClass` and `tearDownClass` (TODO: make this optional)
|
|
||||||
- all test method names begin with `test_`
|
|
||||||
|
|
||||||
The suite will call the `setUpClass` method _before_ uploading the sketch. This
|
|
||||||
allows any test setup to be performed before the sketch runs - such as connecting
|
|
||||||
a client and subscribing to topics.
|
|
||||||
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
The file `testcases/settings.py` is used to config the test environment.
|
|
||||||
|
|
||||||
- `server_ip` - the IP address of the broker the client should connect to (the broker port is assumed to be 1883).
|
|
||||||
- `arduino_ip` - the IP address the arduino should use (when not testing DHCP).
|
|
||||||
|
|
||||||
Before each sketch is compiled, these values are automatically substituted in. To
|
|
||||||
do this, the suite looks for lines that _start_ with the following:
|
|
||||||
|
|
||||||
byte server[] = {
|
|
||||||
byte ip[] = {
|
|
||||||
|
|
||||||
and replaces them with the appropriate values.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
#include "PubSubClient.h"
|
|
||||||
#include "ShimClient.h"
|
|
||||||
#include "Buffer.h"
|
|
||||||
#include "BDDTest.h"
|
|
||||||
#include "trace.h"
|
|
||||||
|
|
||||||
|
|
||||||
byte server[] = { 172, 16, 0, 2 };
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
// handle message arrived
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int test_connect_fails_no_network() {
|
|
||||||
IT("fails to connect if underlying client doesn't connect");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(false);
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_FALSE(rc);
|
|
||||||
int state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_CONNECT_FAILED);
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_fails_on_no_response() {
|
|
||||||
IT("fails to connect if no response received after 15 seconds");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_FALSE(rc);
|
|
||||||
int state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_CONNECTION_TIMEOUT);
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_properly_formatted() {
|
|
||||||
IT("sends a properly formatted connect packet and succeeds");
|
|
||||||
ShimClient shimClient;
|
|
||||||
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
byte expectServer[] = { 172, 16, 0, 2 };
|
|
||||||
shimClient.expectConnect(expectServer,1883);
|
|
||||||
byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
|
|
||||||
shimClient.expect(connect,26);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_DISCONNECTED);
|
|
||||||
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_CONNECTED);
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_properly_formatted_hostname() {
|
|
||||||
IT("accepts a hostname");
|
|
||||||
ShimClient shimClient;
|
|
||||||
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
shimClient.expectConnect((char* const)"localhost",1883);
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client((char* const)"localhost", 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int test_connect_fails_on_bad_rc() {
|
|
||||||
IT("fails to connect if a bad return code is received");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x01 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
int state = client.state();
|
|
||||||
IS_TRUE(state == 0x01);
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_non_clean_session() {
|
|
||||||
IT("sends a properly formatted non-clean session connect packet and succeeds");
|
|
||||||
ShimClient shimClient;
|
|
||||||
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
byte expectServer[] = { 172, 16, 0, 2 };
|
|
||||||
shimClient.expectConnect(expectServer,1883);
|
|
||||||
byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x0,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
|
|
||||||
shimClient.expect(connect,26);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_DISCONNECTED);
|
|
||||||
|
|
||||||
int rc = client.connect((char*)"client_test1",0,0,0,0,0,0,0);
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_CONNECTED);
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_accepts_username_password() {
|
|
||||||
IT("accepts a username and password");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connect[] = { 0x10,0x24,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0xc2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x4,0x75,0x73,0x65,0x72,0x0,0x4,0x70,0x61,0x73,0x73};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.expect(connect,0x26);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1",(char*)"user",(char*)"pass");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_accepts_username_no_password() {
|
|
||||||
IT("accepts a username but no password");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connect[] = { 0x10,0x1e,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x82,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x4,0x75,0x73,0x65,0x72};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.expect(connect,0x20);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1",(char*)"user",0);
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
int test_connect_accepts_username_blank_password() {
|
|
||||||
IT("accepts a username and blank password");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connect[] = { 0x10,0x20,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0xc2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x4,0x75,0x73,0x65,0x72,0x0,0x0};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.expect(connect,0x26);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1",(char*)"user",(char*)"pass");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_ignores_password_no_username() {
|
|
||||||
IT("ignores a password but no username");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.expect(connect,26);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1",0,(char*)"pass");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_with_will() {
|
|
||||||
IT("accepts a will");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connect[] = {0x10,0x30,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0xe,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x9,0x77,0x69,0x6c,0x6c,0x54,0x6f,0x70,0x69,0x63,0x0,0xb,0x77,0x69,0x6c,0x6c,0x4d,0x65,0x73,0x73,0x61,0x67,0x65};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.expect(connect,0x32);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1",(char*)"willTopic",1,0,(char*)"willMessage");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_with_will_username_password() {
|
|
||||||
IT("accepts a will, username and password");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connect[] = {0x10,0x40,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0xce,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x9,0x77,0x69,0x6c,0x6c,0x54,0x6f,0x70,0x69,0x63,0x0,0xb,0x77,0x69,0x6c,0x6c,0x4d,0x65,0x73,0x73,0x61,0x67,0x65,0x0,0x4,0x75,0x73,0x65,0x72,0x0,0x8,0x70,0x61,0x73,0x73,0x77,0x6f,0x72,0x64};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.expect(connect,0x42);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1",(char*)"user",(char*)"password",(char*)"willTopic",1,0,(char*)"willMessage");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_connect_disconnect_connect() {
|
|
||||||
IT("connects, disconnects and connects again");
|
|
||||||
ShimClient shimClient;
|
|
||||||
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
byte expectServer[] = { 172, 16, 0, 2 };
|
|
||||||
shimClient.expectConnect(expectServer,1883);
|
|
||||||
byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31};
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
|
|
||||||
shimClient.expect(connect,26);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
|
|
||||||
int state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_DISCONNECTED);
|
|
||||||
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_CONNECTED);
|
|
||||||
|
|
||||||
byte disconnect[] = {0xE0,0x00};
|
|
||||||
shimClient.expect(disconnect,2);
|
|
||||||
|
|
||||||
client.disconnect();
|
|
||||||
|
|
||||||
IS_FALSE(client.connected());
|
|
||||||
IS_FALSE(shimClient.connected());
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_DISCONNECTED);
|
|
||||||
|
|
||||||
shimClient.expect(connect,28);
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_CONNECTED);
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
SUITE("Connect");
|
|
||||||
|
|
||||||
test_connect_fails_no_network();
|
|
||||||
test_connect_fails_on_no_response();
|
|
||||||
|
|
||||||
test_connect_properly_formatted();
|
|
||||||
test_connect_non_clean_session();
|
|
||||||
test_connect_accepts_username_password();
|
|
||||||
test_connect_fails_on_bad_rc();
|
|
||||||
test_connect_properly_formatted_hostname();
|
|
||||||
|
|
||||||
test_connect_accepts_username_no_password();
|
|
||||||
test_connect_ignores_password_no_username();
|
|
||||||
test_connect_with_will();
|
|
||||||
test_connect_with_will_username_password();
|
|
||||||
test_connect_disconnect_connect();
|
|
||||||
FINISH
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
#include "PubSubClient.h"
|
|
||||||
#include "ShimClient.h"
|
|
||||||
#include "Buffer.h"
|
|
||||||
#include "BDDTest.h"
|
|
||||||
#include "trace.h"
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
byte server[] = { 172, 16, 0, 2 };
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
// handle message arrived
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int test_keepalive_pings_idle() {
|
|
||||||
IT("keeps an idle connection alive (takes 1 minute)");
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte pingreq[] = { 0xC0,0x0 };
|
|
||||||
shimClient.expect(pingreq,2);
|
|
||||||
byte pingresp[] = { 0xD0,0x0 };
|
|
||||||
shimClient.respond(pingresp,2);
|
|
||||||
|
|
||||||
for (int i = 0; i < 50; i++) {
|
|
||||||
sleep(1);
|
|
||||||
if ( i == 15 || i == 31 || i == 47) {
|
|
||||||
shimClient.expect(pingreq,2);
|
|
||||||
shimClient.respond(pingresp,2);
|
|
||||||
}
|
|
||||||
rc = client.loop();
|
|
||||||
IS_TRUE(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_keepalive_pings_with_outbound_qos0() {
|
|
||||||
IT("keeps a connection alive that only sends qos0 (takes 1 minute)");
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
|
|
||||||
for (int i = 0; i < 50; i++) {
|
|
||||||
TRACE(i<<":");
|
|
||||||
shimClient.expect(publish,16);
|
|
||||||
rc = client.publish((char*)"topic",(char*)"payload");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
sleep(1);
|
|
||||||
if ( i == 15 || i == 31 || i == 47) {
|
|
||||||
byte pingreq[] = { 0xC0,0x0 };
|
|
||||||
shimClient.expect(pingreq,2);
|
|
||||||
byte pingresp[] = { 0xD0,0x0 };
|
|
||||||
shimClient.respond(pingresp,2);
|
|
||||||
}
|
|
||||||
rc = client.loop();
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
}
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_keepalive_pings_with_inbound_qos0() {
|
|
||||||
IT("keeps a connection alive that only receives qos0 (takes 1 minute)");
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
|
|
||||||
for (int i = 0; i < 50; i++) {
|
|
||||||
TRACE(i<<":");
|
|
||||||
sleep(1);
|
|
||||||
if ( i == 15 || i == 31 || i == 47) {
|
|
||||||
byte pingreq[] = { 0xC0,0x0 };
|
|
||||||
shimClient.expect(pingreq,2);
|
|
||||||
byte pingresp[] = { 0xD0,0x0 };
|
|
||||||
shimClient.respond(pingresp,2);
|
|
||||||
}
|
|
||||||
shimClient.respond(publish,16);
|
|
||||||
rc = client.loop();
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
}
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_keepalive_no_pings_inbound_qos1() {
|
|
||||||
IT("does not send pings for connections with inbound qos1 (takes 1 minute)");
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x32,0x10,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x12,0x34,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
byte puback[] = {0x40,0x2,0x12,0x34};
|
|
||||||
|
|
||||||
for (int i = 0; i < 50; i++) {
|
|
||||||
shimClient.respond(publish,18);
|
|
||||||
shimClient.expect(puback,4);
|
|
||||||
sleep(1);
|
|
||||||
rc = client.loop();
|
|
||||||
IS_TRUE(rc);
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
}
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_keepalive_disconnects_hung() {
|
|
||||||
IT("disconnects a hung connection (takes 30 seconds)");
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte pingreq[] = { 0xC0,0x0 };
|
|
||||||
shimClient.expect(pingreq,2);
|
|
||||||
|
|
||||||
for (int i = 0; i < 32; i++) {
|
|
||||||
sleep(1);
|
|
||||||
rc = client.loop();
|
|
||||||
}
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
int state = client.state();
|
|
||||||
IS_TRUE(state == MQTT_CONNECTION_TIMEOUT);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
SUITE("Keep-alive");
|
|
||||||
test_keepalive_pings_idle();
|
|
||||||
test_keepalive_pings_with_outbound_qos0();
|
|
||||||
test_keepalive_pings_with_inbound_qos0();
|
|
||||||
test_keepalive_no_pings_inbound_qos1();
|
|
||||||
test_keepalive_disconnects_hung();
|
|
||||||
|
|
||||||
FINISH
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#ifndef Arduino_h
|
|
||||||
#define Arduino_h
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <math.h>
|
|
||||||
#include "Print.h"
|
|
||||||
|
|
||||||
|
|
||||||
extern "C"{
|
|
||||||
typedef uint8_t byte ;
|
|
||||||
typedef uint8_t boolean ;
|
|
||||||
|
|
||||||
/* sketch */
|
|
||||||
extern void setup( void ) ;
|
|
||||||
extern void loop( void ) ;
|
|
||||||
uint32_t millis( void );
|
|
||||||
}
|
|
||||||
|
|
||||||
#define PROGMEM
|
|
||||||
#define pgm_read_byte_near(x) *(x)
|
|
||||||
|
|
||||||
#define yield(x) {}
|
|
||||||
|
|
||||||
#endif // Arduino_h
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#include "BDDTest.h"
|
|
||||||
#include "trace.h"
|
|
||||||
#include <sstream>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
#include <list>
|
|
||||||
|
|
||||||
int testCount = 0;
|
|
||||||
int testPasses = 0;
|
|
||||||
const char* testDescription;
|
|
||||||
|
|
||||||
std::list<std::string> failureList;
|
|
||||||
|
|
||||||
void bddtest_suite(const char* name) {
|
|
||||||
LOG(name << "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
int bddtest_test(const char* file, int line, const char* assertion, int result) {
|
|
||||||
if (!result) {
|
|
||||||
LOG("✗\n");
|
|
||||||
std::ostringstream os;
|
|
||||||
os << " ! "<<testDescription<<"\n " <<file << ":" <<line<<" : "<<assertion<<" ["<<result<<"]";
|
|
||||||
failureList.push_back(os.str());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bddtest_start(const char* description) {
|
|
||||||
LOG(" - "<<description<<" ");
|
|
||||||
testDescription = description;
|
|
||||||
testCount ++;
|
|
||||||
}
|
|
||||||
void bddtest_end() {
|
|
||||||
LOG("✓\n");
|
|
||||||
testPasses ++;
|
|
||||||
}
|
|
||||||
|
|
||||||
int bddtest_summary() {
|
|
||||||
for (std::list<std::string>::iterator it = failureList.begin(); it != failureList.end(); it++) {
|
|
||||||
LOG("\n");
|
|
||||||
LOG(*it);
|
|
||||||
LOG("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG(std::dec << testPasses << "/" << testCount << " tests passed\n\n");
|
|
||||||
if (testPasses == testCount) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#ifndef bddtest_h
|
|
||||||
#define bddtest_h
|
|
||||||
|
|
||||||
void bddtest_suite(const char* name);
|
|
||||||
int bddtest_test(const char*, int, const char*, int);
|
|
||||||
void bddtest_start(const char*);
|
|
||||||
void bddtest_end();
|
|
||||||
int bddtest_summary();
|
|
||||||
|
|
||||||
#define SUITE(x) { bddtest_suite(x); }
|
|
||||||
#define TEST(x) { if (!bddtest_test(__FILE__, __LINE__, #x, (x))) return false; }
|
|
||||||
|
|
||||||
#define IT(x) { bddtest_start(x); }
|
|
||||||
#define END_IT { bddtest_end();return true;}
|
|
||||||
|
|
||||||
#define FINISH { return bddtest_summary(); }
|
|
||||||
|
|
||||||
#define IS_TRUE(x) TEST(x)
|
|
||||||
#define IS_FALSE(x) TEST(!(x))
|
|
||||||
#define IS_EQUAL(x,y) TEST(x==y)
|
|
||||||
#define IS_NOT_EQUAL(x,y) TEST(x!=y)
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#include "Buffer.h"
|
|
||||||
#include "Arduino.h"
|
|
||||||
|
|
||||||
Buffer::Buffer() {
|
|
||||||
this->pos = 0;
|
|
||||||
this->length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Buffer::Buffer(uint8_t* buf, size_t size) {
|
|
||||||
this->pos = 0;
|
|
||||||
this->length = 0;
|
|
||||||
this->add(buf,size);
|
|
||||||
}
|
|
||||||
bool Buffer::available() {
|
|
||||||
return this->pos < this->length;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t Buffer::next() {
|
|
||||||
if (this->available()) {
|
|
||||||
return this->buffer[this->pos++];
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Buffer::reset() {
|
|
||||||
this->pos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Buffer::add(uint8_t* buf, size_t size) {
|
|
||||||
uint16_t i = 0;
|
|
||||||
for (;i<size;i++) {
|
|
||||||
this->buffer[this->length++] = buf[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#ifndef buffer_h
|
|
||||||
#define buffer_h
|
|
||||||
|
|
||||||
#include "Arduino.h"
|
|
||||||
|
|
||||||
class Buffer {
|
|
||||||
private:
|
|
||||||
uint8_t buffer[1024];
|
|
||||||
uint16_t pos;
|
|
||||||
uint16_t length;
|
|
||||||
|
|
||||||
public:
|
|
||||||
Buffer();
|
|
||||||
Buffer(uint8_t* buf, size_t size);
|
|
||||||
|
|
||||||
virtual bool available();
|
|
||||||
virtual uint8_t next();
|
|
||||||
virtual void reset();
|
|
||||||
|
|
||||||
virtual void add(uint8_t* buf, size_t size);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#ifndef client_h
|
|
||||||
#define client_h
|
|
||||||
#include "IPAddress.h"
|
|
||||||
|
|
||||||
class Client {
|
|
||||||
public:
|
|
||||||
virtual int connect(IPAddress ip, uint16_t port) =0;
|
|
||||||
virtual int connect(const char *host, uint16_t port) =0;
|
|
||||||
virtual size_t write(uint8_t) =0;
|
|
||||||
virtual size_t write(const uint8_t *buf, size_t size) =0;
|
|
||||||
virtual int available() = 0;
|
|
||||||
virtual int read() = 0;
|
|
||||||
virtual int read(uint8_t *buf, size_t size) = 0;
|
|
||||||
virtual int peek() = 0;
|
|
||||||
virtual void flush() = 0;
|
|
||||||
virtual void stop() = 0;
|
|
||||||
virtual uint8_t connected() = 0;
|
|
||||||
virtual operator bool() = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <IPAddress.h>
|
|
||||||
|
|
||||||
IPAddress::IPAddress()
|
|
||||||
{
|
|
||||||
memset(_address, 0, sizeof(_address));
|
|
||||||
}
|
|
||||||
|
|
||||||
IPAddress::IPAddress(uint8_t first_octet, uint8_t second_octet, uint8_t third_octet, uint8_t fourth_octet)
|
|
||||||
{
|
|
||||||
_address[0] = first_octet;
|
|
||||||
_address[1] = second_octet;
|
|
||||||
_address[2] = third_octet;
|
|
||||||
_address[3] = fourth_octet;
|
|
||||||
}
|
|
||||||
|
|
||||||
IPAddress::IPAddress(uint32_t address)
|
|
||||||
{
|
|
||||||
memcpy(_address, &address, sizeof(_address));
|
|
||||||
}
|
|
||||||
|
|
||||||
IPAddress::IPAddress(const uint8_t *address)
|
|
||||||
{
|
|
||||||
memcpy(_address, address, sizeof(_address));
|
|
||||||
}
|
|
||||||
|
|
||||||
IPAddress& IPAddress::operator=(const uint8_t *address)
|
|
||||||
{
|
|
||||||
memcpy(_address, address, sizeof(_address));
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
IPAddress& IPAddress::operator=(uint32_t address)
|
|
||||||
{
|
|
||||||
memcpy(_address, (const uint8_t *)&address, sizeof(_address));
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IPAddress::operator==(const uint8_t* addr)
|
|
||||||
{
|
|
||||||
return memcmp(addr, _address, sizeof(_address)) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
*
|
|
||||||
* MIT License:
|
|
||||||
* Copyright (c) 2011 Adrian McEwen
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
* adrianm@mcqn.com 1/1/2011
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef IPAddress_h
|
|
||||||
#define IPAddress_h
|
|
||||||
|
|
||||||
|
|
||||||
// A class to make it easier to handle and pass around IP addresses
|
|
||||||
|
|
||||||
class IPAddress {
|
|
||||||
private:
|
|
||||||
uint8_t _address[4]; // IPv4 address
|
|
||||||
// Access the raw byte array containing the address. Because this returns a pointer
|
|
||||||
// to the internal structure rather than a copy of the address this function should only
|
|
||||||
// be used when you know that the usage of the returned uint8_t* will be transient and not
|
|
||||||
// stored.
|
|
||||||
uint8_t* raw_address() { return _address; };
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructors
|
|
||||||
IPAddress();
|
|
||||||
IPAddress(uint8_t first_octet, uint8_t second_octet, uint8_t third_octet, uint8_t fourth_octet);
|
|
||||||
IPAddress(uint32_t address);
|
|
||||||
IPAddress(const uint8_t *address);
|
|
||||||
|
|
||||||
// Overloaded cast operator to allow IPAddress objects to be used where a pointer
|
|
||||||
// to a four-byte uint8_t array is expected
|
|
||||||
operator uint32_t() { return *((uint32_t*)_address); };
|
|
||||||
bool operator==(const IPAddress& addr) { return (*((uint32_t*)_address)) == (*((uint32_t*)addr._address)); };
|
|
||||||
bool operator==(const uint8_t* addr);
|
|
||||||
|
|
||||||
// Overloaded index operator to allow getting and setting individual octets of the address
|
|
||||||
uint8_t operator[](int index) const { return _address[index]; };
|
|
||||||
uint8_t& operator[](int index) { return _address[index]; };
|
|
||||||
|
|
||||||
// Overloaded copy operators to allow initialisation of IPAddress objects from other types
|
|
||||||
IPAddress& operator=(const uint8_t *address);
|
|
||||||
IPAddress& operator=(uint32_t address);
|
|
||||||
|
|
||||||
|
|
||||||
friend class EthernetClass;
|
|
||||||
friend class UDP;
|
|
||||||
friend class Client;
|
|
||||||
friend class Server;
|
|
||||||
friend class DhcpClass;
|
|
||||||
friend class DNSClient;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
Print.h - Base class that provides print() and println()
|
|
||||||
Copyright (c) 2008 David A. Mellis. All right reserved.
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library 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
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef Print_h
|
|
||||||
#define Print_h
|
|
||||||
|
|
||||||
class Print {
|
|
||||||
public:
|
|
||||||
virtual size_t write(uint8_t) = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
#include "ShimClient.h"
|
|
||||||
#include "trace.h"
|
|
||||||
#include <iostream>
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <ctime>
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
uint32_t millis(void) {
|
|
||||||
return time(0)*1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ShimClient::ShimClient() {
|
|
||||||
this->responseBuffer = new Buffer();
|
|
||||||
this->expectBuffer = new Buffer();
|
|
||||||
this->_allowConnect = true;
|
|
||||||
this->_connected = false;
|
|
||||||
this->_error = false;
|
|
||||||
this->expectAnything = true;
|
|
||||||
this->_received = 0;
|
|
||||||
this->_expectedPort = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ShimClient::connect(IPAddress ip, uint16_t port) {
|
|
||||||
if (this->_allowConnect) {
|
|
||||||
this->_connected = true;
|
|
||||||
}
|
|
||||||
if (this->_expectedPort !=0) {
|
|
||||||
// if (memcmp(ip,this->_expectedIP,4) != 0) {
|
|
||||||
// TRACE( "ip mismatch\n");
|
|
||||||
// this->_error = true;
|
|
||||||
// }
|
|
||||||
if (port != this->_expectedPort) {
|
|
||||||
TRACE( "port mismatch\n");
|
|
||||||
this->_error = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this->_connected;
|
|
||||||
}
|
|
||||||
int ShimClient::connect(const char *host, uint16_t port) {
|
|
||||||
if (this->_allowConnect) {
|
|
||||||
this->_connected = true;
|
|
||||||
}
|
|
||||||
if (this->_expectedPort !=0) {
|
|
||||||
if (strcmp(host,this->_expectedHost) != 0) {
|
|
||||||
TRACE( "host mismatch\n");
|
|
||||||
this->_error = true;
|
|
||||||
}
|
|
||||||
if (port != this->_expectedPort) {
|
|
||||||
TRACE( "port mismatch\n");
|
|
||||||
this->_error = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return this->_connected;
|
|
||||||
}
|
|
||||||
size_t ShimClient::write(uint8_t b) {
|
|
||||||
this->_received += 1;
|
|
||||||
TRACE(std::hex << (unsigned int)b);
|
|
||||||
if (!this->expectAnything) {
|
|
||||||
if (this->expectBuffer->available()) {
|
|
||||||
uint8_t expected = this->expectBuffer->next();
|
|
||||||
if (expected != b) {
|
|
||||||
this->_error = true;
|
|
||||||
TRACE("!=" << (unsigned int)expected);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->_error = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TRACE("\n"<< std::dec);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
size_t ShimClient::write(const uint8_t *buf, size_t size) {
|
|
||||||
this->_received += size;
|
|
||||||
TRACE( "[" << std::dec << (unsigned int)(size) << "] ");
|
|
||||||
uint16_t i=0;
|
|
||||||
for (;i<size;i++) {
|
|
||||||
if (i>0) {
|
|
||||||
TRACE(":");
|
|
||||||
}
|
|
||||||
TRACE(std::hex << (unsigned int)(buf[i]));
|
|
||||||
|
|
||||||
if (!this->expectAnything) {
|
|
||||||
if (this->expectBuffer->available()) {
|
|
||||||
uint8_t expected = this->expectBuffer->next();
|
|
||||||
if (expected != buf[i]) {
|
|
||||||
this->_error = true;
|
|
||||||
TRACE("!=" << (unsigned int)expected);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->_error = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TRACE("\n"<<std::dec);
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
int ShimClient::available() {
|
|
||||||
return this->responseBuffer->available();
|
|
||||||
}
|
|
||||||
int ShimClient::read() { return this->responseBuffer->next(); }
|
|
||||||
int ShimClient::read(uint8_t *buf, size_t size) {
|
|
||||||
uint16_t i = 0;
|
|
||||||
for (;i<size;i++) {
|
|
||||||
buf[i] = this->read();
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
int ShimClient::peek() { return 0; }
|
|
||||||
void ShimClient::flush() {}
|
|
||||||
void ShimClient::stop() {
|
|
||||||
this->setConnected(false);
|
|
||||||
}
|
|
||||||
uint8_t ShimClient::connected() { return this->_connected; }
|
|
||||||
ShimClient::operator bool() { return true; }
|
|
||||||
|
|
||||||
|
|
||||||
ShimClient* ShimClient::respond(uint8_t *buf, size_t size) {
|
|
||||||
this->responseBuffer->add(buf,size);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShimClient* ShimClient::expect(uint8_t *buf, size_t size) {
|
|
||||||
this->expectAnything = false;
|
|
||||||
this->expectBuffer->add(buf,size);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimClient::setConnected(bool b) {
|
|
||||||
this->_connected = b;
|
|
||||||
}
|
|
||||||
void ShimClient::setAllowConnect(bool b) {
|
|
||||||
this->_allowConnect = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShimClient::error() {
|
|
||||||
return this->_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t ShimClient::received() {
|
|
||||||
return this->_received;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimClient::expectConnect(IPAddress ip, uint16_t port) {
|
|
||||||
this->_expectedIP = ip;
|
|
||||||
this->_expectedPort = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimClient::expectConnect(const char *host, uint16_t port) {
|
|
||||||
this->_expectedHost = host;
|
|
||||||
this->_expectedPort = port;
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#ifndef shimclient_h
|
|
||||||
#define shimclient_h
|
|
||||||
|
|
||||||
#include "Arduino.h"
|
|
||||||
#include "Client.h"
|
|
||||||
#include "IPAddress.h"
|
|
||||||
#include "Buffer.h"
|
|
||||||
|
|
||||||
|
|
||||||
class ShimClient : public Client {
|
|
||||||
private:
|
|
||||||
Buffer* responseBuffer;
|
|
||||||
Buffer* expectBuffer;
|
|
||||||
bool _allowConnect;
|
|
||||||
bool _connected;
|
|
||||||
bool expectAnything;
|
|
||||||
bool _error;
|
|
||||||
uint16_t _received;
|
|
||||||
IPAddress _expectedIP;
|
|
||||||
uint16_t _expectedPort;
|
|
||||||
const char* _expectedHost;
|
|
||||||
|
|
||||||
public:
|
|
||||||
ShimClient();
|
|
||||||
virtual int connect(IPAddress ip, uint16_t port);
|
|
||||||
virtual int connect(const char *host, uint16_t port);
|
|
||||||
virtual size_t write(uint8_t);
|
|
||||||
virtual size_t write(const uint8_t *buf, size_t size);
|
|
||||||
virtual int available();
|
|
||||||
virtual int read();
|
|
||||||
virtual int read(uint8_t *buf, size_t size);
|
|
||||||
virtual int peek();
|
|
||||||
virtual void flush();
|
|
||||||
virtual void stop();
|
|
||||||
virtual uint8_t connected();
|
|
||||||
virtual operator bool();
|
|
||||||
|
|
||||||
virtual ShimClient* respond(uint8_t *buf, size_t size);
|
|
||||||
virtual ShimClient* expect(uint8_t *buf, size_t size);
|
|
||||||
|
|
||||||
virtual void expectConnect(IPAddress ip, uint16_t port);
|
|
||||||
virtual void expectConnect(const char *host, uint16_t port);
|
|
||||||
|
|
||||||
virtual uint16_t received();
|
|
||||||
virtual bool error();
|
|
||||||
|
|
||||||
virtual void setAllowConnect(bool b);
|
|
||||||
virtual void setConnected(bool b);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#include "Stream.h"
|
|
||||||
#include "trace.h"
|
|
||||||
#include <iostream>
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
Stream::Stream() {
|
|
||||||
this->expectBuffer = new Buffer();
|
|
||||||
this->_error = false;
|
|
||||||
this->_written = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t Stream::write(uint8_t b) {
|
|
||||||
this->_written++;
|
|
||||||
TRACE(std::hex << (unsigned int)b);
|
|
||||||
if (this->expectBuffer->available()) {
|
|
||||||
uint8_t expected = this->expectBuffer->next();
|
|
||||||
if (expected != b) {
|
|
||||||
this->_error = true;
|
|
||||||
TRACE("!=" << (unsigned int)expected);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->_error = true;
|
|
||||||
}
|
|
||||||
TRACE("\n"<< std::dec);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bool Stream::error() {
|
|
||||||
return this->_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Stream::expect(uint8_t *buf, size_t size) {
|
|
||||||
this->expectBuffer->add(buf,size);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t Stream::length() {
|
|
||||||
return this->_written;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#ifndef Stream_h
|
|
||||||
#define Stream_h
|
|
||||||
|
|
||||||
#include "Arduino.h"
|
|
||||||
#include "Buffer.h"
|
|
||||||
|
|
||||||
class Stream {
|
|
||||||
private:
|
|
||||||
Buffer* expectBuffer;
|
|
||||||
bool _error;
|
|
||||||
uint16_t _written;
|
|
||||||
|
|
||||||
public:
|
|
||||||
Stream();
|
|
||||||
virtual size_t write(uint8_t);
|
|
||||||
|
|
||||||
virtual bool error();
|
|
||||||
virtual void expect(uint8_t *buf, size_t size);
|
|
||||||
virtual uint16_t length();
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#ifndef trace_h
|
|
||||||
#define trace_h
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#define LOG(x) {std::cout << x << std::flush; }
|
|
||||||
#define TRACE(x) {if (getenv("TRACE")) { std::cout << x << std::flush; }}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
#include "PubSubClient.h"
|
|
||||||
#include "ShimClient.h"
|
|
||||||
#include "Buffer.h"
|
|
||||||
#include "BDDTest.h"
|
|
||||||
#include "trace.h"
|
|
||||||
|
|
||||||
|
|
||||||
byte server[] = { 172, 16, 0, 2 };
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
// handle message arrived
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_publish() {
|
|
||||||
IT("publishes a null-terminated string");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
shimClient.expect(publish,16);
|
|
||||||
|
|
||||||
rc = client.publish((char*)"topic",(char*)"payload");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int test_publish_bytes() {
|
|
||||||
IT("publishes a byte array");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte payload[] = { 0x01,0x02,0x03,0x0,0x05 };
|
|
||||||
int length = 5;
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x30,0xc,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x1,0x2,0x3,0x0,0x5};
|
|
||||||
shimClient.expect(publish,14);
|
|
||||||
|
|
||||||
rc = client.publish((char*)"topic",payload,length);
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int test_publish_retained() {
|
|
||||||
IT("publishes retained - 1");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte payload[] = { 0x01,0x02,0x03,0x0,0x05 };
|
|
||||||
int length = 5;
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x31,0xc,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x1,0x2,0x3,0x0,0x5};
|
|
||||||
shimClient.expect(publish,14);
|
|
||||||
|
|
||||||
rc = client.publish((char*)"topic",payload,length,true);
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_publish_retained_2() {
|
|
||||||
IT("publishes retained - 2");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x31,0xc,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,'A','B','C','D','E'};
|
|
||||||
shimClient.expect(publish,14);
|
|
||||||
|
|
||||||
rc = client.publish((char*)"topic",(char*)"ABCDE",true);
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_publish_not_connected() {
|
|
||||||
IT("publish fails when not connected");
|
|
||||||
ShimClient shimClient;
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
|
|
||||||
int rc = client.publish((char*)"topic",(char*)"payload");
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_publish_too_long() {
|
|
||||||
IT("publish fails when topic/payload are too long");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2
|
|
||||||
rc = client.publish((char*)"topic",(char*)"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890");
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_publish_P() {
|
|
||||||
IT("publishes using PROGMEM");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte payload[] = { 0x01,0x02,0x03,0x0,0x05 };
|
|
||||||
int length = 5;
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x31,0xc,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x1,0x2,0x3,0x0,0x5};
|
|
||||||
shimClient.expect(publish,14);
|
|
||||||
|
|
||||||
rc = client.publish_P((char*)"topic",payload,length,true);
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
SUITE("Publish");
|
|
||||||
test_publish();
|
|
||||||
test_publish_bytes();
|
|
||||||
test_publish_retained();
|
|
||||||
test_publish_retained_2();
|
|
||||||
test_publish_not_connected();
|
|
||||||
test_publish_too_long();
|
|
||||||
test_publish_P();
|
|
||||||
|
|
||||||
FINISH
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
#include "PubSubClient.h"
|
|
||||||
#include "ShimClient.h"
|
|
||||||
#include "Buffer.h"
|
|
||||||
#include "BDDTest.h"
|
|
||||||
#include "trace.h"
|
|
||||||
|
|
||||||
|
|
||||||
byte server[] = { 172, 16, 0, 2 };
|
|
||||||
|
|
||||||
bool callback_called = false;
|
|
||||||
char lastTopic[1024];
|
|
||||||
char lastPayload[1024];
|
|
||||||
unsigned int lastLength;
|
|
||||||
|
|
||||||
void reset_callback() {
|
|
||||||
callback_called = false;
|
|
||||||
lastTopic[0] = '\0';
|
|
||||||
lastPayload[0] = '\0';
|
|
||||||
lastLength = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
callback_called = true;
|
|
||||||
strcpy(lastTopic,topic);
|
|
||||||
memcpy(lastPayload,payload,length);
|
|
||||||
lastLength = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_receive_callback() {
|
|
||||||
IT("receives a callback message");
|
|
||||||
reset_callback();
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
shimClient.respond(publish,16);
|
|
||||||
|
|
||||||
rc = client.loop();
|
|
||||||
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_TRUE(callback_called);
|
|
||||||
IS_TRUE(strcmp(lastTopic,"topic")==0);
|
|
||||||
IS_TRUE(memcmp(lastPayload,"payload",7)==0);
|
|
||||||
IS_TRUE(lastLength == 7);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_receive_stream() {
|
|
||||||
IT("receives a streamed callback message");
|
|
||||||
reset_callback();
|
|
||||||
|
|
||||||
Stream stream;
|
|
||||||
stream.expect((uint8_t*)"payload",7);
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient, stream);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
shimClient.respond(publish,16);
|
|
||||||
|
|
||||||
rc = client.loop();
|
|
||||||
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_TRUE(callback_called);
|
|
||||||
IS_TRUE(strcmp(lastTopic,"topic")==0);
|
|
||||||
IS_TRUE(lastLength == 7);
|
|
||||||
|
|
||||||
IS_FALSE(stream.error());
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_receive_max_sized_message() {
|
|
||||||
IT("receives an max-sized message");
|
|
||||||
reset_callback();
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
int length = MQTT_MAX_PACKET_SIZE;
|
|
||||||
byte publish[] = {0x30,length-2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
byte bigPublish[length];
|
|
||||||
memset(bigPublish,'A',length);
|
|
||||||
bigPublish[length] = 'B';
|
|
||||||
memcpy(bigPublish,publish,16);
|
|
||||||
shimClient.respond(bigPublish,length);
|
|
||||||
|
|
||||||
rc = client.loop();
|
|
||||||
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_TRUE(callback_called);
|
|
||||||
IS_TRUE(strcmp(lastTopic,"topic")==0);
|
|
||||||
IS_TRUE(lastLength == length-9);
|
|
||||||
IS_TRUE(memcmp(lastPayload,bigPublish+9,lastLength)==0);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_receive_oversized_message() {
|
|
||||||
IT("drops an oversized message");
|
|
||||||
reset_callback();
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
int length = MQTT_MAX_PACKET_SIZE+1;
|
|
||||||
byte publish[] = {0x30,length-2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
byte bigPublish[length];
|
|
||||||
memset(bigPublish,'A',length);
|
|
||||||
bigPublish[length] = 'B';
|
|
||||||
memcpy(bigPublish,publish,16);
|
|
||||||
shimClient.respond(bigPublish,length);
|
|
||||||
|
|
||||||
rc = client.loop();
|
|
||||||
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(callback_called);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_drop_invalid_remaining_length_message() {
|
|
||||||
IT("drops invalid remaining length message");
|
|
||||||
reset_callback();
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x30,0x92,0x92,0x92,0x92,0x01,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
shimClient.respond(publish,20);
|
|
||||||
|
|
||||||
rc = client.loop();
|
|
||||||
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(callback_called);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int test_receive_oversized_stream_message() {
|
|
||||||
IT("drops an oversized message");
|
|
||||||
reset_callback();
|
|
||||||
|
|
||||||
Stream stream;
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient, stream);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
int length = MQTT_MAX_PACKET_SIZE+1;
|
|
||||||
byte publish[] = {0x30,length-2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
|
|
||||||
byte bigPublish[length];
|
|
||||||
memset(bigPublish,'A',length);
|
|
||||||
bigPublish[length] = 'B';
|
|
||||||
memcpy(bigPublish,publish,16);
|
|
||||||
|
|
||||||
shimClient.respond(bigPublish,length);
|
|
||||||
stream.expect(bigPublish+9,length-9);
|
|
||||||
|
|
||||||
rc = client.loop();
|
|
||||||
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_TRUE(callback_called);
|
|
||||||
IS_TRUE(strcmp(lastTopic,"topic")==0);
|
|
||||||
IS_TRUE(lastLength == length-9);
|
|
||||||
|
|
||||||
IS_FALSE(stream.error());
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_receive_qos1() {
|
|
||||||
IT("receives a qos1 message");
|
|
||||||
reset_callback();
|
|
||||||
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte publish[] = {0x32,0x10,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x12,0x34,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64};
|
|
||||||
shimClient.respond(publish,18);
|
|
||||||
|
|
||||||
byte puback[] = {0x40,0x2,0x12,0x34};
|
|
||||||
shimClient.expect(puback,4);
|
|
||||||
|
|
||||||
rc = client.loop();
|
|
||||||
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_TRUE(callback_called);
|
|
||||||
IS_TRUE(strcmp(lastTopic,"topic")==0);
|
|
||||||
IS_TRUE(memcmp(lastPayload,"payload",7)==0);
|
|
||||||
IS_TRUE(lastLength == 7);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
SUITE("Receive");
|
|
||||||
test_receive_callback();
|
|
||||||
test_receive_stream();
|
|
||||||
test_receive_max_sized_message();
|
|
||||||
test_drop_invalid_remaining_length_message();
|
|
||||||
test_receive_oversized_message();
|
|
||||||
test_receive_oversized_stream_message();
|
|
||||||
test_receive_qos1();
|
|
||||||
|
|
||||||
FINISH
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
#include "PubSubClient.h"
|
|
||||||
#include "ShimClient.h"
|
|
||||||
#include "Buffer.h"
|
|
||||||
#include "BDDTest.h"
|
|
||||||
#include "trace.h"
|
|
||||||
|
|
||||||
|
|
||||||
byte server[] = { 172, 16, 0, 2 };
|
|
||||||
|
|
||||||
void callback(char* topic, byte* payload, unsigned int length) {
|
|
||||||
// handle message arrived
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_subscribe_no_qos() {
|
|
||||||
IT("subscribe without qos defaults to 0");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte subscribe[] = { 0x82,0xa,0x0,0x2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x0 };
|
|
||||||
shimClient.expect(subscribe,12);
|
|
||||||
byte suback[] = { 0x90,0x3,0x0,0x2,0x0 };
|
|
||||||
shimClient.respond(suback,5);
|
|
||||||
|
|
||||||
rc = client.subscribe((char*)"topic");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_subscribe_qos_1() {
|
|
||||||
IT("subscribes qos 1");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte subscribe[] = { 0x82,0xa,0x0,0x2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x1 };
|
|
||||||
shimClient.expect(subscribe,12);
|
|
||||||
byte suback[] = { 0x90,0x3,0x0,0x2,0x1 };
|
|
||||||
shimClient.respond(suback,5);
|
|
||||||
|
|
||||||
rc = client.subscribe((char*)"topic",1);
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_subscribe_not_connected() {
|
|
||||||
IT("subscribe fails when not connected");
|
|
||||||
ShimClient shimClient;
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
|
|
||||||
int rc = client.subscribe((char*)"topic");
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_subscribe_invalid_qos() {
|
|
||||||
IT("subscribe fails with invalid qos values");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
rc = client.subscribe((char*)"topic",2);
|
|
||||||
IS_FALSE(rc);
|
|
||||||
rc = client.subscribe((char*)"topic",254);
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_subscribe_too_long() {
|
|
||||||
IT("subscribe fails with too long topic");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
// max length should be allowed
|
|
||||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2
|
|
||||||
rc = client.subscribe((char*)"12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2
|
|
||||||
rc = client.subscribe((char*)"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890");
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int test_unsubscribe() {
|
|
||||||
IT("unsubscribes");
|
|
||||||
ShimClient shimClient;
|
|
||||||
shimClient.setAllowConnect(true);
|
|
||||||
|
|
||||||
byte connack[] = { 0x20, 0x02, 0x00, 0x00 };
|
|
||||||
shimClient.respond(connack,4);
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
int rc = client.connect((char*)"client_test1");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
byte unsubscribe[] = { 0xA2,0x9,0x0,0x2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63 };
|
|
||||||
shimClient.expect(unsubscribe,12);
|
|
||||||
byte unsuback[] = { 0xB0,0x2,0x0,0x2 };
|
|
||||||
shimClient.respond(unsuback,4);
|
|
||||||
|
|
||||||
rc = client.unsubscribe((char*)"topic");
|
|
||||||
IS_TRUE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int test_unsubscribe_not_connected() {
|
|
||||||
IT("unsubscribe fails when not connected");
|
|
||||||
ShimClient shimClient;
|
|
||||||
|
|
||||||
PubSubClient client(server, 1883, callback, shimClient);
|
|
||||||
|
|
||||||
int rc = client.unsubscribe((char*)"topic");
|
|
||||||
IS_FALSE(rc);
|
|
||||||
|
|
||||||
IS_FALSE(shimClient.error());
|
|
||||||
|
|
||||||
END_IT
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
SUITE("Subscribe");
|
|
||||||
test_subscribe_no_qos();
|
|
||||||
test_subscribe_qos_1();
|
|
||||||
test_subscribe_not_connected();
|
|
||||||
test_subscribe_invalid_qos();
|
|
||||||
test_subscribe_too_long();
|
|
||||||
test_unsubscribe();
|
|
||||||
test_unsubscribe_not_connected();
|
|
||||||
FINISH
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import settings
|
|
||||||
import time
|
|
||||||
import mosquitto
|
|
||||||
|
|
||||||
|
|
||||||
def on_message(mosq, obj, msg):
|
|
||||||
obj.message_queue.append(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class mqtt_basic(unittest.TestCase):
|
|
||||||
|
|
||||||
message_queue = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(self):
|
|
||||||
self.client = mosquitto.Mosquitto("pubsubclient_ut", clean_session=True, obj=self)
|
|
||||||
self.client.connect(settings.server_ip)
|
|
||||||
self.client.on_message = on_message
|
|
||||||
self.client.subscribe("outTopic", 0)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(self):
|
|
||||||
self.client.disconnect()
|
|
||||||
|
|
||||||
def test_one(self):
|
|
||||||
i = 30
|
|
||||||
while len(self.message_queue) == 0 and i > 0:
|
|
||||||
self.client.loop()
|
|
||||||
time.sleep(0.5)
|
|
||||||
i -= 1
|
|
||||||
self.assertTrue(i > 0, "message receive timed-out")
|
|
||||||
self.assertEqual(len(self.message_queue), 1, "unexpected number of messages received")
|
|
||||||
msg = self.message_queue[0]
|
|
||||||
self.assertEqual(msg.mid, 0, "message id not 0")
|
|
||||||
self.assertEqual(msg.topic, "outTopic", "message topic incorrect")
|
|
||||||
self.assertEqual(msg.payload, "hello world")
|
|
||||||
self.assertEqual(msg.qos, 0, "message qos not 0")
|
|
||||||
self.assertEqual(msg.retain, False, "message retain flag incorrect")
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import settings
|
|
||||||
import time
|
|
||||||
import mosquitto
|
|
||||||
|
|
||||||
|
|
||||||
def on_message(mosq, obj, msg):
|
|
||||||
obj.message_queue.append(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class mqtt_publish_in_callback(unittest.TestCase):
|
|
||||||
|
|
||||||
message_queue = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(self):
|
|
||||||
self.client = mosquitto.Mosquitto("pubsubclient_ut", clean_session=True, obj=self)
|
|
||||||
self.client.connect(settings.server_ip)
|
|
||||||
self.client.on_message = on_message
|
|
||||||
self.client.subscribe("outTopic", 0)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(self):
|
|
||||||
self.client.disconnect()
|
|
||||||
|
|
||||||
def test_connect(self):
|
|
||||||
i = 30
|
|
||||||
while len(self.message_queue) == 0 and i > 0:
|
|
||||||
self.client.loop()
|
|
||||||
time.sleep(0.5)
|
|
||||||
i -= 1
|
|
||||||
self.assertTrue(i > 0, "message receive timed-out")
|
|
||||||
self.assertEqual(len(self.message_queue), 1, "unexpected number of messages received")
|
|
||||||
msg = self.message_queue.pop(0)
|
|
||||||
self.assertEqual(msg.mid, 0, "message id not 0")
|
|
||||||
self.assertEqual(msg.topic, "outTopic", "message topic incorrect")
|
|
||||||
self.assertEqual(msg.payload, "hello world")
|
|
||||||
self.assertEqual(msg.qos, 0, "message qos not 0")
|
|
||||||
self.assertEqual(msg.retain, False, "message retain flag incorrect")
|
|
||||||
|
|
||||||
def test_publish(self):
|
|
||||||
self.assertEqual(len(self.message_queue), 0, "message queue not empty")
|
|
||||||
payload = "abcdefghij"
|
|
||||||
self.client.publish("inTopic", payload)
|
|
||||||
|
|
||||||
i = 30
|
|
||||||
while len(self.message_queue) == 0 and i > 0:
|
|
||||||
self.client.loop()
|
|
||||||
time.sleep(0.5)
|
|
||||||
i -= 1
|
|
||||||
|
|
||||||
self.assertTrue(i > 0, "message receive timed-out")
|
|
||||||
self.assertEqual(len(self.message_queue), 1, "unexpected number of messages received")
|
|
||||||
msg = self.message_queue.pop(0)
|
|
||||||
self.assertEqual(msg.mid, 0, "message id not 0")
|
|
||||||
self.assertEqual(msg.topic, "outTopic", "message topic incorrect")
|
|
||||||
self.assertEqual(msg.payload, payload)
|
|
||||||
self.assertEqual(msg.qos, 0, "message qos not 0")
|
|
||||||
self.assertEqual(msg.retain, False, "message retain flag incorrect")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
server_ip = "172.16.0.2"
|
|
||||||
arduino_ip = "172.16.0.100"
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import os
|
|
||||||
import os.path
|
|
||||||
import sys
|
|
||||||
import shutil
|
|
||||||
from subprocess import call
|
|
||||||
import importlib
|
|
||||||
import unittest
|
|
||||||
import re
|
|
||||||
|
|
||||||
from testcases import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Workspace(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.root_dir = os.getcwd()
|
|
||||||
self.build_dir = os.path.join(self.root_dir, "tmpbin")
|
|
||||||
self.log_dir = os.path.join(self.root_dir, "logs")
|
|
||||||
self.tests_dir = os.path.join(self.root_dir, "testcases")
|
|
||||||
self.examples_dir = os.path.join(self.root_dir, "../PubSubClient/examples")
|
|
||||||
self.examples = []
|
|
||||||
self.tests = []
|
|
||||||
if not os.path.isdir("../PubSubClient"):
|
|
||||||
raise Exception("Cannot find PubSubClient library")
|
|
||||||
try:
|
|
||||||
return __import__('ino')
|
|
||||||
except ImportError:
|
|
||||||
raise Exception("ino tool not installed")
|
|
||||||
|
|
||||||
def init(self):
|
|
||||||
if os.path.isdir(self.build_dir):
|
|
||||||
shutil.rmtree(self.build_dir)
|
|
||||||
os.mkdir(self.build_dir)
|
|
||||||
if os.path.isdir(self.log_dir):
|
|
||||||
shutil.rmtree(self.log_dir)
|
|
||||||
os.mkdir(self.log_dir)
|
|
||||||
|
|
||||||
os.chdir(self.build_dir)
|
|
||||||
call(["ino", "init"])
|
|
||||||
|
|
||||||
shutil.copytree("../../PubSubClient", "lib/PubSubClient")
|
|
||||||
|
|
||||||
filenames = []
|
|
||||||
for root, dirs, files in os.walk(self.examples_dir):
|
|
||||||
filenames += [os.path.join(root, f) for f in files if f.endswith(".ino")]
|
|
||||||
filenames.sort()
|
|
||||||
for e in filenames:
|
|
||||||
self.examples.append(Sketch(self, e))
|
|
||||||
|
|
||||||
filenames = []
|
|
||||||
for root, dirs, files in os.walk(self.tests_dir):
|
|
||||||
filenames += [os.path.join(root, f) for f in files if f.endswith(".ino")]
|
|
||||||
filenames.sort()
|
|
||||||
for e in filenames:
|
|
||||||
self.tests.append(Sketch(self, e))
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
shutil.rmtree(self.build_dir)
|
|
||||||
|
|
||||||
|
|
||||||
class Sketch(object):
|
|
||||||
def __init__(self, wksp, fn):
|
|
||||||
self.w = wksp
|
|
||||||
self.filename = fn
|
|
||||||
self.basename = os.path.basename(self.filename)
|
|
||||||
self.build_log = os.path.join(self.w.log_dir, "%s.log" % (os.path.basename(self.filename),))
|
|
||||||
self.build_err_log = os.path.join(self.w.log_dir, "%s.err.log" % (os.path.basename(self.filename),))
|
|
||||||
self.build_upload_log = os.path.join(self.w.log_dir, "%s.upload.log" % (os.path.basename(self.filename),))
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
sys.stdout.write(" Build: ")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
# Copy sketch over, replacing IP addresses as necessary
|
|
||||||
fin = open(self.filename, "r")
|
|
||||||
lines = fin.readlines()
|
|
||||||
fin.close()
|
|
||||||
fout = open(os.path.join(self.w.build_dir, "src", "sketch.ino"), "w")
|
|
||||||
for l in lines:
|
|
||||||
if re.match(r"^byte server\[\] = {", l):
|
|
||||||
fout.write("byte server[] = { %s };\n" % (settings.server_ip.replace(".", ", "),))
|
|
||||||
elif re.match(r"^byte ip\[\] = {", l):
|
|
||||||
fout.write("byte ip[] = { %s };\n" % (settings.arduino_ip.replace(".", ", "),))
|
|
||||||
else:
|
|
||||||
fout.write(l)
|
|
||||||
fout.flush()
|
|
||||||
fout.close()
|
|
||||||
|
|
||||||
# Run build
|
|
||||||
fout = open(self.build_log, "w")
|
|
||||||
ferr = open(self.build_err_log, "w")
|
|
||||||
rc = call(["ino", "build"], stdout=fout, stderr=ferr)
|
|
||||||
fout.close()
|
|
||||||
ferr.close()
|
|
||||||
if rc == 0:
|
|
||||||
sys.stdout.write("pass")
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
sys.stdout.write("fail")
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
with open(self.build_err_log) as f:
|
|
||||||
for line in f:
|
|
||||||
print(" " + line)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def upload(self):
|
|
||||||
sys.stdout.write(" Upload: ")
|
|
||||||
sys.stdout.flush()
|
|
||||||
fout = open(self.build_upload_log, "w")
|
|
||||||
rc = call(["ino", "upload"], stdout=fout, stderr=fout)
|
|
||||||
fout.close()
|
|
||||||
if rc == 0:
|
|
||||||
sys.stdout.write("pass")
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
sys.stdout.write("fail")
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
with open(self.build_upload_log) as f:
|
|
||||||
for line in f:
|
|
||||||
print(" " + line)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test(self):
|
|
||||||
# import the matching test case, if it exists
|
|
||||||
try:
|
|
||||||
basename = os.path.basename(self.filename)[:-4]
|
|
||||||
i = importlib.import_module("testcases." + basename)
|
|
||||||
except:
|
|
||||||
sys.stdout.write(" Test: no tests found")
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
return
|
|
||||||
c = getattr(i, basename)
|
|
||||||
|
|
||||||
testmethods = [m for m in dir(c) if m.startswith("test_")]
|
|
||||||
testmethods.sort()
|
|
||||||
tests = []
|
|
||||||
for m in testmethods:
|
|
||||||
tests.append(c(m))
|
|
||||||
|
|
||||||
result = unittest.TestResult()
|
|
||||||
c.setUpClass()
|
|
||||||
if self.upload():
|
|
||||||
sys.stdout.write(" Test: ")
|
|
||||||
sys.stdout.flush()
|
|
||||||
for t in tests:
|
|
||||||
t.run(result)
|
|
||||||
print(str(result.testsRun - len(result.failures) - len(result.errors)) + "/" + str(result.testsRun))
|
|
||||||
if not result.wasSuccessful():
|
|
||||||
if len(result.failures) > 0:
|
|
||||||
for f in result.failures:
|
|
||||||
print("-- " + str(f[0]))
|
|
||||||
print(f[1])
|
|
||||||
if len(result.errors) > 0:
|
|
||||||
print(" Errors:")
|
|
||||||
for f in result.errors:
|
|
||||||
print("-- " + str(f[0]))
|
|
||||||
print(f[1])
|
|
||||||
c.tearDownClass()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
run_tests = True
|
|
||||||
|
|
||||||
w = Workspace()
|
|
||||||
w.init()
|
|
||||||
|
|
||||||
for e in w.examples:
|
|
||||||
print("--------------------------------------")
|
|
||||||
print("[" + e.basename + "]")
|
|
||||||
if e.build() and run_tests:
|
|
||||||
e.test()
|
|
||||||
for e in w.tests:
|
|
||||||
print("--------------------------------------")
|
|
||||||
print("[" + e.basename + "]")
|
|
||||||
if e.build() and run_tests:
|
|
||||||
e.test()
|
|
||||||
|
|
||||||
w.clean()
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Custom partition table without SPIFFS.
|
||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, 0xe000, 0x2000,
|
||||||
|
app0, app, ota_0, 0x10000, 0x220000,
|
||||||
|
app1, app, ota_1, 0x230000,0x220000,
|
||||||
|
+22
-5
@@ -8,13 +8,30 @@
|
|||||||
; Please visit documentation for the other options and examples
|
; Please visit documentation for the other options and examples
|
||||||
; https://docs.platformio.org/page/projectconf.html
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
[env:esp32]
|
[platformio]
|
||||||
|
default_envs = esp32
|
||||||
|
|
||||||
|
[env]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp-wrover-kit
|
board = esp-wrover-kit
|
||||||
framework = arduino
|
framework = arduino
|
||||||
upload_speed = 512000
|
upload_speed = 512000
|
||||||
build_flags=!./build_version.sh
|
lib_deps =
|
||||||
lib_deps = MFRC522
|
63 ; MFRC522
|
||||||
upload_port = /dev/cu.SLAB_USBtoUART
|
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
||||||
monitor_speed = 74480
|
64 ; ArduinoJSON
|
||||||
|
6691 ; TinyXML
|
||||||
|
monitor_speed = 115200
|
||||||
|
board_build.embed_files =
|
||||||
|
src/webinterface/timezones.json.gz
|
||||||
|
src/webinterface/index.html.gz
|
||||||
|
;board_build.partitions = partitions.csv
|
||||||
;monitor_port = /dev/cu.wchusbserial1420
|
;monitor_port = /dev/cu.wchusbserial1420
|
||||||
|
extra_scripts =
|
||||||
|
post:tools/post_build.py
|
||||||
|
|
||||||
|
[env:esp32]
|
||||||
|
build_flags=!./build_version.sh
|
||||||
|
upload_port = /dev/cu.SLAB_USBtoUART
|
||||||
|
|
||||||
|
[env:deploy]
|
||||||
|
|||||||
+281
-95
@@ -1,31 +1,38 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
#include "main.h"
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "playlist.h"
|
||||||
|
#include "http_server.h"
|
||||||
|
#include "updater.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
Controller::Controller(Player* p, SPIMaster* s) {
|
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
|
||||||
_player = p;
|
player = p;
|
||||||
_spi = s;
|
pm = playlist_manager;
|
||||||
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
|
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
|
||||||
|
|
||||||
|
player->register_controller(this);
|
||||||
|
|
||||||
BTN_NEXT_SETUP();
|
BTN_NEXT_SETUP();
|
||||||
BTN_PREV_SETUP();
|
BTN_PREV_SETUP();
|
||||||
BTN_VOL_UP_SETUP();
|
BTN_VOL_UP_SETUP();
|
||||||
BTN_VOL_DOWN_SETUP();
|
BTN_VOL_DOWN_SETUP();
|
||||||
|
|
||||||
_spi->select_rc522();
|
SPIMaster::select_rc522();
|
||||||
DEBUG("Initializing RC522...\n");
|
DEBUG("Initializing RC522...\n");
|
||||||
_rfid->PCD_Init();
|
_rfid->PCD_Init();
|
||||||
#ifdef SHOW_DEBUG
|
#ifdef SHOW_DEBUG
|
||||||
_rfid->PCD_DumpVersionToSerial();
|
_rfid->PCD_DumpVersionToSerial();
|
||||||
#endif
|
#endif
|
||||||
_spi->select_rc522(false);
|
SPIMaster::select_rc522(false);
|
||||||
INFO("RC522 initialized.\n");
|
INFO("RC522 initialized.\n");
|
||||||
|
|
||||||
for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0;
|
for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::set_mqtt_client(MQTTClient* m) {
|
void Controller::register_http_server(HTTPServer* h) {
|
||||||
_mqtt_client = m;
|
_http_server = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::loop() {
|
void Controller::loop() {
|
||||||
@@ -34,17 +41,34 @@ void Controller::loop() {
|
|||||||
_check_rfid();
|
_check_rfid();
|
||||||
_last_rfid_scan_at = now;
|
_last_rfid_scan_at = now;
|
||||||
}
|
}
|
||||||
|
if ((_last_position_info_at < now - POSITION_SEND_INTERVAL) || (now < _last_position_info_at)) {
|
||||||
|
send_position();
|
||||||
|
_last_position_info_at = now;
|
||||||
|
}
|
||||||
_check_serial();
|
_check_serial();
|
||||||
_check_buttons();
|
_check_buttons();
|
||||||
|
if (_cmd_queue.length() > 0) {
|
||||||
|
process_message(_cmd_queue);
|
||||||
|
_cmd_queue = "";
|
||||||
|
}
|
||||||
|
|
||||||
if ((_last_mqtt_report_at < now - MQTT_REPORT_INTERVAL) || (now < _last_mqtt_report_at)) {
|
#ifdef OTA_UPDATE_URL
|
||||||
_send_mqtt_report();
|
if (!player->is_playing() && _last_update_check_at < now && _last_update_check_at + OTA_CHECK_INTERVAL < now) {
|
||||||
_last_mqtt_report_at = now;
|
Updater::run();
|
||||||
|
} else {
|
||||||
|
_last_update_check_at = now;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!player->is_playing() && !WiFi.isConnected() && _last_wifi_try_at < now && _last_wifi_try_at + 5*60*1000 < now) {
|
||||||
|
wifi_connect();
|
||||||
|
} else {
|
||||||
|
_last_wifi_try_at = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Controller::_get_rfid_card_uid() {
|
uint32_t Controller::_get_rfid_card_uid() {
|
||||||
_spi->select_rc522();
|
SPIMaster::select_rc522();
|
||||||
if (!_rfid->PICC_ReadCardSerial()) {
|
if (!_rfid->PICC_ReadCardSerial()) {
|
||||||
if (!_rfid->PICC_IsNewCardPresent()) {
|
if (!_rfid->PICC_IsNewCardPresent()) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -53,34 +77,36 @@ uint32_t Controller::_get_rfid_card_uid() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_spi->select_rc522(false);
|
SPIMaster::select_rc522(false);
|
||||||
uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3];
|
uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3];
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_check_rfid() {
|
void Controller::_check_rfid() {
|
||||||
|
//TRACE("check_rfid running...\n");
|
||||||
MFRC522::StatusCode status;
|
MFRC522::StatusCode status;
|
||||||
if (_rfid_present) {
|
if (_rfid_present) {
|
||||||
byte buffer[2];
|
byte buffer[2];
|
||||||
byte buffer_size = 2;
|
byte buffer_size = 2;
|
||||||
_spi->select_rc522();
|
SPIMaster::select_rc522();
|
||||||
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
|
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
|
||||||
if (status == MFRC522::STATUS_OK) {
|
if (status == MFRC522::STATUS_OK) {
|
||||||
// Card is still present.
|
// Card is still present.
|
||||||
_rfid->PICC_HaltA();
|
_rfid->PICC_HaltA();
|
||||||
_spi->select_rc522(false);
|
SPIMaster::select_rc522(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_spi->select_rc522(false);
|
SPIMaster::select_rc522(false);
|
||||||
// Card is now gone
|
// Card is now gone
|
||||||
_rfid_present = false;
|
_rfid_present = false;
|
||||||
INFO("No more RFID card.\n");
|
INFO("No more RFID card.\n");
|
||||||
_player->stop();
|
if (_state != LOCKED) {
|
||||||
|
player->stop();
|
||||||
|
}
|
||||||
|
send_controller_status();
|
||||||
} else {
|
} else {
|
||||||
uint32_t uid = _get_rfid_card_uid();
|
uint32_t uid = _get_rfid_card_uid();
|
||||||
if (uid > 0) {
|
if (uid > 0) {
|
||||||
_mqtt_client->publish_rfid_uid(uid);
|
|
||||||
//_no_rfid_card_count = 0;
|
|
||||||
String temp = String(uid, HEX);
|
String temp = String(uid, HEX);
|
||||||
String s_uid = "";
|
String s_uid = "";
|
||||||
for (int i=0; i<(8-temp.length()); i++) {
|
for (int i=0; i<(8-temp.length()); i++) {
|
||||||
@@ -88,27 +114,82 @@ void Controller::_check_rfid() {
|
|||||||
}
|
}
|
||||||
s_uid.concat(temp);
|
s_uid.concat(temp);
|
||||||
INFO("New RFID card uid: %s\n", s_uid.c_str());
|
INFO("New RFID card uid: %s\n", s_uid.c_str());
|
||||||
|
_last_rfid_uid = s_uid;
|
||||||
_rfid_present = true;
|
_rfid_present = true;
|
||||||
|
|
||||||
String data = _read_rfid_data();
|
String data = _read_rfid_data();
|
||||||
|
_last_rfid_data = data;
|
||||||
|
|
||||||
_player->play_id(s_uid);
|
Playlist* pl = pm->get_playlist_for_id(s_uid);
|
||||||
|
if (data.indexOf("[lock]") != -1) {
|
||||||
|
if (_state == LOCKED) {
|
||||||
|
_state = NORMAL;
|
||||||
|
DEBUG("ControllerState is now UNLOCKED\n");
|
||||||
|
} else {
|
||||||
|
DEBUG("ControllerState is now LOCKING\n");
|
||||||
|
_state = LOCKING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pl==NULL) {
|
||||||
|
INFO("Could not find album for id '%s'.\n", s_uid.c_str());
|
||||||
|
send_controller_status();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int index;
|
||||||
|
if (data.indexOf("[advent]") != -1 && pl->is_fresh()) {
|
||||||
|
struct tm time;
|
||||||
|
getLocalTime(&time);
|
||||||
|
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
|
||||||
|
pl->advent_shuffle(time.tm_mday);
|
||||||
|
} else {
|
||||||
|
DEBUG("Album is in advent mode, but it isn't december (yet). Not playing.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (data.indexOf("[random]") != -1 && pl->is_fresh()) {
|
||||||
|
pl->shuffle();
|
||||||
|
} else if ((index=data.indexOf("[random:")) != -1 && pl->is_fresh()) {
|
||||||
|
String temp = data.substring(index + 8);
|
||||||
|
index = temp.indexOf("]");
|
||||||
|
TRACE("temp: %s, temp.substring(0, %d): %s\n", temp.c_str(), index, temp.substring(0, index).c_str());
|
||||||
|
if (index>0) {
|
||||||
|
uint8_t random_offset = temp.substring(0, index).toInt();
|
||||||
|
pl->shuffle(random_offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_state == LOCKED) {
|
||||||
|
DEBUG("ControllerState is LOCKED, ignoring card.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_state == LOCKING) {
|
||||||
|
_state = LOCKED;
|
||||||
|
DEBUG("ControllerState is now LOCKED.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
player->play(pl);
|
||||||
|
//send_playlist_manager_status();
|
||||||
|
send_controller_status();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String Controller::_read_rfid_data() {
|
String Controller::_read_rfid_data() {
|
||||||
_spi->select_rc522();
|
TRACE("_read_rfid_data() running...\n");
|
||||||
|
static MFRC522::MIFARE_Key keys[8] = {
|
||||||
|
{{0xd3, 0xf7, 0xd3, 0xf7, 0xd3, 0xf7}}, // D3 F7 D3 F7 D3 F7
|
||||||
|
{{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}, // FF FF FF FF FF FF = factory default
|
||||||
|
{{0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5}}, // A0 A1 A2 A3 A4 A5
|
||||||
|
{{0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5}}, // B0 B1 B2 B3 B4 B5
|
||||||
|
{{0x4d, 0x3a, 0x99, 0xc3, 0x51, 0xdd}}, // 4D 3A 99 C3 51 DD
|
||||||
|
{{0x1a, 0x98, 0x2c, 0x7e, 0x45, 0x9a}}, // 1A 98 2C 7E 45 9A
|
||||||
|
{{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}}, // AA BB CC DD EE FF
|
||||||
|
{{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}} // 00 00 00 00 00 00
|
||||||
|
};
|
||||||
|
SPIMaster::select_rc522();
|
||||||
DEBUG("Trying to read RFID data...\n");
|
DEBUG("Trying to read RFID data...\n");
|
||||||
|
|
||||||
String data = "";
|
String data = "";
|
||||||
MFRC522::MIFARE_Key key;
|
|
||||||
key.keyByte[0] = 0xD3;
|
|
||||||
key.keyByte[1] = 0xF7;
|
|
||||||
key.keyByte[2] = 0xD3;
|
|
||||||
key.keyByte[3] = 0xF7;
|
|
||||||
key.keyByte[4] = 0xD3;
|
|
||||||
key.keyByte[5] = 0xF7;
|
|
||||||
MFRC522::PICC_Type type = _rfid->PICC_GetType(_rfid->uid.sak);
|
MFRC522::PICC_Type type = _rfid->PICC_GetType(_rfid->uid.sak);
|
||||||
|
|
||||||
uint8_t sectors = 0;
|
uint8_t sectors = 0;
|
||||||
@@ -118,18 +199,27 @@ String Controller::_read_rfid_data() {
|
|||||||
case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break;
|
case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break;
|
||||||
default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str());
|
default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str());
|
||||||
}
|
}
|
||||||
|
sectors = 2; // Pretend we have only two sectors, so we read only sector #1.
|
||||||
|
int good_key_index = -1;
|
||||||
for (uint8_t sector=1; sector<sectors; sector++) {
|
for (uint8_t sector=1; sector<sectors; sector++) {
|
||||||
uint8_t blocks = (sector < 32) ? 4 : 16;
|
uint8_t blocks = (sector < 32) ? 4 : 16;
|
||||||
uint8_t block_offset = (sector < 32) ? sector * 4 : 128 + (sector - 32) * 16;
|
uint8_t block_offset = (sector < 32) ? sector * 4 : 128 + (sector - 32) * 16;
|
||||||
|
|
||||||
MFRC522::StatusCode status;
|
MFRC522::StatusCode status;
|
||||||
status = _rfid->PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block_offset, &key, &_rfid->uid);
|
|
||||||
if (status != MFRC522::STATUS_OK) {
|
|
||||||
DEBUG("PCD_Authenticate() for sector %d failed: %s\n", sector, String(_rfid->GetStatusCodeName(status)).c_str());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (int i=0; i<8; i++) {
|
||||||
|
MFRC522::MIFARE_Key *k = &keys[i];
|
||||||
|
TRACE("Trying MIFARE key %02X %02X %02X %02X %02X %02X...\n", k->keyByte[0], k->keyByte[1], k->keyByte[2], k->keyByte[3], k->keyByte[4], k->keyByte[5]);
|
||||||
|
status = _rfid->PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block_offset, k, &_rfid->uid);
|
||||||
|
if (status == MFRC522::STATUS_OK) {
|
||||||
|
TRACE("Authentication succeeded with key #%d\n", i);
|
||||||
|
good_key_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (good_key_index == -1) {
|
||||||
|
TRACE("Could not find a valid MIFARE key.\n");
|
||||||
|
} else {
|
||||||
for (uint8_t block=0; block<blocks-1; block++) {
|
for (uint8_t block=0; block<blocks-1; block++) {
|
||||||
byte buffer[18];
|
byte buffer[18];
|
||||||
uint8_t byte_count = 18;
|
uint8_t byte_count = 18;
|
||||||
@@ -143,11 +233,12 @@ String Controller::_read_rfid_data() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_rfid->PICC_HaltA();
|
_rfid->PICC_HaltA();
|
||||||
_rfid->PCD_StopCrypto1();
|
_rfid->PCD_StopCrypto1();
|
||||||
DEBUG("Data from RFID: %s", data.c_str());
|
DEBUG("Data from RFID: %s\n", data.c_str());
|
||||||
_spi->select_rc522(false);
|
SPIMaster::select_rc522(false);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +248,7 @@ void Controller::_check_serial() {
|
|||||||
Serial.printf("%c", c);
|
Serial.printf("%c", c);
|
||||||
if (c==10 || c==13) {
|
if (c==10 || c==13) {
|
||||||
if (_serial_buffer.length()>0) {
|
if (_serial_buffer.length()>0) {
|
||||||
_execute_serial_command(_serial_buffer);
|
process_message(_serial_buffer);
|
||||||
_serial_buffer = String();
|
_serial_buffer = String();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -166,60 +257,90 @@ void Controller::_check_serial() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_execute_serial_command(String cmd) {
|
bool Controller::process_message(String cmd) {
|
||||||
DEBUG("Executing command: %s\n", cmd.c_str());
|
DEBUG("Executing command: %s\n", cmd.c_str());
|
||||||
|
|
||||||
if (cmd.equals("ls")) {
|
if (cmd.startsWith("play ")) {
|
||||||
_execute_command_ls("/");
|
Playlist* p = pm->get_playlist_for_folder(cmd.substring(5));
|
||||||
} else if (cmd.startsWith("ls ")) {
|
player->play(p);
|
||||||
_execute_command_ls(cmd.substring(3));
|
|
||||||
} else if (cmd.equals("play")) {
|
} else if (cmd.equals("play")) {
|
||||||
_player->play_random_album();
|
player->play();
|
||||||
} else if (cmd.startsWith("play ")) {
|
|
||||||
_player->play_id(cmd.substring(5));
|
|
||||||
} else if (cmd.startsWith("sys ")) {
|
|
||||||
_player->play_system_sound(cmd.substring(4));
|
|
||||||
} else if (cmd.equals("stop")) {
|
} else if (cmd.equals("stop")) {
|
||||||
_player->stop();
|
player->stop();
|
||||||
} else if (cmd.equals("help")) {
|
} else if (cmd.equals("help")) {
|
||||||
_execute_command_help();
|
_execute_command_help();
|
||||||
} else if (cmd.equals("-")) {
|
} else if (cmd.equals("-")) {
|
||||||
_player->vol_down();
|
player->vol_down();
|
||||||
} else if (cmd.equals("+")) {
|
} else if (cmd.equals("+")) {
|
||||||
_player->vol_up();
|
player->vol_up();
|
||||||
} else if (cmd.equals("p")) {
|
} else if (cmd.startsWith("volume=")) {
|
||||||
_player->track_prev();
|
uint8_t vol = cmd.substring(7).toInt();
|
||||||
} else if (cmd.equals("n")) {
|
player->set_volume(vol);
|
||||||
_player->track_next();
|
} else if (cmd.equals("track_prev")) {
|
||||||
|
player->track_prev();
|
||||||
|
} else if (cmd.equals("track_next")) {
|
||||||
|
player->track_next();
|
||||||
|
} else if (cmd.startsWith("track=")) {
|
||||||
|
uint8_t track = cmd.substring(6).toInt();
|
||||||
|
player->set_track(track);
|
||||||
} else if (cmd.equals("ids")) {
|
} else if (cmd.equals("ids")) {
|
||||||
_execute_command_ids();
|
pm->dump_ids();
|
||||||
|
} else if (cmd.equals("reset_vs1053")) {
|
||||||
|
player->stop();
|
||||||
|
player->init();
|
||||||
|
} else if (cmd.equals("reboot")) {
|
||||||
|
ESP.restart();
|
||||||
|
} else if (cmd.startsWith("add_mapping=")) {
|
||||||
|
String rest = cmd.substring(12);
|
||||||
|
uint8_t idx = rest.indexOf('=');
|
||||||
|
String id = rest.substring(0, idx);
|
||||||
|
String folder = rest.substring(idx + 1);
|
||||||
|
pm->add_mapping(id, folder);
|
||||||
|
send_playlist_manager_status();
|
||||||
|
#ifdef OTA_UPDATE_URL
|
||||||
|
} else if (cmd.equals("update")) {
|
||||||
|
Updater::run();
|
||||||
|
#endif
|
||||||
|
} else if (cmd.startsWith("trace=")) {
|
||||||
|
int val = cmd.substring(6).toInt();
|
||||||
|
if (val==0) {
|
||||||
|
trace_enabled = false;
|
||||||
|
prefs.putBool("trace_enabled", false);
|
||||||
|
} else if (val==1) {
|
||||||
|
trace_enabled = true;
|
||||||
|
prefs.putBool("trace_enabled", true);
|
||||||
|
}
|
||||||
|
} else if (cmd.startsWith("debug=")) {
|
||||||
|
int val = cmd.substring(6).toInt();
|
||||||
|
if (val==0) {
|
||||||
|
debug_enabled = false;
|
||||||
|
prefs.putBool("debug_enabled", false);
|
||||||
|
} else if (val==1) {
|
||||||
|
debug_enabled = true;
|
||||||
|
prefs.putBool("debug_enabled", true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ERROR("Unknown command: %s\n", cmd.c_str());
|
ERROR("Unknown command: %s\n", cmd.c_str());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_execute_command_ls(String path) {
|
void Controller::_execute_command_ls(String path) {
|
||||||
INFO("Listing contents of %s:\n", path.c_str());
|
INFO("Listing contents of %s:\n", path.c_str());
|
||||||
std::list<String> files = _player->ls(path);
|
// TODO
|
||||||
for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) {
|
//std::list<String> files = player->ls(path);
|
||||||
INFO(" %s\n", (*it).c_str());
|
//for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) {
|
||||||
}
|
// INFO(" %s\n", (*it).c_str());
|
||||||
}
|
//}
|
||||||
|
|
||||||
void Controller::_execute_command_ids() {
|
|
||||||
for (std::map<String, String>::iterator it = _player->id_to_folder_map.begin(); it!=_player->id_to_folder_map.end(); ++it) {
|
|
||||||
INFO(" %s -> %s\n", it->first.c_str(), it->second.c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_execute_command_help() {
|
void Controller::_execute_command_help() {
|
||||||
INFO("Valid commands are:");
|
INFO("Valid commands are:");
|
||||||
INFO(" help - Displays this help\n");
|
INFO(" help - Displays this help\n");
|
||||||
INFO(" ls [dir] - Lists the contents of [dir] or, if not given, of /\n");
|
//INFO(" ls [dir] - Lists the contents of [dir] or, if not given, of /\n");
|
||||||
INFO(" ids - Lists all known ID-to-folder mappings\n");
|
INFO(" ids - Lists all known ID-to-folder mappings\n");
|
||||||
INFO(" play [id] - Plays the album with the given id\n");
|
INFO(" play [id] - Plays the album with the given id\n");
|
||||||
INFO(" sys [file]- Plays the file as system sound\n");
|
|
||||||
INFO(" stop - Stops playback\n");
|
INFO(" stop - Stops playback\n");
|
||||||
INFO(" - / + - Decrease or increase the volume\n");
|
INFO(" - / + - Decrease or increase the volume\n");
|
||||||
INFO(" p / n - Previous or next track\n");
|
INFO(" p / n - Previous or next track\n");
|
||||||
@@ -227,13 +348,21 @@ void Controller::_execute_command_help() {
|
|||||||
|
|
||||||
void Controller::_check_buttons() {
|
void Controller::_check_buttons() {
|
||||||
if (BTN_PREV() && _debounce_button(0)) {
|
if (BTN_PREV() && _debounce_button(0)) {
|
||||||
_player->track_prev();
|
if (_state == NORMAL) {
|
||||||
|
player->track_prev();
|
||||||
|
} else {
|
||||||
|
DEBUG("Ignoring btn_prev because state is LOCKED.\n");
|
||||||
|
}
|
||||||
} else if (BTN_VOL_UP() && _debounce_button(1)) {
|
} else if (BTN_VOL_UP() && _debounce_button(1)) {
|
||||||
_player->vol_up();
|
player->vol_up();
|
||||||
} else if (BTN_VOL_DOWN() && _debounce_button(2)) {
|
} else if (BTN_VOL_DOWN() && _debounce_button(2)) {
|
||||||
_player->vol_down();
|
player->vol_down();
|
||||||
} else if (BTN_NEXT() && _debounce_button(3)) {
|
} else if (BTN_NEXT() && _debounce_button(3)) {
|
||||||
_player->track_next();
|
if (_state == NORMAL) {
|
||||||
|
player->track_next();
|
||||||
|
} else {
|
||||||
|
DEBUG("Ignoring btn_next because state is LOCKED.\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,32 +375,89 @@ bool Controller::_debounce_button(uint8_t index) {
|
|||||||
_button_last_pressed_at[index] = millis();
|
_button_last_pressed_at[index] = millis();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
String Controller::get_status_json() {
|
|
||||||
String response = String("{");
|
String Controller::json() {
|
||||||
response.concat("\"state\": \"");
|
DynamicJsonDocument json(1024);
|
||||||
response.concat(_player->is_playing() ? "playing" : "idle");
|
json["_type"] = "controller";
|
||||||
response.concat("\", ");
|
switch(_state) {
|
||||||
if (_player->is_playing()) {
|
case LOCKED: json["state"] = "locked"; break;
|
||||||
response.concat("\"album\": \"");
|
case LOCKING: json["state"] = "locking"; break;
|
||||||
response.concat(_player->album());
|
case NORMAL: json["state"] = "normal"; break;
|
||||||
response.concat("\", \"track\": ");
|
|
||||||
response.concat(_player->track());
|
|
||||||
response.concat(", \"position\": ");
|
|
||||||
response.concat(_player->position());
|
|
||||||
response.concat(", ");
|
|
||||||
}
|
}
|
||||||
response.concat("\"volume\": ");
|
json["is_rfid_present"] = _rfid_present;
|
||||||
response.concat(_player->volume());
|
JsonObject rfid = json.createNestedObject("last_rfid");
|
||||||
response.concat(", \"volume_max\": ");
|
rfid["uid"] = _last_rfid_uid;
|
||||||
response.concat(VOLUME_MAX);
|
rfid["data"] = _last_rfid_data;
|
||||||
response.concat(", \"volume_min\": ");
|
json["uptime"] = millis() / 1000;
|
||||||
response.concat(VOLUME_MIN);
|
json["free_heap"] = ESP.getFreeHeap();
|
||||||
response.concat(", \"rfid_uid\": ");
|
JsonObject versions = json.createNestedObject("versions");
|
||||||
response.concat(String(_last_rfid_card_uid, HEX));
|
versions["ota"] = OTA_VERSION;
|
||||||
response.concat("}\n");
|
#ifdef VERSION
|
||||||
return response;
|
versions["release"] = VERSION;
|
||||||
|
#else
|
||||||
|
versions["release"] = "unknown";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
JsonObject wifi = json.createNestedObject("wifi");
|
||||||
|
if (WiFi.isConnected()) {
|
||||||
|
wifi["connected"] = true;
|
||||||
|
wifi["ssid"] = WiFi.SSID();
|
||||||
|
wifi["rssi"] = WiFi.RSSI();
|
||||||
|
} else {
|
||||||
|
wifi["connected"] = false;
|
||||||
|
}
|
||||||
|
return json.as<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_send_mqtt_report() {
|
void Controller::send_player_status() {
|
||||||
|
TRACE("In send_player_status()...\n");
|
||||||
|
|
||||||
|
if (_http_server->ws->count() > 0) {
|
||||||
|
_http_server->ws->textAll(player->json());
|
||||||
|
_http_server->ws->textAll(player->position_json());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::send_playlist_manager_status() {
|
||||||
|
TRACE("In send_playlist_manager_status()...\n");
|
||||||
|
if (_http_server->ws->count() > 0) {
|
||||||
|
_http_server->ws->textAll(pm->json());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::send_position() {
|
||||||
|
TRACE("In send_position()...\n");
|
||||||
|
if (_http_server->ws->count() > 0) {
|
||||||
|
_http_server->ws->textAll(player->position_json());
|
||||||
|
}
|
||||||
|
_last_position_info_at = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::send_controller_status() {
|
||||||
|
TRACE("In send_controller_status()...\n");
|
||||||
|
if (_http_server->ws->count() > 0) {
|
||||||
|
_http_server->ws->textAll(json());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::inform_new_client(AsyncWebSocketClient* client) {
|
||||||
|
String s;
|
||||||
|
s += pm->json();
|
||||||
|
s += '\n';
|
||||||
|
s += player->json();
|
||||||
|
s += '\n';
|
||||||
|
s += player->position_json();
|
||||||
|
s += '\n';
|
||||||
|
s += json();
|
||||||
|
client->text(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::queue_command(String s) {
|
||||||
|
DEBUG("Enqeueing command '%s'.\n", s.c_str());
|
||||||
|
_cmd_queue = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::update_playlist_manager() {
|
||||||
|
pm->scan_files();
|
||||||
|
send_playlist_manager_status();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include "data_sources.h"
|
||||||
|
|
||||||
|
void DataSource::skip_id3_tag() {
|
||||||
|
if (peek(0)=='I' && peek(1)=='D' && peek(2)=='3') {
|
||||||
|
DEBUG("ID3 tag found\n");
|
||||||
|
// Skip ID3 tag marker
|
||||||
|
read(); read(); read();
|
||||||
|
// Skip ID3 tag version
|
||||||
|
read(); read();
|
||||||
|
byte tags = read();
|
||||||
|
bool footer_present = tags & 0x10;
|
||||||
|
DEBUG("ID3 footer found: %d\n", footer_present);
|
||||||
|
uint32_t offset = 0;
|
||||||
|
for (byte i=0; i<4; i++) {
|
||||||
|
offset <<= 7;
|
||||||
|
offset |= (0x7F & read());
|
||||||
|
}
|
||||||
|
offset += 10;
|
||||||
|
if (footer_present) offset += 10;
|
||||||
|
DEBUG("ID3 tag length is %d bytes.\n", offset);
|
||||||
|
seek(offset);
|
||||||
|
} else {
|
||||||
|
DEBUG("No ID3 tag found\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////// SDDataSource //////////////
|
||||||
|
SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); }
|
||||||
|
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
|
||||||
|
size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); }
|
||||||
|
int SDDataSource::read() { return _file.read(); }
|
||||||
|
size_t SDDataSource::position() { return _file.position(); }
|
||||||
|
void SDDataSource::seek(size_t position) { _file.seek(position); }
|
||||||
|
size_t SDDataSource::size() { return _file.size(); }
|
||||||
|
void SDDataSource::close() { _file.close(); }
|
||||||
|
bool SDDataSource::usable() { return _file; }
|
||||||
|
int SDDataSource::peek(int offset) {
|
||||||
|
if (offset==0) return _file.peek();
|
||||||
|
size_t start_position = _file.position();
|
||||||
|
_file.seek(start_position + offset);
|
||||||
|
int result = _file.peek();
|
||||||
|
_file.seek(start_position);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////////// HTTPSDataSource //////////////
|
||||||
|
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
|
||||||
|
_url = url;
|
||||||
|
_init(url, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPSDataSource::_init(String url, uint32_t offset) {
|
||||||
|
_url = url;
|
||||||
|
_http = new HTTPClientWrapper();
|
||||||
|
if (!_http->get(url, offset)) return;
|
||||||
|
_position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTPSDataSource::~HTTPSDataSource() {
|
||||||
|
_http->close();
|
||||||
|
delete _http;
|
||||||
|
}
|
||||||
|
bool HTTPSDataSource::usable() { return _http; }
|
||||||
|
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
|
||||||
|
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
|
||||||
|
size_t HTTPSDataSource::position() { return _position; }
|
||||||
|
void HTTPSDataSource::seek(size_t position) { _http->close(); delete _http; _init(_url, position); }
|
||||||
|
size_t HTTPSDataSource::size() { return _http->getSize(); }
|
||||||
|
void HTTPSDataSource::close() { _http->close(); }
|
||||||
|
int HTTPSDataSource::peek(int offset) { return _http->peek(offset); }
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
#include "http_client_wrapper.h"
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
|
||||||
|
HTTPClientWrapper::HTTPClientWrapper() {
|
||||||
|
_buffer = new uint8_t[512];
|
||||||
|
_buffer_size = 512;
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTPClientWrapper::~HTTPClientWrapper() {
|
||||||
|
if (_http) {
|
||||||
|
_http->end();
|
||||||
|
delete _http;
|
||||||
|
}
|
||||||
|
delete _buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPClientWrapper::close() {
|
||||||
|
_http->end();
|
||||||
|
_connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HTTPClientWrapper::get(String url, uint32_t offset, uint8_t redirection_count) { return _request("GET", url, offset, redirection_count); }
|
||||||
|
bool HTTPClientWrapper::head(String url, uint32_t offset, uint8_t redirection_count) { return _request("HEAD", url, offset, redirection_count); }
|
||||||
|
|
||||||
|
bool HTTPClientWrapper::_request(String method, String url, uint32_t offset, uint8_t redirection_count) {
|
||||||
|
if (redirection_count>=5) return false;
|
||||||
|
|
||||||
|
//if (_http) delete _http;
|
||||||
|
|
||||||
|
DEBUG("%s %s\n", method.c_str(), url.c_str());
|
||||||
|
_http = new HTTPClient();
|
||||||
|
|
||||||
|
|
||||||
|
_http->setUserAgent("PodBox/0.1");
|
||||||
|
if (offset) {
|
||||||
|
String s = "bytes=";
|
||||||
|
s += offset;
|
||||||
|
s += "-";
|
||||||
|
_http->addHeader("Range: ", s);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* headers[] = {"Location", "Content-Type", "Transfer-Encoding"};
|
||||||
|
_http->collectHeaders(headers, 3);
|
||||||
|
bool result;
|
||||||
|
/*if (url.startsWith("https:")) {
|
||||||
|
BearSSL::WiFiClientSecure* client = new BearSSL::WiFiClientSecure();
|
||||||
|
client->setInsecure();
|
||||||
|
result = _http->begin(*client, url);
|
||||||
|
} else {
|
||||||
|
result = _http->begin(url);
|
||||||
|
}*/
|
||||||
|
result = _http->begin(url);
|
||||||
|
TRACE("HTTP->begin result: %d\n", result);
|
||||||
|
if (!result) return false;
|
||||||
|
|
||||||
|
int status = _http->sendRequest(method.c_str());
|
||||||
|
TRACE("HTTP Status code: %d\n", status);
|
||||||
|
if (status == HTTP_CODE_FOUND || status==HTTP_CODE_MOVED_PERMANENTLY || status==HTTP_CODE_TEMPORARY_REDIRECT) {
|
||||||
|
if (_http->hasHeader("Location")) {
|
||||||
|
url = _http->header("Location");
|
||||||
|
_http->end();
|
||||||
|
delete _http;
|
||||||
|
_http = NULL;
|
||||||
|
return _request(method, url, offset, redirection_count+1);
|
||||||
|
} else {
|
||||||
|
ERROR("Got redirection HTTP code, but no Location header.\n");
|
||||||
|
delete _http;
|
||||||
|
_http = NULL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (status != HTTP_CODE_OK) {
|
||||||
|
DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffer_position = 0;
|
||||||
|
_buffer_length = 0;
|
||||||
|
|
||||||
|
_connected = true;
|
||||||
|
_length = _http->getSize() + offset;
|
||||||
|
if (_http->hasHeader("Content-Type")) {
|
||||||
|
_content_type = _http->header("Content-Type");
|
||||||
|
} else {
|
||||||
|
_content_type = "";
|
||||||
|
}
|
||||||
|
_is_chunked = (_http->hasHeader("Transfer-Encoding")) && (_http->header("Transfer-Encoding").indexOf("chunked")!=-1);
|
||||||
|
_stream = _http->getStreamPtr();
|
||||||
|
if (_is_chunked) {
|
||||||
|
_read_next_chunk_header(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPClientWrapper::_read_next_chunk_header(bool first) {
|
||||||
|
if (!_connected) {
|
||||||
|
_chunk_length = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first) {
|
||||||
|
// read() returns an error if no bytes is available right at this moment.
|
||||||
|
// So we wait until 2 bytes are available or the connection times out.
|
||||||
|
while (_stream->connected() && !_stream->available()) { delay(1); }
|
||||||
|
int c1 = _stream->read();
|
||||||
|
while (_stream->connected() && !_stream->available()) { delay(1); }
|
||||||
|
int c2 = _stream->read();
|
||||||
|
if (c1==-1 || c2==-1) {
|
||||||
|
ERROR("Connection timeout.\n");
|
||||||
|
DEBUG("_stream.connected() returns %d\n", _stream->connected());
|
||||||
|
_chunk_length = 0;
|
||||||
|
_connected = false;
|
||||||
|
return;
|
||||||
|
} else if (c1!='\r' || c2!='\n') {
|
||||||
|
ERROR("Invalid chunk border found. Found: 0x%02X 0x%02X\n", c1, c2);
|
||||||
|
_chunk_length = 0;
|
||||||
|
_connected = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String chunk_header = _stream->readStringUntil('\n');
|
||||||
|
chunk_header.trim();
|
||||||
|
_chunk_length = strtol(chunk_header.c_str(), NULL, 16);
|
||||||
|
if (_chunk_length == 0) {
|
||||||
|
_connected = false;
|
||||||
|
TRACE("Empty chunk found -> EOF reached.\n");
|
||||||
|
} else {
|
||||||
|
TRACE("Chunk found. Length: %d\n", _chunk_length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t HTTPClientWrapper::_fill_buffer() {
|
||||||
|
if (!_connected) {
|
||||||
|
_buffer_position = 0;
|
||||||
|
_buffer_length = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t bytes_to_fill = _buffer_size;
|
||||||
|
uint16_t bytes_filled = 0;
|
||||||
|
while (bytes_to_fill > 0) {
|
||||||
|
uint16_t bytes_to_request = bytes_to_fill;
|
||||||
|
if (_is_chunked && _chunk_length < bytes_to_fill) bytes_to_request = _chunk_length;
|
||||||
|
TRACE("fill_buffer loop. _is_chunked: %d, _chunk_length: %d, _buffer_size: %d, bytes_filled: %d, bytes_to_fill: %d, bytes_to_request: %d", _is_chunked, _chunk_length, _buffer_size, bytes_filled, bytes_to_fill, bytes_to_request);
|
||||||
|
uint16_t result = _stream->readBytes(_buffer + bytes_filled, bytes_to_request);
|
||||||
|
TRACE(", result: %d\n", result);
|
||||||
|
bytes_filled += result;
|
||||||
|
bytes_to_fill -= result;
|
||||||
|
if (_is_chunked) {
|
||||||
|
_chunk_length -= result;
|
||||||
|
if (_chunk_length == 0) _read_next_chunk_header(false);
|
||||||
|
}
|
||||||
|
if (result == 0) {
|
||||||
|
_connected = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_buffer_position = 0;
|
||||||
|
_buffer_length = bytes_filled;
|
||||||
|
TRACE("Buffer filled. _buffer_length: %d\n", _buffer_length);
|
||||||
|
return bytes_filled;
|
||||||
|
}
|
||||||
|
|
||||||
|
String HTTPClientWrapper::getContentType() {
|
||||||
|
return _content_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
int HTTPClientWrapper::read() {
|
||||||
|
if (_buffer_position >= _buffer_length) _fill_buffer();
|
||||||
|
if (_buffer_position >= _buffer_length) return -1;
|
||||||
|
return _buffer[_buffer_position++];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t HTTPClientWrapper::read(uint8_t* dst, uint32_t len) {
|
||||||
|
TRACE("Reading %d bytes...\n", len);
|
||||||
|
uint32_t bytes_filled = 0;
|
||||||
|
while (1) {
|
||||||
|
if (_buffer_position >= _buffer_length) _fill_buffer();
|
||||||
|
if (_buffer_position >= _buffer_length) break;
|
||||||
|
|
||||||
|
uint32_t bytes_to_fill = len;
|
||||||
|
if (bytes_to_fill > _buffer_length - _buffer_position) bytes_to_fill = _buffer_length - _buffer_position;
|
||||||
|
|
||||||
|
TRACE("read_loop: _buffer_length=%d, _buffer_position=%d, len=%d, bytes_to_fill=%d\n", _buffer_length, _buffer_position, len, bytes_to_fill);
|
||||||
|
memcpy(dst + bytes_filled, _buffer + _buffer_position, bytes_to_fill);
|
||||||
|
_buffer_position += bytes_to_fill;
|
||||||
|
bytes_filled += bytes_to_fill;
|
||||||
|
len -= bytes_to_fill;
|
||||||
|
if (bytes_to_fill==0 || len==0) break;
|
||||||
|
}
|
||||||
|
return bytes_filled;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t HTTPClientWrapper::getSize() {return _length; }
|
||||||
|
|
||||||
|
String HTTPClientWrapper::readUntil(String sep) {
|
||||||
|
String result = "";
|
||||||
|
while(true) {
|
||||||
|
int i = read();
|
||||||
|
if (i==-1) break;
|
||||||
|
char c = i;
|
||||||
|
if (sep.indexOf(c)!=-1) {
|
||||||
|
// separator
|
||||||
|
if (result.length()>0) break;
|
||||||
|
} else {
|
||||||
|
result.concat(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String HTTPClientWrapper::readLine() {
|
||||||
|
return readUntil("\n\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
int HTTPClientWrapper::peek(int offset) {
|
||||||
|
if (_buffer_position >= _buffer_length) _fill_buffer();
|
||||||
|
|
||||||
|
if (_buffer_position + offset < 0 || _buffer_position + offset >= _buffer_length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return _buffer[_buffer_position + offset];
|
||||||
|
}
|
||||||
+98
-51
@@ -1,32 +1,61 @@
|
|||||||
/*
|
|
||||||
#include "http_server.h"
|
#include "http_server.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "spi_master.h"
|
||||||
|
#include <ESPmDNS.h>
|
||||||
|
|
||||||
HTTPServer::HTTPServer(Player* p, Controller* c) {
|
HTTPServer::HTTPServer(Player* p, Controller* c) {
|
||||||
_player = p;
|
_player = p;
|
||||||
_controller = c;
|
_controller = c;
|
||||||
_http_server = new ESP8266WebServer(80);
|
_server = new AsyncWebServer(80);
|
||||||
|
ws = new AsyncWebSocket("/ws");
|
||||||
|
_server->addHandler(ws);
|
||||||
|
ws->onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){this->_onEvent(server, client, type, arg, data, len);});
|
||||||
|
|
||||||
//_http_server->onFileUpload([&]() { _handle_upload(); yield();});
|
_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
_http_server->on("/upload", HTTP_POST, [&]() {
|
req->send_P(200, "text/html", file_index_html_start, file_index_html_size);
|
||||||
_http_server->sendHeader("Connection", "close");
|
|
||||||
_http_server->send(200, "text/plain", "OK");
|
|
||||||
}, [&]() {
|
|
||||||
_handle_upload();
|
|
||||||
yield();
|
|
||||||
});
|
});
|
||||||
_http_server->on("/", HTTP_GET, [&](){ _handle_index(); });
|
_server->on("/timezone.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
_http_server->on("/status", HTTP_GET, [&](){ _handle_status(); });
|
AsyncWebServerResponse* res = req->beginResponse_P(200, "application/json", file_timezones_json_start, file_timezones_json_size);
|
||||||
_http_server->begin();
|
res->addHeader("Content-Encoding", "gzip");
|
||||||
|
req->send(res);
|
||||||
|
});
|
||||||
|
_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200);
|
||||||
|
}, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||||
|
this->_handle_upload(request, filename, index, data, len, final);
|
||||||
|
});
|
||||||
|
_server->on("/_mapping.txt", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "text/plain", _controller->pm->create_mapping_txt());
|
||||||
|
});
|
||||||
|
_server->on("/player.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "application/json", _controller->player->json());
|
||||||
|
});
|
||||||
|
_server->on("/playlist_manager.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "application/json", _controller->pm->json());
|
||||||
|
});
|
||||||
|
_server->on("/controller.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "application/json", _controller->json());
|
||||||
|
});
|
||||||
|
_server->on("/position.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "application/json", _controller->player->position_json());
|
||||||
|
});
|
||||||
|
_server->on("/cmd", HTTP_POST, [&](AsyncWebServerRequest *req) {
|
||||||
|
if (req->hasParam("cmd", true)) {
|
||||||
|
_controller->queue_command(req->getParam("cmd", true)->value());
|
||||||
|
req->send(200);
|
||||||
|
} else {
|
||||||
|
req->send(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_server->begin();
|
||||||
MDNS.addService("http", "tcp", 80);
|
MDNS.addService("http", "tcp", 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPServer::_handle_upload() {
|
void HTTPServer::_handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) {
|
||||||
// https://www.gnu.org/software/tar/manual/html_node/Standard.html
|
// https://www.gnu.org/software/tar/manual/html_node/Standard.html
|
||||||
// https://www.mkssoftware.com/docs/man4/tar.4.asp
|
// https://www.mkssoftware.com/docs/man4/tar.4.asp
|
||||||
HTTPUpload* upload = &_http_server->upload();
|
|
||||||
DEBUG("_handle_upload Status: %d, length: %d\n", upload->status, upload->currentSize);
|
|
||||||
|
|
||||||
if (upload->status == UPLOAD_FILE_START) {
|
if (index == 0) { // Starting upload
|
||||||
_chunk = new uint8_t[512];
|
_chunk = new uint8_t[512];
|
||||||
_chunk_length = 0;
|
_chunk_length = 0;
|
||||||
_upload_position = 0;
|
_upload_position = 0;
|
||||||
@@ -35,26 +64,20 @@ void HTTPServer::_handle_upload() {
|
|||||||
_need_header = true;
|
_need_header = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (upload->status == UPLOAD_FILE_END || upload->status == UPLOAD_FILE_ABORTED) {
|
|
||||||
// Close the file
|
|
||||||
delete _chunk;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t upload_offset = 0;
|
uint32_t upload_offset = 0;
|
||||||
while (upload_offset < upload->currentSize) {
|
while (upload_offset < len) {
|
||||||
// Load a chunk
|
// Load a chunk
|
||||||
if (_chunk_length < 512 && upload->currentSize > upload_offset) {
|
if (_chunk_length < 512 && len > upload_offset) {
|
||||||
uint16_t needed = 512 - _chunk_length;
|
uint16_t needed = 512 - _chunk_length;
|
||||||
if (needed > upload->currentSize - upload_offset) needed = upload->currentSize - upload_offset;
|
if (needed > len - upload_offset) needed = len - upload_offset;
|
||||||
memcpy(_chunk + _chunk_length, upload->buf + upload_offset, needed);
|
memcpy(_chunk + _chunk_length, data + upload_offset, needed);
|
||||||
_chunk_length += needed;
|
_chunk_length += needed;
|
||||||
upload_offset += needed;
|
upload_offset += needed;
|
||||||
_upload_position += needed;
|
_upload_position += needed;
|
||||||
|
|
||||||
if (_chunk_length == 512) {
|
if (_chunk_length == 512) {
|
||||||
// Process chunk
|
// Process chunk
|
||||||
DEBUG("Chunk.\n");
|
DEBUG(".");
|
||||||
if (_need_header) {
|
if (_need_header) {
|
||||||
if (_chunk[257]=='u'&&_chunk[258]=='s'&&_chunk[259]=='t'&&_chunk[260]=='a'&&_chunk[261]=='r') {
|
if (_chunk[257]=='u'&&_chunk[258]=='s'&&_chunk[259]=='t'&&_chunk[260]=='a'&&_chunk[261]=='r') {
|
||||||
DEBUG("It is a valid header, starting at 0x%X!\n", _upload_position-512);
|
DEBUG("It is a valid header, starting at 0x%X!\n", _upload_position-512);
|
||||||
@@ -70,9 +93,26 @@ void HTTPServer::_handle_upload() {
|
|||||||
DEBUG("filesize: %d\n", _file_size);
|
DEBUG("filesize: %d\n", _file_size);
|
||||||
uint8_t type = _chunk[156] - '0';
|
uint8_t type = _chunk[156] - '0';
|
||||||
if (type==0) {
|
if (type==0) {
|
||||||
DEBUG("Is a file.\n");
|
String path = "/";
|
||||||
|
path += filename;
|
||||||
|
DEBUG("Opening file %s\n", path.c_str());
|
||||||
|
uint8_t state = SPIMaster::state;
|
||||||
|
SPIMaster::disable();
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
// Better safe than sorry. ;-)
|
||||||
|
_upload_file.close();
|
||||||
|
_upload_file = SD.open(path, "w");
|
||||||
|
SPIMaster::set_state(state);
|
||||||
} else if (type==5) {
|
} else if (type==5) {
|
||||||
DEBUG("Is a directory.\n");
|
String dirname = "/";
|
||||||
|
dirname += filename;
|
||||||
|
dirname.remove(dirname.length()-1);
|
||||||
|
uint8_t state = SPIMaster::state;
|
||||||
|
SPIMaster::disable();
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
bool res = SD.mkdir(dirname);
|
||||||
|
SPIMaster::set_state(state);
|
||||||
|
DEBUG("Creating folder '%s' returned %d.\n", dirname.c_str(), res);
|
||||||
} else {
|
} else {
|
||||||
ERROR("Unknown file type %d\n", type);
|
ERROR("Unknown file type %d\n", type);
|
||||||
}
|
}
|
||||||
@@ -87,39 +127,46 @@ void HTTPServer::_handle_upload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
uint16_t bytes_to_write = _file_size - _file_size_done;
|
uint32_t bytes_to_write = _file_size - _file_size_done;
|
||||||
if (bytes_to_write > 512) bytes_to_write=512;
|
if (bytes_to_write > 512) bytes_to_write=512;
|
||||||
// Write bytes...
|
uint8_t state = SPIMaster::state;
|
||||||
|
SPIMaster::disable();
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
_upload_file.write(_chunk, bytes_to_write);
|
||||||
_file_size_done += bytes_to_write;
|
_file_size_done += bytes_to_write;
|
||||||
if (_file_size_done >= _file_size) _need_header = true;
|
if (_file_size_done >= _file_size) {
|
||||||
|
_upload_file.close();
|
||||||
|
_need_header = true;
|
||||||
|
}
|
||||||
|
SPIMaster::set_state(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
_chunk_length = 0;
|
_chunk_length = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (final == true) {
|
||||||
|
uint8_t state = SPIMaster::state;
|
||||||
|
SPIMaster::disable();
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
_upload_file.close();
|
||||||
|
SPIMaster::set_state(state);
|
||||||
|
delete _chunk;
|
||||||
|
_controller->update_playlist_manager();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPServer::_handle_index() {
|
void HTTPServer::_onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) {
|
||||||
String response = String("<html><head><title>ESMP3</title><script>function play_album(e) {}</script></head><body>");
|
if (type==WS_EVT_CONNECT) {
|
||||||
response.concat("Albums on SD card:<table>");
|
_controller->inform_new_client(client);
|
||||||
std::list<String> files = _player->ls("/", false, true, false);
|
} else if (type==WS_EVT_DATA) {
|
||||||
for(std::list<String>::iterator it=files.begin(); it!=files.end(); it++) {
|
AwsFrameInfo* info = (AwsFrameInfo*) arg;
|
||||||
response.concat("<tr><td>");
|
if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) {
|
||||||
response.concat(*it);
|
data[len]='\0';
|
||||||
response.concat("</td><td><a href='#' onclick='play_album();'>Play</a></td></tr>\n");
|
DEBUG("Received ws message: %s\n", (char*)data);
|
||||||
|
_controller->queue_command((char*)data);
|
||||||
}
|
}
|
||||||
response.concat("</table></body></html>");
|
|
||||||
_http_server->send(200, "text/html", response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPServer::_handle_status() {
|
|
||||||
_http_server->send(200, "application/json", _controller->get_status_json());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPServer::loop() {
|
|
||||||
_http_server->handleClient();
|
|
||||||
MDNS.update();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
+92
-37
@@ -2,74 +2,136 @@
|
|||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include <WiFiMulti.h>
|
||||||
|
#include <ESPmDNS.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include "main.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include "http_server.h"
|
#include "http_server.h"
|
||||||
#include "mqtt_client.h"
|
#include "playlist_manager.h"
|
||||||
#include <ESP8266FtpServer.h>
|
#include "updater.h"
|
||||||
|
|
||||||
Controller* controller;
|
Controller* controller;
|
||||||
Player* player;
|
Player* player;
|
||||||
//HTTPServer* http_server;
|
PlaylistManager* pm;
|
||||||
FtpServer* ftp_server;
|
HTTPServer* http_server;
|
||||||
MQTTClient* mqtt_client;
|
|
||||||
unsigned long last_mqtt_report = 0;
|
uint8_t SPIMaster::state = 0;
|
||||||
|
|
||||||
|
bool debug_enabled = true;
|
||||||
|
bool trace_enabled = false;
|
||||||
|
Preferences prefs;
|
||||||
|
|
||||||
|
void wifi_connect() {
|
||||||
|
INFO("Connecting to WiFi...\n");
|
||||||
|
WiFiMulti wifi;
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
if (SD.exists("/_wifis.txt")) {
|
||||||
|
DEBUG("Reading /_wifis.txt\n");
|
||||||
|
File f = SD.open("/_wifis.txt", "r");
|
||||||
|
while (String line = f.readStringUntil('\n')) {
|
||||||
|
if (line.length()==0) {
|
||||||
|
break;
|
||||||
|
} else if (line.startsWith("#") || line.indexOf('=')==-1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String ssid = line.substring(0, line.indexOf('='));
|
||||||
|
String pass = line.substring(line.indexOf('=')+1);
|
||||||
|
wifi.addAP(ssid.c_str(), pass.c_str());
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
} else {
|
||||||
|
File f = SD.open("/_wifis.txt", "w");
|
||||||
|
f.print("# WiFi definitions. Syntax: <SSID>=<PASS>. Lines starting with # are ignored. Example:\n# My WiFi=VerySecretPassword\n");
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
|
||||||
|
#if defined(WIFI_SSID) and defined(WIFI_PASS)
|
||||||
|
wifi.addAP(WIFI_SSID, WIFI_PASS);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (wifi.run() == WL_CONNECTED) {
|
||||||
|
DEBUG("Connected to WiFi \"%s\".\n", WiFi.SSID().c_str());
|
||||||
|
} else {
|
||||||
|
DEBUG("No WiFi connection!\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
delay(500);
|
// Small delay to give the Serial console a bit of time to connect.
|
||||||
Serial.begin(74880);
|
delay(1000);
|
||||||
|
Serial.begin(115200);
|
||||||
Serial.println("Starting...");
|
Serial.println("Starting...");
|
||||||
Serial.println("Started.");
|
Serial.println("Started.");
|
||||||
INFO("Starting.\n");
|
INFO("Starting.\n");
|
||||||
#ifdef VERSION
|
#ifdef VERSION
|
||||||
INFO("ESMP3 version %s\n", VERSION);
|
INFO("ESMP3 version %s (OTA_VERSION %d)\n", VERSION, OTA_VERSION);
|
||||||
#else
|
#else
|
||||||
INFO("ESMP3, version unknown\n");
|
INFO("ESMP3, version unknown (OTA_VERSION %d)\n", OTA_VERSION);
|
||||||
#endif
|
#endif
|
||||||
INFO("Initializing...\n");
|
INFO("Initializing...\n");
|
||||||
|
prefs.begin("esmp3");
|
||||||
|
debug_enabled = prefs.getBool("debug_enabled", true);
|
||||||
|
trace_enabled = prefs.getBool("trace_enabled", false);
|
||||||
|
|
||||||
|
PIN_SPEAKER_L_SETUP();
|
||||||
|
PIN_SPEAKER_R_SETUP();
|
||||||
|
PIN_SPEAKER_L(LOW);
|
||||||
|
PIN_SPEAKER_R(LOW);
|
||||||
|
|
||||||
DEBUG("Setting up SPI...\n");
|
DEBUG("Setting up SPI...\n");
|
||||||
SPI.begin();
|
SPI.begin();
|
||||||
SPI.setHwCs(false);
|
SPI.setHwCs(false);
|
||||||
|
SPIMaster::init();
|
||||||
SPIMaster* spi = new SPIMaster();
|
SPIMaster* spi = new SPIMaster();
|
||||||
INFO("SPI initialized.\n");
|
INFO("SPI initialized.\n");
|
||||||
|
|
||||||
DEBUG("Setting up SD card...\n");
|
DEBUG("Setting up SD card...\n");
|
||||||
spi->select_sd();
|
spi->select_sd();
|
||||||
if (SD.begin(42)) {
|
if (SD.begin(42, SPI, 25000000)) {
|
||||||
INFO("SD card initialized.\n");
|
INFO("SD card initialized.\n");
|
||||||
} else {
|
} else {
|
||||||
ERROR("Could not initialize SD card.\n");
|
ERROR("Could not initialize SD card.\n");
|
||||||
}
|
}
|
||||||
spi->select_sd(false);
|
spi->select_sd(false);
|
||||||
|
|
||||||
|
DEBUG("Initializing PlaylistManager...\n");
|
||||||
|
pm = new PlaylistManager();
|
||||||
|
|
||||||
DEBUG("Initializing Player and Controller...\n");
|
DEBUG("Initializing Player and Controller...\n");
|
||||||
player = new Player(spi);
|
player = new Player(spi);
|
||||||
controller = new Controller(player, spi);
|
controller = new Controller(player, pm);
|
||||||
INFO("Player and controller initialized.\n");
|
INFO("Player and controller initialized.\n");
|
||||||
|
|
||||||
DEBUG("Connecting to wifi \"%s\"...\n", WIFI_SSID);
|
wifi_connect();
|
||||||
WiFi.mode(WIFI_AP_STA);
|
|
||||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
MDNS.begin("esmp3");
|
||||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
|
||||||
ERROR("Could not connect to Wifi. Rebooting.");
|
DEBUG("Setting up HTTP server...\n");
|
||||||
delay(1000);
|
http_server = new HTTPServer(player, controller);
|
||||||
ESP.restart();
|
controller->register_http_server(http_server);
|
||||||
|
|
||||||
|
DEBUG("Starting NTP client...\n");
|
||||||
|
// Taken from https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h
|
||||||
|
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "europe.pool.ntp.org");
|
||||||
|
struct tm time;
|
||||||
|
if (getLocalTime(&time, 10000)) {
|
||||||
|
char buffer[100];
|
||||||
|
strftime(buffer, 100, "%Y-%m-%d %H:%M:%S", &time);
|
||||||
|
DEBUG("Got time: %s\n", buffer);
|
||||||
|
} else {
|
||||||
|
INFO("Could not fetch current time via NTP.\n");
|
||||||
}
|
}
|
||||||
INFO("WiFi connected.\n");
|
|
||||||
|
|
||||||
mqtt_client = new MQTTClient();
|
#ifdef VERSION
|
||||||
//MDNS.begin("esmp3");
|
INFO("ESMP3 version %s (OTA_VERSION %d)\n", VERSION, OTA_VERSION);
|
||||||
|
#else
|
||||||
controller->set_mqtt_client(mqtt_client);
|
INFO("ESMP3, version unknown (OTA_VERSION %d)\n", OTA_VERSION);
|
||||||
|
#endif
|
||||||
DEBUG("Setting up WiFi and web server...\n");
|
|
||||||
//http_server = new HTTPServer(player, controller);
|
|
||||||
|
|
||||||
ftp_server = new FtpServer();
|
|
||||||
ftp_server->begin("user", "pass");
|
|
||||||
|
|
||||||
INFO("Initialization completed.\n");
|
INFO("Initialization completed.\n");
|
||||||
}
|
}
|
||||||
@@ -79,11 +141,4 @@ void loop() {
|
|||||||
if (more_data_needed) return;
|
if (more_data_needed) return;
|
||||||
|
|
||||||
controller->loop();
|
controller->loop();
|
||||||
//http_server->loop();
|
|
||||||
ftp_server->handleFTP();
|
|
||||||
mqtt_client->loop();
|
|
||||||
if ((last_mqtt_report + 10000 < millis()) || last_mqtt_report > millis()) {
|
|
||||||
last_mqtt_report = millis();
|
|
||||||
mqtt_client->publish_status(controller->get_status_json());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
#include "mqtt_client.h"
|
|
||||||
|
|
||||||
MQTTClient::MQTTClient() {
|
|
||||||
_wifi_client = new WiFiClient();
|
|
||||||
_mqtt = new PubSubClient(*_wifi_client);
|
|
||||||
|
|
||||||
_mqtt->setServer("10.10.2.1", 1883);
|
|
||||||
_last_reconnect_attempt = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTClient::loop() {
|
|
||||||
if (!_mqtt->connected()) {
|
|
||||||
unsigned long now = millis();
|
|
||||||
if (now - _last_reconnect_attempt > 5000) {
|
|
||||||
DEBUG("Connecting to MQTT server...\n");
|
|
||||||
_last_reconnect_attempt = now;
|
|
||||||
_reconnect();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_mqtt->loop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTClient::_reconnect() {
|
|
||||||
_mqtt->connect("esmp3", "esmp3", "pairugp598hpknfblaaagvaie58oh", "esmp3/state", 0, true, "OFFLINE");
|
|
||||||
_mqtt->publish("esmp3/state", "ONLINE", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTClient::publish_status(String s) {
|
|
||||||
_mqtt->publish("esmp3/status", s.c_str(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MQTTClient::publish_rfid_uid(uint32_t uid) {
|
|
||||||
String s = String(uid, HEX);
|
|
||||||
_mqtt->publish("esmp3/rfid_uid", s.c_str(), true);
|
|
||||||
}
|
|
||||||
+101
-269
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
//Player::_spi_settings
|
//Player::_spi_settings
|
||||||
|
|
||||||
@@ -9,15 +10,15 @@ Player::Player(SPIMaster* s) {
|
|||||||
_spi = s;
|
_spi = s;
|
||||||
PIN_VS1053_XRESET_SETUP();
|
PIN_VS1053_XRESET_SETUP();
|
||||||
PIN_VS1053_XRESET(HIGH);
|
PIN_VS1053_XRESET(HIGH);
|
||||||
PIN_SPEAKER_L_SETUP();
|
|
||||||
PIN_SPEAKER_R_SETUP();
|
|
||||||
_speaker_off();
|
_speaker_off();
|
||||||
_spi->disable();
|
_spi->disable();
|
||||||
PIN_VS1053_DREQ_SETUP();
|
PIN_VS1053_DREQ_SETUP();
|
||||||
|
|
||||||
_fill_id_to_folder_map();
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
_init();
|
void Player::register_controller(Controller* c) {
|
||||||
|
_controller = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::_reset() {
|
void Player::_reset() {
|
||||||
@@ -29,7 +30,7 @@ void Player::_reset() {
|
|||||||
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
|
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::_init() {
|
void Player::init() {
|
||||||
DEBUG("Resetting VS1053...\n");
|
DEBUG("Resetting VS1053...\n");
|
||||||
_reset();
|
_reset();
|
||||||
|
|
||||||
@@ -70,14 +71,6 @@ void Player::_init() {
|
|||||||
|
|
||||||
INFO("VS1053 initialization completed.\n");
|
INFO("VS1053 initialization completed.\n");
|
||||||
|
|
||||||
INFO("Checking system sounds...\n");
|
|
||||||
_spi->select_sd();
|
|
||||||
_check_system_sound("no_prev_song.mp3");
|
|
||||||
_check_system_sound("no_next_song.mp3");
|
|
||||||
_check_system_sound("volume_max.mp3");
|
|
||||||
_check_system_sound("volume_min.mp3");
|
|
||||||
_spi->select_sd(false);
|
|
||||||
|
|
||||||
_state = idle;
|
_state = idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +94,7 @@ void Player::_sleep() {
|
|||||||
_write_control_register(SCI_AUDATA, 0x0010);
|
_write_control_register(SCI_AUDATA, 0x0010);
|
||||||
set_volume(0, false);
|
set_volume(0, false);
|
||||||
_state = sleeping;
|
_state = sleeping;
|
||||||
|
TRACE("VS1053 is sleeping now.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::_wakeup() {
|
void Player::_wakeup() {
|
||||||
@@ -149,15 +143,6 @@ void Player::_record() {
|
|||||||
_state = recording;
|
_state = recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::_check_system_sound(String filename) {
|
|
||||||
String path = String("/system/") + filename;
|
|
||||||
if (!SD.exists(path)) {
|
|
||||||
ERROR("System sound %s is missing on the sd card!\n", path.c_str());
|
|
||||||
} else {
|
|
||||||
DEBUG("%s found.\n", path.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Player::_wait() {
|
inline void Player::_wait() {
|
||||||
while(!PIN_VS1053_DREQ());
|
while(!PIN_VS1053_DREQ());
|
||||||
}
|
}
|
||||||
@@ -476,15 +461,17 @@ void Player::set_volume(uint8_t vol, bool save) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::vol_up() {
|
void Player::vol_up() {
|
||||||
if (_volume == VOLUME_MAX) play_system_sound("volume_max.mp3");
|
if (!is_playing()) return;
|
||||||
else if (_volume + VOLUME_STEP > VOLUME_MAX) set_volume(VOLUME_MAX);
|
uint16_t vol = _volume + VOLUME_STEP;
|
||||||
else set_volume(_volume + VOLUME_STEP);
|
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
|
||||||
|
set_volume(vol);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::vol_down() {
|
void Player::vol_down() {
|
||||||
if (_volume >= VOLUME_MIN + VOLUME_STEP) set_volume(_volume - VOLUME_STEP);
|
if (!is_playing()) return;
|
||||||
else if (_volume == VOLUME_MIN) play_system_sound("volume_min.mp3");
|
int16_t vol = _volume - VOLUME_STEP;
|
||||||
else set_volume(VOLUME_MIN);
|
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
|
||||||
|
set_volume(vol);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::_mute() {
|
void Player::_mute() {
|
||||||
@@ -501,228 +488,76 @@ void Player::_unmute() {
|
|||||||
|
|
||||||
void Player::track_next() {
|
void Player::track_next() {
|
||||||
if (_state != playing) return;
|
if (_state != playing) return;
|
||||||
if (_playing_index + 1 >= _playing_album_songs) {
|
if (!_current_playlist->has_track_next()) {
|
||||||
play_system_sound("no_next_song.mp3");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stop();
|
stop();
|
||||||
play_song(_playing_album, _playing_index + 1);
|
_current_playlist->track_next();
|
||||||
|
play();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::track_prev() {
|
void Player::track_prev() {
|
||||||
if (_state != playing) return;
|
if (_state != playing) return;
|
||||||
if (_current_play_position > 100000) {
|
if (_current_play_position > 100000) {
|
||||||
stop();
|
stop();
|
||||||
play_song(_playing_album, _playing_index);
|
_current_playlist->track_restart();
|
||||||
|
play();
|
||||||
} else {
|
} else {
|
||||||
if (_playing_index == 0) {
|
if (!_current_playlist->has_track_prev()) {
|
||||||
play_system_sound("no_prev_song.mp3");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stop();
|
stop();
|
||||||
play_song(_playing_album, _playing_index - 1);
|
_current_playlist->track_prev();
|
||||||
|
play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Player::set_track(uint8_t id) {
|
||||||
|
stop();
|
||||||
|
_current_playlist->set_track(id);
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
|
||||||
bool Player::is_playing() {
|
bool Player::is_playing() {
|
||||||
return _state == playing;
|
return _state == playing;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::list<String> Player::ls(String path, bool withFiles, bool withDirs, bool withHidden) {
|
bool Player::play(Playlist* p) {
|
||||||
_spi->select_sd();
|
_current_playlist = p;
|
||||||
std::list<String> result;
|
return play();
|
||||||
if (!SD.exists(path)) return result;
|
|
||||||
File dir = SD.open(path);
|
|
||||||
File entry;
|
|
||||||
while (entry = dir.openNextFile()) {
|
|
||||||
if (!withDirs && entry.isDirectory()) continue;
|
|
||||||
if (!withFiles && !entry.isDirectory()) continue;
|
|
||||||
String filename = entry.name();
|
|
||||||
if (!withHidden && filename.startsWith(".")) continue;
|
|
||||||
if (entry.isDirectory()) filename.concat("/");
|
|
||||||
result.push_back(filename);
|
|
||||||
}
|
|
||||||
_spi->select_sd(false);
|
|
||||||
result.sort();
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String Player::_find_album_dir(String id) {
|
bool Player::play() {
|
||||||
_spi->select_sd();
|
|
||||||
if (id.endsWith("/")) id = id.substring(0, id.length() - 1);
|
|
||||||
String id_with_divider = id + " - ";
|
|
||||||
File root = SD.open("/");
|
|
||||||
File entry;
|
|
||||||
String result = String("");
|
|
||||||
while ((result.length()==0) && (entry = root.openNextFile())) {
|
|
||||||
String name = entry.name() + 1;
|
|
||||||
TRACE("Checking if '%s' startsWith '%s'...\n", name.c_str(), id.c_str());
|
|
||||||
if (entry.isDirectory() && (name.startsWith(id_with_divider) || name.equals(id))) {
|
|
||||||
result = name;
|
|
||||||
}
|
|
||||||
entry.close();
|
|
||||||
}
|
|
||||||
root.close();
|
|
||||||
_spi->select_sd(false);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::list<String> Player::_files_in_dir(String path) {
|
|
||||||
_spi->select_sd();
|
|
||||||
TRACE("Examining folder %s...\n", path.c_str());
|
|
||||||
if (!path.startsWith("/")) path = String("/") + path;
|
|
||||||
//if (!path.endsWith("/")) path.concat("/");
|
|
||||||
std::list<String> result;
|
|
||||||
if (!SD.exists(path)) {
|
|
||||||
DEBUG("Could not open path '%s'.\n", path.c_str());
|
|
||||||
_spi->select_sd(false);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
File dir = SD.open(path);
|
|
||||||
File entry;
|
|
||||||
while (entry = dir.openNextFile()) {
|
|
||||||
String filename = entry.name();
|
|
||||||
filename = filename.substring(path.length() + 1);
|
|
||||||
if (!entry.isDirectory() &&
|
|
||||||
!filename.startsWith(".") &&
|
|
||||||
( filename.endsWith(".mp3") ||
|
|
||||||
filename.endsWith(".ogg") ||
|
|
||||||
filename.endsWith(".wma") ||
|
|
||||||
filename.endsWith(".mp4") ||
|
|
||||||
filename.endsWith(".mpa"))) {
|
|
||||||
TRACE(" Adding entry %s\n", entry.name());
|
|
||||||
result.push_back(entry.name());
|
|
||||||
} else {
|
|
||||||
TRACE(" Ignoring entry %s\n", filename.c_str());
|
|
||||||
}
|
|
||||||
entry.close();
|
|
||||||
}
|
|
||||||
dir.close();
|
|
||||||
_spi->select_sd(false);
|
|
||||||
result.sort();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_fill_id_to_folder_map() {
|
|
||||||
DEBUG("_fill_id_to_folder_map() running...");
|
|
||||||
_spi->select_sd();
|
|
||||||
File root = SD.open("/");
|
|
||||||
File entry;
|
|
||||||
while (entry = root.openNextFile()) {
|
|
||||||
String foldername = entry.name();
|
|
||||||
// Remove trailing slash
|
|
||||||
foldername.remove(foldername.length());
|
|
||||||
TRACE("Looking at %s...\n", foldername.c_str());
|
|
||||||
if (!entry.isDirectory() || foldername.startsWith("/.")) continue;
|
|
||||||
if (!SD.exists(foldername + "/ids.txt")) {
|
|
||||||
TRACE("Folder %s does not contain ids.txt -> ignoring\n", foldername.c_str());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
TRACE("Reading contents of %s...\n", (foldername + "/ids.txt").c_str());
|
|
||||||
File f = SD.open(foldername + "/ids.txt");
|
|
||||||
String buffer = "";
|
|
||||||
while (f.available()) {
|
|
||||||
char c = f.read();
|
|
||||||
if (c=='\n' || c=='\r') {
|
|
||||||
if (buffer.length() > 0) {
|
|
||||||
id_to_folder_map[buffer] = foldername;
|
|
||||||
DEBUG("Adding mapping '%s'=>'%s'\n", buffer.c_str(), foldername.c_str());
|
|
||||||
buffer = "";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buffer.concat(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.close();
|
|
||||||
|
|
||||||
if (buffer.length() > 0) {
|
|
||||||
id_to_folder_map[buffer] = foldername;
|
|
||||||
DEBUG("Adding mapping '%s'=>'%s'\n", buffer.c_str(), foldername.c_str());
|
|
||||||
}
|
|
||||||
entry.close();
|
|
||||||
}
|
|
||||||
root.close();
|
|
||||||
DEBUG("fill_id_to_folder_map done.\n");
|
|
||||||
_spi->select_sd(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
String Player::_random_album() {
|
|
||||||
std::list<String> albums = ls("/", false, true, false);
|
|
||||||
uint8_t rnd = random(albums.size());
|
|
||||||
std::list<String>::iterator it = albums.begin();
|
|
||||||
for (int i=0; i<rnd; i++) { it++; }
|
|
||||||
return *it;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::play_random_album() {
|
|
||||||
play_album(_random_album());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Player::play_id(String id) {
|
|
||||||
String folder = _foldername_for_id(id);
|
|
||||||
if (folder.length()==0) return false;
|
|
||||||
return play_album(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
String Player::_foldername_for_id(String id) {
|
|
||||||
DEBUG("Searching for id %s...\n", id.c_str());
|
|
||||||
std::map<String, String>::iterator it = id_to_folder_map.find(id);
|
|
||||||
if (it != id_to_folder_map.end()) {
|
|
||||||
DEBUG("Found folder '%s' for id %s.\n", it->first.c_str(), it->second.c_str());
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
DEBUG("No folder found for id %s.\n", id.c_str());
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Player::play_album(String album) {
|
|
||||||
album_state s = _last_tracks[album.c_str()];
|
|
||||||
DEBUG("Last index for album '%s' was %d,%d\n", album.c_str(), s.index, s.position);
|
|
||||||
return play_song(album, s.index, s.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Player::play_song(String album, uint8_t index, uint32_t skip_to) {
|
|
||||||
if (_state == sleeping || _state == recording) _wakeup();
|
if (_state == sleeping || _state == recording) _wakeup();
|
||||||
if (_state != idle) return false;
|
if (_state != idle) return false;
|
||||||
DEBUG("Trying to play song at index %d, offset %d of album %s\n", index, skip_to, album.c_str());
|
if (_current_playlist == NULL) return false;
|
||||||
std::list<String> files = _files_in_dir(album);
|
if (_current_playlist->get_file_count()==0) return false;
|
||||||
_playing_album_songs = files.size();
|
_speaker_on();
|
||||||
DEBUG("Found %d songs in album\n", files.size());
|
_current_playlist->start();
|
||||||
if (index >= files.size()) {
|
String file;
|
||||||
ERROR("No matching file found - not playing.\n");
|
if (!_current_playlist->get_current_file(&file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String file = *(std::next(files.begin(), index));
|
uint32_t position = _current_playlist->get_position();
|
||||||
_state = playing;
|
_state = playing;
|
||||||
_playing_album = album;
|
_play_file(file, position);
|
||||||
_playing_index = index;
|
_controller->send_player_status();
|
||||||
_set_last_track(album.c_str(), index, skip_to);
|
|
||||||
_play_file(file, skip_to);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::play_system_sound(String filename) {
|
|
||||||
String file = filename;
|
|
||||||
if (!SD.exists(file)) {
|
|
||||||
ERROR("File %s does not exist!\n", file.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_state == playing) {
|
|
||||||
stop();
|
|
||||||
_state = system_sound_while_playing;
|
|
||||||
} else {
|
|
||||||
_state = system_sound_while_stopped;
|
|
||||||
}
|
|
||||||
_play_file(file, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_play_file(String file, uint32_t file_offset) {
|
void Player::_play_file(String file, uint32_t file_offset) {
|
||||||
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
|
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
|
||||||
_spi->select_sd();
|
_spi->select_sd();
|
||||||
_file = SD.open(file);
|
if (file.startsWith("/")) {
|
||||||
|
_file = new SDDataSource(file);
|
||||||
|
} else if (file.startsWith("http")) {
|
||||||
|
_file = new HTTPSDataSource(file);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_file_size = _file->size();
|
||||||
_spi->select_sd(false);
|
_spi->select_sd(false);
|
||||||
if (!_file) {
|
if (!_file || !_file->usable()) {
|
||||||
DEBUG("Could not open file %s", file.c_str());
|
DEBUG("Could not open file %s", file.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -735,39 +570,16 @@ void Player::_play_file(String file, uint32_t file_offset) {
|
|||||||
|
|
||||||
_spi->select_sd();
|
_spi->select_sd();
|
||||||
if (file_offset == 0) {
|
if (file_offset == 0) {
|
||||||
_file.seek(_id3_tag_offset(_file));
|
_file->skip_id3_tag();
|
||||||
}
|
}
|
||||||
_refills = 0;
|
_refills = 0;
|
||||||
_current_play_position = _file.position();
|
_current_play_position = _file->position();
|
||||||
_spi->select_sd(false);
|
_spi->select_sd(false);
|
||||||
_skip_to = file_offset;
|
_skip_to = file_offset;
|
||||||
if (_skip_to>0) _mute();
|
if (_skip_to>0) _mute();
|
||||||
else _speaker_on();
|
else _speaker_on();
|
||||||
INFO("Now playing.\n");
|
INFO("Now playing.\n");
|
||||||
}
|
_controller->send_player_status();
|
||||||
|
|
||||||
uint32_t Player::_id3_tag_offset(File f) {
|
|
||||||
uint32_t original_position = f.position();
|
|
||||||
uint32_t offset = 0;
|
|
||||||
if (f.read()=='I' && f.read()=='D' && f.read()=='3') {
|
|
||||||
DEBUG("ID3 tag found\n");
|
|
||||||
// Skip ID3 tag version
|
|
||||||
f.read(); f.read();
|
|
||||||
byte tags = f.read();
|
|
||||||
bool footer_present = tags & 0x10;
|
|
||||||
DEBUG("ID3 footer found: %d\n", footer_present);
|
|
||||||
for (byte i=0; i<4; i++) {
|
|
||||||
offset <<= 7;
|
|
||||||
offset |= (0x7F & f.read());
|
|
||||||
}
|
|
||||||
offset += 10;
|
|
||||||
if (footer_present) offset += 10;
|
|
||||||
DEBUG("ID3 tag length is %d bytes.\n", offset);
|
|
||||||
} else {
|
|
||||||
DEBUG("No ID3 tag found\n");
|
|
||||||
}
|
|
||||||
f.seek(original_position);
|
|
||||||
return offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::_flush(uint count, int8_t byte) {
|
void Player::_flush(uint count, int8_t byte) {
|
||||||
@@ -793,15 +605,15 @@ void Player::_finish_playing() {
|
|||||||
// If we reached this, the Chip didn't stop. That should not happen.
|
// If we reached this, the Chip didn't stop. That should not happen.
|
||||||
// (That's written in the manual.)
|
// (That's written in the manual.)
|
||||||
// Reset the chip.
|
// Reset the chip.
|
||||||
_init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::stop(bool turn_speaker_off) {
|
void Player::stop(bool turn_speaker_off) {
|
||||||
if (_state != playing /* && _state != system_sound_while_playing && _state != system_sound_while_stopped*/) return;
|
if (_state != playing) return;
|
||||||
INFO("Stopping...\n");
|
INFO("Stopping...\n");
|
||||||
if (_state == playing) {
|
_current_playlist->set_position(_current_play_position);
|
||||||
_set_last_track(_playing_album.c_str(), _playing_index, (uint32_t)_file.position());
|
_controller->pm->persist(_current_playlist);
|
||||||
}
|
|
||||||
_state = stopping;
|
_state = stopping;
|
||||||
_stop_delay = 0;
|
_stop_delay = 0;
|
||||||
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
||||||
@@ -826,51 +638,52 @@ void Player::_finish_stopping(bool turn_speaker_off) {
|
|||||||
_state = idle;
|
_state = idle;
|
||||||
_stopped_at = millis();
|
_stopped_at = millis();
|
||||||
if (_file) {
|
if (_file) {
|
||||||
_file.close();
|
_file->close();
|
||||||
|
delete _file;
|
||||||
}
|
}
|
||||||
|
_current_play_position = 0;
|
||||||
|
_file_size = 0;
|
||||||
INFO("Stopped.\n");
|
INFO("Stopped.\n");
|
||||||
|
_controller->send_player_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::_refill() {
|
void Player::_refill() {
|
||||||
_spi->select_sd();
|
_spi->select_sd();
|
||||||
_refills++;
|
_refills++;
|
||||||
if (_refills % 1000 == 0) DEBUG(".");
|
if (_refills % 1000 == 0) DEBUG(".");
|
||||||
uint8_t result = _file.read(_buffer, sizeof(_buffer));
|
uint8_t result = _file->read(_buffer, sizeof(_buffer));
|
||||||
_spi->select_sd(false);
|
_spi->select_sd(false);
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
// File is over.
|
// File is over.
|
||||||
DEBUG("EOF reached.\n");
|
DEBUG("EOF reached.\n");
|
||||||
_skip_to = 0;
|
_skip_to = 0;
|
||||||
_finish_playing();
|
_finish_playing();
|
||||||
if (_state == system_sound_while_playing) {
|
|
||||||
_finish_stopping(false);
|
_finish_stopping(false);
|
||||||
play_album(_playing_album);
|
if (_current_playlist->has_track_next()) {
|
||||||
return;
|
_current_playlist->track_next();
|
||||||
} else if (_state == system_sound_while_stopped) {
|
play();
|
||||||
_finish_stopping(true);
|
} else {
|
||||||
return;
|
_current_playlist->reset();
|
||||||
}
|
_controller->send_player_status();
|
||||||
_finish_stopping(false);
|
|
||||||
bool result = play_song(_playing_album, _playing_index + 1);
|
|
||||||
if (!result) {
|
|
||||||
_set_last_track(_playing_album.c_str(), 0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_current_play_position+=result;
|
_current_play_position+=result;
|
||||||
_write_data(_buffer);
|
_write_data(_buffer);
|
||||||
|
|
||||||
if (_skip_to > 0) {
|
if (_skip_to > 0) {
|
||||||
if (_skip_to > _file.position()) {
|
if (_skip_to > _file->position()) {
|
||||||
uint16_t status = _read_control_register(SCI_STATUS);
|
uint16_t status = _read_control_register(SCI_STATUS);
|
||||||
if ((status & SS_DO_NOT_JUMP) == 0) {
|
if ((status & SS_DO_NOT_JUMP) == 0) {
|
||||||
DEBUG("Skipping to %d.\n", _skip_to);
|
DEBUG("Skipping to %d.\n", _skip_to);
|
||||||
_flush(2048, _get_endbyte());
|
_flush(2048, _get_endbyte());
|
||||||
_spi->select_sd();
|
_spi->select_sd();
|
||||||
_file.seek(_skip_to);
|
_file->seek(_skip_to);
|
||||||
_spi->select_sd(false);
|
_spi->select_sd(false);
|
||||||
_skip_to = 0;
|
_skip_to = 0;
|
||||||
_unmute();
|
_unmute();
|
||||||
|
_controller->send_position();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_skip_to = 0;
|
_skip_to = 0;
|
||||||
@@ -880,10 +693,7 @@ void Player::_refill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Player::_refill_needed() {
|
bool Player::_refill_needed() {
|
||||||
return _state==playing ||
|
return _state==playing || _state==stopping;
|
||||||
_state==stopping ||
|
|
||||||
_state==system_sound_while_playing ||
|
|
||||||
_state==system_sound_while_stopped;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Player::loop() {
|
bool Player::loop() {
|
||||||
@@ -915,7 +725,29 @@ bool Player::loop() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::_set_last_track(const char* album, uint8_t index, uint32_t position) {
|
String Player::json() {
|
||||||
DEBUG("Setting _last_track[%s]=%d,%d.\n", album, index, position);
|
DynamicJsonDocument json(10240);
|
||||||
_last_tracks[album] = {index, position};
|
json["_type"] = "player";
|
||||||
|
json["playing"] = is_playing();
|
||||||
|
if (_current_playlist) {
|
||||||
|
JsonObject playlist = json.createNestedObject("playlist");
|
||||||
|
_current_playlist->json(playlist);
|
||||||
|
} else {
|
||||||
|
json["playlist"] = nullptr;
|
||||||
|
}
|
||||||
|
JsonObject volume = json.createNestedObject("volume");
|
||||||
|
volume["current"] = _volume;
|
||||||
|
volume["min"] = VOLUME_MIN;
|
||||||
|
volume["max"] = VOLUME_MAX;
|
||||||
|
volume["step"] = VOLUME_STEP;
|
||||||
|
return json.as<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
String Player::position_json() {
|
||||||
|
if (!is_playing()) return "null";
|
||||||
|
DynamicJsonDocument json(200);
|
||||||
|
json["_type"] = "position";
|
||||||
|
json["position"] = _current_play_position;
|
||||||
|
json["file_size"] = _file_size;
|
||||||
|
return json.as<String>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
#include <playlist.h>
|
||||||
|
#include "spi_master.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include <SD.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <TinyXML.h>
|
||||||
|
|
||||||
|
Playlist::Playlist(String path) {
|
||||||
|
_path = path;
|
||||||
|
if (path.startsWith("/")) {
|
||||||
|
persistence = PERSIST_TEMPORARY;
|
||||||
|
_add_path(path);
|
||||||
|
} else if (path.startsWith("http")) {
|
||||||
|
_examine_http_url(path);
|
||||||
|
}
|
||||||
|
if (_title.length()==0) _title=path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::_add_path(String path) {
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
TRACE("Examining folder %s...\n", path.c_str());
|
||||||
|
if (!path.startsWith("/")) path = String("/") + path;
|
||||||
|
if (!SD.exists(path)) {
|
||||||
|
DEBUG("Could not open path '%s'.\n", path.c_str());
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_title = path.substring(1);
|
||||||
|
int idx = _title.indexOf('/');
|
||||||
|
if (idx>0) {
|
||||||
|
_title.remove(idx);
|
||||||
|
}
|
||||||
|
File dir = SD.open(path);
|
||||||
|
File entry;
|
||||||
|
while (entry = dir.openNextFile()) {
|
||||||
|
String filename = entry.name();
|
||||||
|
filename = filename.substring(path.length() + 1);
|
||||||
|
String ext = filename.substring(filename.length() - 4);
|
||||||
|
if (!entry.isDirectory() &&
|
||||||
|
!filename.startsWith(".") &&
|
||||||
|
( ext.equals(".mp3") ||
|
||||||
|
ext.equals(".ogg") ||
|
||||||
|
ext.equals(".wma") ||
|
||||||
|
ext.equals(".mp4") ||
|
||||||
|
ext.equals(".mpa"))) {
|
||||||
|
TRACE(" Adding entry %s\n", entry.name());
|
||||||
|
String title = filename.substring(0, filename.length() - 4);
|
||||||
|
_files.push_back({.filename=entry.name(), .title=title, .id=String(_files.size())});
|
||||||
|
bool non_ascii_chars = false;
|
||||||
|
for(int i=0; i<filename.length(); i++) {
|
||||||
|
char c = filename.charAt(i);
|
||||||
|
if (c < 0x20 || c >= 0x7F) {
|
||||||
|
non_ascii_chars = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (non_ascii_chars) {
|
||||||
|
ERROR("WARNING: File '%s' contains non-ascii chars!\n", filename.c_str());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TRACE(" Ignoring entry %s\n", filename.c_str());
|
||||||
|
}
|
||||||
|
entry.close();
|
||||||
|
}
|
||||||
|
dir.close();
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
std::sort(_files.begin(), _files.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::_examine_http_url(String url) {
|
||||||
|
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||||
|
if (!http->get(url)) {
|
||||||
|
DEBUG("Could not GET %s.\n", url.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String ct = http->getContentType();
|
||||||
|
DEBUG("Content-Type is %s.\n", ct.c_str());
|
||||||
|
if (ct.startsWith("audio/x-mpegurl")) {
|
||||||
|
_parse_m3u(http);
|
||||||
|
} else if (ct.startsWith("audio/")) {
|
||||||
|
persistence = PERSIST_NONE;
|
||||||
|
_files.push_back({.filename=url, .title=url, .id="none"});
|
||||||
|
} else if (ct.startsWith("application/rss+xml") || ct.startsWith("application/xml")) {
|
||||||
|
persistence = PERSIST_PERMANENTLY;
|
||||||
|
_parse_rss(http);
|
||||||
|
} else if (ct.startsWith("application/pls+xml")) {
|
||||||
|
persistence = PERSIST_PERMANENTLY;
|
||||||
|
_parse_pls(http);
|
||||||
|
} else {
|
||||||
|
ERROR("Unknown content type %s.\n", ct.c_str());
|
||||||
|
}
|
||||||
|
http->close();
|
||||||
|
delete http;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PlaylistEntry>* xml_files_ptr = NULL;
|
||||||
|
String xml_last_tag = "";
|
||||||
|
String xml_title = "";
|
||||||
|
String xml_album_title = "";
|
||||||
|
String xml_url = "";
|
||||||
|
String xml_enclosure_url = "";
|
||||||
|
String xml_guid = "";
|
||||||
|
bool xml_enclosure_is_audio = false;
|
||||||
|
|
||||||
|
void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t dataLen) {
|
||||||
|
String tag(tagName);
|
||||||
|
if (status & STATUS_START_TAG) xml_last_tag = tag;
|
||||||
|
|
||||||
|
if (trace_enabled) {
|
||||||
|
if (status & STATUS_START_TAG) {
|
||||||
|
TRACE("Start of tag: %s\n", tagName);
|
||||||
|
} else if (status & STATUS_END_TAG) {
|
||||||
|
TRACE("End of tag: %s\n", tagName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.equals("/rss/channel/title") && (status & STATUS_TAG_TEXT)) {
|
||||||
|
xml_album_title = data;
|
||||||
|
} else if (tag.endsWith("/title") && (status & STATUS_TAG_TEXT)) {
|
||||||
|
xml_title = String(data);
|
||||||
|
} else if (tag.endsWith("/guid") && (status & STATUS_TAG_TEXT)) {
|
||||||
|
xml_guid = data;
|
||||||
|
//} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) {
|
||||||
|
// DEBUG("tag: %s, data: %s\n", tag.c_str(), data);
|
||||||
|
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
|
||||||
|
DEBUG("enclosure is audio\n");
|
||||||
|
xml_enclosure_is_audio = true;
|
||||||
|
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) {
|
||||||
|
DEBUG("found url\n");
|
||||||
|
xml_enclosure_url = String(data);
|
||||||
|
} else if (tag.endsWith("/enclosure") && (status & STATUS_END_TAG)) {
|
||||||
|
DEBUG("end of enclosure. xml_enclosure_is_audio: %d, xml_enclosure_url: %s\n", xml_enclosure_is_audio, xml_enclosure_url.c_str());
|
||||||
|
if (xml_enclosure_is_audio && xml_enclosure_url.length()>0) {
|
||||||
|
xml_url = xml_enclosure_url;
|
||||||
|
}
|
||||||
|
xml_enclosure_is_audio = false;
|
||||||
|
xml_enclosure_url = "";
|
||||||
|
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG || status & STATUS_START_TAG)) {
|
||||||
|
if (xml_title.length()>0 && xml_url.length()>0) {
|
||||||
|
if (xml_files_ptr->size() > 20) return;
|
||||||
|
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
|
||||||
|
xml_files_ptr->insert(xml_files_ptr->begin(), {.filename=xml_url, .title=xml_title, .id=xml_guid});
|
||||||
|
}
|
||||||
|
xml_title = "";
|
||||||
|
xml_url = "";
|
||||||
|
xml_guid = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::_parse_rss(HTTPClientWrapper* http) {
|
||||||
|
DEBUG("RSS parser running.\n");
|
||||||
|
// http is already initialized
|
||||||
|
int i;
|
||||||
|
|
||||||
|
TinyXML xml;
|
||||||
|
uint8_t* buffer = new uint8_t[150];
|
||||||
|
xml.init(buffer, 150, &xmlcb);
|
||||||
|
xml_files_ptr = &_files;
|
||||||
|
xml_title = "";
|
||||||
|
xml_album_title = "";
|
||||||
|
xml_url = "";
|
||||||
|
xml_enclosure_is_audio = false;
|
||||||
|
xml_enclosure_url = "";
|
||||||
|
while ((i = http->read()) >= 0) {
|
||||||
|
xml.processChar(i);
|
||||||
|
}
|
||||||
|
_current_track = _files.size()-1;
|
||||||
|
xml_files_ptr = NULL;
|
||||||
|
if (xml_album_title.length()>0) {
|
||||||
|
_title = xml_album_title;
|
||||||
|
}
|
||||||
|
xml_album_title = "";
|
||||||
|
delete buffer;
|
||||||
|
// don't close http at the end
|
||||||
|
DEBUG("RSS parser finished.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::_parse_m3u(HTTPClientWrapper* http) {
|
||||||
|
// http is already initialized
|
||||||
|
String line = "";
|
||||||
|
String title = "";
|
||||||
|
int i;
|
||||||
|
do {
|
||||||
|
i = http->read();
|
||||||
|
char c = i;
|
||||||
|
if (i>=0 && c!='\r' && c!='\n') {
|
||||||
|
line += c;
|
||||||
|
} else {
|
||||||
|
if (line.equals("#EXTM3U")) {
|
||||||
|
// Do nothing
|
||||||
|
} else if (line.startsWith("#EXTINF")) {
|
||||||
|
int idx = line.indexOf(",");
|
||||||
|
if (idx>4) {
|
||||||
|
// Get the title
|
||||||
|
title = line.substring(idx+1);
|
||||||
|
if (_title.length()==0) _title=title;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("http")) {
|
||||||
|
if (title.length()==0) title = line;
|
||||||
|
_files.push_back({.filename=line, .title=title, .id="none"});
|
||||||
|
title = "";
|
||||||
|
}
|
||||||
|
line = "";
|
||||||
|
}
|
||||||
|
} while (i>=0);
|
||||||
|
// don't close http at the end
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::_parse_pls(HTTPClientWrapper* http) {
|
||||||
|
// http is already initialized
|
||||||
|
String line;
|
||||||
|
String title = "";
|
||||||
|
String url = "";
|
||||||
|
int last_index = -1;
|
||||||
|
int index;
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
line = http->readLine();
|
||||||
|
if (line.startsWith("Title")) {
|
||||||
|
uint8_t eq_idx = line.indexOf('=');
|
||||||
|
if (eq_idx==-1) continue;
|
||||||
|
|
||||||
|
index = line.substring(5, eq_idx-4).toInt();
|
||||||
|
title = line.substring(eq_idx+1);
|
||||||
|
if (index != last_index) {
|
||||||
|
url = "";
|
||||||
|
last_index = index;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("File")) {
|
||||||
|
uint8_t eq_idx = line.indexOf('=');
|
||||||
|
if (eq_idx==-1) continue;
|
||||||
|
|
||||||
|
index = line.substring(5, eq_idx-4).toInt();
|
||||||
|
url = line.substring(eq_idx+1);
|
||||||
|
if (index != last_index) {
|
||||||
|
title = "";
|
||||||
|
last_index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.length()>0 && url.length()>0) {
|
||||||
|
_files.push_back({.filename=url, .title=title, .id="none"});
|
||||||
|
last_index = -1;
|
||||||
|
title = "";
|
||||||
|
url = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// don't close http at the end
|
||||||
|
}
|
||||||
|
|
||||||
|
String Playlist::path() {
|
||||||
|
return _path;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t Playlist::get_file_count() {
|
||||||
|
return _files.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::start() {
|
||||||
|
_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::has_track_prev() {
|
||||||
|
return _current_track > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::has_track_next() {
|
||||||
|
return _current_track < _files.size()-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::track_prev() {
|
||||||
|
if (_current_track > 0) {
|
||||||
|
_current_track--;
|
||||||
|
_position = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::track_next() {
|
||||||
|
if (_current_track < _files.size()-1) {
|
||||||
|
_current_track++;
|
||||||
|
_position = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::set_track(uint8_t track) {
|
||||||
|
if (track < _files.size()) {
|
||||||
|
_current_track = track;
|
||||||
|
_position = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::set_track_by_id(String id) {
|
||||||
|
for (int i=0; i<_files.size(); i++) {
|
||||||
|
if (id.equals(_files[i].id)) {
|
||||||
|
set_track(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::track_restart() {
|
||||||
|
_position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::shuffle(uint8_t random_offset) {
|
||||||
|
DEBUG("Shuffling the playlist with an offset of %d...\n", random_offset);
|
||||||
|
for (int i=random_offset; i<_files.size(); i++) {
|
||||||
|
int j = random(random_offset, _files.size()-1);
|
||||||
|
if (i!=j) {
|
||||||
|
TRACE(" Swapping elements %d and %d.\n", i, j);
|
||||||
|
PlaylistEntry temp = _files[i];
|
||||||
|
_files[i] = _files[j];
|
||||||
|
_files[j] = temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_shuffled = true;
|
||||||
|
TRACE("Done.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::advent_shuffle(uint8_t day) {
|
||||||
|
TRACE("advent_shuffle running...\n");
|
||||||
|
|
||||||
|
// Not enough songs till the current day? Play all songs in the default order.
|
||||||
|
if (day > _files.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are in the "different playlist every day" mode. So we don't persist it in order to not miss changes.
|
||||||
|
persistence = PERSIST_NONE;
|
||||||
|
|
||||||
|
|
||||||
|
_files.insert(_files.begin(), _files[day - 1]);
|
||||||
|
_files.erase(_files.begin() + day, _files.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::reset() {
|
||||||
|
std::sort(_files.begin(), _files.end());
|
||||||
|
_current_track = 0;
|
||||||
|
_position = 0;
|
||||||
|
_shuffled = false;
|
||||||
|
_started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String Playlist::get_current_track_id() {
|
||||||
|
if (_current_track > _files.size()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return _files[_current_track].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::get_current_file(String* dst) {
|
||||||
|
if (_current_track > _files.size()) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
dst->concat(_files[_current_track].filename);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t Playlist::get_position() {
|
||||||
|
return _position;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::set_position(uint32_t p) {
|
||||||
|
_position = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::is_fresh() {
|
||||||
|
return !_shuffled && !_started && _position==0 && _current_track==0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::dump() {
|
||||||
|
for (int i=0; i<_files.size(); i++) {
|
||||||
|
DEBUG(" %02d %2s %s\n", i+1, (i==_current_track) ? "->" : "", _files[i].filename.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::json(JsonObject json) {
|
||||||
|
json["_type"] = "playlist";
|
||||||
|
json["title"] = _title;
|
||||||
|
JsonArray files = json.createNestedArray("files");
|
||||||
|
for (PlaylistEntry entry: _files) {
|
||||||
|
JsonObject o = files.createNestedObject();
|
||||||
|
o["filename"] = entry.filename;
|
||||||
|
o["title"] = entry.title;
|
||||||
|
o["id"] = entry.id;
|
||||||
|
}
|
||||||
|
json["current_track"] = _current_track;
|
||||||
|
json["has_track_next"] = has_track_next();
|
||||||
|
json["has_track_prev"] = has_track_prev();
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#include "playlist_manager.h"
|
||||||
|
#include <SD.h>
|
||||||
|
#include "spi_master.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
PlaylistManager::PlaylistManager() {
|
||||||
|
scan_files();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::scan_files() {
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
std::vector<String> folders;
|
||||||
|
File root = SD.open("/");
|
||||||
|
File entry;
|
||||||
|
while (entry = root.openNextFile()) {
|
||||||
|
String foldername = entry.name();
|
||||||
|
if (foldername.startsWith("/.")) continue;
|
||||||
|
foldername.remove(foldername.length());
|
||||||
|
folders.push_back(foldername);
|
||||||
|
_check_for_special_chars(foldername);
|
||||||
|
entry.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_map.clear();
|
||||||
|
if (!SD.exists("/_mapping.txt\n")) {
|
||||||
|
ERROR("WARNING: File /_mapping.txt not found.\n");
|
||||||
|
} else {
|
||||||
|
File f = SD.open("/_mapping.txt");
|
||||||
|
DEBUG("Reading /_mapping.txt...\n");
|
||||||
|
while (f.available()) {
|
||||||
|
char buffer[512];
|
||||||
|
size_t pos = f.readBytesUntil('\n', buffer, 511);
|
||||||
|
buffer[pos] = '\0';
|
||||||
|
|
||||||
|
String data = buffer;
|
||||||
|
uint8_t eq = data.indexOf('=');
|
||||||
|
if (eq>0 && eq<data.length()-1) {
|
||||||
|
String rfid_id = data.substring(0, eq);
|
||||||
|
String folder = data.substring(eq + 1);
|
||||||
|
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
||||||
|
_map[rfid_id] = folder;
|
||||||
|
|
||||||
|
if (folder.charAt(0)=='/') {
|
||||||
|
bool found=false;
|
||||||
|
for (String f: folders) {
|
||||||
|
if (f.equals(folder)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
INFO("WARNING: Found mapping for RFID id %s which maps to non-existing folder %s!\n", rfid_id.c_str(), folder.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
root.close();
|
||||||
|
|
||||||
|
_unmapped_folders.clear();
|
||||||
|
for (String folder: folders) {
|
||||||
|
bool found = false;
|
||||||
|
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
|
||||||
|
if (it->second.equals(folder)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
// Checking folder for media files
|
||||||
|
File dir = SD.open(folder);
|
||||||
|
while (entry = dir.openNextFile()) {
|
||||||
|
String filename = entry.name();
|
||||||
|
filename = filename.substring(folder.length() + 1);
|
||||||
|
if (!filename.startsWith(".") && filename.endsWith(".mp3")) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
entry.close();
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
if (found) {
|
||||||
|
_unmapped_folders.push_back(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::_check_for_special_chars(String s) {
|
||||||
|
for(int i=0; i<s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c < 0x20 || c >= 0x7F) {
|
||||||
|
ERROR("WARNING: Folder / file '%s' contains non-ascii chars!\n", s.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Playlist* PlaylistManager::get_playlist_for_id(String id) {
|
||||||
|
if (!_map.count(id)) return NULL;
|
||||||
|
String folder = _map[id];
|
||||||
|
return get_playlist_for_folder(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
|
||||||
|
Playlist* p;
|
||||||
|
if (!_playlists.count(folder)) {
|
||||||
|
p = new Playlist(folder);
|
||||||
|
_playlists[folder] = p;
|
||||||
|
if (p->persistence == PERSIST_PERMANENTLY) {
|
||||||
|
String search = folder;
|
||||||
|
search += "=";
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
if (SD.exists("/_positions.txt")) {
|
||||||
|
File f = SD.open("/_positions.txt", "r");
|
||||||
|
while (true) {
|
||||||
|
String s = f.readStringUntil('\n');
|
||||||
|
if (s.length()==0) break;
|
||||||
|
if (s.startsWith(search)) {
|
||||||
|
s = s.substring(search.length());
|
||||||
|
int idx = s.indexOf(',');
|
||||||
|
String title_index = s.substring(0, idx);
|
||||||
|
uint32_t position = s.substring(idx+1).toInt();
|
||||||
|
p->set_track_by_id(title_index);
|
||||||
|
p->set_position(position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p = _playlists[folder];
|
||||||
|
if (p->persistence == PERSIST_NONE) {
|
||||||
|
p->reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::dump_ids() {
|
||||||
|
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
|
||||||
|
INFO(" %s -> %s\n", it->first.c_str(), it->second.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String PlaylistManager::json() {
|
||||||
|
DynamicJsonDocument json(10240);
|
||||||
|
json["_type"] = "playlist_manager";
|
||||||
|
JsonObject folders = json.createNestedObject("folders");
|
||||||
|
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
|
||||||
|
folders[it->second] = it->first;
|
||||||
|
}
|
||||||
|
JsonArray started = json.createNestedArray("started");
|
||||||
|
for (std::map<String, Playlist*>::iterator it = _playlists.begin(); it!=_playlists.end(); it++) {
|
||||||
|
if (it->second->is_fresh()) continue;
|
||||||
|
started.add(it->first);
|
||||||
|
}
|
||||||
|
JsonArray unmapped = json.createNestedArray("unmapped");
|
||||||
|
for(String folder : _unmapped_folders) {
|
||||||
|
unmapped.add(folder);
|
||||||
|
}
|
||||||
|
return json.as<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlaylistManager::add_mapping(String id, String folder) {
|
||||||
|
DEBUG("Adding mapping: %s=>%s\n", id.c_str(), folder.c_str());
|
||||||
|
_map[id] = folder;
|
||||||
|
_save_mapping();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::_save_mapping() {
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
File f = SD.open("/_mapping.txt", "w");
|
||||||
|
String s = create_mapping_txt();
|
||||||
|
unsigned char* buf = new unsigned char[s.length()];
|
||||||
|
s.getBytes(buf, s.length());
|
||||||
|
f.write(buf, s.length()-1);
|
||||||
|
f.close();
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
delete buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
String PlaylistManager::create_mapping_txt() {
|
||||||
|
String s;
|
||||||
|
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
|
||||||
|
s += it->first;
|
||||||
|
s += "=";
|
||||||
|
s += it->second;
|
||||||
|
s += '\n';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::persist(Playlist* p) {
|
||||||
|
if (p->persistence == PERSIST_NONE) {
|
||||||
|
_playlists.erase(p->path());
|
||||||
|
return;
|
||||||
|
} else if (p->persistence == PERSIST_PERMANENTLY) {
|
||||||
|
|
||||||
|
String search = p->path();
|
||||||
|
search += '=';
|
||||||
|
|
||||||
|
bool old_file_existed = false;
|
||||||
|
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
if (SD.exists("_positions.txt")) {
|
||||||
|
SD.rename("/_positions.txt", "/_positions.temp.txt");
|
||||||
|
old_file_existed = true;
|
||||||
|
}
|
||||||
|
File dst = SD.open("/_positions.txt", "w");
|
||||||
|
|
||||||
|
if (old_file_existed) {
|
||||||
|
File src = SD.open("/_positions.temp.txt", "r");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
String line = src.readStringUntil('\n');
|
||||||
|
line.trim();
|
||||||
|
if (line.startsWith(search)) continue;
|
||||||
|
dst.println(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
src.close();
|
||||||
|
SD.remove("/_positions.temp.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
dst.print(search);
|
||||||
|
dst.print(p->get_current_track_id());
|
||||||
|
dst.print(',');
|
||||||
|
dst.println(p->get_position());
|
||||||
|
dst.close();
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
|
||||||
|
_playlists.erase(p->path());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <Update.h>
|
||||||
|
#include "config.h"
|
||||||
|
#include "updater.h"
|
||||||
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
|
void Updater::run() {
|
||||||
|
DEBUG("Updater is running...\n");
|
||||||
|
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||||
|
DEBUG("Requesting update info...\n");
|
||||||
|
bool result = http->get(OTA_UPDATE_URL);
|
||||||
|
if (!result) {
|
||||||
|
ERROR("Updater failed requesting %s.\n", OTA_UPDATE_URL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String line_str = "";
|
||||||
|
if (!read_line(&line_str, http, "VERSION")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint16_t version = line_str.toInt();
|
||||||
|
if (version==0) {
|
||||||
|
ERROR("Could not parse version number.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DEBUG("Found version %d. My version is %d.\n", version, OTA_VERSION);
|
||||||
|
if (version <= OTA_VERSION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String image_path = "";
|
||||||
|
if (!read_line(&image_path, http, "IMAGE_PATH")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String image_md5 = "";
|
||||||
|
if (!read_line(&image_md5, http, "IMAGE_MD5")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
http->close();
|
||||||
|
delete http;
|
||||||
|
|
||||||
|
if(do_update(U_FLASH, image_path, image_md5)) {
|
||||||
|
DEBUG("Update done. Rebooting...\n");
|
||||||
|
} else {
|
||||||
|
DEBUG("Update failed. Rebooting...\n");
|
||||||
|
}
|
||||||
|
delay(1000);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Updater::read_line(String* dst, HTTPClientWrapper* http, String expected_key) {
|
||||||
|
expected_key += "=";
|
||||||
|
String line = http->readUntil("\n");
|
||||||
|
if (!line.startsWith(expected_key)) {
|
||||||
|
ERROR("Expected line start with '%s', but it started with '%s'.\n", expected_key.c_str(), line.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
line = line.substring(expected_key.length());
|
||||||
|
line.trim();
|
||||||
|
dst->concat(line);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Updater::do_update(int command, String url, String expected_md5) {
|
||||||
|
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||||
|
bool result = http->get(url);
|
||||||
|
if (!result) {
|
||||||
|
ERROR("Updater failed requesting %s.\n", url.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Update.begin(http->getSize(), command);
|
||||||
|
if (!result) {
|
||||||
|
ERROR("Update could not be started.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Update.setMD5(expected_md5.c_str());
|
||||||
|
uint8_t buf[512];
|
||||||
|
uint16_t len;
|
||||||
|
while((len = http->read(buf, 512))) {
|
||||||
|
Update.write(buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
http->close();
|
||||||
|
delete http;
|
||||||
|
|
||||||
|
result = Update.end();
|
||||||
|
if (!result) {
|
||||||
|
const char* error = Update.errorString();
|
||||||
|
ERROR("Writing the update failed. The error was: %s\n", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<title>ESMP3</title>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://kit.fontawesome.com/272149490a.js" crossorigin="anonymous"></script>
|
||||||
|
<style>
|
||||||
|
.overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
margin: auto 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: white;
|
||||||
|
font-size: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="overlay" class="overlay">Not connected...</div>
|
||||||
|
<div class="container bg-dark text-light">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-1">
|
||||||
|
<h1 id="play_state_icon"><i class="fa fa-stop"></i></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<h2><i class="fa fa-compact-disc"></i> <span id="album"></span></h2>
|
||||||
|
<h2><i class="fa fa-scroll"></i> <span id="track"></span></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<input type="range" class="custom-range" id="position_slider" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1">
|
||||||
|
<h3><i class="fa fa-volume-down"></i></h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="range" class="custom-range" id="volume_slider">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1">
|
||||||
|
<h3><i class="fa fa-volume-up"></i></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_track_prev"><i class="fa fa-step-backward"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_stop"><i class="fa fa-stop"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_play"><i class="fa fa-play"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_track_next"><i class="fa fa-step-forward"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_lock"><i class="fa fa-lock-open"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_open"><i class="fa fa-eject"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_settings"><i class="fa fa-cog"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<table class="table table-hover table-sm" id="track_list_table">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Nr.</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Track</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="" id="track_list">
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="modal fade" id="openModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Album öffnen</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6>Open URL</h6>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fa fa-link"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" id="input_url" />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-primary" id="button_url_open">Go</button>
|
||||||
|
<button class="btn btn-warning" id="button_url_add_mapping" style="display: none;"><i class="fa fa-arrows-alt-h"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="albums_without_id_area">
|
||||||
|
<h6>Albums without RFID card</h6>
|
||||||
|
<table class="table table-hover table-sm">
|
||||||
|
<tbody id="albums_without_id">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6>Albums with RFID</h6>
|
||||||
|
<table class="table table-hover table-sm">
|
||||||
|
<tbody id="albums_with_id">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Settings</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<h6>Last RFID id:</h6>
|
||||||
|
<span id="last_rfid_id"></span> <button class="btn btn-warning" id="button_add_mapping"><i class="fa fa-arrows-alt-h"></i></button>
|
||||||
|
|
||||||
|
<h6>Last RFID data:</h6>
|
||||||
|
<span id="last_rfid_data"></span>
|
||||||
|
|
||||||
|
<h6>Actions</h6>
|
||||||
|
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reset_vs1053">Reset VS1053 chip</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reboot">Reboot ESMP3</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_update">Check for and install update</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
update_player = function(data) {
|
||||||
|
$('#play_state_icon i').removeClass('fa-stop', 'fa-play').addClass(data.playing ? 'fa-play' : 'fa-stop');
|
||||||
|
|
||||||
|
if (data.playing) {
|
||||||
|
$('#button_play').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||||
|
$('#button_stop').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
|
||||||
|
} else if (data.playlist) {
|
||||||
|
$('#button_play').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
|
||||||
|
$('#button_stop').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||||
|
} else {
|
||||||
|
$('#button_play').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||||
|
$('#button_stop').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#volume_slider').attr('min', data.volume.min).attr('max', data.volume.max).val(data.volume.current);
|
||||||
|
|
||||||
|
if (data.playlist) update_playlist(data.playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_playlist = function(data) {
|
||||||
|
$('#track_list tr').remove();
|
||||||
|
for (var i=0; i<data.files.length; i++) {
|
||||||
|
tr = $('<tr>').data('track', i);
|
||||||
|
tr.append($('<td>').html(i + 1));
|
||||||
|
tr.append($('<td>').html(data.current_track==i ? '<i class="fa fa-play"></i>' : ''));
|
||||||
|
tr.append($('<td>').html(data.files[i].title));
|
||||||
|
$('#track_list').append(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.has_track_next) {
|
||||||
|
$('#button_track_next').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
|
||||||
|
} else {
|
||||||
|
$('#button_track_next').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.has_track_prev) {
|
||||||
|
$('#button_track_prev').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
|
||||||
|
} else {
|
||||||
|
$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#album').html(data.title);
|
||||||
|
var file = data.files[data.current_track];
|
||||||
|
if (file) {
|
||||||
|
$('#track').html(file.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_controller = function(data) {
|
||||||
|
if (data.lock_state == "locked") {
|
||||||
|
$('#button_lock').removeClass('btn-primary', 'btn-warning').addClass('btn-danger');
|
||||||
|
$('#button_lock i').removeClass('fa-lock-open').addClass('fa-lock');
|
||||||
|
} else if (data.lock_state == "locking") {
|
||||||
|
$('#button_lock').removeClass('btn-primary', 'btn-danger').addClass('btn-warning');
|
||||||
|
$('#button_lock i').removeClass('fa-lock-open').addClass('fa-lock');
|
||||||
|
} else {
|
||||||
|
$('#button_lock').removeClass('btn-danger', 'btn-warning').addClass('btn-primary');
|
||||||
|
$('#button_lock i').removeClass('fa-lock').addClass('fa-lock-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#button_add_mapping').toggle(data.last_rfid.uid.length>0);
|
||||||
|
$('#last_rfid_id').html(data.last_rfid.uid);
|
||||||
|
$('#last_rfid_data').html(data.last_rfid.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_playlist_manager = function(data) {
|
||||||
|
if (data.unmapped.length > 0) {
|
||||||
|
$('#albums_without_id_area').show();
|
||||||
|
$('#albums_without_id tr').remove();
|
||||||
|
data.unmapped = data.unmapped.sort();
|
||||||
|
for (var i=0; i<data.unmapped.length; i++) {
|
||||||
|
var tr = $('<tr>').attr('data-folder', data.unmapped[i]);
|
||||||
|
tr.append($('<td>').html(data.unmapped[i].substr(1)));
|
||||||
|
tr.append($('<td>').append($('<button>').addClass('button btn-warning add_mapping_button').hide().append($('<i>').addClass('fa fa-arrows-alt-h'))));
|
||||||
|
$('#albums_without_id').append(tr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$('#albums_without_id_area').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders = Object.keys(data.folders).sort();
|
||||||
|
for (var i in folders) {
|
||||||
|
var folder = folders[i];
|
||||||
|
var tr = $('<tr>').attr('data-folder', folder);
|
||||||
|
tr.append($('<td>').html(folder.substr(1)));
|
||||||
|
tr.append($('<td>').append($('<button>').addClass('button btn-danger add_mapping_button').hide().append($('<i>').addClass('fa fa-arrows-alt-h'))));
|
||||||
|
$('#albums_with_id').append(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_position = function(data) {
|
||||||
|
$('#position_slider').attr('max', data.file_size).val(data.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
process_ws_message = function(event) {
|
||||||
|
var data = event.data.split("\n");;
|
||||||
|
for (var i=0; i<data.length; i++) {
|
||||||
|
var json = JSON.parse(data[i]);
|
||||||
|
console.log(json);
|
||||||
|
if (json === null) continue;
|
||||||
|
switch(json["_type"]) {
|
||||||
|
case "position": update_position(json); break;
|
||||||
|
case "player": update_player(json); break;
|
||||||
|
case "playlist_manager": update_playlist_manager(json); break;
|
||||||
|
case "controller": update_controller(json); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var play_on_click = true;
|
||||||
|
interval = null;
|
||||||
|
ws = null;
|
||||||
|
|
||||||
|
var start_reconnect_timer = function() {
|
||||||
|
console.log("start_reconnect_timer() running...");
|
||||||
|
$('#overlay').show();
|
||||||
|
interval = setInterval(connect_to_ws, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
var connect_to_ws = function() {
|
||||||
|
if (!ws || ws.readyState >= ws.CLOSING) {
|
||||||
|
ws = new WebSocket("ws://" + location.host + "/ws");
|
||||||
|
ws.onopen = function() {
|
||||||
|
console.log("on_open() running...");
|
||||||
|
clearInterval(interval);
|
||||||
|
$('#overlay').hide();
|
||||||
|
};
|
||||||
|
ws.onmessage = process_ws_message;
|
||||||
|
ws.onclose = start_reconnect_timer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
start_reconnect_timer();
|
||||||
|
|
||||||
|
$('#volume_slider').change(function(e) { ws.send("volume=" + e.target.value); });
|
||||||
|
$('#button_play').click(function(e) { ws.send("play"); });
|
||||||
|
$('#button_stop').click(function(e) { ws.send("stop"); });
|
||||||
|
$('#button_track_next').click(function(e) { ws.send("track_next"); });
|
||||||
|
$('#button_track_prev').click(function(e) { ws.send("track_prev"); });
|
||||||
|
$('#button_open').click(function(e) { $('#openModal').modal('show'); });
|
||||||
|
$('#track_list').on('click', 'tr', function(e) { ws.send("track=" + $(e.target).parent().data('track')); });
|
||||||
|
$('#albums_without_id, #albums_with_id').on('click', 'tr', function(e) { if (play_on_click) {ws.send("play " + $(e.target).parents('tr').data('folder')); $('#openModal').modal('hide');} });
|
||||||
|
$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); });
|
||||||
|
$('#button_reset_vs1053').click(function(e) { ws.send("reset_vs1053"); $('#settingsModal').modal('hide'); });
|
||||||
|
$('#button_reboot').click(function(e) { ws.send("reboot"); $('#settingsModal').modal('hide'); });
|
||||||
|
$('#button_update').click(function(e) { ws.send("update"); $('#settingsModal').modal('hide'); });
|
||||||
|
$('#button_url_open').click(function(e) { ws.send("play " + $('#input_url').val()); $('#openModal').modal('hide');});
|
||||||
|
$('#button_add_mapping').click(function(e) {
|
||||||
|
$('#settingsModal').modal('hide');
|
||||||
|
$('#openModal').modal('show');
|
||||||
|
$('.add_mapping_button').show();
|
||||||
|
$('#button_url_open').hide();
|
||||||
|
$('#button_url_add_mapping').show();
|
||||||
|
play_on_click = false;
|
||||||
|
});
|
||||||
|
$('#openModal').on('click', '.add_mapping_button', function(e) {
|
||||||
|
ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $(e.target).parents('tr').data('folder'));
|
||||||
|
$('#openModal').modal('hide');
|
||||||
|
$('.add_mapping_button').hide();
|
||||||
|
$('#button_url_open').hide();
|
||||||
|
$('#button_url_add_mapping').show();
|
||||||
|
e.stopPropagation();
|
||||||
|
play_on_click=true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
$('#button_url_add_mapping').click(function(e) {
|
||||||
|
ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $('#input_url').val());
|
||||||
|
$('#openModal').modal('hide');
|
||||||
|
$('.add_mapping_button').hide();
|
||||||
|
$('#button_url_open').hide();
|
||||||
|
$('#button_url_add_mapping').show();
|
||||||
|
e.stopPropagation();
|
||||||
|
play_on_click=true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
{"timezones": {
|
||||||
|
"Africa/Abidjan":"GMT0",
|
||||||
|
"Africa/Accra":"GMT0",
|
||||||
|
"Africa/Addis_Ababa":"EAT-3",
|
||||||
|
"Africa/Algiers":"CET-1",
|
||||||
|
"Africa/Asmara":"EAT-3",
|
||||||
|
"Africa/Bamako":"GMT0",
|
||||||
|
"Africa/Bangui":"WAT-1",
|
||||||
|
"Africa/Banjul":"GMT0",
|
||||||
|
"Africa/Bissau":"GMT0",
|
||||||
|
"Africa/Blantyre":"CAT-2",
|
||||||
|
"Africa/Brazzaville":"WAT-1",
|
||||||
|
"Africa/Bujumbura":"CAT-2",
|
||||||
|
"Africa/Cairo":"EET-2",
|
||||||
|
"Africa/Casablanca":"<+01>-1",
|
||||||
|
"Africa/Ceuta":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Africa/Conakry":"GMT0",
|
||||||
|
"Africa/Dakar":"GMT0",
|
||||||
|
"Africa/Dar_es_Salaam":"EAT-3",
|
||||||
|
"Africa/Djibouti":"EAT-3",
|
||||||
|
"Africa/Douala":"WAT-1",
|
||||||
|
"Africa/El_Aaiun":"<+01>-1",
|
||||||
|
"Africa/Freetown":"GMT0",
|
||||||
|
"Africa/Gaborone":"CAT-2",
|
||||||
|
"Africa/Harare":"CAT-2",
|
||||||
|
"Africa/Johannesburg":"SAST-2",
|
||||||
|
"Africa/Juba":"EAT-3",
|
||||||
|
"Africa/Kampala":"EAT-3",
|
||||||
|
"Africa/Khartoum":"CAT-2",
|
||||||
|
"Africa/Kigali":"CAT-2",
|
||||||
|
"Africa/Kinshasa":"WAT-1",
|
||||||
|
"Africa/Lagos":"WAT-1",
|
||||||
|
"Africa/Libreville":"WAT-1",
|
||||||
|
"Africa/Lome":"GMT0",
|
||||||
|
"Africa/Luanda":"WAT-1",
|
||||||
|
"Africa/Lubumbashi":"CAT-2",
|
||||||
|
"Africa/Lusaka":"CAT-2",
|
||||||
|
"Africa/Malabo":"WAT-1",
|
||||||
|
"Africa/Maputo":"CAT-2",
|
||||||
|
"Africa/Maseru":"SAST-2",
|
||||||
|
"Africa/Mbabane":"SAST-2",
|
||||||
|
"Africa/Mogadishu":"EAT-3",
|
||||||
|
"Africa/Monrovia":"GMT0",
|
||||||
|
"Africa/Nairobi":"EAT-3",
|
||||||
|
"Africa/Ndjamena":"WAT-1",
|
||||||
|
"Africa/Niamey":"WAT-1",
|
||||||
|
"Africa/Nouakchott":"GMT0",
|
||||||
|
"Africa/Ouagadougou":"GMT0",
|
||||||
|
"Africa/Porto-Novo":"WAT-1",
|
||||||
|
"Africa/Sao_Tome":"GMT0",
|
||||||
|
"Africa/Tripoli":"EET-2",
|
||||||
|
"Africa/Tunis":"CET-1",
|
||||||
|
"Africa/Windhoek":"CAT-2",
|
||||||
|
"America/Adak":"HST10HDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Anchorage":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Anguilla":"AST4",
|
||||||
|
"America/Antigua":"AST4",
|
||||||
|
"America/Araguaina":"<-03>3",
|
||||||
|
"America/Argentina/Buenos_Aires":"<-03>3",
|
||||||
|
"America/Argentina/Catamarca":"<-03>3",
|
||||||
|
"America/Argentina/Cordoba":"<-03>3",
|
||||||
|
"America/Argentina/Jujuy":"<-03>3",
|
||||||
|
"America/Argentina/La_Rioja":"<-03>3",
|
||||||
|
"America/Argentina/Mendoza":"<-03>3",
|
||||||
|
"America/Argentina/Rio_Gallegos":"<-03>3",
|
||||||
|
"America/Argentina/Salta":"<-03>3",
|
||||||
|
"America/Argentina/San_Juan":"<-03>3",
|
||||||
|
"America/Argentina/San_Luis":"<-03>3",
|
||||||
|
"America/Argentina/Tucuman":"<-03>3",
|
||||||
|
"America/Argentina/Ushuaia":"<-03>3",
|
||||||
|
"America/Aruba":"AST4",
|
||||||
|
"America/Asuncion":"<-04>4<-03>,M10.1.0/0,M3.4.0/0",
|
||||||
|
"America/Atikokan":"EST5",
|
||||||
|
"America/Bahia":"<-03>3",
|
||||||
|
"America/Bahia_Banderas":"CST6CDT,M4.1.0,M10.5.0",
|
||||||
|
"America/Barbados":"AST4",
|
||||||
|
"America/Belem":"<-03>3",
|
||||||
|
"America/Belize":"CST6",
|
||||||
|
"America/Blanc-Sablon":"AST4",
|
||||||
|
"America/Boa_Vista":"<-04>4",
|
||||||
|
"America/Bogota":"<-05>5",
|
||||||
|
"America/Boise":"MST7MDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Cambridge_Bay":"MST7MDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Campo_Grande":"<-04>4",
|
||||||
|
"America/Cancun":"EST5",
|
||||||
|
"America/Caracas":"<-04>4",
|
||||||
|
"America/Cayenne":"<-03>3",
|
||||||
|
"America/Cayman":"EST5",
|
||||||
|
"America/Chicago":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Chihuahua":"MST7MDT,M4.1.0,M10.5.0",
|
||||||
|
"America/Costa_Rica":"CST6",
|
||||||
|
"America/Creston":"MST7",
|
||||||
|
"America/Cuiaba":"<-04>4",
|
||||||
|
"America/Curacao":"AST4",
|
||||||
|
"America/Danmarkshavn":"GMT0",
|
||||||
|
"America/Dawson":"PST8PDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Dawson_Creek":"MST7",
|
||||||
|
"America/Denver":"MST7MDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Detroit":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Dominica":"AST4",
|
||||||
|
"America/Edmonton":"MST7MDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Eirunepe":"<-05>5",
|
||||||
|
"America/El_Salvador":"CST6",
|
||||||
|
"America/Fortaleza":"<-03>3",
|
||||||
|
"America/Fort_Nelson":"MST7",
|
||||||
|
"America/Glace_Bay":"AST4ADT,M3.2.0,M11.1.0",
|
||||||
|
"America/Godthab":"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1",
|
||||||
|
"America/Goose_Bay":"AST4ADT,M3.2.0,M11.1.0",
|
||||||
|
"America/Grand_Turk":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Grenada":"AST4",
|
||||||
|
"America/Guadeloupe":"AST4",
|
||||||
|
"America/Guatemala":"CST6",
|
||||||
|
"America/Guayaquil":"<-05>5",
|
||||||
|
"America/Guyana":"<-04>4",
|
||||||
|
"America/Halifax":"AST4ADT,M3.2.0,M11.1.0",
|
||||||
|
"America/Havana":"CST5CDT,M3.2.0/0,M11.1.0/1",
|
||||||
|
"America/Hermosillo":"MST7",
|
||||||
|
"America/Indiana/Indianapolis":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Indiana/Knox":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Indiana/Marengo":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Indiana/Petersburg":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Indiana/Tell_City":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Indiana/Vevay":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Indiana/Vincennes":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Indiana/Winamac":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Inuvik":"MST7MDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Iqaluit":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Jamaica":"EST5",
|
||||||
|
"America/Juneau":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Kentucky/Louisville":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Kentucky/Monticello":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Kralendijk":"AST4",
|
||||||
|
"America/La_Paz":"<-04>4",
|
||||||
|
"America/Lima":"<-05>5",
|
||||||
|
"America/Los_Angeles":"PST8PDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Lower_Princes":"AST4",
|
||||||
|
"America/Maceio":"<-03>3",
|
||||||
|
"America/Managua":"CST6",
|
||||||
|
"America/Manaus":"<-04>4",
|
||||||
|
"America/Marigot":"AST4",
|
||||||
|
"America/Martinique":"AST4",
|
||||||
|
"America/Matamoros":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Mazatlan":"MST7MDT,M4.1.0,M10.5.0",
|
||||||
|
"America/Menominee":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Merida":"CST6CDT,M4.1.0,M10.5.0",
|
||||||
|
"America/Metlakatla":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Mexico_City":"CST6CDT,M4.1.0,M10.5.0",
|
||||||
|
"America/Miquelon":"<-03>3<-02>,M3.2.0,M11.1.0",
|
||||||
|
"America/Moncton":"AST4ADT,M3.2.0,M11.1.0",
|
||||||
|
"America/Monterrey":"CST6CDT,M4.1.0,M10.5.0",
|
||||||
|
"America/Montevideo":"<-03>3",
|
||||||
|
"America/Montreal":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Montserrat":"AST4",
|
||||||
|
"America/Nassau":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/New_York":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Nipigon":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Nome":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Noronha":"<-02>2",
|
||||||
|
"America/North_Dakota/Beulah":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/North_Dakota/Center":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/North_Dakota/New_Salem":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Ojinaga":"MST7MDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Panama":"EST5",
|
||||||
|
"America/Pangnirtung":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Paramaribo":"<-03>3",
|
||||||
|
"America/Phoenix":"MST7",
|
||||||
|
"America/Port-au-Prince":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Port_of_Spain":"AST4",
|
||||||
|
"America/Porto_Velho":"<-04>4",
|
||||||
|
"America/Puerto_Rico":"AST4",
|
||||||
|
"America/Punta_Arenas":"<-03>3",
|
||||||
|
"America/Rainy_River":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Rankin_Inlet":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Recife":"<-03>3",
|
||||||
|
"America/Regina":"CST6",
|
||||||
|
"America/Resolute":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Rio_Branco":"<-05>5",
|
||||||
|
"America/Santarem":"<-03>3",
|
||||||
|
"America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24",
|
||||||
|
"America/Santo_Domingo":"AST4",
|
||||||
|
"America/Sao_Paulo":"<-03>3",
|
||||||
|
"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
|
||||||
|
"America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||||
|
"America/St_Barthelemy":"AST4",
|
||||||
|
"America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0",
|
||||||
|
"America/St_Kitts":"AST4",
|
||||||
|
"America/St_Lucia":"AST4",
|
||||||
|
"America/St_Thomas":"AST4",
|
||||||
|
"America/St_Vincent":"AST4",
|
||||||
|
"America/Swift_Current":"CST6",
|
||||||
|
"America/Tegucigalpa":"CST6",
|
||||||
|
"America/Thule":"AST4ADT,M3.2.0,M11.1.0",
|
||||||
|
"America/Thunder_Bay":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Tijuana":"PST8PDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Toronto":"EST5EDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Tortola":"AST4",
|
||||||
|
"America/Vancouver":"PST8PDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Whitehorse":"PST8PDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||||
|
"America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0",
|
||||||
|
"Antarctica/Casey":"<+08>-8",
|
||||||
|
"Antarctica/Davis":"<+07>-7",
|
||||||
|
"Antarctica/DumontDUrville":"<+10>-10",
|
||||||
|
"Antarctica/Macquarie":"<+11>-11",
|
||||||
|
"Antarctica/Mawson":"<+05>-5",
|
||||||
|
"Antarctica/McMurdo":"NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||||
|
"Antarctica/Palmer":"<-03>3",
|
||||||
|
"Antarctica/Rothera":"<-03>3",
|
||||||
|
"Antarctica/Syowa":"<+03>-3",
|
||||||
|
"Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
|
||||||
|
"Antarctica/Vostok":"<+06>-6",
|
||||||
|
"Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Asia/Aden":"<+03>-3",
|
||||||
|
"Asia/Almaty":"<+06>-6",
|
||||||
|
"Asia/Amman":"EET-2EEST,M3.5.4/24,M10.5.5/1",
|
||||||
|
"Asia/Anadyr":"<+12>-12",
|
||||||
|
"Asia/Aqtau":"<+05>-5",
|
||||||
|
"Asia/Aqtobe":"<+05>-5",
|
||||||
|
"Asia/Ashgabat":"<+05>-5",
|
||||||
|
"Asia/Atyrau":"<+05>-5",
|
||||||
|
"Asia/Baghdad":"<+03>-3",
|
||||||
|
"Asia/Bahrain":"<+03>-3",
|
||||||
|
"Asia/Baku":"<+04>-4",
|
||||||
|
"Asia/Bangkok":"<+07>-7",
|
||||||
|
"Asia/Barnaul":"<+07>-7",
|
||||||
|
"Asia/Beirut":"EET-2EEST,M3.5.0/0,M10.5.0/0",
|
||||||
|
"Asia/Bishkek":"<+06>-6",
|
||||||
|
"Asia/Brunei":"<+08>-8",
|
||||||
|
"Asia/Chita":"<+09>-9",
|
||||||
|
"Asia/Choibalsan":"<+08>-8",
|
||||||
|
"Asia/Colombo":"<+0530>-5:30",
|
||||||
|
"Asia/Damascus":"EET-2EEST,M3.5.5/0,M10.5.5/0",
|
||||||
|
"Asia/Dhaka":"<+06>-6",
|
||||||
|
"Asia/Dili":"<+09>-9",
|
||||||
|
"Asia/Dubai":"<+04>-4",
|
||||||
|
"Asia/Dushanbe":"<+05>-5",
|
||||||
|
"Asia/Famagusta":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Asia/Gaza":"EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||||
|
"Asia/Hebron":"EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||||
|
"Asia/Ho_Chi_Minh":"<+07>-7",
|
||||||
|
"Asia/Hong_Kong":"HKT-8",
|
||||||
|
"Asia/Hovd":"<+07>-7",
|
||||||
|
"Asia/Irkutsk":"<+08>-8",
|
||||||
|
"Asia/Jakarta":"WIB-7",
|
||||||
|
"Asia/Jayapura":"WIT-9",
|
||||||
|
"Asia/Jerusalem":"IST-2IDT,M3.4.4/26,M10.5.0",
|
||||||
|
"Asia/Kabul":"<+0430>-4:30",
|
||||||
|
"Asia/Kamchatka":"<+12>-12",
|
||||||
|
"Asia/Karachi":"PKT-5",
|
||||||
|
"Asia/Kathmandu":"<+0545>-5:45",
|
||||||
|
"Asia/Khandyga":"<+09>-9",
|
||||||
|
"Asia/Kolkata":"IST-5:30",
|
||||||
|
"Asia/Krasnoyarsk":"<+07>-7",
|
||||||
|
"Asia/Kuala_Lumpur":"<+08>-8",
|
||||||
|
"Asia/Kuching":"<+08>-8",
|
||||||
|
"Asia/Kuwait":"<+03>-3",
|
||||||
|
"Asia/Macau":"CST-8",
|
||||||
|
"Asia/Magadan":"<+11>-11",
|
||||||
|
"Asia/Makassar":"WITA-8",
|
||||||
|
"Asia/Manila":"PST-8",
|
||||||
|
"Asia/Muscat":"<+04>-4",
|
||||||
|
"Asia/Nicosia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Asia/Novokuznetsk":"<+07>-7",
|
||||||
|
"Asia/Novosibirsk":"<+07>-7",
|
||||||
|
"Asia/Omsk":"<+06>-6",
|
||||||
|
"Asia/Oral":"<+05>-5",
|
||||||
|
"Asia/Phnom_Penh":"<+07>-7",
|
||||||
|
"Asia/Pontianak":"WIB-7",
|
||||||
|
"Asia/Pyongyang":"KST-9",
|
||||||
|
"Asia/Qatar":"<+03>-3",
|
||||||
|
"Asia/Qyzylorda":"<+05>-5",
|
||||||
|
"Asia/Riyadh":"<+03>-3",
|
||||||
|
"Asia/Sakhalin":"<+11>-11",
|
||||||
|
"Asia/Samarkand":"<+05>-5",
|
||||||
|
"Asia/Seoul":"KST-9",
|
||||||
|
"Asia/Shanghai":"CST-8",
|
||||||
|
"Asia/Singapore":"<+08>-8",
|
||||||
|
"Asia/Srednekolymsk":"<+11>-11",
|
||||||
|
"Asia/Taipei":"CST-8",
|
||||||
|
"Asia/Tashkent":"<+05>-5",
|
||||||
|
"Asia/Tbilisi":"<+04>-4",
|
||||||
|
"Asia/Tehran":"<+0330>-3:30<+0430>,J79/24,J263/24",
|
||||||
|
"Asia/Thimphu":"<+06>-6",
|
||||||
|
"Asia/Tokyo":"JST-9",
|
||||||
|
"Asia/Tomsk":"<+07>-7",
|
||||||
|
"Asia/Ulaanbaatar":"<+08>-8",
|
||||||
|
"Asia/Urumqi":"<+06>-6",
|
||||||
|
"Asia/Ust-Nera":"<+10>-10",
|
||||||
|
"Asia/Vientiane":"<+07>-7",
|
||||||
|
"Asia/Vladivostok":"<+10>-10",
|
||||||
|
"Asia/Yakutsk":"<+09>-9",
|
||||||
|
"Asia/Yangon":"<+0630>-6:30",
|
||||||
|
"Asia/Yekaterinburg":"<+05>-5",
|
||||||
|
"Asia/Yerevan":"<+04>-4",
|
||||||
|
"Atlantic/Azores":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
|
||||||
|
"Atlantic/Bermuda":"AST4ADT,M3.2.0,M11.1.0",
|
||||||
|
"Atlantic/Canary":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||||
|
"Atlantic/Cape_Verde":"<-01>1",
|
||||||
|
"Atlantic/Faroe":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||||
|
"Atlantic/Madeira":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||||
|
"Atlantic/Reykjavik":"GMT0",
|
||||||
|
"Atlantic/South_Georgia":"<-02>2",
|
||||||
|
"Atlantic/Stanley":"<-03>3",
|
||||||
|
"Atlantic/St_Helena":"GMT0",
|
||||||
|
"Australia/Adelaide":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||||
|
"Australia/Brisbane":"AEST-10",
|
||||||
|
"Australia/Broken_Hill":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||||
|
"Australia/Currie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||||
|
"Australia/Darwin":"ACST-9:30",
|
||||||
|
"Australia/Eucla":"<+0845>-8:45",
|
||||||
|
"Australia/Hobart":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||||
|
"Australia/Lindeman":"AEST-10",
|
||||||
|
"Australia/Lord_Howe":"<+1030>-10:30<+11>-11,M10.1.0,M4.1.0",
|
||||||
|
"Australia/Melbourne":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||||
|
"Australia/Perth":"AWST-8",
|
||||||
|
"Australia/Sydney":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||||
|
"Europe/Amsterdam":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Andorra":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Astrakhan":"<+04>-4",
|
||||||
|
"Europe/Athens":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Belgrade":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Berlin":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Bratislava":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Brussels":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Bucharest":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Budapest":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Busingen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Chisinau":"EET-2EEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Copenhagen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Dublin":"IST-1GMT0,M10.5.0,M3.5.0/1",
|
||||||
|
"Europe/Gibraltar":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Guernsey":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||||
|
"Europe/Helsinki":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Isle_of_Man":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||||
|
"Europe/Istanbul":"<+03>-3",
|
||||||
|
"Europe/Jersey":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||||
|
"Europe/Kaliningrad":"EET-2",
|
||||||
|
"Europe/Kiev":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Kirov":"<+03>-3",
|
||||||
|
"Europe/Lisbon":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||||
|
"Europe/Ljubljana":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/London":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||||
|
"Europe/Luxembourg":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Madrid":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Malta":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Mariehamn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Minsk":"<+03>-3",
|
||||||
|
"Europe/Monaco":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Moscow":"MSK-3",
|
||||||
|
"Europe/Oslo":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Paris":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Podgorica":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Prague":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Riga":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Rome":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Samara":"<+04>-4",
|
||||||
|
"Europe/San_Marino":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Sarajevo":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Saratov":"<+04>-4",
|
||||||
|
"Europe/Simferopol":"MSK-3",
|
||||||
|
"Europe/Skopje":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Sofia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Stockholm":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Tallinn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Tirane":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Ulyanovsk":"<+04>-4",
|
||||||
|
"Europe/Uzhgorod":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Vaduz":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Vatican":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Vienna":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Vilnius":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Volgograd":"<+04>-4",
|
||||||
|
"Europe/Warsaw":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Zagreb":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Europe/Zaporozhye":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||||
|
"Europe/Zurich":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||||
|
"Indian/Antananarivo":"EAT-3",
|
||||||
|
"Indian/Chagos":"<+06>-6",
|
||||||
|
"Indian/Christmas":"<+07>-7",
|
||||||
|
"Indian/Cocos":"<+0630>-6:30",
|
||||||
|
"Indian/Comoro":"EAT-3",
|
||||||
|
"Indian/Kerguelen":"<+05>-5",
|
||||||
|
"Indian/Mahe":"<+04>-4",
|
||||||
|
"Indian/Maldives":"<+05>-5",
|
||||||
|
"Indian/Mauritius":"<+04>-4",
|
||||||
|
"Indian/Mayotte":"EAT-3",
|
||||||
|
"Indian/Reunion":"<+04>-4",
|
||||||
|
"Pacific/Apia":"<+13>-13<+14>,M9.5.0/3,M4.1.0/4",
|
||||||
|
"Pacific/Auckland":"NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||||
|
"Pacific/Bougainville":"<+11>-11",
|
||||||
|
"Pacific/Chatham":"<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45",
|
||||||
|
"Pacific/Chuuk":"<+10>-10",
|
||||||
|
"Pacific/Easter":"<-06>6<-05>,M9.1.6/22,M4.1.6/22",
|
||||||
|
"Pacific/Efate":"<+11>-11",
|
||||||
|
"Pacific/Enderbury":"<+13>-13",
|
||||||
|
"Pacific/Fakaofo":"<+13>-13",
|
||||||
|
"Pacific/Fiji":"<+12>-12<+13>,M11.2.0,M1.2.3/99",
|
||||||
|
"Pacific/Funafuti":"<+12>-12",
|
||||||
|
"Pacific/Galapagos":"<-06>6",
|
||||||
|
"Pacific/Gambier":"<-09>9",
|
||||||
|
"Pacific/Guadalcanal":"<+11>-11",
|
||||||
|
"Pacific/Guam":"ChST-10",
|
||||||
|
"Pacific/Honolulu":"HST10",
|
||||||
|
"Pacific/Kiritimati":"<+14>-14",
|
||||||
|
"Pacific/Kosrae":"<+11>-11",
|
||||||
|
"Pacific/Kwajalein":"<+12>-12",
|
||||||
|
"Pacific/Majuro":"<+12>-12",
|
||||||
|
"Pacific/Marquesas":"<-0930>9:30",
|
||||||
|
"Pacific/Midway":"SST11",
|
||||||
|
"Pacific/Nauru":"<+12>-12",
|
||||||
|
"Pacific/Niue":"<-11>11",
|
||||||
|
"Pacific/Norfolk":"<+11>-11<+12>,M10.1.0,M4.1.0/3",
|
||||||
|
"Pacific/Noumea":"<+11>-11",
|
||||||
|
"Pacific/Pago_Pago":"SST11",
|
||||||
|
"Pacific/Palau":"<+09>-9",
|
||||||
|
"Pacific/Pitcairn":"<-08>8",
|
||||||
|
"Pacific/Pohnpei":"<+11>-11",
|
||||||
|
"Pacific/Port_Moresby":"<+10>-10",
|
||||||
|
"Pacific/Rarotonga":"<-10>10",
|
||||||
|
"Pacific/Saipan":"ChST-10",
|
||||||
|
"Pacific/Tahiti":"<-10>10",
|
||||||
|
"Pacific/Tarawa":"<+12>-12",
|
||||||
|
"Pacific/Tongatapu":"<+13>-13",
|
||||||
|
"Pacific/Wake":"<+12>-12",
|
||||||
|
"Pacific/Wallis":"<+12>-12",
|
||||||
|
"Etc/GMT":"GMT0",
|
||||||
|
"Etc/GMT-0":"GMT0",
|
||||||
|
"Etc/GMT-1":"<+01>-1",
|
||||||
|
"Etc/GMT-2":"<+02>-2",
|
||||||
|
"Etc/GMT-3":"<+03>-3",
|
||||||
|
"Etc/GMT-4":"<+04>-4",
|
||||||
|
"Etc/GMT-5":"<+05>-5",
|
||||||
|
"Etc/GMT-6":"<+06>-6",
|
||||||
|
"Etc/GMT-7":"<+07>-7",
|
||||||
|
"Etc/GMT-8":"<+08>-8",
|
||||||
|
"Etc/GMT-9":"<+09>-9",
|
||||||
|
"Etc/GMT-10":"<+10>-10",
|
||||||
|
"Etc/GMT-11":"<+11>-11",
|
||||||
|
"Etc/GMT-12":"<+12>-12",
|
||||||
|
"Etc/GMT-13":"<+13>-13",
|
||||||
|
"Etc/GMT-14":"<+14>-14",
|
||||||
|
"Etc/GMT0":"GMT0",
|
||||||
|
"Etc/GMT+0":"GMT0",
|
||||||
|
"Etc/GMT+1":"<-01>1",
|
||||||
|
"Etc/GMT+2":"<-02>2",
|
||||||
|
"Etc/GMT+3":"<-03>3",
|
||||||
|
"Etc/GMT+4":"<-04>4",
|
||||||
|
"Etc/GMT+5":"<-05>5",
|
||||||
|
"Etc/GMT+6":"<-06>6",
|
||||||
|
"Etc/GMT+7":"<-07>7",
|
||||||
|
"Etc/GMT+8":"<-08>8",
|
||||||
|
"Etc/GMT+9":"<-09>9",
|
||||||
|
"Etc/GMT+10":"<-10>10",
|
||||||
|
"Etc/GMT+11":"<-11>11",
|
||||||
|
"Etc/GMT+12":"<-12>12",
|
||||||
|
"Etc/UCT":"UTC0",
|
||||||
|
"Etc/UTC":"UTC0",
|
||||||
|
"Etc/Greenwich":"GMT0",
|
||||||
|
"Etc/Universal":"UTC0",
|
||||||
|
"Etc/Zulu":"UTC0"
|
||||||
|
}}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
URL="https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv"
|
||||||
|
(
|
||||||
|
first=1
|
||||||
|
echo "{\"timezones\": {"
|
||||||
|
curl --silent "$URL" | while read line; do
|
||||||
|
[ $first -ne 1 ] && echo ","
|
||||||
|
first=0
|
||||||
|
echo -n "${line/\",\"/\":\"}"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "}}"
|
||||||
|
) > src/webinterface/timezones.json
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Import("env")
|
||||||
|
|
||||||
|
env.Execute("gzip -9 < src/webinterface/index.html > src/webinterface/index.html.gz")
|
||||||
|
env.Execute("gzip -9 < src/webinterface/timezones.json > src/webinterface/timezones.json.gz")
|
||||||
|
|
||||||
|
def build(source, target, env):
|
||||||
|
env.Execute("rm src/webinterface/index.html.gz src/webinterface/timezones.json.gz")
|
||||||
|
|
||||||
|
env.AddPostAction("buildprog", build)
|
||||||
Reference in New Issue
Block a user