Merge pull request #363 from HomeSpan/dev

Dev
This commit is contained in:
HomeSpan 2022-08-20 17:11:09 -04:00 committed by GitHub
commit d6d87c621d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1616 additions and 627 deletions

View File

@ -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 <WebServer.h> // 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<MAX_LIGHTS;i++){ // create Light Accessories based on saved data
if(lightData[i].aid)
addLight(i);
}
new SpanUserCommand('a',"<name> - add non-dimmable light accessory using name=<name>",[](const char *c){addLight(c+1,false,NO_COLOR);});
new SpanUserCommand('A',"<name> - add dimmable light accessory using name=<name>",[](const char *c){addLight(c+1,true,NO_COLOR);});
new SpanUserCommand('t',"<name> - add non-dimmable light accessory with color-temperature control using name=<name>",[](const char *c){addLight(c+1,false,TEMPERATURE_ONLY);});
new SpanUserCommand('T',"<name> - add dimmable light accessory with color-temperature control using name=<name>",[](const char *c){addLight(c+1,true,TEMPERATURE_ONLY);});
new SpanUserCommand('r',"<name> - add non-dimmable light accessory with full RGB color control using name=<name>",[](const char *c){addLight(c+1,false,FULL_RGB);});
new SpanUserCommand('R',"<name> - add dimmable light accessory with full RGB color control using name=<name>",[](const char *c){addLight(c+1,true,FULL_RGB);});
new SpanUserCommand('l'," - list all light accessories",listAccessories);
new SpanUserCommand('d',"<index> - delete a light accessory with index=<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;index<MAX_LIGHTS && lightData[index].aid;index++);
if(index==MAX_LIGHTS){
Serial.printf("Can't add Light Accessory - maximum number of %d are already defined.\n",MAX_LIGHTS);
return(-1);
}
int n=strncpy_trim(lightData[index].name,name,sizeof(lightData[index].name));
if(n==1){
Serial.printf("Can't add Light Accessory without a name specified.\n");
return(-1);
}
if(n>sizeof(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<sSize?dSize:sSize)-1)='\0';
return(sSize); // return total size needed for entire trimmed string, including null terminator
}
///////////////////////////
void deleteAccessory(int index){
if(index<0 || index>=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<MAX_LIGHTS;i++){
homeSpan.deleteAccessory(lightData[i].aid);
lightData[i].aid=0;
}
nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS
nvs_commit(savedData);
Serial.printf("All Light Accessories deleted!\n");
}
///////////////////////////
void updateAccessories(const char *buf){
if(homeSpan.updateDatabase())
Serial.printf("Accessories Database updated. New configuration number broadcasted...\n");
else
Serial.printf("Nothing to update - no changes were made!\n");
}
///////////////////////////
void listAccessories(const char *buf){
Serial.printf("\nIndex Dimmable Color Name\n");
Serial.printf("----- -------- ----- ");
for(int i=0;i<MAX_NAME_LENGTH;i++)
Serial.printf("-");
Serial.printf("\n");
for(int i=0;i<MAX_LIGHTS;i++){
if(lightData[i].aid)
Serial.printf("%5d %8s %5s %-s\n",i,lightData[i].isDimmable?"YES":"NO",lightData[i].colorType==NO_COLOR?"NONE":(lightData[i].colorType==TEMPERATURE_ONLY?"TEMP":"RGB"),lightData[i].name);
}
Serial.printf("\n");
}
///////////////////////////
void setupWeb(){
Serial.printf("Starting Light Server Hub at %s.local\n\n",HUB_NAME);
webServer.begin();
webServer.on("/", []() {
String response = "<html><head><title>HomeSpan Programmable Light Hub</title>";
response += "<style>table, th, td {border: 1px solid black; border-collapse: collapse;} th, td { padding: 5px; text-align: center; } </style></head>\n";
response += "<body><h2>HomeSpan Lights</h2>";
response += "<form action='/addLight' method='get'>";
response += "<table><tr><th style='text-align:left;'>Accessory</th><th>Dim?</th><th>Color Control</th><th>Action</th></tr>";
int openSlots=MAX_LIGHTS;
for(int i=0;i<MAX_LIGHTS;i++){
if(lightData[i].aid){
response += "<tr><td style='text-align:left;'>" + String(lightData[i].name) + "</td>";
response += "<td><input type='checkbox' disabled " + String(lightData[i].isDimmable?"checked>":">") + "</td>";
response += "<td><input type='radio' disabled " + String(lightData[i].colorType==NO_COLOR?"checked>":">") + " NONE ";
response += "<input type='radio' disabled " + String(lightData[i].colorType==TEMPERATURE_ONLY?"checked>":">") + " TEMP ONLY ";
response += "<input type='radio' disabled " + String(lightData[i].colorType==FULL_RGB?"checked>":">") + " FULL COLOR </td>";
response += "<td><button type='button' onclick=\"document.location='/deleteLight?index=" + String(i) + "'\">Delete Light</button></td>";
response += "</tr>";
openSlots--;
}
}
response += "<tr><td style='text-align:left;'><input type='text' name='name' required placeholder='Type accessory name here...' size='"
+ String(MAX_NAME_LENGTH) + "' maxlength='" + String(MAX_NAME_LENGTH) + "'></td>";
response += "<td><input type='checkbox' name='isDimmable'></td>";
response += "<td><input type='radio' checked name='colorType' for='no_color' value='" + String(NO_COLOR) + "'><label for='no_color'> NONE </label>";
response += "<input type='radio' name='colorType' for='temp_only' value='" + String(TEMPERATURE_ONLY) + "'><label for='temp_only'> TEMP ONLY </label>";
response += "<input type='radio' name='colorType' for='full_rgb' value='" + String(FULL_RGB) + "'><label for='full_rgb'> FULL COLOR </label></td>";
response += "<td><input type='submit' value='Add Light'" + String(openSlots?"":" disabled") + "></td>";
response += "</tr>";
response += "</table>";
response += "</form>";
if(!openSlots)
response += "<p>Can't add any more Light Accessories. Max="+ String(MAX_LIGHTS) + "</p>";
response += "<p>Press here to delete ALL Light Accessories: <button type='button' onclick=\"document.location='/deleteAll'\">Delete All Lights</button></p>";
response += "<p>Press here to update the Home App when finished making changes: <button type='button' onclick=\"document.location='/update'\">Upddate HomeKit</button></p>";
response += "</body></html>";
webServer.send(200, "text/html", response);
});
webServer.on("/deleteLight", []() {
int index=atoi(webServer.arg(0).c_str());
String response = "<html><head><title>HomeSpan Programmable Light Hub</title><meta http-equiv='refresh' content = '3; url=/'/></head>";
response += "<body>Deleting Light Accessory '" + String(lightData[index].name) + "'...</body></html>";
deleteAccessory(index);
webServer.send(200, "text/html", response);
});
webServer.on("/deleteAll", []() {
String response = "<html><head><title>HomeSpan Programmable Light Hub</title><meta http-equiv='refresh' content = '3; url=/'/></head>";
response += "<body>Deleting All Light Accessories...</body></html>";
webServer.send(200, "text/html", response);
deleteAllAccessories("");
});
webServer.on("/update", []() {
String response = "<html><head><title>HomeSpan Programmable Light Hub</title><meta http-equiv='refresh' content = '3; url=/'/></head><body>";
if(homeSpan.updateDatabase())
response += "Accessories Database updated. New configuration number broadcasted...";
else
response += "Nothing to update - no changes were made...";
response += "...</body></html>";
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<webServer.args();i++){
if(!webServer.argName(i).compareTo(String("colorType")))
colorType=(colorType_t)webServer.arg(i).toInt();
else if(!webServer.argName(i).compareTo(String("isDimmable")))
isDimmable=true;
else if(!webServer.argName(i).compareTo(String("name")))
iName=i;
}
String response = "<html><head><title>HomeSpan Programmable Light Hub</title><meta http-equiv='refresh' content = '3; url=/'/></head><body>";
if(iName!=-1){
int index=addLight(webServer.arg(iName).c_str(),isDimmable,colorType);
response += "Adding Light Accessory '" + String(lightData[index].name) + "'";
} else
response += "Error - bad URL request";
response += "...</body></html>";
webServer.send(200, "text/html", response);
});
}
///////////////////////////

View File

@ -35,6 +35,9 @@ In addition to listening for incoming HAP requests, HomeSpan also continuously p
* **d** - print the full HAP Accessory Attributes Database in JSON format * **d** - print the full HAP Accessory Attributes Database in JSON format
* This outputs the full HAP Database in JSON format, exactly as it is transmitted to any HomeKit device that requests it (with the exception of the newlines and spaces that make it easier to read on the screen). Note that the value tag for each Characteristic will reflect the *current* value on the device for that Characteristic. * This outputs the full HAP Database in JSON format, exactly as it is transmitted to any HomeKit device that requests it (with the exception of the newlines and spaces that make it easier to read on the screen). Note that the value tag for each Characteristic will reflect the *current* value on the device for that Characteristic.
* **m** - print free heap memory (in bytes)
* This prints the amount of memory available for use when creating new objects or allocating memory. Useful for developers only.
* **W** - configure WiFi Credentials and restart * **W** - configure WiFi Credentials and restart
* HomeSpan sketches *do not* contain WiFi network names or WiFi passwords. Rather, this information is separately stored in a dedicated Non-Volatile Storage (NVS) partition in the ESP32's flash memory, where it is permanently retained until updated (with this command) or erased (see below). When HomeSpan receives this command it first scans for any local WiFi networks. If your network is found, you can specify it by number when prompted for the WiFi SSID. Otherwise, you can directly type your WiFi network name. After you then type your WiFi Password, HomeSpan updates the NVS with these new WiFi Credentials, and restarts the device. * HomeSpan sketches *do not* contain WiFi network names or WiFi passwords. Rather, this information is separately stored in a dedicated Non-Volatile Storage (NVS) partition in the ESP32's flash memory, where it is permanently retained until updated (with this command) or erased (see below). When HomeSpan receives this command it first scans for any local WiFi networks. If your network is found, you can specify it by number when prompted for the WiFi SSID. Otherwise, you can directly type your WiFi network name. After you then type your WiFi Password, HomeSpan updates the NVS with these new WiFi Credentials, and restarts the device.

View File

@ -34,10 +34,6 @@
* No, HomeSpan is coded specifically for the ESP32 and will not operate on an ESP8266 device. * No, HomeSpan is coded specifically for the ESP32 and will not operate on an ESP8266 device.
#### Will HomeSpan work on an ESP32-S2 or ESP32-C3?
* Yes! Starting with version 1.4.0, HomeSpan is fully compatible with Espressif's ESP32-S2 and ESP32-C3 chips, as well as the original ESP32 chip. Note that to select an ESP32-S2 or ESP32-C3 device from the Arduino IDE you'll need to install Version 2 of the [Arduino-ESP32 Board Manager](https://github.com/espressif/arduino-esp32).
#### How can I read HomeSpan's MDNS broadcast mentioned in the [OTA](OTA.md) documentation? #### How can I read HomeSpan's MDNS broadcast mentioned in the [OTA](OTA.md) documentation?
* HomeSpan uses MDNS (multicast DNS) to broadcast a variety of HAP information used by Controllers wishing to pair with HomeSpan. Apple uses the name *Bonjour* to refer to MDNS, and originally included a Bonjour "Browser" in Safari that has since been discontinued. However, there are a number of alternative MDNS browsers available for free that operate on both the Mac and the iPhone, such as the [Discovery - DNS-SD Browser](https://apps.apple.com/us/app/discovery-dns-sd-browser/id1381004916?mt=12). You'll find all your HomeSpan devices, as well as any other HomeKit devices you may have, under the MDNS service named *_hap._tcp.* The fields broadcast by HomeSpan are a combination of all data elements requires by HAP (HAP-R2, Table 6-7) plus three additional HomeSpan fields: * HomeSpan uses MDNS (multicast DNS) to broadcast a variety of HAP information used by Controllers wishing to pair with HomeSpan. Apple uses the name *Bonjour* to refer to MDNS, and originally included a Bonjour "Browser" in Safari that has since been discontinued. However, there are a number of alternative MDNS browsers available for free that operate on both the Mac and the iPhone, such as the [Discovery - DNS-SD Browser](https://apps.apple.com/us/app/discovery-dns-sd-browser/id1381004916?mt=12). You'll find all your HomeSpan devices, as well as any other HomeKit devices you may have, under the MDNS service named *_hap._tcp.* The fields broadcast by HomeSpan are a combination of all data elements requires by HAP (HAP-R2, Table 6-7) plus three additional HomeSpan fields:
@ -64,7 +60,7 @@
#### Can you add a Web Server to HomeSpan? #### Can you add a Web Server to HomeSpan?
* Yes, provided you implement your Web Server using standard ESP32-Arduino libraries, such as `WebServer.h`. See [ProgrammableHub](https://github.com/HomeSpan/ProgrammableHub) for an illustrative example of how to easily integrate a Web Server into HomeSpan. This project also covers various other advanced topics, including TCP slot management, dynamic creation of Accessories, and saving arbitrary data in the ESP32's NVS. * Yes, provided you implement your Web Server using standard ESP32-Arduino libraries, such as `WebServer.h`. See [ProgrammableHub](../Other%20Examples/ProgrammableHub) for an illustrative example of how to easily integrate a Web Server into HomeSpan. This project also covers various other advanced topics, including TCP slot management, dynamic creation of Accessories, and saving arbitrary data in the ESP32's NVS.
#### Can you add *custom* Services and Characteristics to HomeSpan? #### Can you add *custom* Services and Characteristics to HomeSpan?

View File

@ -1,10 +0,0 @@
# HomeKit Primer
*(editing in progress)*
The standard reference for all HomeKit devices is [Apple's HomeKit Accessory Protocol Specification Release R2 (HAP-R2)](https://developer.apple.com/support/homekit-accessory-protocol/). HomeSpan rigorously implements HAP-R2, so if you are already familiar with HomeKit concepts and terminology, using HomeSpan to create your own HomeKit devices should be extremely easy. However, if this is your first time programming for HomeKit, some of the basic HomeKit terminology can be confusing, and unfortunately Apple's HAP-R2 guide does not provide much in the way of an introduction or overview to HomeKit itself. This page hopefully provides you with the missing "overview" you need to better understand the overall HomeKit ecosystem.
---
[↩️](README.md) Back to the Welcome page

View File

@ -42,6 +42,30 @@ void loop(){
} // end of loop() } // end of loop()
``` ```
Note that as an *alternative*, you can intruct HomeSpan to create separate task that repeatedly calls `homeSpan.poll()` in the background. To do so, **replace** the call to `homeSpan.poll()` in the main `loop()` with a call to `homeSpan.autoPoll()` at the end of the `setup()` function:
```C++
#include "HomeSpan.h" // include the HomeSpan library
void setup() {
Serial.begin(115200); // start the Serial interface
homeSpan.begin(); // initialize HomeSpan
/// DEFINITION OF HAP ACCESSORY ATTRIBUTE DATABASE GOES HERE ///
homeSpan.autoPoll(); // start a task that repeatedly calls `homeSpan.poll()` in the background
} // end of setup()
void loop(){
} // end of loop()
```
This is particularly efficient when using dual-core processors since HomeSpan will run the polling task on the "free" processor that is otherwise not performing any other Arduino functions.
## Creating the HAP Accessory Attribute Database ## Creating the HAP Accessory Attribute Database
The next step is to implement the code that defines the HAP Accessory Attribute Database, which is not really a database but simply a list of all HAP accessory objects, Service objects, and Characteristic objects implemented by this HomeSpan device. The next step is to implement the code that defines the HAP Accessory Attribute Database, which is not really a database but simply a list of all HAP accessory objects, Service objects, and Characteristic objects implemented by this HomeSpan device.

View File

@ -65,6 +65,7 @@ The following PWM resources are available:
* ESP32: 16 Channels / 8 Timers (arranged in two distinct sets of 8 Channels and 4 Timers) * ESP32: 16 Channels / 8 Timers (arranged in two distinct sets of 8 Channels and 4 Timers)
* ESP32-S2: 8 Channels / 4 Timers * ESP32-S2: 8 Channels / 4 Timers
* ESP32-C3: 6 Channels / 4 Timers * ESP32-C3: 6 Channels / 4 Timers
* ESP32-S3: 8 Channels / 4 Timers
HomeSpan *automatically* allocates Channels and Timers to LedPin and ServoPin objects as they are instantiated. Every pin assigned consumes a single Channel; every *unique* frequency specified among all channels (within the same set, for the ESP32) consumes a single Timer. HomeSpan will conserve resources by re-using the same Timer for all Channels operating at the same frequency. *HomeSpan also automatically configures each Timer to support the maximum duty-resolution possible for the frequency specified.* HomeSpan *automatically* allocates Channels and Timers to LedPin and ServoPin objects as they are instantiated. Every pin assigned consumes a single Channel; every *unique* frequency specified among all channels (within the same set, for the ESP32) consumes a single Timer. HomeSpan will conserve resources by re-using the same Timer for all Channels operating at the same frequency. *HomeSpan also automatically configures each Timer to support the maximum duty-resolution possible for the frequency specified.*

View File

@ -4,7 +4,7 @@ Welcome to HomeSpan - a robust and extremely easy-to-use Arduino library for cre
HomeSpan provides a microcontroller-focused implementation of [Apple's HomeKit Accessory Protocol Specification Release R2 (HAP-R2)](https://developer.apple.com/homekit/specification/) designed specifically for the Espressif ESP32 microcontroller running within the Arduino IDE. HomeSpan pairs directly to HomeKit via your home WiFi network without the need for any external bridges or components. With HomeSpan you can use the full power of the ESP32's I/O functionality to create custom control software and/or hardware to automatically operate external devices from the Home App on your iPhone, iPad, or Mac, or with Siri. HomeSpan provides a microcontroller-focused implementation of [Apple's HomeKit Accessory Protocol Specification Release R2 (HAP-R2)](https://developer.apple.com/homekit/specification/) designed specifically for the Espressif ESP32 microcontroller running within the Arduino IDE. HomeSpan pairs directly to HomeKit via your home WiFi network without the need for any external bridges or components. With HomeSpan you can use the full power of the ESP32's I/O functionality to create custom control software and/or hardware to automatically operate external devices from the Home App on your iPhone, iPad, or Mac, or with Siri.
HomeSpan is fully compatible with both Versions 1 and 2 of the [Arduino-ESP32 Board Manager](https://github.com/espressif/arduino-esp32). Under Version 1, HomeSpan can be run only on the original ESP32. Under Version 2, HomeSpan can be run on the original ESP32 as well as Espressif's ESP32-S2 and ESP32-C3 chips. HomeSpan requires version 2.0.0 or later of the [Arduino-ESP32 Board Manager](https://github.com/espressif/arduino-esp32), and has been tested up through version 2.0.4 (recommended). HomeSpan can be run on the original ESP32 as well as Espressif's ESP32-S2, ESP32-C3, and ESP32-S3 chips.
### HomeSpan Highlights ### HomeSpan Highlights
@ -26,7 +26,9 @@ HomeSpan is fully compatible with both Versions 1 and 2 of the [Arduino-ESP32 Bo
* Dedicated classes that utilize the ESP32's 16-channel PWM peripheral for easy control of: * Dedicated classes that utilize the ESP32's 16-channel PWM peripheral for easy control of:
* LED Brightness * LED Brightness
* Servo Motors * Servo Motors
* Integrated Push Button functionality supporting single, double, and long presses * Integrated Push Button functionality supporting single, double, and long presses of:
* Physical pushbuttons that connect an ESP32 pin to either ground or VCC
* Touch pads/sensors connected to an ESP32 pin (for ESP32 devices that support touch pads)
* Integrated access to the ESP32's on-chip Remote Control peripheral for easy generation of IR and RF signals * Integrated access to the ESP32's on-chip Remote Control peripheral for easy generation of IR and RF signals
* Dedicated classes to control one- and two-wire addressable RGB and RGBW LEDs and LED strips * Dedicated classes to control one- and two-wire addressable RGB and RGBW LEDs and LED strips
* Integrated Web Log for user-defined log messages * Integrated Web Log for user-defined log messages
@ -45,34 +47,38 @@ HomeSpan is fully compatible with both Versions 1 and 2 of the [Arduino-ESP32 Bo
* Launch the WiFi Access Point * Launch the WiFi Access Point
* A standalone, detailed End-User Guide * A standalone, detailed End-User Guide
## ❗Latest Update - HomeSpan 1.5.1 (4/17/2022) ## ❗Latest Update - HomeSpan 1.6.0 (8/21/2022)
* **New Web Logging functionality** * **Support for ESP32-S3 devices**
* HomeSpan can now host a Web Log page for message logging * Requires Arduino-ESP32 Board Manager version 2.0.4 or later
* New WEBLOG() macro makes is easy to create user-defined log messages
* Provides for the optional use of an NTP Time Server to set the device clock so all messages can be properly timestamped * **New functionality to *dynamically* add/delete Accessories on the fly without rebooting the device**
* See [HomeSpan Message Logging](https://github.com/HomeSpan/HomeSpan/blob/master/docs/Logging.md) for full details * Adds `homeSpan.deleteAccessory()` and `homeSpan.updateDatabase()` methods
* Includes new [Example 20 - AdvancedTechniques](https://github.com/HomeSpan/HomeSpan/blob/master/examples/20-AdvancedTechniques) to demonstrate how these methods can be used to create a dynamic bridge
* **New *printf*-style formatting for LOG() macros** * See [HomeSpan API Reference](https://github.com/HomeSpan/HomeSpan/blob/master/docs/Reference.md) for details
* Adds variadic forms of the LOG0(), LOG1(), and LOG2() macros so they can be used in the same manner as a standard C printf function
* Greatly simplifies the creation of log messages * **New support for Touch Pads**
* See [HomeSpan Message Logging](https://github.com/HomeSpan/HomeSpan/blob/master/docs/Logging.md) for full details * `SpanButton()` now supports three pin trigger methods: *TRIGGER_ON_LOW*, *TRIGGER_ON_HIGH*, and *TRIGGER_ON_TOUCH*
* Also allows users to add their own trigger methods so `SpanButton()` can monitor pushbuttons attached to pin extenders, pin multiplexers, or any other device that requires calling third-party library functions
* **New CUSTOM_SERV() macro**
* Allows for the creation of Custom Services
* Can be used in conjunction with the existing CUSTOM_CHAR() macro to produce Services beyond those provided in HAP-R2
* Includes a fully worked example of a custom [Pressure Sensor Accessory](https://github.com/HomeSpan/HomeSpan/blob/master/Other%20Examples/CustomService) that is recognized by *Eve for HomeKit*
* See [HomeSpan API Reference](https://github.com/HomeSpan/HomeSpan/blob/master/docs/Reference.md) for details * See [HomeSpan API Reference](https://github.com/HomeSpan/HomeSpan/blob/master/docs/Reference.md) for details
* **New "Safe-Load" mode for OTA updates** * **Improved WiFi disconnect/reconnect handling**
* HomeSpan can check to make sure the new sketch being uploaded via OTA is another HomeSpan sketch. If not, the upload fails * Fixes code that may, under certain circumstances, have prevented HomeSpan from reconnecting to WiFi after signal is lost
* Upon rebooting after an OTA update, HomeSpan checks to ensure that OTA is enabled in the updated sketch. If not, HomeSpan rolls back to the previous version of the sketch * Adds WiFi diagnostics to Web Logs to monitor for disconnects and WiFi signal strength
* See [HomeSpan OTA](https://github.com/HomeSpan/HomeSpan/blob/master/docs/OTA.md) for full details
* **New option to run HomeSpan as a separate task in its own thread**
* Works with both single- and dual-core processors
* For dual-core processors, polling task will be created on the "free" core not being used for other Arduino functions
* Allows user to add time-sensitive code the the main Arduino `loop()` function without delaying, or being dalyed by, HomeSpan polling
* See [HomeSpan API Reference](https://github.com/HomeSpan/HomeSpan/blob/master/docs/Reference.md) for details
* **New [Programmable Hub](https://github.com/HomeSpan/HomeSpan/blob/master/Other%20Examples/ProgrammableHub) Example**
* Demonstrates how to implement a Web Interface that allows users to dynamically add, delete, and configure up to 12 separate Light Accessories on a bridge device
* Expands upon many of the methods used in [Example 20](https://github.com/HomeSpan/HomeSpan/blob/master/examples/20-AdvancedTechniques)
* **Additional updates include:** * **Additional updates include:**
* a new (optional) argument to `SpanUserCommand()` that allows for passing a pointer to any arbitrary data structure * New 'm' CLI command that prints free bytes remaining in heap memory - useful for monitoring memory usage when dynamically adding Accessories
* a new SPAN_ACCESSORY() macro that expands to a common snippet of code often used when creating Accessories * New `homeSpan.getLogLevel()` method that returns the current log level
* refreshed and streamlined example Tutorials, and fully reworked Examples 7 and 11, to best conform with Home App behavior under iOS 15.4
See [Releases](https://github.com/HomeSpan/HomeSpan/releases) for details on all changes and bug fixes included in this update. See [Releases](https://github.com/HomeSpan/HomeSpan/releases) for details on all changes and bug fixes included in this update.

View File

@ -6,7 +6,7 @@ The ESP32 has an on-chip signal-generator peripheral designed to drive an RF or
## *RFControl(int pin, boolean refClock=true)* ## *RFControl(int pin, boolean refClock=true)*
Creating an instance of this **class** initializes the RF/IR signal generator and specifies the ESP32 *pin* to output the signal. You may create more than one instance of this class if driving more than one RF/IR transmitter (each connected to different *pin*), subject to the following limitations: ESP32 - 8 instances; ESP32-S2 - 4 instances; ESP32-C3 - 2 instances. The optional parameter *refClock* is more fully described further below under the `start()` method. Creating an instance of this **class** initializes the RF/IR signal generator and specifies the ESP32 *pin* to output the signal. You may create more than one instance of this class if driving more than one RF/IR transmitter (each connected to different *pin*), subject to the following limitations: ESP32 - 8 instances; ESP32-S2 and ESP32-S3 - 4 instances; ESP32-C3 - 2 instances. The optional parameter *refClock* is more fully described further below under the `start()` method.
Signals are defined as a sequence of HIGH and LOW phases that together form a pulse train where you specify the duration, in *ticks*, of each HIGH and LOW phase, shown respectively as H1-H4 and L1-L4 in the following diagram: Signals are defined as a sequence of HIGH and LOW phases that together form a pulse train where you specify the duration, in *ticks*, of each HIGH and LOW phase, shown respectively as H1-H4 and L1-L4 in the following diagram:

View File

@ -22,8 +22,8 @@ At runtime HomeSpan will create a global **object** named `homeSpan` that suppor
* `void poll()` * `void poll()`
* checks for HAP requests, local commands, and device activity * checks for HAP requests, local commands, and device activity
* **must** be called repeatedly in each sketch and is typically placed at the top of the Arduino `loop()` method * **must** be called repeatedly in each sketch and is typically placed at the top of the Arduino `loop()` method (*unless* `autoPoll()`, described further below, is used instead)
--- ---
The following **optional** `homeSpan` methods override various HomeSpan initialization parameters used in `begin()`, and therefore **should** be called before `begin()` to take effect. If a method is *not* called, HomeSpan uses the default parameter indicated below: The following **optional** `homeSpan` methods override various HomeSpan initialization parameters used in `begin()`, and therefore **should** be called before `begin()` to take effect. If a method is *not* called, HomeSpan uses the default parameter indicated below:
@ -40,7 +40,7 @@ The following **optional** `homeSpan` methods override various HomeSpan initiali
* if *duration* is set to zero, auto-off is disabled (Status LED will remain on indefinitely) * if *duration* is set to zero, auto-off is disabled (Status LED will remain on indefinitely)
* `int getStatusPin()` * `int getStatusPin()`
* returns the pin number of the Status LED as set by `setStatusPin(pin)`, or -1 if no pin has been set * returns the pin number of the Status LED as set by `setStatusPin(pin)`, or -1 if no pin has been set
* `void setApSSID(const char *ssid)` * `void setApSSID(const char *ssid)`
* sets the SSID (network name) of the HomeSpan Setup Access Point (default="HomeSpan-Setup") * sets the SSID (network name) of the HomeSpan Setup Access Point (default="HomeSpan-Setup")
@ -61,6 +61,9 @@ The following **optional** `homeSpan` methods override various HomeSpan initiali
* 2 = all HomeSpan status messages plus all HAP communication packets to and from the HomeSpan device, as well as all `LOG1()` and `LOG2()` messages specified in the sketch by the user * 2 = all HomeSpan status messages plus all HAP communication packets to and from the HomeSpan device, as well as all `LOG1()` and `LOG2()` messages specified in the sketch by the user
* note the log level can also be changed at runtime with the 'L' command via the [HomeSpan CLI](CLI.md) * note the log level can also be changed at runtime with the 'L' command via the [HomeSpan CLI](CLI.md)
* see [Message Logging](Logging.md) for complete details * see [Message Logging](Logging.md) for complete details
* `int getLogLevel()`
* returns the current Log Level as set by `setLogLevel(level)`
* `void reserveSocketConnections(uint8_t nSockets)` * `void reserveSocketConnections(uint8_t nSockets)`
* reserves *nSockets* network sockets for uses **other than** by the HomeSpan HAP Server for HomeKit Controller Connections * reserves *nSockets* network sockets for uses **other than** by the HomeSpan HAP Server for HomeKit Controller Connections
@ -115,8 +118,7 @@ The following **optional** `homeSpan` methods enable additional features and pro
* sets the SSID (*ssid*) and password (*pwd*) of the WiFi network to which HomeSpan will connect * sets the SSID (*ssid*) and password (*pwd*) of the WiFi network to which HomeSpan will connect
* *ssid* and *pwd* are automatically saved in HomeSpan's non-volatile storage (NVS) for retrieval when the device restarts * *ssid* and *pwd* are automatically saved in HomeSpan's non-volatile storage (NVS) for retrieval when the device restarts
* note that the saved values are truncated if they exceed the maximum allowable characters (ssid=32; pwd=64) * note that the saved values are truncated if they exceed the maximum allowable characters (ssid=32; pwd=64)
* :warning: SECURITY WARNING: The purpose of this function is to allow advanced users to *dynamically* set the device's WiFi Credentials using a customized Access Point function specified by `setApFunction(func)`. It it NOT recommended to use this function to hardcode your WiFi SSID and password directly into your sketch. Instead, use one of the more secure methods provided by HomeSpan, such as typing 'W' from the CLI, or launching HomeSpan's Access Point, to set your WiFi credentials without hardcoding them into your sketch
> :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)())` * `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 * 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");` * 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 * 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 * 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 \<code\>' from the CLI, or launching HomeSpan's Access Point, to set your Pairing Code without hardcoding it into your 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 \<code\>' 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
* `void setSketchVersion(const char *sVer)` * `void setSketchVersion(const char *sVer)`
* sets the version of a HomeSpan sketch to *sVer*, which can be any arbitrary character string * 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)` * `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 * 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)* ## *SpanAccessory(uint32_t aid)*
Creating an instance of this **class** adds a new HAP Accessory to the HomeSpan HAP Database. 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 * returns a pointer to the Characteristic itself so that the method can be chained during instantiation
* example: `(new Characteristic::RotationSpeed())->setUnit("percentage");` * 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. 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 * instantiating a Button without first instantiating a Service throws an error during initialization
The first argument is required; the rest are optional: 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) * *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) * *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) * *doubleTime* - the maximum time (in millis) allowed between two Single Presses to qualify as a Double Press (default=200 ms)
* *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:
Trigger Rules: * `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 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 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 singleTime>longTime, only Long Press triggers can occur
* If doubleTime=0, Double Presses cannot 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: #### Usage ####
* 0=single press (SpanButton::SINGLE) 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:
* 1=double press (SpanButton::DOUBLE) * 0=single press (`SpanButton::SINGLE`)
* 2=long press (SpanButton::LONG) * 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. 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])* ### *SpanUserCommand(char c, const char \*desc, void (\*f)(const char \*buf [,void \*obj]) [,void \*userObject])*

View File

@ -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 * 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 * 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 ## 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* 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) ### [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 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 [↩️](README.md) Back to the Welcome page

View File

@ -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 // 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. // 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 // 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 // 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. // 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 // 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. // 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) // 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) // 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) // 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) // 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 // When a SpanButton() is first instantiated, HomeSpan configures the specified pin in accordance with the triggerType chosen.
// 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.
// 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, // 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, // 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 // 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: // 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, // 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 // 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: // 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 // 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 // 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(); homeSpan.poll();
} // end of loop() } // 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.
//

View File

@ -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 // 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 // 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 // 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. // in the button() method below.

View File

@ -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 <array> // include the C++ standard library array container
std::array<int,10> 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',"<num> - add a new light accessory with id=<num>",addAccessory);
new SpanUserCommand('d',"<num> - delete a light accessory with id=<num>",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 <num>' 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 <num> specified
if(n<1){ // ensure <num> 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 <num>' into the CLI.
void deleteAccessory(const char *buf){
int n=atoi(buf+1); // same as above, we read the specified <num> 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");
}
///////////////////////////

View File

@ -1,9 +1,9 @@
name=HomeSpan name=HomeSpan
version=1.5.1 version=1.6.0
author=Gregg <homespan@icloud.com> author=Gregg <homespan@icloud.com>
maintainer=Gregg <homespan@icloud.com> maintainer=Gregg <homespan@icloud.com>
sentence=A robust and extremely easy-to-use HomeKit implementation for the Espressif ESP32 running on the Arduino IDE. 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 url=https://github.com/HomeSpan/HomeSpan
architectures=esp32 architectures=esp32
includes=HomeSpan.h includes=HomeSpan.h

View File

@ -78,6 +78,7 @@ struct HapChar {
struct HapCharacteristics { struct HapCharacteristics {
HAPCHAR( AccessoryFlags, A6, PR+EV, UINT32, true );
HAPCHAR( Active, B0, PW+PR+EV, UINT8, true ); HAPCHAR( Active, B0, PW+PR+EV, UINT8, true );
HAPCHAR( ActiveIdentifier, E7, PW+PR+EV, UINT32, true ); HAPCHAR( ActiveIdentifier, E7, PW+PR+EV, UINT32, true );
HAPCHAR( AirQuality, 95, PR+EV, UINT8, true ); HAPCHAR( AirQuality, 95, PR+EV, UINT8, true );
@ -113,7 +114,7 @@ struct HapCharacteristics {
HAPCHAR( CurrentVisibilityState, 135, PR+EV, UINT8, true ); HAPCHAR( CurrentVisibilityState, 135, PR+EV, UINT8, true );
HAPCHAR( FilterLifeLevel, AB, PR+EV, FLOAT, false ); HAPCHAR( FilterLifeLevel, AB, PR+EV, FLOAT, false );
HAPCHAR( FilterChangeIndication, AC, PR+EV, UINT8, true ); 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( HardwareRevision, 53, PR, STRING, true );
HAPCHAR( HeatingThresholdTemperature, 12, PR+PW+EV, FLOAT, false ); HAPCHAR( HeatingThresholdTemperature, 12, PR+PW+EV, FLOAT, false );
HAPCHAR( HoldPosition, 6F, PW, BOOL, true ); HAPCHAR( HoldPosition, 6F, PW, BOOL, true );

View File

@ -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 // Facilitates the testing of identical code on an ESP32, ESP32-S2, and ESP32-C3 using a common jig without rewiring
#pragma once #pragma once
#if defined(CONFIG_IDF_TARGET_ESP32) #if defined(ARDUINO_FEATHER_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}; 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 "" #define DEVICE_SUFFIX ""
#elif defined(CONFIG_IDF_TARGET_ESP32S2) #elif defined(ARDUINO_ESP32S2_DEV)
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}; 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" #define DEVICE_SUFFIX "-S2"
#elif defined(CONFIG_IDF_TARGET_ESP32C3) #elif defined(ARDUINO_ESP32C3_DEV)
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}; 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" #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 #endif

View File

@ -30,7 +30,6 @@
#include <MD5Builder.h> #include <MD5Builder.h>
#include "HAP.h" #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 if(!nvs_get_blob(hapNVS,"HAPHASH",NULL,&len)){ // if found HAP HASH structure
nvs_get_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,&len); // retrieve data nvs_get_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,&len); // retrieve data
} else { } else {
Serial.print("Resetting Accessory Configuration number...\n"); Serial.print("Resetting Database Hash...\n");
nvs_set_blob(hapNVS,"HAPHASH",&homeSpan.hapConfig,sizeof(homeSpan.hapConfig)); // update data 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 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"); Serial.print("\n");
uint8_t tHash[48];
TempBuffer <char> 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;i<homeSpan.Accessories.size();i++){ // identify all services with over-ridden loop() methods
for(int j=0;j<homeSpan.Accessories[i]->Services.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++; numIDs++;
char *ids[numIDs]; // reserve space for number of IDs found 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 numIDs=0; // reset number of IDs found
char *lastSpace=strchr(urlBuf,' '); 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"; String response="HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n";
response+="<html><head><title>HomeSpan Status</title>\n"; response+="<html><head><title>" + String(homeSpan.displayName) + "</title>\n";
response+="<style>th, td {padding-right: 10px; padding-left: 10px; border:1px solid black;}"; response+="<style>th, td {padding-right: 10px; padding-left: 10px; border:1px solid black;}";
response+="</style></head>\n"; response+="</style></head>\n";
response+="<body style=\"background-color:lightblue;\">\n"; response+="<body style=\"background-color:lightblue;\">\n";
@ -1286,6 +1260,10 @@ int HAPClient::getStatusURL(){
response+="<tr><td>Up Time:</td><td>" + String(uptime) + "</td></tr>\n"; response+="<tr><td>Up Time:</td><td>" + String(uptime) + "</td></tr>\n";
response+="<tr><td>Current Time:</td><td>" + String(clocktime) + "</td></tr>\n"; response+="<tr><td>Current Time:</td><td>" + String(clocktime) + "</td></tr>\n";
response+="<tr><td>Boot Time:</td><td>" + String(homeSpan.webLog.bootTime) + "</td></tr>\n"; response+="<tr><td>Boot Time:</td><td>" + String(homeSpan.webLog.bootTime) + "</td></tr>\n";
response+="<tr><td>Reset Reason Code:</td><td>" + String(esp_reset_reason()) + "</td></tr>\n";
response+="<tr><td>WiFi Disconnects:</td><td>" + String(homeSpan.connected/2) + "</td></tr>\n";
response+="<tr><td>WiFi Signal:</td><td>" + String(WiFi.RSSI()) + " dBm</td></tr>\n";
response+="<tr><td>WiFi Gateway:</td><td>" + WiFi.gatewayIP().toString() + "</td></tr>\n";
response+="<tr><td>ESP32 Board:</td><td>" + String(ARDUINO_BOARD) + "</td></tr>\n"; response+="<tr><td>ESP32 Board:</td><td>" + String(ARDUINO_BOARD) + "</td></tr>\n";
response+="<tr><td>Arduino-ESP Version:</td><td>" + String(ARDUINO_ESP_VERSION) + "</td></tr>\n"; response+="<tr><td>Arduino-ESP Version:</td><td>" + String(ARDUINO_ESP_VERSION) + "</td></tr>\n";
response+="<tr><td>ESP-IDF Version:</td><td>" + String(ESP_IDF_VERSION_MAJOR) + "." + String(ESP_IDF_VERSION_MINOR) + "." + String(ESP_IDF_VERSION_PATCH) + "</td></tr>\n"; response+="<tr><td>ESP-IDF Version:</td><td>" + String(ESP_IDF_VERSION_MAJOR) + "." + String(ESP_IDF_VERSION_MINOR) + "." + String(ESP_IDF_VERSION_PATCH) + "</td></tr>\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;i<homeSpan.Loops.size();i++) // loop over all services with over-ridden loop() methods
homeSpan.Loops[i]->loop(); // call the loop() method
}
//////////////////////////////////////
void HAPClient::checkPushButtons(){
for(int i=0;i<homeSpan.PushButtons.size();i++){ // loop over all defined pushbuttons
SpanButton *sb=homeSpan.PushButtons[i]; // temporary pointer to SpanButton
if(sb->pushButton->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(){ void HAPClient::checkNotifications(){
if(!homeSpan.Notifications.empty()){ // if there are Notifications to process 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 unsigned long cTime=millis(); // get current time

View File

@ -142,8 +142,6 @@ struct HAPClient {
static void removeControllers(); // removes all Controllers (sets allocated flags to false for all slots) 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 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 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 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 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 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

View File

@ -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 esp_task_wdt_delete(xTaskGetIdleTaskHandleForCPU(0)); // required to avoid watchdog timeout messages from ESP32-C3
controlButton.init(controlPin);
statusLED.init(statusPin,0,autoOffLED); statusLED.init(statusPin,0,autoOffLED);
if(requestedMaxCon<maxConnections) // if specific request for max connections is less than computed max connections if(requestedMaxCon<maxConnections) // if specific request for max connections is less than computed max connections
@ -105,8 +104,8 @@ void Span::begin(Category catID, const char *displayName, const char *hostNameBa
else else
Serial.print("- *** WARNING: Status LED Pin is UNDEFINED"); Serial.print("- *** WARNING: Status LED Pin is UNDEFINED");
Serial.print("\nDevice Control: Pin "); Serial.print("\nDevice Control: Pin ");
if(controlPin>=0) if(getControlPin()>=0)
Serial.print(controlPin); Serial.print(getControlPin());
else else
Serial.print("- *** WARNING: Device Control Pin is UNDEFINED"); Serial.print("- *** WARNING: Device Control Pin is UNDEFINED");
Serial.print("\nSketch Version: "); Serial.print("\nSketch Version: ");
@ -159,42 +158,29 @@ void Span::begin(Category catID, const char *displayName, const char *hostNameBa
void Span::poll() { 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)){ 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); while(1);
} }
if(!isInitialized){ 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 processSerialCommand("i"); // print homeSpan configuration info
if(nFatalErrors>0){ HAPClient::init(); // read NVS and load HAP settings
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
if(!strlen(network.wifiData.ssid)){ if(!strlen(network.wifiData.ssid)){
Serial.print("*** WIFI CREDENTIALS DATA NOT FOUND. "); Serial.print("*** WIFI CREDENTIALS DATA NOT FOUND. ");
@ -209,7 +195,8 @@ void Span::poll() {
homeSpan.statusLED.start(LED_WIFI_CONNECTING); homeSpan.statusLED.start(LED_WIFI_CONNECTING);
} }
controlButton.reset(); if(controlButton)
controlButton->reset();
Serial.print(displayName); Serial.print(displayName);
Serial.print(" is READY!\n\n"); Serial.print(" is READY!\n\n");
@ -221,10 +208,10 @@ void Span::poll() {
checkConnect(); checkConnect();
} }
char cBuf[17]="?"; char cBuf[65]="?";
if(Serial.available()){ if(Serial.available()){
readSerial(cBuf,16); readSerial(cBuf,64);
processSerialCommand(cBuf); processSerialCommand(cBuf);
} }
@ -289,22 +276,28 @@ void Span::poll() {
} // process HAP Client } // process HAP Client
} // for-loop over connection slots } // for-loop over connection slots
HAPClient::callServiceLoops(); snapTime=millis(); // snap the current time for use in ALL loop routines
HAPClient::checkPushButtons();
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::checkNotifications();
HAPClient::checkTimedWrites(); HAPClient::checkTimedWrites();
if(spanOTA.enabled) if(spanOTA.enabled)
ArduinoOTA.handle(); ArduinoOTA.handle();
if(controlButton.primed()){ if(controlButton && controlButton->primed()){
statusLED.start(LED_ALERT); statusLED.start(LED_ALERT);
} }
if(controlButton.triggered(3000,10000)){ if(controlButton && controlButton->triggered(3000,10000)){
statusLED.off(); statusLED.off();
if(controlButton.type()==PushButton::LONG){ if(controlButton->type()==PushButton::LONG){
controlButton.wait(); controlButton->wait();
processSerialCommand("F"); // FACTORY RESET processSerialCommand("F"); // FACTORY RESET
} else { } else {
commandMode(); // COMMAND MODE commandMode(); // COMMAND MODE
@ -348,8 +341,8 @@ void Span::commandMode(){
statusLED.start(LED_ALERT); statusLED.start(LED_ALERT);
delay(2000); delay(2000);
} else } else
if(controlButton.triggered(10,3000)){ if(controlButton->triggered(10,3000)){
if(controlButton.type()==PushButton::SINGLE){ if(controlButton->type()==PushButton::SINGLE){
mode++; mode++;
if(mode==6) if(mode==6)
mode=1; mode=1;
@ -361,7 +354,7 @@ void Span::commandMode(){
} // while } // while
statusLED.start(LED_ALERT); statusLED.start(LED_ALERT);
controlButton.wait(); controlButton->wait();
switch(mode){ switch(mode){
@ -401,12 +394,12 @@ void Span::commandMode(){
void Span::checkConnect(){ void Span::checkConnect(){
if(connected){ if(connected%2){
if(WiFi.status()==WL_CONNECTED) if(WiFi.status()==WL_CONNECTED)
return; return;
Serial.print("\n\n*** WiFi Connection Lost!\n"); // losing and re-establishing connection has not been tested addWebLog(true,"*** WiFi Connection Lost!"); // losing and re-establishing connection has not been tested
connected=false; connected++;
waitTime=60000; waitTime=60000;
alarmConnect=0; alarmConnect=0;
homeSpan.statusLED.start(LED_WIFI_CONNECTING); homeSpan.statusLED.start(LED_WIFI_CONNECTING);
@ -427,11 +420,7 @@ void Span::checkConnect(){
Serial.print(". You may type 'W <return>' to re-configure WiFi, or 'X <return>' to erase WiFi credentials. Will try connecting again in 60 seconds.\n\n"); Serial.print(". You may type 'W <return>' to re-configure WiFi, or 'X <return>' to erase WiFi credentials. Will try connecting again in 60 seconds.\n\n");
waitTime=60000; waitTime=60000;
} else { } else {
Serial.print("Trying to connect to "); addWebLog(true,"Trying to connect to %s. Waiting %d sec...",network.wifiData.ssid,waitTime/1000);
Serial.print(network.wifiData.ssid);
Serial.print(". Waiting ");
Serial.print(waitTime/1000);
Serial.print(" second(s) for response...\n");
WiFi.begin(network.wifiData.ssid,network.wifiData.pwd); WiFi.begin(network.wifiData.ssid,network.wifiData.pwd);
} }
@ -440,14 +429,18 @@ void Span::checkConnect(){
return; return;
} }
connected=true; if(!HAPClient::nAdminControllers())
statusLED.start(LED_PAIRING_NEEDED);
else
statusLED.on();
connected++;
Serial.print("Successfully connected to "); addWebLog(true,"WiFi Connected! IP Address = %s\n",WiFi.localIP().toString().c_str());
Serial.print(network.wifiData.ssid);
Serial.print("! IP Address: ");
Serial.print(WiFi.localIP());
Serial.print("\n");
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 char id[18]; // create string version of Accessory ID for MDNS broadcast
memcpy(id,HAPClient::accessory.ID,17); // copy ID bytes memcpy(id,HAPClient::accessory.ID,17); // copy ID bytes
id[17]='\0'; // add terminating null 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.onStart(spanOTA.start).onEnd(spanOTA.end).onProgress(spanOTA.progress).onError(spanOTA.error);
ArduinoOTA.begin(); ArduinoOTA.begin();
reserveSocketConnections(1);
Serial.print("Starting OTA Server: "); Serial.print("Starting OTA Server: ");
Serial.print(displayName); Serial.print(displayName);
Serial.print(" at "); Serial.print(" at ");
@ -568,12 +560,8 @@ void Span::checkConnect(){
Serial.print("\n"); Serial.print("\n");
if(!HAPClient::nAdminControllers()){ if(!HAPClient::nAdminControllers())
Serial.print("DEVICE NOT YET PAIRED -- PLEASE PAIR WITH HOMEKIT APP\n\n"); Serial.print("DEVICE NOT YET PAIRED -- PLEASE PAIR WITH HOMEKIT APP\n\n");
statusLED.start(LED_PAIRING_NEEDED);
} else {
statusLED.on();
}
if(wifiCallback) if(wifiCallback)
wifiCallback(); wifiCallback();
@ -912,14 +900,128 @@ void Span::processSerialCommand(const char *c){
} }
break; break;
case 'm': {
Serial.printf("Free Memory: %d bytes\n",heap_caps_get_free_size(MALLOC_CAP_DEFAULT));
}
break;
case 'i':{ case 'i':{
Serial.print("\n*** HomeSpan Info ***\n\n"); Serial.print("\n*** HomeSpan Info ***\n\n");
Serial.print(configLog); int nErrors=0;
Serial.print("\nConfigured as Bridge: "); int nWarnings=0;
Serial.print(homeSpan.isBridge?"YES":"NO");
Serial.print("\n\n"); unordered_set<uint32_t> 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 *> 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<<i))
Serial.printf("%s%s",(foundPerms++)?"+":"",pNames[i]);
}
if((*chr)->format!=FORMAT::STRING && (*chr)->format!=FORMAT::BOOL){
if((*chr)->validValues)
Serial.printf(", Valid Values=%s",(*chr)->validValues);
else if((*chr)->uvGet<double>((*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<double>((*chr)->value) < (*chr)->uvGet<double>((*chr)->minValue) || (*chr)->uvGet<double>((*chr)->value) > (*chr)->uvGet<double>((*chr)->maxValue)))
Serial.printf(" *** WARNING! Value of %llg is out of range [%llg,%llg] ***\n",(*chr)->uvGet<double>((*chr)->value),(*chr)->uvGet<double>((*chr)->minValue),(*chr)->uvGet<double>((*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[]="------------------------------"; char d[]="------------------------------";
Serial.printf("%-30s %8s %10s %s %s %s %s %s\n","Service","UUID","AID","IID","Update","Loop","Button","Linked Services"); 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");
} }
} }
Serial.print("\n*** End Info ***\n"); Serial.print("\n*** End Info ***\n\n");
} }
break; break;
@ -952,13 +1054,14 @@ void Span::processSerialCommand(const char *c){
Serial.print(" s - print connection status\n"); Serial.print(" s - print connection status\n");
Serial.print(" i - print summary information about the HAP Database\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(" 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("\n");
Serial.print(" W - configure WiFi Credentials and restart\n"); Serial.print(" W - configure WiFi Credentials and restart\n");
Serial.print(" X - delete WiFi Credentials and restart\n"); Serial.print(" X - delete WiFi Credentials and restart\n");
Serial.print(" S <code> - change the HomeKit Pairing Setup Code to <code>\n"); Serial.print(" S <code> - change the HomeKit Pairing Setup Code to <code>\n");
Serial.print(" Q <id> - change the HomeKit Setup ID for QR Codes to <id>\n"); Serial.print(" Q <id> - change the HomeKit Setup ID for QR Codes to <id>\n");
Serial.print(" O - change the OTA password\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("\n");
Serial.print(" V - delete value settings for all saved Characteristics\n"); Serial.print(" V - delete value settings for all saved Characteristics\n");
Serial.print(" U - unpair device by deleting all Controller data\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; int nBytes=0;
nBytes+=snprintf(cBuf,cBuf?64:0,"{\"accessories\":["); nBytes+=snprintf(cBuf,cBuf?64:0,"{\"accessories\":[");
for(int i=0;i<Accessories.size();i++){ for(int i=0;i<Accessories.size();i++){
nBytes+=Accessories[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL); nBytes+=Accessories[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL,flags);
if(i+1<Accessories.size()) if(i+1<Accessories.size())
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,","); nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",");
} }
@ -1076,6 +1179,21 @@ void Span::prettyPrint(char *buf, int nsp){
Serial.print("\n"); Serial.print("\n");
} // prettyPrint } // prettyPrint
///////////////////////////
boolean Span::deleteAccessory(uint32_t n){
auto it=homeSpan.Accessories.begin();
for(;it!=homeSpan.Accessories.end() && (*it)->aid!=n; it++);
if(it==homeSpan.Accessories.end())
return(false);
delete *it;
return(true);
}
/////////////////////////////// ///////////////////////////////
SpanCharacteristic *Span::find(uint32_t aid, int iid){ 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 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+=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; notifyFlag=true;
} // notification requested } // 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; uint8_t tHash[48];
homeSpan.configLog+="\nRange Check:"; TempBuffer <char> tBuf(sprintfAttributes(NULL,GET_META|GET_PERMS|GET_TYPE|GET_DESC)+1);
sprintfAttributes(tBuf.buf,GET_META|GET_PERMS|GET_TYPE|GET_DESC);
for(int i=0;i<Accessories.size();i++){ 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)
for(int j=0;j<Accessories[i]->Services.size();j++){
for(int k=0;k<Accessories[i]->Services[j]->Characteristics.size();k++){
SpanCharacteristic *chr=Accessories[i]->Services[j]->Characteristics[k];
if(chr->format!=STRING && (chr->uvGet<double>(chr->value) < chr->uvGet<double>(chr->minValue) || chr->uvGet<double>(chr->value) > chr->uvGet<double>(chr->maxValue))){ boolean changed=false;
char c[256];
sprintf(c,"\n \u2718 Characteristic %s with AID=%d, IID=%d: Initial value of %lg is out of range [%llg,%llg]", if(memcmp(tHash,hapConfig.hashCode,48)){ // if hash code of current HAP database does not match stored hash code
chr->hapName,chr->aid,chr->iid,chr->uvGet<double>(chr->value),chr->uvGet<double>(chr->minValue),chr->uvGet<double>(chr->maxValue)); memcpy(hapConfig.hashCode,tHash,48); // update stored hash code
if(okay) hapConfig.configNumber++; // increment configuration number
homeSpan.configLog+="\n"; if(hapConfig.configNumber==65536) // reached max value
homeSpan.configLog+=c; hapConfig.configNumber=1; // reset to 1
homeSpan.nWarnings++;
okay=false; 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) Loops.clear();
homeSpan.configLog+=" No Warnings";
homeSpan.configLog+="\n\n"; 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; this->aid=homeSpan.Accessories.back()->aid+1;
if(!homeSpan.Accessories.back()->Services.empty())
homeSpan.Accessories.back()->Services.back()->validate();
homeSpan.Accessories.back()->validate();
} else { } else {
this->aid=1; this->aid=1;
} }
@ -1424,55 +1547,31 @@ SpanAccessory::SpanAccessory(uint32_t aid){
this->aid=aid; this->aid=aid;
} }
homeSpan.configLog+="\u27a4 Accessory: AID=" + String(this->aid);
for(int i=0;i<homeSpan.Accessories.size()-1;i++){
if(this->aid==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;i<Services.size();i++){ auto acc=homeSpan.Accessories.begin(); // find Accessory in homeSpan vector and erase entry
if(!strcmp(Services[i]->type,"3E")) while((*acc)!=this)
foundInfo=true; acc++;
else if(aid==1) // this is an Accessory with aid=1, but it has more than just AccessoryInfo. So... homeSpan.Accessories.erase(acc);
homeSpan.isBridge=false; // ...this is not a bridge device LOG1("Deleted Accessory AID=%d\n",aid);
}
if(!foundInfo){
homeSpan.configLog+=" \u2718 Service AccessoryInformation";
homeSpan.configLog+=" *** ERROR! Required Service for this Accessory not found. ***\n";
homeSpan.nFatalErrors++;
}
} }
/////////////////////////////// ///////////////////////////////
int SpanAccessory::sprintfAttributes(char *cBuf){ int SpanAccessory::sprintfAttributes(char *cBuf, int flags){
int nBytes=0; int nBytes=0;
nBytes+=snprintf(cBuf,cBuf?64:0,"{\"aid\":%u,\"services\":[",aid); nBytes+=snprintf(cBuf,cBuf?64:0,"{\"aid\":%u,\"services\":[",aid);
for(int i=0;i<Services.size();i++){ for(int i=0;i<Services.size();i++){
nBytes+=Services[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL); nBytes+=Services[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL,flags);
if(i+1<Services.size()) if(i+1<Services.size())
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,","); nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",");
} }
@ -1488,38 +1587,51 @@ int SpanAccessory::sprintfAttributes(char *cBuf){
SpanService::SpanService(const char *type, const char *hapName, boolean isCustom){ SpanService::SpanService(const char *type, const char *hapName, boolean isCustom){
if(!homeSpan.Accessories.empty() && !homeSpan.Accessories.back()->Services.empty()) // this is not the first Service to be defined for this Accessory if(homeSpan.Accessories.empty()){
homeSpan.Accessories.back()->Services.back()->validate(); 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->type=type;
this->hapName=hapName; this->hapName=hapName;
this->isCustom=isCustom; 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); 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)){ SpanService::~SpanService(){
homeSpan.configLog+=" *** ERROR! Format of UUID is invalid. ***";
homeSpan.nFatalErrors++; 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){ auto pb=homeSpan.PushButtons.begin(); // loop through PushButton vector and delete ALL PushButtons associated with this Service
homeSpan.configLog+=" *** ERROR! The AccessoryInformation Service must be defined before any other Services in an Accessory. ***"; while(pb!=homeSpan.PushButtons.end()){
homeSpan.nFatalErrors++; 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; int nBytes=0;
nBytes+=snprintf(cBuf,cBuf?64:0,"{\"iid\":%d,\"type\":\"%s\",",iid,type); 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\":["); nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"characteristics\":[");
for(int i=0;i<Characteristics.size();i++){ for(int i=0;i<Characteristics.size();i++){
nBytes+=Characteristics[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL,GET_META|GET_PERMS|GET_TYPE|GET_DESC); nBytes+=Characteristics[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL,flags);
if(i+1<Characteristics.size()) if(i+1<Characteristics.size())
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,","); nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",");
} }
@ -1579,46 +1691,27 @@ int SpanService::sprintfAttributes(char *cBuf){
return(nBytes); return(nBytes);
} }
///////////////////////////////
void SpanService::validate(){
for(int i=0;i<req.size();i++){
boolean valid=false;
for(int j=0;!valid && j<Characteristics.size();j++)
valid=!strcmp(req[i]->type,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<HapChar *>().swap(opt);
vector<HapChar *>().swap(req);
}
/////////////////////////////// ///////////////////////////////
// SpanCharacteristic // // SpanCharacteristic //
/////////////////////////////// ///////////////////////////////
SpanCharacteristic::SpanCharacteristic(HapChar *hapChar, boolean isCustom){ 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; type=hapChar->type;
perms=hapChar->perms; perms=hapChar->perms;
hapName=hapChar->hapName; hapName=hapChar->hapName;
format=hapChar->format; format=hapChar->format;
staticRange=hapChar->staticRange; staticRange=hapChar->staticRange;
this->isCustom=isCustom; this->isCustom=isCustom;
this->hapChar=hapChar;
homeSpan.configLog+=" \u21e8 Characteristic " + String(hapName); homeSpan.Accessories.back()->Services.back()->Characteristics.push_back(this);
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;
}
iid=++(homeSpan.Accessories.back()->iidCount); iid=++(homeSpan.Accessories.back()->iidCount);
service=homeSpan.Accessories.back()->Services.back(); service=homeSpan.Accessories.back()->Services.back();
aid=homeSpan.Accessories.back()->aid; 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 SpanCharacteristic::sprintfAttributes(char *cBuf, int flags){
int nBytes=0; int nBytes=0;
@ -1640,7 +1756,7 @@ int SpanCharacteristic::sprintfAttributes(char *cBuf, int flags){
if(flags&GET_TYPE) if(flags&GET_TYPE)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"type\":\"%s\"",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)) if(perms&NV && !(flags&GET_NV))
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":null"); nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":null");
else else
@ -1814,35 +1930,26 @@ unsigned long SpanCharacteristic::timeVal(){
/////////////////////////////// ///////////////////////////////
SpanCharacteristic *SpanCharacteristic::setValidValues(int n, ...){ 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_list vl;
va_start(vl,n); va_start(vl,n);
for(int i=0;i<n;i++){ for(int i=0;i<n;i++){
*s+=va_arg(vl,int); s+=(uint8_t)va_arg(vl,int);
if(i!=n-1) if(i!=n-1)
*s+=","; s+=",";
} }
va_end(vl); va_end(vl);
*s+="]"; s+="]";
homeSpan.configLog+=String(" \u2b0c Set Valid Values for ") + String(hapName) + " with IID=" + String(iid); validValues=(char *)realloc(validValues, strlen(s.c_str()) + 1);
strcpy(validValues,s.c_str());
if(validValues){
sprintf(c," *** ERROR! Valid Values already set for this Characteristic! ***\n");
homeSpan.nFatalErrors++;
} else
if(format!=UINT8){
sprintf(c," *** ERROR! Can't set Valid Values for this Characteristic! ***\n");
homeSpan.nFatalErrors++;
} else {
validValues=s->c_str();
sprintf(c,": ValidValues=%s\n",validValues);
}
homeSpan.configLog+=c;
return(this); return(this);
} }
@ -1853,8 +1960,9 @@ SpanCharacteristic *SpanCharacteristic::setValidValues(int n, ...){
SpanRange::SpanRange(int min, int max, int step){ SpanRange::SpanRange(int min, int max, int step){
if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty() || homeSpan.Accessories.back()->Services.back()->Characteristics.empty() ){ 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"; Serial.printf("\nFATAL ERROR! Can't create new SpanRange(%d,%d,%d) without a defined Characteristic ***\n",min,max,step);
homeSpan.nFatalErrors++; Serial.printf("\n=== PROGRAM HALTED ===");
while(1);
} else { } else {
homeSpan.Accessories.back()->Services.back()->Characteristics.back()->setRange(min,max,step); 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::SpanButton(int pin, uint16_t longTime, uint16_t singleTime, uint16_t doubleTime){ SpanButton::SpanButton(int pin, uint16_t longTime, uint16_t singleTime, uint16_t doubleTime, triggerType_t triggerType) : PushButton(pin, triggerType){
homeSpan.configLog+=" \u25bc SpanButton: Pin=" + String(pin) + ", Single=" + String(singleTime) + "ms, Double=" + String(doubleTime) + "ms, Long=" + String(longTime) + "ms";
if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){ if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){
homeSpan.configLog+=" *** ERROR! Can't create new PushButton without a defined Service! ***\n"; Serial.printf("\nFATAL ERROR! Can't create new SpanButton(%d,%u,%u,%u) without a defined Service ***\n",pin,longTime,singleTime,doubleTime);
homeSpan.nFatalErrors++; Serial.printf("\n=== PROGRAM HALTED ===");
return; while(1);
} }
Serial.print("Configuring PushButton: Pin="); // initialization message
Serial.print(pin);
Serial.print("\n");
this->pin=pin;
this->longTime=longTime; this->longTime=longTime;
this->singleTime=singleTime; this->singleTime=singleTime;
this->doubleTime=doubleTime; this->doubleTime=doubleTime;
service=homeSpan.Accessories.back()->Services.back(); 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); 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 // // SpanUserCommand //
@ -1928,6 +2028,8 @@ void SpanWebLog::init(uint16_t maxEntries, const char *serv, const char *tz, con
timeZone=tz; timeZone=tz;
statusURL="GET /" + String(url) + " "; statusURL="GET /" + String(url) + " ";
log = (log_t *)calloc(maxEntries,sizeof(log_t)); log = (log_t *)calloc(maxEntries,sizeof(log_t));
if(timeServer)
homeSpan.reserveSocketConnections(1);
} }
/////////////////////////////// ///////////////////////////////
@ -1942,7 +2044,6 @@ void SpanWebLog::initTime(){
if(getLocalTime(&timeinfo,waitTime)){ if(getLocalTime(&timeinfo,waitTime)){
strftime(bootTime,sizeof(bootTime),"%c",&timeinfo); strftime(bootTime,sizeof(bootTime),"%c",&timeinfo);
Serial.printf("%s\n\n",bootTime); Serial.printf("%s\n\n",bootTime);
homeSpan.reserveSocketConnections(1);
timeInit=true; timeInit=true;
} else { } else {
Serial.printf("Can't access Time Server - time-keeping not initialized!\n\n"); 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, ...){ void SpanWebLog::vLog(boolean sysMsg, const char *fmt, va_list ap){
if(maxEntries==0)
return;
int index=nEntries%maxEntries; char *buf;
vasprintf(&buf,fmt,ap);
log[index].upTime=esp_timer_get_time(); if(sysMsg)
if(timeInit) Serial.printf("%s\n",buf);
getLocalTime(&log[index].clockTime,10); else if(homeSpan.logLevel>0)
else Serial.printf("WEBLOG: %s\n",buf);
log[index].clockTime.tm_year=0;
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); free(buf);
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);
} }
/////////////////////////////// ///////////////////////////////
@ -1984,6 +2089,7 @@ void SpanOTA::init(boolean _auth, boolean _safeLoad){
enabled=true; enabled=true;
safeLoad=_safeLoad; safeLoad=_safeLoad;
auth=_auth; auth=_auth;
homeSpan.reserveSocketConnections(1);
} }
/////////////////////////////// ///////////////////////////////
@ -2036,6 +2142,11 @@ void SpanOTA::error(ota_error_t err){
/////////////////////////////// ///////////////////////////////
void __attribute__((weak)) loop(){
}
///////////////////////////////
int SpanOTA::otaPercent; int SpanOTA::otaPercent;
boolean SpanOTA::safeLoad; boolean SpanOTA::safeLoad;
boolean SpanOTA::enabled=false; boolean SpanOTA::enabled=false;

View File

@ -36,6 +36,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <unordered_set>
#include <nvs.h> #include <nvs.h>
#include <ArduinoOTA.h> #include <ArduinoOTA.h>
@ -48,6 +49,7 @@
using std::vector; using std::vector;
using std::unordered_map; using std::unordered_map;
using std::unordered_set;
enum { enum {
GET_AID=1, GET_AID=1,
@ -57,7 +59,7 @@ enum {
GET_EV=16, GET_EV=16,
GET_DESC=32, GET_DESC=32,
GET_NV=64, GET_NV=64,
GET_ALL=255 GET_VALUE=128
}; };
/////////////////////////////// ///////////////////////////////
@ -75,7 +77,11 @@ struct SpanUserCommand;
extern Span homeSpan; extern Span homeSpan;
/////////////////////////////// #include "HAP.h"
////////////////////////////////////////////////////////
// INTERNAL HOMESPAN STRUCTURES - NOT FOR USER ACCESS //
////////////////////////////////////////////////////////
struct SpanPartition{ struct SpanPartition{
char magicCookie[32]; char magicCookie[32];
@ -104,7 +110,7 @@ struct SpanBuf{ // temporary storage buffer for us
struct SpanWebLog{ // optional web status/log data struct SpanWebLog{ // optional web status/log data
boolean isEnabled=false; // flag to inidicate WebLog has been enabled 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 int nEntries=0; // total cumulative number of log entries
const char *timeServer; // optional time server to use for acquiring clock time const char *timeServer; // optional time server to use for acquiring clock time
const char *timeZone; // optional time-zone specification 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 init(uint16_t maxEntries, const char *serv, const char *tz, const char *url);
void initTime(); 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); 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 *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 *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 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) 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() unsigned long snapTime; // current time (in millis) snapped before entering Service loops() or updates()
boolean isInitialized=false; // flag indicating HomeSpan has been initialized 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) 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 HapQR qrCode; // optional QR Code to use for pairing
const char *sketchVersion="n/a"; // version of the sketch 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 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 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 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 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 const char *defaultSetupCode=DEFAULT_SETUP_CODE; // Setup Code used for pairing
int statusPin=DEFAULT_STATUS_PIN; // pin for Status LED int statusPin=DEFAULT_STATUS_PIN; // pin for Status LED
uint16_t autoOffLED=0; // automatic turn-off duration (in seconds) 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 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 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 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 WiFiServer *hapServer; // pointer to the HAP Server connection
Blinker statusLED; // indicates HomeSpan status 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 Network network; // configures WiFi and Setup Code via either serial monitor or temporary Access Point
SpanWebLog webLog; // optional web status/log SpanWebLog webLog; // optional web status/log
TaskHandle_t pollTaskHandle = NULL; // optional task handle to use for poll() function
SpanOTA spanOTA; // manages OTA process 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 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<char, SpanUserCommand *> UserCommands; // map of pointers to all UserCommands unordered_map<char, SpanUserCommand *> 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, void begin(Category catID=DEFAULT_CATEGORY,
const char *displayName=DEFAULT_DISPLAY_NAME, const char *displayName=DEFAULT_DISPLAY_NAME,
const char *hostNameBase=DEFAULT_HOST_NAME, const char *hostNameBase=DEFAULT_HOST_NAME,
const char *modelName=DEFAULT_MODEL_NAME); const char *modelName=DEFAULT_MODEL_NAME);
void poll(); // poll HAP Clients and process any new HAP requests void poll(); // calls pollTask() with some error checking
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 processSerialCommand(const char *c); // process command 'c' (typically from readSerial, though can be called with any 'c') 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 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
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 boolean deleteAccessory(uint32_t aid); // deletes Accessory with matching aid; returns true if found, else returns false
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 void setControlPin(uint8_t pin){controlButton=new PushButton(pin);} // sets Control Pin
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 setStatusPin(uint8_t pin){statusPin=pin;} // sets Status Pin void setStatusPin(uint8_t pin){statusPin=pin;} // sets Status Pin
void setStatusAutoOff(uint16_t duration){autoOffLED=duration;} // sets Status LED auto off (seconds) void setStatusAutoOff(uint16_t duration){autoOffLED=duration;} // sets Status LED auto off (seconds)
int getStatusPin(){return(statusPin);} // get Status Pin 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 setApSSID(const char *ssid){network.apSSID=ssid;} // sets Access Point SSID
void setApPassword(const char *pwd){network.apPassword=pwd;} // sets Access Point Password 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 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 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) 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 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 setHostNameSuffix(const char *suffix){hostNameSuffix=suffix;} // sets the hostName suffix to be used instead of the 6-byte AccessoryID
void setPortNum(uint16_t port){tcpPortNum=port;} // sets the TCP port number to use for communications between HomeKit and HomeSpan void 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); 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 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.")]] [[deprecated("Please use reserveSocketConnections(n) method instead.")]]
void setMaxConnections(uint8_t n){requestedMaxCon=n;} // sets maximum number of simultaneous HAP connections 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) 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 int iidCount=0; // running count of iid to use for Services and Characteristics associated with this Accessory
vector<SpanService *> Services; // vector of pointers to all Services in this Accessory vector<SpanService *> 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 protected:
void validate(); // error-checks Accessory
~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) int iid=0; // Instance ID (HAP Table 6-2)
const char *type; // Service Type const char *type; // Service Type
@ -294,29 +341,37 @@ struct SpanService{
boolean hidden=false; // optional property indicating service is hidden boolean hidden=false; // optional property indicating service is hidden
boolean primary=false; // optional property indicating service is primary boolean primary=false; // optional property indicating service is primary
vector<SpanCharacteristic *> Characteristics; // vector of pointers to all Characteristics in this Service vector<SpanCharacteristic *> Characteristics; // vector of pointers to all Characteristics in this Service
vector<HapChar *> req; // vector of pointers to all required HAP Characteristic Types for this Service
vector<HapChar *> opt; // vector of pointers to all optional HAP Characteristic Types for this Service
vector<SpanService *> linkedServices; // vector of pointers to any optional linked Services vector<SpanService *> linkedServices; // vector of pointers to any optional linked Services
boolean isCustom; // flag to indicate this is a Custom Service 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<HapChar *> req; // unordered set of pointers to all required HAP Characteristic Types for this Service
unordered_set<HapChar *> 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(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<SpanService *> 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<SpanService *> 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 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 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 { union UVal {
BOOL_t BOOL; BOOL_t BOOL;
@ -330,6 +385,7 @@ struct SpanCharacteristic{
}; };
int iid=0; // Instance ID (HAP Table 6-3) int iid=0; // Instance ID (HAP Table 6-3)
HapChar *hapChar; // pointer to HAP Characteristic structure
const char *type; // Characteristic Type const char *type; // Characteristic Type
const char *hapName; // HAP Name const char *hapName; // HAP Name
UVal value; // Characteristic Value UVal value; // Characteristic Value
@ -342,26 +398,21 @@ struct SpanCharacteristic{
UVal stepValue; // Characteristic step size (not applicable for STRING) 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 staticRange; // Flag that indicates whether Range is static and cannot be changed with setRange()
boolean customRange=false; // Flag for custom ranges 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) boolean *ev; // Characteristic Event Notify Enable (per-connection)
char *nvsKey=NULL; // key for NVS storage of Characteristic value char *nvsKey=NULL; // key for NVS storage of Characteristic value
boolean isCustom; // flag to indicate this is a Custom Characteristic 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 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 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() 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 UVal newValue; // the updated value requested by PUT /characteristic
SpanService *service=NULL; // pointer to Service containing this 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 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.) 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.)
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
String uvPrint(UVal &u){ String uvPrint(UVal &u){
char c[64]; char c[64];
@ -448,35 +499,9 @@ struct SpanCharacteristic{
return(0); // included to prevent compiler warnings return(0); // included to prevent compiler warnings
} }
template <typename A, typename B, typename S=int> SpanCharacteristic *setRange(A min, B max, S step=0){ protected:
char c[256]; ~SpanCharacteristic(); // destructor
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<double>(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()
template <typename T, typename A=boolean, typename B=boolean> void init(T val, boolean nvsStore, A min=0, B max=1){ template <typename T, typename A=boolean, typename B=boolean> void init(T val, boolean nvsStore, A min=0, B max=1){
@ -522,50 +547,12 @@ struct SpanCharacteristic{
uvSet(maxValue,max); uvSet(maxValue,max);
uvSet(stepValue,0); 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 && i<homeSpan.Accessories.back()->Services.back()->req.size(); i++)
valid=!strcmp(type,homeSpan.Accessories.back()->Services.back()->req[i]->type);
for(int i=0; !valid && i<homeSpan.Accessories.back()->Services.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 && i<homeSpan.Accessories.back()->Services.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() } // init()
public:
SpanCharacteristic(HapChar *hapChar, boolean isCustom=false); // constructor
template <class T=int> T getVal(){ template <class T=int> T getVal(){
return(uvGet<T>(value)); return(uvGet<T>(value));
@ -592,7 +579,7 @@ struct SpanCharacteristic{
void setString(const char *val){ void setString(const char *val){
if((perms & EV) == 0){ 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; return;
} }
@ -648,26 +635,29 @@ struct SpanCharacteristic{
} // setVal() } // 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 <typename A, typename B, typename S=int> 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){ SpanCharacteristic *setPerms(uint8_t perms){
this->perms=perms; perms&=0x7F;
homeSpan.configLog+=String(" \u2b0c Change Permissions for ") + String(hapName) + " with AID=" + String(aid) + ", IID=" + String(iid) + ":"; if(perms>0)
this->perms=perms;
char pNames[][7]={"PR","PW","EV","AA","TW","HD","WR"};
char sep=' ';
for(uint8_t i=0;i<7;i++){
if(perms & (1<<i)){
homeSpan.configLog+=String(sep) + String(pNames[i]);
sep='+';
}
}
if(perms==0){
homeSpan.configLog+=" *** ERROR! Undefined Permissions! ***";
homeSpan.nFatalErrors++;
}
homeSpan.configLog+="\n";
return(this); 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 { enum {
SINGLE=0, SINGLE=0,
DOUBLE=1, DOUBLE=1,
LONG=2 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 SpanButton(int pin, uint16_t longTime=2000, uint16_t singleTime=5, uint16_t doubleTime=200, triggerType_t triggerType=TRIGGER_ON_LOW);
uint16_t singleTime; // minimum time (in millis) required to register a single press SpanButton(int pin, triggerType_t triggerType, uint16_t longTime=2000, uint16_t singleTime=5, uint16_t doubleTime=200) : SpanButton(pin,longTime,singleTime,doubleTime,triggerType){};
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
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 const char *s; // description of command
void (*userFunction1)(const char *v)=NULL; // user-defined function to call 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 (*userFunction2)(const char *v, void *arg)=NULL; // user-defined function to call with user-defined arg
void *userArg; void *userArg;
public:
SpanUserCommand(char c, const char *s, void (*f)(const char *)); SpanUserCommand(char c, const char *s, void (*f)(const char *));
SpanUserCommand(char c, const char *s, void (*f)(const char *, void *), void *arg); SpanUserCommand(char c, const char *s, void (*f)(const char *, void *), void *arg);
}; };

View File

@ -152,10 +152,10 @@ void Network::apConfigure(){
while(1){ // loop until we get timed out (which will be accelerated if save/cancel selected) 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."); Serial.print("\n*** Access Point Terminated.");
homeSpan.statusLED.start(LED_ALERT); homeSpan.statusLED.start(LED_ALERT);
homeSpan.controlButton.wait(); homeSpan.controlButton->wait();
Serial.print(" Restarting... \n\n"); Serial.print(" Restarting... \n\n");
homeSpan.statusLED.off(); homeSpan.statusLED.off();
ESP.restart(); ESP.restart();

View File

@ -35,8 +35,8 @@
// HomeSpan Version // // HomeSpan Version //
#define HS_MAJOR 1 #define HS_MAJOR 1
#define HS_MINOR 5 #define HS_MINOR 6
#define HS_PATCH 1 #define HS_PATCH 0
#define STRINGIFY(x) _STR(x) #define STRINGIFY(x) _STR(x)
#define _STR(x) #x #define _STR(x) #x
@ -55,6 +55,10 @@
#define ARDUINO_ESP_VERSION STRINGIFY(ARDUINO_ESP32_GIT_DESC) #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 // // DEFAULT SETTINGS //
@ -67,7 +71,6 @@
#define DEFAULT_QR_ID "HSPN" // change with homeSpan.setQRID(qrID); #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_STATUS_PIN -1 // change with homeSpan.setStatusPin(pin)
#define DEFAULT_AP_SSID "HomeSpan-Setup" // change with homeSpan.setApSSID(ssid) #define DEFAULT_AP_SSID "HomeSpan-Setup" // change with homeSpan.setApSSID(ssid)
@ -104,10 +107,10 @@
// 0=Minimal, 1=Informative, 2=All // // 0=Minimal, 1=Informative, 2=All //
#define LOG0(format,...) Serial.print ##__VA_OPT__(f)(format __VA_OPT__(,) __VA_ARGS__) #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 LOG1(format,...) if(homeSpan.getLogLevel()>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 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 // // Types of Accessory Categories //

View File

@ -31,8 +31,8 @@
// Macros to define vectors of required and optional characteristics for each Span Service structure // Macros to define vectors of required and optional characteristics for each Span Service structure
#define REQ(HAPCHAR) req.push_back(&hapChars.HAPCHAR) #define REQ(HAPCHAR) req.insert(&hapChars.HAPCHAR)
#define OPT(HAPCHAR) opt.push_back(&hapChars.HAPCHAR) #define OPT(HAPCHAR) opt.insert(&hapChars.HAPCHAR)
namespace Service { namespace Service {
@ -43,7 +43,8 @@ namespace Service {
OPT(Model); OPT(Model);
OPT(Name); OPT(Name);
OPT(SerialNumber); OPT(SerialNumber);
OPT(HardwareRevision); OPT(HardwareRevision);
OPT(AccessoryFlags);
}}; }};
struct AirPurifier : SpanService { AirPurifier() : SpanService{"BB","AirPurifier"}{ struct AirPurifier : SpanService { AirPurifier() : SpanService{"BB","AirPurifier"}{
@ -403,6 +404,7 @@ namespace Service {
namespace Characteristic { namespace Characteristic {
CREATE_CHAR(uint32_t,AccessoryFlags,1,1,1);
CREATE_CHAR(uint8_t,Active,0,0,1); CREATE_CHAR(uint8_t,Active,0,0,1);
CREATE_CHAR(uint32_t,ActiveIdentifier,0,0,255); CREATE_CHAR(uint32_t,ActiveIdentifier,0,0,255);
CREATE_CHAR(uint8_t,AirQuality,0,0,5); CREATE_CHAR(uint8_t,AirQuality,0,0,5);

View File

@ -85,34 +85,39 @@ String Utils::mask(char *c, int n){
// PushButton // // PushButton //
//////////////////////////////// ////////////////////////////////
PushButton::PushButton(){} PushButton::PushButton(int pin, triggerType_t triggerType){
//////////////////////////////////////
PushButton::PushButton(int pin){
init(pin);
}
//////////////////////////////////////
void PushButton::init(int pin){
this->pin=pin; this->pin=pin;
if(pin<0) this->triggerType=triggerType;
return;
status=0; status=0;
doubleCheck=false; 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<calibCount;i++)
threshold+=touchRead(pin);
threshold/=calibCount;
#if SOC_TOUCH_VERSION_1
threshold/=2;
Serial.printf("Touch Sensor at pin=%d used for calibration. Triggers when sensor reading < %d.\n",pin,threshold);
#elif SOC_TOUCH_VERSION_2
threshold*=2;
Serial.printf("Touch Sensor at pin=%d used for calibration. Triggers when sensor reading > %d.\n",pin,threshold);
#endif
}
#endif
} }
////////////////////////////////////// //////////////////////////////////////
boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t doubleTime){ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t doubleTime){
if(pin<0)
return(false);
unsigned long cTime=millis(); unsigned long cTime=millis();
switch(status){ switch(status){
@ -124,7 +129,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d
return(true); return(true);
} }
if(!digitalRead(pin)){ // button is pressed if(triggerType(pin)){ // button is "pressed"
singleAlarm=cTime+singleTime; singleAlarm=cTime+singleTime;
if(!doubleCheck){ if(!doubleCheck){
status=1; status=1;
@ -138,7 +143,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d
case 1: case 1:
case 2: case 2:
if(digitalRead(pin)){ // button is released if(!triggerType(pin)){ // button is released
status=0; status=0;
if(cTime>singleAlarm){ if(cTime>singleAlarm){
doubleCheck=true; doubleCheck=true;
@ -154,7 +159,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d
break; break;
case 3: 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; status=0;
else if(cTime>longAlarm){ else if(cTime>longAlarm){
longAlarm=cTime+longTime; longAlarm=cTime+longTime;
@ -164,7 +169,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d
break; break;
case 4: case 4:
if(digitalRead(pin)){ // button is released if(!triggerType(pin)){ // button is released
status=0; status=0;
} else } else
@ -177,7 +182,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d
break; break;
case 5: case 5:
if(digitalRead(pin)) // button has been released after double-click if(!triggerType(pin)) // button has been released after double-click
status=0; status=0;
break; break;
@ -189,10 +194,7 @@ boolean PushButton::triggered(uint16_t singleTime, uint16_t longTime, uint16_t d
////////////////////////////////////// //////////////////////////////////////
boolean PushButton::primed(){ boolean PushButton::primed(){
if(pin<0)
return(false);
if(millis()>singleAlarm && status==1){ if(millis()>singleAlarm && status==1){
status=2; status=2;
return(true); return(true);
@ -209,12 +211,8 @@ int PushButton::type(){
////////////////////////////////////// //////////////////////////////////////
void PushButton::wait(){ void PushButton::wait(){
while(triggerType(pin));
if(pin<0)
return;
while(!digitalRead(pin));
} }
////////////////////////////////////// //////////////////////////////////////
@ -223,6 +221,12 @@ void PushButton::reset(){
status=0; status=0;
} }
//////////////////////////////////////
#if SOC_TOUCH_SENSOR_NUM > 0
touch_value_t PushButton::threshold=0;
#endif
//////////////////////////////// ////////////////////////////////
// Blinker // // Blinker //
//////////////////////////////// ////////////////////////////////

View File

@ -74,13 +74,24 @@ struct TempBuffer {
class PushButton{ class PushButton{
int status; int status;
int pin;
boolean doubleCheck; boolean doubleCheck;
uint32_t singleAlarm; uint32_t singleAlarm;
uint32_t doubleAlarm; uint32_t doubleAlarm;
uint32_t longAlarm; uint32_t longAlarm;
int pressType; 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: public:
enum { enum {
@ -88,28 +99,27 @@ class PushButton{
DOUBLE=1, DOUBLE=1,
LONG=2 LONG=2
}; };
PushButton();
PushButton(int pin);
// Creates generic pushbutton functionality on specified pin static boolean TRIGGER_ON_LOW(int pin){return(!digitalRead(pin));}
// that is wired to connect to ground when the button is pressed. static boolean TRIGGER_ON_HIGH(int pin){return(digitalRead(pin));}
//
// 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
void init(int pin); #if SOC_TOUCH_VERSION_1 // ESP32
static boolean TRIGGER_ON_TOUCH(int pin){return(touchRead(pin)<threshold);}
// Initializes PushButton, if not configured during instantiation. #elif SOC_TOUCH_VERSION_2 // ESP32S2 ESP32S3
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(); void reset();
// Resets state of PushButton. Should be called once before any loops that will // 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 // 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
}; };
//////////////////////////////// ////////////////////////////////

View File

@ -31,7 +31,7 @@
RFControl::RFControl(uint8_t pin, boolean refClock, boolean installDriver){ 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){ if(nChannels==RMT_CHANNEL_MAX/2){
#else #else
if(nChannels==RMT_CHANNEL_MAX){ if(nChannels==RMT_CHANNEL_MAX){
@ -52,6 +52,7 @@ RFControl::RFControl(uint8_t pin, boolean refClock, boolean installDriver){
config->mem_block_num=1; config->mem_block_num=1;
config->gpio_num=(gpio_num_t)pin; config->gpio_num=(gpio_num_t)pin;
config->tx_config.idle_output_en=false; config->tx_config.idle_output_en=false;
config->tx_config.idle_level=RMT_IDLE_LEVEL_LOW;
config->tx_config.loop_en=false; config->tx_config.loop_en=false;
rmt_config(config); rmt_config(config);
@ -64,8 +65,8 @@ RFControl::RFControl(uint8_t pin, boolean refClock, boolean installDriver){
this->refClock=refClock; this->refClock=refClock;
if(refClock) if(refClock)
#ifdef CONFIG_IDF_TARGET_ESP32C3 #ifdef RMT_SYS_CONF_REG
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 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 #else
rmt_set_source_clk(config->channel,RMT_BASECLK_REF); // use 1 MHz REF Tick Clock for ESP32 and ESP32-S2 rmt_set_source_clk(config->channel,RMT_BASECLK_REF); // use 1 MHz REF Tick Clock for ESP32 and ESP32-S2
#endif #endif

View File

@ -13,9 +13,9 @@ void setup() {
Serial.begin(115200); Serial.begin(115200);
homeSpan.setLogLevel(2); // homeSpan.setLogLevel(2);
// homeSpan.setStatusPin(13); // homeSpan.setStatusPin(13);
// homeSpan.setControlPin(33); homeSpan.setControlPin(33);
homeSpan.setHostNameSuffix("-lamp1"); homeSpan.setHostNameSuffix("-lamp1");
homeSpan.setPortNum(1201); homeSpan.setPortNum(1201);