diff --git a/README.md b/README.md
index b0b59f8..5535699 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Welcome to HomeSpan - a robust and extremely easy-to-use Arduino library for cre
HomeSpan provides a microcontroller-focused implementation of Apple's HomeKit Accessory Protocol Specification Release R2 (HAP-R2) designed specifically for the Espressif ESP32 microcontroller running within the Arduino IDE. HomeSpan pairs directly to HomeKit via your home WiFi network without the need for any external bridges or components. With HomeSpan you can use the full power of the ESP32's I/O functionality to create custom control software and/or hardware to automatically operate external devices from the Home App on your iPhone, iPad, or Mac, or with Siri.
-HomeSpan requires version 2.0.0 or later of the [Arduino-ESP32 Board Manager](https://github.com/espressif/arduino-esp32), and has been tested up through version 2.0.11 (recommended). HomeSpan can be run on the original ESP32 as well as Espressif's ESP32-S2, ESP32-C3, and ESP32-S3 chips.
+HomeSpan requires version 2.0.0 or later of the [Arduino-ESP32 Board Manager](https://github.com/espressif/arduino-esp32), and has been tested up through version 2.0.14 (recommended). HomeSpan can be run on the original ESP32 as well as Espressif's ESP32-S2, ESP32-C3, and ESP32-S3 chips.
### HomeSpan Highlights
@@ -49,39 +49,29 @@ HomeSpan requires version 2.0.0 or later of the [Arduino-ESP32 Board Manager](ht
* Launch the WiFi Access Point
* A standalone, detailed End-User Guide
-## ❗Latest Update - HomeSpan 1.8.0 (7/8/2023)
+## ❗Latest Update - HomeSpan 1.8.1 (10/XX/2023)
-* **New Stepper Motor Control!**
+* **Memory-Usage Improvements**
+
+ * reduced RAM usage by 30-50K (as well as reduced reliance on large stack-based buffers)
+
+* **Added ability to "chain" *homeSpan* methods**
- * adds new **StepperControl** class that allows for smooth, uninterrupted operation of one or more stepper motors running in the background while HomeSpan continues to run simultaneously in the foreground
- * supports driver boards with or without PWM, including microstepping modes
- * supports automatic acceleration and deceleration for smooth starts and stops
- * motors can be set to an absolute position or instructucted to move a specified number of steps
- * provides options to automatically enter into a "brake" state after motor stops to conserve power
- * includes a fully worked example of a motorized window shade
- * see [Stepper Motor Control](docs/Stepper.md) for details
+ * converted various *homeSpan* methods that previously returned *void* to now return *Span &*
+ * allows for chaining multiple *homeSpan* methods
+ * example: `homeSpan.setControlPin(21).setStatusPin(13);`
+ * see [HomeSpan API Reference](docs/Reference.md) for details
-* **Upgrades to HomeSpan Web Log output**
+* **New Web Log Customizations**
- * adds new method `void homeSpan.setWebLogCSS(const char *css)` that allows you to define *Custom Style Sheets (CSS)* for the Web Log text, tables, and background
- * adds version numbers for the Sodium and MbedTLS libraries, HomeKit pairing status, and a text description of Reset Reason code
+ * adds new *homeSpan* method `setWebLogCallback(void (*func)(String &))` that provides a callback allowing you to extend the initial Web Log table with additional data of your own, as well as add an other custom HTML
* see [Message Logging](docs/Logging.md) for details
-* **Upgrades to Web Log Time Server initialization**
+* **New WiFi Callback Mechanism**
- * the process for retrieving the time and date from an NTP server upon booting now runs in the background as a separate task
- * HomeSpan is no longer blocked from running during the NTP query
-
-* **Adds new methods to disable HomeSpan's use of the USB Serial port**
-
- * new Log Level, -1, causes HomeSpan to suppress all OUTPUT messages
- * new homeSpan method `setSerialInputDisable(boolean val)` disables/re-enables HomeSpan's reading of CLI commands INPUT into the Arduino Serial Monitor
-
-* **Adds ability to use a non-standard LED as the HomeSpan Status LED**
-
- * new homeSpan method `setStatusDevice(Blinkable *sDev)` sets the Status LED to the Blinkable object *sDev*
- * allows an LED connected to a pin expander, or any other non-standard LED controller (such as an inverted LED that lights when a pin is LOW instead of HIGH) to be used as the HomeSpan Status LED
- * see [Blinkable.md](docs/Blinkable.md) for details (including an example) on how to create Blinkable objects
+ * adds new *homeSpan* method `setWifiCallbackAll(void (*func)(int))` that provides a callback every time WiFi is established, *or re-established after a disconnect*
+ * passes the number of times WiFi has been connected/re-connected as an argument
+ * see [API Reference](docs/Reference.md) for details
See [Releases](https://github.com/HomeSpan/HomeSpan/releases) for details on all changes and bug fixes included in this update.
diff --git a/docs/Logging.md b/docs/Logging.md
index 66d09f0..46ea9f0 100644
--- a/docs/Logging.md
+++ b/docs/Logging.md
@@ -81,7 +81,28 @@ For example, the following CSS changes the background color of the Web Log page
```
Note that HomeSpan outputs the full content of the Web Log HTML, including whatever CSS you may have specified above, to the Serial Monitor whenever the Log Level is set to 1 or greater. Reviewing this output can be helpful when creating your own CSS.
-
+
+### Adding User-Defined Data and/or Custom HTML
+
+Homespan provides a hook into the text used to generate the Web Log that you can extend to add your own data to the initial table as well as more generally add any custom HTML.
+
+To access this text, set a Web Log callback using `homeSpan.setWebLogCallback(void (*func)(String &htmlText))` where
+
+ * *func* is a function of type *void* that takes a single argument of type *String*, and
+ * *htmlText* will be set by HomeSpan to a String reference containing all the HTML text that the Web Log has already generated to produce the initial table.
+
+To add your own data to the table, simply extend the String *htmlText* by adding as many `
` and `| ` HTML tags as needed. If you wish to end the table and add any other HTML, simple include the `` tag in *htmlText*, and then add any other custom HTML. For example, the following function could be used to extend the initial Web Log table to show free DRAM, end the table, and provide a hot link to the HomeSpan Repo:
+
+```C++
+void extraData(String &r){
+ r+=" |
| Free DRAM: | " + String(esp_get_free_internal_heap_size()) + " bytes |
\n";
+ r+="Click Here to Access HomeSpan Repo
";
+}
+```
+
+To embed this custom HTML text in the Web Log, call `homeSpan.setWebLogCallback(extraWebData)` in your sketch.
+
+Note that *r* is being passed as a reference and already includes all the HTML text the Web Log has produced to set up the page and create the initial table. You should therefore *extend* this String using `+=`. Do not simple assign this variable to a new String with `=` or you will overwrite all the HTML already produced!
---
diff --git a/docs/Reference.md b/docs/Reference.md
index 1c49200..a7665bf 100644
--- a/docs/Reference.md
+++ b/docs/Reference.md
@@ -8,7 +8,7 @@ The HomeSpan Library is invoked by including *HomeSpan.h* in your Arduino sketch
## *homeSpan*
-At runtime HomeSpan will create a global **object** named `homeSpan` that supports the following methods:
+At runtime HomeSpan will create a global **object** named `homeSpan` (of type *class Span*) that supports the following methods:
* `void begin(Category catID, const char *displayName, const char *hostNameBase, const char *modelName)`
* initializes HomeSpan
@@ -26,20 +26,21 @@ At runtime HomeSpan will create a global **object** named `homeSpan` that suppor
---
-The following **optional** `homeSpan` methods override various HomeSpan initialization parameters used in `begin()`, and therefore **should** be called before `begin()` to take effect. If a method is *not* called, HomeSpan uses the default parameter indicated below:
+The following **optional** `homeSpan` methods override various HomeSpan initialization parameters used in `begin()`, and therefore **should** be called before `begin()` to take effect.
+Methods with a return type of `Span&` return a reference to `homeSpan` itself and can thus be chained together (e.g. `homeSpan.setControlPin(21).setStatusPin(13);`). If a method is *not* called, HomeSpan uses the default parameter indicated below:
-* `void setControlPin(uint8_t pin)`
+* `Span& setControlPin(uint8_t pin)`
* sets the ESP32 pin to use for the HomeSpan Control Button (which must connect the specified pin to **ground** when pushed). If not specified, HomeSpan will assume there is no Control Button
* `int getControlPin()`
* returns the pin number of the HomeSpan Control Button as set by `setControlPin(pin)`, or -1 if no pin has been set
-* `void setStatusPin(uint8_t pin)`
+* `Span& setStatusPin(uint8_t pin)`
* sets the ESP32 pin to use for the HomeSpan Status LED
* assumes a standard LED will be connected to *pin*
* if neither this method nor any equivalent method is called, HomeSpan will assume there is no Status LED
-* `void setStatusPixel(uint8_t pin, float h=0, float s=100, float v=100)`
+* `Span& setStatusPixel(uint8_t pin, float h=0, float s=100, float v=100)`
* sets the ESP32 pin to use for the HomeSpan Status LED
* this method is an *alternative* to using `setStatusPin()` above
* assumes an RGB NeoPixel (or equivalent) will be connected to *pin*
@@ -52,14 +53,14 @@ The following **optional** `homeSpan` methods override various HomeSpan initiali
* example: `homeSpan.setStatusPixel(8,120,100,20)` sets the Status LED to light green using a NeoPixel attached to pin 8
* if neither this method nor any equivalent method is called, HomeSpan will assume there is no Status LED
-* `void setStatusDevice(Blinkable *sDev)`
+* `Span& setStatusDevice(Blinkable *sDev)`
* sets the Status LED to a user-specified Blinkable device, *sDev*
* this method is an *alternative* to using either `setStatusPin()` or `setStatusPixel()` above
* see [Blinkable](Blinkable.md) for details on how to create generic Blinkable devices
* useful when using an LED connected to a pin expander, or other specialized driver, as the Status LED
* if neither this method nor any equivalent method is called, HomeSpan will assume there is no Status LED
-* `void setStatusAutoOff(uint16_t duration)`
+* `Span& setStatusAutoOff(uint16_t duration)`
* sets Status LED to automatically turn off after *duration* seconds
* Status LED will automatically turn on, and duration timer will be reset, whenever HomeSpan activates a new blinking pattern
* if *duration* is set to zero, auto-off is disabled (Status LED will remain on indefinitely)
@@ -67,19 +68,19 @@ The following **optional** `homeSpan` methods override various HomeSpan initiali
* `int getStatusPin()`
* returns the pin number of the Status LED as set by `setStatusPin(pin)`, or -1 if no pin has been set
-* `void setApSSID(const char *ssid)`
+* `Span& setApSSID(const char *ssid)`
* sets the SSID (network name) of the HomeSpan Setup Access Point (default="HomeSpan-Setup")
-* `void setApPassword(const char *pwd)`
+* `Span& setApPassword(const char *pwd)`
* sets the password of the HomeSpan Setup Access Point (default="homespan")
-* `void setApTimeout(uint16_t nSec)`
+* `Span& setApTimeout(uint16_t nSec)`
* sets the duration (in seconds) that the HomeSpan Setup Access Point, once activated, stays alive before timing out (default=300 seconds)
-* `void setCommandTimeout(uint16_t nSec)`
+* `Span& setCommandTimeout(uint16_t nSec)`
* sets the duration (in seconds) that the HomeSpan End-User Command Mode, once activated, stays alive before timing out (default=120 seconds)
-* `void setLogLevel(int level)`
+* `Span& setLogLevel(int level)`
* sets the logging level for diagnostic messages, where:
* 0 = top-level HomeSpan status messages, and any `LOG0()` messages specified in the sketch by the user (default)
* 1 = all HomeSpan status messages, and any `LOG1()` messages specified in the sketch by the user
@@ -93,7 +94,7 @@ The following **optional** `homeSpan` methods override various HomeSpan initiali
* `int getLogLevel()`
* returns the current Log Level as set by `setLogLevel(level)`
-* `void reserveSocketConnections(uint8_t nSockets)`
+* `Span& reserveSocketConnections(uint8_t nSockets)`
* reserves *nSockets* network sockets for uses **other than** by the HomeSpan HAP Server for HomeKit Controller Connections
* for sketches compiled under Arduino-ESP32 v2.0.1 or later, HomeSpan reserves 14 sockets for HAP Controller Connections
* each call to `reserveSocketConnections(nSockets)` reduces this number by *nSockets*
@@ -102,16 +103,16 @@ The following **optional** `homeSpan` methods override various HomeSpan initiali
* note you do not need to separately reserve sockets for built-in HomeSpan functionality
* for example, `enableOTA()` already contains an embedded call to `reserveSocketConnections(1)` since HomeSpan knows one socket must be reserved to support OTA
-* `void setPortNum(uint16_t port)`
+* `Span& setPortNum(uint16_t port)`
* sets the TCP port number used for communication between HomeKit and HomeSpan (default=80)
-* `void setHostNameSuffix(const char *suffix)`
+* `Span& setHostNameSuffix(const char *suffix)`
* sets the suffix HomeSpan appends to *hostNameBase* to create the full hostName
* if not specified, the default is for HomeSpan to append a dash "-" followed the 6-byte Accessory ID of the HomeSpan device
* setting *suffix* to a null string "" is permitted
* example: `homeSpan.begin(Category::Fans, "Living Room Ceiling Fan", "LivingRoomFan");` will yield a default *hostName* of the form *LivingRoomFan-A1B2C3D4E5F6.local*. Calling `homeSpan.setHostNameSuffix("v2")` prior to `homeSpan.begin()` will instead yield a *hostName* of *LivingRoomFanv2.local*
-* `void setQRID(const char *id)`
+* `Span& setQRID(const char *id)`
* changes the Setup ID, which is used for pairing a device with a [QR Code](QRCodes.md), from the HomeSpan default to *id*
* the HomeSpan default is "HSPN" unless permanently changed for the device via the [HomeSpan CLI](CLI.md) using the 'Q' command
* *id* must be exactly 4 alphanumeric characters (0-9, A-Z, and a-z). If not, the request to change the Setup ID is silently ignored and the default is used instead
@@ -135,11 +136,11 @@ The following **optional** `homeSpan` methods enable additional features and pro
* this command causes HomeSpan to ignore, but does not otherwise alter, any password stored using the 'O' command
* returns 0 if enabling OTA was successful, or -1 and reports an error to the Serial Monitor if not
-* `void enableAutoStartAP()`
+* `Span& enableAutoStartAP()`
* enables automatic start-up of WiFi Access Point if WiFi Credentials are **not** found at boot time
* methods to alter the behavior of HomeSpan's Access Point, such as `setApTimeout()`, must be called prior to `enableAutoStartAP()` to have an effect
-* `void setApFunction(void (*func)())`
+* `Span& setApFunction(void (*func)())`
* replaces HomeSpan's built-in WiFi Access Point with user-defined function *func*
* *func* must be of type *void* and have no arguments
* *func* will be called instead of HomeSpan's built-in WiFi Access Point whenever the Access Point is launched:
@@ -149,22 +150,25 @@ The following **optional** `homeSpan` methods enable additional features and pro
* after identifying the SSID and password of the desired network, *func* must call `setWifiCredentials()` to save and use these values
* it is recommended that *func* terminates by restarting the device using `ESP.restart()`. Upon restart HomeSpan will use the SSID and password just saved
-* `void setWifiCredentials(const char *ssid, const char *pwd)`
+* `Span& setWifiCredentials(const char *ssid, const char *pwd)`
* sets the SSID (*ssid*) and password (*pwd*) of the WiFi network to which HomeSpan will connect
* *ssid* and *pwd* are automatically saved in HomeSpan's non-volatile storage (NVS) for retrieval when the device restarts
* note that the saved values are truncated if they exceed the maximum allowable characters (ssid=32; pwd=64)
* :warning: SECURITY WARNING: The purpose of this function is to allow advanced users to *dynamically* set the device's WiFi Credentials using a customized Access Point function specified by `setApFunction(func)`. It it NOT recommended to use this function to hardcode your WiFi SSID and password directly into your sketch. Instead, use one of the more secure methods provided by HomeSpan, such as typing 'W' from the CLI, or launching HomeSpan's Access Point, to set your WiFi credentials without hardcoding them into your sketch
-* `void setWifiCallback(void (*func)())`
- * sets an optional user-defined callback function, *func*, to be called by HomeSpan upon start-up just after WiFi connectivity has been established. This one-time call to *func* is provided for users that are implementing other network-related services as part of their sketch, but that cannot be started until WiFi connectivity is established. The function *func* must be of type *void* and have no arguments
+* `Span& setWifiCallback(void (*func)())`
+ * sets an optional user-defined callback function, *func*, to be called by HomeSpan upon start-up just after WiFi connectivity has been initially established. This one-time call to *func* is provided for users that are implementing other network-related services as part of their sketch, but that cannot be started until WiFi connectivity is established. The function *func* must be of type *void* and have no arguments
-* `void setPairCallback(void (*func)(boolean status))`
+* `Span& setWifiCallbackAll(void (*func)(int count))`
+ * similar to `setWiFiCallback()` above, but the user-defined callback function, *func*, is called by HomeSpan *every* time WiFi connectivity has been established or re-established after a disconnect. The function *func* must be of type *void* and accept a single int argument, *count*, into which HomeSpan passes the number of times WiFi has been established or re-established (i.e. count=1 on initial WiFi connection; count=2 if re-established after the first disconnect, etc.)
+
+* `Span& setPairCallback(void (*func)(boolean status))`
* sets an optional user-defined callback function, *func*, to be called by HomeSpan upon completion of pairing to a controller (*status=true*) or unpairing from a controller (*status=false*)
* this one-time call to *func* is provided for users that would like to trigger additional actions when the device is first paired, or the device is later unpaired
* note this *func* is **not** called upon start-up and should not be used to simply check whether a device is paired or unpaired. It is only called when pairing status changes
* the function *func* must be of type *void* and accept one *boolean* argument
-* `void setStatusCallback(void (*func)(HS_STATUS status))`
+* `Span& setStatusCallback(void (*func)(HS_STATUS status))`
* sets an optional user-defined callback function, *func*, to be called by HomeSpan whenever its running state (e.g. WiFi Connecting, Pairing Needed...) changes in way that would alter the blinking pattern of the (optional) Status LED
* if *func* is set, it will be called regardless of whether or not a Status LED has actually been defined
* this allows users to reflect changes to the current state of HomeSpan using alternative methods, such as outputting messages to an embedded LCD or E-Ink display
@@ -174,14 +178,14 @@ The following **optional** `homeSpan` methods enable additional features and pro
* returns a pre-defined character string message representing *s*, which must be of enum type [HS_STATUS](HS_STATUS.md)
* typically used in conjunction with `setStatusCallback()` above
-* `void setPairingCode(const char *s)`
+* `Span& setPairingCode(const char *s)`
* sets the Setup Pairing Code to *s*, which **must** be exactly eight numerical digits (no dashes)
* example: `homeSpan.setPairingCode("46637726");`
* a hashed version of the Pairing Code will be saved to the device's non-volatile storage, overwriting any currently-stored Pairing Code
* if *s* contains an invalid code, an error will be reported and the code will *not* be saved. Instead, the currently-stored Pairing Code (or the HomeSpan default Pairing Code if no code has been stored) will be used
* :warning: SECURTY WARNING: Hardcoding a device's Pairing Code into your sketch is considered a security risk and is **not** recommended. Instead, use one of the more secure methods provided by HomeSpan, such as typing 'S \' from the CLI, or launching HomeSpan's Access Point, to set your Pairing Code without hardcoding it into your sketch
-* `void setSketchVersion(const char *sVer)`
+* `Span& setSketchVersion(const char *sVer)`
* sets the version of a HomeSpan sketch to *sVer*, which can be any arbitrary character string
* if unspecified, HomeSpan uses "n/a" as the default version text
* HomeSpan displays the version of the sketch in the Arduino IDE Serial Monitor upon start-up
@@ -191,7 +195,7 @@ The following **optional** `homeSpan` methods enable additional features and pro
* returns the version of a HomeSpan sketch, as set using `void setSketchVersion(const char *sVer)`, or "n/a" if not set
* can by called from anywhere in a sketch
-* `void enableWebLog(uint16_t maxEntries, const char *timeServerURL, const char *timeZone, const char *logURL)`
+* `Span& enableWebLog(uint16_t maxEntries, const char *timeServerURL, const char *timeZone, const char *logURL)`
* enables a rolling Web Log that displays the most recent *maxEntries* entries created by the user with the `WEBLOG()` macro. Parameters, and their default values if unspecified, are as follows:
* *maxEntries* - maximum number of (most recent) entries to save. If unspecified, defaults to 0, in which case the Web Log will only display status without any log entries
* *timeServerURL* - the URL of a time server that HomeSpan will use to set its clock upon startup after a WiFi connection has been established. If unspecified, defaults to NULL, in which case HomeSpan skips setting the device clock
@@ -201,12 +205,18 @@ The following **optional** `homeSpan` methods enable additional features and pro
* when attemping to connect to *timeServerURL*, HomeSpan waits 120 seconds for a response. This is done in the background and does not block HomeSpan from running as usual while it tries to set the time. If no response is received after the 120-second timeout period, HomeSpan assumes the server is unreachable and skips the clock-setting procedure. Use `setTimeServerTimeout()` to re-configure the 120-second timeout period to another value
* see [Message Logging](Logging.md) for complete details
-* `void setTimeServerTimeout(uint32_t tSec)`
+* `Span& setTimeServerTimeout(uint32_t tSec)`
* changes the default 120-second timeout period HomeSpan uses when `enableWebLog()` tries set the device clock from an internet time server to *tSec* seconds
-* `void setWebLogCSS(const char *css)`
+* `Span& setWebLogCSS(const char *css)`
* sets the format of the HomeSpan Web Log to the custom style sheet specified by *css*
* see [Message Logging](Logging.md) for details on how to construct *css*
+
+* `Span& setWebLogCallback(void (*func)(String &htmlText))`
+ * sets an optional user-defined callback function, *func*, to be called by HomeSpan whenever the Web Log is produced
+ * allows user to add additional custom data to the initial table of the Web Log by **extending** the String *htmlText*, which is passed as a reference to *func*
+ * the function *func* must be of type *void* and accept one argument of type String
+ * see [Message Logging](Logging.md) for details on how to construct *htmlText*
* `void processSerialCommand(const char *CLIcommand)`
* processes the *CLIcommand* just as if were typed into the Serial Monitor
@@ -215,7 +225,7 @@ The following **optional** `homeSpan` methods enable additional features and pro
* example: `homeSpan.processSerialCommand("A");` starts the HomeSpan Setup Access Point
* example: `homeSpan.processSerialCommand("Q HUB3");` changes the HomeKit Setup ID for QR Codes to "HUB3"
-* `void setSerialInputDisable(boolean val)`
+* `Span& setSerialInputDisable(boolean val)`
* if *val* is true, disables HomeSpan from reading input from the Serial port
* if *val* is false, re-enables HomeSpan reading input from the Serial port
* useful when the main USB Serial port is needed for reading data from an external Serial peripheral, rather than being used to read input from the Arduino Serial Monitor
@@ -258,7 +268,7 @@ The following `homeSpan` methods are considered experimental, since not all use
* *stackSize* - size of stack, in bytes, used by the polling task. Default=8192 if unspecified
* *priority* - priority at which task runs. Minimum is 1. Maximum is typically 24, but it depends on how the ESP32 operating system is configured. If you set it to an arbitrarily high value (e.g. 999), it will be set to the maximum priority allowed. Default=1 if unspecified
- * *cpu* - specifies the CPU on which the polling task will run. Valid values are 0 and 1. This paramater is ignored on single-cpu boards. Default=0 if unspecified
+ * *cpu* - specifies the CPU on which the polling task will run. Valid values are 0 and 1. This parameter is ignored on single-cpu boards. Default=0 if unspecified
* if used, **must** be placed in a sketch as the last line in the Arduino `setup()` method
* HomeSpan will throw and error and halt if both `poll()`and `autoPoll()` are used in the same sketch - either place `poll()` in the Arduino `loop()` method **or** place `autoPoll()` at the the end of the Arduino `setup()` method
* if this method is used, and you have no need to add your own code to the main Arduino `loop()`, you can safely skip defining a blank `void loop(){}` function in your sketch
@@ -496,7 +506,7 @@ Creating an instance of this **class** attaches a toggle-switch handler to the E
* 3=switch is closed (`SpanToggle::CLOSED`)
* 4=switch is open (`SpanToggle::OPEN`)
-Note there are no *singleTime*, *longTime*, or *doubleTime* paramaters in the constructor since you can't single-press, double-press, or long-press a toggle switch. Instead, the constructor supports the single parameter *toggleTime* (default=5ms if left unspecified) that sets the minimum time at which the switch needs to be moved to the closed position in order to trigger a call to the `button()` method. This effectively "debounces" the toggle switch.
+Note there are no *singleTime*, *longTime*, or *doubleTime* parameters in the constructor since you can't single-press, double-press, or long-press a toggle switch. Instead, the constructor supports the single parameter *toggleTime* (default=5ms if left unspecified) that sets the minimum time at which the switch needs to be moved to the closed position in order to trigger a call to the `button()` method. This effectively "debounces" the toggle switch.
SpanToggle also supports the following additional method:
diff --git a/examples/Other Examples/Pixel/Pixel.ino b/examples/Other Examples/Pixel/Pixel.ino
index 5723de4..7c78dd6 100644
--- a/examples/Other Examples/Pixel/Pixel.ino
+++ b/examples/Other Examples/Pixel/Pixel.ino
@@ -72,9 +72,9 @@ struct NeoPixel_RGB : Service::LightBulb { // Addressable single-wire RGB L
Characteristic::Saturation S{0,true};
Characteristic::Brightness V{100,true};
Pixel *pixel;
- uint8_t nPixels;
+ int nPixels;
- NeoPixel_RGB(uint8_t pin, uint8_t nPixels) : Service::LightBulb(){
+ NeoPixel_RGB(uint8_t pin, int nPixels) : Service::LightBulb(){
V.setRange(5,100,1); // sets the range of the Brightness to be from a min of 5%, to a max of 100%, in steps of 1%
pixel=new Pixel(pin); // creates Pixel LED on specified pin
@@ -106,9 +106,9 @@ struct NeoPixel_RGBW : Service::LightBulb { // Addressable single-wire RGBW
Characteristic::Brightness V{100,true};
Characteristic::ColorTemperature T{140,true};
Pixel *pixel;
- uint8_t nPixels;
+ int nPixels;
- NeoPixel_RGBW(uint8_t pin, uint8_t nPixels) : Service::LightBulb(){
+ NeoPixel_RGBW(uint8_t pin, int nPixels) : Service::LightBulb(){
V.setRange(5,100,1); // sets the range of the Brightness to be from a min of 5%, to a max of 100%, in steps of 1%
pixel=new Pixel(pin,true); // creates Pixel RGBW LED (second parameter set to true for RGBW) on specified pin
@@ -142,9 +142,9 @@ struct DotStar_RGB : Service::LightBulb { // Addressable two-wire RGB LED S
Characteristic::Saturation S{0,true};
Characteristic::Brightness V{100,true};
Dot *pixel;
- uint8_t nPixels;
+ int nPixels;
- DotStar_RGB(uint8_t dataPin, uint8_t clockPin, uint8_t nPixels) : Service::LightBulb(){
+ DotStar_RGB(uint8_t dataPin, uint8_t clockPin, int nPixels) : Service::LightBulb(){
V.setRange(5,100,1); // sets the range of the Brightness to be from a min of 5%, to a max of 100%, in steps of 1%
pixel=new Dot(dataPin,clockPin); // creates Dot LED on specified pins
diff --git a/src/HAP.cpp b/src/HAP.cpp
index 39bed60..66910b3 100644
--- a/src/HAP.cpp
+++ b/src/HAP.cpp
@@ -80,7 +80,7 @@ void HAPClient::init(){
if(!nvs_get_blob(hapNVS,"ACCESSORY",NULL,&len)){ // if found long-term Accessory data in NVS
nvs_get_blob(hapNVS,"ACCESSORY",&accessory,&len); // retrieve data
} else {
- LOG0("Generating new random Accessory ID and Long-Term Ed25519 Signature Keys...\n");
+ LOG0("Generating new random Accessory ID and Long-Term Ed25519 Signature Keys...\n\n");
uint8_t buf[6];
char cBuf[18];
@@ -95,17 +95,15 @@ void HAPClient::init(){
nvs_commit(hapNVS); // commit to NVS
}
- if(!nvs_get_blob(hapNVS,"CONTROLLERS",NULL,&len)){ // if found long-term Controller Pairings data from NVS
- nvs_get_blob(hapNVS,"CONTROLLERS",controllers,&len); // retrieve data
- } else {
- LOG0("Initializing storage for Paired Controllers data...\n\n");
-
- HAPClient::removeControllers(); // clear all Controller data
-
- nvs_set_blob(hapNVS,"CONTROLLERS",controllers,sizeof(controllers)); // update data
- nvs_commit(hapNVS); // commit to NVS
+ if(!nvs_get_blob(hapNVS,"CONTROLLERS",NULL,&len)){ // if found long-term Controller Pairings data from NVS
+ TempBuffer tBuf(len/sizeof(Controller));
+ nvs_get_blob(hapNVS,"CONTROLLERS",tBuf.get(),&len); // retrieve data
+ for(int i=0;iMAX_HTTP){ // exceeded maximum number of bytes allowed
+ badRequestError();
+ LOG0("\n*** ERROR: HTTP message of %d bytes exceeds maximum allowed (%d)\n\n",messageSize,MAX_HTTP);
+ return;
+ }
+
+ TempBuffer httpBuf(messageSize+1); // leave room for null character added below
if(cPair){ // expecting encrypted message
LOG2("<<<< #### ");
LOG2(client.remoteIP());
LOG2(" #### <<<<\n");
- nBytes=receiveEncrypted(); // decrypt and return number of bytes
+ nBytes=receiveEncrypted(httpBuf.get(),messageSize); // decrypt and return number of bytes read
- if(!nBytes){ // decryption failed (error message already printed in function)
+ if(!nBytes){ // decryption failed (error message already printed in function)
badRequestError();
return;
}
@@ -173,22 +176,22 @@ void HAPClient::processRequest(){
LOG2(client.remoteIP());
LOG2(" <<<<<<<<<\n");
- nBytes=client.read(httpBuf,MAX_HTTP+1); // read all available bytes up to maximum allowed+1
-
- if(nBytes>MAX_HTTP){ // exceeded maximum number of bytes allowed
+ nBytes=client.read(httpBuf.get(),messageSize); // read expected number of bytes
+
+ if(nBytes!=messageSize || client.available()!=0){
badRequestError();
- LOG0("\n*** ERROR: Exceeded maximum HTTP message length\n\n");
+ LOG0("\n*** ERROR: HTTP message not read correctly. Expected %d bytes, read %d bytes, %d bytes remaining\n\n",messageSize,nBytes,client.available());
return;
}
-
+
} // encrypted/plaintext
- httpBuf[nBytes]='\0'; // add null character to enable string functions
+ httpBuf.get()[nBytes]='\0'; // add null character to enable string functions
- char *body=(char *)httpBuf; // char pointer to start of HTTP Body
- char *p; // char pointer used for searches
-
- if(!(p=strstr((char *)httpBuf,"\r\n\r\n"))){
+ char *body=(char *)httpBuf.get(); // char pointer to start of HTTP Body
+ char *p; // char pointer used for searches
+
+ if(!(p=strstr((char *)httpBuf.get(),"\r\n\r\n"))){
badRequestError();
LOG0("\n*** ERROR: Malformed HTTP request (can't find blank line indicating end of BODY)\n\n");
return;
@@ -233,7 +236,7 @@ void HAPClient::processRequest(){
tlv8.print(2); // print TLV records in form "TAG(INT) LENGTH(INT) VALUES(HEX)"
LOG2("------------ END TLVS! ------------\n");
- postPairVerifyURL(); // process URL
+ postPairVerifyURL(); // process URL
return;
}
@@ -243,17 +246,7 @@ void HAPClient::processRequest(){
tlv8.print(2); // print TLV records in form "TAG(INT) LENGTH(INT) VALUES(HEX)"
LOG2("------------ END TLVS! ------------\n");
- postPairingsURL(); // process URL
- return;
- }
-
- if(!strncmp(body,"POST /pairings ",15) && // POST PAIRINGS
- strstr(body,"Content-Type: application/pairing+tlv8") && // check that content is TLV8
- tlv8.unpack(content,cLen)){ // read TLV content
- tlv8.print(2); // print TLV records in form "TAG(INT) LENGTH(INT) VALUES(HEX)"
- LOG2("------------ END TLVS! ------------\n");
-
- postPairingsURL(); // process URL
+ postPairingsURL(); // process URL
return;
}
@@ -482,7 +475,7 @@ int HAPClient::postPairSetupURL(){
case pairState_M5: // 'Exchange Request'
- if(!tlv8.buf(kTLVType_EncryptedData)){
+ if(!tlv8.len(kTLVType_EncryptedData)){
LOG0("\n*** ERROR: Required 'EncryptedData' TLV record for this step is bad or missing\n\n");
tlv8.clear(); // clear TLV records
tlv8.val(kTLVType_State,pairState_M6); // set State=
@@ -532,7 +525,7 @@ int HAPClient::postPairSetupURL(){
tlv8.print(2); // print decrypted TLV data
LOG2("------- END DECRYPTED TLVS! -------\n");
- if(!tlv8.buf(kTLVType_Identifier) || !tlv8.buf(kTLVType_PublicKey) || !tlv8.buf(kTLVType_Signature)){
+ if(!tlv8.len(kTLVType_Identifier) || !tlv8.len(kTLVType_PublicKey) || !tlv8.len(kTLVType_Signature)){
LOG0("\n*** ERROR: One or more of required 'Identifier,' 'PublicKey,' and 'Signature' TLV records for this step is bad or missing\n\n");
tlv8.clear(); // clear TLV records
tlv8.val(kTLVType_State,pairState_M6); // set State=
@@ -578,9 +571,6 @@ int HAPClient::postPairSetupURL(){
addController(iosDevicePairingID,iosDeviceLTPK,true); // save Pairing ID and LTPK for this Controller with admin privileges
- nvs_set_blob(hapNVS,"CONTROLLERS",controllers,sizeof(controllers)); // update data
- nvs_commit(hapNVS); // commit to NVS
-
// Now perform the above steps in reverse to securely transmit the AccessoryLTPK to the Controller (HAP Section 5.6.6.2)
uint8_t accessoryX[32];
@@ -604,8 +594,8 @@ int HAPClient::postPairSetupURL(){
crypto_sign_detached(tlv8.buf(kTLVType_Signature,64),NULL,accessoryInfo,accessoryInfoLen,accessory.LTSK); // produce signature of accessoryInfo using AccessoryLTSK (Ed25519 long-term secret key)
- memcpy(tlv8.buf(kTLVType_Identifier,accessoryPairingIDLen),accessoryPairingID,accessoryPairingIDLen); // set Identifier TLV record as accessoryPairingID
- memcpy(tlv8.buf(kTLVType_PublicKey,accessoryLTPKLen),accessoryLTPK,accessoryLTPKLen); // set PublicKey TLV record as accessoryLTPK
+ tlv8.buf(kTLVType_Identifier,accessoryPairingID,accessoryPairingIDLen); // set Identifier TLV record as accessoryPairingID
+ tlv8.buf(kTLVType_PublicKey,accessoryLTPK,accessoryLTPKLen); // set PublicKey TLV record as accessoryLTPK
LOG2("------- ENCRYPTING SUB-TLVS -------\n");
@@ -685,7 +675,7 @@ int HAPClient::postPairVerifyURL(){
case pairState_M1: // 'Verify Start Request'
- if(!tlv8.buf(kTLVType_PublicKey)){
+ if(!tlv8.len(kTLVType_PublicKey)){
LOG0("\n*** ERROR: Required 'PublicKey' TLV record for this step is bad or missing\n\n");
tlv8.clear(); // clear TLV records
tlv8.val(kTLVType_State,pairState_M2); // set State=
@@ -717,7 +707,7 @@ int HAPClient::postPairVerifyURL(){
crypto_sign_detached(tlv8.buf(kTLVType_Signature,64),NULL,accessoryInfo,accessoryInfoLen,accessory.LTSK); // produce signature of accessoryInfo using AccessoryLTSK (Ed25519 long-term secret key)
- memcpy(tlv8.buf(kTLVType_Identifier,accessoryPairingIDLen),accessoryPairingID,accessoryPairingIDLen); // set Identifier TLV record as accessoryPairingID
+ tlv8.buf(kTLVType_Identifier,accessoryPairingID,accessoryPairingIDLen); // set Identifier TLV record as accessoryPairingID
LOG2("------- ENCRYPTING SUB-TLVS -------\n");
@@ -739,9 +729,9 @@ int HAPClient::postPairVerifyURL(){
LOG2("---------- END SUB-TLVS! ----------\n");
- tlv8.buf(kTLVType_EncryptedData,edLen); // set length of EncryptedData TLV record, which should now include the Authentication Tag at the end as required by HAP
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- memcpy(tlv8.buf(kTLVType_PublicKey,32),publicCurveKey,32); // set PublicKey to Accessory's Curve25519 public key
+ tlv8.buf(kTLVType_EncryptedData,edLen); // set length of EncryptedData TLV record, which should now include the Authentication Tag at the end as required by HAP
+ tlv8.val(kTLVType_State,pairState_M2); // set State=
+ tlv8.buf(kTLVType_PublicKey,publicCurveKey,32); // set PublicKey to Accessory's Curve25519 public key
tlvRespond(); // send response to client
return(1);
@@ -751,7 +741,7 @@ int HAPClient::postPairVerifyURL(){
case pairState_M3: // 'Verify Finish Request'
- if(!tlv8.buf(kTLVType_EncryptedData)){
+ if(!tlv8.len(kTLVType_EncryptedData)){
LOG0("\n*** ERROR: Required 'EncryptedData' TLV record for this step is bad or missing\n\n");
tlv8.clear(); // clear TLV records
tlv8.val(kTLVType_State,pairState_M4); // set State=
@@ -788,19 +778,21 @@ int HAPClient::postPairVerifyURL(){
tlv8.print(2); // print decrypted TLV data
LOG2("------- END DECRYPTED TLVS! -------\n");
- if(!tlv8.buf(kTLVType_Identifier) || !tlv8.buf(kTLVType_Signature)){
+ if(!tlv8.len(kTLVType_Identifier) || !tlv8.len(kTLVType_Signature)){
LOG0("\n*** ERROR: One or more of required 'Identifier,' and 'Signature' TLV records for this step is bad or missing\n\n");
tlv8.clear(); // clear TLV records
tlv8.val(kTLVType_State,pairState_M4); // set State=
tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data)
tlvRespond(); // send response to client
return(0);
- };
+ }
Controller *tPair; // temporary pointer to Controller
-
+
if(!(tPair=findController(tlv8.buf(kTLVType_Identifier)))){
- LOG0("\n*** ERROR: Unrecognized Controller PairingID\n\n");
+ LOG0("\n*** ERROR: Unrecognized Controller ID: ");
+ charPrintRow(tlv8.buf(kTLVType_Identifier),36,2);
+ LOG0("\n\n");
tlv8.clear(); // clear TLV records
tlv8.val(kTLVType_State,pairState_M4); // set State=
tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication
@@ -808,6 +800,10 @@ int HAPClient::postPairVerifyURL(){
return(0);
}
+ LOG2("\n*** Verifying session with Controller ID: ");
+ charPrintRow(tPair->ID,36,2);
+ LOG2("...\n");
+
size_t iosDeviceInfoLen=32+36+32;
uint8_t iosDeviceInfo[iosDeviceInfoLen];
@@ -828,7 +824,7 @@ int HAPClient::postPairVerifyURL(){
tlv8.val(kTLVType_State,pairState_M4); // set State=
tlvRespond(); // send response to client (unencrypted since cPair=NULL)
- cPair=tPair; // save Controller for this connection slot - connection is not verified and should be encrypted going forward
+ cPair=tPair; // save Controller for this connection slot - connection is now verified and should be encrypted going forward
hkdf.create(a2cKey,sharedCurveKey,32,"Control-Salt","Control-Read-Encryption-Key"); // create AccessoryToControllerKey (HAP Section 6.5.2)
hkdf.create(c2aKey,sharedCurveKey,32,"Control-Salt","Control-Write-Encryption-Key"); // create ControllerToAccessoryKey (HAP Section 6.5.2)
@@ -864,21 +860,21 @@ int HAPClient::getAccessoriesURL(){
int nBytes = homeSpan.sprintfAttributes(NULL); // get size of HAP attributes JSON
TempBuffer jBuf(nBytes+1);
- homeSpan.sprintfAttributes(jBuf.buf); // create JSON database (will need to re-cast to uint8_t* below)
+ homeSpan.sprintfAttributes(jBuf.get()); // create JSON database (will need to re-cast to uint8_t* below)
- int nChars=snprintf(NULL,0,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); // create '200 OK' Body with Content Length = size of JSON Buf
- char body[nChars+1];
- sprintf(body,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);
+ char *body;
+ asprintf(&body,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);
LOG2("\n>>>>>>>>>> ");
LOG2(client.remoteIP());
LOG2(" >>>>>>>>>>\n");
LOG2(body);
- LOG2(jBuf.buf);
+ LOG2(jBuf.get());
LOG2("\n");
- sendEncrypted(body,(uint8_t *)jBuf.buf,nBytes);
-
+ sendEncrypted(body,(uint8_t *)jBuf.get(),nBytes);
+ free(body);
+
return(1);
} // getAccessories
@@ -892,8 +888,6 @@ int HAPClient::postPairingsURL(){
return(0);
}
- Controller *newCont;
-
LOG1("In Post Pairings #");
LOG1(conNum);
LOG1(" (");
@@ -905,109 +899,87 @@ int HAPClient::postPairingsURL(){
badRequestError(); // return with 400 error, which closes connection
return(0);
}
-
+
switch(tlv8.val(kTLVType_Method)){
- case 3:
+ case 3: {
LOG1("Add...\n");
- if(!tlv8.buf(kTLVType_Identifier) || !tlv8.buf(kTLVType_PublicKey) || !tlv8.buf(kTLVType_Permissions)){
+ if(!tlv8.len(kTLVType_Identifier) || !tlv8.len(kTLVType_PublicKey) || !tlv8.len(kTLVType_Permissions)){
LOG0("\n*** ERROR: One or more of required 'Identifier,' 'PublicKey,' and 'Permissions' TLV records for this step is bad or missing\n\n");
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data)
- break;
- }
+ tlv8.clear();
+ tlv8.val(kTLVType_Error,tagError_Unknown);
- if(!cPair->admin){
+ } else if(!cPair->admin){
LOG0("\n*** ERROR: Controller making request does not have admin privileges to add/update other Controllers\n\n");
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication
- break;
- }
-
- if((newCont=findController(tlv8.buf(kTLVType_Identifier))) && memcmp(tlv8.buf(kTLVType_PublicKey),newCont->LTPK,32)){ // requested Controller already exists, but LTPKs don't match
- LOG0("\n*** ERROR: Invalid request to update the LTPK of an exsiting Controller\n\n");
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown
- break;
- }
-
- if(!addController(tlv8.buf(kTLVType_Identifier),tlv8.buf(kTLVType_PublicKey),tlv8.val(kTLVType_Permissions)==1?true:false)){
- LOG0("\n*** ERROR: Can't pair more than %d Controllers\n\n",MAX_CONTROLLERS);
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- tlv8.val(kTLVType_Error,tagError_MaxPeers); // set Error=MaxPeers
- break;
+ tlv8.clear();
+ tlv8.val(kTLVType_Error,tagError_Authentication);
+
+ } else {
+ tagError err=addController(tlv8.buf(kTLVType_Identifier),tlv8.buf(kTLVType_PublicKey),tlv8.val(kTLVType_Permissions));
+ tlv8.clear();
+ if(err!=tagError_None)
+ tlv8.val(kTLVType_Error,err);
}
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- break;
+ tlv8.val(kTLVType_State,pairState_M2);
+ tlvRespond();
+ return(1);
+ }
- case 4:
+ case 4: {
LOG1("Remove...\n");
- if(!tlv8.buf(kTLVType_Identifier)){
+ uint8_t id[36];
+
+ if(!tlv8.len(kTLVType_Identifier)){
LOG0("\n*** ERROR: Required 'Identifier' TLV record for this step is bad or missing\n\n");
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- tlv8.val(kTLVType_Error,tagError_Unknown); // set Error=Unknown (there is no specific error type for missing/bad TLV data)
- break;
- }
-
- if(!cPair->admin){
+ tlv8.clear();
+ tlv8.val(kTLVType_Error,tagError_Unknown);
+
+ } else if(!cPair->admin){
LOG0("\n*** ERROR: Controller making request does not have admin privileges to remove Controllers\n\n");
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- tlv8.val(kTLVType_Error,tagError_Authentication); // set Error=Authentication
- break;
+ tlv8.clear();
+ tlv8.val(kTLVType_Error,tagError_Authentication);
+ } else {
+ memcpy(id,tlv8.buf(kTLVType_Identifier),36);
+ tlv8.clear();
}
- removeController(tlv8.buf(kTLVType_Identifier));
+ tlv8.val(kTLVType_State,pairState_M2);
+ tlvRespond(); // must send response before removing Controller below
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- break;
+ if(tlv8.val(kTLVType_Error)==-1)
+ removeController(id);
- case 5:
+ return(1);
+ }
+
+ case 5: {
LOG1("List...\n");
- // NEEDS TO BE IMPLEMENTED - UNSURE IF THIS IS EVER USED BY HOMEKIT
+ TempBuffer tBuf(listControllers(NULL));
- tlv8.clear(); // clear TLV records
- tlv8.val(kTLVType_State,pairState_M2); // set State=
- break;
+ char *body;
+ asprintf(&body,"HTTP/1.1 200 OK\r\nContent-Type: application/pairing+tlv8\r\nContent-Length: %d\r\n\r\n",tBuf.len()); // create Body with Content Length = size of TLV data
+
+ LOG2("\n>>>>>>>>>> ");
+ LOG2(client.remoteIP());
+ LOG2(" >>>>>>>>>>\n");
+ LOG2(body);
+ listControllers(tBuf.get());
+ sendEncrypted(body,tBuf.get(),tBuf.len());
+ free(body);
+
+ return(1);
+ }
- default:
+ default: {
LOG0("\n*** ERROR: 'Method' TLV record is either missing or not set to either 3, 4, or 5 as required\n\n");
badRequestError(); // return with 400 error, which closes connection
return(0);
- break;
- }
-
- nvs_set_blob(hapNVS,"CONTROLLERS",controllers,sizeof(controllers)); // update Controller data
- nvs_commit(hapNVS); // commit to NVS
-
- tlvRespond();
-
- // re-check connections and close any (or all) clients as a result of controllers that were removed above
- // must be performed AFTER sending the TLV response, since that connection itself may be terminated below
-
- for(int i=0;iclient){ // if slot is connected
-
- if(!nAdminControllers() || (hap[i]->cPair && !hap[i]->cPair->allocated)){ // accessory unpaired, OR client connection is verified but points to a newly *unallocated* controller
- LOG1("*** Terminating Client #");
- LOG1(i);
- LOG1("\n");
- hap[i]->client.stop();
- }
-
- } // if client connected
- } // loop over all connection slots
+ }
+ } // switch
return(1);
}
@@ -1077,9 +1049,8 @@ int HAPClient::getCharacteristicsURL(char *urlBuf){
boolean sFlag=strstr(jsonBuf,"status"); // status attribute found?
- int nChars=snprintf(NULL,0,"HTTP/1.1 %s\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",!sFlag?"200 OK":"207 Multi-Status",nBytes);
- char body[nChars+1];
- sprintf(body,"HTTP/1.1 %s\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",!sFlag?"200 OK":"207 Multi-Status",nBytes);
+ char *body;
+ asprintf(&body,"HTTP/1.1 %s\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",!sFlag?"200 OK":"207 Multi-Status",nBytes);
LOG2("\n>>>>>>>>>> ");
LOG2(client.remoteIP());
@@ -1089,7 +1060,8 @@ int HAPClient::getCharacteristicsURL(char *urlBuf){
LOG2("\n");
sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t*
-
+ free(body);
+
return(1);
}
@@ -1138,9 +1110,8 @@ int HAPClient::putCharacteristicsURL(char *json){
char jsonBuf[nBytes+1];
homeSpan.sprintfAttributes(pObj,n,jsonBuf);
- int nChars=snprintf(NULL,0,"HTTP/1.1 207 Multi-Status\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); // create Body with Content Length = size of JSON Buf
- char body[nChars+1];
- sprintf(body,"HTTP/1.1 207 Multi-Status\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);
+ char *body;
+ asprintf(&body,"HTTP/1.1 207 Multi-Status\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);
LOG2("\n>>>>>>>>>> ");
LOG2(client.remoteIP());
@@ -1150,6 +1121,7 @@ int HAPClient::putCharacteristicsURL(char *json){
LOG2("\n");
sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t*
+ free(body);
}
@@ -1199,9 +1171,8 @@ int HAPClient::putPrepareURL(char *json){
sprintf(jsonBuf,"{\"status\":%d}",(int)status);
int nBytes=strlen(jsonBuf);
- int nChars=snprintf(NULL,0,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); // create Body with Content Length = size of JSON Buf
- char body[nChars+1];
- sprintf(body,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);
+ char *body;
+ asprintf(&body,"HTTP/1.1 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);
LOG2("\n>>>>>>>>>> ");
LOG2(client.remoteIP());
@@ -1211,6 +1182,7 @@ int HAPClient::putPrepareURL(char *json){
LOG2("\n");
sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t*
+ free(body);
return(1);
}
@@ -1302,9 +1274,13 @@ int HAPClient::getStatusURL(){
char mbtlsv[64];
mbedtls_version_get_string_full(mbtlsv);
response+="| MbedTLS Version: | " + String(mbtlsv) + " |
\n";
-
- response+="| HomeKit Status: | " + String(nAdminControllers()?"PAIRED":"NOT PAIRED") + " |
\n";
+
+ response+="| HomeKit Status: | " + String(HAPClient::nAdminControllers()?"PAIRED":"NOT PAIRED") + " |
\n";
response+="| Max Log Entries: | " + String(homeSpan.webLog.maxEntries) + " |
\n";
+
+ if(homeSpan.weblogCallback)
+ homeSpan.weblogCallback(response);
+
response+="\n";
response+="";
@@ -1393,9 +1369,8 @@ void HAPClient::eventNotify(SpanBuf *pObj, int nObj, int ignoreClient){
char jsonBuf[nBytes+1];
homeSpan.sprintfNotify(pObj,nObj,jsonBuf,cNum);
- int nChars=snprintf(NULL,0,"EVENT/1.0 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes); // create Body with Content Length = size of JSON Buf
- char body[nChars+1];
- sprintf(body,"EVENT/1.0 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);
+ char *body;
+ asprintf(&body,"EVENT/1.0 200 OK\r\nContent-Type: application/hap+json\r\nContent-Length: %d\r\n\r\n",nBytes);
LOG2("\n>>>>>>>>>> ");
LOG2(hap[cNum]->client.remoteIP());
@@ -1405,6 +1380,7 @@ void HAPClient::eventNotify(SpanBuf *pObj, int nObj, int ignoreClient){
LOG2("\n");
hap[cNum]->sendEncrypted(body,(uint8_t *)jsonBuf,nBytes); // note recasting of jsonBuf into uint8_t*
+ free(body);
} // if there are characteristic updates to notify client cNum
} // if client exists
@@ -1417,13 +1393,11 @@ void HAPClient::eventNotify(SpanBuf *pObj, int nObj, int ignoreClient){
void HAPClient::tlvRespond(){
- int nBytes=tlv8.pack(NULL); // return number of bytes needed to pack TLV records into a buffer
- uint8_t tlvData[nBytes]; // create buffer
- tlv8.pack(tlvData); // pack TLV records into buffer
+ TempBuffer tBuf(tlv8.pack(NULL)); // create buffer to hold TLV data
+ tlv8.pack(tBuf.get()); // pack TLV records into buffer
- int nChars=snprintf(NULL,0,"HTTP/1.1 200 OK\r\nContent-Type: application/pairing+tlv8\r\nContent-Length: %d\r\n\r\n",nBytes); // create Body with Content Length = size of TLV data
- char body[nChars+1];
- sprintf(body,"HTTP/1.1 200 OK\r\nContent-Type: application/pairing+tlv8\r\nContent-Length: %d\r\n\r\n",nBytes);
+ char *body;
+ asprintf(&body,"HTTP/1.1 200 OK\r\nContent-Type: application/pairing+tlv8\r\nContent-Length: %d\r\n\r\n",tBuf.len()); // create Body with Content Length = size of TLV data
LOG2("\n>>>>>>>>>> ");
LOG2(client.remoteIP());
@@ -1433,36 +1407,40 @@ void HAPClient::tlvRespond(){
if(!cPair){ // unverified, unencrypted session
client.print(body);
- client.write(tlvData,nBytes);
+ client.write(tBuf.get(),tBuf.len());
LOG2("------------ SENT! --------------\n");
} else {
- sendEncrypted(body,tlvData,nBytes);
+ sendEncrypted(body,tBuf.get(),tBuf.len());
}
+ free(body);
+
} // tlvRespond
//////////////////////////////////////
-int HAPClient::receiveEncrypted(){
+int HAPClient::receiveEncrypted(uint8_t *httpBuf, int messageSize){
- uint8_t buf[1042]; // maximum size of encoded message = 2+1024+16 bytes (HAP Section 6.5.2)
+ uint8_t aad[2];
int nBytes=0;
- while(client.read(buf,2)==2){ // read initial 2-byte AAD record
+ while(client.read(aad,2)==2){ // read initial 2-byte AAD record
- int n=buf[0]+buf[1]*256; // compute number of bytes expected in encoded message
+ int n=aad[0]+aad[1]*256; // compute number of bytes expected in message after decoding
- if(nBytes+n>MAX_HTTP){ // exceeded maximum number of bytes allowed in plaintext message
- LOG0("\n\n*** ERROR: Exceeded maximum HTTP message length\n\n");
+ if(nBytes+n>messageSize){ // exceeded maximum number of bytes allowed in plaintext message
+ LOG0("\n\n*** ERROR: Decrypted message of %d bytes exceeded maximum expected message length of %d bytes\n\n",nBytes+n,messageSize);
return(0);
}
- if(client.read(buf+2,n+16)!=n+16){ // read expected number of total bytes = n bytes in encoded message + 16 bytes for appended authentication tag
+ TempBuffer tBuf(n+16); // expected number of total bytes = n bytes in encoded message + 16 bytes for appended authentication tag
+
+ if(client.read(tBuf.get(),tBuf.len())!=tBuf.len()){
LOG0("\n\n*** ERROR: Malformed encrypted message frame\n\n");
return(0);
}
- if(crypto_aead_chacha20poly1305_ietf_decrypt(httpBuf+nBytes, NULL, NULL, buf+2, n+16, buf, 2, c2aNonce.get(), c2aKey)==-1){
+ if(crypto_aead_chacha20poly1305_ietf_decrypt(httpBuf+nBytes, NULL, NULL, tBuf.get(), tBuf.len(), aad, 2, c2aNonce.get(), c2aKey)==-1){
LOG0("\n\n*** ERROR: Can't Decrypt Message\n\n");
return(0);
}
@@ -1496,10 +1474,10 @@ void HAPClient::sendEncrypted(char *body, uint8_t *dataBuf, int dataLen){
TempBuffer tBuf(totalBytes);
- tBuf.buf[count]=bodyLen%256; // store number of bytes in first frame that encrypts the Body (AAD bytes)
- tBuf.buf[count+1]=bodyLen/256;
+ tBuf.get()[count]=bodyLen%256; // store number of bytes in first frame that encrypts the Body (AAD bytes)
+ tBuf.get()[count+1]=bodyLen/256;
- crypto_aead_chacha20poly1305_ietf_encrypt(tBuf.buf+count+2,&nBytes,(uint8_t *)body,bodyLen,tBuf.buf+count,2,NULL,a2cNonce.get(),a2cKey); // encrypt the Body with authentication tag appended
+ crypto_aead_chacha20poly1305_ietf_encrypt(tBuf.get()+count+2,&nBytes,(uint8_t *)body,bodyLen,tBuf.get()+count,2,NULL,a2cNonce.get(),a2cKey); // encrypt the Body with authentication tag appended
a2cNonce.inc(); // increment nonce
@@ -1512,17 +1490,17 @@ void HAPClient::sendEncrypted(char *body, uint8_t *dataBuf, int dataLen){
if(n>FRAME_SIZE) // maximum number of bytes to encrypt=FRAME_SIZE
n=FRAME_SIZE;
- tBuf.buf[count]=n%256; // store number of bytes that encrypts this frame (AAD bytes)
- tBuf.buf[count+1]=n/256;
+ tBuf.get()[count]=n%256; // store number of bytes that encrypts this frame (AAD bytes)
+ tBuf.get()[count+1]=n/256;
- crypto_aead_chacha20poly1305_ietf_encrypt(tBuf.buf+count+2,&nBytes,dataBuf+i,n,tBuf.buf+count,2,NULL,a2cNonce.get(),a2cKey); // encrypt the next portion of dataBuf with authentication tag appended
+ crypto_aead_chacha20poly1305_ietf_encrypt(tBuf.get()+count+2,&nBytes,dataBuf+i,n,tBuf.get()+count,2,NULL,a2cNonce.get(),a2cKey); // encrypt the next portion of dataBuf with authentication tag appended
a2cNonce.inc(); // increment nonce
count+=2+n+16; // increment count by 2-byte AAD record + length of JSON + 16-byte authentication tag
}
- client.write(tBuf.buf,count); // transmit all encrypted frames to Client
+ client.write(tBuf.get(),count); // transmit all encrypted frames to Client
LOG2("-------- SENT ENCRYPTED! --------\n");
@@ -1565,10 +1543,10 @@ void HAPClient::charPrintRow(uint8_t *buf, int n, int minLogLevel){
//////////////////////////////////////
Controller *HAPClient::findController(uint8_t *id){
-
- for(int i=0;iLTPK,ltpk,32);
- slot->admin=admin;
- LOG2("\n*** Updated Controller: ");
- charPrintRow(id,36,2);
- LOG2(slot->admin?" (admin)\n\n":" (regular)\n\n");
- return(slot);
- }
-
- if((slot=getFreeController())){ // get slot for new controller, if available
- slot->allocated=true;
- memcpy(slot->ID,id,36);
- memcpy(slot->LTPK,ltpk,32);
- slot->admin=admin;
- LOG2("\n*** Added Controller: ");
- charPrintRow(id,36,2);
- LOG2(slot->admin?" (admin)\n\n":" (regular)\n\n");
- return(slot);
- }
-
- return(NULL);
-}
-
-//////////////////////////////////////
-
int HAPClient::nAdminControllers(){
int n=0;
-
- for(int i=0;iLTPK,sizeof(cTemp->LTPK))){ // existing controller with same LTPK
+ LOG2("\n*** Updated Controller: ");
+ charPrintRow(id,36,2);
+ LOG2(" from %s to %s\n\n",cTemp->admin?"(admin)":"(regular)",admin?"(admin)":"(regular)");
+ cTemp->admin=admin;
+ saveControllers();
+ } else {
+ LOG0("\n*** ERROR: Invalid request to update the LTPK of an existing Controller\n\n");
+ err=tagError_Unknown;
+ }
+
+ return(err);
+}
//////////////////////////////////////
void HAPClient::removeController(uint8_t *id){
- Controller *slot;
+ auto it=std::find_if(controllerList.begin(), controllerList.end(), [id](const Controller& cTemp){return(!memcmp(cTemp.ID,id,sizeof(cTemp.ID)));});
- if((slot=findController(id))){ // remove controller if found
- LOG2("\n***Removed Controller: ");
+ if(it==controllerList.end()){
+ LOG2("\n*** Request to Remove Controller Ignored - Controller Not Found: ");
charPrintRow(id,36,2);
- LOG2(slot->admin?" (admin)\n":" (regular)\n");
- slot->allocated=false;
-
- if(nAdminControllers()==0){ // if no more admins, remove all controllers
- removeControllers();
- LOG1("That was last Admin Controller! Removing any remaining Regular Controllers and unpairing Accessory\n");
- mdns_service_txt_item_set("_hap","_tcp","sf","1"); // set Status Flag = 1 (Table 6-8)
-
- STATUS_UPDATE(start(LED_PAIRING_NEEDED),HS_PAIRING_NEEDED)
-
- if(homeSpan.pairCallback) // if set, invoke user-defined Pairing Callback to indicate device has been paired
- homeSpan.pairCallback(false);
- }
-
LOG2("\n");
+ return;
}
-}
+ LOG1("\n*** Removing Controller: ");
+ charPrintRow((*it).ID,36,2);
+ LOG1((*it).admin?" (admin)\n":" (regular)\n");
+
+ tearDown((*it).ID); // teardown any connections using this Controller
+ controllerList.erase(it); // remove Controller
+
+ if(!nAdminControllers()){ // no more admin Controllers
+
+ LOG1("That was last Admin Controller! Removing any remaining Regular Controllers and unpairing Accessory\n");
+
+ tearDown(NULL); // teardown all remaining connections
+ controllerList.clear(); // remove all remaining Controllers
+ mdns_service_txt_item_set("_hap","_tcp","sf","1"); // set Status Flag = 1 (Table 6-8)
+ STATUS_UPDATE(start(LED_PAIRING_NEEDED),HS_PAIRING_NEEDED) // set optional Status LED
+ if(homeSpan.pairCallback) // if set, invoke user-defined Pairing Callback to indicate device has been un-paired
+ homeSpan.pairCallback(false);
+ }
+
+ saveControllers();
+}
+
+//////////////////////////////////////
+
+void HAPClient::tearDown(uint8_t *id){
+
+ for(int i=0;iclient && (id==NULL || (hap[i]->cPair && !memcmp(id,hap[i]->cPair->ID,36)))){
+ LOG1("*** Terminating Client #%d\n",i);
+ hap[i]->client.stop();
+ }
+ }
+}
+
+//////////////////////////////////////
+
+int HAPClient::listControllers(uint8_t *tlvBuf){
+
+ int nBytes=0;
+ int n;
+
+ tlv8.clear();
+ tlv8.val(kTLVType_State,pairState_M2);
+
+ for(auto it=controllerList.begin();it!=controllerList.end();it++){
+ if((*it).allocated){
+ if(tlv8.val(kTLVType_State)==-1) // if State is not set then this is not the first controller found
+ tlv8.val(kTLVType_Separator,1);
+ tlv8.val(kTLVType_Permissions,(*it).admin);
+ tlv8.buf(kTLVType_Identifier,(*it).ID,36);
+ tlv8.buf(kTLVType_PublicKey,(*it).LTPK,32);
+ n=tlv8.pack(tlvBuf);
+ nBytes+=n;
+ if(tlvBuf){
+ tlvBuf+=n;
+ tlv8.print();
+ }
+ tlv8.clear();
+ }
+ }
+
+ return(nBytes);
+}
//////////////////////////////////////
@@ -1670,24 +1678,38 @@ void HAPClient::printControllers(int minLogLevel){
if(homeSpan.logLevel tBuf(controllerList.size()); // create temporary buffer to hold Controller data
+ std::copy(controllerList.begin(),controllerList.end(),tBuf.get()); // copy data from linked list to buffer
+
+ nvs_set_blob(hapNVS,"CONTROLLERS",tBuf.get(),tBuf.len()); // update data
+ nvs_commit(hapNVS); // commit to NVS
}
+
//////////////////////////////////////
Nonce::Nonce(){
@@ -1719,14 +1741,13 @@ void Nonce::inc(){
// instantiate all static HAP Client structures and data
-TLV HAPClient::tlv8;
+TLV HAPClient::tlv8;
nvs_handle HAPClient::hapNVS;
nvs_handle HAPClient::srpNVS;
-uint8_t HAPClient::httpBuf[MAX_HTTP+1];
HKDF HAPClient::hkdf;
pairState HAPClient::pairStatus;
Accessory HAPClient::accessory;
-Controller HAPClient::controllers[MAX_CONTROLLERS];
+list HAPClient::controllerList;
SRP6A HAPClient::srp;
int HAPClient::conNum;
diff --git a/src/HAP.h b/src/HAP.h
index 8c8b937..b8d2107 100644
--- a/src/HAP.h
+++ b/src/HAP.h
@@ -50,10 +50,20 @@ struct Nonce {
// Paired Controller Structure for Permanently-Stored Data
struct Controller {
- boolean allocated=false; // slot is allocated with Controller data
+ boolean allocated=false; // DEPRECATED (but needed for backwards compatability with original NVS storage of Controller info)
boolean admin; // Controller has admin privileges
uint8_t ID[36]; // Pairing ID
uint8_t LTPK[32]; // Long Term Ed2519 Public Key
+
+ Controller(){}
+
+ Controller(uint8_t *id, uint8_t *ltpk, boolean ad){
+ allocated=true;
+ admin=ad;
+ memcpy(ID,id,sizeof(ID));
+ memcpy(LTPK,ltpk,sizeof(LTPK));
+ }
+
};
/////////////////////////////////////////////////
@@ -73,25 +83,24 @@ struct HAPClient {
// common structures and data shared across all HAP Clients
- static const int MAX_HTTP=8095; // max number of bytes in HTTP message buffer
+ static const int MAX_HTTP=8096; // max number of bytes allowed for HTTP message
static const int MAX_CONTROLLERS=16; // maximum number of paired controllers (HAP requires at least 16)
static const int MAX_ACCESSORIES=41; // maximum number of allowed Acessories (HAP limit=150, but not enough memory in ESP32 to run that many)
- static TLV tlv8; // TLV8 structure (HAP Section 14.1) with space for 10 TLV records of type kTLVType (HAP Table 5-6)
+ static TLV tlv8; // TLV8 structure (HAP Section 14.1) with space for 11 TLV records of type kTLVType (HAP Table 5-6)
static nvs_handle hapNVS; // handle for non-volatile-storage of HAP data
static nvs_handle srpNVS; // handle for non-volatile-storage of SRP data
- static uint8_t httpBuf[MAX_HTTP+1]; // buffer to store HTTP messages (+1 to leave room for storing an extra 'overflow' character)
static HKDF hkdf; // generates (and stores) HKDF-SHA-512 32-byte keys derived from an inputKey of arbitrary length, a salt string, and an info string
static pairState pairStatus; // tracks pair-setup status
static SRP6A srp; // stores all SRP-6A keys used for Pair-Setup
static Accessory accessory; // Accessory ID and Ed25519 public and secret keys- permanently stored
- static Controller controllers[MAX_CONTROLLERS]; // Paired Controller IDs and ED25519 long-term public keys - permanently stored
+ static list controllerList; // linked-list of Paired Controller IDs and ED25519 long-term public keys - permanently stored
static int conNum; // connection number - used to keep track of per-connection EV notifications
// individual structures and data defined for each Hap Client connection
WiFiClient client; // handle to client
- Controller *cPair; // pointer to info on current, session-verified Paired Controller (NULL=un-verified, and therefore un-encrypted, connection)
+ Controller *cPair=NULL; // pointer to info on current, session-verified Paired Controller (NULL=un-verified, and therefore un-encrypted, connection)
// These keys are generated in the first call to pair-verify and used in the second call to pair-verify so must persist for a short period
@@ -121,7 +130,7 @@ struct HAPClient {
void tlvRespond(); // respond to client with HTTP OK header and all defined TLV data records (those with length>0)
void sendEncrypted(char *body, uint8_t *dataBuf, int dataLen); // send client complete ChaCha20-Poly1305 encrypted HTTP mesage comprising a null-terminated 'body' and 'dataBuf' with 'dataLen' bytes
- int receiveEncrypted(); // decrypt HTTP request (HAP Section 6.5)
+ int receiveEncrypted(uint8_t *httpBuf, int messageSize); // decrypt HTTP request (HAP Section 6.5)
int notFoundError(); // return 404 error
int badRequestError(); // return 400 error
@@ -135,13 +144,14 @@ struct HAPClient {
static void hexPrintRow(uint8_t *buf, int n, int minLogLevel=0); // prints 'n' bytes of *buf as HEX, all on one row, subject to specified minimum log level
static void charPrintRow(uint8_t *buf, int n, int minLogLevel=0); // prints 'n' bytes of *buf as CHAR, all on one row, subject to specified minimum log level
- static Controller *findController(uint8_t *id); // returns pointer to controller with mathching ID (or NULL if no match)
- static Controller *getFreeController(); // return pointer to next free controller slot (or NULL if no free slots)
- static Controller *addController(uint8_t *id, uint8_t *ltpk, boolean admin); // stores data for new Controller with specified data. Returns pointer to Controller slot on success, else NULL
- static int nAdminControllers(); // returns number of admin Controllers stored
- static void removeControllers(); // removes all Controllers (sets allocated flags to false for all slots)
+ static Controller *findController(uint8_t *id); // returns pointer to controller with matching ID (or NULL if no match)
+ static tagError addController(uint8_t *id, uint8_t *ltpk, boolean admin); // stores data for new Controller with specified data. Returns tagError (if any)
static void removeController(uint8_t *id); // removes specific Controller. If no remaining admin Controllers, remove all others (if any) as per HAP requirements.
static void printControllers(int minLogLevel=0); // prints IDs of all allocated (paired) Controller, subject to specified minimum log level
+ static int listControllers(uint8_t *tlvBuf); // creates and prints a multi-TLV list of Controllers (HAP Section 5.12)
+ static void saveControllers(); // saves Controller list in NVS
+ static int nAdminControllers(); // returns number of admin Controller
+ static void tearDown(uint8_t *id); // tears down connections using Controller with ID=id; tears down all connections if id=NULL
static void checkNotifications(); // checks for Event Notifications and reports to controllers as needed (HAP Section 6.8)
static void checkTimedWrites(); // checks for expired Timed Write PIDs, and clears any found (HAP Section 6.7.2.4)
static void eventNotify(SpanBuf *pObj, int nObj, int ignoreClient=-1); // transmits EVENT Notifications for nObj SpanBuf objects, pObj, with optional flag to ignore a specific client
diff --git a/src/HAPConstants.h b/src/HAPConstants.h
index 5ed6ab3..fc87ad3 100644
--- a/src/HAPConstants.h
+++ b/src/HAPConstants.h
@@ -52,6 +52,7 @@ typedef enum {
// HAP Error Codes (HAP Table 5-5)
typedef enum {
+ tagError_None=0x00,
tagError_Unknown=0x01,
tagError_Authentication=0x02,
tagError_Backoff=0x03,
diff --git a/src/HomeSpan.cpp b/src/HomeSpan.cpp
index fbf045d..f8994a7 100644
--- a/src/HomeSpan.cpp
+++ b/src/HomeSpan.cpp
@@ -260,7 +260,7 @@ void Span::pollTask() {
homeSpan.lastClientIP="0.0.0.0"; // reset stored IP address to show "0.0.0.0" if homeSpan.getClientIP() is used in any other context
if(!hap[i]->client){ // client disconnected by server
- LOG1("** Disconnecting Client #");
+ LOG1("** Disconnected Client #");
LOG1(i);
LOG1(" (");
LOG1(millis()/1000);
@@ -422,38 +422,36 @@ void Span::checkConnect(){
addWebLog(true,"WiFi Connected! IP Address = %s",WiFi.localIP().toString().c_str());
- if(connected>1) // Do not initialize everything below if this is only a reconnect
+ if(connected>1){ // Do not initialize everything below if this is only a reconnect
+ if(wifiCallbackAll)
+ wifiCallbackAll((connected+1)/2);
return;
+ }
char id[18]; // create string version of Accessory ID for MDNS broadcast
memcpy(id,HAPClient::accessory.ID,17); // copy ID bytes
id[17]='\0'; // add terminating null
- // create broadcaset name from server base name plus accessory ID (without ':')
+ // create broadcast name from server base name plus accessory ID (without ':')
- int nChars;
-
- if(!hostNameSuffix)
- nChars=snprintf(NULL,0,"%s-%.2s%.2s%.2s%.2s%.2s%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15);
- else
- nChars=snprintf(NULL,0,"%s%s",hostNameBase,hostNameSuffix);
-
- char hostName[nChars+1];
+ char *hostName;
if(!hostNameSuffix)
- sprintf(hostName,"%s-%.2s%.2s%.2s%.2s%.2s%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15);
+ asprintf(&hostName,"%s-%.2s%.2s%.2s%.2s%.2s%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15);
else
- sprintf(hostName,"%s%s",hostNameBase,hostNameSuffix);
+ asprintf(&hostName,"%s%s",hostNameBase,hostNameSuffix);
- char d[strlen(hostName)+1];
- sscanf(hostName,"%[A-Za-z0-9-]",d);
+ char *d;
+ sscanf(hostName,"%m[A-Za-z0-9-]",&d);
- if(strlen(hostName)>255|| hostName[0]=='-' || hostName[strlen(hostName)-1]=='-' || strlen(hostName)!=strlen(d)){
+ if(strlen(hostName)>255 || hostName[0]=='-' || hostName[strlen(hostName)-1]=='-' || strlen(hostName)!=strlen(d)){
LOG0("\n*** Error: Can't start MDNS due to invalid hostname '%s'.\n",hostName);
LOG0("*** Hostname must consist of 255 or less alphanumeric characters or a hyphen, except that the hyphen cannot be the first or last character.\n");
LOG0("*** PROGRAM HALTED!\n\n");
while(1);
}
+
+ free(d);
LOG0("\nStarting MDNS...\n\n");
LOG0("HostName: %s.local:%d\n",hostName,tcpPortNum);
@@ -534,12 +532,18 @@ void Span::checkConnect(){
if(wifiCallback)
wifiCallback();
+
+ if(wifiCallbackAll)
+ wifiCallbackAll((connected+1)/2);
+
+
+ free(hostName);
} // initWiFi
///////////////////////////////
-void Span::setQRID(const char *id){
+Span& Span::setQRID(const char *id){
char tBuf[5];
sscanf(id,"%4[0-9A-Za-z]",tBuf);
@@ -547,7 +551,8 @@ void Span::setQRID(const char *id){
if(strlen(id)==4 && strlen(tBuf)==4){
sprintf(qrID,"%s",id);
}
-
+
+ return(*this);
} // setQRID
///////////////////////////////
@@ -556,6 +561,16 @@ void Span::processSerialCommand(const char *c){
switch(c[0]){
+ case 'Z': {
+ HAPClient::saveControllers();
+ break;
+ TempBuffer tBuf(HAPClient::listControllers(NULL));
+ HAPClient::listControllers(tBuf.get());
+ Serial.printf("SIZE = %d\n",tBuf.len());
+ HAPClient::hexPrintRow(tBuf.get(),tBuf.len());
+ break;
+ }
+
case 's': {
LOG0("\n*** HomeSpan Status ***\n\n");
@@ -598,10 +613,10 @@ void Span::processSerialCommand(const char *c){
case 'd': {
TempBuffer qBuf(sprintfAttributes(NULL)+1);
- sprintfAttributes(qBuf.buf);
+ sprintfAttributes(qBuf.get());
LOG0("\n*** Attributes Database: size=%d configuration=%d ***\n\n",qBuf.len()-1,hapConfig.configNumber);
- prettyPrint(qBuf.buf);
+ prettyPrint(qBuf.get());
LOG0("\n*** End Database ***\n\n");
}
break;
@@ -682,22 +697,13 @@ void Span::processSerialCommand(const char *c){
case 'U': {
- HAPClient::removeControllers(); // clear all Controller data
- nvs_set_blob(HAPClient::hapNVS,"CONTROLLERS",HAPClient::controllers,sizeof(HAPClient::controllers)); // update data
- nvs_commit(HAPClient::hapNVS); // commit to NVS
+ HAPClient::controllerList.clear(); // clear all Controller data
+ HAPClient::saveControllers();
LOG0("\n*** HomeSpan Pairing Data DELETED ***\n\n");
-
- for(int i=0;iclient){ // if slot is connected
- LOG1("*** Terminating Client #");
- LOG1(i);
- LOG1("\n");
- hap[i]->client.stop();
- }
- }
-
+ HAPClient::tearDown(NULL); // tear down all verified connections
+
LOG0("\nDEVICE NOT YET PAIRED -- PLEASE PAIR WITH HOMEKIT APP\n\n");
- mdns_service_txt_item_set("_hap","_tcp","sf","1"); // set Status Flag = 1 (Table 6-8)
+ mdns_service_txt_item_set("_hap","_tcp","sf","1"); // set Status Flag = 1 (Table 6-8)
if(homeSpan.pairCallback)
homeSpan.pairCallback(false);
@@ -833,7 +839,7 @@ void Span::processSerialCommand(const char *c){
case 'm': {
multi_heap_info_t heapInfo;
heap_caps_get_info(&heapInfo,MALLOC_CAP_INTERNAL);
- LOG0("Total Heap=%d ",heapInfo.total_free_bytes);
+ LOG0("Total Heap=%d (low=%d) ",heapInfo.total_free_bytes,heapInfo.minimum_free_bytes);
heap_caps_get_info(&heapInfo,MALLOC_CAP_DEFAULT);
LOG0("DRAM-Capable=%d ",heapInfo.total_free_bytes);
heap_caps_get_info(&heapInfo,MALLOC_CAP_EXEC);
@@ -1008,13 +1014,11 @@ void Span::processSerialCommand(const char *c){
LOG0("\n*** Pairing Data used for Cloning another Device\n\n");
size_t olen;
TempBuffer tBuf(256);
- mbedtls_base64_encode((uint8_t *)tBuf.buf,256,&olen,(uint8_t *)&HAPClient::accessory,sizeof(struct Accessory));
- LOG0("Accessory data: %s\n",tBuf.buf);
- for(int i=0;i tBuf(200);
size_t olen;
- tBuf.buf[0]='\0';
+ tBuf.get()[0]='\0';
LOG0(">>> Accessory data: ");
- readSerial(tBuf.buf,199);
- if(strlen(tBuf.buf)==0){
+ readSerial(tBuf.get(),199);
+ if(strlen(tBuf.get())==0){
LOG0("(cancelled)\n\n");
return;
}
- mbedtls_base64_decode((uint8_t *)&HAPClient::accessory,sizeof(struct Accessory),&olen,(uint8_t *)tBuf.buf,strlen(tBuf.buf));
+ mbedtls_base64_decode((uint8_t *)&HAPClient::accessory,sizeof(struct Accessory),&olen,(uint8_t *)tBuf.get(),strlen(tBuf.get()));
if(olen!=sizeof(struct Accessory)){
LOG0("\n*** Error in size of Accessory data - cloning cancelled. Restarting...\n\n");
reboot();
@@ -1041,22 +1045,25 @@ void Span::processSerialCommand(const char *c){
HAPClient::charPrintRow(HAPClient::accessory.ID,17);
LOG0("\n");
}
+
+ HAPClient::controllerList.clear();
+ Controller tCont;
- for(int i=0;i>> Controller data: ");
- readSerial(tBuf.buf,199);
- if(strlen(tBuf.buf)==0){
+ readSerial(tBuf.get(),199);
+ if(strlen(tBuf.get())==0){
LOG0("(done)\n");
- while(i tBuf(sprintfAttributes(NULL,GET_META|GET_PERMS|GET_TYPE|GET_DESC)+1);
- sprintfAttributes(tBuf.buf,GET_META|GET_PERMS|GET_TYPE|GET_DESC);
- mbedtls_sha512_ret((uint8_t *)tBuf.buf,tBuf.len(),tHash,1); // create SHA-384 hash of JSON (can be any hash - just looking for a unique key)
+ sprintfAttributes(tBuf.get(),GET_META|GET_PERMS|GET_TYPE|GET_DESC);
+ mbedtls_sha512_ret((uint8_t *)tBuf.get(),tBuf.len(),tHash,1); // create SHA-384 hash of JSON (can be any hash - just looking for a unique key)
boolean changed=false;
diff --git a/src/HomeSpan.h b/src/HomeSpan.h
index 6bfc1ba..7d98776 100644
--- a/src/HomeSpan.h
+++ b/src/HomeSpan.h
@@ -38,6 +38,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -55,6 +56,7 @@
using std::vector;
using std::unordered_map;
using std::unordered_set;
+using std::list;
enum {
GET_AID=1,
@@ -202,7 +204,6 @@ class Span{
const char *displayName; // display name for this device - broadcast as part of Bonjour MDNS
const char *hostNameBase; // base of hostName of this device - full host name broadcast by Bonjour MDNS will have 6-byte accessoryID as well as '.local' automatically appended
const char *hostNameSuffix=NULL; // optional "suffix" of hostName of this device. If specified, will be used as the hostName suffix instead of the 6-byte accessoryID
- char *hostName; // full host name of this device - constructed from hostNameBase and 6-byte AccessoryID
const char *modelName; // model name of this device - broadcast as Bonjour field "md"
char category[3]=""; // category ID of primary accessory - broadcast as Bonjour field "ci" (HAP Section 13)
unsigned long snapTime; // current time (in millis) snapped before entering Service loops() or updates()
@@ -230,7 +231,9 @@ class Span{
unsigned long comModeLife=DEFAULT_COMMAND_TIMEOUT*1000; // length of time (in milliseconds) to keep Command Mode alive before resuming normal operations
uint16_t tcpPortNum=DEFAULT_TCP_PORT; // port for TCP communications between HomeKit and HomeSpan
char qrID[5]=""; // Setup ID used for pairing with QR Code
- void (*wifiCallback)()=NULL; // optional callback function to invoke once WiFi connectivity is established
+ void (*wifiCallback)()=NULL; // optional callback function to invoke once WiFi connectivity is initially established
+ void (*wifiCallbackAll)(int)=NULL; // optional callback function to invoke every time WiFi connectivity is established or re-established
+ void (*weblogCallback)(String &)=NULL; // optional callback function to invoke after header table in Web Log is produced
void (*pairCallback)(boolean isPaired)=NULL; // optional callback function to invoke when pairing is established (true) or lost (false)
boolean autoStartAPEnabled=false; // enables auto start-up of Access Point when WiFi Credentials not found
void (*apFunction)()=NULL; // optional function to invoke when starting Access Point
@@ -292,49 +295,52 @@ class Span{
boolean updateDatabase(boolean updateMDNS=true); // updates HAP Configuration Number and Loop vector; if updateMDNS=true and config number has changed, re-broadcasts MDNS 'c#' record; returns true if config number changed
boolean deleteAccessory(uint32_t aid); // deletes Accessory with matching aid; returns true if found, else returns false
- void setControlPin(uint8_t pin){controlButton=new PushButton(pin);} // sets Control Pin
- void setStatusPin(uint8_t pin){statusDevice=new GenericLED(pin);} // sets Status Device to a simple LED on specified pin
- void setStatusAutoOff(uint16_t duration){autoOffLED=duration;} // sets Status LED auto off (seconds)
- int getStatusPin(){return(statusLED->getPin());} // get Status Pin (getPin will return -1 if underlying statusDevice is undefined)
- int getControlPin(){return(controlButton?controlButton->getPin():-1);} // get Control Pin (returns -1 if undefined)
+ Span& setControlPin(uint8_t pin){controlButton=new PushButton(pin);return(*this);} // sets Control Pin
+ Span& setStatusPin(uint8_t pin){statusDevice=new GenericLED(pin);return(*this);} // sets Status Device to a simple LED on specified pin
+ Span& setStatusAutoOff(uint16_t duration){autoOffLED=duration;return(*this);} // sets Status LED auto off (seconds)
+ int getStatusPin(){return(statusLED->getPin());} // get Status Pin (getPin will return -1 if underlying statusDevice is undefined)
+ int getControlPin(){return(controlButton?controlButton->getPin():-1);} // get Control Pin (returns -1 if undefined)
- void setStatusPixel(uint8_t pin,float h=0,float s=100,float v=100){ // sets Status Device to an RGB Pixel on specified pin
+ Span& setStatusPixel(uint8_t pin,float h=0,float s=100,float v=100){ // sets Status Device to an RGB Pixel on specified pin
statusDevice=((new Pixel(pin))->setOnColor(Pixel::HSV(h,s,v)));
+ return(*this);
}
- void setStatusDevice(Blinkable *sDev){statusDevice=sDev;}
+ Span& setStatusDevice(Blinkable *sDev){statusDevice=sDev;return(*this);}
void refreshStatusDevice(){if(statusLED)statusLED->refresh();}
- void setApSSID(const char *ssid){network.apSSID=ssid;} // sets Access Point SSID
- void setApPassword(const char *pwd){network.apPassword=pwd;} // sets Access Point Password
- void setApTimeout(uint16_t nSec){network.lifetime=nSec*1000;} // sets Access Point Timeout (seconds)
- void setCommandTimeout(uint16_t nSec){comModeLife=nSec*1000;} // sets Command Mode Timeout (seconds)
- void setLogLevel(int level){logLevel=level;} // sets Log Level for log messages (0=baseline, 1=intermediate, 2=all, -1=disable all serial input/output)
- int getLogLevel(){return(logLevel);} // get Log Level
- void setSerialInputDisable(boolean val){serialInputDisabled=val;} // sets whether serial input is disabled (true) or enabled (false)
- boolean getSerialInputDisable(){return(serialInputDisabled);} // returns true if serial input is disabled, or false if serial input in enabled
- void reserveSocketConnections(uint8_t n){maxConnections-=n;} // reserves n socket connections *not* to be used for HAP
- void setHostNameSuffix(const char *suffix){hostNameSuffix=suffix;} // sets the hostName suffix to be used instead of the 6-byte AccessoryID
- void setPortNum(uint16_t port){tcpPortNum=port;} // sets the TCP port number to use for communications between HomeKit and HomeSpan
- void setQRID(const char *id); // sets the Setup ID for optional pairing with a QR Code
- void setSketchVersion(const char *sVer){sketchVersion=sVer;} // set optional sketch version number
- const char *getSketchVersion(){return sketchVersion;} // get sketch version number
- void setWifiCallback(void (*f)()){wifiCallback=f;} // sets an optional user-defined function to call once WiFi connectivity is established
- void setPairCallback(void (*f)(boolean isPaired)){pairCallback=f;} // sets an optional user-defined function to call when Pairing is established (true) or lost (false)
- void setApFunction(void (*f)()){apFunction=f;} // sets an optional user-defined function to call when activating the WiFi Access Point
- void enableAutoStartAP(){autoStartAPEnabled=true;} // enables auto start-up of Access Point when WiFi Credentials not found
- void setWifiCredentials(const char *ssid, const char *pwd); // sets WiFi Credentials
- void setStatusCallback(void (*f)(HS_STATUS status)){statusCallback=f;} // sets an optional user-defined function to call when HomeSpan status changes
- const char* statusString(HS_STATUS s); // returns char string for HomeSpan status change messages
+ Span& setApSSID(const char *ssid){network.apSSID=ssid;return(*this);} // sets Access Point SSID
+ Span& setApPassword(const char *pwd){network.apPassword=pwd;return(*this);} // sets Access Point Password
+ Span& setApTimeout(uint16_t nSec){network.lifetime=nSec*1000;return(*this);} // sets Access Point Timeout (seconds)
+ Span& setCommandTimeout(uint16_t nSec){comModeLife=nSec*1000;return(*this);} // sets Command Mode Timeout (seconds)
+ Span& setLogLevel(int level){logLevel=level;return(*this);} // sets Log Level for log messages (0=baseline, 1=intermediate, 2=all, -1=disable all serial input/output)
+ int getLogLevel(){return(logLevel);} // get Log Level
+ Span& setSerialInputDisable(boolean val){serialInputDisabled=val;return(*this);} // sets whether serial input is disabled (true) or enabled (false)
+ boolean getSerialInputDisable(){return(serialInputDisabled);} // returns true if serial input is disabled, or false if serial input in enabled
+ Span& reserveSocketConnections(uint8_t n){maxConnections-=n;return(*this);} // reserves n socket connections *not* to be used for HAP
+ Span& setHostNameSuffix(const char *suffix){hostNameSuffix=suffix;return(*this);} // sets the hostName suffix to be used instead of the 6-byte AccessoryID
+ Span& setPortNum(uint16_t port){tcpPortNum=port;return(*this);} // sets the TCP port number to use for communications between HomeKit and HomeSpan
+ Span& setQRID(const char *id); // sets the Setup ID for optional pairing with a QR Code
+ Span& setSketchVersion(const char *sVer){sketchVersion=sVer;return(*this);} // set optional sketch version number
+ const char *getSketchVersion(){return sketchVersion;} // get sketch version number
+ Span& setWifiCallback(void (*f)()){wifiCallback=f;return(*this);} // sets an optional user-defined function to call once WiFi connectivity is initially established
+ Span& setWifiCallbackAll(void (*f)(int)){wifiCallbackAll=f;return(*this);} // sets an optional user-defined function to call every time WiFi connectivity is established or re-established
+ Span& setPairCallback(void (*f)(boolean isPaired)){pairCallback=f;return(*this);} // sets an optional user-defined function to call when Pairing is established (true) or lost (false)
+ Span& setApFunction(void (*f)()){apFunction=f;return(*this);} // sets an optional user-defined function to call when activating the WiFi Access Point
+ Span& enableAutoStartAP(){autoStartAPEnabled=true;return(*this);} // enables auto start-up of Access Point when WiFi Credentials not found
+ Span& setWifiCredentials(const char *ssid, const char *pwd); // sets WiFi Credentials
+ Span& setStatusCallback(void (*f)(HS_STATUS status)){statusCallback=f;return(*this);} // sets an optional user-defined function to call when HomeSpan status changes
+ const char* statusString(HS_STATUS s); // returns char string for HomeSpan status change messages
- void setPairingCode(const char *s){sprintf(pairingCodeCommand,"S %9s",s);} // sets the Pairing Code - use is NOT recommended. Use 'S' from CLI instead
- void deleteStoredValues(){processSerialCommand("V");} // deletes stored Characteristic values from NVS
+ Span& setPairingCode(const char *s){sprintf(pairingCodeCommand,"S %9s",s);return(*this);} // sets the Pairing Code - use is NOT recommended. Use 'S' from CLI instead
+ void deleteStoredValues(){processSerialCommand("V");} // deletes stored Characteristic values from NVS
int enableOTA(boolean auth=true, boolean safeLoad=true){return(spanOTA.init(auth, safeLoad, NULL));} // enables Over-the-Air updates, with (auth=true) or without (auth=false) authorization password
int enableOTA(const char *pwd, boolean safeLoad=true){return(spanOTA.init(true, safeLoad, pwd));} // enables Over-the-Air updates, with custom authorization password (overrides any password stored with the 'O' command)
- void enableWebLog(uint16_t maxEntries=0, const char *serv=NULL, const char *tz="UTC", const char *url=DEFAULT_WEBLOG_URL){ // enable Web Logging
+ Span& enableWebLog(uint16_t maxEntries=0, const char *serv=NULL, const char *tz="UTC", const char *url=DEFAULT_WEBLOG_URL){ // enable Web Logging
webLog.init(maxEntries, serv, tz, url);
+ return(*this);
}
void addWebLog(boolean sysMsg, const char *fmt, ...){ // add Web Log entry
@@ -344,7 +350,8 @@ class Span{
va_end(ap);
}
- void setWebLogCSS(const char *css){webLog.css="\n" + String(css) + "\n";}
+ Span& setWebLogCSS(const char *css){webLog.css="\n" + String(css) + "\n";return(*this);}
+ Span& setWebLogCallback(void (*f)(String &)){weblogCallback=f;return(*this);}
void setVerboseWiFiReconnect(bool verbose) { verboseWiFiReconnect = verbose;}
@@ -353,7 +360,7 @@ class Span{
LOG0("\n*** AutoPolling Task started with priority=%d\n\n",uxTaskPriorityGet(pollTaskHandle));
}
- void setTimeServerTimeout(uint32_t tSec){webLog.waitTime=tSec*1000;} // sets wait time (in seconds) for optional web log time server to connect
+ Span& setTimeServerTimeout(uint32_t tSec){webLog.waitTime=tSec*1000;return(*this);} // sets wait time (in seconds) for optional web log time server to connect
[[deprecated("Please use reserveSocketConnections(n) method instead.")]]
void setMaxConnections(uint8_t n){requestedMaxCon=n;} // sets maximum number of simultaneous HAP connections
@@ -713,8 +720,8 @@ class SpanCharacteristic{
size_t olen;
mbedtls_base64_encode(NULL,0,&olen,data,len); // get length of string buffer needed (mbedtls includes the trailing null in this size)
TempBuffer tBuf(olen); // create temporary string buffer, with room for trailing null
- mbedtls_base64_encode((uint8_t*)tBuf.buf,olen,&olen,data,len ); // encode data into string buf
- setString(tBuf.buf); // call setString to continue processing as if characteristic was a string
+ mbedtls_base64_encode((uint8_t*)tBuf.get(),olen,&olen,data,len ); // encode data into string buf
+ setString(tBuf.get()); // call setString to continue processing as if characteristic was a string
}
template void setVal(T val, boolean notify=true){
diff --git a/src/Network.cpp b/src/Network.cpp
index bcfe676..5d69ad2 100644
--- a/src/Network.cpp
+++ b/src/Network.cpp
@@ -116,10 +116,7 @@ void Network::apConfigure(){
WiFiServer apServer(80);
client=0;
-
- TempBuffer tempBuffer(MAX_HTTP+1);
- uint8_t *httpBuf=tempBuffer.buf;
-
+
const byte DNS_PORT = 53;
DNSServer dnsServer;
IPAddress apIP(192, 168, 4, 1);
@@ -178,20 +175,30 @@ void Network::apConfigure(){
LOG2("<<<<<<<<< ");
LOG2(client.remoteIP());
LOG2(" <<<<<<<<<\n");
-
- int nBytes=client.read(httpBuf,MAX_HTTP+1); // read all available bytes up to maximum allowed+1
-
- if(nBytes>MAX_HTTP){ // exceeded maximum number of bytes allowed
+
+ int messageSize=client.available();
+
+ if(messageSize>MAX_HTTP){ // exceeded maximum number of bytes allowed
badRequestError();
- LOG0("\n*** ERROR: Exceeded maximum HTTP message length\n\n");
+ LOG0("\n*** ERROR: HTTP message of %d bytes exceeds maximum allowed (%d)\n\n",messageSize,MAX_HTTP);
+ continue;
+ }
+
+ TempBuffer httpBuf(messageSize+1); // leave room for null character added below
+
+ int nBytes=client.read(httpBuf.get(),messageSize); // read all available bytes up to maximum allowed+1
+
+ if(nBytes!=messageSize || client.available()!=0){
+ badRequestError();
+ LOG0("\n*** ERROR: HTTP message not read correctly. Expected %d bytes, read %d bytes, %d bytes remaining\n\n",messageSize,nBytes,client.available());
continue;
}
-
- httpBuf[nBytes]='\0'; // add null character to enable string functions
- char *body=(char *)httpBuf; // char pointer to start of HTTP Body
+
+ httpBuf.get()[nBytes]='\0'; // add null character to enable string functions
+ char *body=(char *)httpBuf.get(); // char pointer to start of HTTP Body
char *p; // char pointer used for searches
- if(!(p=strstr((char *)httpBuf,"\r\n\r\n"))){
+ if(!(p=strstr((char *)httpBuf.get(),"\r\n\r\n"))){
badRequestError();
LOG0("\n*** ERROR: Malformed HTTP request (can't find blank line indicating end of BODY)\n\n");
continue;
@@ -230,7 +237,7 @@ void Network::processRequest(char *body, char *formData){
String responseHead="HTTP/1.1 200 OK\r\nContent-type: text/html\r\n";
- String responseBody="