diff --git a/.gitignore b/.gitignore index 55a0b11b..98a503ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,19 @@ -*.o /.Build.stamp /.project +/.cproject +CMakeFiles/ +/CMakeCache.txt +cmake_install.cmake +/include/ezsp/config.h +/src/ezsp/Makefile +/src/spi/Makefile +/tests/Makefile +/example/Makefile +/src/spi/libezspspi.so +/src/ezsp/libezsp.so +/example/mainEzspTest +/tests/gptest +/version.h +*.o +*.a +/doc diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 00000000..7a93b07a --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,15 @@ +# Path to sources +#sonar.sources=. +#sonar.exclusions= +#sonar.inclusions= + +# Path to tests +#sonar.tests= +#sonar.test.exclusions= +#sonar.test.inclusions= + +# Source encoding +#sonar.sourceEncoding=UTF-8 + +# Exclusions for copy-paste detection +#sonar.cpd.exclusions= diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..83e8a612 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: cpp +compiler: gcc +dist: trusty + +addons: + sonarcloud: + organization: "legrandgroup" + +script: + - mkdir -p build + - cd build + - cmake ${CMAKE_OPTIONS} -DUSE_CPPTHREADS=ON -DUSE_AESCUSTOM=ON -DUSE_GCOV=y -DUSE_MOCKSERIAL=ON .. + - build-wrapper-linux-x86-64 --out-dir bw-output/ make + - ./tests/gptest + - ../gcov.sh + - cd .. + - sonar-scanner diff --git a/Build b/Build index 78afbf29..a861e101 100755 --- a/Build +++ b/Build @@ -2,5 +2,54 @@ source ../Config.sh -pp_do "$@" +pp_clean_local () { + pp_clean_default +} + +pp_build_local () { + + mkdir -p "build-for-target" + cd "build-for-target" + + CFLAGS=${FW_CFLAGS_3RDPARTY} \ + CXXFLAGS=${FW_CXXFLAGS_3RDPARTY} \ + LDFLAGS=${FW_LDFLAGS_3RDPARTY} \ + cmake \ + -DCMAKE_C_COMPILER="$(type -P "$CC")" \ + -DCMAKE_CXX_COMPILER="$(type -P "$CXX")" \ + -DCMAKE_AR="$(type -P "$AR")" \ + -DCMAKE_C_COMPILER_AR="$(type -P "$AR")" \ + -DCMAKE_CXX_COMPILER_AR="$(type -P "$AR")" \ + -DCMAKE_RANLIB="$(type -P "$RANLIB")" \ + -DCMAKE_C_COMPILER_RANLIB="$(type -P "$RANLIB")" \ + -DCMAKE_CXX_COMPILER_RANLIB="$(type -P "$RANLIB")" \ + -DCMAKE_LINKER="$(type -P "${CROSS}ld")" \ + -DCMAKE_NM="$(type -P "${CROSS}nm")" \ + -DCMAKE_OBJDUMP="$(type -P "${CROSS}objdump")" \ + -DCMAKE_OBJCOPY="$(type -P "${CROSS}objcopy")" \ + -DCMAKE_STRIP="$(type -P "${CROSS}strip")" \ + -DCMAKE_INSTALL_OLDINCLUDEDIR="/include" \ + -DCMAKE_PREFIX_PATH="$DESTDIR" \ + -DCMAKE_INSTALL_PREFIX=/ \ + -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON \ + -DCMAKE_C_FLAGS="${FW_CFLAGS_3RDPARTY}" \ + -DCMAKE_CXX_FLAGS="${FW_CXXFLAGS_3RDPARTY}" \ + -DCMAKE_LD_FLAGS="${FW_LDFLAGS_3RDPARTY}" \ + -DUSE_RARITAN=ON \ + -DUSE_AESCUSTOM=ON \ + .. + make + make install + cd "$OLDPWD" +} + +pp_moddeps_local () { + echo "${LIBC_COMPONENT}" +} + +pp_test_local () { + : +} + +pp_do "$@" diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..b99d53fc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.8) + +# set the project name and version +project(ezsp VERSION 1.0) + +configure_file(version.h.in version.h) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +set(CMAKE_CXX_OUTPUT_EXTENSION_REPLACE ON) + +add_definitions("-fvisibility=hidden") +add_definitions("-fvisibility-inlines-hidden") + +option(USE_GCOV "Use GCOV coverage profiler" OFF) +if(USE_GCOV) + add_definitions(-g -O0 --coverage -fprofile-arcs -ftest-coverage) + link_libraries(--coverage -lgcov -fprofile-arcs) +endif() + +#add_subdirectory(src) +add_subdirectory(src/ezsp) +add_subdirectory(src/spi) +add_subdirectory(example) +add_subdirectory(tests) diff --git a/Makefile b/Makefile deleted file mode 100644 index 864c8c71..00000000 --- a/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -include ../Config.mk - -SUBDIRS = src - - -include $(PP_BUILD_SYS_DIR)/Top.mk - diff --git a/README.md b/README.md index 8492bbae..c912c754 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # libezsp +[![Build Status](https://travis-ci.org/Legrandgroup/libezsp.svg?branch=release)](https://travis-ci.org/Legrandgroup/libezsp) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=legrandgroup_libezsp&metric=alert_status)](https://sonarcloud.io/dashboard?id=legrandgroup_libezsp) C++ library to send/receive wireless traffic to/from a UART transceiver using the EZSP protocol from Silicon Labs. The code for a sample demo program is located in `src/example/mainEzspTest.cpp` and `src/example/CAppDemo.cpp`, this code is designed to work seamlessly on Linux within or outside of the Raritan framework. @@ -9,12 +11,13 @@ The code for a sample demo program is located in `src/example/mainEzspTest.cpp` If you compile in the Raritan environment, you will just have to clone this code into a subfolder or the source code root, move into this folder, and issue the following command: ``` -./Build +./Build && cp ./build-for-target/example/mainEzspTest ../install_root/bin ``` -The newly generated binary will be located in `../install_root/bin/mainEzspTest` (so, on the qemu installation, it will directly be accessible in the path to be run from a terminal on the target): +By default, only the shared library is copied over to the target rootfs, this is why we also manually copy the example binary (mainEzspTest) to `../install_root/bin/mainEzspTest` +This binary will be directly be accessible on the target, and in the default search path, so that it can run from a terminal on the target, by issueing: ``` -mainEzspTest +mainEzspTest --help ``` ### Using libserialcpp and C++11 threads under Linux @@ -33,32 +36,34 @@ This should result in a binary shared library built as file `~/serial/libserialc Now, we have to compile libezsp pointing it to the libserialcpp library we have just generated (in the example below, we assume the sources for libezsp are located in directory `~/libezsp`). Issue the following commands in order to compile libezsp: ``` -cd ~/libezsp/src/example -LOCAL_LDFLAGS=-L$HOME/serial LOCAL_INC=-I$HOME/serial/include make +cd ~/libezsp +LDFLAGS=-L$HOME/serial cmake -DCMAKE_CXX_FLAGS=-isystem\ $HOME/serial/include/ -DUSE_RARITAN=OFF -DUSE_CPPTHREADS=ON -DUSE_SERIALCPP=ON -DUSE_AESCUSTOM=ON +make ``` -This will tell the compiler that libserialcpp.so can be found in `$HOME/serial` and headers are in `$HOME/serial/include` (this should be the default after the libserialcpp compilation steps above). +Additional environment variables tell the compiler that libserialcpp.so can be found in `$HOME/serial` and headers are in `$HOME/serial/include` (this should be the default after the libserialcpp compilation steps above). In order to run the sample code under Linux, issue the following command in a terminal: ``` -cd ~/libezsp/src/example -LD_LIBRARY_PATH=$HOME/serial ./mainEzspTest +cd ~/libezsp +LD_LIBRARY_PATH=$HOME/serial ./example/mainEzspTest -C 11 -c 26 -r '*' -s '0x01510004/0123456789abcdef0123456789abcdef' -d ``` +The example above will open Green Power commissionning mode for 11s, and use the Zigbee channel 26 (for both Zigbee and Green Power transmission and reception). + +The `-r` switch will flush any pre-existing known source ID & associated keys, and `-s` will manually add decoding support for a Green Power device with source ID 0x01510004 (and provides its associated 128-bit AES key). + ### Execution Note that the UART device for communication with the transceiver (eg: `/dev/ttyUSB0`) is hardcoded inside the code (file `src/example/mainEzspTest.cpp`) We then library is run, it will first try to communicate with the dongle over the serial link provided above. -Once this is done, and when launched for the first time, the library will instruct the dongle to first create a Zigbee network. -Each time the sample binary process is subsequently run, it will open its network for any device to join for a limited period of time. You can thus enter a sensor in the network at this moment by pressing the button on the sensor device. - -Once the network is created, the sample application provided will then automatically discover sensors and bind to temperature and humidity, then it will configure reporting to be sent to the dongle by the sensor (each time this configuration will be redone). - -Finally, when the sensor sends periodically updated values of temperature/humidty, the dongle will receive these, the library will handle this incoming traffic and values will displayed in real time on the output stream of the sample process. +Once this is done, and when launched for the first time, the library will instruct the dongle to first create a network on the specified channel. +Each time the sample binary process is subsequently run, it will listen to sensor reports on that channel. +When the sensor sends periodically updated values of temperature/humidty via Green Power radio frames, the dongle will receive these, the library will handle this incoming traffic and values will displayed in real time on the output stream of the sample binary. -In order to force the sensor to send a report, you can also press 4 times on the button. +In order to force the sensor to send a report, you can also press on the button. ## For developpers diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 00000000..55d21894 --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,11 @@ +option(USE_RARITAN "Use RARITAN environment" ON) +option(USE_CPPTHREADS "Use CPPTHREAD" OFF) + +if(USE_CPPTHREADS) +set(USE_RARITAN OFF) +endif() + +add_executable(mainEzspTest mainEzspTest.cpp) + +target_include_directories(mainEzspTest PRIVATE ${PROJECT_SOURCE_DIR}/include) +target_link_libraries(mainEzspTest PUBLIC ezsp ezspspi) diff --git a/example/mainEzspStateMachine.h b/example/mainEzspStateMachine.h new file mode 100644 index 00000000..62ec8ee6 --- /dev/null +++ b/example/mainEzspStateMachine.h @@ -0,0 +1,501 @@ +/** + * @file mainEzspStateMachine.h + * + * @brief State machine for mainEzspTest Sample code + */ + +#include +#include +#include +#include +#include + +#include "spi/TimerBuilder.h" +#include "spi/Logger.h" + +#include +#include + +namespace NSMAIN { + +/** + * @brief Convert an ASCII character representing one hexadecimal digit ([0-9a-fA-F]) to its value (0-15) + * + * @param[in] hDigit The input printable ASCII character + * @param[out] byte The value of the hexadecimal digit as a uint8_t nibble (0-15) + * @return true if the conversion succeeded +**/ +static bool hexDigitToNibble(const char hDigit, uint8_t& byte) { + if (hDigit>='a' && hDigit<='f') { + byte = static_cast(hDigit + 10 - 'a'); + } else if (hDigit>='A' && hDigit<='F') { + byte = static_cast(hDigit + 10 - 'A'); + } else if (hDigit>='0' && hDigit<='9') { + byte = static_cast(hDigit - '0'); + } else { + return false; + } + return true; +} + +enum MainState { + INIT_PENDING, + REMOVE_ALL_GPD, + REMOVE_SPECIFIC_GPD, + ADD_GPD, + COMMISSION_GPD, + SCAN_CHANNELS, + RUN, + FW_UPGRADE, +}; + +class MainStateMachine { +public: + /** + * Constructor + * + * @param timerBuilder An TimerBuilder used to generate ITimer objects + * @param libEzspHandle The CLibEzspMain instance to use to communicate with the EZSP adapter + * @param openGpCommissionning Do we open GP commissionning at EZSP adapter initialization? + * @param authorizeChannelRequestAnswerTimeout During how many second (after startup), we will anwser to a channel request + * @param openZigbeeCommissionning Do we open the Zigbee network at EZSP adapter initialization? + * @param gpRemoveAllDevices A flag to remove all GP devices from monitoring + * @param gpDevicesToAdd A list of GP devices to add to the previous monitoring + * @param gpDevicesToRemove A list of source IDs for GP devices to remove from previous monitoring + * @param switchToFirmwareUpgradeMode Do we immediately put the EZSP adapter into firmware upgrade mode + */ + MainStateMachine(NSSPI::TimerBuilder& timerBuilder, + NSEZSP::CEzsp& libEzspHandle, + bool openGpCommissionning=false, + uint8_t authorizeChannelRequestAnswerTimeout=0, + bool openZigbeeCommissionning=false, + bool gpRemoveAllDevices=false, + const std::vector& gpDevicesToAdd={}, + const std::vector& gpDevicesToRemove={}, + bool switchToFirmwareUpgradeMode=false): + initFailures(0), + timerBuilder(timerBuilder), + libEzsp(libEzspHandle), + openGpCommissionningAtStartup(openGpCommissionning), + channelRequestAnswerTimeoutAtStartup(authorizeChannelRequestAnswerTimeout), + openZigbeeCommissionningAtStartup(openZigbeeCommissionning), + removeAllGPDAtStartup(gpRemoveAllDevices), + gpdAddList(gpDevicesToAdd), + gpdRemoveList(gpDevicesToRemove), + channelRequestAnswerTimer(this->timerBuilder.create()), + currentState(MainState::INIT_PENDING), + startFirmwareUpgrade(switchToFirmwareUpgradeMode) { + /* If the EZSP adapter's application is corrupted, we will never boot the application + In such cases, if we don't specifically handle that scenarion we would catch an ASH timeout but the default libezsp behaviour is to run the application, which would hang. + Therefore, if we have been instructed to perform a firmware upgrade, and only in that case, we will ask libezsp to select the bootloader's firmware upgrade option directly as soon as we hit the ASH timeout + */ + if (this->startFirmwareUpgrade) { + this->libEzsp.forceFirmwareUpgradeOnInitTimeout(); + } + } + + MainStateMachine(const MainStateMachine&) = delete; /* No copy construction allowed */ + + MainStateMachine& operator=(MainStateMachine) = delete; /* No assignment allowed */ + + /** + * @brief Set internal state machine to run mode (waiting for asynchronous sensor reports) + */ + void ezspRun() { + clogI << "Preparation steps finished... switching to run state\n"; + this->currentState = MainState::RUN; + } + + /** + * @brief Upgrade the firmware in the EZSP adapter + */ + void ezspFirmwareUpgrade() { + clogI << "Switchover to bootloader for firmware upgrade\n"; + this->currentState = MainState::FW_UPGRADE; + libEzsp.setFirmwareUpgradeMode(); + } + + /** + * @brief Remove all recorded GPD from the EZSP adapter + */ + void clearAllGPDevices() { + clogI << "Applying remove all GPD action\n"; + this->currentState = MainState::REMOVE_ALL_GPD; + if (!libEzsp.clearAllGPDevices()) { + clogE << "Failed clearing GP device list\n"; + } + this->removeAllGPDAtStartup = false; + this->gpdRemoveList = std::vector(); + } + + /** + * @brief Remove a selected list of GPD from the EZSP adapter + * + * @param[in] gpdToRemove The list of source IDs of the GPD to remove + */ + void removeGPDevices(const std::vector& gpdToRemove) { + clogI << "Removing " << gpdToRemove.size() << " provided GPDs\n"; + this->currentState = MainState::REMOVE_SPECIFIC_GPD; + if (!libEzsp.removeGPDevices(gpdToRemove)) { + clogE << "Failed removing GPDs\n"; + } + } + + /** + * @brief Add a selected list of GPD from the EZSP adapter + * + * @param[in] gpdToAdd The list of source IDs of the GPD to add + */ + void addGPDevices(const std::vector& gpdToAdd) { + clogI << "Adding " << gpdToAdd.size() << " provided GPDs\n"; + this->currentState = MainState::ADD_GPD; + if (!libEzsp.addGPDevices(gpdToAdd)) { + clogE << "Failed adding GPDs\n"; + } + } + + /** + * @brief Scan channels, displaying a result of channel occupation, then switch to run state (wait for sensor reports) + */ + void scanChannelsThenRun() { + this->currentState = MainState::SCAN_CHANNELS; + + auto processEnergyScanResults = [this](std::map channelToEnergyScan) { + std::pair electedChannelRssi = {0xFF, 20}; + for (std::pair scannedChannel : channelToEnergyScan) { + int8_t rssi = scannedChannel.second; + if (rssi < electedChannelRssi.second) { + electedChannelRssi = scannedChannel; + } + } + clogI << "Selecting channel " << static_cast(electedChannelRssi.first) << " with rssi: " << static_cast(electedChannelRssi.second) << " dBm\n"; + //this->setChannel(electedChannelRssi.first); + /* No other startup operations required... move to run state */ + this->ezspRun(); + }; + + libEzsp.startEnergyScan(processEnergyScanResults); /* This will make the underlying CEzspMain object move away from READY state until scan is finished */ + /* Switching to run state will be performed once scanning is done, in the processEnergyScanResults() callback above */ + } + + /** + * @brief Callback invoked when the underlying EZSP library changes state + * + * @param i_state The new state of the EZSP library + */ + void ezspStateChangeCallback(NSEZSP::CLibEzspState i_state) { + clogI << "EZSP library change to state " << NSEZSP::CLibEzspPublic::getStateAsString(i_state) << "\n"; + if (i_state == NSEZSP::CLibEzspState::READY) { + clogI << "EZSP library is ready, entering main state machine with MainState " << static_cast(this->currentState) << "\n"; + if (this->currentState == MainState::INIT_PENDING && this->startFirmwareUpgrade) { + clogI << "Switching to firmware upgrade mode\n"; + this->ezspFirmwareUpgrade(); + } + if (this->currentState == MainState::INIT_PENDING && this->removeAllGPDAtStartup) { + this->clearAllGPDevices(); + } + else if ((this->currentState == MainState::INIT_PENDING) && (this->gpdRemoveList.size() > 0)) { /* If in REMOVE_ALL_GPD state, no need to remove specific GPs, we have already flushed all */ + std::vector copyGpdRemoveList; + std::swap(copyGpdRemoveList, this->gpdRemoveList); + this->removeGPDevices(copyGpdRemoveList); + } + else if ((this->currentState == MainState::INIT_PENDING || + this->currentState == MainState::REMOVE_ALL_GPD || + this->currentState == MainState::REMOVE_SPECIFIC_GPD) && + (this->gpdAddList.size() > 0)) { /* Once init is done or optional GPD remove has been done... */ + std::vector copyGpdAddList; + std::swap(copyGpdAddList, this->gpdAddList); + this->addGPDevices(copyGpdAddList); + } + else if ((this->currentState == MainState::INIT_PENDING || + this->currentState == MainState::REMOVE_ALL_GPD || + this->currentState == MainState::REMOVE_SPECIFIC_GPD || + this->currentState == MainState::ADD_GPD) && + (this->channelRequestAnswerTimeoutAtStartup != 0 || + this->openGpCommissionningAtStartup)) { /* Once init is done or optional GPD remove and addition has been done... */ + if (this->openGpCommissionningAtStartup) { + clogI << "Opening GP commisionning session\n"; + libEzsp.openCommissioningSession(); + } + if (this->channelRequestAnswerTimeoutAtStartup != 0) { + clogI << "Opening GP channel request window for " << std::dec << static_cast(this->channelRequestAnswerTimeoutAtStartup) << "s\n"; + libEzsp.setAnswerToGpfChannelRqstPolicy(true); + // start timer + this->channelRequestAnswerTimer->start(static_cast(this->channelRequestAnswerTimeoutAtStartup*1000), + [this](NSSPI::ITimer *timer) { + clogI << "Closing GP channel request window\n"; + this->libEzsp.setAnswerToGpfChannelRqstPolicy(false); + } + ); + } + + this->scanChannelsThenRun(); + } + // else if (this->openZigbeeCommissionningAtStartup) { + // // If requested to do so, open the zigbee network for a specific duration, so new devices can join + // zb_nwk.openNetwork(60); + + // // we retrieve network information and key and eui64 of dongle (can be done before) + // dongle.sendCommand(EZSP_GET_NETWORK_PARAMETERS); + // dongle.sendCommand(EZSP_GET_EUI64); + // dongle.sendCommand(EZSP_GET_KEY, { EMBER_CURRENT_NETWORK_KEY }); + + // // start discover of existing product inside network + // zb_nwk.startDiscoverProduct([&](EmberNodeType i_type, EmberEUI64 i_eui64, EmberNodeId i_id){ + // clogI << " Is it a new product "; + // clogI << "[type : "<< CEzspEnum::EmberNodeTypeToString(i_type) << "]"; + // clogI << "[eui64 :"; + // for(uint8_t loop=0; loop(this->currentState) << "\n"; + } + } + + /** + * @brief Extract one cluster report from the beginning payload buffer + * + * @note The buffer may contain trailing data, that won't be parsed. @p usedBytes can be used to find out what remains after the cluster report + * + * @param[in] payload The payload (byte buffer) out of which we will parse the leading bytes + * @param[out] usedBytes The number of bytes successfully dissected at the beginning of the buffer + * + * @return true If a cluster report could be parsed, in such case @p usedBytes will contain the number of bytes decoded to generate the cluster report + */ + static bool extractClusterReport( const NSSPI::ByteBuffer& payload, uint8_t& usedBytes ) + { + size_t payloadSize = payload.size(); + + if (payloadSize < 5) + { + clogE << "Attribute reporting frame is too short: " << payloadSize << " bytes\n"; + usedBytes = 0; + return false; + } + + /* Cluster IDs are defined in the ZCL specification (Zigbee Alliance document 07-5123-06) */ + uint16_t clusterId = NSEZSP::dble_u8_to_u16(payload.at(1), payload.at(0)); + /* Attribute IDs are also defined in the ZCL specs */ + uint16_t attributeId = NSEZSP::dble_u8_to_u16(payload.at(3), payload.at(2)); + uint8_t type = payload.at(4); + + switch (clusterId) + { + case 0x0000: /* Basic */ + if ((attributeId == 0x4000 /* SWBuildID */) && (type == NSEZSP::ZCL_CHAR_STRING_ATTRIBUTE_TYPE)) + { + if (payloadSize < 6) + { + clogE << "Firmware version string is too short: " << payloadSize << " bytes\n"; + } + else + { + uint8_t strLength = payload.at(5); + if (payloadSize < 6+strLength) + { + clogE << "String frame is too short: " << payloadSize << " bytes\n"; + usedBytes = 0; + return false; + } + std::string fwVersion(payload.begin()+6, payload.begin()+6+strLength); + std::cout << "Firmware version is \"" << fwVersion << "\"\n"; + usedBytes = 6 + strLength; + return true; + } + } + else + { + clogE << "Wrong type: 0x" << std::hex << std::setw(4) << std::setfill('0') << static_cast(type) << "\n"; + } + break; + case 0x000F: /* Binary input */ + if ((attributeId == 0x0055 /* PresentValue */) && (type == NSEZSP::ZCL_BOOLEAN_ATTRIBUTE_TYPE)) + { + if (payloadSize < 6) + { + clogE << "Binary input frame is too short: " << payloadSize << " bytes\n"; + } + else + { + uint8_t value = payload.at(5); + std::cout << "Door is " << (value?"closed":"open") << "\n"; + usedBytes = 6; + return true; + } + } + else + { + clogE << "Wrong type: 0x" << std::hex << std::setw(4) << std::setfill('0') << static_cast(type) << "\n"; + } + break; + case 0x0402: /* Temperature Measurement */ + if ((attributeId == 0x0000 /* MeasuredValue */) && (type == NSEZSP::ZCL_INT16S_ATTRIBUTE_TYPE)) + { + if (payloadSize < 7) + { + clogE << "Temperature frame is too short: " << payloadSize << " bytes\n"; + usedBytes = 0; + return false; + } + else + { + int16_t value = static_cast(NSEZSP::dble_u8_to_u16(payload.at(6), payload.at(5))); + std::cout << "Temperature: " << value/100 << "." << std::setw(2) << std::setfill('0') << value%100 << "°C\n"; + usedBytes = 7; + return true; + } + } + else + { + clogE << "Wrong type: 0x" << std::hex << std::setw(4) << std::setfill('0') << static_cast(type) << "\n"; + } + break; + case 0x0405: /* Humidity */ + if ((attributeId == 0x0000 /* MeasuredValue */) && (type == NSEZSP::ZCL_INT16U_ATTRIBUTE_TYPE)) + { + if (payloadSize < 7) + { + clogE << "Humidity frame is too short: " << payloadSize << " bytes\n"; + usedBytes = 0; + return false; + } + else + { + int16_t value = static_cast(NSEZSP::dble_u8_to_u16(payload.at(6), payload.at(5))); + std::cout << "Humidity: " << value/100 << "." << std::setw(2) << std::setfill('0') << value%100 << "%\n"; + usedBytes = 7; + return true; + } + } + else + { + clogE << "Wrong type: 0x" << std::hex << std::setw(4) << std::setfill('0') << static_cast(type) << "\n"; + } + break; + case 0x0001: /* Power Configuration */ + if ((attributeId == 0x0020 /* BatteryVoltage */) && (type == NSEZSP::ZCL_INT8U_ATTRIBUTE_TYPE)) + { + if (payloadSize < 6) + { + clogE << "Battery level frame is too short: " << payloadSize << " bytes\n"; + } + else + { + uint8_t value = static_cast(payload.at(5)); + std::cout << "Battery level: " << value/10 << "." << std::setw(1) << std::setfill('0') << value%10 << "V\n"; + usedBytes = 6; + return true; + } + } + else + { + clogE << "Wrong type: 0x" << std::hex << std::setw(4) << std::setfill('0') << static_cast(type) << "\n"; + } + break; + default: + clogE << "Unknown cluster ID: 0x" << std::hex << std::setw(4) << std::setfill('0') << clusterId << "\n"; + } + clogE << "Payload parsing error\n"; + usedBytes = 0; + return false; + } + + /** + * @brief Extract multiple concatenated cluster reports from the beginning payload buffer + * + * @param[in] payload The payload (byte buffer) we will parse + * + * @return true If the buffer only contains 0, 1 or more concatenated cluster report(s) that were succesfully parsed, false otherwise (also returned when there are undecoded trailing bytes) + */ + static bool extractMultiClusterReport( NSSPI::ByteBuffer payload ) + { + uint8_t usedBytes = 0; + bool validBuffer = true; + + while (payload.size()>0 && validBuffer) + { + validBuffer = extractClusterReport(payload, usedBytes); + if (validBuffer) + { + payload.erase(payload.begin(), payload.begin()+static_cast(usedBytes)); + } + } + return validBuffer; + } + + /** + * @brief Handler to be invoked when a new green power frame is received + * + * It will take the appropriate actions + * + * @warning Currently, this method only handles green power attribute single or multi report frames + * + * @param[in] i_gpf The frame received + */ + void onReceivedGPFrame(NSEZSP::CGpFrame &i_gpf) { + switch(i_gpf.getCommandId()) + { + case 0xa0: // Attribute reporting + { + uint8_t usedBytes; + if (!extractClusterReport(i_gpf.getPayload(), usedBytes)) + { + clogE << "Failed decoding attribute reporting payload: "; + for (auto i : i_gpf.getPayload()) + { + clogE << std::hex << std::setw(2) << std::setfill('0') << static_cast(i) << " "; + } + } + } + break; + + case 0xa2: // Multi-Cluster Reporting + { + if (!extractMultiClusterReport(i_gpf.getPayload())) + { + clogE << "Failed to fully decode multi-cluster reporting payload: "; + for (auto i : i_gpf.getPayload()) + { + clogE << std::hex << std::setw(2) << std::setfill('0') << static_cast(i) << " "; + } + } + } + break; + + default: + clogW << "Unknown command ID: 0x" << std::hex << std::setw(2) << std::setfill('0') << static_cast(i_gpf.getCommandId()) << "\n"; + break; + } + } + +private: + unsigned int initFailures; /*!< How many failed init cycles we have done so far */ + NSSPI::TimerBuilder &timerBuilder; /*!< A builder to create timer instances */ + NSEZSP::CEzsp& libEzsp; /*!< The CEzsp instance to use to communicate with the EZSP adapter */ + bool openGpCommissionningAtStartup; /*!< Do we open GP commissionning at EZSP adapter initialization? */ + uint8_t channelRequestAnswerTimeoutAtStartup; /*!< During how many second (after startup), we will anwser to a channel request */ + bool openZigbeeCommissionningAtStartup; /*!< Do we open the Zigbee network at EZSP adapter initialization? */ + bool removeAllGPDAtStartup; /*!< A flag to remove all GP devices from monitoring */ + std::vector gpdAddList; /*!< A list of GP devices to add to the previous monitoring */ + std::vector gpdRemoveList; /*!< A list of source IDs for GP devices to remove from previous monitoring */ + std::unique_ptr channelRequestAnswerTimer; /*!< A timer to temporarily allow channel request */ + MainState currentState; /*!< Our current state (for the internal state machine) */ + bool startFirmwareUpgrade; /*!< Do we immediately put the EZSP adapter into firmware upgrade mode at startup */ +}; + +} // namespace NSMAIN diff --git a/example/mainEzspTest.cpp b/example/mainEzspTest.cpp new file mode 100644 index 00000000..9947fd9c --- /dev/null +++ b/example/mainEzspTest.cpp @@ -0,0 +1,305 @@ +/** + * @file mainEzspTest.cpp + * + * @brief Sample code for driving an EZSP adapter for GP reports + */ + +#include + +#include "spi/TimerBuilder.h" +#include "spi/UartDriverBuilder.h" +#include "spi/Logger.h" +#include +#include +#include +#include +#include + +#include + +#ifdef USE_RARITAN +#include +#endif +#ifdef USE_CPPTHREADS +#include +#include +#include +#endif + +#include + +#include "mainEzspStateMachine.h" + +#ifdef USE_CPPTHREADS +static bool stop = false; //NOSONAR +static std::condition_variable cv; //NOSONAR +static std::mutex m; //NOSONAR +#endif + +static void writeUsage(const std::string& progname, FILE *f) { + ::fprintf(f, "\n"); + ::fprintf(f, "%s - sample test program for libezsp\n\n", progname.c_str()); + ::fprintf(f, "Usage: %s [-d] [-u serialport] [-w|[-c channel] [-Z] [-C time] [-G|[-r *|-r source_id [-r source_id2...]]"\ + "[-s source_id/key [-s source_id2/key...]]]\n", progname.c_str()); + ::fprintf(f, "Available switches:\n"); + ::fprintf(f, "-h (--help) : this help\n"); + ::fprintf(f, "-d (--debug) : enable debug logs\n"); + ::fprintf(f, "-b (--baudrate) : baudrate used to communicate over the serial port\n"); + ::fprintf(f, "-w (--firmware-upgrade) : put the adapter in firmware upgrade mode and return when done\n"); + ::fprintf(f, "-Z (--open-zigbee) : open the zigbee network at startup (for 60s)\n"); + ::fprintf(f, "-G (--open-gp-commissionning) : open the Green Power commissionning session at startup\n"); + ::fprintf(f, "-C (--authorize-ch-request-answer)