├── .gitignore ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── otau_file.cpp ├── otau_file.h ├── otau_file_loader.cpp ├── otau_file_loader.h ├── otau_model.cpp ├── otau_model.h ├── otau_node.cpp ├── otau_node.h ├── std_otau.pro ├── std_otau_plugin.cpp ├── std_otau_plugin.h ├── std_otau_widget.cpp ├── std_otau_widget.h └── std_otau_widget.ui /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | Makefile.Debug 3 | debug 4 | Makefile.Release 5 | release 6 | ui_std_otau_widget.h 7 | .qmake.stash 8 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | project(std_otau_plugin VERSION 2.22.2 LANGUAGES C;CXX) 4 | 5 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 6 | 7 | set(CMAKE_AUTOUIC ON) 8 | set(CMAKE_AUTOMOC ON) 9 | set(CMAKE_AUTORCC ON) 10 | 11 | set(CMAKE_CXX_STANDARD 14) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | 14 | set(CMAKE_C_VISIBILITY_PRESET hidden) 15 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 16 | 17 | find_package(Qt5 COMPONENTS Core Widgets REQUIRED) 18 | 19 | #--- deCONZ library --------------------------------------------------- 20 | 21 | include(FetchContent) 22 | 23 | if (NOT DECONZ_FULL_BUILD) 24 | 25 | FetchContent_Declare( 26 | deconzlib 27 | GIT_REPOSITORY https://github.com/dresden-elektronik/deconz-lib.git 28 | GIT_TAG main 29 | ) 30 | # FetchContent_MakeAvailable(deconzlib) 31 | # FetchContent_MakeAvailable requires CMake 3.14, but Debian Buster has only 3.13 32 | FetchContent_GetProperties(deconzlib) 33 | if (NOT deconzlib_POPULATED) 34 | FetchContent_Populate(deconzlib) 35 | add_subdirectory(${deconzlib_SOURCE_DIR} ${deconzlib_BINARY_DIR}) 36 | endif() 37 | 38 | # --- actor model ----------------------------------------------------- 39 | 40 | FetchContent_Declare( 41 | actormodel 42 | GIT_REPOSITORY https://git.sr.ht/~cryo/actor_model 43 | GIT_TAG master 44 | ) 45 | 46 | # FetchContent_MakeAvailable(actormodel) 47 | # FetchContent_MakeAvailable requires CMake 3.14, but Debian Buster has only 3.13 48 | 49 | FetchContent_GetProperties(actormodel) 50 | if (NOT actormodel_POPULATED) 51 | FetchContent_Populate(actormodel) 52 | add_subdirectory(${actormodel_SOURCE_DIR} ${actormodel_BINARY_DIR}) 53 | endif() 54 | 55 | endif() # NOT DECONZ_FULL_BUILD 56 | #---------------------------------------------------------------------- 57 | 58 | set(PLUGIN_INCLUDE_FILES 59 | std_otau_plugin.h 60 | std_otau_widget.h 61 | otau_file.h 62 | otau_file_loader.h 63 | otau_model.h 64 | otau_node.h 65 | ) 66 | 67 | add_library(${PROJECT_NAME} SHARED 68 | ${PLUGIN_INCLUDE_FILES} 69 | 70 | std_otau_widget.ui 71 | 72 | std_otau_plugin.cpp 73 | std_otau_widget.cpp 74 | otau_file.cpp 75 | otau_file_loader.cpp 76 | otau_model.cpp 77 | otau_node.cpp 78 | ) 79 | 80 | target_compile_definitions(${PROJECT_NAME} PRIVATE USE_ACTOR_MODEL) 81 | 82 | target_link_libraries(${PROJECT_NAME} 83 | PRIVATE Qt5::Core 84 | PRIVATE Qt5::Gui 85 | PRIVATE Qt5::Widgets 86 | PRIVATE deCONZLib 87 | am_plugin_hdr 88 | ) 89 | 90 | #-------------------------------------------------------------- 91 | include(GNUInstallDirs) 92 | 93 | if (UNIX) 94 | if (APPLE) 95 | set_target_properties(${PROJECT_NAME} PROPERTIES 96 | INSTALL_RPATH @loader_path) 97 | else() 98 | set_target_properties(${PROJECT_NAME} PROPERTIES 99 | INSTALL_RPATH "${CMAKE_INSTALL_FULL_LIBDIR}") 100 | endif() 101 | endif() 102 | 103 | 104 | if (${CMAKE_HOST_SYSTEM_NAME} MATCHES "Linux") 105 | install(TARGETS ${PROJECT_NAME} 106 | LIBRARY DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/deCONZ/plugins" 107 | ) 108 | 109 | # following applies only when build as part of deCONZ package 110 | # if (DECONZ_FULL_BUILD) 111 | # endif() 112 | endif() 113 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, dresden elektronik ingenieurtechnik gmbh 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STD OTAU Plugin 2 | The deCONZ STD OTAU plugin implements a ZigBee Over-the-Air-Upgrade (OTAU) server in deCONZ. 3 | 4 | ## Upgrading Firmware 5 | See the [Phoscon help topics](https://phoscon.de/en/support#ota-update-osram-devices) for instructions on how to upgrade the firmware of your ZigBee devices using the STD OTAU plugin. 6 | 7 | _Make sure the source routing beta feature is disabled, before attempting a firmware upgrade. 8 | When source routing is enabled, the upgrade of ubisys, Hue, and Develco devices might hang._ 9 | 10 | Note that after the firmware has upgraded, the deCONZ GUI (and the REST API) might still show the old values (notably the _SW Build ID_ and _Date Code_). 11 | To refresh these, read the _Basic_ cluster attributes, in the _Cluster info_ panel in the GUI. 12 | Likewise, if the device exposes new or different endpoints and/or clusters (e.g. when upgrading from ZLL to ZigBee 3.0), refresh these by reading the _Node Descriptor_ and _Simple Descriptor(s)_ from the left drop-down menu on the node. 13 | 14 | ZigBee firmware is identified by the _Manufacturer Code_, _Image Code_ and _Version_. 15 | On startup, the STD OTAU plugin will read all `*.ota`, `*.ota.signed`, `*.sbl-ota`, and `*.zigbee` files in `~/otau`, and copy them to _mmmm_`_`_iiii_`_`_vvvvvvvv_`.zigbee` files, matching these attributes. 16 | 17 | To find the _Manufacturer Code_ for your device, see the _Node info_ panel in the deCONZ GUI. To find the _Image Code_ and (current) _Version_, see the _STD OTAU Plugin_ panel in the deCONZ GUI. 18 | Select your device from the list (match the IEEE address) and press _Query_ to populate the fields. 19 | Make sure to wake battery-powered devices before pressing _Query_. 20 | 21 | Note that not all ZigBee devices support over-the-air firmware upgrading. 22 | Devices that do support this, expose a client (grey) _OTAU_ cluster (0x0019). 23 | Only these devices are listed in the _STD OTAU Plugin_ panel. 24 | However, not all devices that advertise this cluster have actually implemented it. 25 | 26 | You need to obtain the firmware files from the device manufacturer. 27 | Some manufacturers have officially published their firmware. 28 | For some others, we found the location from where their native gateway downloads the firmware. 29 | 30 | Manufacturer | Code | Firmware Files 31 | -- | -- | -- 32 | IKEA | 117C | Available on the [IKEA website](http://fw.ota.homesmart.ikea.net/feed/version_info.json). Use the [ikea-ota-download.py](https://github.com/dresden-elektronik/deconz-rest-plugin/blob/master/ikea-ota-download.py) script to download the current IKEA firmware files directly to `~/otau`. 33 | Ledvance
OSRAM | 1189
110C | Published to the [Ledvance website](https://update.ledvance.com/firmware-overview?submit=all). 34 | Lutron | 1144 | The firmware for the Aurora Friends-of-Hue dimmer is available through [Hue Firmware](#hue-firmware). 35 | Philips (Signify) | 100B | See [Hue Firmware](#hue-firmware) below. 36 | ubisys | 10F2 | Published to the [ubisys website](http://www.ubisys.de/en/support/firmware/). 37 | Danfoss | 1246 | Published to the [Danfoss website](https://www.danfoss.com/en/products/dhs/smart-heating/smart-heating/danfoss-ally/danfoss-ally-support/). 38 | 39 | See the [Wiki](https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/OTA-Image-Types---Firmware-versions) for a community-maintained list of firmware files, including reports which files have actually been tested. 40 | 41 | #### Hue Firmware 42 | 43 | Signify don't provide their firmware for use with other ZigBee gateways than the Hue bridge. 44 | The communication between the bridge and the server hosting the firmware files is encrypted, so we cannot get an overview of the files available. 45 | To find the firmware files, you need to sniff the traffic from the Hue bridge to the Internet, as it downloads the files. 46 | Unfortunately, the bridge will only download firmware files for connected devices with outdated firmware. 47 | For details, see [issue #10](https://github.com/dresden-elektronik/deconz-ota-plugin/issues/10). 48 | 49 | Alternatively, you might use the Hue Bluetooth app to update the firmware of Bluetooth enabled devices. 50 | 51 | Newer Hue light firmware versions, using the ZHA profile (for Zigbee 3.0) instead of ZHA, support attribute reporting. 52 | This is indicated in the **AR** column below. 53 | The latest firmwares versions of newer Hue white and color ambiance lights support dynamic scenes. 54 | This is indicated in the **DS** column below. 55 | 56 | Below is an overview of known fimrware files: 57 | 58 | Image | Device(s) | Firmware | AR | DS 59 | -- | -- | -- | -- | -- 60 | 0100 | _unknown_ | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_0100/1124097291/ConnectedLamp-TI-Target_0012.sbl-ota) 61 | 0103 | Gamut-A _Color light_ | [67.93.11](http://fds.dc1.philips.com/firmware/ZGB_100B_0103/1124097291/LivingColors-Hue-Target_0012.sbl-ota) | n | n 62 | 0104 | Gamut-B _Extended color light_ | [67.88.1](http://fds.dc1.philips.com:80/firmware/ZGB_100B_0104/1124096001/Atmel_0104_ConnectedLamp-Target_0012_88.1.sbl-ota) | n | n 63 | 0105 | _Dimmable light_ | [5.130.1.30000](http://fds.dc1.philips.com/firmware/ZGB_100B_0105/1107326256/WhiteLamp-Atmel-Target_0105_5.130.1.30000_0012.sbl-ota) | n | n 64 | 0108 | _unknown_ | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_0108/1124097287/100B_0108_LivingColors-Target_0012_93.7.sbl-ota) 65 | 0109 | Hue dimmer switch | [6.1.1.28573](http://fds.dc1.philips.com/firmware/ZGB_100B_0109/1107324829/Switch-ATmega_6.1.1.28573_0012.sbl-ota) | - | - 66 | 010B | _unknown_ | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_010B/1124096001/LSP_010B_ModuLum-ATmega_0012_88.1.sbl-ota) 67 | 010C | Gamut-C _Extended color light_
_Color temperature light_ | [1.88.1](http://fds.dc1.philips.com/firmware/ZGB_100B_010C/16785664/100B-010C-01002100-ConfLight-Lamps_0012.zigbee) | n | n 68 | 010D | Hue motion sensor | [6.1.1.27575](http://fds.dc1.philips.com/firmware/ZGB_100B_010D/1107323831/Sensor-ATmega_6.1.1.27575_0012.sbl-ota) | - | - 69 | 010E | Aurelle Panels, Signes, Playbar, Being White Ambiance | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_010E/16785152/100B-010E-01001F00-ConfLight-ModuLum_0012.zigbee) 70 | 010F | Outdoor led strip (?) | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_010F/16781312/100B-010F-01001000-ConfLight-LedStrips_0012.zigbee) 71 | 0110 | Bluetooth light (?) | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_0110/16785410/100B-0110-01002002-ConfLight-Lamps-EFR32MG13.zigbee) 72 | 0111 | Hue Go (2nd Gen) | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_0111/16784640/100B-0111-01001D00-ConfLight-ModuLum-EFR32MG13.zigbee) 73 | 0112 | Bluetooth E27 | [1.93.11](http://fds.dc1.philips.com/firmware/ZGB_100B_0112/16786178/100B-0112-01002302-ConfLightBLE-Lamps-EFR32MG13.zigbee) | Y | Y 74 | 0114 | Hue white and color ambiance bluetooth | [1.93.11](http://fds.dc1.philips.com:80/firmware/ZGB_100B_0114/16784402/100B-0114-01001C12-ConfLightBLE-Lamps-EFR32MG21.zigbee)| Y | Y 75 | 0115 | Hue smart plug | [1.93.6](http://fds.dc1.philips.com/firmware/ZGB_100B_0115/16781056/100B-0115-01000F00-SmartPlug-EFR32MG13.zigbee) | | - 76 | 0116 | Hue smart button | [2.47.8_h2F968624](http://fds.dc1.philips.com/firmware/ZGB_100B_0116/33566472/100B-0116-02002F08-Switch-EFR32MG13.zigbee) | - | - 77 | 0117 | Hue Lightstrip Plus v4 | [1.93.7](http://fds.dc1.philips.com/firmware/ZGB_100B_0117/16784640/100B-0117-01001D00-ConfLightBLE-ModuLum-EFR32MG21.zigbee) | Y | n 78 | 0118 | Hue gradient lightstrip | [1.97.3](http://fds.dc1.philips.com/firmware/ZGB_100B_0118/16781828/100B-0118-01001204-PixelLum-EFR32MG21.zigbee) | Y | Y 79 | 0119 | Hue dimmer switch (2021) | [2.45.2_hF4400CA](http://fds.dc1.philips.com/firmware/ZGB_100B_0119/33565954/100B-0119-02002D02-Switch-EFR32MG22.zigbee) | - | - 80 | 011A | Hue smart plug | [1.93.6](http://fds.dc1.philips.com/firmware/ZGB_100B_011A/16779776/100B-011A-01000A00-SmartPlug-EFR32MG21.zigbee) | Y | - 81 | 011B | Hue motion sensor (2022) | 2.53.6 | - | - 82 | 011C | Hue wall switch module | 1.0.3 | - | - 83 | 011D | _unknown_ | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_011D/16785154/100B-011D-01001F02-ConfLight-ModuLumV2-EFR32MG13.zigbee) 84 | 011E | Hue Go v2 (?) | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_011E/16785152/100B-011E-01001F00-ConfLight-PortableV2-EFR32MG13.zigbee) 85 | 011F | Hue Ensis | [1.93.11](http://fds.dc1.philips.com/firmware/ZGB_100B_011F/16784902/100B-011F-01001E06-ConfLightBLE-ModuLumV3-EFR32MG21.zigbee) | Y | Y 86 | 0120 | Hue Go v3 (?) | [_unknown_](http://fds.dc1.philips.com/firmware/ZGB_100B_0120/16784896/100B-0120-01001E00-ConfLightBLE-PortableV3-EFR32MG21.zigbee) 87 | 0121 | Hue tap dial switch | 2.59.19 | - | - 88 | 0123 | Hue gradient light string | 1.101.1 | Y | Y 89 | 0000 | Lutron Aurora
_Manufacturer Code_: 1144 | [3.4](http://fds.dc1.philips.com/firmware/ZGB_1144_0000/3040/Superman_v3_04_Release_3040.ota)
[3.8](http://fds.dc1.philips.com/firmware/ZGB_1144_0000/3080/Superman_v3_08_ProdKey_3080.ota)* | - | - 90 | 91 | \* Note that the Lutron Aurora firmware v3.8 doesn't work with deCONZ. 92 | 93 | ## Installation 94 | 95 | Basically, the deCONZ STD OTAU plugin uses the same setup as the [deCONZ REST API plugin](https://github.com/dresden-elektronik/deconz-rest-plugin). 96 | To compile and install the STD OTAU plugin, follow the instructions to compile and install the REST API plugin, substituting the repository in step 1 with this one. 97 | 98 | ## Troubleshooting 99 | Start deCONZ with `--dbg-ota=1` to make the STD OTAU plugin issue debug messages. 100 | -------------------------------------------------------------------------------- /otau_file.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "otau_file.h" 4 | 5 | #define MANDATORY_HEADER_LENGTH 56 6 | #define SEGMENT_HEADER_LENGTH 6 7 | 8 | /*! The constructor. 9 | */ 10 | OtauFile::OtauFile() 11 | { 12 | upgradeFileId = 0x0BEEF11E; 13 | headerVersion = 0x0100; 14 | headerLength = 0; 15 | headerFieldControl = 0x0000; 16 | manufacturerCode = 0x0000; 17 | imageType = 0; 18 | fileVersion = 0; 19 | zigBeeStackVersion = 0x0002; // ZigBee PRO 20 | totalImageSize = 0; 21 | securityCredentialVersion = 0; 22 | upgradeFileDestination = 0; 23 | minHardwareVersion = 0; 24 | maxHardwareVersion = 0; 25 | } 26 | 27 | /*! Packs the file in a byte array. 28 | \return the array holding the otau file 29 | */ 30 | QByteArray OtauFile::toArray() 31 | { 32 | headerLength = MANDATORY_HEADER_LENGTH; // fixed length 33 | 34 | if (headerFieldControl & OF_FC_SECURITY_CREDENTIAL_VERSION) 35 | { 36 | headerLength += 1; 37 | } 38 | 39 | if (headerFieldControl & OF_FC_DEVICE_SPECIFIC) 40 | { 41 | headerLength += 8; // mac address 42 | } 43 | 44 | if (headerFieldControl & OF_FC_HARDWARE_VERSION) 45 | { 46 | headerLength += 2; // min hw version 47 | headerLength += 2; // max hw version 48 | } 49 | 50 | totalImageSize = headerLength; 51 | 52 | { 53 | auto it = subElements.begin(); 54 | auto end = subElements.end(); 55 | for (;it != end; ++it) 56 | { 57 | totalImageSize += (2 + 4); // fixed tag size 58 | totalImageSize += (uint32_t)it->data.size(); 59 | } 60 | } 61 | 62 | QByteArray arr; 63 | QDataStream stream(&arr, QIODevice::WriteOnly); 64 | stream.setByteOrder(QDataStream::LittleEndian); 65 | 66 | stream << upgradeFileId; 67 | stream << headerVersion; 68 | stream << headerLength; 69 | stream << headerFieldControl; 70 | stream << manufacturerCode; 71 | stream << imageType; 72 | stream << fileVersion; 73 | stream << zigBeeStackVersion; 74 | for (uint i = 0; i < 32; i++) 75 | { 76 | stream << headerString[i]; 77 | } 78 | 79 | stream << totalImageSize; 80 | 81 | if (headerFieldControl & OF_FC_SECURITY_CREDENTIAL_VERSION) 82 | { 83 | stream << securityCredentialVersion; 84 | } 85 | 86 | if (headerFieldControl & OF_FC_DEVICE_SPECIFIC) 87 | { 88 | stream << upgradeFileDestination; 89 | } 90 | 91 | if (headerFieldControl & OF_FC_HARDWARE_VERSION) 92 | { 93 | stream << minHardwareVersion; 94 | stream << maxHardwareVersion; 95 | } 96 | 97 | DBG_Printf(DBG_OTA, "OTAU: %s: %d bytes\n", qPrintable(path), totalImageSize); 98 | DBG_Printf(DBG_OTA, "OTAU: ota header (%u bytes)\n", headerLength); 99 | 100 | // append tags 101 | { 102 | auto it = subElements.begin(); 103 | auto end = subElements.end(); 104 | for (;it != end; ++it) 105 | { 106 | stream << it->tag; 107 | stream << it->length; 108 | DBG_Printf(DBG_OTA, "OTAU: tag 0x%04X, length 0x%08X (%d bytes)\n", it->tag, it->length, it->data.size() + SEGMENT_HEADER_LENGTH); 109 | 110 | for (int i = 0; i < it->data.size(); i++) 111 | { 112 | stream << (uint8_t)it->data[i]; 113 | } 114 | } 115 | } 116 | 117 | DBG_Printf(DBG_OTA, "OTAU: packed %d bytes\n", arr.length()); 118 | 119 | return arr; 120 | } 121 | 122 | /*! Reads the otau file from a byte array. 123 | \return true on success or false false if no valid data was found 124 | */ 125 | bool OtauFile::fromArray(const QByteArray &arr) 126 | { 127 | DBG_Printf(DBG_OTA, "OTAU: %s: %d bytes\n", qPrintable(path), arr.length()); 128 | 129 | if (arr.size() < MANDATORY_HEADER_LENGTH) 130 | { 131 | DBG_Printf(DBG_OTA, "OTAU: %s: not an ota file (too small)\n", qPrintable(path)); 132 | return false; 133 | } 134 | 135 | const char *hdr = "\x1e\xf1\xee\x0b"; 136 | 137 | int offset = arr.indexOf(hdr); 138 | if (offset < 0) 139 | { 140 | DBG_Printf(DBG_OTA, "OTAU: %s: not an ota file (header not found)\n", qPrintable(path)); 141 | return false; 142 | } 143 | 144 | uint processedLength = 0; 145 | QDataStream stream(arr); 146 | stream.setByteOrder(QDataStream::LittleEndian); 147 | 148 | for (int i = 0; i < offset; i++) 149 | { 150 | quint8 dummy; 151 | stream >> dummy; 152 | } 153 | 154 | stream >> upgradeFileId; 155 | stream >> headerVersion; 156 | stream >> headerLength; 157 | 158 | if (headerLength < MANDATORY_HEADER_LENGTH) 159 | { 160 | DBG_Printf(DBG_OTA, "OTAU: %s: not an ota file (invalid header length)\n", qPrintable(path)); 161 | return false; 162 | } 163 | 164 | stream >> headerFieldControl; 165 | stream >> manufacturerCode; 166 | stream >> imageType; 167 | stream >> fileVersion; 168 | stream >> zigBeeStackVersion; 169 | for (uint i = 0; i < sizeof(headerString); i++) 170 | { 171 | stream >> headerString[i]; 172 | } 173 | 174 | stream >> totalImageSize; 175 | processedLength = MANDATORY_HEADER_LENGTH; 176 | 177 | if (headerFieldControl & OF_FC_SECURITY_CREDENTIAL_VERSION) 178 | { 179 | stream >> securityCredentialVersion; 180 | processedLength += 1; 181 | } 182 | 183 | if (headerFieldControl & OF_FC_DEVICE_SPECIFIC) 184 | { 185 | stream >> upgradeFileDestination; 186 | processedLength += 8; 187 | } 188 | 189 | if (headerFieldControl & OF_FC_HARDWARE_VERSION) 190 | { 191 | stream >> minHardwareVersion; 192 | stream >> maxHardwareVersion; 193 | processedLength += 4; 194 | } 195 | 196 | // discard optional fields of header 197 | for (quint16 i = 0; headerLength > processedLength; i++) 198 | { 199 | quint8 dummy; 200 | stream >> dummy; 201 | processedLength++; 202 | } 203 | 204 | DBG_Printf(DBG_OTA, "OTAU: offset %6d: ota header (%u bytes)\n", offset, processedLength); 205 | 206 | // read tags 207 | 208 | subElements.clear(); 209 | 210 | while (!stream.atEnd()) 211 | { 212 | SubElement sub; 213 | int size = 0; 214 | int start = offset + processedLength; 215 | 216 | stream >> sub.tag; 217 | processedLength += 2; 218 | stream >> sub.length; 219 | processedLength += 4; 220 | 221 | if (stream.atEnd()) 222 | { 223 | break; 224 | } 225 | 226 | if (sub.length > arr.size() - offset - processedLength) 227 | { 228 | size = arr.size() - offset - processedLength; 229 | } 230 | else 231 | { 232 | size = sub.length; 233 | } 234 | 235 | sub.data.resize(size); 236 | if (stream.readRawData(sub.data.data(), size) == size) 237 | { 238 | processedLength += size; 239 | } 240 | 241 | if (sub.data.size() == size) 242 | { 243 | subElements.push_back(sub); 244 | DBG_Printf(DBG_OTA, "OTAU: offset %6u: tag 0x%04X, length 0x%08X (%d bytes)\n", start, sub.tag, sub.length, size + SEGMENT_HEADER_LENGTH); 245 | } 246 | else 247 | { 248 | DBG_Printf(DBG_OTA, "OTAU: offset %6u: ignore tag 0x%04X with invalid length\n", start, sub.tag); 249 | break; // unkown what current data is 250 | } 251 | 252 | // Total data process = totalImageSize, skip next segments, used only for legrand ATM 253 | if ((manufacturerCode == 0x1021) && (processedLength == totalImageSize)) 254 | { 255 | DBG_Printf(DBG_OTA, "OTAU: Total Image size reached, skip next segments\n"); 256 | break; 257 | } 258 | 259 | } 260 | 261 | if (!stream.atEnd()) 262 | { 263 | DBG_Printf(DBG_OTA, "OTAU: offset %6u: ignore trailing %d bytes\n", offset + processedLength, arr.size() - offset - processedLength); 264 | } 265 | 266 | raw = arr.mid(offset, totalImageSize); 267 | return !subElements.empty(); 268 | } 269 | -------------------------------------------------------------------------------- /otau_file.h: -------------------------------------------------------------------------------- 1 | #ifndef OTAU_FILE_H 2 | #define OTAU_FILE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | //!< Header field control bits 11 | #define OF_FC_SECURITY_CREDENTIAL_VERSION 0x0001 12 | #define OF_FC_DEVICE_SPECIFIC 0x0002 13 | #define OF_FC_HARDWARE_VERSION 0x0004 14 | 15 | #define TAG_UPGRADE_IMAGE 0x0000 16 | 17 | /*! \class OtauFile 18 | 19 | Represents a otau file conform to the ZigBee specification. 20 | */ 21 | struct OtauFile 22 | { 23 | OtauFile(); 24 | QByteArray toArray(); 25 | bool fromArray(const QByteArray &arr); 26 | 27 | struct SubElement 28 | { 29 | /*! 30 | Tag 31 | 32 | 0x0000 Upgrade Image 33 | 0x0001 ECDCA Signature 34 | 0x0002 ECDCA Signing Certificate 35 | 0x0003 - 0xefff Reserved 36 | 0xf000 - 0xffff Manufacturer Specific 37 | */ 38 | uint16_t tag; 39 | uint32_t length; 40 | QByteArray data; 41 | }; 42 | 43 | QString path; 44 | 45 | uint32_t upgradeFileId; //!< 0x0BEEF11E (file magic number) 46 | uint16_t headerVersion; //!< currently 0x0100 (major 01, minor 00) 47 | uint16_t headerLength; 48 | uint16_t headerFieldControl; 49 | uint16_t manufacturerCode; 50 | 51 | /*! 52 | Image Type 53 | 54 | 0x0000 - 0xffbf Manufacturer specific (device unique) 55 | 0xffc0 Security credential 56 | 0xffc1 Configuration 57 | 0xffc2 Log 58 | 0xffc3 - 0xfffe reserved 59 | 0xffff wildcard 60 | */ 61 | uint16_t imageType; 62 | uint32_t fileVersion; 63 | /*! 64 | ZigBee Stack Version 65 | 66 | 0x0000 ZigBee 2006 67 | 0x0001 ZigBee 2007 68 | 0x0002 ZigBee PRO 69 | 0x0003 ZigBee IP 70 | */ 71 | uint16_t zigBeeStackVersion; 72 | uint8_t headerString[32]; 73 | uint32_t totalImageSize; //!< including header 74 | 75 | // optional fields (depend on field control) 76 | /*! 77 | Security Credential Version 78 | 79 | 0x00 SE 1.0 80 | 0x01 SE 1.1 81 | 0x02 SE 2.0 82 | */ 83 | uint8_t securityCredentialVersion; 84 | quint64 upgradeFileDestination; 85 | uint16_t minHardwareVersion; 86 | uint16_t maxHardwareVersion; 87 | 88 | std::vector subElements; 89 | QByteArray raw; 90 | }; 91 | 92 | #endif // OTAU_FILE_H 93 | -------------------------------------------------------------------------------- /otau_file_loader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "otau_file.h" 5 | #include "otau_file_loader.h" 6 | 7 | /*! The constructor. 8 | */ 9 | OtauFileLoader::OtauFileLoader() 10 | { 11 | } 12 | 13 | /*! Reads a otau file from filesystem. 14 | \param path - the filepath 15 | \param of - reference to otau file which shall be filled 16 | \return true on success or false on error 17 | */ 18 | bool OtauFileLoader::readFile(const QString &path, OtauFile &of) 19 | { 20 | QFile file(path); 21 | 22 | if (!file.open(QFile::ReadOnly)) 23 | { 24 | qDebug() << Q_FUNC_INFO << file.errorString() << path; 25 | return false; 26 | } 27 | 28 | QByteArray arr = file.readAll(); 29 | 30 | if (arr.isEmpty()) 31 | { 32 | return false; 33 | } 34 | 35 | of.subElements.clear(); 36 | 37 | of.path = path; 38 | 39 | if (path.endsWith(".bin") || path.endsWith(".GCF")) 40 | { 41 | OtauFile::SubElement sub; 42 | 43 | sub.tag = TAG_UPGRADE_IMAGE; 44 | 45 | // BitCloud specific internal container format within the upgrade image data 46 | 47 | // u32 memOffset 48 | // u32 length 49 | // u8 data[length] 50 | // ... 0..n more container ... 51 | // u8 CRC-8 52 | 53 | arr.append((char)0x77); // dummy temp 54 | 55 | { // write BitCloud container header 56 | QDataStream stream(&sub.data, QIODevice::WriteOnly); 57 | stream.setByteOrder(QDataStream::LittleEndian); 58 | 59 | stream << (uint32_t)0; // memOffset 60 | stream << (uint32_t)arr.size(); // length 61 | } 62 | 63 | sub.data.append(arr); // add real data 64 | 65 | char crc8 = 0; // TODO check for what the crc is calculated 66 | sub.data.append(crc8); 67 | 68 | sub.length = (uint32_t)sub.data.size(); 69 | of.subElements.push_back(sub); 70 | return true; 71 | } 72 | else if (path.endsWith(".zigbee") || path.endsWith(".ota.signed") || path.endsWith(".ota") || path.endsWith(".OTA") || path.endsWith(".sbl-ota")) 73 | { 74 | return of.fromArray(arr); 75 | } 76 | else 77 | { 78 | return false; 79 | } 80 | } 81 | 82 | /*! Saves a otau file to the filesystem. 83 | \param path - the file path 84 | \param of - the otau file to write 85 | \return true on success false on error 86 | */ 87 | bool OtauFileLoader::saveFile(const QString &path, OtauFile &of) 88 | { 89 | QFile file(path); 90 | 91 | if (!file.open(QFile::WriteOnly)) 92 | { 93 | qDebug() << Q_FUNC_INFO << file.errorString() << path; 94 | return false; 95 | } 96 | 97 | QByteArray arr = of.toArray(); 98 | 99 | if (file.write(arr) == -1) 100 | { 101 | return false; 102 | } 103 | 104 | return true; 105 | } 106 | -------------------------------------------------------------------------------- /otau_file_loader.h: -------------------------------------------------------------------------------- 1 | #ifndef OTAU_FILE_LOADER_H 2 | #define OTAU_FILE_LOADER_H 3 | 4 | class QString; 5 | struct OtauFile; 6 | 7 | /*! \class OtauFileLoader 8 | 9 | Helper class to read and write otau files to and from the filesystem. 10 | */ 11 | class OtauFileLoader 12 | { 13 | public: 14 | OtauFileLoader(); 15 | bool readFile(const QString &path, OtauFile &of); 16 | bool saveFile(const QString &path, OtauFile &of); 17 | }; 18 | 19 | #endif // OTAU_FILE_LOADER_H 20 | -------------------------------------------------------------------------------- /otau_model.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "deconz.h" 3 | #include "otau_file.h" 4 | #include "otau_node.h" 5 | #include "otau_model.h" 6 | #include "std_otau_plugin.h" 7 | 8 | /*! The constructor. 9 | */ 10 | OtauModel::OtauModel(QObject *parent) : 11 | QAbstractTableModel(parent) 12 | { 13 | } 14 | 15 | OtauModel::~OtauModel() 16 | { 17 | for (OtauNode *&n : m_nodes) 18 | { 19 | if (n) 20 | { 21 | delete n; 22 | n = nullptr; 23 | } 24 | } 25 | m_nodes.clear(); 26 | } 27 | 28 | /*! Returns the model rowcount. 29 | */ 30 | int OtauModel::rowCount(const QModelIndex &parent) const 31 | { 32 | Q_UNUSED(parent); 33 | return m_nodes.size(); 34 | } 35 | 36 | /*! Returns the model columncount. 37 | */ 38 | int OtauModel::columnCount(const QModelIndex &parent) const 39 | { 40 | Q_UNUSED(parent); 41 | return SectionCount; 42 | } 43 | 44 | /*! Returns the model headerdata for a column. 45 | */ 46 | QVariant OtauModel::headerData(int section, Qt::Orientation orientation, int role) const 47 | { 48 | Q_UNUSED(orientation); 49 | 50 | if (role == Qt::DisplayRole && (orientation == Qt::Horizontal)) 51 | { 52 | switch (section) 53 | { 54 | case SectionAddress: 55 | return tr("Address"); 56 | 57 | case SectionManufacturer: 58 | return tr("Vendor"); 59 | 60 | case SectionImageType: 61 | return tr("Image"); 62 | 63 | case SectionSoftwareVersion: 64 | return tr("Version"); 65 | 66 | case SectionProgress: 67 | return tr("Progress"); 68 | 69 | case SectionDuration: 70 | return tr("Time"); 71 | 72 | // case SectionStatus: 73 | // return tr("Status"); 74 | 75 | default: 76 | return tr("Unknown"); 77 | } 78 | } 79 | 80 | return QVariant(); 81 | } 82 | 83 | /*! Returns the model data for a specific column. 84 | */ 85 | QVariant OtauModel::data(const QModelIndex &index, int role) const 86 | { 87 | if (role == Qt::DisplayRole) 88 | { 89 | if (index.row() >= rowCount(QModelIndex())) 90 | { 91 | return QVariant(); 92 | } 93 | 94 | QString str; 95 | OtauNode *node = m_nodes[index.row()]; 96 | 97 | switch(index.column()) 98 | { 99 | case SectionAddress: 100 | if (node->address().hasExt()) 101 | { 102 | str = "0x" + QString("%1").arg(node->address().ext(), 16, 16, QLatin1Char('0')).toUpper(); 103 | } 104 | else if (node->address().hasNwk()) 105 | { 106 | str = "0x" + QString("%1").arg(node->address().nwk(), 4, 16, QLatin1Char('0')).toUpper(); 107 | } 108 | break; 109 | 110 | case SectionManufacturer: 111 | str = "0x" + QString("%1").arg(node->manufacturerId, 4, 16, QLatin1Char('0')); 112 | break; 113 | 114 | 115 | case SectionImageType: 116 | str = "0x" + QString("%1").arg(node->imageType(), 4, 16, QLatin1Char('0')); 117 | break; 118 | 119 | case SectionSoftwareVersion: 120 | str = "0x" + QString("%1").arg(node->softwareVersion(), 8, 16, QLatin1Char('0')); 121 | break; 122 | 123 | case SectionProgress: 124 | if (node->status() == OtauNode::StatusWaitUpgradeEnd) 125 | { 126 | str = tr("Wait to finish"); 127 | } 128 | else if (node->zclCommandId == OTAU_UPGRADE_END_RESPONSE_CMD_ID) 129 | { 130 | switch(node->upgradeEndReq.status) 131 | { 132 | case OTAU_SUCCESS: str = tr("Done"); break; 133 | case OTAU_ABORT: str = tr("Abort"); break; 134 | case OTAU_INVALID_IMAGE: str = tr("Invalid image"); break; 135 | case OTAU_REQUIRE_MORE_IMAGE: str = tr("Require more image"); break; 136 | default: 137 | str = tr("Unknown"); 138 | break; 139 | } 140 | } 141 | else if (node->zclCommandId == OTAU_QUERY_NEXT_IMAGE_RESPONSE_CMD_ID) 142 | { 143 | if (node->hasData()) 144 | { 145 | str = tr("Idle"); 146 | } 147 | else 148 | { 149 | str = tr("No file"); 150 | } 151 | } 152 | else if (node->permitUpdate()) 153 | { 154 | if (node->offset() > 0) 155 | { 156 | if (node->offset() == node->file.totalImageSize) 157 | { 158 | str = tr("Done"); 159 | } 160 | else 161 | { 162 | str = QString("%1%").arg((static_cast(node->offset()) / static_cast(node->file.totalImageSize)) * 100.0, 0, 'f', 2); 163 | } 164 | } 165 | else 166 | { 167 | str = tr("Queued"); 168 | } 169 | } 170 | else if (node->hasData()) 171 | { 172 | str = tr("Paused"); 173 | } 174 | else 175 | { 176 | str = tr("No file"); 177 | } 178 | break; 179 | 180 | case SectionDuration: 181 | { 182 | int min = (node->elapsedTime() / 1000) / 60; 183 | int sec = (node->elapsedTime() / 1000) % 60; 184 | str = QString("%1:%2").arg(min).arg(sec, 2, 10, QLatin1Char('0')); 185 | } 186 | break; 187 | 188 | // case SectionStatus: 189 | // str = node->statusString(); 190 | // break; 191 | 192 | default: 193 | break; 194 | } 195 | 196 | return str; 197 | } 198 | else if (role == Qt::ToolTipRole) 199 | { 200 | if (index.row() >= rowCount(QModelIndex())) 201 | { 202 | return QVariant(); 203 | } 204 | 205 | OtauNode *node = m_nodes[index.row()]; 206 | 207 | switch (index.column()) 208 | { 209 | case SectionSoftwareVersion: 210 | // dresden electronic spezific version format 211 | if (node->softwareVersion() != 0) 212 | { 213 | if ((node->address().ext() & 0x00212EFFFF000000ULL) != 0) 214 | { 215 | return QString("%1.%2 build %3") 216 | .arg((node->softwareVersion() & 0xF0000000U) >> 28) 217 | .arg((node->softwareVersion() & 0x0FF00000U) >> 20) 218 | .arg((node->softwareVersion() & 0x000FFFFFU)); 219 | } 220 | } 221 | break; 222 | 223 | default: 224 | break; 225 | } 226 | } 227 | else if (role == Qt::FontRole) 228 | { 229 | switch (index.column()) 230 | { 231 | case SectionAddress: 232 | case SectionManufacturer: 233 | case SectionImageType: 234 | case SectionSoftwareVersion: 235 | { 236 | QFont font("Monospace"); 237 | font.setStyleHint(QFont::TypeWriter); 238 | return font; 239 | } 240 | 241 | default: 242 | break; 243 | } 244 | } 245 | 246 | return QVariant(); 247 | } 248 | 249 | /*! Returns a OtauNode. 250 | \param addr - the nodes address which must contain nwk and ext address 251 | \param create - true if a OtauNode shall be created if it does not exist yet 252 | \return pointer to a OtauNode or 0 if not found 253 | */ 254 | OtauNode *OtauModel::getNode(const deCONZ::Address &addr, bool create) 255 | { 256 | if (!addr.hasExt() && !addr.hasNwk()) 257 | { 258 | return nullptr; 259 | } 260 | 261 | for (OtauNode *i : m_nodes) 262 | { 263 | if (addr.hasExt() && i->address().hasExt()) 264 | { 265 | if (i->address().ext() == addr.ext()) 266 | { 267 | if (i->address().nwk() != addr.nwk()) 268 | { 269 | // update nwk address 270 | } 271 | return i; 272 | } 273 | } 274 | 275 | if (addr.hasNwk() && i->address().hasNwk()) 276 | { 277 | if (i->address().nwk() == addr.nwk()) 278 | { 279 | return i; 280 | } 281 | } 282 | } 283 | 284 | if (create && addr.hasExt() && addr.hasNwk()) 285 | { 286 | // not found create new 287 | uint row = m_nodes.size(); 288 | 289 | beginInsertRows(QModelIndex(), row, row); 290 | OtauNode *node = new OtauNode(addr); 291 | node->row = row; 292 | node->model = this; 293 | m_nodes.push_back(node); 294 | endInsertRows(); 295 | DBG_Printf(DBG_OTA, "OTAU: node added " FMT_MAC "\n", FMT_MAC_CAST(addr.ext())); 296 | return node; 297 | } 298 | 299 | return 0; 300 | } 301 | 302 | /*! Returns a OtauNode for a given row. 303 | */ 304 | OtauNode *OtauModel::getNodeAtRow(uint row) 305 | { 306 | if (m_nodes.size() > row) 307 | { 308 | return m_nodes[row]; 309 | } 310 | 311 | return 0; 312 | } 313 | 314 | /*! Notify model/view that the data for a given node has changed. 315 | */ 316 | void OtauModel::nodeDataUpdate(OtauNode *node) 317 | { 318 | if (node && (node->row < m_nodes.size())) 319 | { 320 | emit dataChanged(index(node->row, 0), index(node->row, SectionCount - 1), {Qt::DisplayRole}); 321 | } 322 | } 323 | 324 | /*! Returns the internal vector of nodes. 325 | */ 326 | std::vector &OtauModel::nodes() 327 | { 328 | return m_nodes; 329 | } 330 | -------------------------------------------------------------------------------- /otau_model.h: -------------------------------------------------------------------------------- 1 | #ifndef OTAU_MODEL_H 2 | #define OTAU_MODEL_H 3 | 4 | #include 5 | #include 6 | #include "deconz/types.h" 7 | #include "deconz/aps.h" 8 | 9 | struct OtauNode; 10 | 11 | /*! \class OtauModel 12 | 13 | Model which holds and represents all otau activity of nodes. 14 | */ 15 | class OtauModel : public QAbstractTableModel 16 | { 17 | Q_OBJECT 18 | public: 19 | enum Section 20 | { 21 | SectionAddress = 0, 22 | SectionManufacturer, 23 | SectionImageType, 24 | SectionSoftwareVersion, 25 | SectionProgress, 26 | SectionDuration, 27 | // SectionStatus, 28 | 29 | SectionCount 30 | }; 31 | 32 | explicit OtauModel(QObject *parent = nullptr); 33 | ~OtauModel(); 34 | int rowCount(const QModelIndex &parent) const; 35 | int columnCount(const QModelIndex &parent) const; 36 | QVariant headerData(int section, Qt::Orientation orientation, int role) const; 37 | QVariant data(const QModelIndex &index, int role) const; 38 | 39 | OtauNode *getNode(const deCONZ::Address &addr, bool create = false); 40 | OtauNode *getNodeAtRow(uint row); 41 | void nodeDataUpdate(OtauNode *node); 42 | std::vector &nodes(); 43 | signals: 44 | 45 | public slots: 46 | private: 47 | std::vector m_nodes; 48 | }; 49 | 50 | #endif // OTAU_MODEL_H 51 | -------------------------------------------------------------------------------- /otau_node.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "deconz/types.h" 3 | #include "otau_file.h" 4 | #include "otau_node.h" 5 | #include "otau_model.h" 6 | 7 | #define OTAU_QUERY_NEXT_IMAGE_REQUEST_CMD_ID 0x01 8 | 9 | /*! The constructor. 10 | */ 11 | OtauNode::OtauNode(const deCONZ::Address &addr) 12 | : m_addr(addr) 13 | { 14 | m_state = NodeIdle; 15 | m_lastZclCmd = 0; 16 | m_swVersion = 0; 17 | m_hwVersion = 0; 18 | m_offset = 0; 19 | m_imageType = 0; 20 | m_hasData = false; 21 | m_permitUpdate = false; 22 | m_elapsedTime = 0; 23 | m_time.start(); 24 | m_status = StatusSuccess; 25 | apsRequestId = 0xff + 1; // invalid > 8-bit 26 | profileId = HA_PROFILE_ID; 27 | manufacturerId = 0; 28 | endpoint = 0xFF; // for unicast if endpoint not known 29 | endpointNotify = 0; 30 | rxOnWhenIdle = true; 31 | imgBlockReq = {}; 32 | imgPageReq = {}; 33 | } 34 | 35 | /*! Sets the nodes state. 36 | \param state - the new otau node state 37 | */ 38 | void OtauNode::setState(OtauNode::NodeState state) 39 | { 40 | if (state != m_state) 41 | { 42 | m_state = state; 43 | model->nodeDataUpdate(this); 44 | } 45 | } 46 | 47 | /*! Sets the address of the node. 48 | \param addr - the nodes address 49 | */ 50 | void OtauNode::setAddress(const deCONZ::Address &addr) 51 | { 52 | if (m_addr != addr) 53 | { 54 | m_addr = addr; 55 | model->nodeDataUpdate(this); 56 | } 57 | } 58 | 59 | /*! Sets the node software version. 60 | \param version - the 32-bit software/firmware version 61 | */ 62 | void OtauNode::setSoftwareVersion(uint32_t version) 63 | { 64 | if (m_swVersion != version) 65 | { 66 | m_swVersion = version; 67 | model->nodeDataUpdate(this); 68 | } 69 | } 70 | 71 | /*! Sets the node hardware version. 72 | \param version - the 16-bit hardware version 73 | */ 74 | void OtauNode::setHardwareVersion(uint16_t version) 75 | { 76 | if (m_hwVersion != version) 77 | { 78 | m_hwVersion = version; 79 | model->nodeDataUpdate(this); 80 | } 81 | } 82 | 83 | /*! Sets the current otau process file offset. 84 | \param offset - the current offset for file transfer 85 | */ 86 | void OtauNode::setOffset(uint32_t offset) 87 | { 88 | if (m_offset != offset) 89 | { 90 | m_offset = offset; 91 | model->nodeDataUpdate(this); 92 | } 93 | } 94 | 95 | /*! Sets the otau file image type. 96 | \param type - the 16-bit otau file image type 97 | */ 98 | void OtauNode::setImageType(uint16_t type) 99 | { 100 | if (m_imageType != type) 101 | { 102 | m_imageType = type; 103 | model->nodeDataUpdate(this); 104 | } 105 | } 106 | 107 | /*! Sets the command id of the last received ZCL command. 108 | \param commandId - the ZCL command id 109 | */ 110 | void OtauNode::setLastZclCommand(uint8_t commandId) 111 | { 112 | m_lastZclCmd = commandId; 113 | 114 | if (commandId == OTAU_QUERY_NEXT_IMAGE_REQUEST_CMD_ID) 115 | { 116 | m_lastQueryTime = QTime::currentTime(); 117 | } 118 | } 119 | 120 | /*! Returns the last received ZCL command id. 121 | */ 122 | uint8_t OtauNode::lastZclCmd() const 123 | { 124 | return m_lastZclCmd; 125 | } 126 | 127 | /*! Refreshs/resets the otau process timeout. 128 | */ 129 | void OtauNode::refreshTimeout() 130 | { 131 | timeout = NODE_TIMEOUT; 132 | } 133 | 134 | /*! Restarts the timer to measure the elapsed process time. 135 | */ 136 | void OtauNode::restartElapsedTimer() 137 | { 138 | m_elapsedTime = 0; 139 | m_time.restart(); 140 | model->nodeDataUpdate(this); 141 | } 142 | 143 | /*! Notify views to update elapsed time value. 144 | */ 145 | void OtauNode::notifyElapsedTimer() 146 | { 147 | if (m_elapsedTime != m_time.elapsed()) 148 | { 149 | m_elapsedTime = m_time.elapsed(); 150 | model->nodeDataUpdate(this); 151 | } 152 | } 153 | 154 | /*! Returns string for nodes current status. 155 | */ 156 | QString OtauNode::statusString() const 157 | { 158 | switch (status()) 159 | { 160 | case StatusSuccess: return "Ok"; 161 | case StatusInvalidParameter: return "InvalidParameter"; 162 | case StatusWrongOffset: return "WrongOffset"; 163 | case StatusUnknownError: return "UnknownError"; 164 | case StatusAbort: return "Abort"; 165 | case StatusWrongImageType: return "WrongImageType"; 166 | case StatusWrongManufacturer: return "WrongManufacturer"; 167 | case StatusWrongPlatform: return "WrongPlatform"; 168 | case StatusTimeout: return "Timeout"; 169 | case StatusIgnored: return "Ignored"; 170 | case StatusCrcError: return "CrCError"; 171 | case StatusWaitUpgradeEnd: return "WaitUpgradeEnd"; 172 | default: 173 | break; 174 | } 175 | 176 | return "Unknown"; 177 | } 178 | -------------------------------------------------------------------------------- /otau_node.h: -------------------------------------------------------------------------------- 1 | #ifndef OTAU_NODE_H 2 | #define OTAU_NODE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "deconz/types.h" 8 | #include "deconz/aps.h" 9 | #include "deconz/timeref.h" 10 | 11 | #define NODE_TIMEOUT 10000 12 | #define MAX_ACTIVE_BLOCK_REQUESTS 9 13 | 14 | struct OtauFile; 15 | class OtauModel; 16 | 17 | struct ImageNotifyReq 18 | { 19 | // adressing 20 | deCONZ::ApsAddressMode addrMode; 21 | deCONZ::Address addr; 22 | uint8_t dstEndpoint; 23 | uint8_t radius; 24 | }; 25 | 26 | struct ImageBlockReq 27 | { 28 | uint8_t fieldControl; 29 | uint16_t manufacturerCode; 30 | uint16_t imageType; 31 | uint32_t fileVersion; 32 | uint32_t offset; 33 | uint8_t maxDataSize; 34 | // image page request extra fields 35 | uint16_t pageBytesDone; 36 | uint16_t pageSize; 37 | uint16_t responseSpacing; 38 | }; 39 | 40 | struct ImageBlockReqTrack 41 | { 42 | deCONZ::SteadyTimeRef sendTime; 43 | uint8_t apsRequestId; 44 | uint8_t retry; 45 | }; 46 | 47 | struct UpgradeEndReq 48 | { 49 | UpgradeEndReq() : 50 | status(0), manufacturerCode(0), imageType(0), fileVersion(0) 51 | {} 52 | uint8_t status; 53 | uint16_t manufacturerCode; 54 | uint16_t imageType; 55 | uint32_t fileVersion; 56 | }; 57 | 58 | /*! \class OtauNode 59 | 60 | Represents a otau node and its state/data. 61 | */ 62 | struct OtauNode 63 | { 64 | public: 65 | enum NodeState 66 | { 67 | NodeIdle, 68 | NodeBusy, 69 | NodeWaitPageSpacing, 70 | NodeWaitNextRequest, 71 | NodeWaitConfirm, 72 | NodeError, 73 | NodeAbort 74 | }; 75 | 76 | enum Status 77 | { 78 | StatusSuccess = 0x00, 79 | StatusInvalidParameter = 0x01, 80 | StatusWrongOffset = 0x02, 81 | StatusUnknownError = 0x03, 82 | StatusAbort = 0x04, 83 | StatusWrongImageType = 0x05, 84 | StatusWrongManufacturer = 0x06, 85 | StatusWrongPlatform = 0x07, 86 | StatusTimeout = 0x08, 87 | StatusIgnored = 0x09, 88 | StatusCrcError = 0x0A, 89 | StatusWaitUpgradeEnd = 0x0B 90 | }; 91 | 92 | OtauNode(const deCONZ::Address &addr); 93 | 94 | NodeState state() { return m_state; } 95 | void setState(NodeState state); 96 | const deCONZ::Address &address() { return m_addr; } 97 | void setAddress(const deCONZ::Address &addr); 98 | uint32_t softwareVersion() { return m_swVersion; } 99 | void setSoftwareVersion(uint32_t version); 100 | uint32_t hardwareVersion() { return m_hwVersion; } 101 | void setHardwareVersion(uint16_t version); 102 | uint32_t offset() { return m_offset; } 103 | void setOffset(uint32_t offset); 104 | uint16_t imageType() { return m_imageType; } 105 | void setImageType(uint16_t type); 106 | void refreshTimeout(); 107 | bool permitUpdate() { return m_permitUpdate; } 108 | void setPermitUpdate(bool permit) { m_permitUpdate = permit; } 109 | bool hasData() { return m_hasData; } 110 | void setHasData(bool hasData) { m_hasData = hasData; } 111 | void restartElapsedTimer(); 112 | void notifyElapsedTimer(); 113 | int elapsedTime() { return m_elapsedTime; } 114 | Status status() const { return m_status; } 115 | void setStatus(Status status) { m_status = status; } 116 | QString statusString() const; 117 | void setLastZclCommand(uint8_t commandId); 118 | uint8_t lastZclCmd() const; 119 | const QTime &lastQueryTime() const { return m_lastQueryTime; } 120 | 121 | // service for model 122 | uint row; 123 | OtauModel *model; 124 | bool rxOnWhenIdle; 125 | 126 | // TODO: getter and setter 127 | uint16_t apsRequestId; 128 | uint8_t zclCommandId; // last send ZCL command id 129 | uint8_t endpoint; 130 | uint8_t endpointNotify; 131 | uint8_t reqSequenceNumber; 132 | uint16_t profileId; 133 | uint16_t manufacturerId; 134 | uint32_t imageSize; 135 | uint8_t *imageData; 136 | uint32_t timeout; // seconds 137 | QElapsedTimer lastResponseTime; 138 | QElapsedTimer lastActivity; 139 | 140 | OtauFile file; 141 | QByteArray rawFile; 142 | ImageBlockReq imgPageReq; 143 | ImageBlockReq imgBlockReq; 144 | UpgradeEndReq upgradeEndReq; 145 | int imgPageRequestRetry; 146 | int imgBlockResponseRetry; 147 | 148 | std::array imgBlockTrack{}; 149 | 150 | private: 151 | deCONZ::Address m_addr; 152 | NodeState m_state; 153 | uint8_t m_lastZclCmd; 154 | uint32_t m_swVersion; 155 | uint16_t m_hwVersion; 156 | uint32_t m_offset; 157 | uint16_t m_imageType; 158 | bool m_hasData; 159 | bool m_permitUpdate; 160 | QElapsedTimer m_time; 161 | QTime m_lastQueryTime; 162 | int m_elapsedTime; 163 | Status m_status; 164 | }; 165 | 166 | #endif // OTAU_NODE_H 167 | -------------------------------------------------------------------------------- /std_otau.pro: -------------------------------------------------------------------------------- 1 | TARGET = std_otau_plugin 2 | 3 | TARGET = $$qtLibraryTarget($$TARGET) 4 | 5 | DEFINES += USE_ULIB_SHARED=1 \ 6 | USE_ACTOR_MODEL 7 | 8 | unix:contains(QMAKE_HOST.arch, armv6l) { 9 | DEFINES += ARCH_ARM ARCH_ARMV6 10 | } 11 | 12 | win32:LIBS+= -L../.. -ldeCONZ1 13 | unix:LIBS+= -L../.. -ldeCONZ 14 | win32:CONFIG += dll 15 | 16 | TEMPLATE = lib 17 | CONFIG += plugin \ 18 | += debug_and_release \ 19 | += c++14 20 | 21 | greaterThan(QT_MAJOR_VERSION, 4) { 22 | QT += core gui widgets 23 | } 24 | 25 | CONFIG(debug, debug|release) { 26 | LIBS += -L../../debug 27 | } 28 | 29 | CONFIG(release, debug|release) { 30 | LIBS += -L../../release 31 | } 32 | 33 | QMAKE_CXXFLAGS += -Wno-attributes \ 34 | -Wall 35 | 36 | INCLUDEPATH += ../../lib \ 37 | ../.. \ 38 | ../../3rdparty/actor_model 39 | 40 | HEADERS = std_otau_plugin.h \ 41 | std_otau_widget.h \ 42 | otau_file.h \ 43 | otau_file_loader.h \ 44 | otau_model.h \ 45 | otau_node.h 46 | 47 | SOURCES = std_otau_plugin.cpp \ 48 | std_otau_widget.cpp \ 49 | otau_file.cpp \ 50 | otau_file_loader.cpp \ 51 | otau_model.cpp \ 52 | otau_node.cpp 53 | 54 | win32:DESTDIR = ../../debug/plugins # TODO adjust 55 | unix:DESTDIR = .. 56 | 57 | FORMS += \ 58 | std_otau_widget.ui 59 | -------------------------------------------------------------------------------- /std_otau_plugin.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "std_otau_plugin.h" 8 | #include "std_otau_widget.h" 9 | #include "otau_file.h" 10 | #include "otau_file_loader.h" 11 | #include "otau_node.h" 12 | #include "otau_model.h" 13 | 14 | #include 15 | #include 16 | #include "deconz/am_vfs.h" 17 | 18 | #define VENDOR_BUSCH_JAEGER 0x112E 19 | #define VENDOR_DDEL 0x1135 20 | 21 | #define IMG_TYPE_FLS_PP3_H3 0x0000 22 | #define IMG_TYPE_FLS_NB 0x0002 23 | #define IMG_TYPE_FLS_A2 0x0004 24 | #define IMG_TYPE_FLS_H3 0x0008 25 | 26 | #define MAX_RADIUS 0 27 | #define MAX_ASDU_SIZE 82 28 | /* Source routing adds additional bytes to the NWK header, reducing the ASDU size. 29 | U8 relay count 30 | U8 relay index 31 | U16 relay 1 32 | U16 relay 2 33 | ... 34 | U16 relay n 35 | By default, maximum hops is set to 5. 36 | Ideally, the core would report whether source routing is enabled, and the value of max hops. 37 | For now, we count the number of consecutive NO_ACK errors to try and detect source routing. 38 | */ 39 | //#define SOURCE_ROUTING_MAX_HOPS 7 40 | //#define SOURCE_ROUTING_SIZE (1 + 1 + (2 * SOURCE_ROUTING_MAX_HOPS)) 41 | 42 | // some older devices support max. 40 bytes data size 43 | // use this also als fallback due source routing overhead 44 | #define MAX_SAFE_ASDU_SIZE (40 + ZCL_HEADER_SIZE + IMAGE_BLOCK_RSP_HEADER_SIZE) 45 | #define NO_ACK_THRESHOLD 3 46 | 47 | // #define MAX_ASDU_SIZE1 45 48 | // #define MAX_ASDU_SIZE2 45 49 | // #define MAX_ASDU_SIZE3 82 50 | /* 51 | U8 status 52 | U16 manufacturerCode; 53 | U16 imageType; 54 | U32 fileVersion; 55 | U32 offset; 56 | U8 dataSize 57 | */ 58 | #define IMAGE_BLOCK_RSP_HEADER_SIZE (1 + 2 + 2 + 4 + 4 + 1) // 14 59 | #define ZCL_HEADER_SIZE (1 + 1 + 1) // frame control + seq + commandId 60 | // for widest device support use 50 bytes max 61 | #define MAX_DATA_SIZE qMin(50, (m_maxAsduDataSize - (ZCL_HEADER_SIZE + IMAGE_BLOCK_RSP_HEADER_SIZE))) 62 | #define MIN_RESPONSE_SPACING 20 63 | #define MAX_RESPONSE_SPACING 500 64 | #define DEFAULT_UPGRADE_TIME 5 65 | #define CLEANUP_TIMER_DELAY (3 * 60 * 1000) 66 | #define CLEANUP_DELAY (4 * 60 * 60 * 1000) 67 | #define IMAGE_PAGE_TIMER_DELAY 10 68 | #define ACTIVITY_TIMER_DELAY 3000 69 | #define MAX_ACTIVITY 120 // hits 0 after 5 seconds 70 | #define MAX_IMG_PAGE_REQ_RETRY 5 71 | #define MAX_IMG_BLOCK_RSP_RETRY 10 72 | #define WAIT_NEXT_REQUEST_TIMEOUT 60000 73 | #define INVALID_APS_REQ_ID (0xff + 1) // request ids are 8-bit 74 | 75 | #define FAST_PAGE_SPACEING 25 76 | #define MIN_PAGE_SPACEING 20 77 | #define MAX_PAGE_SPACEING 3000 78 | 79 | #define OTA_TIME_INFINITE 0xFFFFFFFFUL 80 | #define DONT_CARE_FILE_VERSION 0xFFFFFFFFUL 81 | 82 | /* .ota-cache file 83 | 84 | 4096 byte pages 85 | 86 | Entry { 87 | U16 marker // 0 = empty 88 | U16 page_count 89 | U16 mfcode 90 | U16 image_type 91 | U32 crc32 92 | 93 | U8 filename_length 94 | U8 filename[127] // '\0' right padded 95 | U8 sha256[32] 96 | 97 | } 98 | 99 | */ 100 | 101 | const quint64 macPrefixMask = 0xffffff0000000000ULL; 102 | 103 | // const quint64 develcoMacPrefix = 0x0015bc0000000000ULL; 104 | // const quint64 philipsMacPrefix = 0x0017880000000000ULL; 105 | // const quint64 ubisysMacPrefix = 0x001fee0000000000ULL; 106 | const quint64 osramMacPrefix = 0x8418260000000000ULL; 107 | // const quint64 bjeMacPrefix = 0xd85def0000000000ULL; 108 | 109 | const deCONZ::SimpleDescriptor *getSimpleDescriptor(const deCONZ::Node *node, quint8 ep) 110 | { 111 | if (!node) 112 | { 113 | return nullptr; 114 | } 115 | 116 | const auto i = std::find_if(node->simpleDescriptors().cbegin(), node->simpleDescriptors().cend(), 117 | [ep](const deCONZ::SimpleDescriptor &sd){ return sd.endpoint() == ep; }); 118 | 119 | if (i != node->simpleDescriptors().cend()) 120 | { 121 | return &*i; 122 | } 123 | 124 | return nullptr; 125 | } 126 | 127 | #ifdef USE_ACTOR_MODEL 128 | 129 | #define AM_ACTOR_ID_OTA 9000 130 | #define AM_ACTOR_ID_CORE_APS 2005 131 | 132 | #define OTA_M_ID_QUERY_NEXT_IMAGE_NOTIFY AM_MESSAGE_ID_SPECIFIC_NOTIFY(0x0001) 133 | 134 | static struct am_actor am_actor_ota0; 135 | struct am_api_functions *am = nullptr; 136 | 137 | static int OTA_ReadEntryRequest(struct am_message *msg) 138 | { 139 | struct am_message *m; 140 | 141 | uint16_t tag; 142 | am_string url; 143 | 144 | uint32_t mode = 0; 145 | uint64_t mtime = 0; 146 | 147 | tag = am->msg_get_u16(msg); 148 | url = am->msg_get_string(msg); 149 | 150 | if (msg->status != AM_MSG_STATUS_OK) 151 | return AM_CB_STATUS_INVALID; 152 | 153 | m = am->msg_alloc(); 154 | if (!m) 155 | return AM_CB_STATUS_MESSAGE_ALLOC_FAILED; 156 | 157 | am->msg_put_u16(m, tag); 158 | am->msg_put_u8(m, AM_RESPONSE_STATUS_OK); 159 | 160 | if (url == ".actor/name") 161 | { 162 | am->msg_put_cstring(m, "str"); 163 | am->msg_put_u32(m, mode); 164 | am->msg_put_u64(m, mtime); 165 | am->msg_put_cstring(m, "ota"); 166 | } 167 | else 168 | { 169 | m->pos = 0; 170 | } 171 | 172 | if (m->pos == 0) 173 | { 174 | am->msg_put_u16(m, tag); 175 | am->msg_put_u8(m, AM_RESPONSE_STATUS_NOT_FOUND); 176 | } 177 | 178 | m->src = msg->dst; 179 | m->dst = msg->src; 180 | m->id = VFS_M_ID_READ_ENTRY_RSP; 181 | am->send_message(m); 182 | 183 | return AM_CB_STATUS_OK; 184 | } 185 | 186 | static int OTA0_MessageCallback(struct am_message *msg) 187 | { 188 | if (msg->id == VFS_M_ID_READ_ENTRY_REQ) 189 | return OTA_ReadEntryRequest(msg); 190 | 191 | return AM_CB_STATUS_UNSUPPORTED; 192 | } 193 | 194 | /* actor model init entry point, called by actor model service in core */ 195 | extern "C" DECONZ_EXPORT 196 | int am_plugin_init(struct am_api_functions *api) 197 | { 198 | struct am_message *m; 199 | am = api; 200 | 201 | AM_INIT_ACTOR(&am_actor_ota0, AM_ACTOR_ID_OTA, OTA0_MessageCallback); 202 | am->register_actor(&am_actor_ota0); 203 | am->subscribe(AM_ACTOR_ID_CORE_APS, AM_ACTOR_ID_OTA); 204 | 205 | return 1; 206 | } 207 | 208 | #endif // USE_ACTOR_MODEL 209 | 210 | /*! The constructor. 211 | */ 212 | StdOtauPlugin::StdOtauPlugin(QObject *parent) : 213 | QObject(parent) 214 | { 215 | m_state = StateEnabled; 216 | m_w = nullptr; 217 | m_srcEndpoint = 0x01; // TODO: ask from controller 218 | m_model = new OtauModel(this); 219 | m_imagePageTimer = new QTimer(this); 220 | m_maxAsduDataSize = MAX_ASDU_SIZE; 221 | m_nNoAckErrors = 0; 222 | 223 | m_imagePageTimer->setSingleShot(true); 224 | m_imagePageTimer->setInterval(IMAGE_PAGE_TIMER_DELAY); 225 | 226 | connect(m_imagePageTimer, SIGNAL(timeout()), 227 | this, SLOT(imagePageTimerFired())); 228 | 229 | m_cleanupTimer = new QTimer(this); 230 | m_cleanupTimer->setSingleShot(true); 231 | m_cleanupTimer->setInterval(CLEANUP_TIMER_DELAY); 232 | 233 | connect(m_cleanupTimer, SIGNAL(timeout()), 234 | this, SLOT(cleanupTimerFired())); 235 | 236 | m_activityTimer = new QTimer(this); 237 | m_activityTimer->setSingleShot(false); 238 | 239 | connect(m_activityTimer, SIGNAL(timeout()), 240 | this, SLOT(activityTimerFired())); 241 | 242 | //QString defaultImgPath = deCONZ::getStorageLocation(deCONZ::ApplicationsDataLocation) + "/otau"; 243 | QString defaultImgPath = deCONZ::getStorageLocation(deCONZ::HomeLocation) + "/otau"; 244 | m_imgPath = deCONZ::appArgumentString("--otau-img-path", defaultImgPath); 245 | 246 | QDir otauDir(m_imgPath); 247 | 248 | if (otauDir.exists()) 249 | { 250 | DBG_Printf(DBG_OTA, "OTAU: image path: %s\n", qPrintable(m_imgPath)); 251 | } 252 | else 253 | { 254 | DBG_Printf(DBG_ERROR, "OTAU: image path does not exist: %s\n", qPrintable(m_imgPath)); 255 | } 256 | 257 | deCONZ::ApsController *apsCtrl = deCONZ::ApsController::instance(); 258 | 259 | connect(apsCtrl, SIGNAL(apsdeDataConfirm(deCONZ::ApsDataConfirm)), 260 | this, SLOT(apsdeDataConfirm(deCONZ::ApsDataConfirm))); 261 | 262 | connect(apsCtrl, SIGNAL(apsdeDataIndication(deCONZ::ApsDataIndication)), 263 | this, SLOT(apsdeDataIndication(deCONZ::ApsDataIndication))); 264 | 265 | connect(apsCtrl, SIGNAL(nodeEvent(deCONZ::NodeEvent)), 266 | this, SLOT(nodeEvent(deCONZ::NodeEvent))); 267 | 268 | 269 | QSettings config(deCONZ::getStorageLocation(deCONZ::ConfigLocation), QSettings::IniFormat); 270 | 271 | // fast page spacing 272 | bool ok = false; 273 | m_fastPageSpaceing = FAST_PAGE_SPACEING; 274 | if (config.contains("otau/fast-page-spacing")) 275 | { 276 | int sp = config.value("otau/fast-page-spacing", FAST_PAGE_SPACEING).toInt(&ok); 277 | if (ok && sp >= MIN_PAGE_SPACEING && sp < MAX_PAGE_SPACEING) 278 | { 279 | m_fastPageSpaceing = sp; 280 | } 281 | } 282 | 283 | if (!ok) 284 | { 285 | config.setValue("otau/fast-page-spacing", m_fastPageSpaceing); 286 | } 287 | 288 | checkFileLinks(); 289 | } 290 | 291 | /*! APSDE-DATA.indication callback. 292 | \param ind - the indication primitive 293 | \note Will be called from the main application for each incoming indication. 294 | Any filtering for nodes, profiles, clusters must be handled by this plugin. 295 | */ 296 | void StdOtauPlugin::apsdeDataIndication(const deCONZ::ApsDataIndication &ind) 297 | { 298 | deCONZ::ApsController *apsCtrl = deCONZ::ApsController::instance(); 299 | if (!apsCtrl) 300 | { 301 | return; 302 | } 303 | 304 | if (apsCtrl->getParameter(deCONZ::ParamOtauActive) == 0) 305 | { 306 | setState(StateDisabled); 307 | 308 | } 309 | else if (state() == StateDisabled) 310 | { 311 | setState(StateEnabled); 312 | } 313 | 314 | if (ind.profileId() == ZDP_PROFILE_ID && ind.clusterId() == ZDP_MATCH_DESCRIPTOR_CLID) 315 | { 316 | matchDescriptorRequest(ind); 317 | } 318 | 319 | if (ind.clusterId() != OTAU_CLUSTER_ID) 320 | { 321 | return; 322 | } 323 | 324 | deCONZ::ZclFrame zclFrame; 325 | 326 | QDataStream stream(ind.asdu()); 327 | stream.setByteOrder(QDataStream::LittleEndian); 328 | zclFrame.readFromStream(stream); 329 | 330 | // filter 331 | if (zclFrame.isClusterCommand()) 332 | { 333 | switch (zclFrame.commandId()) 334 | { 335 | case OTAU_QUERY_NEXT_IMAGE_REQUEST_CMD_ID: 336 | case OTAU_IMAGE_BLOCK_REQUEST_CMD_ID: 337 | case OTAU_IMAGE_PAGE_REQUEST_CMD_ID: 338 | case OTAU_UPGRADE_END_REQUEST_CMD_ID: 339 | m_cleanupTimer->stop(); 340 | m_cleanupTimer->start(); 341 | break; 342 | 343 | default: 344 | return; 345 | } 346 | } 347 | else 348 | { 349 | if (zclFrame.commandId() == deCONZ::ZclDefaultResponseId) 350 | { 351 | switch (zclFrame.defaultResponseCommandId()) 352 | { 353 | case OTAU_QUERY_NEXT_IMAGE_REQUEST_CMD_ID: 354 | case OTAU_QUERY_NEXT_IMAGE_RESPONSE_CMD_ID: 355 | case OTAU_IMAGE_BLOCK_REQUEST_CMD_ID: 356 | case OTAU_IMAGE_BLOCK_RESPONSE_CMD_ID: 357 | case OTAU_IMAGE_PAGE_REQUEST_CMD_ID: 358 | case OTAU_UPGRADE_END_REQUEST_CMD_ID: 359 | case OTAU_UPGRADE_END_RESPONSE_CMD_ID: 360 | DBG_Printf(DBG_OTA, "OTAU: 0x%016llX default rsp cmd: 0x%02X, status 0x%02X, seq: %u\n", ind.srcAddress().ext(), zclFrame.defaultResponseCommandId(), (uint8_t)zclFrame.defaultResponseStatus(), zclFrame.sequenceNumber()); 361 | break; 362 | 363 | default: 364 | break; 365 | } 366 | return; 367 | } 368 | } 369 | 370 | bool create = true; 371 | OtauNode *node = m_model->getNode(ind.srcAddress(), create); 372 | 373 | if (!node) 374 | { 375 | return; 376 | } 377 | 378 | node->lastActivity.invalidate(); 379 | node->lastActivity.start(); 380 | if (!zclFrame.isDefaultResponse()) 381 | { 382 | node->setLastZclCommand(zclFrame.commandId()); 383 | } 384 | 385 | // filter 386 | if (zclFrame.isClusterCommand()) 387 | { 388 | switch (zclFrame.commandId()) 389 | { 390 | case OTAU_QUERY_NEXT_IMAGE_REQUEST_CMD_ID: 391 | queryNextImageRequest(ind, zclFrame); 392 | break; 393 | 394 | case OTAU_IMAGE_BLOCK_REQUEST_CMD_ID: 395 | imageBlockRequest(ind, zclFrame); 396 | break; 397 | 398 | case OTAU_IMAGE_PAGE_REQUEST_CMD_ID: 399 | imagePageRequest(ind, zclFrame); 400 | break; 401 | 402 | case OTAU_UPGRADE_END_REQUEST_CMD_ID: 403 | upgradeEndRequest(ind, zclFrame); 404 | break; 405 | 406 | default: 407 | break; 408 | } 409 | } 410 | 411 | m_model->nodeDataUpdate(node); 412 | } 413 | 414 | /*! APSDE-DATA.confirm callback. 415 | \param conf - the confirm primitive 416 | \note Will be called from the main application for each incoming confirmation, 417 | even if the APSDE-DATA.request was not issued by this plugin. 418 | */ 419 | void StdOtauPlugin::apsdeDataConfirm(const deCONZ::ApsDataConfirm &conf) 420 | { 421 | if (!conf.dstAddress().isNwkUnicast()) 422 | return; 423 | 424 | OtauNode *node = m_model->getNode(conf.dstAddress()); 425 | 426 | if (node) 427 | { 428 | if (node->state() == OtauNode::NodeAbort) 429 | { 430 | return; 431 | } 432 | 433 | if (node->apsRequestId == INVALID_APS_REQ_ID) 434 | { } 435 | else if (node->apsRequestId == conf.id()) 436 | { 437 | node->apsRequestId = INVALID_APS_REQ_ID; 438 | 439 | if (conf.status() != deCONZ::ApsSuccessStatus) 440 | { 441 | DBG_Printf(DBG_OTA, "OTAU: aps conf failed status 0x%02X\n", conf.status()); 442 | // FIXME hack to detect source routing 443 | // note that no ack doesn't always refer to source routing but this provides a safe fallback 444 | if (conf.status() == deCONZ::ApsNoAckStatus || conf.status() == 0xE5 /* ??? */) 445 | { 446 | if (++m_nNoAckErrors > NO_ACK_THRESHOLD || 447 | (node->zclCommandId == OTAU_IMAGE_BLOCK_RESPONSE_CMD_ID && 448 | node->imgBlockReq.offset == 0) 449 | ) 450 | { 451 | if (m_maxAsduDataSize > MAX_SAFE_ASDU_SIZE) 452 | { 453 | m_maxAsduDataSize = MAX_SAFE_ASDU_SIZE; 454 | DBG_Printf(DBG_OTA, "OTAU: reducing max data size to %d\n", MAX_DATA_SIZE); 455 | } 456 | } 457 | } 458 | else 459 | { 460 | m_nNoAckErrors = 0; 461 | } 462 | // End FIXME 463 | } 464 | else 465 | { 466 | node->refreshTimeout(); 467 | 468 | if (node->zclCommandId == OTAU_IMAGE_BLOCK_RESPONSE_CMD_ID) 469 | { 470 | node->imgBlockReq.pageBytesDone += node->imgBlockReq.maxDataSize; 471 | node->imgBlockReq.offset += node->imgBlockReq.maxDataSize; 472 | node->reqSequenceNumber++; 473 | 474 | if (node->state() == OtauNode::NodeWaitPageSpacing) 475 | { 476 | imagePageResponse(node); 477 | } 478 | } 479 | } 480 | 481 | if (node->zclCommandId == OTAU_UPGRADE_END_RESPONSE_CMD_ID) 482 | { 483 | if (conf.status() == deCONZ::ApsSuccessStatus) 484 | { 485 | node->setHasData(false); 486 | } 487 | } 488 | } 489 | } 490 | } 491 | 492 | /*! Handler for node events. 493 | \param event - the event which occured 494 | */ 495 | void StdOtauPlugin::nodeEvent(const deCONZ::NodeEvent &event) 496 | { 497 | if (event.event() != deCONZ::NodeEvent::NodeDeselected && !event.node()) 498 | { 499 | return; 500 | } 501 | 502 | if (event.event() == deCONZ::NodeEvent::UpdatedSimpleDescriptor) 503 | { 504 | checkIfNewOtauNode(event.node(), event.endpoint()); 505 | } 506 | else if (event.event() == deCONZ::NodeEvent::NodeSelected) 507 | { 508 | nodeSelected(event.node()); 509 | } 510 | else if (event.event() == deCONZ::NodeEvent::NodeDeselected) 511 | { 512 | m_w->clearNode(); 513 | } 514 | else if (event.event() == deCONZ::NodeEvent::NodeRemoved) 515 | { 516 | // TODO: Remove node from model and tableview 517 | } 518 | } 519 | 520 | void StdOtauPlugin::nodeSelected(const deCONZ::Node *node) 521 | { 522 | if (!m_model || m_model->nodes().empty()) 523 | { 524 | return; 525 | } 526 | OtauNode *otauNode = m_model->getNode(node->address()); 527 | if (otauNode != nullptr) 528 | { 529 | m_w->displayNode(otauNode, m_model->index(otauNode->row, 0)); 530 | } 531 | else 532 | { 533 | m_w->clearNode(); 534 | } 535 | } 536 | 537 | /*! Checks if a new otau image for the node is available in the otau folder. 538 | Otau images must be in the directory and must have a proper formatted filename. 539 | All numbers are hexaecimal and in capital letters. 540 | filename: ---someArbitraryText.zigbee 541 | example: 113D-AB12-1F010400-FLS-RGB.zigbee 542 | 543 | \param node - the node for which the check will be done 544 | \param path - the path to look for .zigbee files 545 | */ 546 | bool StdOtauPlugin::checkForUpdateImageImage(OtauNode *node, const QString &path) 547 | { 548 | deCONZ::ApsController *apsCtrl = deCONZ::ApsController::instance(); 549 | if (!apsCtrl) 550 | { 551 | return false; 552 | } 553 | 554 | if (apsCtrl->getParameter(deCONZ::ParamOtauActive) == 0) 555 | { 556 | return false; 557 | } 558 | 559 | bool ok; 560 | uint32_t cmpFileVersion = node->softwareVersion(); 561 | uint32_t fileVersion; 562 | uint16_t imageType; 563 | uint16_t manufacturerId; 564 | QString updateFile = ""; 565 | QDir dir(path); 566 | 567 | if (!dir.exists()) 568 | { 569 | DBG_Printf(DBG_OTA, "OTAU: image path does not exist: %s\n", qPrintable(path)); 570 | return false; 571 | } 572 | 573 | QStringList ls = dir.entryList(); 574 | 575 | auto i = ls.begin(); 576 | auto end = ls.end(); 577 | 578 | for (; i != end; ++i) 579 | { 580 | if (!i->endsWith(".zigbee")) 581 | { 582 | continue; 583 | } 584 | 585 | QString plain = *i; 586 | plain.replace(".zigbee", ""); 587 | 588 | QStringList args = plain.split('-'); 589 | 590 | if (args.size() >= 3) 591 | { 592 | manufacturerId = args[0].toUShort(&ok, 16); 593 | 594 | if (!ok || manufacturerId != node->manufacturerId) 595 | { 596 | continue; 597 | } 598 | 599 | imageType = args[1].toUShort(&ok, 16); 600 | 601 | if (!ok) 602 | { 603 | continue; 604 | } 605 | 606 | if (imageType == node->imageType()) 607 | { 608 | fileVersion = args[2].toUInt(&ok, 16); 609 | 610 | if (!ok) 611 | { 612 | continue; 613 | } 614 | 615 | if (fileVersion > cmpFileVersion) 616 | { 617 | updateFile = *i; 618 | cmpFileVersion = fileVersion; 619 | DBG_Printf(DBG_OTA, "OTAU: Match otau version 0x%08X image type 0x%04X\n", fileVersion, imageType); 620 | } 621 | } 622 | 623 | } 624 | } 625 | 626 | if (!updateFile.isEmpty()) 627 | { 628 | updateFile.prepend(path + "/"); 629 | OtauFileLoader ld; 630 | 631 | if (ld.readFile(updateFile, node->file)) 632 | { 633 | node->setHasData(true); 634 | DBG_Printf(DBG_OTA, "OTAU: found update file %s\n", qPrintable(updateFile)); 635 | } 636 | else 637 | { 638 | node->setHasData(false); 639 | DBG_Printf(DBG_OTA, "OTAU: found invalid update file %s\n", qPrintable(updateFile)); 640 | } 641 | } 642 | 643 | return false; 644 | } 645 | 646 | /*! Invalidates the upgrade end request. 647 | */ 648 | void StdOtauPlugin::invalidateUpdateEndRequest(OtauNode *node) 649 | { 650 | if (node) 651 | { 652 | if ((node->upgradeEndReq.fileVersion != 0) || (node->upgradeEndReq.manufacturerCode != 0)) 653 | { 654 | DBG_Printf(DBG_OTA, "OTAU: invalid update end request for node " FMT_MAC "\n", FMT_MAC_CAST(node->address().ext())); 655 | } 656 | 657 | node->upgradeEndReq.status = 0; 658 | node->upgradeEndReq.manufacturerCode = 0; 659 | node->upgradeEndReq.fileVersion = 0; 660 | node->upgradeEndReq.imageType = 0; 661 | } 662 | } 663 | 664 | /*! Handler to automatically send image page responses. 665 | */ 666 | void StdOtauPlugin::imagePageTimerFired() 667 | { 668 | if (!m_model || m_model->nodes().empty()) 669 | { 670 | return; 671 | } 672 | 673 | deCONZ::ApsController *apsCtrl = deCONZ::ApsController::instance(); 674 | if (!apsCtrl) 675 | { 676 | return; 677 | } 678 | 679 | if (apsCtrl->getParameter(deCONZ::ParamOtauActive) == 0) 680 | { 681 | return; 682 | } 683 | 684 | bool refire = false; 685 | 686 | for (OtauNode *node : m_model->nodes()) 687 | { 688 | if (!node) 689 | continue; 690 | 691 | if (node->state() == OtauNode::NodeWaitPageSpacing) 692 | { 693 | refire = true; 694 | if (!imagePageResponse(node)) 695 | { 696 | if (node->imgBlockResponseRetry >= MAX_IMG_BLOCK_RSP_RETRY) 697 | { 698 | // giveup 699 | node->setState(OtauNode::NodeIdle); 700 | } 701 | } 702 | } 703 | else if (node->state() == OtauNode::NodeWaitNextRequest) 704 | { 705 | refire = true; 706 | 707 | if (node->lastActivity.hasExpired(WAIT_NEXT_REQUEST_TIMEOUT)) 708 | { 709 | node->imgPageRequestRetry++; 710 | if (node->imgPageRequestRetry >= MAX_IMG_PAGE_REQ_RETRY) 711 | { 712 | // giveup 713 | node->setState(OtauNode::NodeIdle); 714 | } 715 | else 716 | { 717 | DBG_Printf(DBG_OTA, "OTAU: wait request timeout (retry %d)\n", node->imgPageRequestRetry); 718 | node->apsRequestId = INVALID_APS_REQ_ID; // don't wait for prior requests 719 | 720 | if (node->imgPageRequestRetry < 3) 721 | { 722 | unicastImageNotify(node->address()); 723 | } 724 | } 725 | } 726 | } 727 | } 728 | 729 | if (refire && !m_imagePageTimer->isActive()) 730 | { 731 | m_imagePageTimer->start(IMAGE_PAGE_TIMER_DELAY); 732 | } 733 | } 734 | 735 | /*! Handler to cleanup timed out nodes. 736 | */ 737 | void StdOtauPlugin::cleanupTimerFired() 738 | { 739 | if (!m_model) 740 | { 741 | return; 742 | } 743 | 744 | int activeNodes = 0; 745 | 746 | std::vector::iterator i = m_model->nodes().begin(); 747 | std::vector::iterator end = m_model->nodes().end(); 748 | 749 | for (; i != end; ++i) 750 | { 751 | OtauNode *node = *i; 752 | if (node->hasData()) 753 | { 754 | if (node->lastActivity.hasExpired(CLEANUP_DELAY)) 755 | { 756 | node->file.subElements.clear(); 757 | node->setHasData(false); 758 | DBG_Printf(DBG_OTA, "OTAU: cleanup node\n"); 759 | } 760 | else 761 | { 762 | activeNodes++; 763 | } 764 | } 765 | } 766 | 767 | if (activeNodes) 768 | { 769 | m_cleanupTimer->start(); 770 | } 771 | } 772 | 773 | void StdOtauPlugin::activityTimerFired() 774 | { 775 | const auto now = deCONZ::steadyTimeRef(); 776 | 777 | auto i = std::find_if(m_otauTracker.begin(), m_otauTracker.end(), [&](const OtauTracker &t) 778 | { 779 | return deCONZ::TimeSeconds{10} < (now - t.lastActivity); 780 | }); 781 | 782 | if (i != m_otauTracker.end()) 783 | { 784 | m_otauTracker.erase(i); 785 | } 786 | 787 | if (m_otauTracker.empty()) 788 | { 789 | m_activityTimer->stop(); 790 | } 791 | } 792 | 793 | void StdOtauPlugin::markOtauActivity(const deCONZ::Address &address) 794 | { 795 | if (!address.hasExt()) 796 | { 797 | return; 798 | } 799 | 800 | auto i = std::find_if(m_otauTracker.begin(), m_otauTracker.end(), [&](const OtauTracker &t) 801 | { 802 | return t.extAddr == address.ext(); 803 | }); 804 | 805 | if (i != m_otauTracker.end()) 806 | { 807 | i->lastActivity = deCONZ::steadyTimeRef(); 808 | } 809 | else if (m_otauTracker.size() < OTAU_MAX_ACTIVE) 810 | { 811 | OtauTracker t; 812 | t.extAddr = address.ext(); 813 | t.lastActivity = deCONZ::steadyTimeRef(); 814 | m_otauTracker.push_back(t); 815 | } 816 | 817 | if (!m_activityTimer->isActive()) 818 | { 819 | m_activityTimer->start(ACTIVITY_TIMER_DELAY); 820 | } 821 | } 822 | 823 | void StdOtauPlugin::checkFileLinks() 824 | { 825 | QStringList paths; 826 | paths.append(m_imgPath); 827 | //paths.append(deCONZ::getStorageLocation(deCONZ::ApplicationsDataLocation) + "/otau"); 828 | 829 | for (const QString &path : paths) 830 | { 831 | QDir dir(path); 832 | if (!dir.exists()) 833 | { 834 | continue; 835 | } 836 | 837 | const QStringList ls = dir.entryList(); 838 | 839 | for (const QString &n : ls) 840 | { 841 | QFile file(path + "/" + n); 842 | if (!file.open(QFile::ReadOnly)) 843 | continue; 844 | 845 | QByteArray arr = file.readAll(); 846 | if (arr.isEmpty()) 847 | continue; 848 | 849 | OtauFile of; 850 | of.path = n; 851 | if (!of.fromArray(arr)) 852 | continue; 853 | 854 | const QString fname = QString("%1-%2-%3").arg(of.manufacturerCode, 4, 16, QLatin1Char('0')) 855 | .arg(of.imageType, 4, 16, QLatin1Char('0')) 856 | .arg(of.fileVersion, 8, 16, QLatin1Char('0')) 857 | .toUpper(); 858 | 859 | bool ok= false; 860 | for (const QString &n2 : ls) 861 | { 862 | if (n2.startsWith(fname)) 863 | { 864 | ok = true; 865 | break; 866 | } 867 | } 868 | 869 | if (ok) 870 | continue; 871 | 872 | DBG_Printf(DBG_INFO, "OTAU: create %s.zigbee\n", qPrintable(fname)); 873 | file.copy(path + "/" + fname + ".zigbee"); 874 | } 875 | } 876 | } 877 | 878 | /*! Sends a image notify request. 879 | \param notf - the request parameters 880 | \return true on success false otherwise 881 | */ 882 | bool StdOtauPlugin::imageNotify(ImageNotifyReq *notf) 883 | { 884 | if (m_state == StateEnabled) 885 | { 886 | deCONZ::ApsDataRequest req; 887 | deCONZ::ZclFrame zclFrame; 888 | 889 | OtauNode *node = m_model->getNode(notf->addr); 890 | 891 | req.setDstAddressMode(notf->addrMode); 892 | req.dstAddress() = notf->addr; 893 | req.setDstEndpoint(notf->dstEndpoint); 894 | req.setSrcEndpoint(m_srcEndpoint); 895 | req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission); 896 | if (node) 897 | { 898 | req.setProfileId(node->profileId); 899 | DBG_Printf(DBG_OTA, "OTAU: send img notify to " FMT_MAC "\n", FMT_MAC_CAST(node->address().ext())); 900 | } 901 | else 902 | { 903 | req.setProfileId(0x0104); 904 | } 905 | req.setClusterId(OTAU_CLUSTER_ID); 906 | 907 | req.setRadius(notf->radius); 908 | 909 | zclFrame.setSequenceNumber(m_zclSeq++); 910 | zclFrame.setCommandId(OTAU_IMAGE_NOTIFY_CMD_ID); 911 | 912 | uint8_t frameControl = deCONZ::ZclFCClusterCommand | 913 | deCONZ::ZclFCDirectionServerToClient; 914 | 915 | if (notf->addr.isNwkBroadcast()) 916 | { 917 | frameControl |= deCONZ::ZclFCDisableDefaultResponse; 918 | } 919 | 920 | zclFrame.setFrameControl(frameControl); 921 | 922 | { // ZCL payload 923 | QDataStream stream(&zclFrame.payload(), QIODevice::WriteOnly); 924 | stream.setByteOrder(QDataStream::LittleEndian); 925 | 926 | stream << static_cast(0x00); // query jitter 927 | stream << static_cast(100); // query jitter value 928 | } 929 | 930 | { // ZCL frame 931 | QDataStream stream(&req.asdu(), QIODevice::WriteOnly); 932 | stream.setByteOrder(QDataStream::LittleEndian); 933 | zclFrame.writeToStream(stream); 934 | } 935 | 936 | if (deCONZ::ApsController::instance()->apsdeDataRequest(req) == deCONZ::Success) 937 | { 938 | return true; 939 | } 940 | } 941 | 942 | return false; 943 | } 944 | 945 | /*! Broadcasts a image notify request. 946 | */ 947 | bool StdOtauPlugin::broadcastImageNotify() 948 | { 949 | ImageNotifyReq notf; 950 | 951 | notf.radius = 0; 952 | notf.addr.setNwk(deCONZ::BroadcastRxOnWhenIdle); 953 | notf.addrMode = deCONZ::ApsNwkAddress; 954 | notf.dstEndpoint = 0xFF; // broadcast endpoint 955 | 956 | return imageNotify(¬f); 957 | } 958 | 959 | /*! Sends a image notify request per unicast. 960 | \param addr - the destination address 961 | \return true on success false otherwise 962 | */ 963 | bool StdOtauPlugin::unicastImageNotify(const deCONZ::Address &addr) 964 | { 965 | if (addr.hasExt()) 966 | { 967 | ImageNotifyReq notf; 968 | 969 | OtauNode *node = m_model->getNode(addr); 970 | 971 | if (!node) 972 | { 973 | return false; 974 | } 975 | 976 | notf.radius = 0; 977 | notf.addr = addr; 978 | notf.addrMode = deCONZ::ApsExtAddress; 979 | notf.dstEndpoint = node->endpoint; 980 | 981 | // blacklist some faulty versions tue image notify bug in BitCloud 3.2, 3.3 982 | if (node->manufacturerId == VENDOR_DDEL) 983 | { 984 | node->endpointNotify = 0x0A; 985 | notf.dstEndpoint = node->endpointNotify; 986 | 987 | if (node->imageType() == IMG_TYPE_FLS_PP3_H3) 988 | { 989 | notf.dstEndpoint = 0x0A; 990 | } 991 | else if (node->imageType() == IMG_TYPE_FLS_A2) 992 | { 993 | if (node->softwareVersion() > 0 && node->softwareVersion() < 0x201000C4) 994 | { 995 | return false; 996 | } 997 | } 998 | else if (node->imageType() == IMG_TYPE_FLS_NB) 999 | { 1000 | if (node->softwareVersion() > 0 && node->softwareVersion() < 0x200000C8) 1001 | { 1002 | return false; 1003 | } 1004 | } 1005 | } 1006 | 1007 | return imageNotify(¬f); 1008 | } 1009 | 1010 | return false; 1011 | } 1012 | 1013 | /*! Sends a upgrade end request to a node via unicast. 1014 | \param addr - the destination address 1015 | */ 1016 | void StdOtauPlugin::unicastUpgradeEndRequest(const deCONZ::Address &addr) 1017 | { 1018 | if (addr.hasExt()) 1019 | { 1020 | OtauNode *node = m_model->getNode(addr); 1021 | 1022 | DBG_Assert(node != nullptr); 1023 | if (node) 1024 | { 1025 | if (!upgradeEndResponse(node, DEFAULT_UPGRADE_TIME)) 1026 | { 1027 | DBG_Printf(DBG_OTA, "OTAU: failed to send upgrade end response\n"); 1028 | } 1029 | } 1030 | } 1031 | } 1032 | 1033 | /*! Handles a match descriptor request and sends the response if needed. 1034 | \param ind - the APSDE-DATA.indication 1035 | */ 1036 | void StdOtauPlugin::matchDescriptorRequest(const deCONZ::ApsDataIndication &ind) 1037 | { 1038 | bool sendResponse = false; 1039 | uint8_t seqNo; 1040 | uint16_t shortAddr; 1041 | uint16_t profileId; 1042 | uint8_t serverClusterCount; 1043 | 1044 | if (ind.asdu().size() < 7) // minimum size for match descriptor request 1045 | { 1046 | DBG_Printf(DBG_OTA, "OTAU: ignore match descriptor req from 0x%04X with asduSize %d\n", ind.srcAddress().nwk(), ind.asdu().size()); 1047 | } 1048 | 1049 | { 1050 | QDataStream stream(ind.asdu()); 1051 | stream.setByteOrder(QDataStream::LittleEndian); 1052 | stream >> seqNo; 1053 | stream >> shortAddr; 1054 | stream >> profileId; 1055 | stream >> serverClusterCount; 1056 | 1057 | for (uint i = 0; i < serverClusterCount; i++) 1058 | { 1059 | uint16_t clusterId; 1060 | stream >> clusterId; 1061 | 1062 | if (clusterId == OTAU_CLUSTER_ID && (profileId == ZLL_PROFILE_ID || profileId == HA_PROFILE_ID)) 1063 | { 1064 | const deCONZ::Node *coord = nullptr; 1065 | deCONZ::ApsController::instance()->getNode(0, &coord); 1066 | 1067 | DBG_Assert(coord != nullptr); 1068 | if (!coord) 1069 | { 1070 | return; 1071 | } 1072 | 1073 | for (const deCONZ::SimpleDescriptor &sd : coord->simpleDescriptors()) 1074 | { 1075 | if (sd.profileId() == profileId) 1076 | { 1077 | return; // firmware will handle this 1078 | } 1079 | } 1080 | 1081 | DBG_Printf(DBG_OTA, "OTAU: match descriptor req, profileId 0x%04X from 0x%04X\n", profileId, ind.srcAddress().nwk()); 1082 | sendResponse = true; 1083 | break; 1084 | } 1085 | } 1086 | } 1087 | 1088 | if (sendResponse) 1089 | { 1090 | deCONZ::ApsDataRequest req; 1091 | 1092 | //req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission); 1093 | req.dstAddress() = ind.srcAddress(); 1094 | req.setDstAddressMode(deCONZ::ApsNwkAddress); 1095 | req.setProfileId(ZDP_PROFILE_ID); 1096 | req.setClusterId(ZDP_MATCH_DESCRIPTOR_RSP_CLID); 1097 | req.setDstEndpoint(ZDO_ENDPOINT); 1098 | req.setSrcEndpoint(ZDO_ENDPOINT); 1099 | 1100 | QDataStream stream(&req.asdu(), QIODevice::WriteOnly); 1101 | stream.setByteOrder(QDataStream::LittleEndian); 1102 | 1103 | uint8_t status = 0x00; // success 1104 | shortAddr = 0x0000; // TODO query from apsController 1105 | uint8_t matchLength = 0x01; 1106 | uint8_t matchList = m_srcEndpoint; 1107 | 1108 | stream << seqNo; 1109 | stream << status; 1110 | stream << shortAddr; 1111 | stream << matchLength; 1112 | stream << matchList; 1113 | 1114 | if (deCONZ::ApsController::instance()->apsdeDataRequest(req) == deCONZ::Success) 1115 | { 1116 | DBG_Printf(DBG_OTA, "OTAU: send match descriptor rsp, match endpoint 0x%02X\n", matchList); 1117 | } 1118 | else 1119 | { 1120 | DBG_Printf(DBG_OTA, "OTAU: send match descriptor rsp failed\n"); 1121 | } 1122 | } 1123 | } 1124 | 1125 | /*! Handles a query next image request and sends the response. 1126 | \param ind - the APSDE-DATA.indication 1127 | \param zclFrame - the ZCL frame 1128 | */ 1129 | void StdOtauPlugin::queryNextImageRequest(const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame) 1130 | { 1131 | uint16_t u16; 1132 | uint32_t u32; 1133 | OtauNode *node = m_model->getNode(ind.srcAddress()); 1134 | 1135 | if (!node) 1136 | { 1137 | DBG_Printf(DBG_OTA, "OTAU: query next image request for unknown node " FMT_MAC "\n", FMT_MAC_CAST(ind.srcAddress().ext())); 1138 | return; 1139 | } 1140 | 1141 | if ((zclFrame.payload().size() != 9) && 1142 | (zclFrame.payload().size() != 11)) // with hardware version present 1143 | { 1144 | DBG_Printf(DBG_OTA, "OTAU: query next image request for node " FMT_MAC " invalid payload length %d\n", FMT_MAC_CAST(ind.srcAddress().ext()), zclFrame.payload().size()); 1145 | return; 1146 | } 1147 | 1148 | invalidateUpdateEndRequest(node); 1149 | 1150 | node->reqSequenceNumber = zclFrame.sequenceNumber(); 1151 | node->endpoint = ind.srcEndpoint(); 1152 | node->profileId = ind.profileId(); 1153 | node->setAddress(ind.srcAddress()); 1154 | node->refreshTimeout(); 1155 | node->restartElapsedTimer(); 1156 | node->setStatus(OtauNode::StatusSuccess); 1157 | 1158 | QDataStream stream(zclFrame.payload()); 1159 | stream.setByteOrder(QDataStream::LittleEndian); 1160 | 1161 | uint8_t fieldControl; 1162 | 1163 | stream >> fieldControl; 1164 | stream >> node->manufacturerId; 1165 | stream >> u16; 1166 | node->setImageType(u16); 1167 | stream >> u32; 1168 | node->setSoftwareVersion(u32); 1169 | if (fieldControl & 0x01) 1170 | { 1171 | stream >> u16; 1172 | node->setHardwareVersion(u16); 1173 | } 1174 | else 1175 | { 1176 | node->setHardwareVersion(0xFFFF); 1177 | } 1178 | 1179 | DBG_Printf(DBG_OTA, "OTAU: query next img req: " FMT_MAC " mfCode: 0x%04X, img type: 0x%04X, sw version: 0x%08X\n", 1180 | FMT_MAC_CAST(ind.srcAddress().ext()), node->manufacturerId, node->imageType(), node->softwareVersion()); 1181 | 1182 | if (deCONZ::ApsController::instance()->getParameter(deCONZ::ParamOtauActive) != 0) 1183 | { 1184 | // check for image 1185 | if (!node->hasData() && m_otauTracker.size() < OTAU_MAX_ACTIVE) 1186 | { 1187 | node->file.subElements.clear(); 1188 | node->setHasData(false); 1189 | node->setPermitUpdate(false); 1190 | 1191 | if (!checkForUpdateImageImage(node, m_imgPath)) 1192 | { 1193 | QString secondaryPath = deCONZ::getStorageLocation(deCONZ::ApplicationsDataLocation) + "/otau"; 1194 | 1195 | if (!checkForUpdateImageImage(node, secondaryPath)) 1196 | { 1197 | 1198 | } 1199 | } 1200 | } 1201 | } 1202 | 1203 | #ifdef USE_ACTOR_MODEL 1204 | if (am) 1205 | { 1206 | // broadcast OTA0 QUERY_NEXT_IMAGE_NOTIFY message 1207 | struct am_message *m; 1208 | 1209 | m = am->msg_alloc(); 1210 | m->src = AM_ACTOR_ID_OTA; 1211 | m->dst = AM_ACTOR_ID_SUBSCRIBERS; 1212 | m->id = OTA_M_ID_QUERY_NEXT_IMAGE_NOTIFY; 1213 | 1214 | am->msg_put_u64(m, (am_u64)(node->address().ext())); 1215 | am->msg_put_u16(m, node->manufacturerId); 1216 | am->msg_put_u16(m, node->imageType()); 1217 | am->msg_put_u32(m, node->softwareVersion()); 1218 | am->msg_put_u16(m, (unsigned short)node->hardwareVersion()); 1219 | 1220 | if (node->hasData()) 1221 | { 1222 | am->msg_put_u32(m, node->file.fileVersion); 1223 | } 1224 | else 1225 | { 1226 | am->msg_put_u32(m, 0); 1227 | } 1228 | 1229 | am->send_message(m); 1230 | } 1231 | #endif // USE_ACTOR_MODEL 1232 | 1233 | if (node->hasData() && node->rxOnWhenIdle) 1234 | { // sleeping devices must be manually enabled 1235 | node->setPermitUpdate(true); 1236 | } 1237 | 1238 | if (queryNextImageResponse(node)) 1239 | { 1240 | node->setState(OtauNode::NodeWaitConfirm); 1241 | } 1242 | else 1243 | { 1244 | // TODO handle error 1245 | node->setState(OtauNode::NodeIdle); 1246 | } 1247 | } 1248 | 1249 | /*! Sends a query next image response. 1250 | \param node - the destination node 1251 | \return true on success false otherwise 1252 | */ 1253 | bool StdOtauPlugin::queryNextImageResponse(OtauNode *node) 1254 | { 1255 | deCONZ::ApsDataRequest req; 1256 | deCONZ::ZclFrame zclFrame; 1257 | 1258 | DBG_Assert(node->address().hasExt()); 1259 | if (!node->address().hasExt()) 1260 | { 1261 | return false; 1262 | } 1263 | 1264 | req.setProfileId(node->profileId); 1265 | req.setDstEndpoint(node->endpoint); 1266 | req.setClusterId(OTAU_CLUSTER_ID); 1267 | req.dstAddress().setExt(node->address().ext()); 1268 | req.setDstAddressMode(deCONZ::ApsExtAddress); 1269 | req.setSrcEndpoint(m_srcEndpoint); 1270 | req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission); 1271 | req.setRadius(MAX_RADIUS); 1272 | 1273 | zclFrame.setSequenceNumber(node->reqSequenceNumber); 1274 | zclFrame.setCommandId(OTAU_QUERY_NEXT_IMAGE_RESPONSE_CMD_ID); 1275 | 1276 | zclFrame.setFrameControl(deCONZ::ZclFCClusterCommand | 1277 | deCONZ::ZclFCDirectionServerToClient | 1278 | deCONZ::ZclFCDisableDefaultResponse); 1279 | 1280 | { // ZCL payload 1281 | QDataStream stream(&zclFrame.payload(), QIODevice::WriteOnly); 1282 | stream.setByteOrder(QDataStream::LittleEndian); 1283 | 1284 | if (node->state() == OtauNode::NodeAbort) 1285 | { 1286 | stream << (uint8_t)OTAU_ABORT; 1287 | DBG_Printf(DBG_OTA, "OTAU: send query next image response: OTAU_ABORT\n"); 1288 | } 1289 | else if (m_otauTracker.size() >= OTAU_MAX_ACTIVE) 1290 | { 1291 | DBG_Printf(DBG_OTA, "OTAU: busy, don't answer and let node run in timeout\n"); 1292 | return false; 1293 | } 1294 | else if (node->manufacturerId == VENDOR_DDEL && 1295 | node->imageType() == IMG_TYPE_FLS_PP3_H3 && 1296 | node->softwareVersion() >= 0x20000050 && 1297 | node->softwareVersion() <= 0x20000054 && 1298 | node->file.fileVersion < 0x201000eb) 1299 | { 1300 | // workaround to prevent update FLS-H lp with older FLS-PP lp versions 1301 | stream << (uint8_t)OTAU_NO_IMAGE_AVAILABLE; 1302 | DBG_Printf(DBG_OTA, "OTAU: send query next image response: OTAU_NO_IMAGE_AVAILABLE to FLS-H lp\n"); 1303 | } 1304 | else if (node->permitUpdate() && node->hasData() && node->file.raw.size() != 0) 1305 | { 1306 | node->rawFile = node->file.raw; 1307 | stream << (uint8_t)OTAU_SUCCESS; 1308 | stream << node->file.manufacturerCode; 1309 | stream << node->file.imageType; 1310 | stream << node->file.fileVersion; 1311 | stream << node->file.totalImageSize; 1312 | 1313 | markOtauActivity(node->address()); 1314 | } 1315 | else 1316 | { 1317 | if (node->manufacturerId == VENDOR_BUSCH_JAEGER) 1318 | { 1319 | stream << (uint8_t)OTAU_ABORT; 1320 | DBG_Printf(DBG_OTA, "OTAU: send query next image response: OTAU_ABORT\n"); 1321 | } 1322 | else 1323 | { 1324 | stream << (uint8_t)OTAU_NO_IMAGE_AVAILABLE; 1325 | DBG_Printf(DBG_OTA, "OTAU: send query next image response: OTAU_NO_IMAGE_AVAILABLE\n"); 1326 | } 1327 | } 1328 | } 1329 | 1330 | if ((node->address().ext() & macPrefixMask) == osramMacPrefix && 1331 | !(node->permitUpdate() && node->hasData())) 1332 | { 1333 | DBG_Printf(DBG_OTA, "OTAU: don't answer OSRAM node: OTAU_NO_IMAGE_AVAILABLE\n"); 1334 | return false; 1335 | } 1336 | 1337 | { // ZCL frame 1338 | QDataStream stream(&req.asdu(), QIODevice::WriteOnly); 1339 | stream.setByteOrder(QDataStream::LittleEndian); 1340 | zclFrame.writeToStream(stream); 1341 | } 1342 | 1343 | if (deCONZ::ApsController::instance()->apsdeDataRequest(req) == 0) 1344 | { 1345 | node->apsRequestId = req.id(); 1346 | node->zclCommandId = zclFrame.commandId(); 1347 | return true; 1348 | } 1349 | 1350 | return false; 1351 | } 1352 | 1353 | /*! Handles a image block request and sends the response. 1354 | \param ind - the APSDE-DATA.indication 1355 | \param zclFrame - the ZCL frame 1356 | */ 1357 | void StdOtauPlugin::imageBlockRequest(const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame) 1358 | { 1359 | OtauNode *node = m_model->getNode(ind.srcAddress()); 1360 | 1361 | if (!node) 1362 | { 1363 | return; 1364 | } 1365 | 1366 | markOtauActivity(node->address()); 1367 | 1368 | node->refreshTimeout(); 1369 | invalidateUpdateEndRequest(node); 1370 | 1371 | QDataStream stream(zclFrame.payload()); 1372 | stream.setByteOrder(QDataStream::LittleEndian); 1373 | 1374 | stream >> node->imgBlockReq.fieldControl; 1375 | stream >> node->imgBlockReq.manufacturerCode; 1376 | stream >> node->imgBlockReq.imageType; 1377 | stream >> node->imgBlockReq.fileVersion; 1378 | stream >> node->imgBlockReq.offset; 1379 | stream >> node->imgBlockReq.maxDataSize; 1380 | 1381 | if (node->imgBlockReq.fileVersion == DONT_CARE_FILE_VERSION) 1382 | { 1383 | node->imgBlockReq.fileVersion = node->file.fileVersion; 1384 | } 1385 | 1386 | node->setStatus(OtauNode::StatusSuccess); 1387 | node->setOffset(node->imgBlockReq.offset); 1388 | node->setImageType(node->imgBlockReq.imageType); 1389 | node->notifyElapsedTimer(); 1390 | 1391 | node->reqSequenceNumber = zclFrame.sequenceNumber(); 1392 | node->endpoint = ind.srcEndpoint(); 1393 | node->profileId = ind.profileId(); 1394 | 1395 | DBG_Printf(DBG_OTA, "OTAU: img block req fwVersion:0x%08X, offset: 0x%08X, maxsize: %u\n", node->imgBlockReq.fileVersion, node->imgBlockReq.offset, node->imgBlockReq.maxDataSize); 1396 | 1397 | // IEEE address present? 1398 | if (node->imgBlockReq.fieldControl & 0x01) 1399 | { 1400 | quint64 extAddr; 1401 | stream >> extAddr; 1402 | 1403 | deCONZ::Address addr = node->address(); 1404 | addr.setExt(extAddr); 1405 | node->setAddress(addr); 1406 | } 1407 | 1408 | node->apsRequestId = INVALID_APS_REQ_ID; 1409 | if (imageBlockResponse(node)) 1410 | { 1411 | node->setState(OtauNode::NodeWaitConfirm); 1412 | } 1413 | else 1414 | { 1415 | DBG_Printf(DBG_OTA, "OTAU: failed to send image block response\n"); 1416 | node->setState(OtauNode::NodeIdle); 1417 | } 1418 | } 1419 | 1420 | /*! Sends a image block response. 1421 | \param node - the destination node 1422 | \return true on success false otherwise 1423 | */ 1424 | bool StdOtauPlugin::imageBlockResponse(OtauNode *node) 1425 | { 1426 | DBG_Assert(node->address().hasExt()); 1427 | if (!node->address().hasExt()) 1428 | { 1429 | return false; 1430 | } 1431 | 1432 | if (node->apsRequestId != INVALID_APS_REQ_ID) 1433 | { 1434 | if (node->lastResponseTime.isValid() && 1435 | node->lastResponseTime.elapsed() < (1000 * 10)) // prevent stallation 1436 | { 1437 | //DBG_Printf(DBG_OTA, "OTAU: ...\n"); 1438 | return false; 1439 | } 1440 | 1441 | DBG_Printf(DBG_OTA, "OTAU: warn apsRequestId != 0\n"); 1442 | } 1443 | 1444 | uint8_t dataSize = 0; 1445 | deCONZ::ApsDataRequest req; 1446 | deCONZ::ZclFrame zclFrame; 1447 | 1448 | req.setProfileId(node->profileId); 1449 | req.setDstEndpoint(node->endpoint); 1450 | req.setClusterId(OTAU_CLUSTER_ID); 1451 | req.dstAddress() = node->address(); 1452 | req.setDstAddressMode(deCONZ::ApsExtAddress); 1453 | //req.setTxOptions(deCONZ::ApsTxFragmentationPermitted); 1454 | req.setSrcEndpoint(m_srcEndpoint); 1455 | // APS ACKs are enabled for single image block requests 1456 | // they are disabled for image page request responses 1457 | if ((node->lastZclCmd() == OTAU_IMAGE_BLOCK_REQUEST_CMD_ID) || (node->state() == OtauNode::NodeAbort) || m_w->acksEnabled()) 1458 | { 1459 | req.setTxOptions(req.txOptions() | deCONZ::ApsTxAcknowledgedTransmission); 1460 | } 1461 | 1462 | zclFrame.setSequenceNumber(node->reqSequenceNumber); 1463 | req.setRadius(MAX_RADIUS); 1464 | 1465 | zclFrame.setCommandId(OTAU_IMAGE_BLOCK_RESPONSE_CMD_ID); 1466 | 1467 | zclFrame.setFrameControl(deCONZ::ZclFCClusterCommand | 1468 | deCONZ::ZclFCDirectionServerToClient | 1469 | deCONZ::ZclFCDisableDefaultResponse); 1470 | 1471 | { // ZCL payload 1472 | QDataStream stream(&zclFrame.payload(), QIODevice::WriteOnly); 1473 | stream.setByteOrder(QDataStream::LittleEndian); 1474 | 1475 | if ((node->imgBlockReq.fileVersion != node->file.fileVersion) || 1476 | (node->imgBlockReq.imageType != node->file.imageType) || 1477 | (node->imgBlockReq.manufacturerCode != node->file.manufacturerCode)) 1478 | { 1479 | stream << (uint8_t)OTAU_ABORT; 1480 | node->setState(OtauNode::NodeAbort); 1481 | DBG_Printf(DBG_OTA, "OTAU: send img block 0x%016llX OTAU_ABORT\n", node->address().ext()); 1482 | } 1483 | else if (node->state() == OtauNode::NodeAbort) 1484 | { 1485 | stream << (uint8_t)OTAU_ABORT; 1486 | DBG_Printf(DBG_OTA, "OTAU: send img block 0x%016llX OTAU_ABORT\n", node->address().ext()); 1487 | } 1488 | else if (!node->permitUpdate() || !node->hasData()) 1489 | { 1490 | stream << (uint8_t)OTAU_NO_IMAGE_AVAILABLE; 1491 | DBG_Printf(DBG_OTA, "OTAU: send img block 0x%016llX OTAU_NO_IMAGE_AVAILABLE\n", node->address().ext()); 1492 | } 1493 | else if (node->imgBlockReq.offset < (uint32_t)node->rawFile.size()) 1494 | { 1495 | if (node->imgBlockReq.maxDataSize > MAX_DATA_SIZE) 1496 | { 1497 | dataSize = MAX_DATA_SIZE; 1498 | } 1499 | else 1500 | { 1501 | dataSize = node->imgBlockReq.maxDataSize; 1502 | } 1503 | 1504 | // some older DDEL and BJ devices have an error in BitCloud stack to support larger payloads 1505 | if ((node->manufacturerId == VENDOR_DDEL || node->manufacturerId == VENDOR_BUSCH_JAEGER) && dataSize > 40) 1506 | { 1507 | dataSize = 40; 1508 | } 1509 | 1510 | uint32_t offset = node->imgBlockReq.offset; 1511 | 1512 | stream << (uint8_t)OTAU_SUCCESS; 1513 | stream << node->file.manufacturerCode; 1514 | stream << node->file.imageType; 1515 | stream << node->file.fileVersion; 1516 | stream << node->imgBlockReq.offset; 1517 | 1518 | dataSize = (uint8_t)qMin((uint32_t)dataSize, ((uint32_t)node->rawFile.size() - offset)); 1519 | 1520 | if (node->lastZclCmd() == OTAU_IMAGE_PAGE_REQUEST_CMD_ID) 1521 | { 1522 | // only fill till page boundary 1523 | dataSize = qMin((uint32_t)dataSize, (uint32_t)(node->imgBlockReq.pageSize - node->imgBlockReq.pageBytesDone)); 1524 | 1525 | if (dataSize == 0) 1526 | { 1527 | // dont send empty block response 1528 | DBG_Printf(DBG_OTA, "OTAU: prevent img block rsp with dataSize = 0 0x%016llX\n", node->address().ext()); 1529 | return false; 1530 | } 1531 | } 1532 | 1533 | // truncate datasize if not enough data is left 1534 | uint32_t avail = static_cast(node->rawFile.size()) - offset; 1535 | if (avail < dataSize) 1536 | { 1537 | dataSize = static_cast(avail); 1538 | } 1539 | 1540 | if (dataSize == 0) 1541 | { 1542 | DBG_Printf(DBG_OTA, "OTAU: warn img block rsp with dataSize = 0 0x%016llX\n", node->address().ext()); 1543 | } 1544 | 1545 | stream << dataSize; 1546 | 1547 | for (uint i = 0; (i < dataSize) && (offset < (uint32_t)node->rawFile.size()); i++) 1548 | { 1549 | stream << (uint8_t)node->rawFile[offset++]; 1550 | } 1551 | 1552 | node->imgBlockReq.maxDataSize = dataSize; // remember 1553 | } 1554 | else 1555 | { 1556 | DBG_Printf(DBG_OTA, "OTAU: send img block 0x%016llX OTAU_MALFORMED_COMMAND\n", node->address().ext()); 1557 | stream << (uint8_t)OTAU_MALFORMED_COMMAND; 1558 | } 1559 | } 1560 | 1561 | { // ZCL frame 1562 | QDataStream stream(&req.asdu(), QIODevice::WriteOnly); 1563 | stream.setByteOrder(QDataStream::LittleEndian); 1564 | zclFrame.writeToStream(stream); 1565 | } 1566 | 1567 | if (deCONZ::ApsController::instance()->apsdeDataRequest(req) == deCONZ::Success) 1568 | { 1569 | if (zclFrame.payload().size() > 1) 1570 | { 1571 | DBG_Printf(DBG_OTA, "OTAU: send img block rsp seq: %u offset: 0x%08X dataSize %u status: 0x%02X 0x%016llX\n", zclFrame.sequenceNumber(), node->imgBlockReq.offset, dataSize, quint8(zclFrame.payload().at(0)), node->address().ext()); 1572 | } 1573 | 1574 | node->apsRequestId = req.id(); 1575 | node->zclCommandId = zclFrame.commandId(); 1576 | node->lastResponseTime.invalidate(); 1577 | node->lastResponseTime.start(); 1578 | return true; 1579 | } 1580 | 1581 | DBG_Printf(DBG_OTA, "OTAU: send img block response failed\n"); 1582 | return false; 1583 | } 1584 | 1585 | /*! Handles a image page request and sends the response. 1586 | \param ind - the APSDE-DATA.indication 1587 | \param zclFrame - the ZCL frame 1588 | */ 1589 | void StdOtauPlugin::imagePageRequest(const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame) 1590 | { 1591 | OtauNode *node = m_model->getNode(ind.srcAddress()); 1592 | 1593 | if (!node) 1594 | { 1595 | return; 1596 | } 1597 | 1598 | markOtauActivity(node->address()); 1599 | 1600 | deCONZ::ApsController *apsCtrl = deCONZ::ApsController::instance(); 1601 | if (!apsCtrl) 1602 | { 1603 | return; 1604 | } 1605 | 1606 | if (!m_w->pageRequestEnabled()) 1607 | { 1608 | return; 1609 | } 1610 | 1611 | node->reqSequenceNumber = zclFrame.sequenceNumber(); 1612 | 1613 | if (node->state() == OtauNode::NodeAbort) 1614 | { 1615 | defaultResponse(node, zclFrame.commandId(), OTAU_ABORT); 1616 | return; 1617 | } 1618 | 1619 | if (!m_w->pageRequestEnabled()) 1620 | { 1621 | defaultResponse(node, zclFrame.commandId(), OTAU_UNSUP_CLUSTER_COMMAND); 1622 | return; 1623 | } 1624 | 1625 | node->refreshTimeout(); 1626 | invalidateUpdateEndRequest(node); 1627 | 1628 | QDataStream stream(zclFrame.payload()); 1629 | stream.setByteOrder(QDataStream::LittleEndian); 1630 | 1631 | stream >> node->imgPageReq.fieldControl; 1632 | stream >> node->imgPageReq.manufacturerCode; 1633 | stream >> node->imgPageReq.imageType; 1634 | stream >> node->imgPageReq.fileVersion; 1635 | stream >> node->imgPageReq.offset; 1636 | stream >> node->imgPageReq.maxDataSize; 1637 | stream >> node->imgPageReq.pageSize; 1638 | stream >> node->imgPageReq.responseSpacing; 1639 | 1640 | if (node->imgPageReq.fileVersion == DONT_CARE_FILE_VERSION) 1641 | { 1642 | node->imgPageReq.fileVersion = node->file.fileVersion; 1643 | } 1644 | 1645 | if (node->imgPageReq.responseSpacing > MAX_RESPONSE_SPACING) 1646 | { 1647 | node->imgPageReq.responseSpacing = MAX_RESPONSE_SPACING; 1648 | } 1649 | else if (node->imgPageReq.responseSpacing < MIN_RESPONSE_SPACING) 1650 | { 1651 | node->imgPageReq.responseSpacing = MIN_RESPONSE_SPACING; 1652 | } 1653 | 1654 | node->imgPageReq.pageBytesDone = 0; 1655 | 1656 | node->imgBlockReq = node->imgPageReq; 1657 | 1658 | node->setOffset(node->imgBlockReq.offset); 1659 | node->setImageType(node->imgBlockReq.imageType); 1660 | node->notifyElapsedTimer(); 1661 | 1662 | node->endpoint = ind.srcEndpoint(); 1663 | node->profileId = ind.profileId(); 1664 | 1665 | if (DBG_IsEnabled(DBG_OTA)) 1666 | { 1667 | DBG_Printf(DBG_OTA, "OTAU: img page req fwVersion:0x%08X, offset: 0x%08X, pageSize: %u, rspSpacing: %u ms\n", node->imgBlockReq.fileVersion, node->imgBlockReq.offset, node->imgBlockReq.pageSize, node->imgBlockReq.responseSpacing); 1668 | } 1669 | 1670 | // IEEE address present? 1671 | if (node->imgBlockReq.fieldControl & 0x01) 1672 | { 1673 | quint64 requestNodeAddress; 1674 | stream >> requestNodeAddress; 1675 | // not used 1676 | } 1677 | 1678 | node->apsRequestId = INVALID_APS_REQ_ID; // don't wait for prior requests 1679 | node->imgPageRequestRetry = 0; 1680 | node->imgBlockResponseRetry = 0; 1681 | 1682 | node->setState(OtauNode::NodeWaitPageSpacing); 1683 | node->lastResponseTime.start(); 1684 | if (!m_imagePageTimer->isActive()) 1685 | { 1686 | m_imagePageTimer->start(IMAGE_PAGE_TIMER_DELAY); 1687 | } 1688 | } 1689 | 1690 | /*! Sends a image block responses for a whole page. 1691 | \param node - the destination node 1692 | \return true on success false otherwise 1693 | */ 1694 | bool StdOtauPlugin::imagePageResponse(OtauNode *node) 1695 | { 1696 | DBG_Assert(node != nullptr); 1697 | if (!node) 1698 | { 1699 | return false; 1700 | } 1701 | 1702 | if (node->lastZclCmd() != OTAU_IMAGE_PAGE_REQUEST_CMD_ID) 1703 | { 1704 | return false; 1705 | } 1706 | 1707 | if (node->state() == OtauNode::NodeAbort) 1708 | { 1709 | return imageBlockResponse(node); 1710 | } 1711 | 1712 | if (node->apsRequestId != INVALID_APS_REQ_ID && node->zclCommandId == OTAU_IMAGE_BLOCK_RESPONSE_CMD_ID) 1713 | { 1714 | // wait confirm 1715 | return true; 1716 | } 1717 | 1718 | if (node->imgBlockReq.pageBytesDone >= node->imgBlockReq.pageSize) 1719 | { 1720 | node->setState(OtauNode::NodeWaitNextRequest); 1721 | 1722 | if (!m_imagePageTimer->isActive()) 1723 | { 1724 | m_imagePageTimer->start(IMAGE_PAGE_TIMER_DELAY); 1725 | } 1726 | return true; 1727 | } 1728 | 1729 | //if (node->imgBlockReq.pageBytesDone > 0) 1730 | { 1731 | int spacing = m_w->packetSpacingMs(); 1732 | 1733 | if (node->lastResponseTime.isValid() && !node->lastResponseTime.hasExpired(spacing)) 1734 | { 1735 | node->setState(OtauNode::NodeWaitPageSpacing); 1736 | 1737 | if (!m_imagePageTimer->isActive()) 1738 | { 1739 | m_imagePageTimer->start(IMAGE_PAGE_TIMER_DELAY); 1740 | } 1741 | 1742 | return true; 1743 | } 1744 | } 1745 | 1746 | int succ = 0; 1747 | 1748 | if (imageBlockResponse(node)) 1749 | { 1750 | node->imgBlockResponseRetry = 0; 1751 | succ++; 1752 | } 1753 | else 1754 | { 1755 | node->setState(OtauNode::NodeWaitPageSpacing); 1756 | node->imgBlockResponseRetry++; 1757 | DBG_Printf(DBG_OTA, "OTAU: failed send img block rsp (retry %d)\n", node->imgBlockResponseRetry); 1758 | 1759 | } 1760 | 1761 | return succ > 0; 1762 | } 1763 | 1764 | /*! Handles a upgrade end request and sends the response. 1765 | \param ind - the APSDE-DATA.indication 1766 | \param zclFrame - the ZCL frame 1767 | */ 1768 | void StdOtauPlugin::upgradeEndRequest(const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame) 1769 | { 1770 | OtauNode *node = m_model->getNode(ind.srcAddress()); 1771 | 1772 | if (!node) 1773 | { 1774 | return; 1775 | } 1776 | 1777 | node->refreshTimeout(); 1778 | 1779 | QDataStream stream(zclFrame.payload()); 1780 | stream.setByteOrder(QDataStream::LittleEndian); 1781 | 1782 | stream >> node->upgradeEndReq.status; 1783 | stream >> node->upgradeEndReq.manufacturerCode; 1784 | stream >> node->upgradeEndReq.imageType; 1785 | stream >> node->upgradeEndReq.fileVersion; 1786 | 1787 | if (node->hasData()) 1788 | { 1789 | node->setOffset(node->imgBlockReq.offset); 1790 | node->setImageType(node->imgBlockReq.imageType); 1791 | } 1792 | node->notifyElapsedTimer(); 1793 | 1794 | node->reqSequenceNumber = zclFrame.sequenceNumber(); 1795 | node->endpoint = ind.srcEndpoint(); 1796 | node->profileId = ind.profileId(); 1797 | 1798 | DBG_Printf(DBG_OTA, "OTAU: upgrade end req: status: 0x%02X, fwVersion:0x%08X, imgType: 0x%04X\n", node->upgradeEndReq.status, node->upgradeEndReq.fileVersion, node->upgradeEndReq.imageType); 1799 | 1800 | node->setState(OtauNode::NodeIdle); 1801 | 1802 | if (node->upgradeEndReq.status == OTAU_SUCCESS) 1803 | { 1804 | if (node->imgBlockReq.offset == 0) 1805 | { 1806 | // This is a workaround for buggy Osram/Ledvance Plug Z3 firmware (maybe Plug 01 as well) 1807 | // it sends upgrade end request _without_ any update happening before, 1808 | // send a ABORT status to break the reboot cycle. 1809 | defaultResponse(node, zclFrame.commandId(), OTAU_ABORT); 1810 | return; 1811 | } 1812 | 1813 | node->setStatus(OtauNode::StatusWaitUpgradeEnd); 1814 | node->setOffset(node->file.totalImageSize); // mark done 1815 | 1816 | node->file.subElements.clear(); 1817 | node->setHasData(false); 1818 | node->setPermitUpdate(false); 1819 | 1820 | // use time from widget 1821 | uint32_t upgradeTime = m_w->restartTime(); 1822 | 1823 | if (!upgradeEndResponse(node, upgradeTime)) 1824 | { 1825 | DBG_Printf(DBG_OTA, "OTAU: failed to send upgrade end response\n"); 1826 | } 1827 | } 1828 | else 1829 | { // TODO show detailed status 1830 | node->setStatus(OtauNode::StatusUnknownError); 1831 | defaultResponse(node, zclFrame.commandId(), deCONZ::ZclSuccessStatus); 1832 | } 1833 | } 1834 | 1835 | /*! Sends a upgrade end response. 1836 | \param node - the destination node 1837 | \param upgradeTime - time in seconds to wait for switching to new image 1838 | \return true on success false otherwise 1839 | */ 1840 | bool StdOtauPlugin::upgradeEndResponse(OtauNode *node, uint32_t upgradeTime) 1841 | { 1842 | deCONZ::ApsDataRequest req; 1843 | deCONZ::ZclFrame zclFrame; 1844 | 1845 | DBG_Assert(node->address().hasExt()); 1846 | if (!node->address().hasExt()) 1847 | { 1848 | return false; 1849 | } 1850 | 1851 | if (node->upgradeEndReq.manufacturerCode == 0 && 1852 | node->upgradeEndReq.fileVersion == 0 && 1853 | node->upgradeEndReq.status != OTAU_SUCCESS) 1854 | { 1855 | DBG_Printf(DBG_OTA,"OTAU: upgrade end response not send because status is not success but 0x%02X\n", node->upgradeEndReq.status); 1856 | return false; 1857 | } 1858 | 1859 | req.setProfileId(node->profileId); 1860 | req.setDstEndpoint(node->endpoint); 1861 | req.setClusterId(OTAU_CLUSTER_ID); 1862 | req.dstAddress() = node->address(); 1863 | req.setDstAddressMode(deCONZ::ApsExtAddress); 1864 | req.setSrcEndpoint(m_srcEndpoint); 1865 | req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission); 1866 | req.setRadius(MAX_RADIUS); 1867 | 1868 | zclFrame.setSequenceNumber(node->reqSequenceNumber); 1869 | zclFrame.setCommandId(OTAU_UPGRADE_END_RESPONSE_CMD_ID); 1870 | 1871 | zclFrame.setFrameControl(deCONZ::ZclFCClusterCommand | 1872 | deCONZ::ZclFCDirectionServerToClient | 1873 | deCONZ::ZclFCDisableDefaultResponse); 1874 | 1875 | { // ZCL payload 1876 | QDataStream stream(&zclFrame.payload(), QIODevice::WriteOnly); 1877 | stream.setByteOrder(QDataStream::LittleEndian); 1878 | 1879 | stream << node->upgradeEndReq.manufacturerCode; 1880 | stream << node->upgradeEndReq.imageType; 1881 | stream << node->upgradeEndReq.fileVersion; 1882 | 1883 | uint32_t currentTime = 0; 1884 | 1885 | stream << currentTime; 1886 | stream << upgradeTime; 1887 | } 1888 | 1889 | { // ZCL frame 1890 | QDataStream stream(&req.asdu(), QIODevice::WriteOnly); 1891 | stream.setByteOrder(QDataStream::LittleEndian); 1892 | zclFrame.writeToStream(stream); 1893 | } 1894 | 1895 | bool ret = false; 1896 | 1897 | if (deCONZ::ApsController::instance()->apsdeDataRequest(req) == 0) 1898 | { 1899 | node->apsRequestId = req.id(); 1900 | node->zclCommandId = zclFrame.commandId(); 1901 | if (upgradeTime < OTA_TIME_INFINITE) 1902 | { 1903 | node->setStatus(OtauNode::StatusSuccess); 1904 | } 1905 | ret = true; 1906 | } 1907 | 1908 | return ret; 1909 | } 1910 | 1911 | bool StdOtauPlugin::defaultResponse(OtauNode *node, quint8 commandId, quint8 status) 1912 | { 1913 | deCONZ::ApsDataRequest req; 1914 | deCONZ::ZclFrame zclFrame; 1915 | 1916 | DBG_Assert(node->address().hasExt()); 1917 | if (!node->address().hasExt()) 1918 | { 1919 | return false; 1920 | } 1921 | 1922 | req.setProfileId(node->profileId); 1923 | req.setDstEndpoint(node->endpoint); 1924 | req.setClusterId(OTAU_CLUSTER_ID); 1925 | req.dstAddress() = node->address(); 1926 | req.setDstAddressMode(deCONZ::ApsExtAddress); 1927 | 1928 | req.setSrcEndpoint(m_srcEndpoint); 1929 | req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission); 1930 | 1931 | zclFrame.setSequenceNumber(node->reqSequenceNumber); 1932 | req.setRadius(MAX_RADIUS); 1933 | 1934 | zclFrame.setCommandId(deCONZ::ZclDefaultResponseId); 1935 | 1936 | zclFrame.setFrameControl(deCONZ::ZclFCProfileCommand | 1937 | deCONZ::ZclFCDirectionServerToClient | 1938 | deCONZ::ZclFCDisableDefaultResponse); 1939 | 1940 | { // ZCL payload 1941 | QDataStream stream(&zclFrame.payload(), QIODevice::WriteOnly); 1942 | stream.setByteOrder(QDataStream::LittleEndian); 1943 | stream << commandId; 1944 | stream << status; 1945 | } 1946 | 1947 | { // ZCL frame 1948 | QDataStream stream(&req.asdu(), QIODevice::WriteOnly); 1949 | stream.setByteOrder(QDataStream::LittleEndian); 1950 | zclFrame.writeToStream(stream); 1951 | } 1952 | 1953 | if (deCONZ::ApsController::instance()->apsdeDataRequest(req) == deCONZ::Success) 1954 | { 1955 | node->apsRequestId = req.id(); 1956 | node->zclCommandId = zclFrame.commandId(); 1957 | node->lastResponseTime.restart(); 1958 | return true; 1959 | } 1960 | 1961 | return false; 1962 | } 1963 | 1964 | /*! Queries this plugin which features are supported. 1965 | \param feature - feature to be checked 1966 | \return true if supported 1967 | */ 1968 | bool StdOtauPlugin::hasFeature(Features feature) 1969 | { 1970 | switch (feature) 1971 | { 1972 | case WidgetFeature: 1973 | return true; 1974 | 1975 | default: 1976 | break; 1977 | } 1978 | 1979 | return false; 1980 | } 1981 | 1982 | /*! Creates a control widget for this plugin. 1983 | \return the plugin widget 1984 | */ 1985 | QWidget *StdOtauPlugin::createWidget() 1986 | { 1987 | if (!m_w) 1988 | { 1989 | m_w = new StdOtauWidget(nullptr); 1990 | 1991 | connect(m_w, SIGNAL(unicastImageNotify(deCONZ::Address)), 1992 | this, SLOT(unicastImageNotify(deCONZ::Address))); 1993 | 1994 | connect(m_w, SIGNAL(unicastUpgradeEndRequest(deCONZ::Address)), 1995 | this, SLOT(unicastUpgradeEndRequest(deCONZ::Address))); 1996 | 1997 | connect(m_w, SIGNAL(broadcastImageNotify()), 1998 | this, SLOT(broadcastImageNotify())); 1999 | 2000 | connect(m_w, SIGNAL(activatedNodeAtRow(int)), 2001 | this, SLOT(activatedNodeAtRow(int))); 2002 | 2003 | connect(this, SIGNAL(stateChanged(int)), m_w, SLOT(stateChanged(int))); 2004 | 2005 | m_w->setOtauModel(m_model); 2006 | m_w->setPacketSpacingMs(m_fastPageSpaceing); 2007 | } 2008 | 2009 | return m_w; 2010 | } 2011 | 2012 | /*! Creates a control dialog for this plugin. 2013 | \return 0 - not implemented 2014 | */ 2015 | QDialog *StdOtauPlugin::createDialog() 2016 | { 2017 | return nullptr; 2018 | } 2019 | 2020 | /*! Returns the name of this plugin. 2021 | */ 2022 | const char *StdOtauPlugin::name() 2023 | { 2024 | return "STD OTAU Plugin"; 2025 | } 2026 | 2027 | /*! Sets the plugin state. 2028 | \param state - the new state 2029 | */ 2030 | void StdOtauPlugin::setState(StdOtauPlugin::State state) 2031 | { 2032 | if (m_state != state) 2033 | { 2034 | m_state = state; 2035 | emit stateChanged(m_state); 2036 | } 2037 | } 2038 | 2039 | /*! Checks if the node is a not yet known otau node. 2040 | If the node is unknown it will be added to the model. 2041 | \param node - the node to check 2042 | \param endpoint - otau endpoint of the node 2043 | */ 2044 | void StdOtauPlugin::checkIfNewOtauNode(const deCONZ::Node *node, uint8_t endpoint) 2045 | { 2046 | DBG_Assert(node != nullptr); 2047 | if (!node) 2048 | { 2049 | return; 2050 | } 2051 | 2052 | if (node->nodeDescriptor().isNull()) 2053 | { 2054 | return; 2055 | } 2056 | 2057 | const deCONZ::SimpleDescriptor *sd = nullptr; 2058 | 2059 | // on dresden elektronik FLS only first OTA endpoint should be used 2060 | if (node->nodeDescriptor().manufacturerCode() == VENDOR_DDEL && endpoint > 0x0A && endpoint < 0x20) 2061 | { 2062 | const auto i = std::find_if(node->simpleDescriptors().cbegin(), node->simpleDescriptors().cend(), [](const deCONZ::SimpleDescriptor &s) 2063 | { 2064 | if (s.endpoint() != 0x0A) 2065 | { 2066 | return false; 2067 | } 2068 | 2069 | for (const deCONZ::ZclCluster &cl : s.outClusters()) 2070 | { 2071 | if (cl.id() == OTAU_CLUSTER_ID) 2072 | { 2073 | return true; 2074 | } 2075 | } 2076 | 2077 | return false; 2078 | }); 2079 | 2080 | if (i != node->simpleDescriptors().cend()) 2081 | { 2082 | endpoint = i->endpoint(); 2083 | sd = &*i; 2084 | } 2085 | } 2086 | 2087 | if (!sd) 2088 | { 2089 | sd = getSimpleDescriptor(node, endpoint); 2090 | } 2091 | 2092 | if (!sd) 2093 | { 2094 | return; 2095 | } 2096 | 2097 | { 2098 | auto i = sd->outClusters().cbegin(); 2099 | const auto end = sd->outClusters().cend(); 2100 | for (; i != end; ++i) 2101 | { 2102 | // okay has server image notify cluster 2103 | if (i->id() == OTAU_CLUSTER_ID) 2104 | { 2105 | // create node if not exist 2106 | bool create = true; 2107 | OtauNode *otauNode = m_model->getNode(node->address(), create); 2108 | 2109 | if (otauNode) 2110 | { 2111 | otauNode->rxOnWhenIdle = node->nodeDescriptor().receiverOnWhenIdle(); 2112 | otauNode->endpointNotify = sd->endpoint(); 2113 | } 2114 | 2115 | if (otauNode && otauNode->profileId != sd->profileId()) 2116 | { 2117 | uint16_t profileId = 0; 2118 | 2119 | if (sd->profileId() == ZLL_PROFILE_ID) 2120 | { 2121 | profileId = HA_PROFILE_ID; 2122 | } 2123 | else 2124 | { 2125 | profileId = sd->profileId(); 2126 | } 2127 | 2128 | if (profileId != otauNode->profileId) 2129 | { 2130 | DBG_Printf(DBG_OTA, "OTAU: set node profileId to 0x%04X\n", profileId); 2131 | otauNode->profileId = profileId; 2132 | } 2133 | } 2134 | 2135 | return; 2136 | } 2137 | } 2138 | } 2139 | } 2140 | 2141 | void StdOtauPlugin::activatedNodeAtRow(int row) 2142 | { 2143 | OtauNode *node = m_model->getNodeAtRow(row); 2144 | 2145 | if (node) 2146 | { 2147 | m_w->displayNode(node); 2148 | } 2149 | } 2150 | 2151 | #if QT_VERSION < 0x050000 2152 | Q_EXPORT_PLUGIN2(std_otau_plugin, StdOtauPlugin) 2153 | #endif 2154 | -------------------------------------------------------------------------------- /std_otau_plugin.h: -------------------------------------------------------------------------------- 1 | #ifndef STD_OTAU_PLUGIN_H 2 | #define STD_OTAU_PLUGIN_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "deconz.h" 8 | #include "otau_file.h" 9 | 10 | #define ONOFF_CLUSTER_ID 0x0006 11 | #define LEVEL_CLUSTER_ID 0x0008 12 | #define OTAU_CLUSTER_ID 0x0019 13 | #define DE_CLUSTER_ID 0xFC00 14 | 15 | #define OTAU_IMAGE_NOTIFY_CMD_ID 0x00 16 | #define OTAU_QUERY_NEXT_IMAGE_REQUEST_CMD_ID 0x01 17 | #define OTAU_QUERY_NEXT_IMAGE_RESPONSE_CMD_ID 0x02 18 | #define OTAU_IMAGE_BLOCK_REQUEST_CMD_ID 0x03 19 | #define OTAU_IMAGE_PAGE_REQUEST_CMD_ID 0x04 20 | #define OTAU_IMAGE_BLOCK_RESPONSE_CMD_ID 0x05 21 | #define OTAU_UPGRADE_END_REQUEST_CMD_ID 0x06 22 | #define OTAU_UPGRADE_END_RESPONSE_CMD_ID 0x07 23 | 24 | #define OTAU_MAX_ACTIVE 4 25 | 26 | /*! Otau ZCL status codes. */ 27 | typedef enum 28 | { 29 | OTAU_SUCCESS = 0x00, 30 | OTAU_ABORT = 0x95, 31 | OTAU_NOT_AUTHORIZED = 0x7E, 32 | OTAU_INVALID_IMAGE = 0x96, 33 | OTAU_WAIT_FOR_DATA = 0x97, 34 | OTAU_NO_IMAGE_AVAILABLE = 0x98, 35 | OTAU_MALFORMED_COMMAND = 0x80, 36 | OTAU_UNSUP_CLUSTER_COMMAND = 0x81, 37 | OTAU_REQUIRE_MORE_IMAGE = 0x99 38 | } OtauStatus_t; 39 | 40 | class QFileSystemWatcher; 41 | class QTimer; 42 | class StdOtauWidget; 43 | struct OtauNode; 44 | struct ImageNotifyReq; 45 | class OtauModel; 46 | 47 | struct OtauTracker 48 | { 49 | uint64_t extAddr; 50 | deCONZ::SteadyTimeRef lastActivity; 51 | }; 52 | 53 | class StdOtauPlugin : public QObject, 54 | public deCONZ::NodeInterface 55 | { 56 | Q_OBJECT 57 | Q_INTERFACES(deCONZ::NodeInterface) 58 | #if QT_VERSION >= 0x050000 59 | Q_PLUGIN_METADATA(IID "org.dresden-elektronik.StdOtauPlugin") 60 | #endif 61 | public: 62 | enum State 63 | { 64 | StateEnabled, 65 | StateDisabled, 66 | StateBusySensors 67 | }; 68 | 69 | explicit StdOtauPlugin(QObject *parent = 0); 70 | const char *name(); 71 | bool hasFeature(Features feature); 72 | QWidget *createWidget(); 73 | QDialog *createDialog(); 74 | State state() const { return m_state; } 75 | 76 | public Q_SLOTS: 77 | void apsdeDataIndication(const deCONZ::ApsDataIndication &ind); 78 | void apsdeDataConfirm(const deCONZ::ApsDataConfirm &conf); 79 | bool imageNotify(ImageNotifyReq *notf); 80 | void activatedNodeAtRow(int row); 81 | bool broadcastImageNotify(); 82 | bool unicastImageNotify(const deCONZ::Address &addr); 83 | void unicastUpgradeEndRequest(const deCONZ::Address &addr); 84 | void matchDescriptorRequest(const deCONZ::ApsDataIndication &ind); 85 | void queryNextImageRequest(const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame); 86 | bool queryNextImageResponse(OtauNode *node); 87 | void imageBlockRequest(const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame); 88 | bool imageBlockResponse(OtauNode *node); 89 | void imagePageRequest(const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame); 90 | bool imagePageResponse(OtauNode *node); 91 | void upgradeEndRequest(const deCONZ::ApsDataIndication &ind, const deCONZ::ZclFrame &zclFrame); 92 | bool upgradeEndResponse(OtauNode *node, uint32_t upgradeTime); 93 | bool defaultResponse(OtauNode *node, quint8 commandId, quint8 status); 94 | void nodeEvent(const deCONZ::NodeEvent &event); 95 | void nodeSelected(const deCONZ::Node *node); 96 | bool checkForUpdateImageImage(OtauNode *node, const QString &path); 97 | void invalidateUpdateEndRequest(OtauNode *node); 98 | void imagePageTimerFired(); 99 | void cleanupTimerFired(); 100 | void activityTimerFired(); 101 | void markOtauActivity(const deCONZ::Address &address); 102 | void checkFileLinks(); 103 | 104 | Q_SIGNALS: 105 | void stateChanged(int state); 106 | 107 | private: 108 | void setState(State state); 109 | void checkIfNewOtauNode(const deCONZ::Node *node, uint8_t endpoint); 110 | deCONZ::Address m_delayedImageNotifyAddr; 111 | QString m_imgPath; 112 | OtauModel *m_model; 113 | State m_state; 114 | quint8 m_srcEndpoint; 115 | StdOtauWidget *m_w; 116 | quint8 m_zclSeq; 117 | quint8 m_maxAsduDataSize; 118 | quint8 m_nNoAckErrors; 119 | QTimer *m_imagePageTimer; 120 | QTimer *m_cleanupTimer; 121 | QTimer *m_activityTimer; 122 | std::vector m_otauTracker; 123 | int m_fastPageSpaceing; 124 | }; 125 | 126 | #endif // STD_OTAU_PLUGIN_H 127 | -------------------------------------------------------------------------------- /std_otau_widget.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "std_otau_widget.h" 8 | #include "std_otau_plugin.h" 9 | #include "otau_file_loader.h" 10 | #include "otau_model.h" 11 | #include "otau_node.h" 12 | #include "ui_std_otau_widget.h" 13 | 14 | #define UNLIMITED_WATING_TIME 0xfffffffful 15 | 16 | #ifdef USE_ACTOR_MODEL 17 | 18 | #include 19 | 20 | // am_api is defined in std_otau_plugin.cpp 21 | // in future the widget might become its own plugin 22 | extern struct am_api_functions *am; 23 | #define AM_ACTOR_ID_OTA 9000 24 | #define AM_ACTOR_ID_OTA_UI 9001 25 | 26 | #define OTA_M_ID_QUERY_NEXT_IMAGE_NOTIFY AM_MESSAGE_ID_SPECIFIC_NOTIFY(0x0001) 27 | 28 | static struct am_actor am_actor_oui0; 29 | static struct am_message out_msg; 30 | static unsigned char out_data[4096]; 31 | 32 | static int OUI0_MessageCallback(struct am_message *msg) 33 | { 34 | if (msg->src == AM_ACTOR_ID_OTA) 35 | { 36 | if (msg->id == OTA_M_ID_QUERY_NEXT_IMAGE_NOTIFY) 37 | { 38 | DBG_Printf(DBG_OTA, "OTAU: received QUERY_NEXT_IMAGE_NOTIFY\n"); 39 | return AM_CB_STATUS_OK; 40 | } 41 | } 42 | 43 | return AM_CB_STATUS_UNSUPPORTED; 44 | } 45 | #endif // USE_ACTOR_MODEL 46 | 47 | StdOtauWidget::StdOtauWidget(QWidget *parent) : 48 | QWidget(parent), 49 | ui(new Ui::StdOtauWidget) 50 | { 51 | ui->setupUi(this); 52 | 53 | // OTAU update tab 54 | m_ouNode = nullptr; 55 | 56 | connect(ui->ou_queryButton, SIGNAL(clicked()), 57 | this, SLOT(queryClicked())); 58 | 59 | connect(ui->ou_abortButton, SIGNAL(clicked()), 60 | this, SLOT(abortClicked())); 61 | 62 | connect(ui->ou_updateButton, SIGNAL(clicked()), 63 | this, SLOT(updateClicked())); 64 | 65 | connect(ui->ou_fileSelectButton, SIGNAL(clicked()), 66 | this, SLOT(fileSelectClicked())); 67 | 68 | connect(ui->tableView, SIGNAL(clicked(QModelIndex)), 69 | this, SLOT(otauTableActivated(QModelIndex))); 70 | 71 | // OTAU file tab 72 | connect(ui->saveButton, SIGNAL(clicked()), 73 | this, SLOT(saveClicked())); 74 | 75 | connect(ui->saveAsButton, SIGNAL(clicked()), 76 | this, SLOT(saveAsClicked())); 77 | 78 | 79 | connect(ui->openButton, SIGNAL(clicked()), 80 | this, SLOT(openClicked())); 81 | 82 | ui->tableView->setSortingEnabled(true); 83 | ui->tableView->setStyleSheet("QTableView::item { border: 0px; padding-left: 2px; padding-right: 2px; padding-top: 0px; padding-bottom: 0px; }"); 84 | 85 | #ifdef USE_ACTOR_MODEL 86 | if (am) 87 | { 88 | AM_INIT_ACTOR(&am_actor_oui0, AM_ACTOR_ID_OTA_UI, OUI0_MessageCallback); 89 | am->register_actor(&am_actor_oui0); 90 | am->subscribe(AM_ACTOR_ID_OTA, AM_ACTOR_ID_OTA_UI); 91 | } 92 | #endif 93 | } 94 | 95 | StdOtauWidget::~StdOtauWidget() 96 | { 97 | delete ui; 98 | } 99 | 100 | void StdOtauWidget::setOtauModel(OtauModel *model) 101 | { 102 | if (!proxyModel) 103 | { 104 | proxyModel = new QSortFilterProxyModel(this); 105 | proxyModel->setDynamicSortFilter(true); 106 | } 107 | 108 | proxyModel->setSourceModel(model); 109 | ui->tableView->setModel(proxyModel); 110 | 111 | connect(model, &OtauModel::rowsInserted, [this, model](const QModelIndex &, int, int) { 112 | if (model->rowCount(QModelIndex()) == 1) 113 | { 114 | // adjust columns 115 | ui->tableView->resizeColumnToContents(OtauModel::SectionAddress); 116 | ui->tableView->resizeColumnToContents(OtauModel::SectionManufacturer); 117 | ui->tableView->resizeColumnToContents(OtauModel::SectionImageType); 118 | ui->tableView->resizeColumnToContents(OtauModel::SectionSoftwareVersion); 119 | ui->tableView->resizeColumnToContents(OtauModel::SectionProgress); 120 | ui->tableView->resizeColumnToContents(OtauModel::SectionDuration); 121 | } 122 | 123 | ui->tableView->isSortingEnabled(); 124 | ui->tableView->sortByColumn(0, Qt::AscendingOrder); 125 | 126 | }); 127 | } 128 | 129 | uint StdOtauWidget::restartTime() 130 | { 131 | if (ui->restartAfterUpgradeCheckbox->isChecked()) 132 | { 133 | return ui->restartAfterUpgradeSpinBox->value(); 134 | } 135 | return UNLIMITED_WATING_TIME; 136 | } 137 | 138 | void StdOtauWidget::queryClicked() 139 | { 140 | if (m_ouNode) 141 | { 142 | deCONZ::ApsController::instance()->setParameter(deCONZ::ParamOtauActive, 1); 143 | 144 | emit unicastImageNotify(m_ouNode->address()); 145 | } 146 | } 147 | 148 | void StdOtauWidget::abortClicked() 149 | { 150 | if (m_ouNode) 151 | { 152 | m_ouNode->setPermitUpdate(false); 153 | m_ouNode->setState(OtauNode::NodeAbort); 154 | } 155 | } 156 | 157 | void StdOtauWidget::updateClicked() 158 | { 159 | deCONZ::ApsController *apsCtrl = deCONZ::ApsController::instance(); 160 | if (!apsCtrl) 161 | { 162 | return; 163 | } 164 | 165 | // if otau is not activated yet do it here since thats what the user expects 166 | if (apsCtrl->getParameter(deCONZ::ParamOtauActive) == 0) 167 | { 168 | if (!apsCtrl->setParameter(deCONZ::ParamOtauActive, 1)) 169 | { 170 | DBG_Printf(DBG_OTA, "OTAU: failed to enable otau server\n"); 171 | } 172 | } 173 | 174 | if (m_ouNode) 175 | { 176 | m_ouNode->setState(OtauNode::NodeIdle); 177 | if (m_ouNode->hasData()) 178 | { 179 | m_ouNode->setPermitUpdate(true); 180 | emit unicastImageNotify(m_ouNode->address()); 181 | } 182 | } 183 | } 184 | 185 | void StdOtauWidget::fileSelectClicked() 186 | { 187 | if (m_ouNode) 188 | { 189 | QString dirpath; 190 | 191 | if (!m_path.isEmpty()) 192 | { 193 | QFileInfo fi(m_path); 194 | dirpath = fi.dir().absolutePath(); 195 | } 196 | 197 | if (dirpath.isEmpty()) 198 | { 199 | QString defaultImgPath = deCONZ::getStorageLocation(deCONZ::HomeLocation) + "/otau"; 200 | dirpath = deCONZ::appArgumentString("--otau-img-path", defaultImgPath); 201 | } 202 | 203 | QString path = QFileDialog::getOpenFileName(this, 204 | "Select a firmware file", 205 | dirpath, 206 | "Firmware (*.zigbee *.ota.signed *.ota *.fw2 *.sbl-ota)"); 207 | if (path.isEmpty()) 208 | { 209 | clearSettingsBox(); 210 | } 211 | else 212 | { 213 | OtauFileLoader ld; 214 | 215 | if (ld.readFile(path, m_ouNode->file)) 216 | { 217 | m_ouNode->setHasData(true); 218 | m_ouNode->lastActivity.restart(); 219 | updateSettingsBox(); 220 | } 221 | else 222 | { 223 | clearSettingsBox(); 224 | } 225 | } 226 | } 227 | } 228 | 229 | bool StdOtauWidget::acksEnabled() const 230 | { 231 | return ui->useAcksCheckBox->isChecked(); 232 | } 233 | 234 | bool StdOtauWidget::pageRequestEnabled() const 235 | { 236 | return ui->usePageRequestCheckBox->isChecked(); 237 | } 238 | 239 | int StdOtauWidget::packetSpacingMs() const 240 | { 241 | return ui->spacingSpinBox->value(); 242 | } 243 | 244 | void StdOtauWidget::setPacketSpacingMs(int spacing) 245 | { 246 | ui->spacingSpinBox->setValue(spacing); 247 | } 248 | 249 | void StdOtauWidget::stateChanged(int state) 250 | { 251 | if (state == StdOtauPlugin::StateDisabled) 252 | { 253 | ui->labelOtauState->setText(tr("OTAU disabled")); 254 | } 255 | else if (state == StdOtauPlugin::StateDisabled) 256 | { 257 | ui->labelOtauState->setText(tr("OTAU wait sensors idle")); 258 | } 259 | else 260 | { 261 | ui->labelOtauState->setText(tr("OTAU enabled")); 262 | } 263 | } 264 | 265 | void StdOtauWidget::clearSettingsBox() 266 | { 267 | ui->ou_fileEdit->setText(QString()); 268 | ui->ou_fileVersionEdit->setText("0x00000000"); 269 | ui->ou_fileVersionEdit->setToolTip(QString()); 270 | ui->ou_imageTypeEdit->setText("0x0000"); 271 | ui->ou_manufacturerEdit->setText("0x0000"); 272 | ui->ou_SizeEdit->setText("0x00000000"); 273 | } 274 | 275 | void StdOtauWidget::updateSettingsBox() 276 | { 277 | if (m_ouNode) 278 | { 279 | if (m_ouNode->hasData()) 280 | { 281 | const OtauFile &of = m_ouNode->file; 282 | 283 | ui->ou_fileEdit->setText(of.path); 284 | 285 | QString str = "0x" + QString("%1").arg(of.fileVersion, 8, 16, QLatin1Char('0')).toUpper(); 286 | ui->ou_fileVersionEdit->setText(str); 287 | 288 | QString version; 289 | if (of.fileVersion != 0) 290 | { 291 | version = QString("%1.%2 build %3").arg((of.fileVersion & 0xF0000000U) >> 28).arg((of.fileVersion & 0x0FF00000U) >> 20).arg((of.fileVersion & 0x000FFFFFU)); 292 | } 293 | ui->ou_fileVersionEdit->setToolTip(version); 294 | 295 | str = "0x" + QString("%1").arg(of.imageType, 4, 16, QLatin1Char('0')).toUpper(); 296 | ui->ou_imageTypeEdit->setText(str); 297 | 298 | str = "0x" + QString("%1").arg(of.manufacturerCode, 4, 16, QLatin1Char('0')).toUpper(); 299 | ui->ou_manufacturerEdit->setText(str); 300 | 301 | str = "0x" + QString("%1 (%2 kB)").arg(of.totalImageSize, 8, 16, QLatin1Char('0')).arg(of.totalImageSize / 1014).toUpper(); 302 | ui->ou_SizeEdit->setText(str); 303 | } 304 | else 305 | { 306 | clearSettingsBox(); 307 | } 308 | } 309 | } 310 | 311 | void StdOtauWidget::otauTableActivated(const QModelIndex &index) 312 | { 313 | if (index.isValid()) 314 | { 315 | emit activatedNodeAtRow(proxyModel->mapToSource(index).row()); 316 | } 317 | } 318 | 319 | void StdOtauWidget::saveClicked() 320 | { 321 | if (m_path.endsWith(".bin")) 322 | { 323 | m_path.replace(".bin", ".zigbee"); 324 | ui->fileNameLabel->setText(m_path); 325 | } 326 | else if (m_path.endsWith(".bin.GCF")) 327 | { 328 | m_path.replace(".bin.GCF", ".zigbee"); 329 | ui->fileNameLabel->setText(m_path); 330 | } 331 | else if (m_path.endsWith(".GCF")) 332 | { 333 | m_path.replace(".GCF", ".zigbee"); 334 | ui->fileNameLabel->setText(m_path); 335 | } 336 | 337 | m_editOf.fileVersion = ui->of_FileVersionEdit->text().toUInt(nullptr, 16); 338 | m_editOf.headerVersion = ui->of_headerVersionEdit->text().toUShort(nullptr, 16); 339 | m_editOf.imageType = ui->of_imageTypeEdit->text().toUShort(nullptr, 16); 340 | m_editOf.manufacturerCode = ui->of_manufacturerEdit->text().toUShort(nullptr, 16); 341 | m_editOf.zigBeeStackVersion = ui->of_zigbeeStackVersionEdit->text().toUShort(nullptr, 16); 342 | 343 | // TODO: description 344 | 345 | 346 | // preserve sub element "upgrade image" 347 | OtauFile::SubElement subImage; 348 | { 349 | auto it = m_editOf.subElements.begin(); 350 | auto end = m_editOf.subElements.end(); 351 | 352 | for (;it != end; ++it) 353 | { 354 | if (it->tag == TAG_UPGRADE_IMAGE) 355 | { 356 | subImage = *it; 357 | } 358 | } 359 | } 360 | // clean all elements 361 | m_editOf.subElements.clear(); 362 | // restore upgrade image as first sub element 363 | m_editOf.subElements.push_back(subImage); 364 | 365 | OtauFileLoader ld; 366 | if (!ld.saveFile(m_path, m_editOf)) 367 | { 368 | 369 | } 370 | } 371 | 372 | void StdOtauWidget::saveAsClicked() 373 | { 374 | } 375 | 376 | void StdOtauWidget::openClicked() 377 | { 378 | QString path; 379 | 380 | if (!m_path.isEmpty()) 381 | { 382 | QFileInfo fi(m_path); 383 | path = fi.dir().absolutePath(); 384 | } 385 | 386 | if (path.isEmpty()) 387 | { 388 | QString defaultImgPath = deCONZ::getStorageLocation(deCONZ::HomeLocation) + "/otau"; 389 | path = deCONZ::appArgumentString("--otau-img-path", defaultImgPath); 390 | } 391 | 392 | m_path = QFileDialog::getOpenFileName(this, 393 | tr("Select a firmware file"), 394 | path, 395 | "Firmware (*.GCF *.bin *.zigbee *.ota.signed *.ota *.OTA *.fw2 *.sbl-ota)"); 396 | 397 | if (!m_path.isEmpty()) 398 | { 399 | 400 | OtauFileLoader ld; 401 | 402 | if (ld.readFile(m_path, m_editOf)) 403 | { 404 | ui->fileNameLabel->setText(m_path); 405 | updateEditor(); 406 | } 407 | else 408 | { 409 | ui->fileNameLabel->setText(tr("Invalid file")); 410 | } 411 | } 412 | } 413 | 414 | void StdOtauWidget::displayNode(OtauNode *node) 415 | { 416 | m_ouNode = node; 417 | 418 | if (node) 419 | { 420 | updateSettingsBox(); 421 | 422 | if (node->lastQueryTime().isValid()) 423 | { 424 | ui->lastQueryLabel->setText(node->lastQueryTime().toString("hh:mm:ss")); 425 | } 426 | else 427 | { 428 | ui->lastQueryLabel->setText(tr("None")); 429 | } 430 | } 431 | else 432 | { 433 | ui->lastQueryLabel->setText(tr("None")); 434 | clearSettingsBox(); 435 | } 436 | } 437 | 438 | void StdOtauWidget::displayNode(OtauNode *node, const QModelIndex &index) 439 | { 440 | ui->tableView->selectRow(proxyModel->mapFromSource(index).row()); 441 | displayNode(node); 442 | } 443 | 444 | void StdOtauWidget::clearNode() 445 | { 446 | ui->tableView->clearSelection(); 447 | displayNode(nullptr); 448 | } 449 | 450 | void StdOtauWidget::updateEditor() 451 | { 452 | QString str; 453 | 454 | str = "0x" + QString("%1").arg(m_editOf.fileVersion, 8, 16, QLatin1Char('0')).toUpper(); 455 | ui->of_FileVersionEdit->setText(str); 456 | 457 | str = "0x" + QString("%1").arg(m_editOf.headerVersion, 4, 16, QLatin1Char('0')).toUpper(); 458 | ui->of_headerVersionEdit->setText(str); 459 | 460 | str = "0x" + QString("%1").arg(m_editOf.imageType, 4, 16, QLatin1Char('0')).toUpper(); 461 | ui->of_imageTypeEdit->setText(str); 462 | 463 | str = "0x" + QString("%1").arg(m_editOf.manufacturerCode, 4, 16, QLatin1Char('0')).toUpper(); 464 | ui->of_manufacturerEdit->setText(str); 465 | 466 | str = "0x" + QString("%1").arg(m_editOf.zigBeeStackVersion, 4, 16, QLatin1Char('0')).toUpper(); 467 | ui->of_zigbeeStackVersionEdit->setText(str); 468 | 469 | QString descr; 470 | for (size_t i = 0; i < sizeof(m_editOf.headerString); i++) 471 | { 472 | if (isprint(m_editOf.headerString[i])) 473 | { 474 | descr.append(static_cast(m_editOf.headerString[i])); 475 | } 476 | else 477 | { 478 | descr.append(' '); 479 | } 480 | } 481 | ui->of_descriptionEdit->setPlainText(descr); 482 | 483 | str = "0x" + QString("%1").arg(m_editOf.minHardwareVersion, 4, 16, QLatin1Char('0')).toUpper(); 484 | ui->of_minHwVersionEdit->setText(str); 485 | 486 | str = "0x" + QString("%1").arg(m_editOf.maxHardwareVersion, 4, 16, QLatin1Char('0')).toUpper(); 487 | ui->of_maxHwVersionEdit->setText(str); 488 | 489 | str = QString::number(m_editOf.totalImageSize); 490 | ui->of_firmwareSizeEdit->setText(str); 491 | } 492 | -------------------------------------------------------------------------------- /std_otau_widget.h: -------------------------------------------------------------------------------- 1 | #ifndef STD_OTAU_WIDGET_H 2 | #define STD_OTAU_WIDGET_H 3 | 4 | #include 5 | #include "deconz/types.h" 6 | #include "deconz/aps.h" 7 | #include "otau_file.h" 8 | 9 | namespace Ui { 10 | class StdOtauWidget; 11 | } 12 | 13 | class StdOtauPlugin; 14 | struct OtauFile; 15 | class OtauModel; 16 | struct OtauNode; 17 | class QModelIndex; 18 | class QSortFilterProxyModel; 19 | 20 | class StdOtauWidget : public QWidget 21 | { 22 | Q_OBJECT 23 | 24 | public: 25 | explicit StdOtauWidget(QWidget *parent); 26 | ~StdOtauWidget(); 27 | void setOtauModel(OtauModel *model); 28 | uint restartTime(); 29 | 30 | public Q_SLOTS: 31 | void stateChanged(int state); 32 | void clearSettingsBox(); 33 | void updateSettingsBox(); 34 | void otauTableActivated(const QModelIndex &index); 35 | 36 | // OTAU upgrade 37 | void queryClicked(); 38 | void abortClicked(); 39 | void updateClicked(); 40 | void fileSelectClicked(); 41 | 42 | bool acksEnabled() const; 43 | bool pageRequestEnabled() const; 44 | int packetSpacingMs() const; 45 | void setPacketSpacingMs(int spacing); 46 | 47 | // OTAU file tab 48 | void saveClicked(); 49 | void saveAsClicked(); 50 | void openClicked(); 51 | void displayNode(OtauNode *node); 52 | void displayNode(OtauNode *node, const QModelIndex &index); 53 | void clearNode(); 54 | 55 | Q_SIGNALS: 56 | void broadcastImageNotify(); 57 | void activatedNodeAtRow(int); 58 | void unicastImageNotify(deCONZ::Address); 59 | void unicastUpgradeEndRequest(deCONZ::Address); 60 | 61 | private: 62 | void updateEditor(); 63 | 64 | Ui::StdOtauWidget *ui = nullptr; 65 | QSortFilterProxyModel *proxyModel = nullptr; 66 | QString m_path; 67 | OtauFile m_editOf; 68 | OtauNode *m_ouNode = nullptr; 69 | }; 70 | 71 | #endif // STD_OTAU_WIDGET_H 72 | -------------------------------------------------------------------------------- /std_otau_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | StdOtauWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 682 10 | 561 11 | 12 | 13 | 14 | OTA Update 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | 0 36 | 37 | 38 | 39 | OTAU Update 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Otau File 51 | 52 | 53 | ou_fileEdit 54 | 55 | 56 | 57 | 58 | 59 | 60 | true 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 32 69 | 16777215 70 | 71 | 72 | 73 | <html><head/><body><p>Chose otau file for selected node.</p></body></html> 74 | 75 | 76 | ... 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | Manufacturer Id 88 | 89 | 90 | ou_manufacturerEdit 91 | 92 | 93 | 94 | 95 | 96 | 97 | \0\xHHHHHHHH 98 | 99 | 100 | 0x0000 101 | 102 | 103 | true 104 | 105 | 106 | 107 | 108 | 109 | 110 | Image Type 111 | 112 | 113 | ou_imageTypeEdit 114 | 115 | 116 | 117 | 118 | 119 | 120 | <html><head/><body><p>The otau client might use the image type as additional filter to accept or deny a otau image.</p></body></html> 121 | 122 | 123 | \0\xHHHHHHHH 124 | 125 | 126 | 0x0000 127 | 128 | 129 | true 130 | 131 | 132 | 133 | 134 | 135 | 136 | File Version 137 | 138 | 139 | ou_fileVersionEdit 140 | 141 | 142 | 143 | 144 | 145 | 146 | <html><head/><body><p><br/></p></body></html> 147 | 148 | 149 | \0\xHHHHHHHH 150 | 151 | 152 | 0x00000000 153 | 154 | 155 | true 156 | 157 | 158 | 159 | 160 | 161 | 162 | Size 163 | 164 | 165 | ou_SizeEdit 166 | 167 | 168 | 169 | 170 | 171 | 172 | <html><head/><body><p>Size of OTAU file including header.</p></body></html> 173 | 174 | 175 | 0x00000000 176 | 177 | 178 | true 179 | 180 | 181 | 182 | 183 | 184 | 185 | true 186 | 187 | 188 | Restart Device After Upgrade 189 | 190 | 191 | true 192 | 193 | 194 | 195 | 196 | 197 | 198 | Last Image Query 199 | 200 | 201 | 202 | 203 | 204 | 205 | 00:00:00 206 | 207 | 208 | 209 | 210 | 211 | 212 | true 213 | 214 | 215 | Use ACKs 216 | 217 | 218 | false 219 | 220 | 221 | 222 | 223 | 224 | 225 | Page Request 226 | 227 | 228 | true 229 | 230 | 231 | 232 | 233 | 234 | 235 | true 236 | 237 | 238 | s 239 | 240 | 241 | 3600 242 | 243 | 244 | 5 245 | 246 | 247 | 248 | 249 | 250 | 251 | ms 252 | 253 | 254 | 255 | 256 | 257 | 4 258 | 259 | 260 | 8000 261 | 262 | 263 | 100 264 | 265 | 266 | 267 | 268 | 269 | 270 | Packet Spacing 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | OTAU enabled 282 | 283 | 284 | 285 | 286 | 287 | 288 | Qt::Horizontal 289 | 290 | 291 | QSizePolicy::MinimumExpanding 292 | 293 | 294 | 295 | 40 296 | 20 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | true 305 | 306 | 307 | <html><head/><body><p>Requests the selected node to query for new update image.</p></body></html> 308 | 309 | 310 | Query 311 | 312 | 313 | 314 | 315 | 316 | 317 | <html><head/><body><p>Aborts update for selected node.</p></body></html> 318 | 319 | 320 | Abort 321 | 322 | 323 | 324 | 325 | 326 | 327 | <html><head/><body><p>Starts update for selected node. File must be chosen first.</p></body></html> 328 | 329 | 330 | Update 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | QAbstractItemView::SingleSelection 340 | 341 | 342 | QAbstractItemView::SelectRows 343 | 344 | 345 | true 346 | 347 | 348 | false 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | OTAU File 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | File Version 372 | 373 | 374 | 375 | 376 | 377 | 378 | <html><head/><body><p>Version field is a 32-bit value 0xXYBBPPRR</p><p>X - Major release</p><p>Y - Minor release</p><p>BB - Application build</p><p>PP - Platform</p><p>RR - Reserved (Set to 0)</p><p>Example: 0x14420100 Version 1.4 build 42 Platform 0x01</p><p><br/></p></body></html> 379 | 380 | 381 | 0x00000000 382 | 383 | 384 | 385 | 386 | 387 | 388 | Header Version 389 | 390 | 391 | 392 | 393 | 394 | 395 | 0x0100 396 | 397 | 398 | 399 | 400 | 401 | 402 | Image Type 403 | 404 | 405 | 406 | 407 | 408 | 409 | 0x0000 410 | 411 | 412 | 413 | 414 | 415 | 416 | Manufacturer Code 417 | 418 | 419 | 420 | 421 | 422 | 423 | 0x1135 424 | 425 | 426 | 427 | 428 | 429 | 430 | ZigBee Stack Version 431 | 432 | 433 | 434 | 435 | 436 | 437 | 0x0002 438 | 439 | 440 | 441 | 442 | 443 | 444 | true 445 | 446 | 447 | Description 448 | 449 | 450 | 451 | 452 | 453 | 454 | true 455 | 456 | 457 | 458 | 459 | 460 | 461 | Raw Firmware Size 462 | 463 | 464 | 465 | 466 | 467 | 468 | true 469 | 470 | 471 | <html><head/><body><p>Size of raw firmware (without OTAU file header).</p></body></html> 472 | 473 | 474 | 0x1FFFF 475 | 476 | 477 | true 478 | 479 | 480 | 481 | 482 | 483 | 484 | Min HW Version 485 | 486 | 487 | 488 | 489 | 490 | 491 | 0x0000 492 | 493 | 494 | 495 | 496 | 497 | 498 | Max HW Version 499 | 500 | 501 | 502 | 503 | 504 | 505 | 0x0000 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | Save 517 | 518 | 519 | 520 | 521 | 522 | 523 | false 524 | 525 | 526 | Save As 527 | 528 | 529 | 530 | 531 | 532 | 533 | <html><head/><body><p>Open a *.zigbee file</p></body></html> 534 | 535 | 536 | Open 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | restartAfterUpgradeCheckbox 552 | toggled(bool) 553 | restartAfterUpgradeSpinBox 554 | setEnabled(bool) 555 | 556 | 557 | 148 558 | 131 559 | 560 | 561 | 201 562 | 133 563 | 564 | 565 | 566 | 567 | 568 | --------------------------------------------------------------------------------