commit
84cff6e877
|
|
@ -193,7 +193,7 @@ void setup() {
|
|||
new Characteristic::TargetVisibilityState(0);
|
||||
|
||||
SpanService *hdmi10 = new Service::InputSource();
|
||||
new Characteristic::ConfiguredNameStatic("HDMI 10"); // Source Name is static and cannot be edited in Settings Screen
|
||||
(new Characteristic::ConfiguredName("HDMI 10"))->removePerms(PW); // Source Name permissions changed and now cannot be edited in Settings Screen
|
||||
new Characteristic::Identifier(10);
|
||||
new Characteristic::IsConfigured(1); // Source included in the Settings Screen...
|
||||
new Characteristic::CurrentVisibilityState(0); // ...and included in the Selection List...
|
||||
|
|
|
|||
|
|
@ -41,13 +41,20 @@ HomeSpan is fully compatible with both Versions 1 and 2 of the [Arduino-ESP32 Bo
|
|||
* Launch the WiFi Access Point
|
||||
* A standalone, detailed End-User Guide
|
||||
|
||||
## ❗Latest Update - HomeSpan 1.4.1 (10/31/2021)
|
||||
## ❗Latest Update - HomeSpan 1.4.2 (11/27/2021)
|
||||
|
||||
* **Television Services and Characteristics have been added to HomeSpan!** See [HomeSpan Television Services](https://github.com/HomeSpan/HomeSpan/blob/master/docs/TVServices.md) for complete details
|
||||
* **Updated for compatability with Arduino-ESP32 Board Manager 2.0.1**
|
||||
* Maintains backward compatability with all previous versions
|
||||
|
||||
* **The RFControl library has been updated to allow for the generation of a modulating carrier wave suitable for controlling an infrared LED.** This allows you to create HomeKit-enabled TV remote controls with HomeSpan. See [HomeSpan Projects](https://github.com/topics/homespan) for some real-world examples!
|
||||
* **Some new methods and options for advance-use circumstances:**
|
||||
|
||||
* **User-defined Custom Characteristics can be added to HomeSpan with a new macro.** See the [HomeSpan API](https://github.com/HomeSpan/HomeSpan/blob/master/docs/Reference.md#define-custom_charnameuuidpermsformatdefaultvalueminvaluemaxvaluestaticrange) for details (for *advanced* users only)
|
||||
* Added *optional* second argument to the `setVal()` method that allows the value of a Characteristic to be updated *without* sending notification messages to HomeKit. Useful for keeping track of duration time when implementing a Sprinkler System - see [HomeSpan Reference Sprinkler](https://github.com/HomeSpan/HomeSpanReferenceSketches/tree/main/ReferenceSprinklers) for an example
|
||||
|
||||
* Added `getLinks()` as a new method to SpanService. Returns a vector of pointers to SpanServices that have been linked to another Service with the addLink() method. Useful for looping over all linked services, such as checking all valves in a Shower System - see [HomeSpan Reference Shower](https://github.com/HomeSpan/HomeSpanReferenceSketches/tree/main/ReferenceShower) or an example
|
||||
|
||||
* Added `setPerms()`, `addPerms()`, and `removePerms()` as new methods to SpanCharacteristic. Allows the user to modify (set/add/remove) the default permissions for any Characteristic. Useful for adding/deleting write-permissions for certain Characteristics
|
||||
|
||||
* Added `setPairingCode()` method to the global homeSpan object that allows for programmatically configuring the Pairing Setup Code inside your sketch. See the HomeSpan API for important security considerations when using this function!
|
||||
|
||||
See [Releases](https://github.com/HomeSpan/HomeSpan/releases) for details on all changes included in this update.
|
||||
|
||||
|
|
|
|||
|
|
@ -106,11 +106,18 @@ The following **optional** `homeSpan` methods enable additional features and pro
|
|||
* *ssid* and *pwd* are automatically saved in HomeSpan's non-volatile storage (NVS) for retrieval when the device restarts
|
||||
* note that the saved values are truncated if they exceed the maximum allowable characters (ssid=32; pwd=64)
|
||||
|
||||
> :warning: SECURITY WARNING: The purpose of this function is to allow advanced users to *dynamically* set the device's WiFi Credentials using a customized Access Point function specified by `setApFunction(func)`. It it NOT recommended to use this function to hardcode your WiFi SSID and password directly into your sketch. Instead, use one of the more secure methods provided by HomeSpan, such as typing 'W' from the CLI, or launching HomeSpan's Access Point by typing 'A' from the CLI, to set your WiFi credentials without hardcoding them into your sketch.
|
||||
> :warning: SECURITY WARNING: The purpose of this function is to allow advanced users to *dynamically* set the device's WiFi Credentials using a customized Access Point function specified by `setApFunction(func)`. It it NOT recommended to use this function to hardcode your WiFi SSID and password directly into your sketch. Instead, use one of the more secure methods provided by HomeSpan, such as typing 'W' from the CLI, or launching HomeSpan's Access Point, to set your WiFi credentials without hardcoding them into your sketch
|
||||
|
||||
* `void setWifiCallback(void (*func)())`
|
||||
* Sets an optional user-defined callback function, *func*, to be called by HomeSpan upon start-up just after WiFi connectivity has been established. This one-time call to *func* is provided for users that are implementing other network-related services as part of their sketch, but that cannot be started until WiFi connectivity is established. The function *func* must be of type *void* and have no arguments
|
||||
|
||||
* `void setPairingCode(const char *s)`
|
||||
* sets the Setup Pairing Code to *s*, which **must** be exactly eight numerical digits (no dashes)
|
||||
* example: `homeSpan.setPairingCode("46637726");`
|
||||
* a hashed version of the Pairing Code will be saved to the device's non-volatile storage, overwriting any currently-stored Pairing Code
|
||||
* if *s* contains an invalid code, an error will be reported and the code will *not* be saved. Instead, the currently-stored Pairing Code (or the HomeSpan default Pairing Code if no code has been stored) will be used
|
||||
* :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
|
||||
|
||||
* `void setSketchVersion(const char *sVer)`
|
||||
* sets the version of a HomeSpan sketch to *sVer*, which can be any arbitrary character string
|
||||
* if unspecified, HomeSpan uses "n/a" as the default version text
|
||||
|
|
@ -160,6 +167,12 @@ The following methods are supported:
|
|||
* note that Linked Services are only applicable for select HAP Services. See Apple's [HAP-R2](https://developer.apple.com/support/homekit-accessory-protocol/) documentation for full details.
|
||||
* example: `(new Service::Faucet)->addLink(new Service::Valve)->addLink(new Service::Valve);` (links two Valves to a Faucet)
|
||||
|
||||
* `vector<SpanService *> getLinks()`
|
||||
* returns a vector of pointers to Services that were added using `addLink()`
|
||||
* useful for creating loops that iterate over all linked Services
|
||||
* note that the returned vector points to generic SpanServices, which should be re-cast as needed
|
||||
* example: `for(auto myValve : faucet::getLinks()) { if((MyValve *)myValve)->active->getVal()) ... }` checks all Valves linked to to a Faucet
|
||||
|
||||
* `virtual boolean update()`
|
||||
* HomeSpan calls this method upon receiving a request from a HomeKit Controller to update one or more Characteristics associated with the Service. Users should override this method with code that implements that requested updates using one or more of the SpanCharacteristic methods below. Method **must** return *true* if update succeeds, or *false* if not.
|
||||
|
||||
|
|
@ -201,8 +214,8 @@ The following methods are supported:
|
|||
* `boolean updated()`
|
||||
* returns *true* if a HomeKit Controller has requested an update to the value of the Characteristic, otherwise *false*. The requested value itself can retrieved with `getNewVal<>()`
|
||||
|
||||
* `void setVal(value)`
|
||||
* sets the value of the Characteristic to *value*, and notifies all HomeKit Controllers of the change
|
||||
* `void setVal(value [,boolean notify])`
|
||||
* sets the value of the Characteristic to *value*, and, if *notify* is set to true, notifies all HomeKit Controllers of the change. The *notify* flag is optional and will be set to true if not specified. Setting the *notify* flag to false allows you to update a Characateristic without notifying any HomeKit Controllers, which is useful for Characteristics that HomeKit automatically adjusts (such as a countdown timer) but will be requested from the Accessory if the Home App closes and is then re-opened
|
||||
* works with any integer, boolean, or floating-based numerical *value*, though HomeSpan will convert *value* into the appropriate type for each Characteristic (e.g. calling `setValue(5.5)` on an integer-based Characteristic results in *value*=5)
|
||||
* throws a runtime warning if *value* is outside of the min/max range for the Characteristic, where min/max is either the HAP default, or any new min/max range set via a prior call to `setRange()`
|
||||
* *value* is **not** restricted to being an increment of the step size; for example it is perfectly valid to call `setVal(43.5)` after calling `setRange(0,100,5)` on a floating-based Characteristic even though 43.5 does does not align with the step size specified. The Home App will properly retain the value as 43.5, though it will round to the nearest step size increment (in this case 45) when used in a slider graphic (such as setting the temperature of a thermostat)
|
||||
|
|
@ -228,6 +241,22 @@ The following methods are supported:
|
|||
* returns a pointer to the Characteristic itself so that the method can be chained during instantiation
|
||||
* example: `(new Characteristic::SecuritySystemTargetState())->setValidValues(3,0,1,3);` creates a new Valid Value list of length=3 containing the values 0, 1, and 3. This has the effect of informing HomeKit that a SecuritySystemTargetState value of 2 (Night Arm) is not valid and should not be shown as a choice in the Home App
|
||||
|
||||
* `SpanCharacteristic *setPerms(uint8_t perms)`
|
||||
* changes the default permissions for a Characteristic to *perms*, where *perms* is an additive list of permissions as described in HAP-R2 Table 6-4. Valid values are PR, PW, EV, AA, TW, HD, and WR
|
||||
* returns a pointer to the Characteristic itself so that the method can be chained during instantiation
|
||||
* example: `(new Characteristic::IsConfigured(1))->setPerms(PW+PR+EV);`
|
||||
|
||||
* `SpanCharacteristic *addPerms(uint8_t perms)`
|
||||
* adds new permissions, *perms*, to the default permissions for a Characteristic, where *perms* is an additive list of permissions as described in HAP-R2 Table 6-4. Valid values are PR, PW, EV, AA, TW, HD, and WR
|
||||
* returns a pointer to the Characteristic itself so that the method can be chained during instantiation
|
||||
* example: `(new Characteristic::IsConfigured(1))->addPerms(PW);`
|
||||
|
||||
* `SpanCharacteristic *removePerms(uint8_t perms)`
|
||||
* removes permissions, *perms*, from the default permissions of a Characteristic, where *perms* is an additive list of permissions as described in HAP-R2 Table 6-4. Valid values are PR, PW, EV, AA, TW, HD, and WR
|
||||
* returns a pointer to the Characteristic itself so that the method can be chained during instantiation
|
||||
* example: `(new Characteristic::ConfiguredName("HDMI 1"))->removePerms(PW);`
|
||||
|
||||
|
||||
## *SpanButton(int pin, uint16_t longTime, uint16_t singleTime, uint16_t doubleTime)*
|
||||
|
||||
Creating an instance of this **class** attaches a pushbutton handler to the ESP32 *pin* specified.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
name=HomeSpan
|
||||
version=1.4.1
|
||||
version=1.4.2
|
||||
author=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.
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ struct HapCharacteristics {
|
|||
HAPCHAR( CoolingThresholdTemperature, D, PR+PW+EV, FLOAT, false );
|
||||
HAPCHAR( ColorTemperature, CE, PR+PW+EV, UINT32, false );
|
||||
HAPCHAR( ConfiguredName, E3, PW+PR+EV, STRING, false );
|
||||
HAPCHAR( ConfiguredNameStatic, E3, PR+EV, STRING, false );
|
||||
HAPCHAR( ContactSensorState, 6A, PR+EV, UINT8, true );
|
||||
HAPCHAR( CurrentAmbientLightLevel, 6B, PR+EV, FLOAT, false );
|
||||
HAPCHAR( CurrentHorizontalTiltAngle, 6C, PR+EV, INT, false );
|
||||
|
|
|
|||
12
src/HAP.cpp
12
src/HAP.cpp
|
|
@ -52,6 +52,10 @@ void HAPClient::init(){
|
|||
otaPwdHash.getChars(homeSpan.otaPwd);
|
||||
}
|
||||
|
||||
if(strlen(homeSpan.pairingCodeCommand)){ // load verification setup code if provided
|
||||
homeSpan.processSerialCommand(homeSpan.pairingCodeCommand); // if load failed due to invalid code, the logic below still runs and will pick up previous code or use the default one
|
||||
}
|
||||
|
||||
struct { // temporary structure to hold SRP verification code and salt stored in NVS
|
||||
uint8_t salt[16];
|
||||
uint8_t verifyCode[384];
|
||||
|
|
@ -1282,13 +1286,17 @@ void HAPClient::checkTimedWrites(){
|
|||
|
||||
char c[64];
|
||||
|
||||
for(auto tw=homeSpan.TimedWrites.begin(); tw!=homeSpan.TimedWrites.end(); tw++){ // loop over all Timed Writes using an iterator
|
||||
auto tw=homeSpan.TimedWrites.begin();
|
||||
while(tw!=homeSpan.TimedWrites.end()){
|
||||
if(cTime>tw->second){ // timer has expired
|
||||
sprintf(c,"Removing PID=%llu ALARM=%u\n",tw->first,tw->second);
|
||||
LOG2(c);
|
||||
homeSpan.TimedWrites.erase(tw);
|
||||
tw=homeSpan.TimedWrites.erase(tw);
|
||||
}
|
||||
else
|
||||
tw++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -721,11 +721,11 @@ void Span::processSerialCommand(const char *c){
|
|||
sscanf(c+1," %9[0-9]",setupCode);
|
||||
|
||||
if(strlen(setupCode)!=8){
|
||||
Serial.print("\n*** Invalid request to change Setup Code. Code must be exactly 8 digits.\n");
|
||||
Serial.print("\n*** Invalid request to change Setup Code. Code must be exactly 8 digits.\n\n");
|
||||
} else
|
||||
|
||||
if(!network.allowedCode(setupCode)){
|
||||
Serial.print("\n*** Invalid request to change Setup Code. Code too simple.\n");
|
||||
Serial.print("\n*** Invalid request to change Setup Code. Code too simple.\n\n");
|
||||
} else {
|
||||
|
||||
sprintf(buf,"\n\nGenerating SRP verification data for new Setup Code: %.3s-%.2s-%.3s ... ",setupCode,setupCode+3,setupCode+5);
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ struct Span{
|
|||
const char *sketchVersion="n/a"; // version of the sketch
|
||||
nvs_handle charNVS; // handle for non-volatile-storage of Characteristics data
|
||||
nvs_handle wifiNVS=0; // handle for non-volatile-storage of WiFi data
|
||||
char pairingCodeCommand[12]=""; // user-specified Pairing Code - only needed if Pairing Setup Code is specified in sketch using setPairingCode()
|
||||
|
||||
boolean connected=false; // WiFi connection status
|
||||
unsigned long waitTime=60000; // time to wait (in milliseconds) between WiFi connection attempts
|
||||
|
|
@ -188,6 +189,8 @@ struct Span{
|
|||
void setWifiCallback(void (*f)()){wifiCallback=f;} // sets an optional user-defined function to call once WiFi connectivity is established
|
||||
void setApFunction(void (*f)()){apFunction=f;} // sets an optional user-defined function to call when activating the WiFi Access Point
|
||||
|
||||
void setPairingCode(const char *s){sprintf(pairingCodeCommand,"S %9s",s);} // sets the Pairing Code - use is NOT recommended. Use 'S' from CLI instead
|
||||
|
||||
void enableAutoStartAP(){autoStartAPEnabled=true;} // enables auto start-up of Access Point when WiFi Credentials not found
|
||||
void setWifiCredentials(const char *ssid, const char *pwd); // sets WiFi Credentials
|
||||
};
|
||||
|
|
@ -225,6 +228,7 @@ struct SpanService{
|
|||
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
|
||||
|
|
@ -238,7 +242,6 @@ struct SpanService{
|
|||
|
||||
struct SpanCharacteristic{
|
||||
|
||||
|
||||
union UVal {
|
||||
BOOL_t BOOL;
|
||||
UINT8_t UINT8;
|
||||
|
|
@ -534,7 +537,7 @@ struct SpanCharacteristic{
|
|||
|
||||
} // setString()
|
||||
|
||||
template <typename T> void setVal(T val){
|
||||
template <typename T> void setVal(T val, boolean notify=true){
|
||||
|
||||
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);
|
||||
|
|
@ -551,20 +554,53 @@ struct SpanCharacteristic{
|
|||
|
||||
updateTime=homeSpan.snapTime;
|
||||
|
||||
SpanBuf sb; // create SpanBuf object
|
||||
sb.characteristic=this; // set characteristic
|
||||
sb.status=StatusCode::OK; // set status
|
||||
char dummy[]="";
|
||||
sb.val=dummy; // set dummy "val" so that sprintfNotify knows to consider this "update"
|
||||
homeSpan.Notifications.push_back(sb); // store SpanBuf in Notifications vector
|
||||
if(notify){
|
||||
SpanBuf sb; // create SpanBuf object
|
||||
sb.characteristic=this; // set characteristic
|
||||
sb.status=StatusCode::OK; // set status
|
||||
char dummy[]="";
|
||||
sb.val=dummy; // set dummy "val" so that sprintfNotify knows to consider this "update"
|
||||
homeSpan.Notifications.push_back(sb); // store SpanBuf in Notifications vector
|
||||
|
||||
if(nvsKey){
|
||||
nvs_set_blob(homeSpan.charNVS,nvsKey,&value,sizeof(UVal)); // store data
|
||||
nvs_commit(homeSpan.charNVS);
|
||||
if(nvsKey){
|
||||
nvs_set_blob(homeSpan.charNVS,nvsKey,&value,sizeof(UVal)); // store data
|
||||
nvs_commit(homeSpan.charNVS);
|
||||
}
|
||||
}
|
||||
|
||||
} // setVal()
|
||||
|
||||
SpanCharacteristic *setPerms(uint8_t perms){
|
||||
this->perms=perms;
|
||||
homeSpan.configLog+=String(" \u2b0c Change Permissions for ") + String(hapName) + " with AID=" + String(aid) + ", IID=" + String(iid) + ":";
|
||||
|
||||
char pNames[][7]={"PR","PW","EV","AA","TW","HD","WR"};
|
||||
char sep=' ';
|
||||
|
||||
for(uint8_t i=0;i<7;i++){
|
||||
if(perms & (1<<i)){
|
||||
homeSpan.configLog+=String(sep) + String(pNames[i]);
|
||||
sep='+';
|
||||
}
|
||||
}
|
||||
|
||||
if(perms==0){
|
||||
homeSpan.configLog+=" *** ERROR! Undefined Permissions! ***";
|
||||
homeSpan.nFatalErrors++;
|
||||
}
|
||||
|
||||
homeSpan.configLog+="\n";
|
||||
return(this);
|
||||
}
|
||||
|
||||
SpanCharacteristic *addPerms(uint8_t dPerms){
|
||||
return(setPerms(perms|dPerms));
|
||||
}
|
||||
|
||||
SpanCharacteristic *removePerms(uint8_t dPerms){
|
||||
return(setPerms(perms&(~dPerms)));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
///////////////////////////////
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
#define HS_MAJOR 1
|
||||
#define HS_MINOR 4
|
||||
#define HS_PATCH 1
|
||||
#define HS_PATCH 2
|
||||
|
||||
#define STRINGIFY(x) _STR(x)
|
||||
#define _STR(x) #x
|
||||
|
|
|
|||
|
|
@ -420,7 +420,6 @@ namespace Characteristic {
|
|||
CREATE_CHAR(uint32_t,ColorTemperature,200,140,500);
|
||||
CREATE_CHAR(uint8_t,ContactSensorState,1,0,1);
|
||||
CREATE_CHAR(const char *,ConfiguredName,"unnamed",0,1);
|
||||
CREATE_CHAR(const char *,ConfiguredNameStatic,"unnamed",0,1);
|
||||
CREATE_CHAR(double,CurrentAmbientLightLevel,1,0.0001,100000);
|
||||
CREATE_CHAR(int,CurrentHorizontalTiltAngle,0,-90,90);
|
||||
CREATE_CHAR(uint8_t,CurrentAirPurifierState,1,0,2);
|
||||
|
|
|
|||
|
|
@ -279,7 +279,9 @@ void Blinker::isrTimer(void *arg){
|
|||
|
||||
Blinker *b=(Blinker *)arg;
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) // use new method that is generic to ESP32, S2, and C3
|
||||
timer_group_clr_intr_status_in_isr(b->group,b->idx);
|
||||
#else // use older method that is only for ESP32
|
||||
if(b->group){
|
||||
if(b->idx)
|
||||
TIMERG1.int_clr_timers.t1=1;
|
||||
|
|
@ -291,24 +293,6 @@ void Blinker::isrTimer(void *arg){
|
|||
else
|
||||
TIMERG0.int_clr_timers.t0=1;
|
||||
}
|
||||
#elif CONFIG_IDF_TARGET_ESP32S2 // for some reason, the ESP32-S2 and ESP32-C3 use "int_clr" instead of "int_clr_timers" in their timer structure
|
||||
if(b->group){
|
||||
if(b->idx)
|
||||
TIMERG1.int_clr.t1=1;
|
||||
else
|
||||
TIMERG1.int_clr.t0=1;
|
||||
} else {
|
||||
if(b->idx)
|
||||
TIMERG0.int_clr.t1=1;
|
||||
else
|
||||
TIMERG0.int_clr.t0=1;
|
||||
}
|
||||
#elif CONFIG_IDF_TARGET_ESP32C3 // ESP32-C3 only has one timer per timer group
|
||||
if(b->group){
|
||||
TIMERG1.int_clr.t0=1;
|
||||
} else {
|
||||
TIMERG0.int_clr.t0=1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if(!digitalRead(b->pin)){
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ void setup() {
|
|||
Serial.begin(115200);
|
||||
|
||||
homeSpan.setLogLevel(2);
|
||||
homeSpan.setStatusPin(5);
|
||||
homeSpan.setControlPin(33);
|
||||
// homeSpan.setStatusPin(13);
|
||||
// homeSpan.setControlPin(33);
|
||||
|
||||
homeSpan.setHostNameSuffix("-lamp1");
|
||||
homeSpan.setPortNum(1201);
|
||||
|
|
@ -74,6 +74,7 @@ void setup() {
|
|||
new Characteristic::Name("Light 3");
|
||||
new Characteristic::TargetPosition();
|
||||
new Characteristic::OzoneDensity();
|
||||
(new Characteristic::OzoneDensity())->addPerms(PW|AA)->removePerms(EV|PR);
|
||||
|
||||
} // end of setup()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue