/* xdrv_23_zigbee.ino - zigbee support for Tasmota Copyright (C) 2021 Theo Arends and Stephan Hadinger This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_ZIGBEE // Ensure persistence of devices into Flash // // Structure: // (from file info): // uint16 - start address in Flash (offset) // uint16 - length in bytes (makes sure parsing stops) // // First byte: // 0x00 - Empty or V3 format // 0x01-0xFE - Legacy format // 0xFF - invalid // // // V1 Legacy // ========= // File structure: // uint8 - number of devices, 0=none, 0xFF=invalid entry (probably Flash was erased) // // [Array of devices] // [Offset = 2] // uint8 - length of device record // uint16 - short address // uint64 - long IEEE address // uint8 - number of endpoints // [Array of endpoints] // uint8 - endpoint number // uint16 - profileID of the endpoint // Array of uint8 - clusters In codes, 0xFF end marker // Array of uint8 - clusters Out codes, 0xFF end marker // // str - ModelID (null terminated C string, 32 chars max) // str - Manuf (null terminated C string, 32 chars max) // str - FriendlyName (null terminated C string, 32 chars max) // reserved for extensions // -- V2 -- // int8_t - zigbee profile of the device // // ======================= // v3 with version number // File structure: // // uint8 - number of devices, 0=none, 0xFF=invalid entry (probably Flash was erased) // // [Array of devices] // [Offset = 2] // uint8 - length of device record // uint16 - short address // uint64 - long IEEE address // // str - ModelID (null terminated C string, 32 chars max) // str - Manuf (null terminated C string, 32 chars max) // str - FriendlyName (null terminated C string, 32 chars max) // // [Array of endpoints] // uint8 - endpoint number, 0xFF marks the end of endpoints // uint8[] - list of configuration bytes, 0xFF marks the end // i.e. 0xFF-0xFF marks the end of the array of endpoints // // Memory footprint #ifdef ESP8266 const static uint16_t z_spi_start_sector = 0xFF; // Force last bank of first MB const static uint8_t* z_spi_start = (uint8_t*) 0x402FF000; // 0x402FF000 const static uint8_t* z_dev_start = z_spi_start + 0x0800; // 0x402FF800 - 2KB const static size_t z_spi_len = 0x1000; // 4kb blocks const static size_t z_block_offset = 0x0800; const static size_t z_block_len = 0x0800; // 2kb #endif // ESP8266 #ifdef ESP32 uint8_t* z_dev_start; const static size_t z_spi_len = 0x1000; // 4kb blocks const static size_t z_block_offset = 0x0000; // No offset needed const static size_t z_block_len = 0x1000; // 4kb #endif // ESP32 // Each entry consumes 8 bytes class Z_Flashentry { public: uint32_t name; // simple 4 letters name. Currently 'zig1', 'zig2'. 0xFFFFFFFF if not entry uint16_t len; // len of object in bytes, 0xFFFF if no entry uint16_t start; // address of start, 0xFFFF if empty, must be aligned on 128 bytes boundaries }; class Z_Flashdirectory { public: // 8 bytes header uint32_t magic; // magic value 'Tsmt' to check that the block is initialized uint32_t clock; // clock vector to discard entries that are made before this one. This should be incremented by 1 for each new entry (future anti-weavering) // entries, 14*8 = 112 bytes Z_Flashentry entries[14]; uint32_t name; // simple 4 letters name. Currently 'skey', 'crt ', 'crt1', 'crt2' uint16_t len; // len of object uint16_t reserved; // align on 4 bytes boundary // link to next entry, none for now, but may be used for anti-weavering uint16_t next_dir; // 0xFFFF if none uint16_t reserved1; // must be 0xFFFF uint32_t reserved2; // must be 0xFFFFFFFF }; const static size_t Z_MAX_FLASH = z_block_len - sizeof(Z_Flashentry); // 2040 bool hibernateDeviceConfiguration(SBuffer & buf, const class Z_Data_Set & data, uint8_t endpoint) { bool found = false; for (auto & elt : data) { if (endpoint == elt.getEndpoint()) { buf.add8(elt.getConfigByte()); found = true; } } return found; } /*********************************************************************************************\ * hibernateDevice * * Transforms a single device into a SBuffer binary representation. * Only supports v2 (not the legacy old one long forgotten) \*********************************************************************************************/ SBuffer hibernateDevice(const struct Z_Device &device) { SBuffer buf(128); buf.add8(0x00); // overall length, will be updated later buf.add16(device.shortaddr); buf.add64(device.longaddr); char *names[3] = { device.modelId, device.manufacturerId, device.friendlyName }; for (uint32_t i=0; i 32) { len = 32; } // max 32 chars buf.addBuffer(p, len); } buf.add8(0x00); // end of string marker } // check if we need to write fake endpoint 0x00 buf.add8(0x00); if (hibernateDeviceConfiguration(buf, device.data, 0)) { buf.add8(0xFF); // end of configuration } else { buf.setLen(buf.len()-1); // remove 1 byte header } // scan endpoints for (uint32_t i=0; i 250) { devices_size = 250; } // arbitrarily limit to 250 devices in EEPROM instead of 32 in Flash write_data.writeBytes(&devices_size, sizeof(devices_size)); for (const auto & device : zigbee_devices.getDevices()) { const SBuffer buf = hibernateDevice(device); if (buf.len() > 0) { int32_t ret = write_data.writeBytes(buf.getBuffer(), buf.len()); if (ret != buf.len()) { AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "Error writing Devices, written = %d, expected = %d"), ret, buf.len()); return false; } } } return true; } // parse a single string from the saved data // if something wrong happens, returns nullptr to ignore the string // Index d is incremented to just after the string const char * hydrateSingleString(const SBuffer & buf, uint32_t *d) { size_t s_len = buf.strlen(*d); const char * ptr = s_len ? buf.charptr(*d) : ""; *d += s_len + 1; return ptr; } /*********************************************************************************************\ * hydrateSingleDevice * * Transforms a binary representation to a Zigbee device * Supports only v2 \*********************************************************************************************/ void hydrateSingleDevice(const SBuffer & buf_d) { uint32_t d = 1; // index in device buffer uint16_t shortaddr = buf_d.get16(d); d += 2; uint64_t longaddr = buf_d.get64(d); d += 8; size_t buf_len = buf_d.len(); Z_Device & device = zigbee_devices.updateDevice(shortaddr, longaddr); // update device's addresses // ModelId device.setModelId(hydrateSingleString(buf_d, &d)); // ManufID device.setManufId(hydrateSingleString(buf_d, &d)); // FriendlyName device.setFriendlyName(hydrateSingleString(buf_d, &d)); if (d >= buf_len) { return; } // Hue bulbtype - if present while (d < buf_len) { uint8_t ep = buf_d.get8(d++); if (0xFF == ep) { break; } // ep 0xFF marks the end of the endpoints if (ep > 240) { ep = 0xFF; } // ep == 0xFF means ignore device.addEndpoint(ep); // it will ignore invalid endpoints while (d < buf_len) { uint8_t config_type = buf_d.get8(d++); if (0xFF == config_type) { break; } // 0xFF marks the end of congiguration uint8_t config = config_type & 0x0F; Z_Data_Type type = (Z_Data_Type) (config_type >> 4); // set the configuration if (ep != 0xFF) { Z_Data & z_data = device.data.getByType(type, ep); if (&z_data != nullptr) { z_data.setConfig(config); Z_Data_Set::updateData(z_data); } } } } } /*********************************************************************************************\ * loadZigbeeDevices * * Load device configuration from storage. * Order of storage for loading is: 1/ EEPROM 2/ File system 3/ Flash (ESP8266 only) \*********************************************************************************************/ // dump = true, only dump to logs, don't actually load bool loadZigbeeDevices(void) { Univ_Read_File f; // universal reader const char * storage_class = PSTR(""); #ifdef USE_ZIGBEE_EEPROM if (zigbee.eeprom_ready) { f.init(ZIGB_NAME2); storage_class = PSTR("EEPROM"); } #endif // USE_ZIGBEE_EEPROM #ifdef USE_UFILESYS File file; if (!f.valid() && dfsp) { file = dfsp->open(TASM_FILE_ZIGBEE, "r"); if (file) { uint32_t signature = 0x0000; file.read((uint8_t*)&signature, 4); if (signature == ZIGB_NAME2) { // skip another 4 bytes file.read((uint8_t*)&signature, 4); } else { file.seek(0); // seek back to beginning of file } f.init(&file); storage_class = PSTR("File System"); } } #endif // USE_UFILESYS #ifdef ESP8266 if (!f.valid() && flash_valid()) { // Read binary data from Flash Z_Flashentry flashdata; memcpy_P(&flashdata, z_dev_start, sizeof(Z_Flashentry)); // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "z_dev_start %p"), z_dev_start); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "Zigbee signature in Flash: %08X - %d"), flashdata.name, flashdata.len); // Check the signature if ( ((flashdata.name == ZIGB_NAME1) || (flashdata.name == ZIGB_NAME2)) && (flashdata.len > 0)) { uint16_t buf_len = flashdata.len; // uint32_t version = (flashdata.name == ZIGB_NAME2) ? 2 : 1; f.init(z_dev_start + sizeof(Z_Flashentry), buf_len); storage_class = PSTR("Flash"); } } #endif // ESP8266 uint32_t file_len = 0; uint8_t num_devices = 0; if (f.valid()) { file_len = f.len; f.readBytes(&num_devices, sizeof(num_devices)); } if ((file_len < 10) || (num_devices == 0x00) || (num_devices == 0xFF)) { // No data AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "No Zigbee device information")); return false; } AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee device information found in %s (%d devices - %d bytes)"), storage_class, num_devices, file_len); uint32_t k = 1; // byte index in global buffer for (uint32_t i = 0; (i < num_devices) && (k < file_len); i++) { uint8_t dev_record_len = 0; f.readBytes(&dev_record_len, sizeof(dev_record_len)); // int32_t ret = ZFS::readBytes(ZIGB_NAME2, &dev_record_len, 1, k, 1); if (dev_record_len == 0) { AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Invalid device information, aborting")); zigbee_devices.clean(); // don't write back to Flash what we just loaded return false; } SBuffer buf(dev_record_len); buf.setLen(dev_record_len); buf.set8(0, dev_record_len); // push the first byte (len including this first byte) int32_t ret = f.readBytes(buf.buf(1), dev_record_len - 1); // ret = ZFS::readBytes(ZIGB_NAME2, buf.getBuffer(), dev_record_len, k, dev_record_len); if (ret != dev_record_len - 1) { AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Invalid device information, aborting")); zigbee_devices.clean(); // don't write back to Flash what we just loaded return false; } hydrateSingleDevice(buf); // next iteration k += dev_record_len; } zigbee_devices.clean(); // don't write back to Flash what we just loaded return true; } /*********************************************************************************************\ * saveZigbeeDevices * * Save device configuration from storage. * Order of storage for saving is: 1/ EEPROM 2/ File system 3/ Flash (ESP8266 only) \*********************************************************************************************/ void saveZigbeeDevices(void) { Univ_Write_File f; const char * storage_class = PSTR(""); #ifdef USE_ZIGBEE_EEPROM if (!f.valid() && zigbee.eeprom_ready) { f.init(ZIGB_NAME2); storage_class = PSTR("EEPROM"); } #endif #ifdef USE_UFILESYS File file; if (!f.valid() && dfsp) { file = dfsp->open(TASM_FILE_ZIGBEE, "w"); if (file) { f.init(&file); storage_class = PSTR("File System"); } } #endif #if defined(ESP8266) uint8_t *sbuffer = nullptr; static const size_t max_flash_size = 2040; if (!f.valid() && flash_valid()) { sbuffer = (uint8_t*) malloc(max_flash_size); f.init(sbuffer, max_flash_size); storage_class = PSTR("Flash"); } #endif // defined(ESP8266) bool written = false; size_t buf_len = 0; if (f.valid()) { written = hibernateDevices(f); buf_len = f.getCursor(); f.close(); AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee Devices Data saved in %s (%d bytes)"), storage_class, buf_len); } #if defined(ESP8266) if (written && sbuffer != nullptr) { // first copy SPI buffer into ram uint8_t *spi_buffer = (uint8_t*) malloc(z_spi_len); if (!spi_buffer) { AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "Cannot allocate 4KB buffer")); free(sbuffer); return; } ESP.flashRead(z_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE); Z_Flashentry *flashdata = (Z_Flashentry*)(spi_buffer + z_block_offset); flashdata->name = ZIGB_NAME2; // v2 flashdata->len = buf_len; flashdata->start = 0; memcpy(spi_buffer + z_block_offset + sizeof(Z_Flashentry), sbuffer, buf_len); // buffer is now ready, write it back if (ESP.flashEraseSector(z_spi_start_sector)) { ESP.flashWrite(z_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE); } AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee Devices Data store in Flash (0x%08X - %d bytes)"), z_dev_start, buf_len); free(spi_buffer); free(sbuffer); } #endif // defined(ESP8266) } /*********************************************************************************************\ * eraseZigbeeDevices * * Erase all storage locations: 1/ EEPROM, 2/ File system 3/ Flash (ESP8266 only if no filesystem) \*********************************************************************************************/ // Erase the flash area containing the ZigbeeData void eraseZigbeeDevices(void) { zigbee_devices.clean(); // avoid writing data to flash after erase #ifdef USE_ZIGBEE_EEPROM ZFS_Erase(); #endif // USE_ZIGBEE_EEPROM #if defined(ESP8266) && !defined(USE_UFILESYS) // first copy SPI buffer into ram uint8_t *spi_buffer = (uint8_t*) malloc(z_spi_len); if (!spi_buffer) { AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "Cannot allocate 4KB buffer")); return; } // copy the flash into RAM to make local change, and write back the whole buffer ESP.flashRead(z_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE); // Fill the Zigbee area with 0xFF memset(spi_buffer + z_block_offset, 0xFF, z_block_len); // buffer is now ready, write it back if (ESP.flashEraseSector(z_spi_start_sector)) { ESP.flashWrite(z_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE); } free(spi_buffer); AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee Devices Data erased in %s"), PSTR("Flash")); #endif // defined(ESP8266) && !defined(USE_UFILESYS) #ifdef USE_UFILESYS if (TfsDeleteFile(TASM_FILE_ZIGBEE)) { AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee Devices Data erased")); } #endif // USE_UFILESYS } /*********************************************************************************************\ * restoreDumpAllDevices * * Dump all devices in `ZbRestore ` format ready to copy/paste \*********************************************************************************************/ void restoreDumpAllDevices(void) { for (const auto & device : zigbee_devices.getDevices()) { const SBuffer buf = hibernateDevice(device); if (buf.len() > 0) { Response_P(PSTR("{\"" D_PRFX_ZB D_CMND_ZIGBEE_RESTORE "\":\"ZbRestore %_B\"}"), &buf); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR(D_PRFX_ZB D_CMND_ZIGBEE_DATA)); } } } #endif // USE_ZIGBEE