├── .DS_Store ├── AbstractPL1167.h ├── Data.cpp ├── Data.h ├── ESP8266SSDP.cpp ├── ESP8266SSDP.h ├── LICENSE ├── MiLightRadio.cpp ├── MiLightRadio.h ├── MuzzleyRegister.cpp ├── MuzzleyRegister.h ├── PL1167_nRF24.cpp ├── PL1167_nRF24.h ├── README.md ├── ReadmeImages ├── .DS_Store ├── IMG_1654.PNG ├── IMG_1655.PNG ├── IMG_1656.PNG ├── IMG_1657.PNG ├── IMG_1658.PNG ├── IMG_1659.PNG ├── nodemcu-nrf24l01-muzzley-crop.png ├── nodemcu-nrf24l01-muzzley-real.jpg └── screens.png ├── SupportFunctions.cpp ├── SupportFunctions.h ├── canny.ino └── muzzleyprofilespec └── profilespec.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/.DS_Store -------------------------------------------------------------------------------- /AbstractPL1167.h: -------------------------------------------------------------------------------- 1 | /* 2 | * AbstractPL1167.h 3 | * 4 | * Created on: 29 May 2015 5 | * Author: henryk 6 | */ 7 | 8 | #ifdef ARDUINO 9 | #include "Arduino.h" 10 | #else 11 | #include 12 | #include 13 | #endif 14 | 15 | #ifndef ABSTRACTPL1167_H_ 16 | #define ABSTRACTPL1167_H_ 17 | 18 | class AbstractPL1167 { 19 | public: 20 | virtual int open() = 0; 21 | 22 | virtual int setPreambleLength(uint8_t preambleLength) = 0; 23 | virtual int setSyncword(uint16_t syncword0, uint16_t syncword3) = 0; 24 | virtual int setTrailerLength(uint8_t trailerLength) = 0; 25 | virtual int setMaxPacketLength(uint8_t maxPacketLength) = 0; 26 | virtual int setCRC(bool crc) = 0; 27 | virtual int writeFIFO(const uint8_t data[], size_t data_length) = 0; 28 | virtual int transmit(uint8_t channel) = 0; 29 | virtual int receive(uint8_t channel) = 0; 30 | virtual int readFIFO(uint8_t data[], size_t &data_length) = 0; 31 | }; 32 | 33 | 34 | 35 | 36 | #endif /* ABSTRACTPL1167_H_ */ 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Data.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Data.h" 3 | #include 4 | #include 5 | 6 | Data::Data() 7 | { 8 | 9 | } 10 | 11 | 12 | void Data::printconfig() { 13 | 14 | if (SPIFFS.exists("/bulbdata.json")) { 15 | File configFile = SPIFFS.open("/bulbdata.json", "r"); 16 | 17 | while (configFile.available()) { 18 | //Lets read line by line from the file 19 | String line = configFile.readString(); 20 | Serial.println(line); 21 | } 22 | configFile.close(); 23 | } else { 24 | Serial.println("printconfig: failed to open bulbdata.json"); 25 | } 26 | 27 | 28 | //end read 29 | } 30 | 31 | 32 | void Data::mountbulbdata() { 33 | //clean FS, for testing 34 | //SPIFFS.format(); 35 | 36 | //read configuration from FS json 37 | //Serial.println("mounting Buµlb FS..."); 38 | 39 | 40 | //Serial.println(" . starting"); 41 | if (SPIFFS.exists("/bulbdata.json")) { 42 | File configFile = SPIFFS.open("/bulbdata.json", "r"); 43 | 44 | printconfig(); 45 | 46 | if (configFile) { 47 | size_t size = configFile.size(); 48 | // Allocate a buffer to store contents of the file. 49 | std::unique_ptr buf(new char[size]); 50 | 51 | configFile.readBytes(buf.get(), size); 52 | DynamicJsonBuffer jsonBuffer; 53 | JsonObject& json = jsonBuffer.parseObject(buf.get()); 54 | if (json.success()) { 55 | //Serial.println("\nbulbdata parsed json"); 56 | } else { 57 | Serial.println("failed to load bulb data file"); 58 | } 59 | } 60 | } 61 | 62 | //end read 63 | } 64 | 65 | String Data::get_config() { 66 | if (SPIFFS.exists("/bulbdata.json")) { 67 | File configFile = SPIFFS.open("/bulbdata.json", "r"); 68 | 69 | if (configFile) { 70 | size_t size = configFile.size(); 71 | // Allocate a buffer to store contents of the file. 72 | std::unique_ptr buf(new char[size]); 73 | 74 | configFile.readBytes(buf.get(), size); 75 | DynamicJsonBuffer jsonBuffer; 76 | JsonObject& json = jsonBuffer.parseObject(buf.get()); 77 | //json.printTo(Serial); 78 | if (json.success()) { 79 | //Serial.println("\nbulbdata parsed json"); 80 | String tmpjson; 81 | json.printTo(tmpjson); 82 | configFile.close(); 83 | return tmpjson; 84 | } else { 85 | Serial.println("Data::get_config(): failed to parse bulb data file"); 86 | return ""; 87 | } 88 | } 89 | } else { 90 | Serial.println("Data::get_config(): File does not exist"); 91 | return ""; 92 | } 93 | } 94 | 95 | 96 | 97 | void Data::save_config(JsonObject& root) 98 | { 99 | File configFile = SPIFFS.open("/bulbdata.json", "w"); 100 | 101 | if (!configFile) { 102 | Serial.println("failed to open config file for writing"); 103 | } 104 | 105 | root.printTo(configFile); 106 | configFile.close(); 107 | } 108 | 109 | void Data::create_component(JsonObject& root, String component) 110 | { 111 | 112 | //genenate control ID 113 | int randOne = 0xB0; 114 | int randTwo = 0x27; randTwo = random(0x27, 0x88); 115 | int randThree = 0x59; randThree = random(0x10, 0x6D); 116 | 117 | 118 | JsonObject& bulb = root.createNestedObject(component); 119 | bulb["ComponentID"] = component; 120 | bulb["type"] = 1; // 1 for color, 2 for white, ... 121 | bulb["ControlIDone"] = randOne; 122 | bulb["ControlIDTwo"] = randTwo; 123 | bulb["ControlIDThree"] = randThree; 124 | JsonObject& state = bulb.createNestedObject("state"); 125 | state["status"] = false; 126 | state["brightness"] = 254; 127 | state["colorr"] = 0; 128 | state["colorg"] = 0; 129 | state["colorb"] = 0; 130 | 131 | // save to file 132 | save_config(root); 133 | } 134 | 135 | // Updates a specific state for a component 136 | bool Data::update_bulb_state(String component, String properties, int value){ 137 | String config = get_config(); 138 | DynamicJsonBuffer jsonBuffer; 139 | JsonObject& obj = jsonBuffer.parseObject(config); 140 | 141 | JsonObject& bulb = obj[component]; 142 | bulb["ComponentID"] = bulb["ComponentID"]; 143 | bulb["type"] = bulb["type"]; 144 | bulb["ControlIDone"] = bulb["ControlIDone"]; 145 | bulb["ControlIDTwo"] = bulb["ControlIDTwo"]; 146 | bulb["ControlIDThree"] = bulb["ControlIDThree"]; 147 | 148 | // keeping previous states 149 | int status = bulb["state"]["status"]; 150 | int brightness = bulb["state"]["brightness"]; 151 | int colorr = bulb["state"]["colorr"]; 152 | int colorg = bulb["state"]["colorg"]; 153 | int colorb = bulb["state"]["colorb"]; 154 | 155 | JsonObject& state = bulb.createNestedObject("state"); 156 | 157 | // update the color Status 158 | if (properties == "status"){ 159 | state["status"] = value; 160 | }else{ 161 | state["status"] = status; 162 | } 163 | 164 | // update the color Brightness 165 | if (properties == "brightness"){ 166 | state["brightness"] = value; 167 | }else{ 168 | state["brightness"] = brightness; 169 | } 170 | 171 | // update the color R 172 | if (properties == "colorr"){ 173 | state["colorr"] = value; 174 | }else{ 175 | state["colorr"] = colorr; 176 | } 177 | 178 | // update the color G 179 | if (properties == "colorg"){ 180 | state["colorg"] = value; 181 | }else{ 182 | state["colorg"] = colorg; 183 | } 184 | 185 | // set the color B 186 | if (properties == "colorb"){ 187 | state["colorb"] = value; 188 | }else{ 189 | state["colorr"] = colorb; 190 | } 191 | 192 | 193 | // save to file 194 | save_config(obj); 195 | 196 | } 197 | 198 | 199 | // returns 0/false if doesn't exist; 1/true if exists 200 | bool Data::search_bulb(String component){ 201 | String config = get_config(); 202 | DynamicJsonBuffer jsonBuffer; 203 | JsonObject& obj = jsonBuffer.parseObject(config); 204 | bool exists = false; 205 | if (obj.containsKey(component)) 206 | { 207 | exists = true; 208 | }else{ 209 | exists = false; 210 | } 211 | return exists; 212 | } 213 | 214 | void Data::add_bulb(String component) 215 | { 216 | String config = get_config(); 217 | DynamicJsonBuffer jsonBuffer; 218 | JsonObject& obj = jsonBuffer.parseObject(config); 219 | create_component(obj, component); 220 | 221 | } 222 | 223 | -------------------------------------------------------------------------------- /Data.h: -------------------------------------------------------------------------------- 1 | #ifndef Data_h 2 | #define Data_h 3 | 4 | #include 5 | 6 | 7 | class Data 8 | { 9 | public: 10 | Data(); 11 | void printconfig(); 12 | void save_config(JsonObject& root); 13 | void mountbulbdata(); 14 | void create_component(JsonObject& root, String component); 15 | String get_config(); 16 | bool search_bulb(String component); 17 | bool update_bulb_state(String component, String properties, int value); 18 | void add_bulb(String component); 19 | private: 20 | }; 21 | 22 | #endif 23 | 24 | -------------------------------------------------------------------------------- /ESP8266SSDP.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Fork of the ESP8266 SSDP to extend Muzzley discovery protocol 4 | 5 | */ 6 | 7 | #define LWIP_OPEN_SRC 8 | #include 9 | #include "ESP8266SSDP.h" 10 | #include "WiFiUdp.h" 11 | #include "debug.h" 12 | 13 | extern "C" { 14 | #include "osapi.h" 15 | #include "ets_sys.h" 16 | #include "user_interface.h" 17 | } 18 | 19 | #include "lwip/opt.h" 20 | #include "lwip/udp.h" 21 | #include "lwip/inet.h" 22 | #include "lwip/igmp.h" 23 | #include "lwip/mem.h" 24 | #include "include/UdpContext.h" 25 | 26 | // #define DEBUG_SSDP Serial 27 | 28 | #define SSDP_INTERVAL 1200 29 | #define SSDP_PORT 1900 30 | #define SSDP_METHOD_SIZE 10 31 | #define SSDP_URI_SIZE 2 32 | #define SSDP_BUFFER_SIZE 64 33 | #define SSDP_MULTICAST_TTL 2 34 | static const IPAddress SSDP_MULTICAST_ADDR(239, 255, 255, 250); 35 | 36 | 37 | 38 | static const char* _ssdp_response_template = 39 | "HTTP/1.1 200 OK\r\n" 40 | "EXT:\r\n" 41 | "ST: upnp:rootdevice\r\n"; 42 | 43 | static const char* _ssdp_notify_template = 44 | "NOTIFY * HTTP/1.1\r\n" 45 | "HOST: 239.255.255.250:1900\r\n" 46 | "NT: upnp:rootdevice\r\n" 47 | "NTS: ssdp:alive\r\n" 48 | "USN: uuid:1111111111111111111111::upnp:rootdevice\r\n"; 49 | 50 | static const char* _ssdp_packet_template = 51 | "%s" // _ssdp_response_template / _ssdp_notify_template 52 | "CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL 53 | "SERVER: Arduino/1.0 UPNP/1.1 %s/%s\r\n" // _modelName, _modelNumber 54 | "USN: uuid:%s\r\n" // _uuid 55 | "LOCATION: http://%u.%u.%u.%u:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL 56 | "ST: urn:Muzzley:device:%s:1\r\n" 57 | "USN: urn:Muzzley:device:%s:1\r\n" 58 | "\r\n"; 59 | 60 | static const char* _ssdp_schema_template = 61 | "HTTP/1.1 200 OK\r\n" 62 | "Content-Type: text/xml\r\n" 63 | "Connection: close\r\n" 64 | "Access-Control-Allow-Origin: *\r\n" 65 | "\r\n" 66 | "" 67 | "" 68 | "" 69 | "1" 70 | "0" 71 | "" 72 | "http://%u.%u.%u.%u:%u/" // WiFi.localIP(), _port 73 | "" 74 | "%s" 75 | "%s" 76 | "%s" 77 | "%s" 78 | "%s" 79 | "%s" 80 | "%s" 81 | "%s" 82 | "%s" 83 | "%s" 84 | "uuid:%s" 85 | "%s" 86 | "" 87 | "" 88 | "bulb20" 89 | "" 90 | "bulb" 91 | "" 92 | "" 93 | "bulb10" 94 | "" 95 | "bulb" 96 | "" 97 | "" 98 | "" 99 | "" 100 | "\r\n"; 101 | 102 | 103 | struct SSDPTimer { 104 | ETSTimer timer; 105 | }; 106 | 107 | SSDPClass::SSDPClass() : 108 | _server(0), 109 | _timer(new SSDPTimer), 110 | _port(80), 111 | _ttl(SSDP_MULTICAST_TTL), 112 | _respondToPort(0), 113 | _pending(false), 114 | _delay(0), 115 | _process_time(0), 116 | _notify_time(0) 117 | { 118 | _uuid[0] = '\0'; 119 | _modelNumber[0] = '\0'; 120 | sprintf(_deviceType, "urn:schemas-upnp-org:device:Basic:1"); 121 | _friendlyName[0] = '\0'; 122 | _macAddress[0] = '\0'; 123 | _presentationURL[0] = '\0'; 124 | _serialNumber[0] = '\0'; 125 | _modelName[0] = '\0'; 126 | _modelURL[0] = '\0'; 127 | _manufacturer[0] = '\0'; 128 | _manufacturerURL[0] = '\0'; 129 | _mProfileID[0] = '\0'; 130 | _deviceKey[0] = '\0'; 131 | sprintf(_schemaURL, "ssdp/schema.xml"); 132 | } 133 | 134 | SSDPClass::~SSDPClass(){ 135 | delete _timer; 136 | } 137 | 138 | bool SSDPClass::begin(){ 139 | _pending = false; 140 | 141 | uint32_t chipId = ESP.getChipId(); 142 | sprintf(_uuid, "40e30d82-7cd2-477c-b3b0-700e184e0652", 143 | (uint16_t) ((chipId >> 16) & 0xff), 144 | (uint16_t) ((chipId >> 8) & 0xff), 145 | (uint16_t) chipId & 0xff ); 146 | 147 | #ifdef DEBUG_SSDP 148 | DEBUG_SSDP.printf("SSDP UUID: %s\n", (char *)_uuid); 149 | #endif 150 | 151 | if (_server) { 152 | _server->unref(); 153 | _server = 0; 154 | } 155 | 156 | _server = new UdpContext; 157 | _server->ref(); 158 | 159 | ip_addr_t ifaddr; 160 | ifaddr.addr = WiFi.localIP(); 161 | ip_addr_t multicast_addr; 162 | multicast_addr.addr = (uint32_t) SSDP_MULTICAST_ADDR; 163 | if (igmp_joingroup(&ifaddr, &multicast_addr) != ERR_OK ) { 164 | DEBUGV("SSDP failed to join igmp group"); 165 | return false; 166 | } 167 | 168 | if (!_server->listen(*IP_ADDR_ANY, SSDP_PORT)) { 169 | return false; 170 | } 171 | 172 | _server->setMulticastInterface(ifaddr); 173 | _server->setMulticastTTL(_ttl); 174 | _server->onRx(std::bind(&SSDPClass::_update, this)); 175 | if (!_server->connect(multicast_addr, SSDP_PORT)) { 176 | return false; 177 | } 178 | 179 | _startTimer(); 180 | 181 | return true; 182 | } 183 | 184 | void SSDPClass::_send(ssdp_method_t method){ 185 | char buffer[1460]; 186 | uint32_t ip = WiFi.localIP(); 187 | 188 | int len = snprintf(buffer, sizeof(buffer), 189 | _ssdp_packet_template, 190 | (method == NONE)?_ssdp_response_template:_ssdp_notify_template, 191 | SSDP_INTERVAL, 192 | _modelName, _modelNumber, 193 | _uuid, 194 | IP2STR(&ip), _port, _schemaURL, 195 | _mProfileID, _mProfileID 196 | ); 197 | 198 | _server->append(buffer, len); 199 | 200 | ip_addr_t remoteAddr; 201 | uint16_t remotePort; 202 | if(method == NONE) { 203 | remoteAddr.addr = _respondToAddr; 204 | remotePort = _respondToPort; 205 | #ifdef DEBUG_SSDP 206 | DEBUG_SSDP.print("Sending Response to "); 207 | #endif 208 | } else { 209 | remoteAddr.addr = SSDP_MULTICAST_ADDR; 210 | remotePort = SSDP_PORT; 211 | #ifdef DEBUG_SSDP 212 | DEBUG_SSDP.println("Sending Notify to "); 213 | #endif 214 | } 215 | #ifdef DEBUG_SSDP 216 | DEBUG_SSDP.print(IPAddress(remoteAddr.addr)); 217 | DEBUG_SSDP.print(":"); 218 | DEBUG_SSDP.println(remotePort); 219 | #endif 220 | 221 | _server->send(&remoteAddr, remotePort); 222 | } 223 | 224 | void SSDPClass::schema(WiFiClient client){ 225 | uint32_t ip = WiFi.localIP(); 226 | client.printf(_ssdp_schema_template, 227 | IP2STR(&ip), _port, 228 | _deviceType, 229 | _friendlyName, 230 | _presentationURL, 231 | _serialNumber, 232 | _macAddress, 233 | _modelName, 234 | _modelNumber, 235 | _modelURL, 236 | _manufacturer, 237 | _manufacturerURL, 238 | _mProfileID, 239 | _deviceKey, 240 | _uuid 241 | ); 242 | } 243 | 244 | void SSDPClass::_update(){ 245 | if(!_pending && _server->next()) { 246 | ssdp_method_t method = NONE; 247 | 248 | _respondToAddr = _server->getRemoteAddress(); 249 | _respondToPort = _server->getRemotePort(); 250 | 251 | typedef enum {METHOD, URI, PROTO, KEY, VALUE, ABORT} states; 252 | states state = METHOD; 253 | 254 | typedef enum {START, MAN, ST, MX} headers; 255 | headers header = START; 256 | 257 | uint8_t cursor = 0; 258 | uint8_t cr = 0; 259 | 260 | char buffer[SSDP_BUFFER_SIZE] = {0}; 261 | 262 | while(_server->getSize() > 0){ 263 | char c = _server->read(); 264 | 265 | (c == '\r' || c == '\n') ? cr++ : cr = 0; 266 | 267 | switch(state){ 268 | case METHOD: 269 | if(c == ' '){ 270 | if(strcmp(buffer, "M-SEARCH") == 0) method = SEARCH; 271 | else if(strcmp(buffer, "NOTIFY") == 0) method = NOTIFY; 272 | 273 | if(method == NONE) state = ABORT; 274 | else state = URI; 275 | cursor = 0; 276 | 277 | } else if(cursor < SSDP_METHOD_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; } 278 | break; 279 | case URI: 280 | if(c == ' '){ 281 | if(strcmp(buffer, "*")) state = ABORT; 282 | else state = PROTO; 283 | cursor = 0; 284 | } else if(cursor < SSDP_URI_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; } 285 | break; 286 | case PROTO: 287 | if(cr == 2){ state = KEY; cursor = 0; } 288 | break; 289 | case KEY: 290 | if(cr == 4){ _pending = true; _process_time = millis(); } 291 | else if(c == ' '){ cursor = 0; state = VALUE; } 292 | else if(c != '\r' && c != '\n' && c != ':' && cursor < SSDP_BUFFER_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; } 293 | break; 294 | case VALUE: 295 | if(cr == 2){ 296 | switch(header){ 297 | case START: 298 | break; 299 | case MAN: 300 | #ifdef DEBUG_SSDP 301 | DEBUG_SSDP.printf("MAN: %s\n", (char *)buffer); 302 | #endif 303 | break; 304 | case ST: 305 | if(strcmp(buffer, "ssdp:all")){ 306 | state = ABORT; 307 | #ifdef DEBUG_SSDP 308 | DEBUG_SSDP.printf("REJECT: %s\n", (char *)buffer); 309 | #endif 310 | } 311 | // if the search type matches our type, we should respond instead of ABORT 312 | if(strcmp(buffer, _deviceType) == 0){ 313 | _pending = true; 314 | _process_time = millis(); 315 | state = KEY; 316 | } 317 | break; 318 | case MX: 319 | _delay = random(0, atoi(buffer)) * 1000L; 320 | break; 321 | } 322 | 323 | if(state != ABORT){ state = KEY; header = START; cursor = 0; } 324 | } else if(c != '\r' && c != '\n'){ 325 | if(header == START){ 326 | if(strncmp(buffer, "MA", 2) == 0) header = MAN; 327 | else if(strcmp(buffer, "ST") == 0) header = ST; 328 | else if(strcmp(buffer, "MX") == 0) header = MX; 329 | } 330 | 331 | if(cursor < SSDP_BUFFER_SIZE - 1){ buffer[cursor++] = c; buffer[cursor] = '\0'; } 332 | } 333 | break; 334 | case ABORT: 335 | _pending = false; _delay = 0; 336 | break; 337 | } 338 | } 339 | } 340 | 341 | if(_pending && (millis() - _process_time) > _delay){ 342 | _pending = false; _delay = 0; 343 | _send(NONE); 344 | } else if(_notify_time == 0 || (millis() - _notify_time) > (SSDP_INTERVAL * 1000L)){ 345 | _notify_time = millis(); 346 | _send(NOTIFY); 347 | } 348 | 349 | if (_pending) { 350 | while (_server->next()) 351 | _server->flush(); 352 | } 353 | 354 | } 355 | 356 | void SSDPClass::setSchemaURL(const char *url){ 357 | strlcpy(_schemaURL, url, sizeof(_schemaURL)); 358 | } 359 | 360 | void SSDPClass::setHTTPPort(uint16_t port){ 361 | _port = port; 362 | } 363 | 364 | void SSDPClass::setDeviceType(const char *deviceType){ 365 | strlcpy(_deviceType, deviceType, sizeof(_deviceType)); 366 | } 367 | 368 | void SSDPClass::setName(const char *name){ 369 | strlcpy(_friendlyName, name, sizeof(_friendlyName)); 370 | } 371 | 372 | void SSDPClass::setURL(const char *url){ 373 | strlcpy(_presentationURL, url, sizeof(_presentationURL)); 374 | } 375 | 376 | void SSDPClass::setSerialNumber(const char *serialNumber){ 377 | strlcpy(_serialNumber, serialNumber, sizeof(_serialNumber)); 378 | } 379 | 380 | void SSDPClass::setSerialNumber(const uint32_t serialNumber){ 381 | snprintf(_serialNumber, sizeof(uint32_t)*2+1, "%08X", serialNumber); 382 | } 383 | 384 | void SSDPClass::setModelName(const char *name){ 385 | strlcpy(_modelName, name, sizeof(_modelName)); 386 | } 387 | 388 | void SSDPClass::setModelNumber(const char *num){ 389 | strlcpy(_modelNumber, num, sizeof(_modelNumber)); 390 | } 391 | 392 | void SSDPClass::setModelURL(const char *url){ 393 | strlcpy(_modelURL, url, sizeof(_modelURL)); 394 | } 395 | 396 | void SSDPClass::setManufacturer(const char *name){ 397 | strlcpy(_manufacturer, name, sizeof(_manufacturer)); 398 | } 399 | 400 | void SSDPClass::setManufacturerURL(const char *url){ 401 | strlcpy(_manufacturerURL, url, sizeof(_manufacturerURL)); 402 | } 403 | 404 | void SSDPClass::setmProfileID(const char *mProfileID){ 405 | strlcpy(_mProfileID, mProfileID, sizeof(_mProfileID)); 406 | } 407 | 408 | void SSDPClass::setMACAddress(const char *mac){ 409 | strlcpy(_macAddress, mac, sizeof(_macAddress)); 410 | } 411 | 412 | void SSDPClass::setDeviceKey(const char *deviceKey){ 413 | strlcpy(_deviceKey, deviceKey, sizeof(_deviceKey)); 414 | } 415 | 416 | void SSDPClass::setTTL(const uint8_t ttl){ 417 | _ttl = ttl; 418 | } 419 | 420 | void SSDPClass::_onTimerStatic(SSDPClass* self) { 421 | self->_update(); 422 | } 423 | 424 | void SSDPClass::_startTimer() { 425 | ETSTimer* tm = &(_timer->timer); 426 | const int interval = 1000; 427 | os_timer_disarm(tm); 428 | os_timer_setfn(tm, reinterpret_cast(&SSDPClass::_onTimerStatic), reinterpret_cast(this)); 429 | os_timer_arm(tm, interval, 1 /* repeat */); 430 | } 431 | 432 | SSDPClass SSDP; 433 | 434 | -------------------------------------------------------------------------------- /ESP8266SSDP.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Fork of the ESP8266 SSDP to extend Muzzley discovery protocol 4 | 5 | */ 6 | 7 | #ifndef ESP8266SSDP_H 8 | #define ESP8266SSDP_H 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | class UdpContext; 15 | 16 | #define SSDP_UUID_SIZE 37 17 | #define SSDP_SCHEMA_URL_SIZE 64 18 | #define SSDP_DEVICE_TYPE_SIZE 64 19 | #define SSDP_FRIENDLY_NAME_SIZE 64 20 | #define SSDP_SERIAL_NUMBER_SIZE 32 21 | #define SSDP_PRESENTATION_URL_SIZE 128 22 | #define SSDP_MAC_SIZE 32 23 | #define SSDP_MODEL_NAME_SIZE 64 24 | #define SSDP_MODEL_URL_SIZE 128 25 | #define SSDP_MODEL_VERSION_SIZE 32 26 | #define SSDP_MANUFACTURER_SIZE 64 27 | #define SSDP_MANUFACTURER_URL_SIZE 128 28 | #define SSDP_MPROFILEID_SIZE 64 29 | #define SSDP_DEVICEKEY_SIZE 128 30 | 31 | typedef enum { 32 | NONE, 33 | SEARCH, 34 | NOTIFY 35 | } ssdp_method_t; 36 | 37 | 38 | struct SSDPTimer; 39 | 40 | class SSDPClass{ 41 | public: 42 | SSDPClass(); 43 | ~SSDPClass(); 44 | 45 | bool begin(); 46 | 47 | void schema(WiFiClient client); 48 | 49 | void setDeviceType(const String& deviceType) { setDeviceType(deviceType.c_str()); } 50 | void setDeviceType(const char *deviceType); 51 | void setName(const String& name) { setName(name.c_str()); } 52 | void setName(const char *name); 53 | void setURL(const String& url) { setURL(url.c_str()); } 54 | void setURL(const char *url); 55 | void setSchemaURL(const String& url) { setSchemaURL(url.c_str()); } 56 | void setSchemaURL(const char *url); 57 | void setSerialNumber(const String& serialNumber) { setSerialNumber(serialNumber.c_str()); } 58 | void setSerialNumber(const char *serialNumber); 59 | void setSerialNumber(const uint32_t serialNumber); 60 | void setModelName(const String& name) { setModelName(name.c_str()); } 61 | void setModelName(const char *name); 62 | void setModelNumber(const String& num) { setModelNumber(num.c_str()); } 63 | void setModelNumber(const char *num); 64 | void setModelURL(const String& url) { setModelURL(url.c_str()); } 65 | void setModelURL(const char *url); 66 | void setManufacturer(const String& name) { setManufacturer(name.c_str()); } 67 | void setManufacturer(const char *name); 68 | void setManufacturerURL(const String& url) { setManufacturerURL(url.c_str()); } 69 | void setManufacturerURL(const char *url); 70 | void setmProfileID(const char *mProfileID); 71 | void setHTTPPort(uint16_t port); 72 | void setMACAddress(const String& mac) { setMACAddress(mac.c_str()); }; 73 | void setMACAddress(const char *mac); 74 | void setDeviceKey(const String& deviceKey) { setDeviceKey(deviceKey.c_str()); }; 75 | void setDeviceKey(const char *deviceKey); 76 | void setTTL(uint8_t ttl); 77 | 78 | protected: 79 | void _send(ssdp_method_t method); 80 | void _update(); 81 | void _startTimer(); 82 | static void _onTimerStatic(SSDPClass* self); 83 | 84 | UdpContext* _server; 85 | SSDPTimer* _timer; 86 | uint16_t _port; 87 | uint8_t _ttl; 88 | 89 | IPAddress _respondToAddr; 90 | uint16_t _respondToPort; 91 | 92 | bool _pending; 93 | unsigned short _delay; 94 | unsigned long _process_time; 95 | unsigned long _notify_time; 96 | 97 | char _schemaURL[SSDP_SCHEMA_URL_SIZE]; 98 | char _uuid[SSDP_UUID_SIZE]; 99 | char _deviceType[SSDP_DEVICE_TYPE_SIZE]; 100 | char _friendlyName[SSDP_FRIENDLY_NAME_SIZE]; 101 | char _serialNumber[SSDP_SERIAL_NUMBER_SIZE]; 102 | char _presentationURL[SSDP_PRESENTATION_URL_SIZE]; 103 | char _macAddress[SSDP_MAC_SIZE]; 104 | char _manufacturer[SSDP_MANUFACTURER_SIZE]; 105 | char _manufacturerURL[SSDP_MANUFACTURER_URL_SIZE]; 106 | char _mProfileID[SSDP_MPROFILEID_SIZE]; 107 | char _deviceKey[SSDP_DEVICEKEY_SIZE]; 108 | char _modelName[SSDP_MODEL_NAME_SIZE]; 109 | char _modelURL[SSDP_MODEL_URL_SIZE]; 110 | char _modelNumber[SSDP_MODEL_VERSION_SIZE]; 111 | }; 112 | 113 | extern SSDPClass SSDP; 114 | 115 | #endif 116 | 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 djsb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MiLightRadio.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * MiLightRadio.cpp 3 | * 4 | * Created on: 29 May 2015 5 | * Author: henryk 6 | */ 7 | 8 | #include "MiLightRadio.h" 9 | 10 | #define PACKET_ID(packet) ( ((packet[1] & 0xF0)<<24) | (packet[2]<<16) | (packet[3]<<8) | (packet[7]) ) 11 | 12 | static const uint8_t CHANNELS[] = {9, 40, 71}; 13 | #define NUM_CHANNELS (sizeof(CHANNELS)/sizeof(CHANNELS[0])) 14 | 15 | MiLightRadio::MiLightRadio(AbstractPL1167 &pl1167) 16 | : _pl1167(pl1167) { 17 | _waiting = false; 18 | } 19 | 20 | int MiLightRadio::begin() 21 | { 22 | int retval = _pl1167.open(); 23 | if (retval < 0) { 24 | return retval; 25 | } 26 | 27 | retval = _pl1167.setCRC(true); 28 | if (retval < 0) { 29 | return retval; 30 | } 31 | 32 | retval = _pl1167.setPreambleLength(3); 33 | if (retval < 0) { 34 | return retval; 35 | } 36 | 37 | retval = _pl1167.setTrailerLength(4); 38 | if (retval < 0) { 39 | return retval; 40 | } 41 | 42 | retval = _pl1167.setSyncword(0x147A, 0x258B); 43 | if (retval < 0) { 44 | return retval; 45 | } 46 | 47 | retval = _pl1167.setMaxPacketLength(8); 48 | if (retval < 0) { 49 | return retval; 50 | } 51 | 52 | available(); 53 | 54 | return 0; 55 | } 56 | 57 | bool MiLightRadio::available() 58 | { 59 | if (_waiting) { 60 | return true; 61 | } 62 | 63 | if (_pl1167.receive(CHANNELS[0]) > 0) { 64 | size_t packet_length = sizeof(_packet); 65 | if (_pl1167.readFIFO(_packet, packet_length) < 0) { 66 | return false; 67 | } 68 | if (packet_length == 0 || packet_length != _packet[0] + 1U) { 69 | return false; 70 | } 71 | 72 | uint32_t packet_id = PACKET_ID(_packet); 73 | if (packet_id == _prev_packet_id) { 74 | _dupes_received++; 75 | } else { 76 | _prev_packet_id = packet_id; 77 | _waiting = true; 78 | } 79 | } 80 | 81 | return _waiting; 82 | } 83 | 84 | int MiLightRadio::dupesReceived() 85 | { 86 | return _dupes_received; 87 | } 88 | 89 | 90 | int MiLightRadio::read(uint8_t frame[], size_t &frame_length) 91 | { 92 | if (!_waiting) { 93 | frame_length = 0; 94 | return -1; 95 | } 96 | 97 | if (frame_length > sizeof(_packet) - 1) { 98 | frame_length = sizeof(_packet) - 1; 99 | } 100 | 101 | if (frame_length > _packet[0]) { 102 | frame_length = _packet[0]; 103 | } 104 | 105 | memcpy(frame, _packet + 1, frame_length); 106 | _waiting = false; 107 | 108 | return _packet[0]; 109 | } 110 | 111 | int MiLightRadio::write(uint8_t frame[], size_t frame_length) 112 | { 113 | if (frame_length > sizeof(_out_packet) - 1) { 114 | return -1; 115 | } 116 | 117 | memcpy(_out_packet + 1, frame, frame_length); 118 | _out_packet[0] = frame_length; 119 | 120 | int retval = resend(); 121 | if (retval < 0) { 122 | return retval; 123 | } 124 | return frame_length; 125 | } 126 | 127 | int MiLightRadio::resend() 128 | { 129 | for (size_t i = 0; i < NUM_CHANNELS; i++) { 130 | _pl1167.writeFIFO(_out_packet, _out_packet[0] + 1); 131 | _pl1167.transmit(CHANNELS[i]); 132 | } 133 | return 0; 134 | } 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /MiLightRadio.h: -------------------------------------------------------------------------------- 1 | /* 2 | * MiLightRadio.h 3 | * 4 | * Created on: 29 May 2015 5 | * Author: henryk 6 | */ 7 | 8 | #ifdef ARDUINO 9 | #include "Arduino.h" 10 | #else 11 | #include 12 | #include 13 | #include 14 | #endif 15 | 16 | #include "AbstractPL1167.h" 17 | 18 | #ifndef MILIGHTRADIO_H_ 19 | #define MILIGHTRADIO_H_ 20 | 21 | class MiLightRadio { 22 | public: 23 | MiLightRadio(AbstractPL1167 &pl1167); 24 | int begin(); 25 | bool available(); 26 | int read(uint8_t frame[], size_t &frame_length); 27 | int dupesReceived(); 28 | int write(uint8_t frame[], size_t frame_length); 29 | int resend(); 30 | private: 31 | AbstractPL1167 &_pl1167; 32 | uint32_t _prev_packet_id; 33 | 34 | uint8_t _packet[8], _out_packet[8]; 35 | bool _waiting; 36 | int _dupes_received; 37 | }; 38 | 39 | 40 | 41 | #endif /* MILIGHTRADIO_H_ */ 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /MuzzleyRegister.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "MuzzleyRegister.h" 5 | #include "SupportFunctions.h" 6 | 7 | 8 | 9 | 10 | // constructor 11 | MuzzleyRegister::MuzzleyRegister() 12 | { 13 | int _serialNumber = 010; 14 | } 15 | 16 | void MuzzleyRegister::getDeviceKey(String *d1, String *d2){ 17 | 18 | if (SPIFFS.exists("/muzzleyconfig.json")) { 19 | //file exists, reading and loading 20 | //Serial.println("MuzzleyRegister::getDeviceKey(): reading config file"); 21 | File configFile = SPIFFS.open("/muzzleyconfig.json", "r"); 22 | if (configFile) { 23 | //Serial.println("MuzzleyRegister::getDeviceKey(): opened config file"); 24 | size_t size = configFile.size(); 25 | // Allocate a buffer to store contents of the file. 26 | std::unique_ptr buf(new char[size]); 27 | 28 | configFile.readBytes(buf.get(), size); 29 | DynamicJsonBuffer jsonBuffer; 30 | JsonObject& json = jsonBuffer.parseObject(buf.get()); 31 | json.printTo(Serial); 32 | if (json.success()) { 33 | //Serial.println("\nMuzzleyRegister::getDeviceKey(): parsed json"); 34 | String tmpjson = json["deviceKey"]; 35 | String tmpserial = json["serialNumber"]; 36 | *d1 = tmpjson; 37 | *d2 = tmpserial; 38 | configFile.close(); 39 | 40 | } else { 41 | Serial.println("MuzzleyRegister::getDeviceKey(): failed to load json config"); 42 | } 43 | } 44 | }else{ 45 | Serial.println("MuzzleyRegister::getDeviceKey(): config file does not exist. Calling Muzzley register..."); 46 | 47 | // register on Muzzley and get deviceKey 48 | 49 | 50 | } 51 | 52 | 53 | 54 | } 55 | 56 | 57 | void MuzzleyRegister::save_config(String deviceKey, String serialNumber) 58 | { 59 | 60 | DynamicJsonBuffer jsonBuffer; 61 | JsonObject& settings = jsonBuffer.createObject(); 62 | settings["deviceKey"] = deviceKey; 63 | settings["serialNumber"] = serialNumber; 64 | 65 | File configFile = SPIFFS.open("/muzzleyconfig.json", "w"); 66 | 67 | if (!configFile) { 68 | Serial.println("failed to open config file for writing"); 69 | } 70 | 71 | settings.printTo(configFile); 72 | configFile.close(); 73 | 74 | } 75 | 76 | 77 | void MuzzleyRegister::registerDeviceKey(String serialNumber, String payload, String *_deviceKey) { 78 | 79 | // extract from pauyload the deviceKey 80 | DynamicJsonBuffer jsonBuffer; 81 | JsonObject& json = jsonBuffer.parseObject(payload); 82 | if (json.success()) { 83 | String deviceKey = json["deviceKey"]; 84 | *_deviceKey = deviceKey; 85 | 86 | // save device key to muzzleyconfig.json 87 | save_config(deviceKey, serialNumber); 88 | 89 | } else { 90 | Serial.println("E: 1002"); 91 | } 92 | 93 | 94 | 95 | } 96 | 97 | 98 | -------------------------------------------------------------------------------- /MuzzleyRegister.h: -------------------------------------------------------------------------------- 1 | #ifndef MuzzleyRegister_h 2 | #define MuzzleyRegister_h 3 | 4 | class MuzzleyRegister 5 | { 6 | public: 7 | MuzzleyRegister(); 8 | void getDeviceKey(String *d1, String *d2); 9 | void registerDeviceKey(String serialNumber, String payload, String *deviceKey); 10 | void save_config(String deviceKey, String serialNumber); 11 | 12 | private: 13 | 14 | }; 15 | 16 | #endif 17 | 18 | -------------------------------------------------------------------------------- /PL1167_nRF24.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * PL1167_nRF24.cpp 3 | * 4 | * Created on: 29 May 2015 5 | * Author: henryk 6 | */ 7 | 8 | #include "PL1167_nRF24.h" 9 | 10 | static uint16_t calc_crc(uint8_t *data, size_t data_length); 11 | static uint8_t reverse_bits(uint8_t data); 12 | static void demangle_packet(uint8_t *in, uint8_t *out) ; 13 | 14 | PL1167_nRF24::PL1167_nRF24(RF24 &radio) 15 | : _radio(radio) { } 16 | 17 | static const uint8_t pipe[] = {0xd1, 0x28, 0x5e, 0x55, 0x55}; 18 | 19 | int PL1167_nRF24::open() 20 | { 21 | _radio.begin(); 22 | return recalc_parameters(); 23 | } 24 | 25 | int PL1167_nRF24::recalc_parameters() 26 | { 27 | int nrf_address_length = _preambleLength - 1 + _syncwordLength; 28 | int address_overflow = 0; 29 | if (nrf_address_length > 5) { 30 | address_overflow = nrf_address_length - 5; 31 | nrf_address_length = 5; 32 | } 33 | int packet_length = address_overflow + ( (_trailerLength + 7) / 8) + _maxPacketLength; 34 | if (_crc) { 35 | packet_length += 2; 36 | } 37 | 38 | if (packet_length > sizeof(_packet) || nrf_address_length < 3) { 39 | return -1; 40 | } 41 | 42 | uint8_t preamble = 0; 43 | if (_syncword0 & 0x01) { 44 | preamble = 0x55; 45 | } else { 46 | preamble = 0xAA; 47 | } 48 | 49 | int nrf_address_pos = nrf_address_length; 50 | for (int i = 0; i < _preambleLength - 1; i++) { 51 | _nrf_pipe[ --nrf_address_pos ] = reverse_bits(preamble); 52 | } 53 | 54 | if (nrf_address_pos) { 55 | _nrf_pipe[ --nrf_address_pos ] = reverse_bits(_syncword0 & 0xff); 56 | } 57 | if (nrf_address_pos) { 58 | _nrf_pipe[ --nrf_address_pos ] = reverse_bits( (_syncword0 >> 8) & 0xff); 59 | } 60 | 61 | if (_syncwordLength == 4) { 62 | if (nrf_address_pos) { 63 | _nrf_pipe[ --nrf_address_pos ] = reverse_bits(_syncword3 & 0xff); 64 | } 65 | if (nrf_address_pos) { 66 | _nrf_pipe[ --nrf_address_pos ] = reverse_bits( (_syncword3 >> 8) & 0xff); 67 | } 68 | } 69 | 70 | _receive_length = packet_length; 71 | _preamble = preamble; 72 | 73 | _nrf_pipe_length = nrf_address_length; 74 | _radio.setAddressWidth(_nrf_pipe_length); 75 | _radio.openWritingPipe(_nrf_pipe); 76 | _radio.openReadingPipe(1, _nrf_pipe); 77 | 78 | _radio.setChannel(2 + _channel); 79 | 80 | 81 | _radio.setPayloadSize( packet_length ); 82 | _radio.setAutoAck(false); 83 | _radio.setPALevel(RF24_PA_MAX); 84 | _radio.setDataRate(RF24_1MBPS); 85 | _radio.disableCRC(); 86 | 87 | return 0; 88 | } 89 | 90 | 91 | int PL1167_nRF24::setPreambleLength(uint8_t preambleLength) 92 | { 93 | if (preambleLength > 8) { 94 | return -1; 95 | } 96 | _preambleLength = preambleLength; 97 | return recalc_parameters(); 98 | } 99 | 100 | 101 | int PL1167_nRF24::setSyncword(uint16_t syncword0, uint16_t syncword3) 102 | { 103 | _syncwordLength = 4; 104 | _syncword0 = syncword0; 105 | _syncword3 = syncword3; 106 | return recalc_parameters(); 107 | } 108 | 109 | int PL1167_nRF24::setTrailerLength(uint8_t trailerLength) 110 | { 111 | if (trailerLength < 4) { 112 | return -1; 113 | } 114 | if (trailerLength > 18) { 115 | return -1; 116 | } 117 | if (trailerLength & 0x01) { 118 | return -1; 119 | } 120 | _trailerLength = trailerLength; 121 | return recalc_parameters(); 122 | } 123 | 124 | int PL1167_nRF24::setCRC(bool crc) 125 | { 126 | _crc = crc; 127 | return recalc_parameters(); 128 | } 129 | 130 | int PL1167_nRF24::setMaxPacketLength(uint8_t maxPacketLength) 131 | { 132 | _maxPacketLength = maxPacketLength; 133 | return recalc_parameters(); 134 | } 135 | 136 | int PL1167_nRF24::receive(uint8_t channel) 137 | { 138 | if (channel != _channel) { 139 | _channel = channel; 140 | int retval = recalc_parameters(); 141 | if (retval < 0) { 142 | return retval; 143 | } 144 | } 145 | 146 | _radio.startListening(); 147 | if (_radio.available()) { 148 | internal_receive(); 149 | } 150 | 151 | if(_received) { 152 | return _packet_length; 153 | } else { 154 | return 0; 155 | } 156 | } 157 | 158 | int PL1167_nRF24::readFIFO(uint8_t data[], size_t &data_length) 159 | { 160 | if (data_length > _packet_length) { 161 | data_length = _packet_length; 162 | } 163 | memcpy(data, _packet, data_length); 164 | _packet_length -= data_length; 165 | if (_packet_length) { 166 | memmove(_packet, _packet + data_length, _packet_length); 167 | } 168 | return _packet_length; 169 | } 170 | 171 | int PL1167_nRF24::writeFIFO(const uint8_t data[], size_t data_length) 172 | { 173 | if (data_length > sizeof(_packet)) { 174 | data_length = sizeof(_packet); 175 | } 176 | memcpy(_packet, data, data_length); 177 | _packet_length = data_length; 178 | _received = false; 179 | 180 | return data_length; 181 | } 182 | 183 | int PL1167_nRF24::transmit(uint8_t channel) 184 | { 185 | if (channel != _channel) { 186 | _channel = channel; 187 | int retval = recalc_parameters(); 188 | if (retval < 0) { 189 | return retval; 190 | } 191 | } 192 | 193 | _radio.stopListening(); 194 | uint8_t tmp[sizeof(_packet)]; 195 | 196 | uint8_t trailer = (_packet[0] & 1) ? 0x55 : 0xAA; // NOTE: This is a guess, it might also be based upon the last 197 | // syncword bit, or fixed 198 | int outp = 0; 199 | 200 | for (; outp < _receive_length; outp++) { 201 | uint8_t outbyte = 0; 202 | 203 | if (outp + 1 + _nrf_pipe_length < _preambleLength) { 204 | outbyte = _preamble; 205 | } else if (outp + 1 + _nrf_pipe_length < _preambleLength + _syncwordLength) { 206 | int syncp = outp - _preambleLength + 1 + _nrf_pipe_length; 207 | switch (syncp) { 208 | case 0: 209 | outbyte = _syncword0 & 0xFF; 210 | break; 211 | case 1: 212 | outbyte = (_syncword0 >> 8) & 0xFF; 213 | break; 214 | case 2: 215 | outbyte = _syncword3 & 0xFF; 216 | break; 217 | case 3: 218 | outbyte = (_syncword3 >> 8) & 0xFF; 219 | break; 220 | } 221 | } else if (outp + 1 + _nrf_pipe_length < _preambleLength + _syncwordLength + (_trailerLength / 8) ) { 222 | outbyte = trailer; 223 | } else { 224 | break; 225 | } 226 | 227 | tmp[outp] = reverse_bits(outbyte); 228 | } 229 | 230 | int buffer_fill; 231 | bool last_round = false; 232 | uint16_t buffer = 0; 233 | uint16_t crc; 234 | if (_crc) { 235 | crc = calc_crc(_packet, _packet_length); 236 | } 237 | 238 | buffer = trailer >> (8 - (_trailerLength % 8)); 239 | buffer_fill = _trailerLength % 8; 240 | for (int inp = 0; inp < _packet_length + (_crc ? 2 : 0) + 1; inp++) { 241 | if (inp < _packet_length) { 242 | buffer |= _packet[inp] << buffer_fill; 243 | buffer_fill += 8; 244 | } else if (_crc && inp < _packet_length + 2) { 245 | buffer |= ((crc >> ( (inp - _packet_length) * 8)) & 0xff) << buffer_fill; 246 | buffer_fill += 8; 247 | } else { 248 | last_round = true; 249 | } 250 | 251 | while (buffer_fill > (last_round ? 0 : 8)) { 252 | if (outp >= sizeof(tmp)) { 253 | return -1; 254 | } 255 | tmp[outp++] = reverse_bits(buffer & 0xff); 256 | buffer >>= 8; 257 | buffer_fill -= 8; 258 | } 259 | } 260 | 261 | _radio.write(tmp, outp); 262 | return 0; 263 | } 264 | 265 | 266 | int PL1167_nRF24::internal_receive() 267 | { 268 | uint8_t tmp[sizeof(_packet)]; 269 | int outp = 0; 270 | 271 | _radio.read(tmp, _receive_length); 272 | 273 | // HACK HACK HACK: Reset radio 274 | open(); 275 | 276 | uint8_t shift_amount = _trailerLength % 8; 277 | uint16_t buffer = 0; 278 | 279 | #ifdef DEBUG_PRINTF 280 | printf("Packet received: "); 281 | for (int i = 0; i < _receive_length; i++) { 282 | printf("%02X", reverse_bits(tmp[i])); 283 | } 284 | printf("\n"); 285 | #endif 286 | 287 | for (int inp = 0; inp < _receive_length; inp++) { 288 | uint8_t inbyte = reverse_bits(tmp[inp]); 289 | buffer = (buffer >> 8) | (inbyte << 8); 290 | 291 | if (inp + 1 + _nrf_pipe_length < _preambleLength) { 292 | if (inbyte != _preamble) { 293 | #ifdef DEBUG_PRINTF 294 | printf("Preamble fail (%i: %02X)\n", inp, inbyte); 295 | #endif 296 | return 0; 297 | } 298 | } else if (inp + 1 + _nrf_pipe_length < _preambleLength + _syncwordLength) { 299 | int syncp = inp - _preambleLength + 1 + _nrf_pipe_length; 300 | switch (syncp) { 301 | case 0: 302 | if (inbyte != _syncword0 & 0xFF) { 303 | #ifdef DEBUG_PRINTF 304 | printf("Sync 0l fail (%i: %02X)\n", inp, inbyte); 305 | #endif 306 | return 0; 307 | } break; 308 | case 1: 309 | if (inbyte != (_syncword0 >> 8) & 0xFF) { 310 | #ifdef DEBUG_PRINTF 311 | printf("Sync 0h fail (%i: %02X)\n", inp, inbyte); 312 | #endif 313 | return 0; 314 | } break; 315 | case 2: 316 | if ((_syncwordLength == 4) && (inbyte != _syncword3 & 0xFF)) { 317 | #ifdef DEBUG_PRINTF 318 | printf("Sync 3l fail (%i: %02X)\n", inp, inbyte); 319 | #endif 320 | return 0; 321 | } break; 322 | case 3: 323 | if ((_syncwordLength == 4) && (inbyte != (_syncword3 >> 8) & 0xFF)) { 324 | #ifdef DEBUG_PRINTF 325 | printf("Sync 3h fail (%i: %02X)\n", inp, inbyte); 326 | #endif 327 | return 0; 328 | } break; 329 | } 330 | } else if (inp + 1 + _nrf_pipe_length < _preambleLength + _syncwordLength + ((_trailerLength + 7) / 8) ) { 331 | 332 | } else { 333 | tmp[outp++] = buffer >> shift_amount; 334 | } 335 | } 336 | 337 | 338 | #ifdef DEBUG_PRINTF 339 | printf("Packet transformed: "); 340 | for (int i = 0; i < outp; i++) { 341 | printf("%02X", tmp[i]); 342 | } 343 | printf("\n"); 344 | #endif 345 | 346 | 347 | if (_crc) { 348 | if (outp < 2) { 349 | return 0; 350 | } 351 | uint16_t crc = calc_crc(tmp, outp - 2); 352 | if ( ((crc & 0xff) != tmp[outp - 2]) || (((crc >> 8) & 0xff) != tmp[outp - 1]) ) { 353 | return 0; 354 | } 355 | outp -= 2; 356 | } 357 | 358 | memcpy(_packet, tmp, outp); 359 | _packet_length = outp; 360 | _received = true; 361 | return outp; 362 | } 363 | 364 | #define CRC_POLY 0x8408 365 | 366 | static uint16_t calc_crc(uint8_t *data, size_t data_length) { 367 | uint16_t state = 0; 368 | for (size_t i = 0; i < data_length; i++) { 369 | uint8_t byte = data[i]; 370 | for (int j = 0; j < 8; j++) { 371 | if ((byte ^ state) & 0x01) { 372 | state = (state >> 1) ^ CRC_POLY; 373 | } else { 374 | state = state >> 1; 375 | } 376 | byte = byte >> 1; 377 | } 378 | } 379 | return state; 380 | } 381 | 382 | static uint8_t reverse_bits(uint8_t data) { 383 | uint8_t result = 0; 384 | for (int i = 0; i < 8; i++) { 385 | result <<= 1; 386 | result |= data & 1; 387 | data >>= 1; 388 | } 389 | return result; 390 | } 391 | 392 | 393 | 394 | -------------------------------------------------------------------------------- /PL1167_nRF24.h: -------------------------------------------------------------------------------- 1 | /* 2 | * PL1167_nRF24.h 3 | * 4 | * Created on: 29 May 2015 5 | * Author: henryk 6 | */ 7 | 8 | #ifdef ARDUINO 9 | #include "Arduino.h" 10 | #endif 11 | 12 | #include "AbstractPL1167.h" 13 | #include "RF24.h" 14 | 15 | #ifndef PL1167_NRF24_H_ 16 | #define PL1167_NRF24_H_ 17 | 18 | class PL1167_nRF24 : public AbstractPL1167 { 19 | public: 20 | PL1167_nRF24(RF24 &radio); 21 | int open(); 22 | int setPreambleLength(uint8_t preambleLength); 23 | int setSyncword(uint16_t syncword0, uint16_t syncword3); 24 | int setTrailerLength(uint8_t trailerLength); 25 | int setCRC(bool crc); 26 | int setMaxPacketLength(uint8_t maxPacketLength); 27 | int writeFIFO(const uint8_t data[], size_t data_length); 28 | int transmit(uint8_t channel); 29 | int receive(uint8_t channel); 30 | int readFIFO(uint8_t data[], size_t &data_length); 31 | 32 | private: 33 | RF24 &_radio; 34 | 35 | bool _crc; 36 | uint8_t _preambleLength = 1; 37 | uint16_t _syncword0 = 0, _syncword3 = 0; 38 | uint8_t _syncwordLength = 4; 39 | uint8_t _trailerLength = 4; 40 | uint8_t _maxPacketLength = 8; 41 | 42 | uint8_t _channel = 0; 43 | 44 | uint8_t _nrf_pipe[5]; 45 | uint8_t _nrf_pipe_length; 46 | 47 | uint8_t _packet_length = 0; 48 | uint8_t _receive_length = 0; 49 | uint8_t _preamble = 0; 50 | uint8_t _packet[32]; 51 | bool _received = false; 52 | 53 | int recalc_parameters(); 54 | int internal_receive(); 55 | 56 | }; 57 | 58 | 59 | #endif /* PL1167_NRF24_H_ */ 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | canny 2 | ===== 3 | 4 | Canny is a \$7 (hw components cost) implementation of a controller for the cheap 5 | bulbs sold under the brands MiLight, LimitlessLed, Easybulb or any other 6 | futlight clone. 7 | 8 | It uses a ESP8266 device and a nRF24L01 radio module to directly interface with 9 | the compatible bulbs. 10 | 11 | ![](https://cdn.muzzley.com/things/profiles/futlight/profile.jpg) 12 | 13 | Features 14 | -------- 15 | 16 | Canny pretends to overcome the limitations of these devices by bringing 17 | additional features and reliability. 18 | 19 | **Gateway capabilities:** 20 | 21 | - Individually control up to 2048 bulbs 22 | 23 | **Software capabilities:** 24 | 25 | - Manual control over bulbs or groups of bulbs 26 | 27 | - Geofence rules 28 | 29 | - Timers 30 | 31 | - Bulbs as notification devices for smartphone calls, others 32 | 33 | **IoT capabilities:** 34 | 35 | - Expandable to other usages over the Muzzley platform 36 | 37 | **Security:** 38 | 39 | - Additional security to standard milight/easybulb bridge authentication 40 | 41 |   42 | 43 | Requirements 44 | ------------ 45 | 46 | Canny relies on specific hardware and software requirements. 47 | 48 | **Hardware:** 49 | 50 | - ESP8266 compatible module (amazon 51 | [link](https://www.amazon.com/NodeMcu-Internet-Things-Development-ESP8266/dp/B01H701G6M/ref=sr_1_16?s=pc&ie=UTF8&qid=1486169007&sr=1-16&keywords=esp8266)) 52 | 53 | - nRF24L01 radio module (amazon 54 | [link](https://www.amazon.com/Leatest-2-4Ghz-nRF24L01-Transceiver-Module/dp/B007ZZANPA/ref=sr_1_1?s=pc&ie=UTF8&qid=1486169071&sr=1-1&keywords=nRF24L01)) 55 | 56 | - some wiring to connect the ESP8266 and nRF24L01 (amazon 57 | [link](https://www.amazon.com/Header-Copper-Flexible-Arduino-Breadboard/dp/B0126HIIDA/ref=sr_1_3?s=pc&ie=UTF8&qid=1486169146&sr=1-3&keywords=arduino+wire)) 58 | 59 | **Software:** 60 | 61 | - Arduino.cc IDE ([link](http://arduino.cc/)) 62 | 63 | - Arduino.cc with opened Canny project file (this project). 64 | 65 | - Arduino IDE: Installed support for the ESP8266 board: "Tools -\> Board”, 66 | selecting the correct ESP module 67 | 68 | - Arduino IDE: Correctly installed library: wifi manager 69 | 70 | - Arduino IDE: Correctly installed library: “MQTT” by Joel Gaelwiler 71 | 72 | - Arduino IDE: Correctly installed library: “arduino json” by Benoit Blanchon 73 | 74 | - Arduino IDE: Correctly installed library: “RF24” by TMRh20 75 | 76 |   77 | 78 | Setup 79 | ----- 80 | 81 | ### Setup: Hardware Setup 82 | 83 | The hardware setup is quite simple if you have a ESP8266 development board with 84 | physical pins for IO. All you have to do it connect both hardware modules and 85 | power the main board. 86 | 87 | - **DIAGRAM** 88 | 89 | The above diagram illustrates the connectivity between the ESP8266 and the 90 | nRF24L01 module. 91 | 92 | ![](https://raw.githubusercontent.com/djsb/canny/master/ReadmeImages/nodemcu-nrf24l01-muzzley-crop.png) 93 | 94 | - **PICTURE** 95 | 96 | ![](https://raw.githubusercontent.com/djsb/canny/master/ReadmeImages/nodemcu-nrf24l01-muzzley-real.jpg) 97 | 98 | ### Setup: Muzzley developer account 99 | 100 | This step is optional and you should consider it, if you’d like to fork this 101 | project and have your own implementation or use Muzzley for business features 102 | (example: have your own controlling App). 103 | 104 | Otherwise, just proceed to the next step. 105 | 106 |   107 | 108 | ### Setup: ESP8266 Arduino IDE Environment 109 | 110 | 1. **Increase MQTT buffer size:** Update the installed “MQTT” by Joel Gaelwiler 111 | library by editing the file “MQTTClient.h” inside the “src” folder from 112 | "\#define MQTT_BUFFER_SIZE 128” to "\#define MQTT_BUFFER_SIZE 512” 113 | 114 | 2. Open the Canny.ino project into Arduino.cc IDE 115 | 116 | 3. Compile 117 | 118 | 4. Push code into arduino 119 | 120 | *Note: If you have gone through the step “Setup: Muzzley developer account”, you 121 | need to update Muzzley’s credentials at canny.ino file. Variables to update: 122 | MProfileID, MUUID and MAppToken.* 123 | 124 |   125 | 126 | ### Setup: Configuring and Uploading Code 127 | 128 |   129 | 130 | First Time Usage 131 | ---------------- 132 | 133 | Find the first usage guidelines, once you have deployed the software onto the 134 | ESP8266 and you have all the hardware setup done. 135 | 136 | ### First Time Usage: Configuring WiFi 137 | 138 | For first time you deploy the software into the ESP8266 or if it cannot connect 139 | to your WiFi network, it will automatically enter in configuration mode. You 140 | should to the following steps: 141 | 142 | 1. Connect your smartphone to a WiFi network named: “**CANNY-SETUP**“. 143 | [Screenshot 144 | link](https://github.com/djsb/canny/blob/master/ReadmeImages/IMG_1654.PNG). 145 | 146 | 2. Wait a few seconds until you get a configuration screen. Follow the steps in 147 | order to connect your Canny gateway to your local network. [Screenshot 148 | link](https://github.com/djsb/canny/blob/master/ReadmeImages/IMG_1655.PNG). 149 | 150 | *note: after your configuration, due to a existing bug, you need to manually 151 | reset the ESP8266. Just unplug the power cable.* 152 | 153 |   154 | 155 | ### First Time Usage: Pairing Bulbs 156 | 157 | Once you get Canny connected to your local network, you’ll be able to configure 158 | and control your home bulbs. Steps: 159 | 160 | 1. Connect your smartphone to your local WiFi network (important step!). 161 | 162 | 2. Download and open the Muzzley App. Follow the link: [Google 163 | Play](https://play.google.com/store/apps/details?id=com.muzzley&hl=en), [App 164 | Store](https://itunes.apple.com/pt/app/muzzley/id604133373?l=en&mt=8) or 165 | [Windows 10 166 | Store](https://www.microsoft.com/en-us/store/p/muzzley/9wzdncrdrjk1) 167 | 168 | 3. Once you’re logged on Muzzley, click the **Add Device** button and scroll 169 | down to the Milight, Easybulb (Canny GW). [Screenshot 170 | link](https://github.com/djsb/canny/blob/master/ReadmeImages/IMG_1656.PNG). 171 | 172 | 4. Make sure your smartphone is connected to the same network as your Canny GW. 173 | Hit the Search button. Note: This is only for setup; you can later control 174 | the bulbs from the internet. Screenshot link. 175 | 176 | **More bulbs:** Repeat step 3 for each bulb you’d like to add to the Muzzley 177 | app. 178 | 179 |   180 | 181 | After you add a bulb to the Muzzley App you need to pair each entry to a 182 | physical bulb. Do the following: 183 | 184 | 1. On the Muzzley App press the bulb you’ve just added. [Screenshot 185 | link](https://github.com/djsb/canny/blob/master/ReadmeImages/IMG_1657.PNG). 186 | 187 | 2. You’ll find a pairing button on the top left corner of the screen. Press it. 188 | [Screenshot 189 | link](https://github.com/djsb/canny/blob/master/ReadmeImages/IMG_1658.PNG). 190 | 191 | 3. At the same time: Turn the bulb on (physical switch) and press the “Pair / 192 | Unpair” button. [Screenshot 193 | link](https://github.com/djsb/canny/blob/master/ReadmeImages/IMG_1659.PNG). 194 | 195 | 4. Your light should blink. If not, repeat step 3. 196 | 197 |   198 | 199 | Features & Screenshots 200 | ---------------------- 201 | 202 | These are some of the features and screenshots of the Muzzley App. 203 | 204 | ![](https://raw.githubusercontent.com/djsb/canny/master/ReadmeImages/screens.png) 205 | 206 |   207 | 208 | Improvements 209 | ------------ 210 | 211 | Please send your feedback or pull requests. 212 | 213 | Feature improvements: 214 | 215 | - Add hardreset button to clear wifi and paired bulbs configuration 216 | 217 | - Better Support for white only bulbs 218 | 219 | - Store bulbs status to circumvent the protocol unidirectional limitations 220 | 221 | - Pair with remote controls and allow them to control other Muzzley compatible 222 | devices 223 | 224 | Other project improvements: 225 | 226 | - 3D print case 227 | 228 | Credits 229 | ------- 230 | 231 | This project uses libraries from other people as well as research made on the 232 | milight protocol. Some credits: 233 | 234 | - Henryk Plotz: PL1167 promiscuous reverse engineering over protocol 235 | 236 | - Joel Gaelwiler: MQTT implementation 237 | 238 | - Benoit Blanchon: jSON Parser 239 | 240 | License 241 | ------- 242 | 243 | Otherwise stated on individual project files, Canny's license is MIT. 244 | -------------------------------------------------------------------------------- /ReadmeImages/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/.DS_Store -------------------------------------------------------------------------------- /ReadmeImages/IMG_1654.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/IMG_1654.PNG -------------------------------------------------------------------------------- /ReadmeImages/IMG_1655.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/IMG_1655.PNG -------------------------------------------------------------------------------- /ReadmeImages/IMG_1656.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/IMG_1656.PNG -------------------------------------------------------------------------------- /ReadmeImages/IMG_1657.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/IMG_1657.PNG -------------------------------------------------------------------------------- /ReadmeImages/IMG_1658.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/IMG_1658.PNG -------------------------------------------------------------------------------- /ReadmeImages/IMG_1659.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/IMG_1659.PNG -------------------------------------------------------------------------------- /ReadmeImages/nodemcu-nrf24l01-muzzley-crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/nodemcu-nrf24l01-muzzley-crop.png -------------------------------------------------------------------------------- /ReadmeImages/nodemcu-nrf24l01-muzzley-real.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/nodemcu-nrf24l01-muzzley-real.jpg -------------------------------------------------------------------------------- /ReadmeImages/screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsb/canny/ef6acd8c297d3f259b14841b5347399a712e7faa/ReadmeImages/screens.png -------------------------------------------------------------------------------- /SupportFunctions.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "SupportFunctions.h" 3 | 4 | 5 | // constructor 6 | SupportFunctions::SupportFunctions() 7 | { 8 | 9 | } 10 | 11 | String SupportFunctions::getValue(String data, char separator, int index) { 12 | int found = 0; 13 | int strIndex[] = { 14 | 0, -1 }; 15 | int maxIndex = data.length()-1; 16 | for(int i=0; i<=maxIndex && found<=index; i++){ 17 | if(data.charAt(i)==separator || i==maxIndex){ 18 | found++; 19 | strIndex[0] = strIndex[1]+1; 20 | strIndex[1] = (i == maxIndex) ? i+1 : i; 21 | } 22 | } 23 | return found>index ? data.substring(strIndex[0], strIndex[1]) : ""; 24 | } 25 | 26 | 27 | double SupportFunctions::rgbToHue(byte r, byte g, byte b) { 28 | double rd = (double) r/255; 29 | double gd = (double) g/255; 30 | double bd = (double) b/255; 31 | double max = threeway_max(rd, gd, bd); 32 | double min = threeway_min(rd, gd, bd); 33 | double h, s, l = (max + min) / 2; 34 | 35 | if (max == min) { 36 | h = s = 0; // achromatic 37 | } else { 38 | double d = max - min; 39 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 40 | if (max == rd) { 41 | h = (gd - bd) / d + (gd < bd ? 6 : 0); 42 | } else if (max == gd) { 43 | h = (bd - rd) / d + 2; 44 | } else if (max == bd) { 45 | h = (rd - gd) / d + 4; 46 | } 47 | h /= 6; 48 | } 49 | return h; 50 | } 51 | 52 | double SupportFunctions::threeway_max(double a, double b, double c) { 53 | return max(a, max(b, c)); 54 | } 55 | 56 | double SupportFunctions::threeway_min(double a, double b, double c) { 57 | 58 | return min(a, min(b, c)); 59 | } 60 | 61 | String SupportFunctions::macToStr(const uint8_t* mac) 62 | { 63 | String result; 64 | for (int i = 0; i < 6; ++i) { 65 | result += String(mac[i], 16); 66 | if (i < 5) 67 | result += ':'; 68 | } 69 | return result; 70 | } 71 | 72 | String SupportFunctions::randomStr(int length, String *randomstring) 73 | { 74 | 75 | char letters[] = {'a', 'b','c', 'd', 'e', 'f', 'g', 'h', 'i', 'j','k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'x', 'y', 'z'}; 76 | const byte lettersLength = sizeof(letters) / sizeof(letters[0]); 77 | 78 | for (int n = 0; n < length ; n++) 79 | { 80 | *randomstring = String(*randomstring + letters[random(0, lettersLength)]); 81 | } 82 | 83 | 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /SupportFunctions.h: -------------------------------------------------------------------------------- 1 | #ifndef SupportFunctions_h 2 | #define SupportFunctions_h 3 | 4 | class SupportFunctions 5 | { 6 | public: 7 | SupportFunctions(); 8 | String getValue(String data, char separator, int index); 9 | double rgbToHue(byte r, byte g, byte b); 10 | double threeway_max(double a, double b, double c); 11 | double threeway_min(double a, double b, double c); 12 | String macToStr(const uint8_t* mac); 13 | String randomStr(int length, String *randomstring); 14 | private: 15 | }; 16 | 17 | #endif 18 | 19 | -------------------------------------------------------------------------------- /canny.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "RF24.h" 8 | #include "PL1167_nRF24.h" 9 | #include "MiLightRadio.h" 10 | #include 11 | #include 12 | #include 13 | #include "ESP8266SSDP.h" // for muzzley local discovery 14 | #include "Data.h" 15 | #include "SupportFunctions.h" 16 | #include "MuzzleyRegister.h" 17 | #include 18 | 19 | // Muzzley Configuration. Get these from the developers selfcare at www.muzzley.com 20 | char MProfileID[25] = "56f5ac9fca1a124ae2906977"; 21 | char MUUID[37] = "40e30d82-7cd2-477c-b3b0-700e184e0652"; 22 | char MAppToken[17] = "2a3a08767d145205"; 23 | // End of Muzzley configuration 24 | 25 | #define CE_PIN 2 26 | #define CSN_PIN 15 27 | 28 | RF24 radio(CE_PIN, CSN_PIN); 29 | PL1167_nRF24 prf(radio); 30 | MiLightRadio mlr(prf); 31 | int sequence = 0xFF; 32 | int globalcolor = 0x00; 33 | int reset_counter = 0; 34 | 35 | WiFiClientSecure net; 36 | MQTTClient client; 37 | WiFiManager wifiManager; 38 | 39 | ESP8266WebServer HTTP(80); 40 | 41 | Data data; 42 | MuzzleyRegister muzzleyconf; 43 | SupportFunctions sf; 44 | 45 | // ********************* Declaring functions **************************** 46 | void set_status(int ControlIDone, int ControlIDTwo, int ControlIDThree, bool status); 47 | void send_command(int outgoingPckt [7]); 48 | void set_brightness(int ControlIDone, int ControlIDTwo, int ControlIDThree, int bright); 49 | void set_maxwhite(int ControlIDone, int ControlIDTwo, int ControlIDThree); 50 | void circle_brightness(); 51 | void set_color(int ControlIDone, int ControlIDTwo, int ControlIDThree, int color); 52 | void set_pair(int ControlIDone, int ControlIDTwo, int ControlIDThree, String component); 53 | 54 | 55 | // ********************* WiFi, status and first setup ******************* 56 | 57 | //gets called when WiFiManager enters configuration mode 58 | void configModeCallback (WiFiManager *myWiFiManager) { 59 | Serial.println("Entered config mode"); 60 | Serial.println(WiFi.softAPIP()); 61 | //if you used auto generated SSID, print it 62 | Serial.println(myWiFiManager->getConfigPortalSSID()); 63 | //entered config mode, make led toggle faster 64 | } 65 | 66 | 67 | 68 | 69 | void connect(); // <- predefine connect() for setup() 70 | 71 | void setup() { 72 | Serial.begin(115200); 73 | 74 | SPIFFS.begin(); 75 | //SPIFFS.format(); Serial.println("SPIFS Formatted"); delay(50000); 76 | 77 | //WiFiManager 78 | //Local intialization. Once its business is done, there is no need to keep it around 79 | WiFiManager wifiManager; 80 | 81 | //reset settings - for testing 82 | //wifiManager.resetSettings(); 83 | 84 | //set callback that gets called when connecting to previous WiFi fails, and enters Access Point mode 85 | wifiManager.setAPCallback(configModeCallback); 86 | 87 | //fetches ssid and pass and tries to connect 88 | //if it does not connect it starts an access point with the specified name 89 | //here "AutoConnectAP" 90 | //and goes into a blocking loop awaiting configuration 91 | if (!wifiManager.autoConnect("CANNY-SETUP")) { 92 | Serial.println("failed to connect and hit timeout"); 93 | //reset and try again, or maybe put it to deep sleep 94 | ESP.reset(); 95 | delay(1000); 96 | } 97 | 98 | 99 | delay(200); 100 | // init Muzzley DeviceKey and SerialNumber 101 | String deviceKey; 102 | uint8_t mac[6]; 103 | WiFi.macAddress(mac); 104 | String serialNumber = sf.macToStr(mac); 105 | 106 | muzzleyconf.getDeviceKey(&deviceKey, &serialNumber); 107 | if (deviceKey == ""){ 108 | Serial.println("D: 1001"); 109 | 110 | HTTPClient httpclient; 111 | httpclient.begin("http://global-manager.muzzley.com/deviceapp/register"); 112 | 113 | httpclient.POST("{\"profileId\": \""+String(MProfileID)+"\",\"serialNumber\": \"" + serialNumber + "\"}"); 114 | 115 | muzzleyconf.registerDeviceKey(serialNumber, httpclient.getString(), &deviceKey); 116 | httpclient.end(); 117 | ESP.restart(); 118 | }else{ 119 | // found device Key 120 | Serial.println("D: 1000"); 121 | } 122 | 123 | 124 | 125 | 126 | // initialize SSD discovery 127 | char urn[50]; // allocate enough memory for Chord[navigator], " : " and Camelot[navigator] 128 | strcpy(urn, "urn:Muzzley:device:"); 129 | strcat(urn, MProfileID); 130 | strcat(urn, ":1"); 131 | //char urn[48] = "urn:Muzzley:device:".MProfileID.":1"; 132 | SSDP.setDeviceType(urn); 133 | SSDP.setSchemaURL("description.xml"); 134 | SSDP.setHTTPPort(80); 135 | SSDP.setName("Muzzley"); 136 | SSDP.setSerialNumber(serialNumber); 137 | SSDP.setMACAddress(sf.macToStr(mac)); 138 | SSDP.setURL("index.html"); 139 | SSDP.setModelName("Muzzley"); 140 | //SSDP.setModelNumber("929000226503"); 141 | SSDP.setModelURL("http://www.canny.io"); 142 | SSDP.setManufacturer("Canny"); 143 | SSDP.setManufacturerURL("http://canny.io"); 144 | SSDP.setmProfileID(MProfileID); 145 | SSDP.setDeviceKey(deviceKey); 146 | SSDP.begin(); 147 | 148 | 149 | 150 | // initiliaze web server 151 | HTTP.on("/index.html", HTTP_GET, [](){ 152 | Serial.println("HTTP index.html request"); 153 | HTTP.send(200, "text/plain", "Future Canny configuration tool!"); 154 | }); 155 | HTTP.on("/description.xml", HTTP_GET, [](){ 156 | //Serial.println("\ndescription.xml requested"); 157 | SSDP.schema(HTTP.client()); 158 | }); 159 | HTTP.begin(); 160 | 161 | 162 | 163 | client.begin("geoplatform.muzzley.com", 8883, net); // MQTT brokers usually use port 8883 for secure connections 164 | //connect(); 165 | 166 | //data.mountbulbdata(); 167 | 168 | delay (200); 169 | mlr.begin(); 170 | 171 | } 172 | 173 | 174 | 175 | void connect() { 176 | // Loop until we're reconnected 177 | while (!client.connected()) { 178 | Serial.print("\nAttempting MQTT connection..."); 179 | delay (10); // TODO djsb - crash without this 180 | if (client.connect("ESP8266Client", MUUID, MAppToken)) { 181 | Serial.println("connected"); 182 | String deviceKey; 183 | String serialNumber; 184 | muzzleyconf.getDeviceKey(&deviceKey, &serialNumber); 185 | String substopic = "v1/iot/profiles/"+String(MProfileID)+"/channels/"+deviceKey+"/#"; 186 | client.subscribe(substopic); 187 | } else { 188 | Serial.print("failed, rc="); 189 | Serial.println(" try again in 5 seconds"); 190 | delay(1000); 191 | } 192 | } 193 | } 194 | 195 | 196 | void loop() { 197 | client.loop(); 198 | delay(20); 199 | 200 | if (!client.connected()) { 201 | connect(); 202 | } 203 | 204 | 205 | 206 | if (mlr.available()) { 207 | uint8_t packet[7]; 208 | size_t packet_length = sizeof(packet); 209 | mlr.read(packet, packet_length); 210 | printf("\nReceived packet: "); 211 | for (int i = 0; i < packet_length; i++) { 212 | printf("%02X ", packet[i]); 213 | } 214 | } 215 | 216 | // check if the reset button is pressed. If so, enable reset procedure 217 | // TODO: The reset procedure is making the device crash 218 | 219 | // handle http 220 | HTTP.handleClient(); 221 | } 222 | 223 | 224 | void messageReceived(String topic, String payload, char * bytes, unsigned int length) { 225 | Serial.print("\nIncoming message: \n - "); 226 | Serial.print(topic); 227 | Serial.print("\n - "); 228 | Serial.print(payload); 229 | Serial.println(); 230 | 231 | String component = sf.getValue(topic, '/', 7); 232 | String properties = sf.getValue(topic, '/', 9); 233 | 234 | // get bulb details (if bulb is known) 235 | String config = data.get_config(); 236 | DynamicJsonBuffer jsonBuf; 237 | JsonObject& bulb = jsonBuf.parseObject(config); 238 | 239 | // initialize bulb properties 240 | int ControlIDone = 0; 241 | int ControlIDTwo = 0; 242 | int ControlIDThree = 0; 243 | bool status = false; 244 | int brightness = 0; 245 | int colorr = 0; 246 | int colorg = 0; 247 | int colorb = 0; 248 | 249 | // check if this bulb exists in the json bulb data 250 | if (bulb.containsKey(component)) 251 | { 252 | //Serial.println("messageReceived(): Bulb already exists"); 253 | ControlIDone = bulb[component]["ControlIDone"]; 254 | ControlIDTwo = bulb[component]["ControlIDTwo"]; 255 | ControlIDThree = bulb[component]["ControlIDThree"]; 256 | status = bulb[component]["state"]["status"]; 257 | brightness = bulb[component]["state"]["brightness"]; 258 | colorr = bulb[component]["state"]["colorr"]; 259 | colorg = bulb[component]["state"]["colorg"]; 260 | colorb = bulb[component]["state"]["colorb"]; 261 | }else{ 262 | //Serial.println("messageReceived(): Bulb does not yet exist"); 263 | data.add_bulb(component); 264 | // after updating the json with the new bulb, reload the json 265 | config = data.get_config(); 266 | JsonObject& bulb = jsonBuf.parseObject(config); 267 | } 268 | 269 | bulb.printTo(Serial); 270 | 271 | ControlIDone = bulb[component]["ControlIDone"]; 272 | ControlIDTwo = bulb[component]["ControlIDTwo"]; 273 | ControlIDThree = bulb[component]["ControlIDThree"]; 274 | status = bulb[component]["state"]["status"]; 275 | brightness = bulb[component]["state"]["brightness"]; 276 | colorr = bulb[component]["state"]["colorr"]; 277 | colorg = bulb[component]["state"]["colorg"]; 278 | colorb = bulb[component]["state"]["colorb"]; 279 | 280 | // parse topic 281 | if (properties == "assign-button"){ 282 | set_pair(ControlIDone, ControlIDTwo, ControlIDThree, component); 283 | } 284 | 285 | // parse json payload 286 | StaticJsonBuffer<400> jsonBuffer; 287 | JsonObject& root = jsonBuffer.parseObject(payload); 288 | 289 | // check if the following operations are actions to perform on the device 290 | if ((strcmp (root["io"],"w") == 0)){ 291 | 292 | // Set status (on/off) 293 | if ((properties == "status")){ 294 | bool status = root["data"]["value"]; 295 | set_status(ControlIDone, ControlIDTwo, ControlIDThree, status); 296 | data.update_bulb_state(component, properties, status); 297 | } 298 | 299 | // Set color RGB 300 | if ((properties == "color")){ 301 | byte r = root["data"]["value"]["r"]; 302 | byte g = root["data"]["value"]["g"]; 303 | byte b = root["data"]["value"]["b"]; 304 | int hue = int(sf.rgbToHue(r,g,b) * 100); 305 | 306 | // if the user presses the white color button, override the HUE to 1024. 307 | if ((r == 255) && (g == 255) && (b == 255)){ 308 | set_maxwhite(ControlIDone, ControlIDTwo, ControlIDThree); 309 | } 310 | else{ 311 | set_color(ControlIDone, ControlIDTwo, ControlIDThree, hue); 312 | } 313 | 314 | // update the bulb state for color r, g, b 315 | int colorr = root["data"]["value"]["r"]; 316 | int colorg = root["data"]["value"]["g"]; 317 | int colorb = root["data"]["value"]["b"]; 318 | data.update_bulb_state(component, "colorr", colorr); 319 | data.update_bulb_state(component, "colorg", colorg); 320 | data.update_bulb_state(component, "colorb", colorb); 321 | } 322 | 323 | // Set brightness. From 0 to 1 324 | if ((properties == "brightness")){ 325 | double bright = root["data"]["value"]; 326 | int brightness = bright*100; 327 | set_brightness(ControlIDone, ControlIDTwo, ControlIDThree, bright*100); 328 | data.update_bulb_state(component, properties, brightness); 329 | } 330 | } // end of Muzzley IO Write actions 331 | } 332 | 333 | // ******************* Mi Light Features **************** 334 | 335 | void set_maxwhite(int ControlIDone, int ControlIDTwo, int ControlIDThree){ 336 | int outgoingPacket2 [7] = { ControlIDone, ControlIDTwo, ControlIDThree, 0x00, 0x00, 0x13, sequence++}; 337 | send_command(outgoingPacket2); 338 | } 339 | 340 | 341 | void circle_brightness(int ControlIDone, int ControlIDTwo, int ControlIDThree){ 342 | 343 | for (int brightnessi=1; brightnessi<=100; brightnessi++){ 344 | set_brightness(ControlIDone, ControlIDTwo, ControlIDThree, brightnessi); 345 | } 346 | } 347 | 348 | void set_color(int ControlIDone, int ControlIDTwo, int ControlIDThree, int color){ 349 | int newcolor = map(color, 0, 99, 0x00, 0xFF); 350 | 351 | int counter=0; 352 | int newbright = 0x90; 353 | counter++; 354 | 355 | int outgoingPacket2 [7] = { ControlIDone, ControlIDTwo, ControlIDThree, newcolor, 0xA8, 0x0F, sequence++}; 356 | send_command(outgoingPacket2); 357 | 358 | globalcolor = newcolor; 359 | } 360 | 361 | void set_brightness(int ControlIDone, int ControlIDTwo, int ControlIDThree, int bright){ 362 | int brightnessi = map(bright, 0, 100, 1, 28); 363 | int counter=0; 364 | int newbright = 0x90; 365 | counter++; 366 | if (brightnessi >= 1 && brightnessi <=19){ 367 | newbright = 0x88 - (brightnessi * 0x08); 368 | } 369 | 370 | if (brightnessi > 19 && brightnessi <=28){ 371 | newbright = 0xF8 - ((brightnessi-20) * 0x08); 372 | } 373 | 374 | int outgoingPacket2 [7] = { ControlIDone, ControlIDTwo, ControlIDThree, globalcolor++, newbright, 0x0E, sequence++}; 375 | send_command(outgoingPacket2); 376 | 377 | } 378 | 379 | void set_status(int ControlIDone, int ControlIDTwo, int ControlIDThree, bool status){ 380 | // 0x03 for on, 0x04 for off 381 | int bulbstatus = 0x03; 382 | if (status==true) 383 | bulbstatus = 0x03; 384 | if (status==false) 385 | bulbstatus = 0x04; 386 | 387 | int outgoingPacket2 [7] = { ControlIDone, ControlIDTwo, ControlIDThree, globalcolor, 0XD1, bulbstatus, sequence++}; 388 | send_command(outgoingPacket2); 389 | } 390 | 391 | 392 | void set_pair(int ControlIDone, int ControlIDTwo, int ControlIDThree, String component){ 393 | 394 | // search ID of component 395 | Serial.println(component); 396 | 397 | 398 | int outgoingPacket2 [7] = { ControlIDone, ControlIDTwo, ControlIDThree, globalcolor, 0XD1, 0x03, sequence++}; 399 | send_command(outgoingPacket2); 400 | 401 | int outgoingPacket3 [7] = { ControlIDone, ControlIDTwo, ControlIDThree, globalcolor, 0xD1, 0x13, sequence++}; 402 | send_command(outgoingPacket3); 403 | delay (300); 404 | send_command(outgoingPacket3); 405 | } 406 | 407 | void send_command(int outgoingPckt [7]){ 408 | 409 | uint8_t outgoingPacket_tmp [7] ; 410 | for (int i = 0; i < 7; i++) 411 | { 412 | outgoingPacket_tmp[i] = (uint8_t)outgoingPckt[i]; 413 | } 414 | 415 | mlr.write(outgoingPacket_tmp, sizeof(outgoingPacket_tmp)); 416 | // printf("\nSending packet: "); 417 | for (int ps = 0; ps < sizeof(outgoingPacket_tmp); ps++) { 418 | printf("%02X ", outgoingPacket_tmp[ps]); 419 | } 420 | delay(50); 421 | 422 | // send the instruction multiple times 423 | for (int resendcounter = 0; resendcounter < 50; resendcounter++) 424 | { 425 | mlr.resend(); 426 | delay(1); 427 | } 428 | printf("Sent!"); 429 | } 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | -------------------------------------------------------------------------------- /muzzleyprofilespec/profilespec.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": [ 3 | { 4 | "id": "bulb", 5 | "label": "Bulb", 6 | "classes": "[\"com.muzzley.components.lightbulb\"]" 7 | }, 8 | { 9 | "id": "bridge", 10 | "label": "Bridge", 11 | "classes": "[\"com.muzzley.components.bridge\"]" 12 | } 13 | ], 14 | "properties": [ 15 | { 16 | "id": "status", 17 | "label": "Status", 18 | "agentLabel": "", 19 | "classes": "[\"com.muzzley.properties.status\"]", 20 | "mainInformationPriority": 0, 21 | "informationPresentation": { 22 | "type": "", 23 | "options": "" 24 | }, 25 | "unitsOptions": { 26 | "targetImperial": "", 27 | "targetMetric": "", 28 | "muzzleyUnit": "" 29 | }, 30 | "mainActionPriority": 0, 31 | "actionPresentation": { 32 | "type": "", 33 | "options": "" 34 | }, 35 | "schema": "https://ontology.muzzley.com/schemas/v1/status-onoff", 36 | "schemaExtension": "{}", 37 | "isTriggerable": true, 38 | "isActionable": true, 39 | "requiredCapabilities": "[]", 40 | "controlInterfaces": [ 41 | { 42 | "id": "ti1", 43 | "controlInterface": "toggle-picker", 44 | "config": "{\"options\":[{\"value\":true,\"label\":\"Ligado\"},{\"value\":false,\"label\":\"Desligado\"}]}" 45 | } 46 | ], 47 | "triggers": [ 48 | { 49 | "id": "8548c9cd-74c3-4b07-b1db-1c23c0f5774f", 50 | "condition": "equals", 51 | "predicateLabel": "is equal to", 52 | "inputsLabel": "{{value}}", 53 | "inputs": [ 54 | { 55 | "id": "value", 56 | "controlInterfaceId": "ti1", 57 | "path": "[{\"source\":\"selection.value\",\"target\":\"data.value\"}]" 58 | } 59 | ], 60 | "label": "" 61 | } 62 | ], 63 | "actions": [ 64 | { 65 | "id": "93639822-d1ed-4495-991b-4c7a34aba55a", 66 | "label": "", 67 | "inputsLabel": "{{value}}", 68 | "inputs": [ 69 | { 70 | "controlInterfaceId": "ti1", 71 | "path": "[{\"source\":\"selection.value\",\"target\":\"data.value\"}]" 72 | } 73 | ] 74 | } 75 | ], 76 | "io": "rws", 77 | "onChange": false, 78 | "rateLimit": 0, 79 | "components": "[\"bulb\"]" 80 | }, 81 | { 82 | "id": "color", 83 | "label": "Color", 84 | "agentLabel": "", 85 | "classes": "[\"com.muzzley.properties.lights.color.rgb\",\"com.muzzley.properties.lights.color\",\"com.muzzley.properties.color\"]", 86 | "mainInformationPriority": 0, 87 | "informationPresentation": { 88 | "type": "", 89 | "options": "" 90 | }, 91 | "unitsOptions": { 92 | "targetImperial": "", 93 | "targetMetric": "", 94 | "muzzleyUnit": "" 95 | }, 96 | "mainActionPriority": 0, 97 | "actionPresentation": { 98 | "type": "", 99 | "options": "" 100 | }, 101 | "schema": "https://ontology.muzzley.com/schemas/v1/color-rgb", 102 | "schemaExtension": "{}", 103 | "isTriggerable": false, 104 | "isActionable": true, 105 | "requiredCapabilities": "[]", 106 | "controlInterfaces": [ 107 | { 108 | "id": "colorI1", 109 | "controlInterface": "color-picker", 110 | "config": "{\"mode\":\"rgb\"}" 111 | } 112 | ], 113 | "triggers": [], 114 | "actions": [ 115 | { 116 | "id": "9fbfe992-dbca-4eca-ab8f-f6ec9d214cc9", 117 | "label": "set color to", 118 | "inputsLabel": "{{value}}", 119 | "inputs": [ 120 | { 121 | "controlInterfaceId": "colorI1", 122 | "path": "[{\"source\":\"selection.value\",\"target\":\"data.value\"}]" 123 | } 124 | ] 125 | } 126 | ], 127 | "io": "rws", 128 | "onChange": false, 129 | "rateLimit": 0, 130 | "components": "[\"bulb\"]" 131 | }, 132 | { 133 | "id": "brightness", 134 | "label": "Brightness", 135 | "agentLabel": "", 136 | "classes": "[\"com.muzzley.properties.lights.brightness\",\"com.muzzley.properties.lights.brightness\",\"com.muzzley.properties.brightness\"]", 137 | "mainInformationPriority": 0, 138 | "informationPresentation": { 139 | "type": "", 140 | "options": "" 141 | }, 142 | "unitsOptions": { 143 | "targetImperial": "", 144 | "targetMetric": "", 145 | "muzzleyUnit": "" 146 | }, 147 | "mainActionPriority": 0, 148 | "actionPresentation": { 149 | "type": "", 150 | "options": "" 151 | }, 152 | "schema": "https://ontology.muzzley.com/schemas/v1/brightness", 153 | "schemaExtension": "{\"minimum\":0,\"maximum\":1}", 154 | "isTriggerable": true, 155 | "isActionable": true, 156 | "requiredCapabilities": "[]", 157 | "controlInterfaces": [ 158 | { 159 | "id": "brigI1", 160 | "controlInterface": "number-picker", 161 | "config": "{\"min\":0,\"max\":1,\"step\":0.1,\"expression\":\"x*100\",\"suffix\":\"%\"}" 162 | } 163 | ], 164 | "triggers": [ 165 | { 166 | "id": "81d4aab8-794c-49cb-8c5d-46494f6fae08", 167 | "condition": "less-than-or-equal", 168 | "predicateLabel": "drops bellow", 169 | "inputsLabel": "{{value}}", 170 | "inputs": [ 171 | { 172 | "id": "value", 173 | "controlInterfaceId": "brigI1", 174 | "path": "[{\"source\":\"selection.value\",\"target\":\"data.value\"}]" 175 | } 176 | ], 177 | "label": "If brightness less than or equal" 178 | }, 179 | { 180 | "id": "227062b8-bcdc-4bfe-9d48-f203753210c0", 181 | "condition": "greater-than-or-equal", 182 | "predicateLabel": "rises above", 183 | "inputsLabel": "{{value}}", 184 | "inputs": [ 185 | { 186 | "id": "value", 187 | "controlInterfaceId": "brigI1", 188 | "path": "[{\"source\":\"selection.value\",\"target\":\"data.value\"}]" 189 | } 190 | ], 191 | "label": "If brightness greater than or equal" 192 | } 193 | ], 194 | "actions": [ 195 | { 196 | "id": "b4467f6b-9908-446c-a3fe-199a0312a402", 197 | "label": "", 198 | "inputsLabel": "{{value}}", 199 | "inputs": [ 200 | { 201 | "controlInterfaceId": "brigI1", 202 | "path": "[{\"source\":\"selection.value\",\"target\":\"data.value\"}]" 203 | } 204 | ] 205 | } 206 | ], 207 | "io": "rws", 208 | "onChange": false, 209 | "rateLimit": 0, 210 | "components": "[\"bulb\"]" 211 | }, 212 | { 213 | "id": "assign-button", 214 | "label": "Assign bulb to button", 215 | "agentLabel": "", 216 | "classes": "[\"com.muzzley.properties.lights.assign\",\"com.muzzley.properties.assign\"]", 217 | "mainInformationPriority": 0, 218 | "informationPresentation": { 219 | "type": "", 220 | "options": "" 221 | }, 222 | "unitsOptions": { 223 | "targetImperial": "", 224 | "targetMetric": "", 225 | "muzzleyUnit": "" 226 | }, 227 | "mainActionPriority": 0, 228 | "actionPresentation": { 229 | "type": "", 230 | "options": "" 231 | }, 232 | "schema": "https://ontology.muzzley.com/schemas/v1/string", 233 | "schemaExtension": "{}", 234 | "isTriggerable": false, 235 | "isActionable": false, 236 | "requiredCapabilities": "[]", 237 | "controlInterfaces": [], 238 | "triggers": [], 239 | "actions": [], 240 | "io": "w", 241 | "onChange": false, 242 | "rateLimit": 0, 243 | "components": "[\"bulb\"]" 244 | }, 245 | { 246 | "id": "connection-status", 247 | "label": "Connection status", 248 | "agentLabel": "", 249 | "classes": "[\"com.muzzley.properties.connection.status\",\"com.muzzley.properties.status.connection\"]", 250 | "mainInformationPriority": 0, 251 | "informationPresentation": { 252 | "type": "", 253 | "options": "" 254 | }, 255 | "unitsOptions": { 256 | "targetImperial": "", 257 | "targetMetric": "", 258 | "muzzleyUnit": "" 259 | }, 260 | "mainActionPriority": 0, 261 | "actionPresentation": { 262 | "type": "", 263 | "options": "" 264 | }, 265 | "schema": "https://ontology.muzzley.com/schemas/v1/connection-status", 266 | "schemaExtension": "{}", 267 | "isTriggerable": false, 268 | "isActionable": false, 269 | "requiredCapabilities": "[]", 270 | "controlInterfaces": [], 271 | "triggers": [], 272 | "actions": [], 273 | "io": "rs", 274 | "onChange": false, 275 | "rateLimit": 0, 276 | "components": "[\"bridge\"]" 277 | }, 278 | { 279 | "id": "heartbeat-lights", 280 | "label": "Lights heartbeat", 281 | "agentLabel": "", 282 | "classes": "[\"com.muzzley.properties.heartbeat\"]", 283 | "mainInformationPriority": 0, 284 | "informationPresentation": { 285 | "type": "", 286 | "options": "" 287 | }, 288 | "unitsOptions": { 289 | "targetImperial": "", 290 | "targetMetric": "", 291 | "muzzleyUnit": "" 292 | }, 293 | "mainActionPriority": 0, 294 | "actionPresentation": { 295 | "type": "", 296 | "options": "" 297 | }, 298 | "schema": "https://ontology.muzzley.com/schemas/v1/heartbeat", 299 | "schemaExtension": "{}", 300 | "isTriggerable": false, 301 | "isActionable": false, 302 | "requiredCapabilities": "[]", 303 | "controlInterfaces": [], 304 | "triggers": [], 305 | "actions": [], 306 | "io": "r", 307 | "onChange": false, 308 | "rateLimit": 0, 309 | "components": "[\"bridge\"]" 310 | } 311 | ], 312 | "tiles": [ 313 | { 314 | "label": "Bulb", 315 | "photoUrl": "https://cdn.muzzley.com/things/profiles/futlight/milights-tiles.jpg", 316 | "photoUrlAlt": "https://cdn.muzzley.com/things/profiles/futlight/milights-tiles-centered.jpg", 317 | "isGroupable": true, 318 | "useInterface": true, 319 | "components": [ 320 | "bulb" 321 | ], 322 | "inclusive": false, 323 | "information": [ 324 | { 325 | "property": "color", 326 | "componentType": "bulb", 327 | "type": "icon-color", 328 | "options": "{\"format\":\"rgb\",\"char\":\"\\\\ue600\"}" 329 | }, 330 | { 331 | "property": "brightness", 332 | "componentType": "bulb", 333 | "type": "text-expression", 334 | "options": "{\"suffix\":\"%\",\"prefix\":\"\",\"mathExpression\":\"x * 100\",\"inputPath\":\"data.value\"}" 335 | } 336 | ], 337 | "actions": [ 338 | { 339 | "property": "status", 340 | "componentType": "bulb", 341 | "type": "tri-state", 342 | "options": "{\"mappings\":{\"off\":false,\"on\":true},\"icon\":\"on_off\",\"outputPath\":\"data.value\"}" 343 | } 344 | ] 345 | } 346 | ] 347 | } 348 | --------------------------------------------------------------------------------