diff --git a/Other Examples/ProgrammableHub/ProgrammableHub.ino b/Other Examples/ProgrammableHub/ProgrammableHub.ino new file mode 100644 index 0000000..fec8a48 --- /dev/null +++ b/Other Examples/ProgrammableHub/ProgrammableHub.ino @@ -0,0 +1,388 @@ +/********************************************************************************* + * MIT License + * + * Copyright (c) 2022 Gregg E. Berman + * + * https://github.com/HomeSpan/HomeSpan + * + * 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. + * + ********************************************************************************/ + +////////////////////////////////////////////////////////////////// +// // +// HomeSpan: A HomeKit implementation for the ESP32 // +// ------------------------------------------------ // +// // +// Demonstrates how to implement a Web Server alongside // +// of HomeSpan to create a Programmable Hub serving up to // +// 12 Configurable Lights. Allows for dynamic changes // +// to Accessories without needing to reboot // +// // +////////////////////////////////////////////////////////////////// + +#include "HomeSpan.h" +#include // include WebServer library + +WebServer webServer(80); // create WebServer on port 80 + +#define MAX_LIGHTS 12 +#define MAX_NAME_LENGTH 32 +#define HUB_NAME "lighthub" + +enum colorType_t : uint8_t { + NO_COLOR, + TEMPERATURE_ONLY, + FULL_RGB +}; + +uint32_t aidStore=2; // keep track of unique AID numbers - start with AID=2 + +struct lightData_t { + char name[MAX_NAME_LENGTH+1]=""; + uint32_t aid=0; + boolean isDimmable:1; + colorType_t colorType:2; +} lightData[MAX_LIGHTS]; + +nvs_handle savedData; + +////////////////////////////////////// + +void setup() { + + Serial.begin(115200); + + size_t len; + nvs_open("SAVED_DATA",NVS_READWRITE,&savedData); // open a new namespace called SAVED_DATA in the NVS + + if(!nvs_get_blob(savedData,"LIGHTDATA",NULL,&len)) // if LIGHTDATA data found + nvs_get_blob(savedData,"LIGHTDATA",&lightData,&len); // retrieve data + + nvs_get_u32(savedData,"AID",&aidStore); // get AID, if it exists + + homeSpan.setLogLevel(1); + + homeSpan.setHostNameSuffix(""); // use null string for suffix (rather than the HomeSpan device ID) + homeSpan.setPortNum(1201); // change port number for HomeSpan so we can use port 80 for the Web Server + homeSpan.setWifiCallback(setupWeb); // need to start Web Server after WiFi is established + + homeSpan.begin(Category::Lighting,"HomeSpan Light Hub",HUB_NAME); + + new SpanAccessory(1); // here we specified the AID=1 for clarity (it would default to 1 anyway if left blank) + new Service::AccessoryInformation(); + new Characteristic::Identify(); + new Characteristic::Model("HomeSpan Programmable Hub"); + new Characteristic::AccessoryFlags(); + + for(int i=0;i - add non-dimmable light accessory using name=",[](const char *c){addLight(c+1,false,NO_COLOR);}); + new SpanUserCommand('A'," - add dimmable light accessory using name=",[](const char *c){addLight(c+1,true,NO_COLOR);}); + new SpanUserCommand('t'," - add non-dimmable light accessory with color-temperature control using name=",[](const char *c){addLight(c+1,false,TEMPERATURE_ONLY);}); + new SpanUserCommand('T'," - add dimmable light accessory with color-temperature control using name=",[](const char *c){addLight(c+1,true,TEMPERATURE_ONLY);}); + new SpanUserCommand('r'," - add non-dimmable light accessory with full RGB color control using name=",[](const char *c){addLight(c+1,false,FULL_RGB);}); + new SpanUserCommand('R'," - add dimmable light accessory with full RGB color control using name=",[](const char *c){addLight(c+1,true,FULL_RGB);}); + + new SpanUserCommand('l'," - list all light accessories",listAccessories); + new SpanUserCommand('d'," - delete a light accessory with index=",[](const char *buf){deleteAccessory(atoi(buf+1));}); + new SpanUserCommand('D'," - delete ALL light accessories",deleteAllAccessories); + new SpanUserCommand('u',"- update accessories database",updateAccessories); + +} // end of setup() + +/////////////////////////// + +void loop(){ + homeSpan.poll(); + webServer.handleClient(); // handle incoming web server traffic +} + +/////////////////////////// + +void addLight(int index){ + + Serial.printf("Adding Light Accessory: Name='%s' Dimmable=%s Color=%s\n", + lightData[index].name,lightData[index].isDimmable?"YES":"NO",lightData[index].colorType==NO_COLOR?"NONE":(lightData[index].colorType==TEMPERATURE_ONLY?"TEMPERATURE_ONLY":"FULL_RGB")); + + new SpanAccessory(lightData[index].aid); + new Service::AccessoryInformation(); + new Characteristic::Identify(); + new Characteristic::Name(lightData[index].name); + char sNum[32]; + sprintf(sNum,"Light-%02d",index); + new Characteristic::SerialNumber(sNum); + + new Service::LightBulb(); + new Characteristic::On(0,true); + if(lightData[index].isDimmable) + new Characteristic::Brightness(100,true); + if(lightData[index].colorType==TEMPERATURE_ONLY) + new Characteristic::ColorTemperature(200,true); + if(lightData[index].colorType==FULL_RGB){ + new Characteristic::Hue(0,true); + new Characteristic::Saturation(0,true); + } + +} + +/////////////////////////// + +int addLight(const char *name, boolean isDimmable, colorType_t colorType){ + + int index=0; + for(index=0;indexsizeof(lightData[index].name)) + Serial.printf("Warning - name trimmed to max length of %d characters.\n",MAX_NAME_LENGTH); + + lightData[index].isDimmable=isDimmable; + lightData[index].colorType=colorType; + lightData[index].aid=aidStore++; + + nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS + nvs_set_u32(savedData,"AID",aidStore); + nvs_commit(savedData); + + addLight(index); + return(index); +} + +/////////////////////////// + +size_t strncpy_trim(char *dest, const char *src, size_t dSize){ + + while(*src==' ') // skip over any leading spaces + src++; + + size_t sLen=strlen(src); // string length of src after skipping over leading spaces + while(sLen>0 && src[sLen-1]==' ') // shorten length to remove trailing spaces + sLen--; + + size_t sSize=sLen+1; // add room for null terminator + + if(dest!=NULL) + *stpncpy(dest,src,(dSize=MAX_LIGHTS){ + Serial.printf("Invalid Light Accessory index - must be between 0 and %d.\n",MAX_LIGHTS-1); + return; + } + + if(homeSpan.deleteAccessory(lightData[index].aid)){ // if deleteAccessory() is true, a match has been found + Serial.printf("Deleting Light Accessory: Name='%s'\n",lightData[index].name); + + lightData[index].aid=0; + nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS + nvs_commit(savedData); + + } else { + Serial.printf("Nothing to delete - there is no Light Accessory at index=%d.\n",index); + } +} + +/////////////////////////// + +void deleteAllAccessories(const char *buf){ + + for(int i=0;i"; + response += "":">") + ""; + response += "":">") + " NONE "; + response += "":">") + " TEMP ONLY "; + response += "":">") + " FULL COLOR "; + response += ""; + response += ""; + openSlots--; + } + } + + response += ""; + response += ""; + response += ""; + response += ""; + response += ""; + response += ""; + response += ""; + + response += ""; + response += ""; + + if(!openSlots) + response += "

Can't add any more Light Accessories. Max="+ String(MAX_LIGHTS) + "

"; + + response += "

Press here to delete ALL Light Accessories:

"; + response += "

Press here to update the Home App when finished making changes:

"; + + response += ""; + webServer.send(200, "text/html", response); + + }); + + webServer.on("/deleteLight", []() { + + int index=atoi(webServer.arg(0).c_str()); + + String response = "HomeSpan Programmable Light Hub"; + response += "Deleting Light Accessory '" + String(lightData[index].name) + "'..."; + + deleteAccessory(index); + + webServer.send(200, "text/html", response); + + }); + + webServer.on("/deleteAll", []() { + + String response = "HomeSpan Programmable Light Hub"; + response += "Deleting All Light Accessories..."; + + webServer.send(200, "text/html", response); + deleteAllAccessories(""); + + }); + + webServer.on("/update", []() { + + String response = "HomeSpan Programmable Light Hub"; + + if(homeSpan.updateDatabase()) + response += "Accessories Database updated. New configuration number broadcasted..."; + else + response += "Nothing to update - no changes were made..."; + + response += "..."; + + webServer.send(200, "text/html", response); + + }); + + webServer.on("/addLight", []() { + + colorType_t colorType=NO_COLOR; + boolean isDimmable=false; + int iName=-1; + + for(int i=0;i :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 + * :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 @@ -132,12 +134,7 @@ The following **optional** `homeSpan` methods enable additional features and pro * 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 - * :exclamation: 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 deleteStoredValues()` - * deletes the value settings of all stored Characteristics from the NVS - * performs the same function as typing 'V' into the CLI - * can by called from anywhere in a sketch + * :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)` * sets the version of a HomeSpan sketch to *sVer*, which can be any arbitrary character string @@ -162,6 +159,43 @@ The following **optional** `homeSpan` methods enable additional features and pro * `void setTimeServerTimeout(uint32_t tSec)` * changes the default 10-second timeout period HomeSpan uses when `enableWebLog()` tries set the device clock from an internet time server to *tSec* seconds +--- + +The following **optional** `homeSpan` methods provide additional run-time functionality for more advanced use cases: + +* `void deleteStoredValues()` + * deletes the value settings of all stored Characteristics from the NVS + * performs the same function as typing 'V' into the CLI + +* `boolean deleteAccessory(uint32_t aid)` + * deletes Accessory with Accessory ID of *aid*, if found + * returns true if successful (match found), or false if the specified *aid* does not match any current Accessories + * allows for dynamically changing the Accessory database during run-time (i.e. changing the configuration *after* the Arduino `setup()` has finished) + * deleting an Accessory automatically deletes all Services, Characteristics, and any other resources it contains + * outputs Level-1 Log Messages listing all deleted components + * note: though deletions take effect immediately, HomeKit Controllers, such as the Home App, will not be aware of these changes until the database configuration number is updated and rebroadcast - see updateDatabase() below + +* `boolean updateDatabase()` + * recomputes the database configuration number and, if changed, rebroadcasts the new number via MDNS so all connected HomeKit Controllers, such as the Home App, can request a full refresh to accurately reflect the new configuration + * returns true if configuration number has changed, false otherwise + * *only* needed if you want to make run-time (i.e. after the Arduino `setup()` function has completed) changes to the device's Accessory database + * use anytime after dynamically adding one or more Accessories (with `new SpanAccessory(aid)`) or deleting one or more Accessories (with `homeSpan.deleteAccessory(aid)`) + * **important**: once you delete an Accessory, you cannot re-use the same *aid* when adding a new Accessory (on the same device) unless the new Accessory is configured with the exact same Services and Characteristics as the deleted Accessory + * note: this method is **not** needed if you have a static Accessory database that is fully defined in the Arduino `setup()` function of a sketch + +--- + +The following `homeSpan` methods are considered experimental, since not all use cases have been explored or debugged. Use with caution: + +* `void autoPoll(uint32_t stackSize)` + * an *optional* method to create a task with *stackSize* bytes of stack memory that repeatedly calls `poll()` in the background. This frees up the Ardino `loop()` method for any user-defined code to run in parallel that would otherwise block, or be blocked by, calling `poll()` in the `loop()` method + * 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 + * can be used with both single-core and dual-core ESP32 boards. If used with a dual-core board, the polling task is created on the free processor that is typically not running other Arduino functions + * if *stackSize* is not specified, defaults to the size used by the system for the normal Arduino `loop()` task (typically 8192 bytes) + * 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 + * warning: if any code you add to the Arduino `loop()` method tries to alter any HomeSpan settings or functions running in the background `poll()` task, race conditions may yield undefined results + ## *SpanAccessory(uint32_t aid)* Creating an instance of this **class** adds a new HAP Accessory to the HomeSpan HAP Database. @@ -317,7 +351,7 @@ This is a **base class** from which all HomeSpan Characteristics are derived, an * returns a pointer to the Characteristic itself so that the method can be chained during instantiation * example: `(new Characteristic::RotationSpeed())->setUnit("percentage");` -## *SpanButton(int pin, uint16_t longTime, uint16_t singleTime, uint16_t doubleTime)* +### *SpanButton(int pin, uint16_t longTime, uint16_t singleTime, uint16_t doubleTime, boolean (\*triggerType)(int))* Creating an instance of this **class** attaches a pushbutton handler to the ESP32 *pin* specified. @@ -325,23 +359,52 @@ Creating an instance of this **class** attaches a pushbutton handler to the ESP3 * instantiating a Button without first instantiating a Service throws an error during initialization The first argument is required; the rest are optional: -* *pin* - the ESP32 pin to which a one pole of a normally-open pushbutton will be connected; the other pole is connected to ground + +* *pin* - the ESP32 pin to which the button is connected * *longTime* - the minimum time (in millis) required for the button to be pressed and held to trigger a Long Press (default=2000 ms) * *singleTime* - the minimum time (in millis) required for the button to be pressed to trigger a Single Press (default=5 ms) * *doubleTime* - the maximum time (in millis) allowed between two Single Presses to qualify as a Double Press (default=200 ms) - -Trigger Rules: +* *triggerType* - pointer to a boolean function that accepts a single *int* argument and returns `true` or `false` depending on whether or not a "press" has been triggered on the *pin* number passed to the *int* argument. For ease of use, you may simply choose from the following built-in functions: + * `SpanButton::TRIGGER_ON_LOW` - triggers when *pin* is driven LOW. Suitable for buttons that connect *pin* to GROUND (this is the default when *triggerType* is not specified) + + * `SpanButton::TRIGGER_ON_HIGH` - triggers when *pin* is driven HIGH. Suitable for buttons that connect *pin* to VCC (typically 3.3V) + * `SpanButton::TRIGGER_ON_TOUCH`- uses the device's touch-sensor peripheral to trigger when a sensor attached to *pin* has been touched (not available on ESP32-C3 devices) + +When any of these built-in functions are selected (or *triggerType* is left unspecified and the default is used), SpanButton will automatically configure the *pin* as needed upon instantiation. + +Alternatively, you can set *triggerType* to any user-defined function of the form `boolean(int arg)` and provide your own logic for determining whether a trigger has occured on the specified *pin*, which is passed through to your function as *arg*. In this case *arg* can either represent an actual device pin, or simply be an arbitrary *int* your function utilizes, such as the virtual pin number on a multiplexer. Note: if you specify your own function for *triggerType* you also must include in your sketch any code needed to initialize the logic or configure whatever resource *triggerType* is utilizing (such as a pin multiplexer). + +For convenience, a second form of the *SpanButton()* constructor is also provided: + * `SpanButton(int pin, boolean (*triggerType)(int), uint16_t longTime=2000, uint16_t singleTime=5, uint16_t doubleTime=200)` + * this allows you to set just the *pin* and *triggerType* while leaving the remaining parameters at their default values + +#### Trigger Rules ### * If button is pressed and continuously held, a Long Press will be triggered every longTime ms until the button is released * If button is pressed for more than singleTime ms but less than longTime ms and then released, a Single Press will be triggered, UNLESS the button is pressed a second time within doubleTime ms AND held again for at least singleTime ms, in which case a DoublePress will be triggered; no further events will occur until the button is released * If singleTime>longTime, only Long Press triggers can occur * If doubleTime=0, Double Presses cannot occur - -HomeSpan automatically calls the `button(int pin, int pressType)` method of a Service upon a trigger event in any Button associated with that Service, where *pin* is the ESP32 pin to which the pushbutton is connected, and *pressType* is an integer that can also be represented by the enum constants indicated: - * 0=single press (SpanButton::SINGLE) - * 1=double press (SpanButton::DOUBLE) - * 2=long press (SpanButton::LONG) + +#### Usage #### +HomeSpan automatically calls the `button(int pin, int pressType)` method of a Service upon a trigger event in any SpanButton associated with that Service, where *pin* is the ESP32 pin to which the pushbutton is connected, and *pressType* is an integer that can also be represented by the enum constants indicated: + * 0=single press (`SpanButton::SINGLE`) + * 1=double press (`SpanButton::DOUBLE`) + * 2=long press (`SpanButton::LONG`) HomeSpan will report a warning, but not an error, during initialization if the user had not overridden the virtual button() method for a Service contaning one or more Buttons; triggers of those Buttons will simply ignored. + +When using one or more Touch Sensors, HomeSpan automatically calibrates the threshold at which they are triggered by polling the baseline sensor reading upon instantiation of first SpanButton of type `SpanButton::TRIGGER_ON_TOUCH`. For ESP32 devices, the threshold is set to 50% of the baseline value since triggers occur when a sensor value falls *below* the threhold level. For ESP32-S2 and ESP32-S3 devices, the threshold is set to 200% of the baseline value since triggers occur when a sensor value rises *above* the threhold level. Normally HomeSpan's auto calibration will result in accurate detection of SINGLE, DOUBLE, and LONG presses of touch sensors. However, if needed you can override the calibration and set your own threshold value using the following class-level method: + + * `void SpanButton::setTouchThreshold(uintXX_t thresh)` + * sets the threshold value above (for ESP32 devices) or below (for ESP32-S2 and ESP32-S3 devices) which touch sensors are triggered to *thresh* + * *XX* is 16 (for ESP32 devices) or 32 (for ESP32-S2 and ESP32-S3 devices) + * the threshold specified is considered global and used for *all* SpanButton instances of type `SpanButton::TRIGGER_ON_TOUCH` + * this method can be called either before or after SpanButtons are created + +In addition, you can also override the ESP32's touch sensor timing parameters using the following class-level method: + +* `void SpanButton::setTouchCycles(uint16_t measureTime, uint16_t sleepTime)` + * changes the measurement time and sleep time clock cycles to *measureTime* and *sleepTime*, respectively. This is simply a pass-though call to the Arduino-ESP32 library `touchSetCycles()` function + * unless a specific threshold value has been set with `setTouchThreshold()`, `setTouchCycles()` must be called *before* instantiating the first SpanButton() of type `SpanButton::TRIGGER_ON_TOUCH` so that HomeSpan will calibrate the touch threshold based on the new timing parameters specified ### *SpanUserCommand(char c, const char \*desc, void (\*f)(const char \*buf [,void \*obj]) [,void \*userObject])* diff --git a/docs/Tutorials.md b/docs/Tutorials.md index b04c2ff..6ea8fd0 100644 --- a/docs/Tutorials.md +++ b/docs/Tutorials.md @@ -99,6 +99,14 @@ Example 19 illustrates, through the implementation of two On/Off LEDs, how to ad * enabling the HomeSpan Web Log and specifying an optional NTP time server with the `homeSpan.enableWebLog()` method * using the `WEBLOG()` macro to create Web Log messages +### [Example 20 - AdvancedTechniques](../examples/20-AdvancedTechniques) +Example 20 illustrates a number of advanced techniques through the implementation of a "dynamic" bridge that allows Light Accessories to be *interactively* added and deleted at any time without the need to reboot the device. New HomeSpan API topics covered in this example include: + +* creating custom CLI commands using `SpanUserCommand()` +* dynamically deleting Accessories with `homeSpan.deleteAccessory()` +* refreshing the Accessory database (which automatically updates the Home App) using `homeSpan.updateDatabase()` +* using `homeSpan.autoPoll()` to implement HomeSpan Polling in the background (and on the second core, if available) + ## Other Examples The following examples showcase a variety of HomeSpan and HomeKit functionality as referenced in different sections of the HomeSpan documentation. The sketches can be found in the Arduino IDE under *File → Examples → HomeSpan → Other Examples* @@ -121,6 +129,9 @@ Demonstrates how to use HomeSpan's *Pixel* and *Dot* classes to control one- and ### [CustomService](../Other%20Examples/CustomService) Demonstrates how to create Custom Services and Custom Characteristics in HomeSpan to implement an Atmospheric Pressure Sensor recognized by the *Eve for HomeKit* app. See [Custom Characteristics and Custom Services Macros](Reference.md#custom-characteristics-and-custom-services-macros) for full details +### [ProgrammableHub](../Other%20Examples/ProgrammableHub) +Demonstrates how to implement a fully programmable Light Accessory Hub that allows the user to *dynamically* add/delete up to 12 Light Accessories directly through a device-hosted *web interface* or via HomeSpan's *command-line inteface*. Each light can be configured as dimmable/non-dimmable with either no color control, full RGB color control, or color-temperature control. Builds upon many of the techniques used in [Example 20](../examples/20-AdvancedTechniques) + --- [↩️](README.md) Back to the Welcome page diff --git a/examples/15-RealPushButtons/15-RealPushButtons.ino b/examples/15-RealPushButtons/15-RealPushButtons.ino index c6219a8..491a97f 100644 --- a/examples/15-RealPushButtons/15-RealPushButtons.ino +++ b/examples/15-RealPushButtons/15-RealPushButtons.ino @@ -47,9 +47,9 @@ void setup() { // Additionally, we want HomeKit to reflect any changes in the device as a result of such manual actions - HomeKit should know // when the light has been turned on or off manually. - // One way to accomplish would be via custom code added to the loop() method of your derived Service that monitors the button, + // One way to accomplish would be via custom code added to the loop() method of your derived Service that monitors a pushbutton, // checks when it is pressed, debounces button noise, performs some actions when pressed, and informs HomeKit of the actions with - // the setVal() method. Or you can use HomeSpan's built-in SpanButton() object. + // the setVal() method. Or you can simply use HomeSpan's built-in SpanButton() object. // SpanButton() is a Service-level object, meaning it attaches itself to the last Service you define. Typically you would instantiate // one of more SpanButton() objects directly inside the constructor for your derived Service. @@ -61,18 +61,21 @@ void setup() { // It's fine to change this to a longer value, but a shorter value is not recommended as this may allow spurious triggers unless // you debounce your switch with hardware. - // The SpanButton() constructor takes 4 arguments, in the following order: + // The SpanButton() constructor takes 5 arguments, in the following order: // // pin - the pin number to which the PushButton is attached (required) // longTime - the minimum length of time (in milliseconds) the button needs to be pushed to be considered a LONG press (optional; default=2000 ms) // singleTime - the minimum length of time (in milliseconds) the button needs to be pushed to be considered a SINGLE press (optional; default=5 ms) // doubleTime - the maximum length of time (in milliseconds) between button presses to create a DOUBLE press (optional; default=200 ms) + // triggerType - the action that causes a trigger on the pin (optional; default=SpanButton::TRIGGER_ON_LOW). Built-in choices include: + // + // SpanButton::TRIGGER_ON_LOW: used for a button that connects pin to GROUND + // SpanButton::TRIGGER_ON_HIGH: used for a button that connects pin to VCC (typically +3.3V) + // SpanButton::TRIGGER_ON_TOUCH: used when a pin is connected to a touch pad/sensor - // When a SpanButton() is instantiated, it sets the specified pin on the ESP32 to be an INPUT with PULL-UP, meaning that the pin will - // normally return a value of HIGH when read. Your actual PushButton should be connected so that this pin is GROUNDED when the button - // is pressed. + // When a SpanButton() is first instantiated, HomeSpan configures the specified pin in accordance with the triggerType chosen. - // HomeSpan automatically polls all pins with associated SpanButton() objects and checks for LOW values, which indicates the button was + // Then, HomeSpan continuously polls all pins with associated SpanButton() objects and checks for triggers, which indicates the button was // pressed, but not yet released. It then starts a timer. If the button is released after being pressed for less than singleTime milliseconds, // nothing happens. If the button is released after being pressed for more than singleTime milliseconds, but for less than longTime milliseconds, // a SINGLE press is triggered, unless you press once again within doubleTime milliseconds to trigger a DOUBLE press. If the button is held for more @@ -90,7 +93,7 @@ void setup() { // Also in contrast with the loop method, the button() method takes two 'int' arguments, and should defined as follows: // - // void button(int pin, int pressType) + // void button(int pin, int pressType) // // where "pin" is the pin number of the PushButton that was triggered, and pressType is set to 0 for a SINGLE press, 1 for a DOUBLE press, // and 2 for a LONG press. You can also use the pre-defined constants SpanButton::SINGLE, SpanButton::DOUBLE, and SpanButton::LONG in place @@ -104,7 +107,7 @@ void setup() { // // C++ Note: For an extra check, you can also place the the contextual keyword "override" after your method definition as such: // - // void button(int buttonPin, int pressType) override {...your code...} + // void button(int buttonPin, int pressType) override {...your code...} // // Doing so allows the compiler to check that you are indeed over-riding the base class button() method and not inadvertently creating a new // button() method with an incorrect signature that will never be called by SpanButton(). In fact, you could add "override" to the definition @@ -150,3 +153,115 @@ void loop(){ homeSpan.poll(); } // end of loop() + +//////////////// ADDITIONAL NOTES //////////////////////// + + // DEFAULT VALUES AND ALTERNATIVE CONSTRUCTORS + // -------------------------------------------- + + // As shown in this example, the following creates a SpanButton suitable for connecting pin 23 to GROUND via a pushbutton, and uses + // SpanButton's default values for longTime, singleTime, and doubleTime: + // + // new SpanButton(23); + // + // This is exactly the same as if you explicitly set each parameter to its default value: + // + // new SpanButton(23,2000,5,200,SpanButton::TRIGGER_ON_LOW); // equivalent to above + // + // If instead you want to create a SpanButton that connects pin 23 to VCC via a pushbutton using SpanButton::TRIGGER_ON-HIGH, + // you need to explictly set all the other parameters, even if you are satisfied with their default values, since triggerType + // is the last argument in the constructor: + // + // new SpanButton(23,2000,5,200,SpanButton::TRIGGER_ON_HIGH); + // + // Because this can be cumbersome, SpanButton includes an alternative constructor where triggerType is the second paramater, instead + // of the last. In this case triggerType is required, but longTime, singleTime, and doubleTime are still optional. + // + // For example, the following creates a SpanButton suitable for connecting pin 23 to a touch pad/sensor, and uses + // SpanButton's default values for longTime, singleTime, and doubleTime: + // + // new SpanButton(23,SpanButton::TRIGGER_ON_TOUCH); + // + // which is of course equivalent to: + // + // new SpanButton(23,SpanButton::TRIGGER_ON_TOUCH,2000,5,200); + + + // TOUCH PAD/SENSOR CALIBRATION + // ---------------------------- + + // SpanButton makes use of the ESP32's internal touch sensor peripheral to monitor pins for "touches". There are a number + // of paramaters that must be specified for touches to be accurately detected, depending on the exact size and shape of your + // touch pads. Upon instantiation of a SpanButton() with triggerType=SpanButton::TRIGGER_ON_TOUCH, SpanButton will conveniently + // perform an automatic calibration that sets an appropriate threshold level for detecting touches. + // + // However, if you need to, you can override this calibration process using the following two class-level functions: + // + // SpanButton::setTouchThreshold() - explicitly sets the threshold for detecting touches (i.e. overrides the auto-calibration) + // SpanButton::setTouchCycles() - explicitly sets the measurement and sleep times used by the ESP32's internal touch peripheral + // + // See the SpanButton secion of the Reference API for details on how to use these optional functions. + + + // THE triggerType FUNCTION + // ------------------------- + + // Though the three triggerType objects supported by SpanButton (SpanButton::TRIGGER_ON_LOW, etc.) may appear to be nothing more than + // constants, they are actually boolean functions that each accept a single integer argument. When SpanButton calls the triggerType function, + // it passes the pin number specified in the constructor as the integer argument, and the triggerType function returns TRUE if the + // "pushbutton" associated with the pin number is "pressed," or FALSE if it is not. + // + // For example, the definitions of SpanButton::TRIGGER_ON_LOW and SpanButton::TRIGGER_ON_HIGH are as follows: + // + // boolean TRIGGER_ON_LOW(int pinArg) { return( !digitalRead(pinArg) ); } + // boolean TRIGGER_ON_HIGH(int pinArg) { return( digitalRead(pinArg) ); } + // + // The definitions for SpanButton::TRIGGER_ON_TOUCH are more complicated since the ESP32 touch sensor library returns either a 2-byte + // or 4-byte numeric value when the state of pin configured as a touch sensor is read, rather than a simple 0 or 1. The triggerType + // function must therefore compare the value read from the touch sensor pin to some pre-computed "threshold" to determine whether or not + // the touch pad has in fact been touched. This is the threshold value that HomeSpan auto-calibrates for you as described above. + // + // Making things even more complex is that the ESP32 touch pins work in the reverse direction as touch pins on the ESP32-S2 and ESP32-S3. + // On the former, the values read from a touch sensor DECREASE when the touch pad is touched. On the latter, the values increase when the + // touch pad is touched. This means that for ESP32 devices, HomeSpan uses the following definition for SpanButton::TRIGGER_ON_TOUCH: + // + // boolean TRIGGER_ON_TOUCH(int pinArg) { return ( touchRead(pinArg) < threshold ); } + // + // whereas on ESP32-S2 and ESP32-S3 devices, HomeSpan uses a definition that flips the direction of the comparison: + // + // boolean TRIGGER_ON_TOUCH(int pinArg) { return ( touchRead(pinArg) > threshold ); } + // + // For ESP32-C3 devices, HomeSpan does not define TRIGGER_ON_TOUCH at all since there are no touch pins on an ESP32-C3 device! The compiler + // will throw an error if you try to create a SpanButton with triggerType=SpanButton::TRIGGER_ON_TOUCH, or if you call either of the + // calibration functions above. + // + + // CREATING YOUR OWN triggerType FUNCTION + // -------------------------------------- + + // You are not limited to choosing among HomeSpan's three built-in triggerType functions. You can instead create your own triggerType function + // and pass it to SpanButton as the triggerType parameter in the SpanButton constructor. Your function must be of the form `boolean func(int)`, + // and should return TRUE if the "pushbutton" associated with the pin number that HomeSpan passes to your function as the integer argument + // has been "pressed", or FALSE if it has not. This allows you to expand the used of SpanButton to work with pin multiplexers, pin extenders, + // or any device that may require custom handling via a third-party library. + // + // For example, if you were using an MCP I/O Port Expander with the Adafruit mcp library, you could create a triggerType function for a pin + // on the MCP device that is connected to ground through a pushbutton as such: + // + // boolean MCP_READ(int mcpPin) { return ( !mcp.digitalRead(mcpPin); ) } + // + // And then simply pass MCP_READ to SpanButton as the triggerType parameter using any of the SpanButton constuctors: + // + // new SpanButton(23,MCP_READ); // uses default longTime, singleTime, and doubleTime + // new SpanButton(23,MCP_READ,2000,5,200); // expliclty sets longTime, singleTime, and doubletime + // new SpanButton(23,2000,5,200,MCP_READ); // alternative constructor with arguments in a different order + // + // Alternatively, you can use a lambda function as the triggerType parameter, thus creating your function on the fly when instantiating a SpanButton: + // + // new SpanButton(23,[](int mcpPin)->boolean{ return ( !mcp.digitalRead(mcpPin); ) } + // + // Note: If you create your own triggerType function, don't forget to perform any initialization of the "pin", or setup/configuration of a + // pin extender, etc., prior to instantiating a SpanButton that uses your custom function. HomeSpan cannot do this for you. + // + + diff --git a/examples/15-RealPushButtons/DEV_LED.h b/examples/15-RealPushButtons/DEV_LED.h index ee2ca6f..1f03838 100644 --- a/examples/15-RealPushButtons/DEV_LED.h +++ b/examples/15-RealPushButtons/DEV_LED.h @@ -36,7 +36,8 @@ struct DEV_DimmableLED : Service::LightBulb { // Dimmable LED // NEW! Below we create three SpanButton() objects. In the first we specify the pin number, as required, but allow SpanButton() to use // its default values for a LONG press (2000 ms), a SINGLE press (5 ms), and a DOUBLE press (200 ms). In the second and third we change the - // default LONG press time to 500 ms, which works well for repeatedly increasing or decreasing the brightness. + // default LONG press time to 500 ms, which works well for repeatedly increasing or decreasing the brightness. Since we do not specify + // a triggerType, SpanButton uses the default TRIGGER_ON_TOUCH, which is suitable for a pushbutton that connects pin to GROUND when pressed. // All of the logic for increasing/decreasing brightness, turning on/off power, and setting/resetting a favorite brightness level is found // in the button() method below. diff --git a/examples/20-AdvancedTechniques/20-AdvancedTechniques.ino b/examples/20-AdvancedTechniques/20-AdvancedTechniques.ino new file mode 100644 index 0000000..90c0c88 --- /dev/null +++ b/examples/20-AdvancedTechniques/20-AdvancedTechniques.ino @@ -0,0 +1,251 @@ +/********************************************************************************* + * MIT License + * + * Copyright (c) 2022 Gregg E. Berman + * + * https://github.com/HomeSpan/HomeSpan + * + * 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. + * + ********************************************************************************/ + +////////////////////////////////////////////////////////////////// +// // +// HomeSpan: A HomeKit implementation for the ESP32 // +// ------------------------------------------------ // +// // +// Example 20: Demonstrates various advance HomeSpan functions // +// by implementing a Bridge in which one or more // +// Lightbulb Accessories can be added and deleted // +// *dynamically* without needing to restart the // +// device // +// // +////////////////////////////////////////////////////////////////// + +#include "HomeSpan.h" + + // In Example 20 we will implement a bridge device supporting up to 10 Lightbulb Accessories. However, rather than pre-specifying the number of Lights, we + // will allow Light Accessories to be added and deleted dynamically by the user via the CLI. Changes are reflected in the Home App without the need to restart + // the device! Note this example uses a variety of advanced HomeSpan functions, as well as some detailed features of both the ESP32-IDF and C++ that have not been used + // in any of the previous examples. + + // We will use a C++ array with 10 elements containing integers representing the Light "ID" of each Lightbulb Accessory implemented. An ID of zero means there is no + // Light defined in that element. + +#include // include the C++ standard library array container + +std::array lights; // declare "lights" to be an array of 10 integers + +using std::fill; // place the std library function fill, remove, and find, into the global namespace so we can use them below without prefacing with "std::" +using std::remove; +using std::find; + + // We will use non-volatile storage (NVS) to store the lights array so that the device can restore the current configuration upon rebooting + +nvs_handle savedData; // declare savdData as a handle to be used with the NVS (see the ESP32-IDF for details on how to use NVS storage) + + +////////////////////////////////////// + +void setup() { + + Serial.begin(115200); + + fill(lights.begin(),lights.end(),0); // initialize lights array with zeros in each of the 10 elements (no Light Accessories defined) + + size_t len; + nvs_open("SAVED_DATA",NVS_READWRITE,&savedData); // open a new namespace called SAVED_DATA in the NVS + if(!nvs_get_blob(savedData,"LIGHTS",NULL,&len)) // if LIGHTS data found + nvs_get_blob(savedData,"LIGHTS",&lights,&len); // retrieve data + + homeSpan.setLogLevel(1); + + homeSpan.begin(Category::Lighting,"HomeSpan Lights"); + + // We begin by creating the Bridge Accessory + + new SpanAccessory(1); // here we specified the AID=1 for clarity (it would default to 1 anyway if left blank) + new Service::AccessoryInformation(); + new Characteristic::Identify(); + new Characteristic::Model("HomeSpan Dynamic Bridge"); // defining the Model is optional + + // Now we create Light Accessories based on what is recorded in the lights array + // We'll use C++ iterators to loop over all elements until we reach the end of the array, or find an element with a value of zero + + for(auto it=lights.begin(); it!=lights.end() && *it!=0; it++) // loop over all elements (stopping when we get to the end, or hit an element with a value of zero) + addLight(*it); // call addLight (defined further below) with an argument equal to the integer stored in that element + + // Next we create four user-defined CLI commands so we can add and delete Light Accessories from the CLI. + // The functions for each command are defined further below. + + new SpanUserCommand('a'," - add a new light accessory with id=",addAccessory); + new SpanUserCommand('d'," - delete a light accessory with id=",deleteAccessory); + new SpanUserCommand('D'," - delete ALL light accessories",deleteAllAccessories); + new SpanUserCommand('u',"- update accessories database",updateAccessories); + + // Finally we call autoPoll to start polling the background. Note this is purely optional and only used here to illustrate how to + // use autoPoll - you could instead have called the usual homeSpan.poll() function by including it inside the Arduino loop() function + + homeSpan.autoPoll(); + +} // end of setup() + +// Usually the Arduino loop() function would be defined somewhere here. But since we used autoPoll in the setup() function, +// we don't have to define the loop() function at all in this sketch! Why don't we get an error? Because HomeSpan includes +// a default loop() function, which prevents the compiler from complaining about loop() being undefined. + +/////////////////////////// + +// This function creates a new Light Accessory with n as the "ID". +// It is called initially in setup() above to create Light Accessories based +// on what was stored in the lights array. It is also called in response to +// typing 'a' into the CLI (see below), which dynamically adds a new Light Accessory +// while the device is running. + +void addLight(int n){ + + char name[32]; + sprintf(name,"Light-%d",n); // create the name of the device using the specified "ID" + char sNum[32]; + sprintf(sNum,"%0.10d",n); // create serial number from the ID - this is helpful in case we rename the Light to something else using the Home App + + Serial.printf("Adding Accessory: %s\n",name); + + new SpanAccessory(n+1); // IMPORTANT: add 1, since first Accessory with AID=1 is already used by the Bridge Accessory + new Service::AccessoryInformation(); + new Characteristic::Identify(); + new Characteristic::Name(name); + new Characteristic::SerialNumber(sNum); + new Service::LightBulb(); + new Characteristic::On(0,true); +} + +/////////////////////////// + +// This function is called in response to typing '@a ' into the CLI. +// It adds a new Light Accessory with ID=num, by calling addLight(num) above. + +void addAccessory(const char *buf){ + + int n=atoi(buf+1); // read the value of specified + + if(n<1){ // ensure is greater than 0 + Serial.printf("Invalid Accessory number!\n"); + return; + } + + if(find(lights.begin(),lights.end(),n)!=lights.end()){ // search for this ID in the existing lights array - if found, report an error and return + Serial.printf("Accessory Light-%d already implemented!\n",n); + return; + } + + auto it=find(lights.begin(),lights.end(),0); // find the next "free" element in the light array (the first element with a value of zero) + + if(it==lights.end()){ // if there were no elements with a zero, the array is full and no new Lights can be added + Serial.printf("Can't add any more lights - max is %d!\n",lights.size()); + return; + } + + *it=n; // save light number + nvs_set_blob(savedData,"LIGHTS",&lights,sizeof(lights)); // update data in the NVS + nvs_commit(savedData); + addLight(n); // add light accessory by calling the function above! +} + +/////////////////////////// + +// This function deletes an existing Light Accessory and is called +// in response to typing '@d ' into the CLI. + +void deleteAccessory(const char *buf){ + + int n=atoi(buf+1); // same as above, we read the specified and check that it is valid (i.e. greater than 0) + + if(n<1){ + Serial.printf("Invalid Accessory number!\n"); + return; + } + + // Below we use the homeSpan method deleteAccessory(aid) to completely delete the Accessory with AID=n+1. + // We add 1 because the AID of the first Light Accessory is 2, since the Bridge Accessory has an AID of 1. + // The deleteAccessory() method returns true if an Accessory with matching AID is found, otherwise it returns false. + // When deleting an Accessory, HomeSpan will print a delete message for every Service, Characteristic, loop() method, + // button() method, and SpanButton, associated with that Accessory. These are Level-1 Log messages, so you'll need + // to have the Log Level in the sketch set to 1 or 2 to receive the output. + + if(homeSpan.deleteAccessory(n+1)){ // if deleteAccessory() is true, a match has been found + Serial.printf("Deleting Accessory: Light-%d\n",n); + + fill(remove(lights.begin(),lights.end(),n),lights.end(),0); // remove entry from lights array and fill any undefined elements with zero + nvs_set_blob(savedData,"LIGHTS",&lights,sizeof(lights)); // update data in the NVS + nvs_commit(savedData); + + } else { + Serial.printf("No such Accessory: Light-%d\n",n); + } +} + +/////////////////////////// + +void deleteAllAccessories(const char *buf){ + +// This function is called in response to typing '@D' into the CLI. +// It deletes all Light Accessories + + if(lights[0]==0){ // first check that there is at least one Light Accessory by checking for a non-zero ID in lights[0] + Serial.printf("There are no Light Accessories to delete!\n"); + return; + } + + for(auto it=lights.begin(); it!=lights.end() && *it!=0; it++) // use an iterator to loop over all non-zero elements in the lights array... + homeSpan.deleteAccessory(*it+1); // ... and delete the matching Light Accessory (don't forgot to add 1 to the Light ID to form the AID) + + fill(lights.begin(),lights.end(),0); // zero out all the elements in the lights array, since all Light Accessories have been deleted + nvs_set_blob(savedData,"LIGHTS",&lights,sizeof(lights)); // update data in the NVS + nvs_commit(savedData); + + Serial.printf("All Light Accessories deleted!\n"); +} + +/////////////////////////// + +// Lastly we have the all-important updateAccessories function. +// This is called in response to typing '@u' into the CLI. +// Though the above functions can be used to add and delete Light Accessories +// dyammically, Controllers such as the Home App that are already connected to +// the device don't yet know additional Light Accessories have been added to (or +// deleted from) the overall Accessories datase. To let them know, HomeSpan needs +// to increment the HAP Configuration Number and re-broadcast it via MDNS so all +// connected Controllers are aware that they need to request a refresh from the device. + +// When you type '@u' into the CLI, you should see a lot of activity between the device +// and any connected Controllers as they request a refresh. Be patient - it can take up to a +// minute for changes to be properly reflected in the Home App on your iPhone or Mac. + +void updateAccessories(const char *buf){ + + // note the updateDatabase() method returns true if the database has indeed changed (e.g. one or more new Light Accessories were added), or false if nothing has changed + + if(homeSpan.updateDatabase()) + Serial.printf("Accessories Database updated. New configuration number broadcasted...\n"); + else + Serial.printf("Nothing to update - no changes were made!\n"); +} + +/////////////////////////// diff --git a/library.properties b/library.properties index 55fa778..0a523e6 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=HomeSpan -version=1.5.1 +version=1.6.0 author=Gregg maintainer=Gregg sentence=A robust and extremely easy-to-use HomeKit implementation for the Espressif ESP32 running on the Arduino IDE. -paragraph=This library provides a microcontroller-focused implementation of Apple's HomeKit Accessory Protocol (HAP - Release R2) designed specifically for the ESP32 running on the Arduino IDE. HomeSpan pairs directly to iOS Home via WiFi without the need for any external bridges or components. Compatible with ESP32, ESP32-S2, and ESP32-C3. +paragraph=This library provides a microcontroller-focused implementation of Apple's HomeKit Accessory Protocol (HAP - Release R2) designed specifically for the ESP32 running on the Arduino IDE. HomeSpan pairs directly to iOS Home via WiFi without the need for any external bridges or components. Supports ESP32, ESP32-S2, ESP32-C3, and ESP32-S3. url=https://github.com/HomeSpan/HomeSpan architectures=esp32 includes=HomeSpan.h diff --git a/src/Characteristics.h b/src/Characteristics.h index 7caa9d9..5bce47c 100644 --- a/src/Characteristics.h +++ b/src/Characteristics.h @@ -78,6 +78,7 @@ struct HapChar { struct HapCharacteristics { + HAPCHAR( AccessoryFlags, A6, PR+EV, UINT32, true ); HAPCHAR( Active, B0, PW+PR+EV, UINT8, true ); HAPCHAR( ActiveIdentifier, E7, PW+PR+EV, UINT32, true ); HAPCHAR( AirQuality, 95, PR+EV, UINT8, true ); @@ -113,7 +114,7 @@ struct HapCharacteristics { HAPCHAR( CurrentVisibilityState, 135, PR+EV, UINT8, true ); HAPCHAR( FilterLifeLevel, AB, PR+EV, FLOAT, false ); HAPCHAR( FilterChangeIndication, AC, PR+EV, UINT8, true ); - HAPCHAR( FirmwareRevision, 52, PR, STRING, true ); + HAPCHAR( FirmwareRevision, 52, PR+EV, STRING, true ); HAPCHAR( HardwareRevision, 53, PR, STRING, true ); HAPCHAR( HeatingThresholdTemperature, 12, PR+PW+EV, FLOAT, false ); HAPCHAR( HoldPosition, 6F, PW, BOOL, true ); diff --git a/src/FeatherPins.h b/src/FeatherPins.h index b4a35d6..7072d86 100644 --- a/src/FeatherPins.h +++ b/src/FeatherPins.h @@ -25,21 +25,45 @@ * ********************************************************************************/ -// For developer use and testing only - provides a common set of pin numbers mapped to the Adafruit Feather Board +// For developer use and testing only - provides a common set of pin numbers mapped to the Adafruit Feather Board. // Facilitates the testing of identical code on an ESP32, ESP32-S2, and ESP32-C3 using a common jig without rewiring #pragma once -#if defined(CONFIG_IDF_TARGET_ESP32) - enum {F13=13,F12=12,F27=27,F33=33,F15=15,F32=32,F14=14,F22=22,F23=23,F26=26,F25=25,F34=34,F39=39,F36=36,F4=4,F5=5,F18=18,F19=19,F16=16,F17=17,F21=21}; +#if defined(ARDUINO_FEATHER_ESP32) + enum { + F13=13,F12=12,F27=27,F15=15,F32=32,F14=14,F16=16,F17=17,F21=21, // Digital Only (9 pins) + F26=26,F25=25,F34=34,F39=39,F36=36,F4=4, // A0-A5 + F22=22,F23=23, // I2C SCL/SDA + F5=5,F18=18,F19=19,F33=33 // SPI SCK/SDO/SDI/CS + }; #define DEVICE_SUFFIX "" -#elif defined(CONFIG_IDF_TARGET_ESP32S2) - enum {F13=11,F12=10,F27=7,F33=3,F15=1,F32=38,F14=33,F22=9,F23=8,F26=17,F25=18,F34=14,F39=12,F36=6,F4=5,F5=36,F18=35,F19=37,F16=44,F17=43}; +#elif defined(ARDUINO_ESP32S2_DEV) + enum { + F13=1,F12=3,F27=7,F15=10,F32=42,F14=11,F16=20,F17=21,F21=16, // Digital Only (9 pins) + F26=17,F25=14,F34=13,F39=12,F36=18,F4=19, // A0-A5 + F22=9,F23=8, // I2C SCL/SDA + F5=36,F18=35,F19=37,F33=34 // SPI SCK/SDO/SDI/CS + }; #define DEVICE_SUFFIX "-S2" -#elif defined(CONFIG_IDF_TARGET_ESP32C3) - enum {F27=2,F33=7,F32=3,F14=10,F22=9,F23=8,F26=0,F25=1,F4=18,F5=4,F18=6,F19=5,F16=20,F17=21,F21=19}; +#elif defined(ARDUINO_ESP32C3_DEV) + enum { + F27=2,F32=3,F14=10,F16=20,F17=21,F21=19, // Digital Only (6 pins) + F26=0,F25=1,F4=18, // A0/A1/A5 + F22=9,F23=8, // I2C SCL/SDA + F5=4,F18=6,F19=5,F33=7 // SPI SCK/SDO/SDI/CS + }; #define DEVICE_SUFFIX "-C3" +#elif defined(ARDUINO_ESP32S3_DEV) + enum { + F13=5,F12=6,F27=7,F15=16,F32=17,F14=18,F16=37,F17=36,F21=35, // Digital Only (9 pins) + F26=1,F25=2,F34=20,F39=19,F36=15,F4=4, // A0-A5 + F22=9,F23=8, // I2C SCL/SDA + F5=12,F18=11,F19=13,F33=10 // SPI SCK/SDO/SDI/CS + }; + #define DEVICE_SUFFIX "-S3" + #endif diff --git a/src/HAP.cpp b/src/HAP.cpp index bad8448..c4b2844 100644 --- a/src/HAP.cpp +++ b/src/HAP.cpp @@ -30,7 +30,6 @@ #include #include "HAP.h" -#include "HomeSpan.h" ////////////////////////////////////// @@ -141,43 +140,18 @@ void HAPClient::init(){ if(!nvs_get_blob(hapNVS,"HAPHASH",NULL,&len)){ // if found HAP HASH structure nvs_get_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,&len); // retrieve data } else { - Serial.print("Resetting Accessory Configuration number...\n"); - nvs_set_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,sizeof(homeSpan.hapConfig)); // update data + Serial.print("Resetting Database Hash...\n"); + nvs_set_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,sizeof(homeSpan.hapConfig)); // save data (will default to all zero values, which will then be updated below) nvs_commit(hapNVS); // commit to NVS } + if(homeSpan.updateDatabase(false)) // create Configuration Number and Loop vector + Serial.printf("\nAccessory configuration has changed. Updating configuration number to %d\n",homeSpan.hapConfig.configNumber); + else + Serial.printf("\nAccessory configuration number: %d\n",homeSpan.hapConfig.configNumber); + Serial.print("\n"); - uint8_t tHash[48]; - TempBuffer tBuf(homeSpan.sprintfAttributes(NULL)+1); - homeSpan.sprintfAttributes(tBuf.buf); - 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) - - if(memcmp(tHash,homeSpan.hapConfig.hashCode,48)){ // if hash code of current HAP database does not match stored hash code - memcpy(homeSpan.hapConfig.hashCode,tHash,48); // update stored hash code - homeSpan.hapConfig.configNumber++; // increment configuration number - if(homeSpan.hapConfig.configNumber==65536) // reached max value - homeSpan.hapConfig.configNumber=1; // reset to 1 - - Serial.print("Accessory configuration has changed. Updating configuration number to "); - Serial.print(homeSpan.hapConfig.configNumber); - Serial.print("\n\n"); - nvs_set_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,sizeof(homeSpan.hapConfig)); // update data - nvs_commit(hapNVS); // commit to NVS - } else { - Serial.print("Accessory configuration number: "); - Serial.print(homeSpan.hapConfig.configNumber); - Serial.print("\n\n"); - } - - for(int i=0;iServices.size();j++){ - SpanService *s=homeSpan.Accessories[i]->Services[j]; - if((void(*)())(s->*(&SpanService::loop)) != (void(*)())(&SpanService::loop)) // save pointers to services in Loops vector - homeSpan.Loops.push_back(s); - } - } - } ////////////////////////////////////// @@ -1071,7 +1045,7 @@ int HAPClient::getCharacteristicsURL(char *urlBuf){ numIDs++; char *ids[numIDs]; // reserve space for number of IDs found - int flags=GET_AID; // flags indicating which characteristic fields to include in response (HAP Table 6-13) + int flags=GET_VALUE|GET_AID; // flags indicating which characteristic fields to include in response (HAP Table 6-13) numIDs=0; // reset number of IDs found char *lastSpace=strchr(urlBuf,' '); @@ -1276,7 +1250,7 @@ int HAPClient::getStatusURL(){ String response="HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n"; - response+="HomeSpan Status\n"; + response+="" + String(homeSpan.displayName) + "\n"; response+="\n"; response+="\n"; @@ -1286,6 +1260,10 @@ int HAPClient::getStatusURL(){ response+="Up Time:" + String(uptime) + "\n"; response+="Current Time:" + String(clocktime) + "\n"; response+="Boot Time:" + String(homeSpan.webLog.bootTime) + "\n"; + response+="Reset Reason Code:" + String(esp_reset_reason()) + "\n"; + response+="WiFi Disconnects:" + String(homeSpan.connected/2) + "\n"; + response+="WiFi Signal:" + String(WiFi.RSSI()) + " dBm\n"; + response+="WiFi Gateway:" + WiFi.gatewayIP().toString() + "\n"; response+="ESP32 Board:" + String(ARDUINO_BOARD) + "\n"; response+="Arduino-ESP Version:" + String(ARDUINO_ESP_VERSION) + "\n"; response+="ESP-IDF Version:" + String(ESP_IDF_VERSION_MAJOR) + "." + String(ESP_IDF_VERSION_MINOR) + "." + String(ESP_IDF_VERSION_PATCH) + "\n"; @@ -1338,30 +1316,6 @@ int HAPClient::getStatusURL(){ ////////////////////////////////////// -void HAPClient::callServiceLoops(){ - - homeSpan.snapTime=millis(); // snap the current time for use in ALL loop routines - - for(int i=0;iloop(); // call the loop() method -} - - -////////////////////////////////////// - -void HAPClient::checkPushButtons(){ - - for(int i=0;ipushButton->triggered(sb->singleTime,sb->longTime,sb->doubleTime)){ // if the underlying PushButton is triggered - sb->service->button(sb->pin,sb->pushButton->type()); // call the Service's button() routine with pin and type as parameters - } - } - -} - -////////////////////////////////////// - void HAPClient::checkNotifications(){ if(!homeSpan.Notifications.empty()){ // if there are Notifications to process @@ -1372,7 +1326,7 @@ void HAPClient::checkNotifications(){ ////////////////////////////////////// -void HAPClient::checkTimedWrites(){ +void HAPClient::checkTimedWrites(){ unsigned long cTime=millis(); // get current time diff --git a/src/HAP.h b/src/HAP.h index 707f438..8ed6db1 100644 --- a/src/HAP.h +++ b/src/HAP.h @@ -142,8 +142,6 @@ struct HAPClient { static void removeControllers(); // removes all Controllers (sets allocated flags to false for all slots) 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(); // prints IDs of all allocated (paired) Controller - static void callServiceLoops(); // call the loop() method for any Service with that over-rode the default method - static void checkPushButtons(); // checks for PushButton presses and calls button() method of attached Services when found 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/HomeSpan.cpp b/src/HomeSpan.cpp index a7c67b2..bba3091 100644 --- a/src/HomeSpan.cpp +++ b/src/HomeSpan.cpp @@ -59,7 +59,6 @@ void Span::begin(Category catID, const char *displayName, const char *hostNameBa esp_task_wdt_delete(xTaskGetIdleTaskHandleForCPU(0)); // required to avoid watchdog timeout messages from ESP32-C3 - controlButton.init(controlPin); statusLED.init(statusPin,0,autoOffLED); if(requestedMaxCon=0) - Serial.print(controlPin); + if(getControlPin()>=0) + Serial.print(getControlPin()); else Serial.print("- *** WARNING: Device Control Pin is UNDEFINED"); Serial.print("\nSketch Version: "); @@ -159,42 +158,29 @@ void Span::begin(Category catID, const char *displayName, const char *hostNameBa void Span::poll() { + if(pollTaskHandle){ + Serial.print("\n** FATAL ERROR: Do not call homeSpan.poll() directly if homeSpan.start() is used!\n** PROGRAM HALTED **\n\n"); + vTaskDelete(pollTaskHandle); + while(1); + } + + pollTask(); +} + +/////////////////////////////// + +void Span::pollTask() { + if(!strlen(category)){ - Serial.print("\n** FATAL ERROR: Cannot run homeSpan.poll() without an initial call to homeSpan.begin()!\n** PROGRAM HALTED **\n\n"); + Serial.print("\n** FATAL ERROR: Cannot start homeSpan polling without an initial call to homeSpan.begin()!\n** PROGRAM HALTED **\n\n"); while(1); } if(!isInitialized){ - if(!homeSpan.Accessories.empty()){ - - if(!homeSpan.Accessories.back()->Services.empty()) - homeSpan.Accessories.back()->Services.back()->validate(); - - homeSpan.Accessories.back()->validate(); - } - - checkRanges(); - - if(nWarnings>0){ - configLog+="\n*** CAUTION: There " + String((nWarnings>1?"are ":"is ")) + String(nWarnings) + " WARNING" + (nWarnings>1?"S":"") + " associated with this configuration that may lead to the device becoming non-responsive, or operating in an unexpected manner. ***\n"; - } - processSerialCommand("i"); // print homeSpan configuration info - - if(nFatalErrors>0){ - Serial.print("\n*** PROGRAM HALTED DUE TO "); - Serial.print(nFatalErrors); - Serial.print(" FATAL ERROR"); - if(nFatalErrors>1) - Serial.print("S"); - Serial.print(" IN CONFIGURATION! ***\n\n"); - while(1); - } - - Serial.print("\n"); - - HAPClient::init(); // read NVS and load HAP settings + + HAPClient::init(); // read NVS and load HAP settings if(!strlen(network.wifiData.ssid)){ Serial.print("*** WIFI CREDENTIALS DATA NOT FOUND. "); @@ -209,7 +195,8 @@ void Span::poll() { homeSpan.statusLED.start(LED_WIFI_CONNECTING); } - controlButton.reset(); + if(controlButton) + controlButton->reset(); Serial.print(displayName); Serial.print(" is READY!\n\n"); @@ -221,10 +208,10 @@ void Span::poll() { checkConnect(); } - char cBuf[17]="?"; + char cBuf[65]="?"; if(Serial.available()){ - readSerial(cBuf,16); + readSerial(cBuf,64); processSerialCommand(cBuf); } @@ -289,22 +276,28 @@ void Span::poll() { } // process HAP Client } // for-loop over connection slots - HAPClient::callServiceLoops(); - HAPClient::checkPushButtons(); + snapTime=millis(); // snap the current time for use in ALL loop routines + + for(auto it=Loops.begin();it!=Loops.end();it++) // call loop() for all Services with over-ridden loop() methods + (*it)->loop(); + + for(auto it=PushButtons.begin();it!=PushButtons.end();it++) // check for SpanButton presses + (*it)->check(); + HAPClient::checkNotifications(); HAPClient::checkTimedWrites(); if(spanOTA.enabled) ArduinoOTA.handle(); - if(controlButton.primed()){ + if(controlButton && controlButton->primed()){ statusLED.start(LED_ALERT); } - if(controlButton.triggered(3000,10000)){ + if(controlButton && controlButton->triggered(3000,10000)){ statusLED.off(); - if(controlButton.type()==PushButton::LONG){ - controlButton.wait(); + if(controlButton->type()==PushButton::LONG){ + controlButton->wait(); processSerialCommand("F"); // FACTORY RESET } else { commandMode(); // COMMAND MODE @@ -348,8 +341,8 @@ void Span::commandMode(){ statusLED.start(LED_ALERT); delay(2000); } else - if(controlButton.triggered(10,3000)){ - if(controlButton.type()==PushButton::SINGLE){ + if(controlButton->triggered(10,3000)){ + if(controlButton->type()==PushButton::SINGLE){ mode++; if(mode==6) mode=1; @@ -361,7 +354,7 @@ void Span::commandMode(){ } // while statusLED.start(LED_ALERT); - controlButton.wait(); + controlButton->wait(); switch(mode){ @@ -401,12 +394,12 @@ void Span::commandMode(){ void Span::checkConnect(){ - if(connected){ + if(connected%2){ if(WiFi.status()==WL_CONNECTED) return; - Serial.print("\n\n*** WiFi Connection Lost!\n"); // losing and re-establishing connection has not been tested - connected=false; + addWebLog(true,"*** WiFi Connection Lost!"); // losing and re-establishing connection has not been tested + connected++; waitTime=60000; alarmConnect=0; homeSpan.statusLED.start(LED_WIFI_CONNECTING); @@ -427,11 +420,7 @@ void Span::checkConnect(){ Serial.print(". You may type 'W ' to re-configure WiFi, or 'X ' to erase WiFi credentials. Will try connecting again in 60 seconds.\n\n"); waitTime=60000; } else { - Serial.print("Trying to connect to "); - Serial.print(network.wifiData.ssid); - Serial.print(". Waiting "); - Serial.print(waitTime/1000); - Serial.print(" second(s) for response...\n"); + addWebLog(true,"Trying to connect to %s. Waiting %d sec...",network.wifiData.ssid,waitTime/1000); WiFi.begin(network.wifiData.ssid,network.wifiData.pwd); } @@ -440,14 +429,18 @@ void Span::checkConnect(){ return; } - connected=true; + if(!HAPClient::nAdminControllers()) + statusLED.start(LED_PAIRING_NEEDED); + else + statusLED.on(); + + connected++; - Serial.print("Successfully connected to "); - Serial.print(network.wifiData.ssid); - Serial.print("! IP Address: "); - Serial.print(WiFi.localIP()); - Serial.print("\n"); + addWebLog(true,"WiFi Connected! IP Address = %s\n",WiFi.localIP().toString().c_str()); + if(connected>1) // Do not initialize everything below if this is only a reconnect + 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 @@ -540,7 +533,6 @@ void Span::checkConnect(){ ArduinoOTA.onStart(spanOTA.start).onEnd(spanOTA.end).onProgress(spanOTA.progress).onError(spanOTA.error); ArduinoOTA.begin(); - reserveSocketConnections(1); Serial.print("Starting OTA Server: "); Serial.print(displayName); Serial.print(" at "); @@ -568,12 +560,8 @@ void Span::checkConnect(){ Serial.print("\n"); - if(!HAPClient::nAdminControllers()){ + if(!HAPClient::nAdminControllers()) Serial.print("DEVICE NOT YET PAIRED -- PLEASE PAIR WITH HOMEKIT APP\n\n"); - statusLED.start(LED_PAIRING_NEEDED); - } else { - statusLED.on(); - } if(wifiCallback) wifiCallback(); @@ -912,14 +900,128 @@ void Span::processSerialCommand(const char *c){ } break; + case 'm': { + Serial.printf("Free Memory: %d bytes\n",heap_caps_get_free_size(MALLOC_CAP_DEFAULT)); + } + break; + case 'i':{ Serial.print("\n*** HomeSpan Info ***\n\n"); - Serial.print(configLog); - Serial.print("\nConfigured as Bridge: "); - Serial.print(homeSpan.isBridge?"YES":"NO"); - Serial.print("\n\n"); + int nErrors=0; + int nWarnings=0; + + unordered_set aidValues; + char pNames[][7]={"PR","PW","EV","AA","TW","HD","WR"}; + + for(auto acc=Accessories.begin(); acc!=Accessories.end(); acc++){ + Serial.printf("\u27a4 Accessory: AID=%d\n",(*acc)->aid); + boolean foundInfo=false; + + if(acc==Accessories.begin() && (*acc)->aid!=1) + Serial.printf(" *** ERROR! AID of first Accessory must always be 1 ***\n",nErrors++); + + if(aidValues.find((*acc)->aid)!=aidValues.end()) + Serial.printf(" *** ERROR! AID already in use for another Accessory ***\n",nErrors++); + + aidValues.insert((*acc)->aid); + + for(auto svc=(*acc)->Services.begin(); svc!=(*acc)->Services.end(); svc++){ + Serial.printf(" \u279f Service %s: IID=%d, %sUUIS=\"%s\"",(*svc)->hapName,(*svc)->iid,(*svc)->isCustom?"Custom-":"",(*svc)->type); + Serial.printf("\n"); + + if(!strcmp((*svc)->type,"3E")){ + foundInfo=true; + if((*svc)->iid!=1) + Serial.printf(" *** ERROR! The Accessory Information Service must be defined before any other Services in an Accessory ***\n",nErrors++); + } + else if((*acc)->aid==1) // this is an Accessory with aid=1, but it has more than just AccessoryInfo. So... + isBridge=false; // ...this is not a bridge device + + unordered_set hapChar; + + for(auto chr=(*svc)->Characteristics.begin(); chr!=(*svc)->Characteristics.end(); chr++){ + Serial.printf(" \u21e8 Characteristic %s(%s): IID=%d, %sUUID=\"%s\", %sPerms=", + (*chr)->hapName,(*chr)->uvPrint((*chr)->value).c_str(),(*chr)->iid,(*chr)->isCustom?"Custom-":"",(*chr)->type,(*chr)->perms!=(*chr)->hapChar->perms?"Custom-":""); + + int foundPerms=0; + for(uint8_t i=0;i<7;i++){ + if((*chr)->perms & (1<format!=FORMAT::STRING && (*chr)->format!=FORMAT::BOOL){ + if((*chr)->validValues) + Serial.printf(", Valid Values=%s",(*chr)->validValues); + else if((*chr)->uvGet((*chr)->stepValue)>0) + Serial.printf(", %sRange=[%s,%s,%s]",(*chr)->customRange?"Custom-":"",(*chr)->uvPrint((*chr)->minValue).c_str(),(*chr)->uvPrint((*chr)->maxValue).c_str(),(*chr)->uvPrint((*chr)->stepValue).c_str()); + else + Serial.printf(", %sRange=[%s,%s]",(*chr)->customRange?"Custom-":"",(*chr)->uvPrint((*chr)->minValue).c_str(),(*chr)->uvPrint((*chr)->maxValue).c_str()); + } + + if((*chr)->nvsKey) + Serial.printf(" (nvs)"); + Serial.printf("\n"); + + if(!(*chr)->isCustom && !(*svc)->isCustom && (*svc)->req.find((*chr)->hapChar)==(*svc)->req.end() && (*svc)->opt.find((*chr)->hapChar)==(*svc)->opt.end()) + Serial.printf(" *** WARNING! Service does not support this Characteristic ***\n",nWarnings++); + else + if(invalidUUID((*chr)->type,(*chr)->isCustom)) + Serial.printf(" *** ERROR! Format of UUID is invalid ***\n",nErrors++); + else + if(hapChar.find((*chr)->hapChar)!=hapChar.end()) + Serial.printf(" *** ERROR! Characteristic already defined for this Service ***\n",nErrors++); + + if((*chr)->setRangeError) + Serial.printf(" *** WARNING! Attempt to set Custom Range for this Characteristic ignored ***\n",nWarnings++); + + if((*chr)->setValidValuesError) + Serial.printf(" *** WARNING! Attempt to set Custom Valid Values for this Characteristic ignored ***\n",nWarnings++); + + if((*chr)->format!=STRING && ((*chr)->uvGet((*chr)->value) < (*chr)->uvGet((*chr)->minValue) || (*chr)->uvGet((*chr)->value) > (*chr)->uvGet((*chr)->maxValue))) + Serial.printf(" *** WARNING! Value of %llg is out of range [%llg,%llg] ***\n",(*chr)->uvGet((*chr)->value),(*chr)->uvGet((*chr)->minValue),(*chr)->uvGet((*chr)->maxValue),nWarnings++); + + hapChar.insert((*chr)->hapChar); + + } // Characteristics + + for(auto req=(*svc)->req.begin(); req!=(*svc)->req.end(); req++){ + if(hapChar.find(*req)==hapChar.end()) + Serial.printf(" *** WARNING! Required '%s' Characteristic for this Service not found ***\n",(*req)->hapName,nWarnings++); + } + + for(auto button=PushButtons.begin(); button!=PushButtons.end(); button++){ + if((*button)->service==(*svc)){ + Serial.printf(" \u25bc SpanButton: Pin=%d, Single=%ums, Double=%ums, Long=%ums, Type=",(*button)->pin,(*button)->singleTime,(*button)->doubleTime,(*button)->longTime); + if((*button)->triggerType==PushButton::TRIGGER_ON_LOW) + Serial.printf("TRIGGER_ON_LOW\n"); + else if((*button)->triggerType==PushButton::TRIGGER_ON_HIGH) + Serial.printf("TRIGGER_ON_HIGH\n"); + +#if SOC_TOUCH_SENSOR_NUM > 0 + else if((*button)->triggerType==PushButton::TRIGGER_ON_TOUCH) + Serial.printf("TRIGGER_ON_TOUCH\n"); +#endif + else + Serial.printf("USER-DEFINED\n"); + + if((void(*)(int,int))((*svc)->*(&SpanService::button))==(void(*)(int,int))(&SpanService::button)) + Serial.printf(" *** WARNING! No button() method defined in this Service ***\n",nWarnings++); + } + } + + } // Services + + if(!foundInfo) + Serial.printf(" *** ERROR! Required 'AccessoryInformation' Service not found ***\n",nErrors++); + + } // Accessories + + Serial.printf("\nConfigured as Bridge: %s\n",isBridge?"YES":"NO"); + if(hapConfig.configNumber>0) + Serial.printf("Configuration Number: %d\n",hapConfig.configNumber); + Serial.printf("\nDatabase Validation: Warnings=%d, Errors=%d\n\n",nWarnings,nErrors); char d[]="------------------------------"; Serial.printf("%-30s %8s %10s %s %s %s %s %s\n","Service","UUID","AID","IID","Update","Loop","Button","Linked Services"); @@ -942,7 +1044,7 @@ void Span::processSerialCommand(const char *c){ Serial.print("\n"); } } - Serial.print("\n*** End Info ***\n"); + Serial.print("\n*** End Info ***\n\n"); } break; @@ -952,13 +1054,14 @@ void Span::processSerialCommand(const char *c){ Serial.print(" s - print connection status\n"); Serial.print(" i - print summary information about the HAP Database\n"); Serial.print(" d - print the full HAP Accessory Attributes Database in JSON format\n"); + Serial.print(" m - print free heap memory\n"); Serial.print("\n"); Serial.print(" W - configure WiFi Credentials and restart\n"); Serial.print(" X - delete WiFi Credentials and restart\n"); Serial.print(" S - change the HomeKit Pairing Setup Code to \n"); Serial.print(" Q - change the HomeKit Setup ID for QR Codes to \n"); Serial.print(" O - change the OTA password\n"); - Serial.print(" A - start the HomeSpan Setup Access Point\n"); + Serial.print(" A - start the HomeSpan Setup Access Point\n"); Serial.print("\n"); Serial.print(" V - delete value settings for all saved Characteristics\n"); Serial.print(" U - unpair device by deleting all Controller data\n"); @@ -1017,14 +1120,14 @@ void Span::setWifiCredentials(const char *ssid, const char *pwd){ /////////////////////////////// -int Span::sprintfAttributes(char *cBuf){ +int Span::sprintfAttributes(char *cBuf, int flags){ int nBytes=0; nBytes+=snprintf(cBuf,cBuf?64:0,"{\"accessories\":["); for(int i=0;isprintfAttributes(cBuf?(cBuf+nBytes):NULL); + nBytes+=Accessories[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL,flags); if(i+1aid!=n; it++); + + if(it==homeSpan.Accessories.end()) + return(false); + + delete *it; + return(true); +} + /////////////////////////////// SpanCharacteristic *Span::find(uint32_t aid, int iid){ @@ -1275,7 +1393,7 @@ int Span::sprintfNotify(SpanBuf *pObj, int nObj, char *cBuf, int conNum){ if(notifyFlag) // already printed at least one other characteristic nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,","); // add preceeding comma before printing next characteristic - nChars+=pObj[i].characteristic->sprintfAttributes(cBuf?(cBuf+nChars):NULL,GET_AID+GET_NV); // get JSON attributes for characteristic + nChars+=pObj[i].characteristic->sprintfAttributes(cBuf?(cBuf+nChars):NULL,GET_VALUE|GET_AID|GET_NV); // get JSON attributes for characteristic notifyFlag=true; } // notification requested @@ -1364,33 +1482,42 @@ int Span::sprintfAttributes(char **ids, int numIDs, int flags, char *cBuf){ /////////////////////////////// -void Span::checkRanges(){ +boolean Span::updateDatabase(boolean updateMDNS){ - boolean okay=true; - homeSpan.configLog+="\nRange Check:"; - - for(int i=0;iServices.size();j++){ - for(int k=0;kServices[j]->Characteristics.size();k++){ - SpanCharacteristic *chr=Accessories[i]->Services[j]->Characteristics[k]; + uint8_t tHash[48]; + TempBuffer 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) - if(chr->format!=STRING && (chr->uvGet(chr->value) < chr->uvGet(chr->minValue) || chr->uvGet(chr->value) > chr->uvGet(chr->maxValue))){ - char c[256]; - sprintf(c,"\n \u2718 Characteristic %s with AID=%d, IID=%d: Initial value of %lg is out of range [%llg,%llg]", - chr->hapName,chr->aid,chr->iid,chr->uvGet(chr->value),chr->uvGet(chr->minValue),chr->uvGet(chr->maxValue)); - if(okay) - homeSpan.configLog+="\n"; - homeSpan.configLog+=c; - homeSpan.nWarnings++; - okay=false; - } - } + boolean changed=false; + + if(memcmp(tHash,hapConfig.hashCode,48)){ // if hash code of current HAP database does not match stored hash code + memcpy(hapConfig.hashCode,tHash,48); // update stored hash code + hapConfig.configNumber++; // increment configuration number + if(hapConfig.configNumber==65536) // reached max value + hapConfig.configNumber=1; // reset to 1 + + nvs_set_blob(HAPClient::hapNVS,"HAPHASH",&hapConfig,sizeof(hapConfig)); // update data + nvs_commit(HAPClient::hapNVS); // commit to NVS + changed=true; + + if(updateMDNS){ + char cNum[16]; + sprintf(cNum,"%d",hapConfig.configNumber); + mdns_service_txt_item_set("_hap","_tcp","c#",cNum); } } - if(okay) - homeSpan.configLog+=" No Warnings"; - homeSpan.configLog+="\n\n"; + Loops.clear(); + + for(auto acc=Accessories.begin(); acc!=Accessories.end(); acc++){ // identify all services with over-ridden loop() methods + for(auto svc=(*acc)->Services.begin(); svc!=(*acc)->Services.end(); svc++){ + if((void(*)())((*svc)->*(&SpanService::loop)) != (void(*)())(&SpanService::loop)) // save pointers to services in Loops vector + homeSpan.Loops.push_back((*svc)); + } + } + + return(changed); } /////////////////////////////// @@ -1409,11 +1536,7 @@ SpanAccessory::SpanAccessory(uint32_t aid){ } this->aid=homeSpan.Accessories.back()->aid+1; - - if(!homeSpan.Accessories.back()->Services.empty()) - homeSpan.Accessories.back()->Services.back()->validate(); - - homeSpan.Accessories.back()->validate(); + } else { this->aid=1; } @@ -1424,55 +1547,31 @@ SpanAccessory::SpanAccessory(uint32_t aid){ this->aid=aid; } - homeSpan.configLog+="\u27a4 Accessory: AID=" + String(this->aid); - - for(int i=0;iaid==homeSpan.Accessories[i]->aid){ - homeSpan.configLog+=" *** ERROR! ID already in use for another Accessory. ***"; - homeSpan.nFatalErrors++; - break; - } - } - - if(homeSpan.Accessories.size()==1 && this->aid!=1){ - homeSpan.configLog+=" *** ERROR! ID of first Accessory must always be 1. ***"; - homeSpan.nFatalErrors++; - } - - homeSpan.configLog+="\n"; - } /////////////////////////////// -void SpanAccessory::validate(){ +SpanAccessory::~SpanAccessory(){ - boolean foundInfo=false; + while(Services.rbegin()!=Services.rend()) // delete all Services in this Accessory + delete *Services.rbegin(); - for(int i=0;itype,"3E")) - foundInfo=true; - else if(aid==1) // this is an Accessory with aid=1, but it has more than just AccessoryInfo. So... - homeSpan.isBridge=false; // ...this is not a bridge device - } - - if(!foundInfo){ - homeSpan.configLog+=" \u2718 Service AccessoryInformation"; - homeSpan.configLog+=" *** ERROR! Required Service for this Accessory not found. ***\n"; - homeSpan.nFatalErrors++; - } - + auto acc=homeSpan.Accessories.begin(); // find Accessory in homeSpan vector and erase entry + while((*acc)!=this) + acc++; + homeSpan.Accessories.erase(acc); + LOG1("Deleted Accessory AID=%d\n",aid); } /////////////////////////////// -int SpanAccessory::sprintfAttributes(char *cBuf){ +int SpanAccessory::sprintfAttributes(char *cBuf, int flags){ int nBytes=0; nBytes+=snprintf(cBuf,cBuf?64:0,"{\"aid\":%u,\"services\":[",aid); for(int i=0;isprintfAttributes(cBuf?(cBuf+nBytes):NULL); + nBytes+=Services[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL,flags); if(i+1Services.empty()) // this is not the first Service to be defined for this Accessory - homeSpan.Accessories.back()->Services.back()->validate(); + if(homeSpan.Accessories.empty()){ + Serial.printf("\nFATAL ERROR! Can't create new Service '%s' without a defined Accessory ***\n",hapName); + Serial.printf("\n=== PROGRAM HALTED ==="); + while(1); + } this->type=type; this->hapName=hapName; this->isCustom=isCustom; - homeSpan.configLog+=" \u279f Service " + String(hapName); - - if(homeSpan.Accessories.empty()){ - homeSpan.configLog+=" *** ERROR! Can't create new Service without a defined Accessory! ***\n"; - homeSpan.nFatalErrors++; - return; - } - homeSpan.Accessories.back()->Services.push_back(this); - iid=++(homeSpan.Accessories.back()->iidCount); + accessory=homeSpan.Accessories.back(); + iid=++(homeSpan.Accessories.back()->iidCount); +} - homeSpan.configLog+=": IID=" + String(iid) + ", " + (isCustom?"Custom-":"") + "UUID=\"" + String(type) + "\""; +/////////////////////////////// - if(Span::invalidUUID(type,isCustom)){ - homeSpan.configLog+=" *** ERROR! Format of UUID is invalid. ***"; - homeSpan.nFatalErrors++; +SpanService::~SpanService(){ + + while(Characteristics.rbegin()!=Characteristics.rend()) // delete all Characteristics in this Service + delete *Characteristics.rbegin(); + + auto svc=accessory->Services.begin(); // find Service in containing Accessory vector and erase entry + while((*svc)!=this) + svc++; + accessory->Services.erase(svc); + + for(svc=homeSpan.Loops.begin(); svc!=homeSpan.Loops.end() && (*svc)!=this; svc++); // search for entry in Loop vector... + if(svc!=homeSpan.Loops.end()){ // ...if it exists, erase it + homeSpan.Loops.erase(svc); + LOG1("Deleted Loop Entry\n"); } - if(!strcmp(this->type,"3E") && iid!=1){ - homeSpan.configLog+=" *** ERROR! The AccessoryInformation Service must be defined before any other Services in an Accessory. ***"; - homeSpan.nFatalErrors++; + auto pb=homeSpan.PushButtons.begin(); // loop through PushButton vector and delete ALL PushButtons associated with this Service + while(pb!=homeSpan.PushButtons.end()){ + if((*pb)->service==this){ + pb=homeSpan.PushButtons.erase(pb); + LOG1("Deleted PushButton on Pin=%d\n",(*pb)->pin); + } + else { + pb++; + } } - - homeSpan.configLog+="\n"; - + + LOG1("Deleted Service AID=%d IID=%d\n",accessory->aid,iid); } /////////////////////////////// @@ -1545,7 +1657,7 @@ SpanService *SpanService::addLink(SpanService *svc){ /////////////////////////////// -int SpanService::sprintfAttributes(char *cBuf){ +int SpanService::sprintfAttributes(char *cBuf, int flags){ int nBytes=0; nBytes+=snprintf(cBuf,cBuf?64:0,"{\"iid\":%d,\"type\":\"%s\",",iid,type); @@ -1569,7 +1681,7 @@ int SpanService::sprintfAttributes(char *cBuf){ nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"characteristics\":["); for(int i=0;isprintfAttributes(cBuf?(cBuf+nBytes):NULL,GET_META|GET_PERMS|GET_TYPE|GET_DESC); + nBytes+=Characteristics[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL,flags); if(i+1type,Characteristics[j]->type); - - if(!valid){ - homeSpan.configLog+=" \u2718 Characteristic " + String(req[i]->hapName); - homeSpan.configLog+=" *** WARNING! Required Characteristic for this Service not found. ***\n"; - homeSpan.nWarnings++; - } - } - - vector().swap(opt); - vector().swap(req); -} - /////////////////////////////// // SpanCharacteristic // /////////////////////////////// SpanCharacteristic::SpanCharacteristic(HapChar *hapChar, boolean isCustom){ + + if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){ + Serial.printf("\nFATAL ERROR! Can't create new Characteristic '%s' without a defined Service ***\n",hapName); + Serial.printf("\n=== PROGRAM HALTED ==="); + while(1); + } + type=hapChar->type; perms=hapChar->perms; hapName=hapChar->hapName; format=hapChar->format; staticRange=hapChar->staticRange; this->isCustom=isCustom; + this->hapChar=hapChar; - homeSpan.configLog+=" \u21e8 Characteristic " + String(hapName); - - if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){ - homeSpan.configLog+=" *** ERROR! Can't create new Characteristic without a defined Service! ***\n"; - homeSpan.nFatalErrors++; - return; - } - + homeSpan.Accessories.back()->Services.back()->Characteristics.push_back(this); iid=++(homeSpan.Accessories.back()->iidCount); service=homeSpan.Accessories.back()->Services.back(); aid=homeSpan.Accessories.back()->aid; @@ -1628,6 +1721,29 @@ SpanCharacteristic::SpanCharacteristic(HapChar *hapChar, boolean isCustom){ /////////////////////////////// +SpanCharacteristic::~SpanCharacteristic(){ + + auto chr=service->Characteristics.begin(); // find Characteristic in containing Service vector and erase entry + while((*chr)!=this) + chr++; + service->Characteristics.erase(chr); + + free(ev); + free(desc); + free(unit); + free(validValues); + free(nvsKey); + + if(format==FORMAT::STRING){ + free(value.STRING); + free(newValue.STRING); + } + + LOG1("Deleted Characteristic AID=%d IID=%d\n",aid,iid); +} + +/////////////////////////////// + int SpanCharacteristic::sprintfAttributes(char *cBuf, int flags){ int nBytes=0; @@ -1640,7 +1756,7 @@ int SpanCharacteristic::sprintfAttributes(char *cBuf, int flags){ if(flags&GET_TYPE) nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"type\":\"%s\"",type); - if(perms&PR){ + if((perms&PR) && (flags&GET_VALUE)){ if(perms&NV && !(flags&GET_NV)) nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":null"); else @@ -1814,35 +1930,26 @@ unsigned long SpanCharacteristic::timeVal(){ /////////////////////////////// SpanCharacteristic *SpanCharacteristic::setValidValues(int n, ...){ - char c[256]; - String *s = new String("["); + + if(format!=UINT8){ + setValidValuesError=true; + return(this); + } + + String s="["; va_list vl; va_start(vl,n); for(int i=0;ic_str(); - sprintf(c,": ValidValues=%s\n",validValues); - } - - homeSpan.configLog+=c; return(this); } @@ -1853,8 +1960,9 @@ SpanCharacteristic *SpanCharacteristic::setValidValues(int n, ...){ SpanRange::SpanRange(int min, int max, int step){ if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty() || homeSpan.Accessories.back()->Services.back()->Characteristics.empty() ){ - homeSpan.configLog+=" \u2718 SpanRange: *** ERROR! Can't create new Range without a defined Characteristic! ***\n"; - homeSpan.nFatalErrors++; + Serial.printf("\nFATAL ERROR! Can't create new SpanRange(%d,%d,%d) without a defined Characteristic ***\n",min,max,step); + Serial.printf("\n=== PROGRAM HALTED ==="); + while(1); } else { homeSpan.Accessories.back()->Services.back()->Characteristics.back()->setRange(min,max,step); } @@ -1864,37 +1972,29 @@ SpanRange::SpanRange(int min, int max, int step){ // SpanButton // /////////////////////////////// -SpanButton::SpanButton(int pin, uint16_t longTime, uint16_t singleTime, uint16_t doubleTime){ - - homeSpan.configLog+=" \u25bc SpanButton: Pin=" + String(pin) + ", Single=" + String(singleTime) + "ms, Double=" + String(doubleTime) + "ms, Long=" + String(longTime) + "ms"; +SpanButton::SpanButton(int pin, uint16_t longTime, uint16_t singleTime, uint16_t doubleTime, triggerType_t triggerType) : PushButton(pin, triggerType){ if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){ - homeSpan.configLog+=" *** ERROR! Can't create new PushButton without a defined Service! ***\n"; - homeSpan.nFatalErrors++; - return; + Serial.printf("\nFATAL ERROR! Can't create new SpanButton(%d,%u,%u,%u) without a defined Service ***\n",pin,longTime,singleTime,doubleTime); + Serial.printf("\n=== PROGRAM HALTED ==="); + while(1); } - Serial.print("Configuring PushButton: Pin="); // initialization message - Serial.print(pin); - Serial.print("\n"); - - this->pin=pin; this->longTime=longTime; this->singleTime=singleTime; this->doubleTime=doubleTime; service=homeSpan.Accessories.back()->Services.back(); - if((void(*)(int,int))(service->*(&SpanService::button))==(void(*)(int,int))(&SpanService::button)){ - homeSpan.configLog+=" *** WARNING: No button() method defined for this PushButton! ***"; - homeSpan.nWarnings++; - } - - pushButton=new PushButton(pin); // create underlying PushButton - - homeSpan.configLog+="\n"; homeSpan.PushButtons.push_back(this); } +/////////////////////////////// + +void SpanButton::check(){ + + if(triggered(singleTime,longTime,doubleTime)) // if the underlying PushButton is triggered + service->button(pin,type()); // call the Service's button() routine with pin and type as parameters +} /////////////////////////////// // SpanUserCommand // @@ -1928,6 +2028,8 @@ void SpanWebLog::init(uint16_t maxEntries, const char *serv, const char *tz, con timeZone=tz; statusURL="GET /" + String(url) + " "; log = (log_t *)calloc(maxEntries,sizeof(log_t)); + if(timeServer) + homeSpan.reserveSocketConnections(1); } /////////////////////////////// @@ -1942,7 +2044,6 @@ void SpanWebLog::initTime(){ if(getLocalTime(&timeinfo,waitTime)){ strftime(bootTime,sizeof(bootTime),"%c",&timeinfo); Serial.printf("%s\n\n",bootTime); - homeSpan.reserveSocketConnections(1); timeInit=true; } else { Serial.printf("Can't access Time Server - time-keeping not initialized!\n\n"); @@ -1951,29 +2052,33 @@ void SpanWebLog::initTime(){ /////////////////////////////// -void SpanWebLog::addLog(const char *fmt, ...){ - if(maxEntries==0) - return; +void SpanWebLog::vLog(boolean sysMsg, const char *fmt, va_list ap){ - int index=nEntries%maxEntries; + char *buf; + vasprintf(&buf,fmt,ap); - log[index].upTime=esp_timer_get_time(); - if(timeInit) - getLocalTime(&log[index].clockTime,10); - else - log[index].clockTime.tm_year=0; + if(sysMsg) + Serial.printf("%s\n",buf); + else if(homeSpan.logLevel>0) + Serial.printf("WEBLOG: %s\n",buf); + + if(maxEntries>0){ + int index=nEntries%maxEntries; + + log[index].upTime=esp_timer_get_time(); + if(timeInit) + getLocalTime(&log[index].clockTime,10); + else + log[index].clockTime.tm_year=0; + + log[index].message=(char *)realloc(log[index].message, strlen(buf) + 1); + strcpy(log[index].message, buf); + + log[index].clientIP=homeSpan.lastClientIP; + nEntries++; + } - free(log[index].message); - va_list ap; - va_start(ap,fmt); - vasprintf(&log[index].message,fmt,ap); - va_end(ap); - - log[index].clientIP=homeSpan.lastClientIP; - nEntries++; - - if(homeSpan.logLevel>0) - Serial.printf("WEBLOG: %s\n",log[index].message); + free(buf); } /////////////////////////////// @@ -1984,6 +2089,7 @@ void SpanOTA::init(boolean _auth, boolean _safeLoad){ enabled=true; safeLoad=_safeLoad; auth=_auth; + homeSpan.reserveSocketConnections(1); } /////////////////////////////// @@ -2036,6 +2142,11 @@ void SpanOTA::error(ota_error_t err){ /////////////////////////////// +void __attribute__((weak)) loop(){ +} + +/////////////////////////////// + int SpanOTA::otaPercent; boolean SpanOTA::safeLoad; boolean SpanOTA::enabled=false; diff --git a/src/HomeSpan.h b/src/HomeSpan.h index c970652..3bac4d5 100644 --- a/src/HomeSpan.h +++ b/src/HomeSpan.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -48,6 +49,7 @@ using std::vector; using std::unordered_map; +using std::unordered_set; enum { GET_AID=1, @@ -57,7 +59,7 @@ enum { GET_EV=16, GET_DESC=32, GET_NV=64, - GET_ALL=255 + GET_VALUE=128 }; /////////////////////////////// @@ -75,7 +77,11 @@ struct SpanUserCommand; extern Span homeSpan; -/////////////////////////////// +#include "HAP.h" + +//////////////////////////////////////////////////////// +// INTERNAL HOMESPAN STRUCTURES - NOT FOR USER ACCESS // +//////////////////////////////////////////////////////// struct SpanPartition{ char magicCookie[32]; @@ -104,7 +110,7 @@ struct SpanBuf{ // temporary storage buffer for us struct SpanWebLog{ // optional web status/log data boolean isEnabled=false; // flag to inidicate WebLog has been enabled - uint16_t maxEntries; // max number of log entries; + uint16_t maxEntries=0; // max number of log entries; int nEntries=0; // total cumulative number of log entries const char *timeServer; // optional time server to use for acquiring clock time const char *timeZone; // optional time-zone specification @@ -122,7 +128,7 @@ struct SpanWebLog{ // optional web status/log data void init(uint16_t maxEntries, const char *serv, const char *tz, const char *url); void initTime(); - void addLog(const char *fmr, ...); + void vLog(boolean sysMsg, const char *fmr, va_list ap); }; /////////////////////////////// @@ -143,10 +149,23 @@ struct SpanOTA{ // manages OTA process static void error(ota_error_t err); }; -/////////////////////////////// +////////////////////////////////////// +// USER API CLASSES BEGINS HERE // +////////////////////////////////////// -struct Span{ +class Span{ + friend class SpanAccessory; + friend class SpanService; + friend class SpanCharacteristic; + friend class SpanUserCommand; + friend class SpanButton; + friend class SpanRange; + friend class SpanWebLog; + friend class SpanOTA; + friend class Network; + friend class HAPClient; + 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 @@ -155,9 +174,6 @@ struct Span{ 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() boolean isInitialized=false; // flag indicating HomeSpan has been initialized - int nFatalErrors=0; // number of fatal errors in user-defined configuration - int nWarnings=0; // number of warnings errors in user-defined configuration - String configLog; // log of configuration process, including any errors boolean isBridge=true; // flag indicating whether device is configured as a bridge (i.e. first Accessory contains nothing but AccessoryInformation and HAPProtocolInformation) HapQR qrCode; // optional QR Code to use for pairing const char *sketchVersion="n/a"; // version of the sketch @@ -168,14 +184,13 @@ struct Span{ String lastClientIP="0.0.0.0"; // IP address of last client accessing device through encrypted channel boolean newCode; // flag indicating new application code has been loaded (based on keeping track of app SHA256) - boolean connected=false; // WiFi connection status + int connected=0; // WiFi connection status (increments upon each connect and disconnect) unsigned long waitTime=60000; // time to wait (in milliseconds) between WiFi connection attempts unsigned long alarmConnect=0; // time after which WiFi connection attempt should be tried again const char *defaultSetupCode=DEFAULT_SETUP_CODE; // Setup Code used for pairing int statusPin=DEFAULT_STATUS_PIN; // pin for Status LED uint16_t autoOffLED=0; // automatic turn-off duration (in seconds) for Status LED - int controlPin=DEFAULT_CONTROL_PIN; // pin for Control Pushbutton uint8_t logLevel=DEFAULT_LOG_LEVEL; // level for writing out log messages to serial monitor uint8_t maxConnections=CONFIG_LWIP_MAX_SOCKETS-2; // maximum number of allowed simultaneous HAP connections uint8_t requestedMaxCon=CONFIG_LWIP_MAX_SOCKETS-2; // requested maximum number of simultaneous HAP connections @@ -189,9 +204,10 @@ struct Span{ WiFiServer *hapServer; // pointer to the HAP Server connection Blinker statusLED; // indicates HomeSpan status - PushButton controlButton; // controls HomeSpan configuration and resets + PushButton *controlButton = NULL; // controls HomeSpan configuration and resets Network network; // configures WiFi and Setup Code via either serial monitor or temporary Access Point SpanWebLog webLog; // optional web status/log + TaskHandle_t pollTaskHandle = NULL; // optional task handle to use for poll() function SpanOTA spanOTA; // manages OTA process SpanConfig hapConfig; // track configuration changes to the HAP Accessory database; used to increment the configuration number (c#) when changes found @@ -203,39 +219,52 @@ struct Span{ unordered_map UserCommands; // map of pointers to all UserCommands + void pollTask(); // poll HAP Clients and process any new HAP requests + int getFreeSlot(); // returns free HAPClient slot number. HAPClients slot keep track of each active HAPClient connection + void checkConnect(); // check WiFi connection; connect if needed + void commandMode(); // allows user to control and reset HomeSpan settings with the control button + + int sprintfAttributes(char *cBuf, int flags=GET_VALUE|GET_META|GET_PERMS|GET_TYPE|GET_DESC); // prints Attributes JSON database into buf, unless buf=NULL; return number of characters printed, excluding null terminator + + void prettyPrint(char *buf, int nsp=2); // print arbitrary JSON from buf to serial monitor, formatted with indentions of 'nsp' spaces + SpanCharacteristic *find(uint32_t aid, int iid); // return Characteristic with matching aid and iid (else NULL if not found) + int countCharacteristics(char *buf); // return number of characteristic objects referenced in PUT /characteristics JSON request + int updateCharacteristics(char *buf, SpanBuf *pObj); // parses PUT /characteristics JSON request 'buf into 'pObj' and updates referenced characteristics; returns 1 on success, 0 on fail + int sprintfAttributes(SpanBuf *pObj, int nObj, char *cBuf); // prints SpanBuf object into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL + int sprintfAttributes(char **ids, int numIDs, int flags, char *cBuf); // prints accessory.characteristic ids into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL + void clearNotify(int slotNum); // set ev notification flags for connection 'slotNum' to false across all characteristics + int sprintfNotify(SpanBuf *pObj, int nObj, char *cBuf, int conNum); // prints notification JSON into buf based on SpanBuf objects and specified connection number + + static boolean invalidUUID(const char *uuid, boolean isCustom){ + int x=0; + sscanf(uuid,"%*8[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*12[0-9a-fA-F]%n",&x); + return(isCustom && (strlen(uuid)!=36 || x!=36)); + } + + public: + void begin(Category catID=DEFAULT_CATEGORY, const char *displayName=DEFAULT_DISPLAY_NAME, const char *hostNameBase=DEFAULT_HOST_NAME, const char *modelName=DEFAULT_MODEL_NAME); - void poll(); // poll HAP Clients and process any new HAP requests - int getFreeSlot(); // returns free HAPClient slot number. HAPClients slot keep track of each active HAPClient connection - void checkConnect(); // check WiFi connection; connect if needed - void commandMode(); // allows user to control and reset HomeSpan settings with the control button + void poll(); // calls pollTask() with some error checking void processSerialCommand(const char *c); // process command 'c' (typically from readSerial, though can be called with any 'c') - void checkRanges(); // checks values of all Characteristics to ensure they are each within range - - int sprintfAttributes(char *cBuf); // prints Attributes JSON database into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL - void prettyPrint(char *buf, int nsp=2); // print arbitrary JSON from buf to serial monitor, formatted with indentions of 'nsp' spaces - SpanCharacteristic *find(uint32_t aid, int iid); // return Characteristic with matching aid and iid (else NULL if not found) - int countCharacteristics(char *buf); // return number of characteristic objects referenced in PUT /characteristics JSON request - int updateCharacteristics(char *buf, SpanBuf *pObj); // parses PUT /characteristics JSON request 'buf into 'pObj' and updates referenced characteristics; returns 1 on success, 0 on fail - int sprintfAttributes(SpanBuf *pObj, int nObj, char *cBuf); // prints SpanBuf object into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL - int sprintfAttributes(char **ids, int numIDs, int flags, char *cBuf); // prints accessory.characteristic ids into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL + 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 clearNotify(int slotNum); // set ev notification flags for connection 'slotNum' to false across all characteristics - int sprintfNotify(SpanBuf *pObj, int nObj, char *cBuf, int conNum); // prints notification JSON into buf based on SpanBuf objects and specified connection number - - void setControlPin(uint8_t pin){controlPin=pin;} // sets Control Pin + void setControlPin(uint8_t pin){controlButton=new PushButton(pin);} // sets Control Pin void setStatusPin(uint8_t pin){statusPin=pin;} // sets Status Pin void setStatusAutoOff(uint16_t duration){autoOffLED=duration;} // sets Status LED auto off (seconds) int getStatusPin(){return(statusPin);} // get Status Pin + int getControlPin(){return(controlButton?controlButton->getPin():-1);} // get Control Pin (returns -1 if undefined) 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(uint8_t level){logLevel=level;} // sets Log Level for log messages (0=baseline, 1=intermediate, 2=all) + int getLogLevel(){return(logLevel);} // get Log Level 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 @@ -257,36 +286,54 @@ struct Span{ webLog.init(maxEntries, serv, tz, url); } + void addWebLog(boolean sysMsg, const char *fmt, ...){ // add Web Log entry + va_list ap; + va_start(ap,fmt); + webLog.vLog(sysMsg,fmt,ap); + va_end(ap); + } + + void autoPoll(uint32_t stackSize=CONFIG_ARDUINO_LOOP_STACK_SIZE){xTaskCreateUniversal([](void *parms){for(;;)homeSpan.pollTask();}, "pollTask", stackSize, NULL, 1, &pollTaskHandle, 0);} // start pollTask() + void setTimeServerTimeout(uint32_t tSec){webLog.waitTime=tSec*1000;} // 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 - - static boolean invalidUUID(const char *uuid, boolean isCustom){ - int x=0; - sscanf(uuid,"%*8[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*12[0-9a-fA-F]%n",&x); - return(isCustom && (strlen(uuid)!=36 || x!=36)); - } - }; /////////////////////////////// -struct SpanAccessory{ +class SpanAccessory{ + + friend class Span; + friend class SpanService; + friend class SpanCharacteristic; + friend class SpanButton; + friend class SpanRange; - uint32_t aid=0; // Accessory Instance ID (HAP Table 6-1) - int iidCount=0; // running count of iid to use for Services and Characteristics associated with this Accessory - vector Services; // vector of pointers to all Services in this Accessory + uint32_t aid=0; // Accessory Instance ID (HAP Table 6-1) + int iidCount=0; // running count of iid to use for Services and Characteristics associated with this Accessory + vector Services; // vector of pointers to all Services in this Accessory - SpanAccessory(uint32_t aid=0); + int sprintfAttributes(char *cBuf, int flags); // prints Accessory JSON database into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL - int sprintfAttributes(char *cBuf); // prints Accessory JSON database into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL - void validate(); // error-checks Accessory + protected: + + ~SpanAccessory(); // destructor + + public: + + SpanAccessory(uint32_t aid=0); // constructor }; /////////////////////////////// -struct SpanService{ +class SpanService{ + + friend class Span; + friend class SpanAccessory; + friend class SpanCharacteristic; + friend class SpanRange; int iid=0; // Instance ID (HAP Table 6-2) const char *type; // Service Type @@ -294,29 +341,37 @@ struct SpanService{ boolean hidden=false; // optional property indicating service is hidden boolean primary=false; // optional property indicating service is primary vector Characteristics; // vector of pointers to all Characteristics in this Service - vector req; // vector of pointers to all required HAP Characteristic Types for this Service - vector opt; // vector of pointers to all optional HAP Characteristic Types for this Service vector linkedServices; // vector of pointers to any optional linked Services boolean isCustom; // flag to indicate this is a Custom Service + SpanAccessory *accessory=NULL; // pointer to Accessory containing this Service + + int sprintfAttributes(char *cBuf, int flags); // prints Service JSON records into buf; return number of characters printed, excluding null terminator + + protected: + + ~SpanService(); // destructor + unordered_set req; // unordered set of pointers to all required HAP Characteristic Types for this Service + unordered_set opt; // unordered set of pointers to all optional HAP Characteristic Types for this Service + + public: SpanService(const char *type, const char *hapName, boolean isCustom=false); // constructor + SpanService *setPrimary(); // sets the Service Type to be primary and returns pointer to self + SpanService *setHidden(); // sets the Service Type to be hidden and returns pointer to self + SpanService *addLink(SpanService *svc); // adds svc as a Linked Service and returns pointer to self + vector getLinks(){return(linkedServices);} // returns linkedServices vector for use as range in "for-each" loops - SpanService *setPrimary(); // sets the Service Type to be primary and returns pointer to self - SpanService *setHidden(); // sets the Service Type to be hidden and returns pointer to self - SpanService *addLink(SpanService *svc); // adds svc as a Linked Service and returns pointer to self - vector getLinks(){return(linkedServices);} // returns linkedServices vector for use as range in "for-each" loops - - int sprintfAttributes(char *cBuf); // prints Service JSON records into buf; return number of characters printed, excluding null terminator - void validate(); // error-checks Service - virtual boolean update() {return(true);} // placeholder for code that is called when a Service is updated via a Controller. Must return true/false depending on success of update - virtual void loop(){} // loops for each Service - called every cycle and can be over-ridden with user-defined code + virtual void loop(){} // loops for each Service - called every cycle if over-ridden with user-defined code virtual void button(int pin, int pressType){} // method called for a Service when a button attached to "pin" has a Single, Double, or Long Press, according to pressType }; /////////////////////////////// -struct SpanCharacteristic{ +class SpanCharacteristic{ + + friend class Span; + friend class SpanService; union UVal { BOOL_t BOOL; @@ -330,6 +385,7 @@ struct SpanCharacteristic{ }; int iid=0; // Instance ID (HAP Table 6-3) + HapChar *hapChar; // pointer to HAP Characteristic structure const char *type; // Characteristic Type const char *hapName; // HAP Name UVal value; // Characteristic Value @@ -342,26 +398,21 @@ struct SpanCharacteristic{ UVal stepValue; // Characteristic step size (not applicable for STRING) boolean staticRange; // Flag that indicates whether Range is static and cannot be changed with setRange() boolean customRange=false; // Flag for custom ranges - const char *validValues=NULL; // Optional JSON array of valid values. Applicable only to uint8 Characteristics + char *validValues=NULL; // Optional JSON array of valid values. Applicable only to uint8 Characteristics boolean *ev; // Characteristic Event Notify Enable (per-connection) char *nvsKey=NULL; // key for NVS storage of Characteristic value boolean isCustom; // flag to indicate this is a Custom Characteristic + boolean setRangeError=false; // flag to indicate attempt to set Range on Characteristic that does not support changes to Range + boolean setValidValuesError=false; // flag to indicate attempt to set Valid Values on Characteristic that does not support changes to Valid Values uint32_t aid=0; // Accessory ID - passed through from Service containing this Characteristic boolean isUpdated=false; // set to true when new value has been requested by PUT /characteristic unsigned long updateTime=0; // last time value was updated (in millis) either by PUT /characteristic OR by setVal() UVal newValue; // the updated value requested by PUT /characteristic SpanService *service=NULL; // pointer to Service containing this Characteristic - - SpanCharacteristic(HapChar *hapChar, boolean isCustom=false); // contructor - + int sprintfAttributes(char *cBuf, int flags); // prints Characteristic JSON records into buf, according to flags mask; return number of characters printed, excluding null terminator - StatusCode loadUpdate(char *val, char *ev); // load updated val/ev from PUT /characteristic JSON request. Return intiial HAP status code (checks to see if characteristic is found, is writable, etc.) - - boolean updated(){return(isUpdated);} // returns isUpdated - unsigned long timeVal(); // returns time elapsed (in millis) since value was last updated - - SpanCharacteristic *setValidValues(int n, ...); // sets a list of 'n' valid values allowed for a Characteristic and returns pointer to self. Only applicable if format=uint8 + StatusCode loadUpdate(char *val, char *ev); // load updated val/ev from PUT /characteristic JSON request. Return intitial HAP status code (checks to see if characteristic is found, is writable, etc.) String uvPrint(UVal &u){ char c[64]; @@ -448,35 +499,9 @@ struct SpanCharacteristic{ return(0); // included to prevent compiler warnings } - template SpanCharacteristic *setRange(A min, B max, S step=0){ + protected: - char c[256]; - homeSpan.configLog+=String(" \u2b0c Set Range for ") + String(hapName) + " with AID=" + String(aid) + ", IID=" + String(iid); - - if(customRange){ - sprintf(c," *** ERROR! Range already set for this Characteristic! ***\n"); - homeSpan.nFatalErrors++; - } else - - if(staticRange){ - sprintf(c," *** ERROR! Can't change range for this Characteristic! ***\n"); - homeSpan.nFatalErrors++; - } else { - - uvSet(minValue,min); - uvSet(maxValue,max); - uvSet(stepValue,step); - customRange=true; - - if(uvGet(stepValue)>0) - sprintf(c,": Min=%s, Max=%s, Step=%s\n",uvPrint(minValue),uvPrint(maxValue),uvPrint(stepValue)); - else - sprintf(c,": Min=%s, Max=%s\n",uvPrint(minValue),uvPrint(maxValue)); - } - homeSpan.configLog+=c; - return(this); - - } // setRange() + ~SpanCharacteristic(); // destructor template void init(T val, boolean nvsStore, A min=0, B max=1){ @@ -522,50 +547,12 @@ struct SpanCharacteristic{ uvSet(maxValue,max); uvSet(stepValue,0); } - - homeSpan.configLog+="(" + uvPrint(value) + ")" + ": IID=" + String(iid) + ", " + (isCustom?"Custom-":"") + "UUID=\"" + String(type) + "\""; - if(format!=FORMAT::STRING && format!=FORMAT::BOOL) - homeSpan.configLog+= ", Range=[" + String(uvPrint(minValue)) + "," + String(uvPrint(maxValue)) + "]"; - - if(nvsFlag==2) - homeSpan.configLog+=" (restored)"; - else if(nvsFlag==1) - homeSpan.configLog+=" (storing)"; - - if(Span::invalidUUID(type,isCustom)){ - homeSpan.configLog+=" *** ERROR! Format of UUID is invalid. ***"; - homeSpan.nFatalErrors++; - } - - boolean valid=isCustom|service->isCustom; // automatically set valid if either Characteristic or containing Service is Custom - - for(int i=0; !valid && iServices.back()->req.size(); i++) - valid=!strcmp(type,homeSpan.Accessories.back()->Services.back()->req[i]->type); - - for(int i=0; !valid && iServices.back()->opt.size(); i++) - valid=!strcmp(type,homeSpan.Accessories.back()->Services.back()->opt[i]->type); - - if(!valid){ - homeSpan.configLog+=" *** WARNING! Service does not support this Characteristic. ***"; - homeSpan.nWarnings++; - } - - boolean repeated=false; - - for(int i=0; !repeated && iServices.back()->Characteristics.size(); i++) - repeated=!strcmp(type,homeSpan.Accessories.back()->Services.back()->Characteristics[i]->type); - - if(valid && repeated){ - homeSpan.configLog+=" *** ERROR! Characteristic already defined for this Service. ***"; - homeSpan.nFatalErrors++; - } - - homeSpan.Accessories.back()->Services.back()->Characteristics.push_back(this); - - homeSpan.configLog+="\n"; - + } // init() + public: + + SpanCharacteristic(HapChar *hapChar, boolean isCustom=false); // constructor template T getVal(){ return(uvGet(value)); @@ -592,7 +579,7 @@ struct SpanCharacteristic{ void setString(const char *val){ if((perms & EV) == 0){ - Serial.printf("\n*** WARNING: Attempt to update Characteristic::%s with setVal() ignored. No NOTIFICATION permission on this characteristic\n\n",hapName); + Serial.printf("\n*** WARNING: Attempt to update Characteristic::%s with setString() ignored. No NOTIFICATION permission on this characteristic\n\n",hapName); return; } @@ -648,26 +635,29 @@ struct SpanCharacteristic{ } // setVal() + boolean updated(){return(isUpdated);} // returns isUpdated + unsigned long timeVal(); // returns time elapsed (in millis) since value was last updated + + SpanCharacteristic *setValidValues(int n, ...); // sets a list of 'n' valid values allowed for a Characteristic and returns pointer to self. Only applicable if format=uint8 + + template SpanCharacteristic *setRange(A min, B max, S step=0){ + + if(!staticRange){ + uvSet(minValue,min); + uvSet(maxValue,max); + uvSet(stepValue,step); + customRange=true; + } else + setRangeError=true; + + return(this); + + } // setRange() + SpanCharacteristic *setPerms(uint8_t perms){ - this->perms=perms; - homeSpan.configLog+=String(" \u2b0c Change Permissions for ") + String(hapName) + " with AID=" + String(aid) + ", IID=" + String(iid) + ":"; - - char pNames[][7]={"PR","PW","EV","AA","TW","HD","WR"}; - char sep=' '; - - for(uint8_t i=0;i<7;i++){ - if(perms & (1<0) + this->perms=perms; return(this); } @@ -701,33 +691,53 @@ struct [[deprecated("Please use Characteristic::setRange() method instead.")]] S /////////////////////////////// -struct SpanButton{ +class SpanButton : PushButton { + + friend class Span; + friend class SpanService; + + uint16_t singleTime; // minimum time (in millis) required to register a single press + uint16_t longTime; // minimum time (in millis) required to register a long press + uint16_t doubleTime; // maximum time (in millis) between single presses to register a double press instead + SpanService *service; // Service to which this PushButton is attached + + void check(); // check PushButton and call button() if pressed + + public: enum { SINGLE=0, DOUBLE=1, LONG=2 }; + + static constexpr triggerType_t TRIGGER_ON_LOW=PushButton::TRIGGER_ON_LOW; + static constexpr triggerType_t TRIGGER_ON_HIGH=PushButton::TRIGGER_ON_HIGH; + +#if SOC_TOUCH_SENSOR_NUM > 0 + static constexpr triggerType_t TRIGGER_ON_TOUCH=PushButton::TRIGGER_ON_TOUCH; + static void setTouchCycles(uint16_t measureTime, uint16_t sleepTime){PushButton::setTouchCycles(measureTime,sleepTime);} + static void setTouchThreshold(touch_value_t thresh){PushButton::setTouchThreshold(thresh);} +#endif - int pin; // pin number - uint16_t singleTime; // minimum time (in millis) required to register a single press - uint16_t longTime; // minimum time (in millis) required to register a long press - uint16_t doubleTime; // maximum time (in millis) between single presses to register a double press instead - SpanService *service; // Service to which this PushButton is attached + SpanButton(int pin, uint16_t longTime=2000, uint16_t singleTime=5, uint16_t doubleTime=200, triggerType_t triggerType=TRIGGER_ON_LOW); + SpanButton(int pin, triggerType_t triggerType, uint16_t longTime=2000, uint16_t singleTime=5, uint16_t doubleTime=200) : SpanButton(pin,longTime,singleTime,doubleTime,triggerType){}; - PushButton *pushButton; // PushButton associated with this SpanButton - - SpanButton(int pin, uint16_t longTime=2000, uint16_t singleTime=5, uint16_t doubleTime=200); }; /////////////////////////////// -struct SpanUserCommand { +class SpanUserCommand { + + friend class Span; + const char *s; // description of command void (*userFunction1)(const char *v)=NULL; // user-defined function to call void (*userFunction2)(const char *v, void *arg)=NULL; // user-defined function to call with user-defined arg void *userArg; + public: + SpanUserCommand(char c, const char *s, void (*f)(const char *)); SpanUserCommand(char c, const char *s, void (*f)(const char *, void *), void *arg); }; diff --git a/src/Network.cpp b/src/Network.cpp index 8b2ad91..cc1c102 100644 --- a/src/Network.cpp +++ b/src/Network.cpp @@ -152,10 +152,10 @@ void Network::apConfigure(){ while(1){ // loop until we get timed out (which will be accelerated if save/cancel selected) - if(homeSpan.controlButton.triggered(9999,3000)){ + if(homeSpan.controlButton && homeSpan.controlButton->triggered(9999,3000)){ Serial.print("\n*** Access Point Terminated."); homeSpan.statusLED.start(LED_ALERT); - homeSpan.controlButton.wait(); + homeSpan.controlButton->wait(); Serial.print(" Restarting... \n\n"); homeSpan.statusLED.off(); ESP.restart(); diff --git a/src/Settings.h b/src/Settings.h index 3da1fbb..73225ad 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -35,8 +35,8 @@ // HomeSpan Version // #define HS_MAJOR 1 -#define HS_MINOR 5 -#define HS_PATCH 1 +#define HS_MINOR 6 +#define HS_PATCH 0 #define STRINGIFY(x) _STR(x) #define _STR(x) #x @@ -55,6 +55,10 @@ #define ARDUINO_ESP_VERSION STRINGIFY(ARDUINO_ESP32_GIT_DESC) +#if ESP_ARDUINO_VERSION_MAJOR<2 + #error HOMESPAN REQUIRES VERSION 2 OF THE ARDUINO ESP32 LIBRARY +#endif + ////////////////////////////////////////////////////// // DEFAULT SETTINGS // @@ -67,7 +71,6 @@ #define DEFAULT_QR_ID "HSPN" // change with homeSpan.setQRID(qrID); -#define DEFAULT_CONTROL_PIN -1 // change with homeSpan.setControlPin(pin) #define DEFAULT_STATUS_PIN -1 // change with homeSpan.setStatusPin(pin) #define DEFAULT_AP_SSID "HomeSpan-Setup" // change with homeSpan.setApSSID(ssid) @@ -104,10 +107,10 @@ // 0=Minimal, 1=Informative, 2=All // #define LOG0(format,...) Serial.print ##__VA_OPT__(f)(format __VA_OPT__(,) __VA_ARGS__) -#define LOG1(format,...) if(homeSpan.logLevel>0)Serial.print ##__VA_OPT__(f)(format __VA_OPT__(,) __VA_ARGS__) -#define LOG2(format,...) if(homeSpan.logLevel>1)Serial.print ##__VA_OPT__(f)(format __VA_OPT__(,) __VA_ARGS__) +#define LOG1(format,...) if(homeSpan.getLogLevel()>0)Serial.print ##__VA_OPT__(f)(format __VA_OPT__(,) __VA_ARGS__) +#define LOG2(format,...) if(homeSpan.getLogLevel()>1)Serial.print ##__VA_OPT__(f)(format __VA_OPT__(,) __VA_ARGS__) -#define WEBLOG(format,...) homeSpan.webLog.addLog(format __VA_OPT__(,) __VA_ARGS__) +#define WEBLOG(format,...) homeSpan.addWebLog(false, format __VA_OPT__(,) __VA_ARGS__) ////////////////////////////////////////////////////// // Types of Accessory Categories // diff --git a/src/Span.h b/src/Span.h index 8b7e4b9..8bb10da 100644 --- a/src/Span.h +++ b/src/Span.h @@ -31,8 +31,8 @@ // Macros to define vectors of required and optional characteristics for each Span Service structure -#define REQ(HAPCHAR) req.push_back(&hapChars.HAPCHAR) -#define OPT(HAPCHAR) opt.push_back(&hapChars.HAPCHAR) +#define REQ(HAPCHAR) req.insert(&hapChars.HAPCHAR) +#define OPT(HAPCHAR) opt.insert(&hapChars.HAPCHAR) namespace Service { @@ -43,7 +43,8 @@ namespace Service { OPT(Model); OPT(Name); OPT(SerialNumber); - OPT(HardwareRevision); + OPT(HardwareRevision); + OPT(AccessoryFlags); }}; struct AirPurifier : SpanService { AirPurifier() : SpanService{"BB","AirPurifier"}{ @@ -403,6 +404,7 @@ namespace Service { namespace Characteristic { + CREATE_CHAR(uint32_t,AccessoryFlags,1,1,1); CREATE_CHAR(uint8_t,Active,0,0,1); CREATE_CHAR(uint32_t,ActiveIdentifier,0,0,255); CREATE_CHAR(uint8_t,AirQuality,0,0,5); diff --git a/src/Utils.cpp b/src/Utils.cpp index 282d0e0..5997e28 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -85,34 +85,39 @@ String Utils::mask(char *c, int n){ // PushButton // //////////////////////////////// -PushButton::PushButton(){} - -////////////////////////////////////// - -PushButton::PushButton(int pin){ - init(pin); -} - -////////////////////////////////////// - -void PushButton::init(int pin){ +PushButton::PushButton(int pin, triggerType_t triggerType){ this->pin=pin; - if(pin<0) - return; - + this->triggerType=triggerType; status=0; doubleCheck=false; - pinMode(pin, INPUT_PULLUP); + + if(triggerType==TRIGGER_ON_LOW) + pinMode(pin, INPUT_PULLUP); + else if(triggerType==TRIGGER_ON_HIGH) + pinMode(pin, INPUT_PULLDOWN); + +#if SOC_TOUCH_SENSOR_NUM > 0 + else if (triggerType==TRIGGER_ON_TOUCH && threshold==0){ + for(int i=0;i %d.\n",pin,threshold); +#endif + } +#endif + } ////////////////////////////////////// boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t doubleTime){ - if(pin<0) - return(false); - unsigned long cTime=millis(); switch(status){ @@ -124,7 +129,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d return(true); } - if(!digitalRead(pin)){ // button is pressed + if(triggerType(pin)){ // button is "pressed" singleAlarm=cTime+singleTime; if(!doubleCheck){ status=1; @@ -138,7 +143,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d case 1: case 2: - if(digitalRead(pin)){ // button is released + if(!triggerType(pin)){ // button is released status=0; if(cTime>singleAlarm){ doubleCheck=true; @@ -154,7 +159,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d break; case 3: - if(digitalRead(pin)) // button has been released after a long press + if(!triggerType(pin)) // button has been released after a long press status=0; else if(cTime>longAlarm){ longAlarm=cTime+longTime; @@ -164,7 +169,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d break; case 4: - if(digitalRead(pin)){ // button is released + if(!triggerType(pin)){ // button is released status=0; } else @@ -177,7 +182,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d break; case 5: - if(digitalRead(pin)) // button has been released after double-click + if(!triggerType(pin)) // button has been released after double-click status=0; break; @@ -189,10 +194,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d ////////////////////////////////////// boolean PushButton::primed(){ - - if(pin<0) - return(false); - + if(millis()>singleAlarm && status==1){ status=2; return(true); @@ -209,12 +211,8 @@ int PushButton::type(){ ////////////////////////////////////// -void PushButton::wait(){ - - if(pin<0) - return; - - while(!digitalRead(pin)); +void PushButton::wait(){ + while(triggerType(pin)); } ////////////////////////////////////// @@ -223,6 +221,12 @@ void PushButton::reset(){ status=0; } +////////////////////////////////////// + +#if SOC_TOUCH_SENSOR_NUM > 0 + touch_value_t PushButton::threshold=0; +#endif + //////////////////////////////// // Blinker // //////////////////////////////// diff --git a/src/Utils.h b/src/Utils.h index ff8784f..b32922c 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -74,13 +74,24 @@ struct TempBuffer { class PushButton{ int status; - int pin; boolean doubleCheck; uint32_t singleAlarm; uint32_t doubleAlarm; uint32_t longAlarm; int pressType; +#if SOC_TOUCH_SENSOR_NUM > 0 + static touch_value_t threshold; + static const int calibCount=20; +#endif + + protected: + + typedef boolean (*triggerType_t)(int pin); + + int pin; + triggerType_t triggerType; + public: enum { @@ -88,28 +99,27 @@ class PushButton{ DOUBLE=1, LONG=2 }; - - PushButton(); - PushButton(int pin); -// Creates generic pushbutton functionality on specified pin -// that is wired to connect to ground when the button is pressed. -// -// In the first form, a PushButton is instantiated without specifying -// the pin. In this case the pin must be specified in a subsequent call -// to init() before the PushButton can be used. -// -// In the second form, a PushButton is instantiated and initialized with -// the specified pin, obviating the need for a separate call to init(). -// -// pin: Pin mumber to which pushbutton connects to ground when pressed + static boolean TRIGGER_ON_LOW(int pin){return(!digitalRead(pin));} + static boolean TRIGGER_ON_HIGH(int pin){return(digitalRead(pin));} - void init(int pin); - -// Initializes PushButton, if not configured during instantiation. +#if SOC_TOUCH_VERSION_1 // ESP32 + static boolean TRIGGER_ON_TOUCH(int pin){return(touchRead(pin)threshold);} +#endif + + PushButton(int pin, triggerType_t triggerType=TRIGGER_ON_LOW); + +// Creates pushbutton of specified type on specified pin // -// pin: Pin mumber to which pushbutton connects to ground when pressed - +// pin: pin number to which the button is connected +// triggerType: a function of of the form 'boolean f(int)' that is passed +// the parameter *pin* and returns TRUE if the button associated +// with *pin* is pressed, or FALSE if not. Can choose from 3 pre-specifed +// triggerType_t functions (TRIGGER_ON_LOW, TRIGGER_ON_HIGH, and TRIGGER_ON_TOUCH), or write your +// own custom handler + void reset(); // Resets state of PushButton. Should be called once before any loops that will @@ -148,6 +158,28 @@ class PushButton{ // Waits for button to be released. Use after Long Press if button release confirmation is desired + int getPin(){return(pin);} + +// Returns pin number + +#if SOC_TOUCH_SENSOR_NUM > 0 + + static void setTouchCycles(uint16_t measureTime, uint16_t sleepTime){touchSetCycles(measureTime,sleepTime);} + +// Sets the measure time and sleep time touch cycles , and lower threshold that triggers a touch - used only when triggerType=PushButton::TRIGGER_ON_TOUCH + +// measureTime: duration of measurement time of all touch sensors in number of clock cycles +// sleepTime: duration of sleep time (between measurements) of all touch sensors number of clock cycles + + static void setTouchThreshold(touch_value_t thresh){threshold=thresh;} + +// Sets the threshold that triggers a touch - used only when triggerType=TRIGGER_ON_TOUCH + +// thresh: the read value of touch sensors, beyond which which sensors are considered touched (i.e. "pressed"). +// This is a class-level value applied to all touch sensor buttons. + +#endif + }; //////////////////////////////// diff --git a/src/extras/RFControl.cpp b/src/extras/RFControl.cpp index 6b6f1b6..31e48c3 100644 --- a/src/extras/RFControl.cpp +++ b/src/extras/RFControl.cpp @@ -31,7 +31,7 @@ RFControl::RFControl(uint8_t pin, boolean refClock, boolean installDriver){ -#ifdef CONFIG_IDF_TARGET_ESP32C3 +#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) if(nChannels==RMT_CHANNEL_MAX/2){ #else if(nChannels==RMT_CHANNEL_MAX){ @@ -52,6 +52,7 @@ RFControl::RFControl(uint8_t pin, boolean refClock, boolean installDriver){ config->mem_block_num=1; config->gpio_num=(gpio_num_t)pin; config->tx_config.idle_output_en=false; + config->tx_config.idle_level=RMT_IDLE_LEVEL_LOW; config->tx_config.loop_en=false; rmt_config(config); @@ -64,8 +65,8 @@ RFControl::RFControl(uint8_t pin, boolean refClock, boolean installDriver){ this->refClock=refClock; if(refClock) -#ifdef CONFIG_IDF_TARGET_ESP32C3 - REG_SET_FIELD(RMT_SYS_CONF_REG,RMT_SCLK_DIV_NUM,79); // ESP32-C3 does not have a 1 MHz REF Tick Clock, but allows the 80 MHz APB clock to be scaled by an additional RMT-specific divider +#ifdef RMT_SYS_CONF_REG + REG_SET_FIELD(RMT_SYS_CONF_REG,RMT_SCLK_DIV_NUM,79); // ESP32-C3 and ESP32-S3 do not have a 1 MHz REF Tick Clock, but allows the 80 MHz APB clock to be scaled by an additional RMT-specific divider #else rmt_set_source_clk(config->channel,RMT_BASECLK_REF); // use 1 MHz REF Tick Clock for ESP32 and ESP32-S2 #endif diff --git a/src/src.ino b/src/src.ino index aea281e..19e4ae3 100644 --- a/src/src.ino +++ b/src/src.ino @@ -13,9 +13,9 @@ void setup() { Serial.begin(115200); - homeSpan.setLogLevel(2); +// homeSpan.setLogLevel(2); // homeSpan.setStatusPin(13); -// homeSpan.setControlPin(33); + homeSpan.setControlPin(33); homeSpan.setHostNameSuffix("-lamp1"); homeSpan.setPortNum(1201);