From 2841d8b374b2be5b0e875a401096f7d0c0dc3c39 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Thu, 22 Dec 2022 23:28:41 +0100 Subject: [PATCH 001/143] :hammer: (cmake): FirmwareKit - depend on os_version file This commit make FirmwareKit (and directory) depend on os_version config file This means that when the version is updated, cmake configure is run again and the os_version.h file is regenerated with the new version This should make development as build number could be added in the future as well --- Makefile | 6 +++--- libs/FirmwareKit/CMakeLists.txt | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9cfd764de7..96c5246cf5 100644 --- a/Makefile +++ b/Makefile @@ -148,12 +148,12 @@ config_tools_target: mkdir_tools_config config_cmake_build: mkdir_cmake_config @echo "" @echo "🏃 Running cmake configuration script for target $(TARGET_BOARD) 📝" - @cmake -S . -B $(TARGET_BUILD_DIR) -GNinja -DCMAKE_CONFIG_DIR="$(CMAKE_CONFIG_DIR)" -DTARGET_BOARD="$(TARGET_BOARD)" -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_LOG_DEBUG=$(ENABLE_LOG_DEBUG) -DENABLE_SYSTEM_STATS=$(ENABLE_SYSTEM_STATS) -DBUILD_TARGETS_TO_USE_WITH_BOOTLOADER=$(BUILD_TARGETS_TO_USE_WITH_BOOTLOADER) -DOS_VERSION=$(OS_VERSION) + @cmake -S . -B $(TARGET_BUILD_DIR) -GNinja -DCMAKE_CONFIG_DIR="$(CMAKE_CONFIG_DIR)" -DTARGET_BOARD="$(TARGET_BOARD)" -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_LOG_DEBUG=$(ENABLE_LOG_DEBUG) -DENABLE_SYSTEM_STATS=$(ENABLE_SYSTEM_STATS) -DBUILD_TARGETS_TO_USE_WITH_BOOTLOADER=$(BUILD_TARGETS_TO_USE_WITH_BOOTLOADER) config_tools_build: mkdir_tools_config @echo "" @echo "🏃 Running cmake configuration script for target $(TARGET_BOARD) 📝" - @cmake -S . -B $(CMAKE_TOOLS_BUILD_DIR) -GNinja -DCMAKE_CONFIG_DIR="$(CMAKE_TOOLS_CONFIG_DIR)" -DTARGET_BOARD="$(TARGET_BOARD)" -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_LOG_DEBUG=ON -DENABLE_SYSTEM_STATS=ON -DOS_VERSION=$(OS_VERSION) + @cmake -S . -B $(CMAKE_TOOLS_BUILD_DIR) -GNinja -DCMAKE_CONFIG_DIR="$(CMAKE_TOOLS_CONFIG_DIR)" -DTARGET_BOARD="$(TARGET_BOARD)" -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_LOG_DEBUG=ON -DENABLE_SYSTEM_STATS=ON # # MARK: - Tests targets @@ -224,7 +224,7 @@ run_unit_tests: config_unit_tests: mkdir_build_unit_tests @echo "" @echo "🏃 Running unit tests cmake configuration script 📝" - cmake -S ./tests/unit -B $(UNIT_TESTS_BUILD_DIR) -GNinja -DCMAKE_BUILD_TYPE=Debug -DCOVERAGE=$(COVERAGE) -DSANITIZERS=$(SANITIZERS) -DOS_VERSION=$(OS_VERSION) -DUT_LITE=$(UT_LITE) -DCI_UT_OPTIMIZATION_LEVEL=$(CI_UT_OPTIMIZATION_LEVEL) + cmake -S ./tests/unit -B $(UNIT_TESTS_BUILD_DIR) -GNinja -DCMAKE_BUILD_TYPE=Debug -DCOVERAGE=$(COVERAGE) -DSANITIZERS=$(SANITIZERS) -DUT_LITE=$(UT_LITE) -DCI_UT_OPTIMIZATION_LEVEL=$(CI_UT_OPTIMIZATION_LEVEL) @mkdir -p $(CMAKE_TOOLS_BUILD_DIR)/unit_tests @ln -sf $(UNIT_TESTS_BUILD_DIR)/compile_commands.json $(CMAKE_TOOLS_BUILD_DIR)/unit_tests/compile_commands.json diff --git a/libs/FirmwareKit/CMakeLists.txt b/libs/FirmwareKit/CMakeLists.txt index f0fc67faec..706817c4bb 100644 --- a/libs/FirmwareKit/CMakeLists.txt +++ b/libs/FirmwareKit/CMakeLists.txt @@ -4,6 +4,10 @@ add_library(FirmwareKit STATIC) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${ROOT_DIR}/config/os_version) + +file(STRINGS "${ROOT_DIR}/config/os_version" OS_VERSION) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/include/os_version.h.in" "${CMAKE_CURRENT_BINARY_DIR}/include/os_version.h") From 09b5c8115c81dec13f87484677e3a97025ab3e44 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 3 Jan 2023 16:48:25 +0100 Subject: [PATCH 002/143] :white_check_mark: (tests): FirmwareKit - set current version using OS_VERSION --- libs/FirmwareKit/tests/FirmwareKit_test.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libs/FirmwareKit/tests/FirmwareKit_test.cpp b/libs/FirmwareKit/tests/FirmwareKit_test.cpp index 0f26a8e73c..bb3c2b6db5 100644 --- a/libs/FirmwareKit/tests/FirmwareKit_test.cpp +++ b/libs/FirmwareKit/tests/FirmwareKit_test.cpp @@ -10,6 +10,7 @@ #include "gtest/gtest.h" #include "mocks/leka/FlashMemory.h" #include "os_version.h" +#include "semver/semver.hpp" using namespace leka; @@ -26,9 +27,11 @@ class FirmwareKitTest : public ::testing::Test // void SetUp() override {} // void TearDown() override {} - Version current_version = Version {1, 3, 0}; - - std::string current_version_str = OS_VERSION; + Version current_version = Version { + semver::version {OS_VERSION}.major, + semver::version {OS_VERSION}.minor, + semver::version {OS_VERSION}.patch, + }; mock::FlashMemory mock_flash {}; FirmwareKit::Config config = {.bin_path_format = "fs/usr/os/LekaOS-%i.%i.%i.bin"}; From 7f77528534f28aa5889338fb24760c96d5ab46d2 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 3 Jan 2023 17:10:26 +0100 Subject: [PATCH 003/143] :test_tube: (tests): on device - add firmwarekit version tests --- tests/functional/CMakeLists.txt | 1 + .../tests/firmware_kit/CMakeLists.txt | 16 +++++++++ .../tests/firmware_kit/suite_firmware_kit.cpp | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 tests/functional/tests/firmware_kit/CMakeLists.txt create mode 100644 tests/functional/tests/firmware_kit/suite_firmware_kit.cpp diff --git a/tests/functional/CMakeLists.txt b/tests/functional/CMakeLists.txt index 68699b0037..68b741d31e 100644 --- a/tests/functional/CMakeLists.txt +++ b/tests/functional/CMakeLists.txt @@ -46,6 +46,7 @@ add_subdirectory(${TESTS_FUNCTIONAL_TESTS_DIR}/deep_sleep_core_pwm) add_subdirectory(${TESTS_FUNCTIONAL_TESTS_DIR}/deep_sleep_log_kit) add_subdirectory(${TESTS_FUNCTIONAL_TESTS_DIR}/deep_sleep_mbed_hal) add_subdirectory(${TESTS_FUNCTIONAL_TESTS_DIR}/file_manager) +add_subdirectory(${TESTS_FUNCTIONAL_TESTS_DIR}/firmware_kit) add_subdirectory(${TESTS_FUNCTIONAL_TESTS_DIR}/imu_kit) add_subdirectory(${TESTS_FUNCTIONAL_TESTS_DIR}/io_expander) add_subdirectory(${TESTS_FUNCTIONAL_TESTS_DIR}/qdac) diff --git a/tests/functional/tests/firmware_kit/CMakeLists.txt b/tests/functional/tests/firmware_kit/CMakeLists.txt new file mode 100644 index 0000000000..b20380069e --- /dev/null +++ b/tests/functional/tests/firmware_kit/CMakeLists.txt @@ -0,0 +1,16 @@ +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +register_functional_test( + TARGET + functional_ut_firmware_kit + + INCLUDE_DIRECTORIES + + SOURCES + suite_firmware_kit.cpp + + LINK_LIBRARIES + FirmwareKit +) diff --git a/tests/functional/tests/firmware_kit/suite_firmware_kit.cpp b/tests/functional/tests/firmware_kit/suite_firmware_kit.cpp new file mode 100644 index 0000000000..1263e0cf77 --- /dev/null +++ b/tests/functional/tests/firmware_kit/suite_firmware_kit.cpp @@ -0,0 +1,36 @@ +// Leka - LekaOS +// Copyright 2022 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "CoreFlashIS25LP016D.h" +#include "CoreFlashManagerIS25LP016D.h" +#include "CoreQSPI.h" +#include "FirmwareKit.h" +#include "os_version.h" +#include "semver/semver.hpp" +#include "tests/config.h" +#include "tests/utils.h" +#include "tests/utils_sleep.h" + +using namespace leka; +using namespace boost::ut; +using namespace std::chrono; + +auto qspi = CoreQSPI(); +auto flash_manager = CoreFlashManagerIS25LP016D(qspi); +auto flash_memory = CoreFlashIS25LP016D(qspi, flash_manager); + +suite suite_firmware_kit = [] { + "config version and firmware kit version are the same"_test = [&] { + auto firmwarekit = FirmwareKit(flash_memory, FirmwareKit::DEFAULT_CONFIG); + auto firmwarekit_version = firmwarekit.getCurrentVersion(); + + auto semver_version = semver::version {OS_VERSION}; + + expect(eq(firmwarekit_version.major, semver_version.major)); + expect(eq(firmwarekit_version.minor, semver_version.minor)); + expect(eq(firmwarekit_version.revision, semver_version.patch)); + }; +}; From 559973312b14b3f584fb80a5bbf9145d680606d8 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 16 Dec 2022 11:52:41 +0100 Subject: [PATCH 004/143] :children_crossing: (ble): Add isConnected --- libs/BLEKit/include/BLEKit.h | 1 + libs/BLEKit/include/CoreGap.h | 1 + libs/BLEKit/include/CoreGapEventHandler.h | 3 ++ libs/BLEKit/source/BLEKit.cpp | 5 +++ libs/BLEKit/source/CoreGap.cpp | 5 +++ libs/BLEKit/source/CoreGapEventHandler.cpp | 7 ++++ libs/BLEKit/tests/BLEKit_test.cpp | 7 ++++ .../BLEKit/tests/CoreGapEventHandler_test.cpp | 32 +++++++++++++++++++ libs/BLEKit/tests/CoreGap_test.cpp | 7 ++++ spikes/lk_ble/main.cpp | 2 ++ 10 files changed, 70 insertions(+) diff --git a/libs/BLEKit/include/BLEKit.h b/libs/BLEKit/include/BLEKit.h index c400dd0772..beace58dd3 100644 --- a/libs/BLEKit/include/BLEKit.h +++ b/libs/BLEKit/include/BLEKit.h @@ -30,6 +30,7 @@ class BLEKit void onConnectionCallback(const std::function &callback); void onDisconnectionCallback(const std::function &callback); + [[nodiscard]] auto isConnected() const -> bool; private: // ? mbed::BLE specific function diff --git a/libs/BLEKit/include/CoreGap.h b/libs/BLEKit/include/CoreGap.h index b6a3e01535..5b0f0896e1 100644 --- a/libs/BLEKit/include/CoreGap.h +++ b/libs/BLEKit/include/CoreGap.h @@ -30,6 +30,7 @@ class CoreGap void onConnectionCallback(const std::function &callback); void onDisconnectionCallback(const std::function &callback); + [[nodiscard]] auto isConnected() const -> bool; private: ble::advertising_handle_t _advertising_handle {ble::LEGACY_ADVERTISING_HANDLE}; diff --git a/libs/BLEKit/include/CoreGapEventHandler.h b/libs/BLEKit/include/CoreGapEventHandler.h index 4786c293c5..a4700dc46b 100644 --- a/libs/BLEKit/include/CoreGapEventHandler.h +++ b/libs/BLEKit/include/CoreGapEventHandler.h @@ -26,8 +26,11 @@ class CoreGapEventHandler : public ble::Gap::EventHandler void onConnectionCallback(const std::function &callback); void onDisconnectionCallback(const std::function &callback); + [[nodiscard]] auto isConnected() const -> bool; private: + bool is_connected = false; + std::function _start_advertising {}; std::function _on_connection_callback {}; diff --git a/libs/BLEKit/source/BLEKit.cpp b/libs/BLEKit/source/BLEKit.cpp index beb5bf158e..131c0e0933 100644 --- a/libs/BLEKit/source/BLEKit.cpp +++ b/libs/BLEKit/source/BLEKit.cpp @@ -55,3 +55,8 @@ void BLEKit::onDisconnectionCallback(const std::function &callback) { _core_gap.onDisconnectionCallback(callback); } + +auto BLEKit::isConnected() const -> bool +{ + return _core_gap.isConnected(); +} diff --git a/libs/BLEKit/source/CoreGap.cpp b/libs/BLEKit/source/CoreGap.cpp index f19c5fb60c..14680fd219 100644 --- a/libs/BLEKit/source/CoreGap.cpp +++ b/libs/BLEKit/source/CoreGap.cpp @@ -67,3 +67,8 @@ void CoreGap::onDisconnectionCallback(const std::function &callback) { _gap_event_handler.onDisconnectionCallback(callback); } + +auto CoreGap::isConnected() const -> bool +{ + return _gap_event_handler.isConnected(); +} diff --git a/libs/BLEKit/source/CoreGapEventHandler.cpp b/libs/BLEKit/source/CoreGapEventHandler.cpp index 472b7b1a59..5b172a1155 100644 --- a/libs/BLEKit/source/CoreGapEventHandler.cpp +++ b/libs/BLEKit/source/CoreGapEventHandler.cpp @@ -30,6 +30,7 @@ void CoreGapEventHandler::onConnectionComplete(ConnectionCompleteEvent const &ev if (_on_connection_callback != nullptr) { _on_connection_callback(); } + is_connected = true; } void CoreGapEventHandler::onDisconnectionComplete(DisconnectionCompleteEvent const &event) @@ -39,6 +40,7 @@ void CoreGapEventHandler::onDisconnectionComplete(DisconnectionCompleteEvent con if (_on_disconnection_callback != nullptr) { _on_disconnection_callback(); } + is_connected = false; } void CoreGapEventHandler::onAdvertisingEnd(AdvertisingEndEvent const &event) @@ -55,3 +57,8 @@ void CoreGapEventHandler::onDisconnectionCallback(const std::function &c { _on_disconnection_callback = callback; } + +auto CoreGapEventHandler::isConnected() const -> bool +{ + return is_connected; +} diff --git a/libs/BLEKit/tests/BLEKit_test.cpp b/libs/BLEKit/tests/BLEKit_test.cpp index d95478c883..35f59b2036 100644 --- a/libs/BLEKit/tests/BLEKit_test.cpp +++ b/libs/BLEKit/tests/BLEKit_test.cpp @@ -136,3 +136,10 @@ TEST_F(BLEKitTest, onDisconnectionCallback) // nothing expected } + +TEST_F(BLEKitTest, isConnectedDefault) +{ + auto is_connected = ble.isConnected(); + + EXPECT_FALSE(is_connected); +} diff --git a/libs/BLEKit/tests/CoreGapEventHandler_test.cpp b/libs/BLEKit/tests/CoreGapEventHandler_test.cpp index 6ef01aa445..627a6a356b 100644 --- a/libs/BLEKit/tests/CoreGapEventHandler_test.cpp +++ b/libs/BLEKit/tests/CoreGapEventHandler_test.cpp @@ -134,3 +134,35 @@ TEST_F(CoreGapEventHandlerTest, onDisconnectionCallback) core_gap_event_handler.onDisconnectionComplete(disconnection_complete_event); } + +TEST_F(CoreGapEventHandlerTest, isConnected) +{ + auto is_connected = core_gap_event_handler.isConnected(); + EXPECT_FALSE(is_connected); + + auto connection_complete_event = + ConnectionCompleteEvent(BLE_ERROR_BUFFER_OVERFLOW, INVALID_ADVERTISING_HANDLE, connection_role_t::CENTRAL, + peer_address_type_t::ANONYMOUS, ble::address_t(), ble::address_t(), ble::address_t(), + ble::conn_interval_t::max(), 0, ble::supervision_timeout_t::max(), 0); + + EXPECT_CALL(mock_start_advertising_func, Call).Times(1); + + core_gap_event_handler.onConnectionComplete(connection_complete_event); + + is_connected = core_gap_event_handler.isConnected(); + EXPECT_TRUE(is_connected); + + // + + auto handler = uintptr_t {}; + auto reason = disconnection_reason_t::AUTHENTICATION_FAILURE; + + auto disconnection_complete_event = DisconnectionCompleteEvent(handler, reason); + + EXPECT_CALL(mock_start_advertising_func, Call).Times(1); + + core_gap_event_handler.onDisconnectionComplete(disconnection_complete_event); + + is_connected = core_gap_event_handler.isConnected(); + EXPECT_FALSE(is_connected); +} diff --git a/libs/BLEKit/tests/CoreGap_test.cpp b/libs/BLEKit/tests/CoreGap_test.cpp index a068e6bce2..c0744a1653 100644 --- a/libs/BLEKit/tests/CoreGap_test.cpp +++ b/libs/BLEKit/tests/CoreGap_test.cpp @@ -181,3 +181,10 @@ TEST_F(CoreGapTest, onDisconnectionCallback) // nothing expected } + +TEST_F(CoreGapTest, isConnectedDefault) +{ + auto is_connected = coregap.isConnected(); + + EXPECT_FALSE(is_connected); +} diff --git a/spikes/lk_ble/main.cpp b/spikes/lk_ble/main.cpp index 865f85356c..0f09b76f96 100644 --- a/spikes/lk_ble/main.cpp +++ b/spikes/lk_ble/main.cpp @@ -85,6 +85,8 @@ auto main() -> int log_info("Main thread running..."); rtos::ThisThread::sleep_for(5s); + log_info("Is connected: %d", blekit.isConnected()); + service_battery.setBatteryLevel(level); ++level; From d93c92c0bca8175efff361e4b489de3eb19eafe3 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 2 Jan 2023 14:18:32 +0100 Subject: [PATCH 005/143] :fire: (rc): Remove redundant timeout registration --- libs/RobotKit/include/RobotController.h | 3 --- libs/RobotKit/tests/RobotController_test.h | 2 -- .../RobotKit/tests/RobotController_test_registerEvents.cpp | 4 ---- libs/RobotKit/tests/RobotController_test_stateWorking.cpp | 7 +++++++ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index 8f50ed81e0..ae591b257e 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -446,9 +446,6 @@ class RobotController : public interface::RobotController // Setup callbacks for each State Machine events - auto on_idle_timeout = [this]() { raise(event::idle_timeout_did_end {}); }; - _timeout.onTimeout(on_idle_timeout); - auto on_charge_did_start = [this]() { raise(event::charge_did_start {}); }; _battery.onChargeDidStart(on_charge_did_start); diff --git a/libs/RobotKit/tests/RobotController_test.h b/libs/RobotKit/tests/RobotController_test.h index 858efc3452..f7ec955c3d 100644 --- a/libs/RobotKit/tests/RobotController_test.h +++ b/libs/RobotKit/tests/RobotController_test.h @@ -201,8 +201,6 @@ class RobotControllerTest : public testing::Test EXPECT_CALL(mbed_mock_gap, setAdvertisingPayload).InSequence(on_data_updated_sequence); EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(2).InSequence(on_data_updated_sequence); - EXPECT_CALL(timeout, onTimeout).WillOnce(GetCallback(&on_idle_timeout)); - EXPECT_CALL(battery, onChargeDidStart).WillOnce(GetCallback>(&on_charge_did_start)); EXPECT_CALL(battery, onChargeDidStop).WillOnce(GetCallback>(&on_charge_did_stop)); diff --git a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp index 3db02fd6b2..0529a7461d 100644 --- a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp +++ b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp @@ -27,8 +27,6 @@ TEST_F(RobotControllerTest, registerEventsBatteryIsNotCharging) // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(2).InSequence(on_data_updated_sequence); - EXPECT_CALL(timeout, onTimeout); - EXPECT_CALL(battery, onChargeDidStart); EXPECT_CALL(battery, onChargeDidStop); @@ -79,8 +77,6 @@ TEST_F(RobotControllerTest, registerEventsBatteryIsCharging) EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(2).InSequence(on_data_updated_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_data_updated_sequence); - EXPECT_CALL(timeout, onTimeout); - EXPECT_CALL(battery, onChargeDidStart); EXPECT_CALL(battery, onChargeDidStop); diff --git a/libs/RobotKit/tests/RobotController_test_stateWorking.cpp b/libs/RobotKit/tests/RobotController_test_stateWorking.cpp index d44b074165..98f58f3780 100644 --- a/libs/RobotKit/tests/RobotController_test_stateWorking.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateWorking.cpp @@ -6,6 +6,13 @@ TEST_F(RobotControllerTest, stateWorkingEventTimeout) { + Sequence get_on_idle_timeout_callback; + EXPECT_CALL(timeout, onTimeout) + .InSequence(get_on_idle_timeout_callback) + .WillOnce(GetCallback(&on_idle_timeout)); + EXPECT_CALL(timeout, start).InSequence(get_on_idle_timeout_callback); + rc.startIdleTimeout(); + rc.state_machine.set_current_states(lksm::state::working); Sequence on_exit_working_sequence; From 583294e7dd9e4a6a4342d50531fb8098c01d2fa5 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 2 Jan 2023 15:04:12 +0100 Subject: [PATCH 006/143] :recycle: (rc): Separate timeout: inner state and state transition --- app/os/main.cpp | 6 ++- libs/RobotKit/include/RobotController.h | 34 ++++++++------- libs/RobotKit/tests/RobotController_test.h | 31 +++++++++---- .../RobotController_test_registerEvents.cpp | 8 ++-- ...troller_test_stateAutonomousActivities.cpp | 20 ++++----- .../RobotController_test_stateCharging.cpp | 43 ++++++++++--------- ...tController_test_stateEmergencyStopped.cpp | 24 +++++------ ...RobotController_test_stateFileExchange.cpp | 16 +++---- .../tests/RobotController_test_stateIdle.cpp | 28 ++++++------ .../RobotController_test_stateSleeping.cpp | 22 +++++----- .../RobotController_test_stateWorking.cpp | 20 ++++----- 11 files changed, 137 insertions(+), 115 deletions(-) diff --git a/app/os/main.cpp b/app/os/main.cpp index d773d33e4d..96e7768a03 100644 --- a/app/os/main.cpp +++ b/app/os/main.cpp @@ -408,7 +408,8 @@ namespace robot { namespace internal { - auto sleep_timeout = CoreTimeout {}; + auto timeout_state_internal = CoreTimeout {}; + auto timeout_state_transition = CoreTimeout {}; auto mcu = CoreMCU {}; auto serialnumberkit = SerialNumberKit {mcu, SerialNumberKit::DEFAULT_CONFIG}; @@ -416,7 +417,8 @@ namespace robot { } // namespace internal auto controller = RobotController { - internal::sleep_timeout, + internal::timeout_state_internal, + internal::timeout_state_transition, battery::cells, internal::serialnumberkit, firmware::kit, diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index ae591b257e..b289d63434 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -44,12 +44,14 @@ class RobotController : public interface::RobotController public: sm_t state_machine {static_cast(*this), logger}; - explicit RobotController(interface::Timeout &timeout, interface::Battery &battery, SerialNumberKit &serialnumberkit, + explicit RobotController(interface::Timeout &timeout_state_internal, interface::Timeout &timeout_state_transition, + interface::Battery &battery, SerialNumberKit &serialnumberkit, interface::FirmwareUpdate &firmware_update, interface::Motor &motor_left, interface::Motor &motor_right, interface::LED &ears, interface::LED &belt, interface::LedKit &ledkit, interface::LCD &lcd, interface::VideoKit &videokit, BehaviorKit &behaviorkit, CommandKit &cmdkit, RFIDKit &rfidkit, ActivityKit &activitykit) - : _timeout(timeout), + : _timeout_state_internal(timeout_state_internal), + _timeout_state_transition(timeout_state_transition), _battery(battery), _serialnumberkit(serialnumberkit), _firmware_update(firmware_update), @@ -81,23 +83,23 @@ class RobotController : public interface::RobotController { using namespace system::robot::sm; auto on_sleep_timeout = [this] { raise(event::sleep_timeout_did_end {}); }; - _timeout.onTimeout(on_sleep_timeout); + _timeout_state_transition.onTimeout(on_sleep_timeout); - _timeout.start(_sleep_timeout_duration); + _timeout_state_transition.start(_sleep_timeout_duration); } - void stopSleepTimeout() final { _timeout.stop(); } + void stopSleepTimeout() final { _timeout_state_transition.stop(); } void startIdleTimeout() final { using namespace system::robot::sm; auto on_idle_timeout = [this] { raise(event::idle_timeout_did_end {}); }; - _timeout.onTimeout(on_idle_timeout); + _timeout_state_transition.onTimeout(on_idle_timeout); - _timeout.start(_idle_timeout_duration); + _timeout_state_transition.start(_idle_timeout_duration); } - void stopIdleTimeout() final { _timeout.stop(); } + void stopIdleTimeout() final { _timeout_state_transition.stop(); } void startWaitingBehavior() final { @@ -125,14 +127,14 @@ class RobotController : public interface::RobotController _event_queue.call(&_lcd, &interface::LCD::turnOff); _event_queue.call(&_ledkit, &interface::LedKit::stop); }; - _timeout.onTimeout(on_sleeping_start_timeout); + _timeout_state_internal.onTimeout(on_sleeping_start_timeout); - _timeout.start(20s); + _timeout_state_internal.start(20s); } void stopSleepingBehavior() final { - _timeout.stop(); + _timeout_state_internal.stop(); _behaviorkit.stop(); } @@ -171,14 +173,14 @@ class RobotController : public interface::RobotController _lcd.turnOn(); auto on_charging_start_timeout = [this] { _event_queue.call(&_lcd, &interface::LCD::turnOff); }; - _timeout.onTimeout(on_charging_start_timeout); + _timeout_state_internal.onTimeout(on_charging_start_timeout); - _timeout.start(1min); + _timeout_state_internal.start(1min); } void stopChargingBehavior() final { - _timeout.stop(); + _timeout_state_internal.stop(); _behaviorkit.stop(); } @@ -495,9 +497,11 @@ class RobotController : public interface::RobotController private: system::robot::sm::logger logger {}; + interface::Timeout &_timeout_state_internal; + std::chrono::seconds _sleep_timeout_duration {60}; std::chrono::seconds _idle_timeout_duration {600}; - interface::Timeout &_timeout; + interface::Timeout &_timeout_state_transition; const rtos::Kernel::Clock::time_point kSystemStartupTimestamp = rtos::Kernel::Clock::now(); diff --git a/libs/RobotKit/tests/RobotController_test.h b/libs/RobotKit/tests/RobotController_test.h index f7ec955c3d..c609933baf 100644 --- a/libs/RobotKit/tests/RobotController_test.h +++ b/libs/RobotKit/tests/RobotController_test.h @@ -76,7 +76,8 @@ class RobotControllerTest : public testing::Test mock::EventQueue event_queue {}; - mock::Timeout timeout {}; + mock::Timeout timeout_state_internal {}; + mock::Timeout timeout_state_transition {}; mock::Battery battery {}; mock::MCU mock_mcu {}; @@ -109,10 +110,22 @@ class RobotControllerTest : public testing::Test stub::EventLoopKit event_loop {}; CommandKit cmdkit {event_loop}; - RobotController> rc { - timeout, battery, serialnumberkit, firmware_update, mock_motor_left, - mock_motor_right, mock_ears, mock_belt, mock_ledkit, mock_lcd, - mock_videokit, bhvkit, cmdkit, rfidkit, activitykit}; + RobotController> rc {timeout_state_internal, + timeout_state_transition, + battery, + serialnumberkit, + firmware_update, + mock_motor_left, + mock_motor_right, + mock_ears, + mock_belt, + mock_ledkit, + mock_lcd, + mock_videokit, + bhvkit, + cmdkit, + rfidkit, + activitykit}; ble::GapMock &mbed_mock_gap = ble::gap_mock(); ble::GattServerMock &mbed_mock_gatt = ble::gatt_server_mock(); @@ -229,10 +242,10 @@ class RobotControllerTest : public testing::Test expectedCallsRunLaunchingBehavior(); Sequence on_idle_entry_sequence; - EXPECT_CALL(timeout, onTimeout) + EXPECT_CALL(timeout_state_transition, onTimeout) .InSequence(on_idle_entry_sequence) .WillOnce(GetCallback(&on_sleep_timeout)); - EXPECT_CALL(timeout, start).InSequence(on_idle_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_idle_entry_sequence); EXPECT_CALL(mock_videokit, playVideoOnRepeat).InSequence(on_idle_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).Times(AtLeast(1)).InSequence(on_idle_entry_sequence); @@ -255,10 +268,10 @@ class RobotControllerTest : public testing::Test EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout) + EXPECT_CALL(timeout_state_internal, onTimeout) .InSequence(start_charging_behavior_sequence) .WillOnce(GetCallback(&on_charging_start_timeout)); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); } void expectedCallsRunLaunchingBehavior() diff --git a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp index 0529a7461d..0d079ab896 100644 --- a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp +++ b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp @@ -47,8 +47,8 @@ TEST_F(RobotControllerTest, registerEventsBatteryIsNotCharging) .InSequence(run_launching_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(run_launching_behavior_sequence); - EXPECT_CALL(timeout, onTimeout); - EXPECT_CALL(timeout, start); + EXPECT_CALL(timeout_state_transition, onTimeout); + EXPECT_CALL(timeout_state_transition, start); EXPECT_CALL(mock_videokit, playVideoOnRepeat); EXPECT_CALL(mock_lcd, turnOn); @@ -106,8 +106,8 @@ TEST_F(RobotControllerTest, registerEventsBatteryIsCharging) EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); } } diff --git a/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp b/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp index be3a45346c..e37abcedf6 100644 --- a/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp @@ -14,8 +14,8 @@ TEST_F(RobotControllerTest, stateAutonomousActivityConnectedEventCommandReceived EXPECT_CALL(mock_motor_right, stop).InSequence(on_autonomous_activity_exit_sequence); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(on_working_entry_sequence); @@ -47,8 +47,8 @@ TEST_F(RobotControllerTest, stateAutonomousActivityEventBleConnection) EXPECT_CALL(mock_videokit, playVideoOnce).Times(1).InSequence(on_ble_connection_sequence); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).Times(2).InSequence(on_working_entry_sequence); @@ -73,8 +73,8 @@ TEST_F(RobotControllerTest, stateAutonomousActivityEventChargeDidStartGuardIsCha EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AtLeast(1)); @@ -210,8 +210,8 @@ TEST_F(RobotControllerTest, stateAutonomousActivityDiceRollDetectedDelayOverEven EXPECT_CALL(mock_motor_right, stop).Times(AtLeast(1)); Sequence on_idle_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_idle_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_idle_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_idle_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_idle_entry_sequence); EXPECT_CALL(mock_videokit, playVideoOnRepeat).InSequence(on_idle_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(on_idle_entry_sequence); @@ -233,8 +233,8 @@ TEST_F(RobotControllerTest, stateAutonomousActivityDiceRollDetectedDelayOverEven EXPECT_CALL(mock_motor_right, stop).Times(AtLeast(1)); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); diff --git a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp index 2036a813a9..499fe5a90a 100644 --- a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp @@ -10,8 +10,9 @@ TEST_F(RobotControllerTest, onChargingStartTimeout) EXPECT_CALL(mock_videokit, displayImage); EXPECT_CALL(mock_ledkit, start); EXPECT_CALL(mock_lcd, turnOn).Times(AnyNumber()); - EXPECT_CALL(timeout, onTimeout).WillOnce(GetCallback(&on_charging_start_timeout)); - EXPECT_CALL(timeout, start).Times(AnyNumber()); + EXPECT_CALL(timeout_state_internal, onTimeout) + .WillOnce(GetCallback(&on_charging_start_timeout)); + EXPECT_CALL(timeout_state_internal, start).Times(AnyNumber()); rc.startChargingBehavior(); EXPECT_CALL(mock_lcd, turnOff); @@ -39,14 +40,14 @@ TEST_F(RobotControllerTest, stateChargingConnectedEventChargeDidStopGuardIsCharg EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); Sequence on_charging_exit_sequence; - EXPECT_CALL(timeout, stop).Times(1).InSequence(on_charging_exit_sequence); + EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopMotors(); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); // TODO: Specify which BLE service and what is expected if necessary @@ -64,14 +65,14 @@ TEST_F(RobotControllerTest, stateChargingDisconnectedEventChargeDidStopGuardIsCh EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); Sequence on_charging_exit_sequence; - EXPECT_CALL(timeout, stop).Times(1).InSequence(on_charging_exit_sequence); + EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopMotors(); Sequence on_idle_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_idle_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_idle_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_idle_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_idle_entry_sequence); EXPECT_CALL(mock_videokit, playVideoOnRepeat).InSequence(on_idle_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(on_idle_entry_sequence); @@ -90,7 +91,7 @@ TEST_F(RobotControllerTest, stateChargingDisconnectedEventBleConnection) EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(true)); Sequence on_charging_exit_sequence; - EXPECT_CALL(timeout, stop).Times(1).InSequence(on_charging_exit_sequence); + EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopActuators(); Sequence on_ble_connection_sequence; @@ -108,8 +109,9 @@ TEST_F(RobotControllerTest, stateChargingDisconnectedEventBleConnection) EXPECT_CALL(mock_videokit, displayImage).InSequence(on_charging_entry_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(on_charging_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).Times(AnyNumber()).InSequence(on_charging_entry_sequence); - EXPECT_CALL(timeout, onTimeout).WillOnce(GetCallback(&on_charging_start_timeout)); - EXPECT_CALL(timeout, start).Times(AnyNumber()).InSequence(on_charging_entry_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout) + .WillOnce(GetCallback(&on_charging_start_timeout)); + EXPECT_CALL(timeout_state_internal, start).Times(AnyNumber()).InSequence(on_charging_entry_sequence); rc.state_machine.process_event(lksm::event::ble_connection {}); @@ -132,7 +134,7 @@ TEST_F(RobotControllerTest, stateChargingEventFileExchangeRequestedGuardIsReadyT EXPECT_CALL(battery, level).InSequence(is_ready_to_file_exchange_sequence).WillRepeatedly(Return(returned_level)); Sequence on_charging_exit_sequence; - EXPECT_CALL(timeout, stop).Times(1).InSequence(on_charging_exit_sequence); + EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopMotors(); @@ -167,7 +169,7 @@ TEST_F(RobotControllerTest, EXPECT_CALL(battery, level).InSequence(is_ready_to_file_exchange_sequence).WillRepeatedly(Return(returned_level)); Sequence on_charging_exit_sequence; - EXPECT_CALL(timeout, stop).Times(1).InSequence(on_charging_exit_sequence); + EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopMotors(); @@ -204,7 +206,7 @@ TEST_F(RobotControllerTest, stateChargingEventFileExchangeRequestedGuardIsReadyT EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).InSequence(is_ready_to_file_exchange_sequence); Sequence stop_charging_behavior; - EXPECT_CALL(timeout, stop).Times(0).InSequence(stop_charging_behavior); + EXPECT_CALL(timeout_state_internal, stop).Times(0).InSequence(stop_charging_behavior); rc.state_machine.process_event(lksm::event::file_exchange_start_requested {}); @@ -229,7 +231,7 @@ TEST_F(RobotControllerTest, EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).InSequence(is_ready_to_file_exchange_sequence); Sequence stop_charging_behavior; - EXPECT_CALL(timeout, stop).Times(0).InSequence(stop_charging_behavior); + EXPECT_CALL(timeout_state_internal, stop).Times(0).InSequence(stop_charging_behavior); rc.state_machine.process_event(lksm::event::file_exchange_start_requested {}); @@ -376,7 +378,7 @@ TEST_F(RobotControllerTest, stateChargingEventEmergencyStopDelayOver) EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(true)); Sequence on_charging_exit_sequence; - EXPECT_CALL(timeout, stop).Times(1).InSequence(on_charging_exit_sequence); + EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopActuators(); EXPECT_CALL(mock_lcd, turnOff); @@ -398,7 +400,7 @@ TEST_F(RobotControllerTest, stateChargingDiceRollDetectedDelayNotOver) EXPECT_CALL(mock_videokit, displayImage).Times(0); EXPECT_CALL(mock_ledkit, start).Times(0); EXPECT_CALL(mock_lcd, turnOn).Times(0); - EXPECT_CALL(timeout, start).Times(0); + EXPECT_CALL(timeout_state_internal, start).Times(0); spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::dice_roll); @@ -414,7 +416,7 @@ TEST_F(RobotControllerTest, stateChargingDiceRollDetectedDelayOverEventAutonomou auto minimal_delay_over = 1001ms; Sequence on_charging_exit_sequence; - EXPECT_CALL(timeout, stop).Times(1).InSequence(on_charging_exit_sequence); + EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopMotors(); @@ -423,8 +425,9 @@ TEST_F(RobotControllerTest, stateChargingDiceRollDetectedDelayOverEventAutonomou EXPECT_CALL(mock_videokit, displayImage).Times(1); EXPECT_CALL(mock_ledkit, start); EXPECT_CALL(mock_lcd, turnOn).Times(AnyNumber()); - EXPECT_CALL(timeout, onTimeout).WillOnce(GetCallback(&on_charging_start_timeout)); - EXPECT_CALL(timeout, start).Times(AnyNumber()); + EXPECT_CALL(timeout_state_internal, onTimeout) + .WillOnce(GetCallback(&on_charging_start_timeout)); + EXPECT_CALL(timeout_state_internal, start).Times(AnyNumber()); spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); rc.onMagicCardAvailable(MagicCard::dice_roll); diff --git a/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp b/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp index abf34e1d28..900cda194e 100644 --- a/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp @@ -11,8 +11,8 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedConnectedEventCommandReceivedIs EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(on_working_entry_sequence); @@ -52,8 +52,8 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedEventBleConnectionGuardIsNotCha EXPECT_CALL(mock_videokit, playVideoOnce).Times(1).InSequence(on_ble_connection_sequence); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).Times(2).InSequence(on_working_entry_sequence); @@ -76,8 +76,8 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedEventChargeDidStartGuardIsCharg EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AtLeast(1)); @@ -112,8 +112,8 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedConnectedEventCommandReceivedGu EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AtLeast(1)); @@ -159,8 +159,8 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedEventBleConnectionGuardIsChargi EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AtLeast(1)); @@ -222,8 +222,8 @@ TEST_F(RobotControllerTest, EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); rc.onMagicCardAvailable(MagicCard::dice_roll); diff --git a/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp b/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp index 78f057d080..8637c06be5 100644 --- a/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp @@ -23,8 +23,8 @@ TEST_F(RobotControllerTest, stateFileExchangeEventFileExchangeStopRequestedGuard EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); rc.state_machine.process_event(lksm::event::file_exchange_stop_requested {}); @@ -46,8 +46,8 @@ TEST_F(RobotControllerTest, stateFileExchangeEventFileExchangeStopRequestedGuard EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AnyNumber()).InSequence(on_file_exchange_end); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); rc.state_machine.process_event(lksm::event::file_exchange_stop_requested {}); @@ -78,8 +78,8 @@ TEST_F(RobotControllerTest, stateFileExchangeEventDisconnectionGuardIsCharging) EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); rc.state_machine.process_event(lksm::event::ble_disconnection {}); @@ -105,8 +105,8 @@ TEST_F(RobotControllerTest, stateFileExchangeEventDisconnectionGuardIsNotChargin EXPECT_CALL(mock_ledkit, start).Times(0).InSequence(start_disconnection_behavior); Sequence on_idle_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_idle_sequence); - EXPECT_CALL(timeout, start).InSequence(on_idle_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_idle_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_idle_sequence); EXPECT_CALL(mock_videokit, playVideoOnRepeat).InSequence(on_idle_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(on_idle_sequence); diff --git a/libs/RobotKit/tests/RobotController_test_stateIdle.cpp b/libs/RobotKit/tests/RobotController_test_stateIdle.cpp index 3b1e3d8e4a..d74311968e 100644 --- a/libs/RobotKit/tests/RobotController_test_stateIdle.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateIdle.cpp @@ -9,7 +9,7 @@ TEST_F(RobotControllerTest, stateIdleEventTimeout) rc.state_machine.set_current_states(lksm::state::idle); Sequence on_exit_idle_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_idle_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_idle_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_idle_sequence); expectedCallsStopMotors(); @@ -17,10 +17,10 @@ TEST_F(RobotControllerTest, stateIdleEventTimeout) EXPECT_CALL(mock_ledkit, start(isSameAnimation(&led::animation::sleeping))).InSequence(on_sleeping_sequence); EXPECT_CALL(mock_videokit, playVideoOnce).InSequence(on_sleeping_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(on_sleeping_sequence); - EXPECT_CALL(timeout, onTimeout) + EXPECT_CALL(timeout_state_internal, onTimeout) .InSequence(on_sleeping_sequence) .WillOnce(GetCallback(&on_sleeping_start_timeout)); - EXPECT_CALL(timeout, start).InSequence(on_sleeping_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(on_sleeping_sequence); on_sleep_timeout(); @@ -37,7 +37,7 @@ TEST_F(RobotControllerTest, stateIdleEventBleConnection) EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); Sequence on_exit_idle_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_idle_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_idle_sequence); expectedCallsStopActuators(); Sequence on_ble_connection_sequence; @@ -48,8 +48,8 @@ TEST_F(RobotControllerTest, stateIdleEventBleConnection) EXPECT_CALL(mock_lcd, turnOn).Times(1).InSequence(on_ble_connection_sequence); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); rc.state_machine.process_event(lksm::event::ble_connection {}); @@ -62,13 +62,13 @@ TEST_F(RobotControllerTest, stateIdleEventCommandReceived) rc.state_machine.set_current_states(lksm::state::idle, lksm::state::connected); Sequence on_exit_idle_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_idle_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_idle_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_idle_sequence); expectedCallsStopMotors(); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); rc.state_machine.process_event(lksm::event::command_received {}); @@ -83,7 +83,7 @@ TEST_F(RobotControllerTest, stateIdleEventChargeDidStartGuardIsChargingTrue) EXPECT_CALL(battery, isCharging).WillOnce(Return(true)); Sequence on_exit_idle_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_idle_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_idle_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_idle_sequence); expectedCallsStopMotors(); @@ -92,8 +92,8 @@ TEST_F(RobotControllerTest, stateIdleEventChargeDidStartGuardIsChargingTrue) EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)); @@ -136,7 +136,7 @@ TEST_F(RobotControllerTest, stateIdleEventEmergencyStopDelayOver) auto delay_over = 11s; Sequence on_exit_idle_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_idle_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_idle_sequence); EXPECT_CALL(mock_motor_left, stop).Times(AtLeast(1)); EXPECT_CALL(mock_motor_right, stop).Times(AtLeast(1)); @@ -174,7 +174,7 @@ TEST_F(RobotControllerTest, stateIdleDiceRollDetectedDelayOverEventAutonomousAct auto minimal_delay_over = 1001ms; Sequence on_exit_idle_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_idle_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_idle_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_idle_sequence); expectedCallsStopMotors(); diff --git a/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp b/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp index 7f5c57dcae..6bf77bf74d 100644 --- a/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp @@ -9,13 +9,13 @@ TEST_F(RobotControllerTest, stateSleepingEventCommandReceived) rc.state_machine.set_current_states(lksm::state::sleeping, lksm::state::connected); Sequence on_exit_sleeping_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_sleeping_sequence); + EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); expectedCallsStopMotors(); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); rc.state_machine.process_event(lksm::event::command_received {}); @@ -30,7 +30,7 @@ TEST_F(RobotControllerTest, stateSleepingEventBleConnection) EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); Sequence on_exit_sleeping_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_sleeping_sequence); + EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); expectedCallsStopActuators(); Sequence on_ble_connection_sequence; @@ -41,8 +41,8 @@ TEST_F(RobotControllerTest, stateSleepingEventBleConnection) EXPECT_CALL(mock_lcd, turnOn).Times(1).InSequence(on_ble_connection_sequence); Sequence on_working_entry_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_working_entry_sequence); - EXPECT_CALL(timeout, start).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_working_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); rc.state_machine.process_event(lksm::event::ble_connection {}); @@ -57,7 +57,7 @@ TEST_F(RobotControllerTest, stateSleepingEventChargeDidStartGuardIsChargingTrue) EXPECT_CALL(battery, isCharging).WillOnce(Return(true)); Sequence on_exit_sleeping_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_sleeping_sequence); + EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); expectedCallsStopMotors(); @@ -66,8 +66,8 @@ TEST_F(RobotControllerTest, stateSleepingEventChargeDidStartGuardIsChargingTrue) EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)); @@ -110,7 +110,7 @@ TEST_F(RobotControllerTest, stateSleepingEventEmergencyStopDelayOver) auto delay_over = 11s; Sequence on_exit_sleeping_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_sleeping_sequence); + EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(mock_motor_left, stop).Times(AtLeast(1)); EXPECT_CALL(mock_motor_right, stop).Times(AtLeast(1)); @@ -148,7 +148,7 @@ TEST_F(RobotControllerTest, stateSleepingDiceRollDetectedDelayOverEventAutonomou auto minimal_delay_over = 1001ms; Sequence on_exit_sleeping_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_sleeping_sequence); + EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); expectedCallsStopMotors(); diff --git a/libs/RobotKit/tests/RobotController_test_stateWorking.cpp b/libs/RobotKit/tests/RobotController_test_stateWorking.cpp index 98f58f3780..2c77b94c71 100644 --- a/libs/RobotKit/tests/RobotController_test_stateWorking.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateWorking.cpp @@ -7,20 +7,20 @@ TEST_F(RobotControllerTest, stateWorkingEventTimeout) { Sequence get_on_idle_timeout_callback; - EXPECT_CALL(timeout, onTimeout) + EXPECT_CALL(timeout_state_transition, onTimeout) .InSequence(get_on_idle_timeout_callback) .WillOnce(GetCallback(&on_idle_timeout)); - EXPECT_CALL(timeout, start).InSequence(get_on_idle_timeout_callback); + EXPECT_CALL(timeout_state_transition, start).InSequence(get_on_idle_timeout_callback); rc.startIdleTimeout(); rc.state_machine.set_current_states(lksm::state::working); Sequence on_exit_working_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_working_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_working_sequence); Sequence on_idle_sequence; - EXPECT_CALL(timeout, onTimeout).InSequence(on_idle_sequence); - EXPECT_CALL(timeout, start).InSequence(on_idle_sequence); + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_idle_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_idle_sequence); EXPECT_CALL(mock_videokit, playVideoOnRepeat).InSequence(on_idle_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(on_idle_sequence); @@ -36,15 +36,15 @@ TEST_F(RobotControllerTest, stateWorkingEventChargeDidStartGuardIsChargingTrue) EXPECT_CALL(battery, isCharging).WillOnce(Return(true)); Sequence on_exit_working_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_working_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_working_sequence); Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_ledkit, start).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, onTimeout).InSequence(start_charging_behavior_sequence); - EXPECT_CALL(timeout, start).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); + EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)); @@ -87,7 +87,7 @@ TEST_F(RobotControllerTest, stateWorkingEventEmergencyStopDelayOver) auto delay_over = 11s; Sequence on_exit_working_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_working_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_working_sequence); EXPECT_CALL(mock_motor_left, stop).Times(2); EXPECT_CALL(mock_motor_right, stop).Times(2); @@ -124,7 +124,7 @@ TEST_F(RobotControllerTest, stateWorkingDiceRollDetectedDelayOverEventAutonomous auto minimal_delay_over = 1001ms; Sequence on_exit_working_sequence; - EXPECT_CALL(timeout, stop).InSequence(on_exit_working_sequence); + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_working_sequence); EXPECT_CALL(mock_videokit, displayImage).Times(1); From 382551e50674cbf71fd9a9cb818abb9c7d6f29cf Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 16 Dec 2022 13:11:58 +0100 Subject: [PATCH 007/143] :truck: (behavior): Rename misleading bleConnection tests bleConnection don't have charging parameter --- libs/BehaviorKit/tests/BehaviorKit_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/BehaviorKit/tests/BehaviorKit_test.cpp b/libs/BehaviorKit/tests/BehaviorKit_test.cpp index 9429141e0c..58a4027b9f 100644 --- a/libs/BehaviorKit/tests/BehaviorKit_test.cpp +++ b/libs/BehaviorKit/tests/BehaviorKit_test.cpp @@ -98,7 +98,7 @@ TEST_F(BehaviorKitTest, batteryBehaviors) behaviorkit.chargingFull(); } -TEST_F(BehaviorKitTest, bleConnectionWhileCharging) +TEST_F(BehaviorKitTest, bleConnectionWithoutVideo) { EXPECT_CALL(mock_videokit, playVideoOnce).Times(0); EXPECT_CALL(mock_ledkit, start(isSameAnimation(&led::animation::ble_connection))).Times(1); @@ -106,7 +106,7 @@ TEST_F(BehaviorKitTest, bleConnectionWhileCharging) behaviorkit.bleConnection(false); } -TEST_F(BehaviorKitTest, bleConnectionWhileNotCharging) +TEST_F(BehaviorKitTest, bleConnectionWithVideo) { EXPECT_CALL(mock_videokit, playVideoOnce); EXPECT_CALL(mock_ledkit, start(isSameAnimation(&led::animation::ble_connection))).Times(1); From 37e0159efcc778a8384f05782abc066aef34e50a Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 16 Dec 2022 13:18:05 +0100 Subject: [PATCH 008/143] :truck: (sm): Rename onFileExchange{Start/End} by {start/stop}FileExchange --- libs/RobotKit/include/RobotController.h | 4 ++-- libs/RobotKit/include/StateMachine.h | 12 +++++----- .../include/interface/RobotController.h | 4 ++-- .../RobotController_test_stateCharging.cpp | 24 +++++++++---------- libs/RobotKit/tests/StateMachine_test.cpp | 16 ++++++------- libs/RobotKit/tests/mocks/RobotController.h | 4 ++-- 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index b289d63434..395cbc8156 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -220,7 +220,7 @@ class RobotController : public interface::RobotController _activitykit.stop(); } - void onFileExchangeStart() final + void startFileExchange() final { _behaviorkit.fileExchange(); if (_battery.isCharging()) { @@ -247,7 +247,7 @@ class RobotController : public interface::RobotController }); } - void onFileExchangeEnd() final + void stopFileExchange() final { _service_file_exchange.setFileExchangeState(false); diff --git a/libs/RobotKit/include/StateMachine.h b/libs/RobotKit/include/StateMachine.h index 892511fb6d..e42dc00856 100644 --- a/libs/RobotKit/include/StateMachine.h +++ b/libs/RobotKit/include/StateMachine.h @@ -137,12 +137,12 @@ namespace sm::action { auto operator()(irc &rc) const { rc.stopChargingBehavior(); } }; - struct on_file_exchange_start { - auto operator()(irc &rc) const { rc.onFileExchangeStart(); } + struct start_file_exchange { + auto operator()(irc &rc) const { rc.startFileExchange(); } }; - struct on_file_exchange_end { - auto operator()(irc &rc) const { rc.onFileExchangeEnd(); } + struct stop_file_exchange { + auto operator()(irc &rc) const { rc.stopFileExchange(); } }; struct apply_update { @@ -231,8 +231,8 @@ struct StateMachine { , sm::state::charging + event = sm::state::emergency_stopped , sm::state::charging + event = sm::state::charging - , sm::state::file_exchange + boost::sml::on_entry<_> / sm::action::on_file_exchange_start {} - , sm::state::file_exchange + boost::sml::on_exit<_> / sm::action::on_file_exchange_end {} + , sm::state::file_exchange + boost::sml::on_entry<_> / sm::action::start_file_exchange {} + , sm::state::file_exchange + boost::sml::on_exit<_> / sm::action::stop_file_exchange {} , sm::state::file_exchange + event [sm::guard::is_not_charging {}] = sm::state::working , sm::state::file_exchange + event [sm::guard::is_charging {}] = sm::state::charging diff --git a/libs/RobotKit/include/interface/RobotController.h b/libs/RobotKit/include/interface/RobotController.h index 07c52bdbbd..80a4030a1a 100644 --- a/libs/RobotKit/include/interface/RobotController.h +++ b/libs/RobotKit/include/interface/RobotController.h @@ -41,8 +41,8 @@ class RobotController virtual void startWorkingBehavior() = 0; - virtual void onFileExchangeStart() = 0; - virtual void onFileExchangeEnd() = 0; + virtual void startFileExchange() = 0; + virtual void stopFileExchange() = 0; virtual auto isReadyToFileExchange() -> bool = 0; virtual auto isReadyToUpdate() -> bool = 0; diff --git a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp index 499fe5a90a..fbd05945e2 100644 --- a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp @@ -139,13 +139,13 @@ TEST_F(RobotControllerTest, stateChargingEventFileExchangeRequestedGuardIsReadyT EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopMotors(); - Sequence on_file_exchange_start; - EXPECT_CALL(mock_videokit, displayImage).InSequence(on_file_exchange_start); - EXPECT_CALL(battery, isCharging).InSequence(on_file_exchange_start).WillRepeatedly(Return(returned_is_charging)); - EXPECT_CALL(mock_ledkit, start).Times(1).InSequence(on_file_exchange_start); - EXPECT_CALL(mock_lcd, turnOn).InSequence(on_file_exchange_start); + Sequence start_file_exchange; + EXPECT_CALL(mock_videokit, displayImage).InSequence(start_file_exchange); + EXPECT_CALL(battery, isCharging).InSequence(start_file_exchange).WillRepeatedly(Return(returned_is_charging)); + EXPECT_CALL(mock_ledkit, start).Times(1).InSequence(start_file_exchange); + EXPECT_CALL(mock_lcd, turnOn).InSequence(start_file_exchange); // TODO: Specify which BLE service and what is expected if necessary - EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AnyNumber()).InSequence(on_file_exchange_start); + EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AnyNumber()).InSequence(start_file_exchange); rc.state_machine.process_event(lksm::event::file_exchange_start_requested {}); @@ -176,13 +176,13 @@ TEST_F(RobotControllerTest, returned_is_charging = false; - Sequence on_file_exchange_start; - EXPECT_CALL(mock_videokit, displayImage).InSequence(on_file_exchange_start); - EXPECT_CALL(battery, isCharging).InSequence(on_file_exchange_start).WillRepeatedly(Return(returned_is_charging)); - EXPECT_CALL(mock_ledkit, start).Times(0).InSequence(on_file_exchange_start); - EXPECT_CALL(mock_lcd, turnOn).InSequence(on_file_exchange_start); + Sequence start_file_exchange; + EXPECT_CALL(mock_videokit, displayImage).InSequence(start_file_exchange); + EXPECT_CALL(battery, isCharging).InSequence(start_file_exchange).WillRepeatedly(Return(returned_is_charging)); + EXPECT_CALL(mock_ledkit, start).Times(0).InSequence(start_file_exchange); + EXPECT_CALL(mock_lcd, turnOn).InSequence(start_file_exchange); // TODO: Specify which BLE service and what is expected if necessary - EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AnyNumber()).InSequence(on_file_exchange_start); + EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AnyNumber()).InSequence(start_file_exchange); rc.state_machine.process_event(lksm::event::file_exchange_start_requested {}); diff --git a/libs/RobotKit/tests/StateMachine_test.cpp b/libs/RobotKit/tests/StateMachine_test.cpp index e3429da144..e409e93d64 100644 --- a/libs/RobotKit/tests/StateMachine_test.cpp +++ b/libs/RobotKit/tests/StateMachine_test.cpp @@ -663,7 +663,7 @@ TEST_F(StateMachineTest, stateChargingEventFileExchangeRequestedGuardTrue) EXPECT_CALL(mock_rc, stopChargingBehavior); EXPECT_CALL(mock_rc, isReadyToFileExchange).WillOnce(Return(true)); - EXPECT_CALL(mock_rc, onFileExchangeStart); + EXPECT_CALL(mock_rc, startFileExchange); sm.process_event(lksm::event::file_exchange_start_requested {}); @@ -675,7 +675,7 @@ TEST_F(StateMachineTest, stateChargingEventFileExchangeRequestedGuardFalse) sm.set_current_states(lksm::state::charging); EXPECT_CALL(mock_rc, isReadyToFileExchange).WillOnce(Return(false)); - EXPECT_CALL(mock_rc, onFileExchangeStart).Times(0); + EXPECT_CALL(mock_rc, startFileExchange).Times(0); sm.process_event(lksm::event::file_exchange_start_requested {}); @@ -686,7 +686,7 @@ TEST_F(StateMachineTest, stateFileExhangeEventFileExchangeStopRequestedGuardIsCh { sm.set_current_states(lksm::state::file_exchange); - EXPECT_CALL(mock_rc, onFileExchangeEnd); + EXPECT_CALL(mock_rc, stopFileExchange); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, startChargingBehavior); @@ -700,7 +700,7 @@ TEST_F(StateMachineTest, stateFileExhangeEventFileExchangeStopRequestedGuardIsNo { sm.set_current_states(lksm::state::file_exchange); - EXPECT_CALL(mock_rc, onFileExchangeEnd); + EXPECT_CALL(mock_rc, stopFileExchange); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(false)); EXPECT_CALL(mock_rc, startWorkingBehavior); @@ -715,7 +715,7 @@ TEST_F(StateMachineTest, stateFileExhangeEventBleDisconnectionGuardIsCharging) { sm.set_current_states(lksm::state::file_exchange, lksm::state::connected); - EXPECT_CALL(mock_rc, onFileExchangeEnd); + EXPECT_CALL(mock_rc, stopFileExchange); EXPECT_CALL(mock_rc, startDisconnectionBehavior); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); @@ -731,7 +731,7 @@ TEST_F(StateMachineTest, stateFileExhangeEventBleDisconnectionGuardIsNotCharging { sm.set_current_states(lksm::state::file_exchange, lksm::state::connected); - EXPECT_CALL(mock_rc, onFileExchangeEnd); + EXPECT_CALL(mock_rc, stopFileExchange); EXPECT_CALL(mock_rc, startDisconnectionBehavior); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(false)); @@ -748,7 +748,7 @@ TEST_F(StateMachineTest, stateFileExhangeEventEmergencyStop) { sm.set_current_states(lksm::state::file_exchange); - EXPECT_CALL(mock_rc, onFileExchangeEnd); + EXPECT_CALL(mock_rc, stopFileExchange); EXPECT_CALL(mock_rc, stopActuatorsAndLcd); @@ -761,7 +761,7 @@ TEST_F(StateMachineTest, stateFileExhangeEventUpdateRequestedGuardTrue) { sm.set_current_states(lksm::state::file_exchange); - EXPECT_CALL(mock_rc, onFileExchangeEnd); + EXPECT_CALL(mock_rc, stopFileExchange); EXPECT_CALL(mock_rc, isReadyToUpdate).WillOnce(Return(true)); EXPECT_CALL(mock_rc, applyUpdate); diff --git a/libs/RobotKit/tests/mocks/RobotController.h b/libs/RobotKit/tests/mocks/RobotController.h index 5b258fc7a2..bd88dda5b1 100644 --- a/libs/RobotKit/tests/mocks/RobotController.h +++ b/libs/RobotKit/tests/mocks/RobotController.h @@ -38,8 +38,8 @@ struct RobotController : public interface::RobotController { MOCK_METHOD(void, startAutonomousActivityMode, (), (override)); MOCK_METHOD(void, stopAutonomousActivityMode, (), (override)); - MOCK_METHOD(void, onFileExchangeStart, (), (override)); - MOCK_METHOD(void, onFileExchangeEnd, (), (override)); + MOCK_METHOD(void, startFileExchange, (), (override)); + MOCK_METHOD(void, stopFileExchange, (), (override)); MOCK_METHOD(bool, isReadyToFileExchange, (), (override)); MOCK_METHOD(bool, isReadyToUpdate, (), (override)); From 50d98bcff0c3cb0df7f2d266f99bb9bafb16bd26 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 13:43:05 +0100 Subject: [PATCH 009/143] :recycle: (behaviorkit): Separate bleConnection into bleConnectionWithoutVideo and bleConnectionWithVideo --- libs/BehaviorKit/include/BehaviorKit.h | 3 ++- libs/BehaviorKit/source/BehaviorKit.cpp | 11 +++++++---- libs/BehaviorKit/tests/BehaviorKit_test.cpp | 4 ++-- libs/RobotKit/include/RobotController.h | 4 ++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/libs/BehaviorKit/include/BehaviorKit.h b/libs/BehaviorKit/include/BehaviorKit.h index 27e15da8c3..cfb7b82fe7 100644 --- a/libs/BehaviorKit/include/BehaviorKit.h +++ b/libs/BehaviorKit/include/BehaviorKit.h @@ -37,7 +37,8 @@ class BehaviorKit void chargingHigh(); void chargingFull(); - void bleConnection(bool with_video); + void bleConnectionWithoutVideo(); + void bleConnectionWithVideo(); void working(); void fileExchange(); diff --git a/libs/BehaviorKit/source/BehaviorKit.cpp b/libs/BehaviorKit/source/BehaviorKit.cpp index 69a9d27f5d..485ab71f1e 100644 --- a/libs/BehaviorKit/source/BehaviorKit.cpp +++ b/libs/BehaviorKit/source/BehaviorKit.cpp @@ -79,12 +79,15 @@ void BehaviorKit::chargingFull() _videokit.displayImage("/fs/home/img/system/robot-battery-charging-quarter_4-green.jpg"); } -void BehaviorKit::bleConnection(bool with_video) +void BehaviorKit::bleConnectionWithoutVideo() { _ledkit.start(&led::animation::ble_connection); - if (with_video) { - _videokit.playVideoOnce("/fs/home/vid/system/robot-system-ble_connection-wink-no_eyebrows.avi"); - } +} + +void BehaviorKit::bleConnectionWithVideo() +{ + _ledkit.start(&led::animation::ble_connection); + _videokit.playVideoOnce("/fs/home/vid/system/robot-system-ble_connection-wink-no_eyebrows.avi"); } void BehaviorKit::working() diff --git a/libs/BehaviorKit/tests/BehaviorKit_test.cpp b/libs/BehaviorKit/tests/BehaviorKit_test.cpp index 58a4027b9f..002d28a550 100644 --- a/libs/BehaviorKit/tests/BehaviorKit_test.cpp +++ b/libs/BehaviorKit/tests/BehaviorKit_test.cpp @@ -103,7 +103,7 @@ TEST_F(BehaviorKitTest, bleConnectionWithoutVideo) EXPECT_CALL(mock_videokit, playVideoOnce).Times(0); EXPECT_CALL(mock_ledkit, start(isSameAnimation(&led::animation::ble_connection))).Times(1); - behaviorkit.bleConnection(false); + behaviorkit.bleConnectionWithoutVideo(); } TEST_F(BehaviorKitTest, bleConnectionWithVideo) @@ -111,7 +111,7 @@ TEST_F(BehaviorKitTest, bleConnectionWithVideo) EXPECT_CALL(mock_videokit, playVideoOnce); EXPECT_CALL(mock_ledkit, start(isSameAnimation(&led::animation::ble_connection))).Times(1); - behaviorkit.bleConnection(true); + behaviorkit.bleConnectionWithVideo(); } TEST_F(BehaviorKitTest, working) diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index 395cbc8156..039a723f61 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -189,11 +189,11 @@ class RobotController : public interface::RobotController using namespace std::chrono_literals; stopActuators(); if (_battery.isCharging()) { - _behaviorkit.bleConnection(false); + _behaviorkit.bleConnectionWithoutVideo(); rtos::ThisThread::sleep_for(5s); _behaviorkit.blinkOnCharge(); } else { - _behaviorkit.bleConnection(true); + _behaviorkit.bleConnectionWithVideo(); _lcd.turnOn(); } } From 8edd374ba06e9eb80407c23f4bf5b258004451a2 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 2 Jan 2023 15:47:18 +0100 Subject: [PATCH 010/143] :white_check_mark: (sm): Fix GMOCK WARNINGS of Uninteresting mock function call --- libs/RobotKit/tests/StateMachine_test.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/libs/RobotKit/tests/StateMachine_test.cpp b/libs/RobotKit/tests/StateMachine_test.cpp index e409e93d64..57c5955664 100644 --- a/libs/RobotKit/tests/StateMachine_test.cpp +++ b/libs/RobotKit/tests/StateMachine_test.cpp @@ -323,6 +323,7 @@ TEST_F(StateMachineTest, stateChargingEventAutonomousActivityRequested) sm.set_current_states(lksm::state::charging); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); EXPECT_CALL(mock_rc, startAutonomousActivityMode).Times(0); sm.process_event(lksm::event::autonomous_activities_mode_requested {}); @@ -334,8 +335,10 @@ TEST_F(StateMachineTest, stateSleepingEventBleConnection) { sm.set_current_states(lksm::state::sleeping, lksm::state::disconnected); + EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); EXPECT_CALL(mock_rc, startConnectionBehavior).Times(1); EXPECT_CALL(mock_rc, startWorkingBehavior).Times(1); + EXPECT_CALL(mock_rc, startIdleTimeout).Times(1); sm.process_event(lksm::event::ble_connection {}); @@ -357,8 +360,11 @@ TEST_F(StateMachineTest, stateIdleEventBleConnection) { sm.set_current_states(lksm::state::idle, lksm::state::disconnected); + EXPECT_CALL(mock_rc, stopWaitingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startConnectionBehavior).Times(1); EXPECT_CALL(mock_rc, startWorkingBehavior).Times(1); + EXPECT_CALL(mock_rc, startIdleTimeout).Times(1); sm.process_event(lksm::event::ble_connection {}); @@ -380,6 +386,7 @@ TEST_F(StateMachineTest, stateChargingEventBleConnection) { sm.set_current_states(lksm::state::charging, lksm::state::disconnected); + EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, startConnectionBehavior).Times(1); @@ -393,6 +400,7 @@ TEST_F(StateMachineTest, stateChargingEventBleDisconnection) { sm.set_current_states(lksm::state::charging, lksm::state::connected); + EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); EXPECT_CALL(mock_rc, startDisconnectionBehavior).Times(1); @@ -405,6 +413,7 @@ TEST_F(StateMachineTest, stateChargingEventCommandReceived) { sm.set_current_states(lksm::state::charging); + EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); sm.process_event(lksm::event::command_received {}); @@ -517,6 +526,7 @@ TEST_F(StateMachineTest, stateEmergencyStoppedEventCommandReceivedGuardIsChargin sm.set_current_states(lksm::state::emergency_stopped, lksm::state::disconnected); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); + EXPECT_CALL(mock_rc, isBleConnected).WillRepeatedly(Return(false)); sm.process_event(lksm::event::command_received {}); @@ -559,6 +569,7 @@ TEST_F(StateMachineTest, stateAutonomousActivityEventBleConnection) EXPECT_CALL(mock_rc, stopAutonomousActivityMode).Times(1); EXPECT_CALL(mock_rc, startConnectionBehavior).Times(1); EXPECT_CALL(mock_rc, startWorkingBehavior).Times(1); + EXPECT_CALL(mock_rc, startIdleTimeout).Times(1); sm.process_event(lksm::event::ble_connection {}); @@ -634,8 +645,11 @@ TEST_F(StateMachineTest, stateAutonomousActivityEventAutonomousActivityExitedDis { sm.set_current_states(lksm::state::autonomous_activities, lksm::state::disconnected); + EXPECT_CALL(mock_rc, isBleConnected).WillRepeatedly(Return(false)); + EXPECT_CALL(mock_rc, stopAutonomousActivityMode).Times(1); EXPECT_CALL(mock_rc, startWaitingBehavior).Times(1); + EXPECT_CALL(mock_rc, startSleepTimeout).Times(1); sm.process_event(lksm::event::autonomous_activities_mode_exited {}); @@ -650,6 +664,7 @@ TEST_F(StateMachineTest, stateAutonomousActivityEventAutonomousActivityExitedCon EXPECT_CALL(mock_rc, stopAutonomousActivityMode).Times(1); EXPECT_CALL(mock_rc, startWorkingBehavior).Times(1); + EXPECT_CALL(mock_rc, startIdleTimeout).Times(1); sm.process_event(lksm::event::autonomous_activities_mode_exited {}); From b34b5378ffd03b04d17335ee3bc7e106856aa8b9 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 2 Jan 2023 13:39:25 +0100 Subject: [PATCH 011/143] :sparkles: (sm): Add DeepSleeping state From Sleeping+Charging with timeout event, add turn_off_hardware action on entry --- libs/RobotKit/include/StateMachine.h | 27 +++++++++++++++++++---- libs/RobotKit/tests/StateMachine_test.cpp | 22 ++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/libs/RobotKit/include/StateMachine.h b/libs/RobotKit/include/StateMachine.h index e42dc00856..dc1b3e1066 100644 --- a/libs/RobotKit/include/StateMachine.h +++ b/libs/RobotKit/include/StateMachine.h @@ -15,6 +15,8 @@ namespace sm::event { }; struct sleep_timeout_did_end { }; + struct deep_sleep_timeout_did_end { + }; struct idle_timeout_did_end { }; struct command_received { @@ -49,6 +51,7 @@ namespace sm::state { inline auto idle = boost::sml::state; inline auto working = boost::sml::state; inline auto sleeping = boost::sml::state; + inline auto deep_sleeping = boost::sml::state; inline auto charging = boost::sml::state; inline auto file_exchange = boost::sml::state; inline auto updating = boost::sml::state; @@ -105,6 +108,14 @@ namespace sm::action { auto operator()(irc &rc) const { rc.stopSleepTimeout(); } }; + struct start_deep_sleep_timeout { + auto operator()(irc &rc) const {} + }; + + struct stop_deep_sleep_timeout { + auto operator()(irc &rc) const {} + }; + struct start_idle_timeout { auto operator()(irc &rc) const { rc.startIdleTimeout(); } }; @@ -165,6 +176,10 @@ namespace sm::action { auto operator()(irc &rc) const { rc.stopActuatorsAndLcd(); } }; + struct suspend_hardware_for_deep_sleep { + auto operator()(irc &rc) const {} + }; + struct reset_emergency_stop_counter { auto operator()(irc &rc) const { rc.resetEmergencyStopCounter(); } }; @@ -209,17 +224,20 @@ struct StateMachine { , sm::state::working + event = sm::state::emergency_stopped , sm::state::working + event = sm::state::autonomous_activities - , sm::state::sleeping + boost::sml::on_entry<_> / sm::action::start_sleeping_behavior {} - , sm::state::sleeping + boost::sml::on_exit<_> / sm::action::stop_sleeping_behavior {} + , sm::state::sleeping + boost::sml::on_entry<_> / (sm::action::start_deep_sleep_timeout {}, sm::action::start_sleeping_behavior {} ) + , sm::state::sleeping + boost::sml::on_exit<_> / (sm::action::stop_deep_sleep_timeout {}, sm::action::stop_sleeping_behavior {} ) , sm::state::sleeping + event [sm::guard::is_connected {}] = sm::state::working , sm::state::sleeping + event = sm::state::working , sm::state::sleeping + event [sm::guard::is_charging {}] = sm::state::charging , sm::state::sleeping + event = sm::state::emergency_stopped , sm::state::sleeping + event = sm::state::autonomous_activities + , sm::state::sleeping + event = sm::state::deep_sleeping + + , sm::state::deep_sleeping + boost::sml::on_entry<_> / sm::action::suspend_hardware_for_deep_sleep {} - , sm::state::charging + boost::sml::on_entry<_> / sm::action::start_charging_behavior {} - , sm::state::charging + boost::sml::on_exit<_> / sm::action::stop_charging_behavior {} + , sm::state::charging + boost::sml::on_entry<_> / (sm::action::start_deep_sleep_timeout {}, sm::action::start_charging_behavior {} ) + , sm::state::charging + boost::sml::on_exit<_> / (sm::action::stop_deep_sleep_timeout {}, sm::action::stop_charging_behavior {} ) , sm::state::charging + event [sm::guard::is_not_charging {} && sm::guard::is_not_connected {}] = sm::state::idle , sm::state::charging + event [sm::guard::is_not_charging {} && sm::guard::is_connected {}] = sm::state::working @@ -230,6 +248,7 @@ struct StateMachine { , sm::state::charging + event = sm::state::charging , sm::state::charging + event = sm::state::emergency_stopped , sm::state::charging + event = sm::state::charging + , sm::state::charging + event = sm::state::deep_sleeping , sm::state::file_exchange + boost::sml::on_entry<_> / sm::action::start_file_exchange {} , sm::state::file_exchange + boost::sml::on_exit<_> / sm::action::stop_file_exchange {} diff --git a/libs/RobotKit/tests/StateMachine_test.cpp b/libs/RobotKit/tests/StateMachine_test.cpp index 57c5955664..0a3f3e2bf3 100644 --- a/libs/RobotKit/tests/StateMachine_test.cpp +++ b/libs/RobotKit/tests/StateMachine_test.cpp @@ -224,6 +224,17 @@ TEST_F(StateMachineTest, stateSleepEventAutonomousActivityRequested) EXPECT_TRUE(sm.is(lksm::state::autonomous_activities)); } +TEST_F(StateMachineTest, stateSleepEventTimeout) +{ + sm.set_current_states(lksm::state::sleeping); + + EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); + + sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); + + EXPECT_TRUE(sm.is(lksm::state::deep_sleeping)); +} + TEST_F(StateMachineTest, stateIdleEventChargeDidStart) { sm.set_current_states(lksm::state::idle); @@ -331,6 +342,17 @@ TEST_F(StateMachineTest, stateChargingEventAutonomousActivityRequested) EXPECT_TRUE(sm.is(lksm::state::charging)); } +TEST_F(StateMachineTest, stateChargingEventTimeout) +{ + sm.set_current_states(lksm::state::charging); + + EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + + sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); + + EXPECT_TRUE(sm.is(lksm::state::deep_sleeping)); +} + TEST_F(StateMachineTest, stateSleepingEventBleConnection) { sm.set_current_states(lksm::state::sleeping, lksm::state::disconnected); From 52d94b3de8e575d56b3839486da4a382b590de43 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 2 Jan 2023 13:52:08 +0100 Subject: [PATCH 012/143] :sparkles: (rc): Add timeout to DeepSleep implementation --- libs/RobotKit/include/RobotController.h | 12 ++++++ libs/RobotKit/include/StateMachine.h | 4 +- .../include/interface/RobotController.h | 3 ++ libs/RobotKit/tests/RobotController_test.h | 5 +++ .../RobotController_test_registerEvents.cpp | 4 ++ ...troller_test_stateAutonomousActivities.cpp | 4 ++ .../RobotController_test_stateCharging.cpp | 38 +++++++++++++++++++ ...tController_test_stateEmergencyStopped.cpp | 16 ++++++++ ...RobotController_test_stateFileExchange.cpp | 8 ++++ .../tests/RobotController_test_stateIdle.cpp | 8 ++++ .../RobotController_test_stateSleeping.cpp | 31 +++++++++++++++ .../RobotController_test_stateWorking.cpp | 4 ++ libs/RobotKit/tests/StateMachine_test.cpp | 32 ++++++++++++++++ libs/RobotKit/tests/mocks/RobotController.h | 4 ++ 14 files changed, 171 insertions(+), 2 deletions(-) diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index 039a723f61..4b37bbb174 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -90,6 +90,17 @@ class RobotController : public interface::RobotController void stopSleepTimeout() final { _timeout_state_transition.stop(); } + void startDeepSleepTimeout() final + { + using namespace system::robot::sm; + auto on_deep_sleep_timeout = [this] { raise(event::deep_sleep_timeout_did_end {}); }; + _timeout_state_transition.onTimeout(on_deep_sleep_timeout); + + _timeout_state_transition.start(_deep_sleep_timeout_duration); + } + + void stopDeepSleepTimeout() final { _timeout_state_transition.stop(); } + void startIdleTimeout() final { using namespace system::robot::sm; @@ -501,6 +512,7 @@ class RobotController : public interface::RobotController std::chrono::seconds _sleep_timeout_duration {60}; std::chrono::seconds _idle_timeout_duration {600}; + std::chrono::seconds _deep_sleep_timeout_duration {600}; interface::Timeout &_timeout_state_transition; const rtos::Kernel::Clock::time_point kSystemStartupTimestamp = rtos::Kernel::Clock::now(); diff --git a/libs/RobotKit/include/StateMachine.h b/libs/RobotKit/include/StateMachine.h index dc1b3e1066..5b8d88acc7 100644 --- a/libs/RobotKit/include/StateMachine.h +++ b/libs/RobotKit/include/StateMachine.h @@ -109,11 +109,11 @@ namespace sm::action { }; struct start_deep_sleep_timeout { - auto operator()(irc &rc) const {} + auto operator()(irc &rc) const { rc.startDeepSleepTimeout(); } }; struct stop_deep_sleep_timeout { - auto operator()(irc &rc) const {} + auto operator()(irc &rc) const { rc.stopDeepSleepTimeout(); } }; struct start_idle_timeout { diff --git a/libs/RobotKit/include/interface/RobotController.h b/libs/RobotKit/include/interface/RobotController.h index 80a4030a1a..b0977c060b 100644 --- a/libs/RobotKit/include/interface/RobotController.h +++ b/libs/RobotKit/include/interface/RobotController.h @@ -18,6 +18,9 @@ class RobotController virtual void startSleepTimeout() = 0; virtual void stopSleepTimeout() = 0; + virtual void startDeepSleepTimeout() = 0; + virtual void stopDeepSleepTimeout() = 0; + virtual void startIdleTimeout() = 0; virtual void stopIdleTimeout() = 0; diff --git a/libs/RobotKit/tests/RobotController_test.h b/libs/RobotKit/tests/RobotController_test.h index c609933baf..238d35f664 100644 --- a/libs/RobotKit/tests/RobotController_test.h +++ b/libs/RobotKit/tests/RobotController_test.h @@ -131,6 +131,7 @@ class RobotControllerTest : public testing::Test ble::GattServerMock &mbed_mock_gatt = ble::gatt_server_mock(); interface::Timeout::callback_t on_sleep_timeout = {}; + interface::Timeout::callback_t on_deep_sleep_timeout = {}; interface::Timeout::callback_t on_idle_timeout = {}; interface::Timeout::callback_t on_sleeping_start_timeout = {}; interface::Timeout::callback_t on_charging_start_timeout = {}; @@ -263,6 +264,10 @@ class RobotControllerTest : public testing::Test expectedCallsRunLaunchingBehavior(); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); diff --git a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp index 0d079ab896..9d0d35d66c 100644 --- a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp +++ b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp @@ -101,6 +101,10 @@ TEST_F(RobotControllerTest, registerEventsBatteryIsCharging) .InSequence(run_launching_behavior_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(run_launching_behavior_sequence); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); diff --git a/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp b/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp index e37abcedf6..0cabf84c82 100644 --- a/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp @@ -68,6 +68,10 @@ TEST_F(RobotControllerTest, stateAutonomousActivityEventChargeDidStartGuardIsCha EXPECT_CALL(mock_motor_left, stop).InSequence(on_autonomous_activity_exit_sequence); EXPECT_CALL(mock_motor_right, stop).InSequence(on_autonomous_activity_exit_sequence); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); diff --git a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp index fbd05945e2..613d2d84c6 100644 --- a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp @@ -40,6 +40,7 @@ TEST_F(RobotControllerTest, stateChargingConnectedEventChargeDidStopGuardIsCharg EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); Sequence on_charging_exit_sequence; + EXPECT_CALL(timeout_state_transition, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); @@ -65,6 +66,7 @@ TEST_F(RobotControllerTest, stateChargingDisconnectedEventChargeDidStopGuardIsCh EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); Sequence on_charging_exit_sequence; + EXPECT_CALL(timeout_state_transition, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); @@ -91,6 +93,7 @@ TEST_F(RobotControllerTest, stateChargingDisconnectedEventBleConnection) EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(true)); Sequence on_charging_exit_sequence; + EXPECT_CALL(timeout_state_transition, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopActuators(); @@ -104,6 +107,10 @@ TEST_F(RobotControllerTest, stateChargingDisconnectedEventBleConnection) .Times(1) .InSequence(on_ble_connection_sequence); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence on_charging_entry_sequence; EXPECT_CALL(battery, level).InSequence(on_charging_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_charging_entry_sequence); @@ -134,6 +141,7 @@ TEST_F(RobotControllerTest, stateChargingEventFileExchangeRequestedGuardIsReadyT EXPECT_CALL(battery, level).InSequence(is_ready_to_file_exchange_sequence).WillRepeatedly(Return(returned_level)); Sequence on_charging_exit_sequence; + EXPECT_CALL(timeout_state_transition, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); @@ -169,6 +177,7 @@ TEST_F(RobotControllerTest, EXPECT_CALL(battery, level).InSequence(is_ready_to_file_exchange_sequence).WillRepeatedly(Return(returned_level)); Sequence on_charging_exit_sequence; + EXPECT_CALL(timeout_state_transition, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); @@ -378,6 +387,7 @@ TEST_F(RobotControllerTest, stateChargingEventEmergencyStopDelayOver) EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(true)); Sequence on_charging_exit_sequence; + EXPECT_CALL(timeout_state_transition, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopActuators(); @@ -416,11 +426,16 @@ TEST_F(RobotControllerTest, stateChargingDiceRollDetectedDelayOverEventAutonomou auto minimal_delay_over = 1001ms; Sequence on_charging_exit_sequence; + EXPECT_CALL(timeout_state_transition, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(timeout_state_internal, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_ledkit, stop).Times(1).InSequence(on_charging_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).Times(1).InSequence(on_charging_exit_sequence); expectedCallsStopMotors(); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(battery, level); EXPECT_CALL(mock_videokit, displayImage).Times(1); EXPECT_CALL(mock_ledkit, start); @@ -434,3 +449,26 @@ TEST_F(RobotControllerTest, stateChargingDiceRollDetectedDelayOverEventAutonomou EXPECT_TRUE(rc.state_machine.is(lksm::state::charging)); } + +TEST_F(RobotControllerTest, stateChargingEventTimeout) +{ + Sequence get_on_deep_sleep_timeout_callback; + EXPECT_CALL(timeout_state_transition, onTimeout) + .InSequence(get_on_deep_sleep_timeout_callback) + .WillOnce(GetCallback(&on_deep_sleep_timeout)); + EXPECT_CALL(timeout_state_transition, start).InSequence(get_on_deep_sleep_timeout_callback); + rc.startDeepSleepTimeout(); + + rc.state_machine.set_current_states(lksm::state::charging); + + Sequence on_charging_exit_sequence; + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_charging_exit_sequence); + EXPECT_CALL(timeout_state_internal, stop).InSequence(on_charging_exit_sequence); + EXPECT_CALL(mock_ledkit, stop).InSequence(on_charging_exit_sequence); + EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_charging_exit_sequence); + expectedCallsStopMotors(); + + on_deep_sleep_timeout(); + + EXPECT_TRUE(rc.state_machine.is(lksm::state::deep_sleeping)); +} diff --git a/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp b/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp index 900cda194e..628a0c672e 100644 --- a/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp @@ -71,6 +71,10 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedEventChargeDidStartGuardIsCharg EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(true)); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); @@ -107,6 +111,10 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedConnectedEventCommandReceivedGu EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(true)); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); @@ -154,6 +162,10 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedEventBleConnectionGuardIsChargi .Times(1) .InSequence(on_ble_connection_sequence); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); @@ -217,6 +229,10 @@ TEST_F(RobotControllerTest, auto minimal_delay_over = 1001ms; + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); diff --git a/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp b/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp index 8637c06be5..4a5b0f2b6a 100644 --- a/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp @@ -18,6 +18,10 @@ TEST_F(RobotControllerTest, stateFileExchangeEventFileExchangeStopRequestedGuard // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(AnyNumber()).InSequence(on_file_exchange_end); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); @@ -73,6 +77,10 @@ TEST_F(RobotControllerTest, stateFileExchangeEventDisconnectionGuardIsCharging) expectedCallsStopActuators(); EXPECT_CALL(mock_ledkit, start).InSequence(start_disconnection_behavior); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); diff --git a/libs/RobotKit/tests/RobotController_test_stateIdle.cpp b/libs/RobotKit/tests/RobotController_test_stateIdle.cpp index d74311968e..15ac9dcb50 100644 --- a/libs/RobotKit/tests/RobotController_test_stateIdle.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateIdle.cpp @@ -13,6 +13,10 @@ TEST_F(RobotControllerTest, stateIdleEventTimeout) EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_idle_sequence); expectedCallsStopMotors(); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence on_sleeping_sequence; EXPECT_CALL(mock_ledkit, start(isSameAnimation(&led::animation::sleeping))).InSequence(on_sleeping_sequence); EXPECT_CALL(mock_videokit, playVideoOnce).InSequence(on_sleeping_sequence); @@ -87,6 +91,10 @@ TEST_F(RobotControllerTest, stateIdleEventChargeDidStartGuardIsChargingTrue) EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_idle_sequence); expectedCallsStopMotors(); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); diff --git a/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp b/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp index 6bf77bf74d..861a84df31 100644 --- a/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp @@ -9,6 +9,7 @@ TEST_F(RobotControllerTest, stateSleepingEventCommandReceived) rc.state_machine.set_current_states(lksm::state::sleeping, lksm::state::connected); Sequence on_exit_sleeping_sequence; + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); expectedCallsStopMotors(); @@ -30,6 +31,7 @@ TEST_F(RobotControllerTest, stateSleepingEventBleConnection) EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); Sequence on_exit_sleeping_sequence; + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); expectedCallsStopActuators(); @@ -57,10 +59,15 @@ TEST_F(RobotControllerTest, stateSleepingEventChargeDidStartGuardIsChargingTrue) EXPECT_CALL(battery, isCharging).WillOnce(Return(true)); Sequence on_exit_sleeping_sequence; + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); expectedCallsStopMotors(); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); @@ -110,6 +117,7 @@ TEST_F(RobotControllerTest, stateSleepingEventEmergencyStopDelayOver) auto delay_over = 11s; Sequence on_exit_sleeping_sequence; + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(mock_motor_left, stop).Times(AtLeast(1)); @@ -148,6 +156,7 @@ TEST_F(RobotControllerTest, stateSleepingDiceRollDetectedDelayOverEventAutonomou auto minimal_delay_over = 1001ms; Sequence on_exit_sleeping_sequence; + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); expectedCallsStopMotors(); @@ -159,3 +168,25 @@ TEST_F(RobotControllerTest, stateSleepingDiceRollDetectedDelayOverEventAutonomou EXPECT_TRUE(rc.state_machine.is(lksm::state::autonomous_activities)); } + +TEST_F(RobotControllerTest, stateSleepingEventTimeout) +{ + Sequence get_on_deep_sleep_timeout_callback; + EXPECT_CALL(timeout_state_transition, onTimeout) + .InSequence(get_on_deep_sleep_timeout_callback) + .WillOnce(GetCallback(&on_deep_sleep_timeout)); + EXPECT_CALL(timeout_state_transition, start).InSequence(get_on_deep_sleep_timeout_callback); + rc.startDeepSleepTimeout(); + + rc.state_machine.set_current_states(lksm::state::sleeping); + + Sequence on_exit_sleeping_sequence; + EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_sleeping_sequence); + EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); + EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); + expectedCallsStopMotors(); + + on_deep_sleep_timeout(); + + EXPECT_TRUE(rc.state_machine.is(lksm::state::deep_sleeping)); +} diff --git a/libs/RobotKit/tests/RobotController_test_stateWorking.cpp b/libs/RobotKit/tests/RobotController_test_stateWorking.cpp index 2c77b94c71..23c7e31312 100644 --- a/libs/RobotKit/tests/RobotController_test_stateWorking.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateWorking.cpp @@ -38,6 +38,10 @@ TEST_F(RobotControllerTest, stateWorkingEventChargeDidStartGuardIsChargingTrue) Sequence on_exit_working_sequence; EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_working_sequence); + Sequence start_deep_sleep_timeout_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(start_deep_sleep_timeout_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(start_deep_sleep_timeout_sequence); + Sequence start_charging_behavior_sequence; EXPECT_CALL(battery, level).InSequence(start_charging_behavior_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(start_charging_behavior_sequence); diff --git a/libs/RobotKit/tests/StateMachine_test.cpp b/libs/RobotKit/tests/StateMachine_test.cpp index 0a3f3e2bf3..df9fa07172 100644 --- a/libs/RobotKit/tests/StateMachine_test.cpp +++ b/libs/RobotKit/tests/StateMachine_test.cpp @@ -74,6 +74,7 @@ TEST_F(StateMachineTest, stateSetupEventSetupCompleteGuardIsChargingTrue) EXPECT_CALL(mock_rc, runLaunchingBehavior).Times(1); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::setup_complete {}); @@ -87,6 +88,7 @@ TEST_F(StateMachineTest, stateIdleEventTimeout) EXPECT_CALL(mock_rc, stopSleepTimeout).Times(1); EXPECT_CALL(mock_rc, stopWaitingBehavior).Times(1); EXPECT_CALL(mock_rc, startSleepingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::sleep_timeout_did_end {}); @@ -142,6 +144,7 @@ TEST_F(StateMachineTest, stateWorkingEventChargeDidStart) EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, stopIdleTimeout).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::charge_did_start {}); @@ -177,6 +180,7 @@ TEST_F(StateMachineTest, stateSleepEventCommandReceived) sm.set_current_states(lksm::state::sleeping); EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startIdleTimeout).Times(1); EXPECT_CALL(mock_rc, startWorkingBehavior).Times(1); EXPECT_CALL(mock_rc, isBleConnected).WillOnce(Return(true)); @@ -191,8 +195,10 @@ TEST_F(StateMachineTest, stateSleepEventChargeDidStart) sm.set_current_states(lksm::state::sleeping); EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, isCharging).WillOnce(Return(true)); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::charge_did_start {}); @@ -204,6 +210,7 @@ TEST_F(StateMachineTest, stateSleepEventEmergencyStop) sm.set_current_states(lksm::state::sleeping); EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, stopActuatorsAndLcd).Times(1); sm.process_event(lksm::event::emergency_stop {}); @@ -216,6 +223,7 @@ TEST_F(StateMachineTest, stateSleepEventAutonomousActivityRequested) sm.set_current_states(lksm::state::sleeping); EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startAutonomousActivityMode).Times(1); @@ -229,6 +237,7 @@ TEST_F(StateMachineTest, stateSleepEventTimeout) sm.set_current_states(lksm::state::sleeping); EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); @@ -243,6 +252,7 @@ TEST_F(StateMachineTest, stateIdleEventChargeDidStart) EXPECT_CALL(mock_rc, stopSleepTimeout).Times(1); EXPECT_CALL(mock_rc, stopWaitingBehavior).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::charge_did_start {}); @@ -269,6 +279,7 @@ TEST_F(StateMachineTest, stateChargingEventChargeDidStopBleConnected) EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(false)); EXPECT_CALL(mock_rc, isBleConnected).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startIdleTimeout).Times(1); EXPECT_CALL(mock_rc, startWorkingBehavior).Times(1); @@ -286,6 +297,7 @@ TEST_F(StateMachineTest, stateChargingEventChargeDidStopBleDisconnected) EXPECT_CALL(mock_rc, startSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startWaitingBehavior).Times(1); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); sm.process_event(lksm::event::charge_did_stop {}); @@ -297,6 +309,7 @@ TEST_F(StateMachineTest, stateChargingEventUpdateRequestedGuardTrue) sm.set_current_states(lksm::state::charging); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, isReadyToUpdate).WillOnce(Return(true)); EXPECT_CALL(mock_rc, applyUpdate).Times(1); @@ -322,6 +335,7 @@ TEST_F(StateMachineTest, stateChargingEventEmergencyStop) sm.set_current_states(lksm::state::charging); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, stopActuatorsAndLcd).Times(1); sm.process_event(lksm::event::emergency_stop {}); @@ -334,7 +348,9 @@ TEST_F(StateMachineTest, stateChargingEventAutonomousActivityRequested) sm.set_current_states(lksm::state::charging); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startAutonomousActivityMode).Times(0); sm.process_event(lksm::event::autonomous_activities_mode_requested {}); @@ -347,6 +363,7 @@ TEST_F(StateMachineTest, stateChargingEventTimeout) sm.set_current_states(lksm::state::charging); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); @@ -358,6 +375,7 @@ TEST_F(StateMachineTest, stateSleepingEventBleConnection) sm.set_current_states(lksm::state::sleeping, lksm::state::disconnected); EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startConnectionBehavior).Times(1); EXPECT_CALL(mock_rc, startWorkingBehavior).Times(1); EXPECT_CALL(mock_rc, startIdleTimeout).Times(1); @@ -409,7 +427,9 @@ TEST_F(StateMachineTest, stateChargingEventBleConnection) sm.set_current_states(lksm::state::charging, lksm::state::disconnected); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, startConnectionBehavior).Times(1); @@ -423,7 +443,9 @@ TEST_F(StateMachineTest, stateChargingEventBleDisconnection) sm.set_current_states(lksm::state::charging, lksm::state::connected); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startDisconnectionBehavior).Times(1); sm.process_event(lksm::event::ble_disconnection {}); @@ -436,7 +458,9 @@ TEST_F(StateMachineTest, stateChargingEventCommandReceived) sm.set_current_states(lksm::state::charging); EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::command_received {}); @@ -522,6 +546,7 @@ TEST_F(StateMachineTest, stateEmergencyStoppedEventChargeDidStartGuardIsCharging EXPECT_CALL(mock_rc, resetEmergencyStopCounter).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::charge_did_start {}); @@ -537,6 +562,7 @@ TEST_F(StateMachineTest, stateEmergencyStoppedEventCommandReceivedGuardIsChargin EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::command_received {}); @@ -563,6 +589,7 @@ TEST_F(StateMachineTest, stateEmergencyStoppedEventBleConnectionGuardIsCharging) EXPECT_CALL(mock_rc, resetEmergencyStopCounter).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); EXPECT_CALL(mock_rc, startConnectionBehavior).Times(1); sm.process_event(lksm::event::ble_connection {}); @@ -578,6 +605,7 @@ TEST_F(StateMachineTest, stateEmergencyStoppedEventAutonomousActivityRequestedGu EXPECT_CALL(mock_rc, resetEmergencyStopCounter).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::autonomous_activities_mode_requested {}); @@ -645,6 +673,7 @@ TEST_F(StateMachineTest, stateAutonomousActivityEventChargeDidStartGuardIsChargi EXPECT_CALL(mock_rc, stopAutonomousActivityMode).Times(1); EXPECT_CALL(mock_rc, startChargingBehavior).Times(1); + EXPECT_CALL(mock_rc, startDeepSleepTimeout).Times(1); sm.process_event(lksm::event::charge_did_start {}); @@ -698,6 +727,7 @@ TEST_F(StateMachineTest, stateChargingEventFileExchangeRequestedGuardTrue) sm.set_current_states(lksm::state::charging); EXPECT_CALL(mock_rc, stopChargingBehavior); + EXPECT_CALL(mock_rc, stopDeepSleepTimeout); EXPECT_CALL(mock_rc, isReadyToFileExchange).WillOnce(Return(true)); EXPECT_CALL(mock_rc, startFileExchange); @@ -727,6 +757,7 @@ TEST_F(StateMachineTest, stateFileExhangeEventFileExchangeStopRequestedGuardIsCh EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, startChargingBehavior); + EXPECT_CALL(mock_rc, startDeepSleepTimeout); sm.process_event(lksm::event::file_exchange_stop_requested {}); @@ -758,6 +789,7 @@ TEST_F(StateMachineTest, stateFileExhangeEventBleDisconnectionGuardIsCharging) EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, startChargingBehavior); + EXPECT_CALL(mock_rc, startDeepSleepTimeout); sm.process_event(lksm::event::ble_disconnection {}); diff --git a/libs/RobotKit/tests/mocks/RobotController.h b/libs/RobotKit/tests/mocks/RobotController.h index bd88dda5b1..a5c0601661 100644 --- a/libs/RobotKit/tests/mocks/RobotController.h +++ b/libs/RobotKit/tests/mocks/RobotController.h @@ -15,6 +15,10 @@ struct RobotController : public interface::RobotController { MOCK_METHOD(void, startSleepTimeout, (), (override)); MOCK_METHOD(void, stopSleepTimeout, (), (override)); + + MOCK_METHOD(void, startDeepSleepTimeout, (), (override)); + MOCK_METHOD(void, stopDeepSleepTimeout, (), (override)); + MOCK_METHOD(void, startIdleTimeout, (), (override)); MOCK_METHOD(void, stopIdleTimeout, (), (override)); From ec6b3703ddcd22ce940807603c6bd9b20ec20b32 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 2 Jan 2023 13:46:58 +0100 Subject: [PATCH 013/143] :sparkles: (rc): Add turnOffHardware implementation --- libs/RobotKit/include/RobotController.h | 2 ++ libs/RobotKit/include/StateMachine.h | 2 +- libs/RobotKit/include/interface/RobotController.h | 4 +++- libs/RobotKit/tests/StateMachine_test.cpp | 2 ++ libs/RobotKit/tests/mocks/RobotController.h | 2 ++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index 4b37bbb174..b945060eae 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -305,6 +305,8 @@ class RobotController : public interface::RobotController stopActuators(); } + void suspendHardwareForDeepSleep() final { log_info("TO IMPLEMENT - configuring hardware for deep sleep"); } + void resetEmergencyStopCounter() final { _emergency_stop_counter = 0; } void raise(auto event) diff --git a/libs/RobotKit/include/StateMachine.h b/libs/RobotKit/include/StateMachine.h index 5b8d88acc7..10c03c75bc 100644 --- a/libs/RobotKit/include/StateMachine.h +++ b/libs/RobotKit/include/StateMachine.h @@ -177,7 +177,7 @@ namespace sm::action { }; struct suspend_hardware_for_deep_sleep { - auto operator()(irc &rc) const {} + auto operator()(irc &rc) const { rc.suspendHardwareForDeepSleep(); } }; struct reset_emergency_stop_counter { diff --git a/libs/RobotKit/include/interface/RobotController.h b/libs/RobotKit/include/interface/RobotController.h index b0977c060b..ef4262a7e0 100644 --- a/libs/RobotKit/include/interface/RobotController.h +++ b/libs/RobotKit/include/interface/RobotController.h @@ -51,7 +51,9 @@ class RobotController virtual auto isReadyToUpdate() -> bool = 0; virtual void applyUpdate() = 0; - virtual void stopActuatorsAndLcd() = 0; + virtual void stopActuatorsAndLcd() = 0; + virtual void suspendHardwareForDeepSleep() = 0; + virtual void resetEmergencyStopCounter() = 0; }; diff --git a/libs/RobotKit/tests/StateMachine_test.cpp b/libs/RobotKit/tests/StateMachine_test.cpp index df9fa07172..93ae2b7c4c 100644 --- a/libs/RobotKit/tests/StateMachine_test.cpp +++ b/libs/RobotKit/tests/StateMachine_test.cpp @@ -238,6 +238,7 @@ TEST_F(StateMachineTest, stateSleepEventTimeout) EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); + EXPECT_CALL(mock_rc, suspendHardwareForDeepSleep).Times(1); sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); @@ -364,6 +365,7 @@ TEST_F(StateMachineTest, stateChargingEventTimeout) EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); + EXPECT_CALL(mock_rc, suspendHardwareForDeepSleep).Times(1); sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); diff --git a/libs/RobotKit/tests/mocks/RobotController.h b/libs/RobotKit/tests/mocks/RobotController.h index a5c0601661..17ceea4333 100644 --- a/libs/RobotKit/tests/mocks/RobotController.h +++ b/libs/RobotKit/tests/mocks/RobotController.h @@ -50,6 +50,8 @@ struct RobotController : public interface::RobotController { MOCK_METHOD(void, applyUpdate, (), (override)); MOCK_METHOD(void, stopActuatorsAndLcd, (), (override)); + MOCK_METHOD(void, suspendHardwareForDeepSleep, (), (override)); + MOCK_METHOD(void, resetEmergencyStopCounter, (), (override)); }; From f32cafbeb3c50c02b31684453b9d8122dd8f1b32 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Thu, 12 Jan 2023 17:24:47 +0100 Subject: [PATCH 014/143] :sparkles: (eventqueue): Add cancel --- drivers/CoreEventQueue/include/CoreEventQueue.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/drivers/CoreEventQueue/include/CoreEventQueue.h b/drivers/CoreEventQueue/include/CoreEventQueue.h index 0c205619c4..f906c18e5a 100644 --- a/drivers/CoreEventQueue/include/CoreEventQueue.h +++ b/drivers/CoreEventQueue/include/CoreEventQueue.h @@ -18,13 +18,15 @@ class CoreEventQueue : public interface::EventQueue void dispatch_forever() final; - void call(auto f, auto... params) { _event_queue.call(f, params...); } + auto call(auto f, auto... params) -> int { return _event_queue.call(f, params...); } - void call_every(std::chrono::duration duration, auto f, auto... params) + auto call_every(std::chrono::duration duration, auto f, auto... params) -> int { - _event_queue.call_every(duration, f, params...); + return _event_queue.call_every(duration, f, params...); } + void cancel(int id) { _event_queue.cancel(id); } + // ? Overload needed for mbed::BLE compatibility void callMbedCallback(mbed::Callback const &f); From 10941eb96a0ef055d45cb34fd10971e6f42e1e7c Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 15:16:17 +0100 Subject: [PATCH 015/143] :white_check_mark: (coretouchsensor): Fix uninteresting mock function call --- drivers/CoreTouchSensor/tests/CoreTouchSensor_test.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/CoreTouchSensor/tests/CoreTouchSensor_test.cpp b/drivers/CoreTouchSensor/tests/CoreTouchSensor_test.cpp index 9d4d0652f7..55c0849ad2 100644 --- a/drivers/CoreTouchSensor/tests/CoreTouchSensor_test.cpp +++ b/drivers/CoreTouchSensor/tests/CoreTouchSensor_test.cpp @@ -47,6 +47,8 @@ TEST_F(CoreTouchSensorTest, initializationDefault) TEST_F(CoreTouchSensorTest, init) { EXPECT_CALL(dac, init).Times(1); + EXPECT_CALL(mockIOExpander, setModeForPin).Times(1); + EXPECT_CALL(mockIOExpander, writePin).Times(1); sensor.init(); } From 45887cd38ad2f12aa228ae435be8a0837602aa45 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 15:10:11 +0100 Subject: [PATCH 016/143] :white_check_mark: (corerfid): Fix uninteresting mock function call --- drivers/CoreRFIDReader/tests/CoreRFIDReaderCR95HF_test.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/CoreRFIDReader/tests/CoreRFIDReaderCR95HF_test.cpp b/drivers/CoreRFIDReader/tests/CoreRFIDReaderCR95HF_test.cpp index ab994007b5..b49da4c971 100644 --- a/drivers/CoreRFIDReader/tests/CoreRFIDReaderCR95HF_test.cpp +++ b/drivers/CoreRFIDReader/tests/CoreRFIDReaderCR95HF_test.cpp @@ -108,6 +108,7 @@ TEST_F(CoreRFIDReaderTest, setCommunicationProtocolSuccess) sendSetGainAndModulation(); } + EXPECT_CALL(callback_detected, Call); callback_sigio(); reader.setCommunicationProtocol(rfid::Protocol::ISO14443A); } @@ -122,6 +123,7 @@ TEST_F(CoreRFIDReaderTest, setCommunicationProtocolFailedOnWrongFirstValue) sendSetGainAndModulation(); } + EXPECT_CALL(callback_detected, Call); callback_sigio(); reader.setCommunicationProtocol(rfid::Protocol::ISO14443A); } @@ -146,6 +148,7 @@ TEST_F(CoreRFIDReaderTest, receiveDataSuccess) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCA, 0x6C}; receiveRFIDReaderAnswer(read_values); + EXPECT_CALL(callback_detected, Call); callback_sigio(); auto tag = reader.getTag(); @@ -162,6 +165,7 @@ TEST_F(CoreRFIDReaderTest, receiveDataFailedWrongAnswerFlag) receiveRFIDReaderAnswer(read_values); + EXPECT_CALL(callback_detected, Call); callback_sigio(); auto tag = reader.getTag(); @@ -177,6 +181,7 @@ TEST_F(CoreRFIDReaderTest, receiveDataFailedWrongLength) receiveRFIDReaderAnswer(read_values); + EXPECT_CALL(callback_detected, Call); callback_sigio(); auto tag = reader.getTag(); @@ -197,8 +202,10 @@ TEST_F(CoreRFIDReaderTest, getTag) receiveRFIDReaderAnswer(read_values); + EXPECT_CALL(callback_detected, Call); callback_sigio(); + EXPECT_CALL(callback_readable, Call); reader.onTagReadable(); auto tag = reader.getTag(); From 0a14ea98f23e457c11915f2a22c64c8b65f3635d Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 14:51:27 +0100 Subject: [PATCH 017/143] :white_check_mark: (rfidkit): Fix uninteresting mock function call --- libs/RFIDKit/tests/RFIDKit_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/RFIDKit/tests/RFIDKit_test.cpp b/libs/RFIDKit/tests/RFIDKit_test.cpp index 45bc13e900..6a4eada13e 100644 --- a/libs/RFIDKit/tests/RFIDKit_test.cpp +++ b/libs/RFIDKit/tests/RFIDKit_test.cpp @@ -176,6 +176,7 @@ TEST_F(RFIDKitTest, getLastMagicCardActivated) rfid_kit.registerMagicCard(); + EXPECT_CALL(mock_callback, Call(MagicCard::emergency_stop)); magic_card_callback(tag); EXPECT_EQ(rfid_kit.getLastMagicCardActivated(), MagicCard::emergency_stop); From 7b7ecca50e1a44469e272f826dc7f70696861d00 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 15:28:09 +0100 Subject: [PATCH 018/143] :white_check_mark: (reinforcerkit): Fix uninteresting mock function call --- libs/ReinforcerKit/tests/ReinforcerKit_test.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp b/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp index 7a0a1528d6..181153866d 100644 --- a/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp +++ b/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp @@ -19,6 +19,7 @@ using namespace leka; using ::testing::AnyNumber; +using ::testing::AtMost; using ::testing::Sequence; MATCHER_P(isSameAnimation, expected_animation, "") @@ -63,6 +64,9 @@ class ReinforcerkitTest : public ::testing::Test EXPECT_CALL(mock_motor_right, stop).Times(1); EXPECT_CALL(mock_motor_left, spin).Times(1); EXPECT_CALL(mock_motor_right, spin).Times(1); + EXPECT_CALL(mock_timeout, stop).Times(AtMost(1)); + EXPECT_CALL(mock_timeout, onTimeout).Times(AtMost(1)); + EXPECT_CALL(mock_timeout, start).Times(AtMost(1)); EXPECT_CALL(mock_ledkit, start(isSameAnimation(animation))); } @@ -163,6 +167,7 @@ TEST_F(ReinforcerkitTest, stop) EXPECT_CALL(mock_videokit, stopVideo); EXPECT_CALL(mock_motor_left, stop); EXPECT_CALL(mock_motor_right, stop); + EXPECT_CALL(mock_timeout, stop); reinforcerkit.stop(); } From b3656ae98eadcf192dfdcf64fb8470a7019a233e Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 15:19:03 +0100 Subject: [PATCH 019/143] :white_check_mark: (activitykit): Fix uninteresting mock function call --- libs/ActivityKit/tests/ActivityKit_test.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/ActivityKit/tests/ActivityKit_test.cpp b/libs/ActivityKit/tests/ActivityKit_test.cpp index c294556bf8..14abc9905e 100644 --- a/libs/ActivityKit/tests/ActivityKit_test.cpp +++ b/libs/ActivityKit/tests/ActivityKit_test.cpp @@ -10,6 +10,7 @@ using namespace leka; +using ::testing::AnyNumber; using ::testing::InSequence; class ActivityKitTest : public ::testing::Test @@ -99,6 +100,7 @@ TEST_F(ActivityKitTest, isPlayingActivityNullPtr) TEST_F(ActivityKitTest, isPlayingActivityStarted) { + EXPECT_CALL(mock_activity_0, start); activitykit.start(MagicCard::number_0); EXPECT_TRUE(activitykit.isPlaying()); @@ -106,7 +108,10 @@ TEST_F(ActivityKitTest, isPlayingActivityStarted) TEST_F(ActivityKitTest, isPlayingActivityStopped) { + EXPECT_CALL(mock_activity_0, start); activitykit.start(MagicCard::number_0); + + EXPECT_CALL(mock_activity_0, stop); activitykit.stop(); EXPECT_FALSE(activitykit.isPlaying()); From 5c676a8c1e86726f8b6e2ff197ab4ce63f85677f Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 15:21:10 +0100 Subject: [PATCH 020/143] :white_check_mark: (behaviorkit): Fix uninteresting mock function call --- libs/BehaviorKit/tests/BehaviorKit_test.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/BehaviorKit/tests/BehaviorKit_test.cpp b/libs/BehaviorKit/tests/BehaviorKit_test.cpp index 002d28a550..76ac9973d2 100644 --- a/libs/BehaviorKit/tests/BehaviorKit_test.cpp +++ b/libs/BehaviorKit/tests/BehaviorKit_test.cpp @@ -80,6 +80,7 @@ TEST_F(BehaviorKitTest, sleeping) TEST_F(BehaviorKitTest, waiting) { + EXPECT_CALL(mock_ledkit, stop); EXPECT_CALL(mock_videokit, playVideoOnRepeat); behaviorkit.waiting(); } @@ -87,6 +88,7 @@ TEST_F(BehaviorKitTest, waiting) TEST_F(BehaviorKitTest, batteryBehaviors) { EXPECT_CALL(mock_videokit, displayImage).Times(6); + EXPECT_CALL(mock_ledkit, stop); EXPECT_CALL(mock_motor_left, stop()).Times(1); EXPECT_CALL(mock_motor_right, stop()).Times(1); From 92bed9973ba9f998cdb83ec172f923a935be368f Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 15:40:57 +0100 Subject: [PATCH 021/143] :white_check_mark: (sm): Fix uninteresting mock function call --- libs/RobotKit/tests/StateMachine_test.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/RobotKit/tests/StateMachine_test.cpp b/libs/RobotKit/tests/StateMachine_test.cpp index 93ae2b7c4c..b682d67334 100644 --- a/libs/RobotKit/tests/StateMachine_test.cpp +++ b/libs/RobotKit/tests/StateMachine_test.cpp @@ -575,6 +575,7 @@ TEST_F(StateMachineTest, stateEmergencyStoppedEventCommandReceivedGuardIsChargin { sm.set_current_states(lksm::state::emergency_stopped, lksm::state::disconnected); + EXPECT_CALL(mock_rc, isBleConnected).WillRepeatedly(Return(false)); EXPECT_CALL(mock_rc, isCharging).WillRepeatedly(Return(true)); EXPECT_CALL(mock_rc, isBleConnected).WillRepeatedly(Return(false)); @@ -701,6 +702,8 @@ TEST_F(StateMachineTest, stateAutonomousActivityEventAutonomousActivityExitedDis EXPECT_CALL(mock_rc, isBleConnected).WillRepeatedly(Return(false)); EXPECT_CALL(mock_rc, stopAutonomousActivityMode).Times(1); + + EXPECT_CALL(mock_rc, isBleConnected).WillRepeatedly(Return(false)); EXPECT_CALL(mock_rc, startWaitingBehavior).Times(1); EXPECT_CALL(mock_rc, startSleepTimeout).Times(1); From 32e6d8a98cb7747353c7fef8e4dc3b8309c9676e Mon Sep 17 00:00:00 2001 From: Mourad Latoundji Date: Sun, 27 Nov 2022 23:25:31 +0100 Subject: [PATCH 022/143] :sparkles: (libs): Add TouchSensorKit Co-Authored-By: YannL --- libs/CMakeLists.txt | 1 + libs/TouchSensorKit/CMakeLists.txt | 27 +++ libs/TouchSensorKit/include/Position.h | 21 +++ libs/TouchSensorKit/include/TouchSensorKit.h | 62 ++++++ libs/TouchSensorKit/source/TouchSensorKit.cpp | 128 +++++++++++++ .../tests/TouchSensorKit_test.cpp | 176 ++++++++++++++++++ tests/unit/CMakeLists.txt | 1 + 7 files changed, 416 insertions(+) create mode 100644 libs/TouchSensorKit/CMakeLists.txt create mode 100644 libs/TouchSensorKit/include/Position.h create mode 100644 libs/TouchSensorKit/include/TouchSensorKit.h create mode 100644 libs/TouchSensorKit/source/TouchSensorKit.cpp create mode 100644 libs/TouchSensorKit/tests/TouchSensorKit_test.cpp diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index b65576532d..2a0a9a1662 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory(${LIBS_DIR}/ReinforcerKit) add_subdirectory(${LIBS_DIR}/RobotKit) add_subdirectory(${LIBS_DIR}/RFIDKit) add_subdirectory(${LIBS_DIR}/SerialNumberKit) +add_subdirectory(${LIBS_DIR}/TouchSensorKit) add_subdirectory(${LIBS_DIR}/UIAnimationKit) add_subdirectory(${LIBS_DIR}/VideoKit) add_subdirectory(${LIBS_DIR}/WebKit) diff --git a/libs/TouchSensorKit/CMakeLists.txt b/libs/TouchSensorKit/CMakeLists.txt new file mode 100644 index 0000000000..f32174b5ab --- /dev/null +++ b/libs/TouchSensorKit/CMakeLists.txt @@ -0,0 +1,27 @@ +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +add_library(TouchSensorKit STATIC) + +target_include_directories(TouchSensorKit + PUBLIC + include +) + +target_sources(TouchSensorKit + PRIVATE + source/TouchSensorKit.cpp +) + +target_link_libraries(TouchSensorKit + mbed-os + CoreEventQueue + CoreTouchSensor +) + +if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") + leka_unit_tests_sources( + tests/TouchSensorKit_test.cpp + ) +endif() diff --git a/libs/TouchSensorKit/include/Position.h b/libs/TouchSensorKit/include/Position.h new file mode 100644 index 0000000000..dda4b38b52 --- /dev/null +++ b/libs/TouchSensorKit/include/Position.h @@ -0,0 +1,21 @@ +// Leka - LekaOS +// Copyright 2022 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +namespace leka { + +enum class Position : uint8_t +{ + ear_left = 0, + ear_right = 1, + belt_left_back = 2, + belt_left_front = 3, + belt_right_back = 4, + belt_right_front = 5, +}; + +} diff --git a/libs/TouchSensorKit/include/TouchSensorKit.h b/libs/TouchSensorKit/include/TouchSensorKit.h new file mode 100644 index 0000000000..dbcb7c5a93 --- /dev/null +++ b/libs/TouchSensorKit/include/TouchSensorKit.h @@ -0,0 +1,62 @@ +// Leka - LekaOS +// Copyright 2022 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "CoreEventQueue.h" +#include "Position.h" +#include "interface/drivers/TouchSensor.h" + +namespace leka { + +class TouchSensorKit +{ + public: + explicit TouchSensorKit(interface::TouchSensor &ear_left, interface::TouchSensor &ear_right, + interface::TouchSensor &belt_left_back, interface::TouchSensor &belt_left_front, + interface::TouchSensor &belt_right_back, interface::TouchSensor &belt_right_front) + : _ear_left(ear_left), + _ear_right(ear_right), + _belt_left_back(belt_left_back), + _belt_left_front(belt_left_front), + _belt_right_back(belt_right_back), + _belt_right_front(belt_right_front) {}; + + void initialize(); + + void setRefreshDelay(std::chrono::milliseconds delay); + void enable(); + void disable(); + + void registerOnSensorTouched(std::function const &on_sensor_touched_callback); + void registerOnSensorReleased(std::function const &on_sensor_released_callback); + + auto isTouched(Position position) -> bool; + + private: + void run(); + + void setSensitivity(Position position, float value); + + CoreEventQueue _event_queue {}; + std::chrono::milliseconds _refresh_delay {100}; + int _event_id {}; + + interface::TouchSensor &_ear_left; + interface::TouchSensor &_ear_right; + interface::TouchSensor &_belt_left_back; + interface::TouchSensor &_belt_left_front; + interface::TouchSensor &_belt_right_back; + interface::TouchSensor &_belt_right_front; + + static constexpr auto default_max_sensitivity_value = float {1.F}; + + std::map _previous_is_touched {}; + + std::function _on_sensor_touched_callback {}; + std::function _on_sensor_released_callback {}; +}; +} // namespace leka diff --git a/libs/TouchSensorKit/source/TouchSensorKit.cpp b/libs/TouchSensorKit/source/TouchSensorKit.cpp new file mode 100644 index 0000000000..f045c89795 --- /dev/null +++ b/libs/TouchSensorKit/source/TouchSensorKit.cpp @@ -0,0 +1,128 @@ +// Leka - LekaOS +// Copyright 2022 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "TouchSensorKit.h" +#include + +#include "rtos/ThisThread.h" + +using namespace leka; +using namespace std::chrono_literals; + +void TouchSensorKit::initialize() +{ + _ear_left.init(); + _ear_right.init(); + _belt_left_back.init(); + _belt_left_front.init(); + _belt_right_back.init(); + _belt_right_front.init(); + + setSensitivity(Position::ear_left, default_max_sensitivity_value); + setSensitivity(Position::ear_right, default_max_sensitivity_value); + setSensitivity(Position::belt_left_back, default_max_sensitivity_value); + setSensitivity(Position::belt_left_front, default_max_sensitivity_value); + setSensitivity(Position::belt_right_back, default_max_sensitivity_value); + setSensitivity(Position::belt_right_front, default_max_sensitivity_value); + + _event_queue.dispatch_forever(); +} + +void TouchSensorKit::setRefreshDelay(std::chrono::milliseconds delay) +{ + _refresh_delay = delay; +} + +void TouchSensorKit::enable() +{ + disable(); + _event_id = _event_queue.call_every(_refresh_delay, [this] { run(); }); +} + +void TouchSensorKit::disable() +{ + _event_queue.cancel(_event_id); +} + +void TouchSensorKit::run() +{ + auto constexpr positions = + std::to_array({Position::ear_left, Position::ear_right, Position::belt_left_back, + Position::belt_left_front, Position::belt_right_back, Position::belt_right_front}); + + for (Position position: positions) { + auto is_touched = isTouched(position); + if (is_touched && !_previous_is_touched[position] && _on_sensor_touched_callback != nullptr) { + _on_sensor_touched_callback(position); + } + if (!is_touched && _previous_is_touched[position] && _on_sensor_released_callback != nullptr) { + _on_sensor_released_callback(position); + } + _previous_is_touched[position] = is_touched; + } +} + +void TouchSensorKit::registerOnSensorTouched(std::function const &on_sensor_touched_callback) +{ + _on_sensor_touched_callback = on_sensor_touched_callback; +} + +void TouchSensorKit::registerOnSensorReleased(std::function const &on_sensor_released_callback) +{ + _on_sensor_released_callback = on_sensor_released_callback; +} + +auto TouchSensorKit::isTouched(Position position) -> bool +{ + auto read = bool {}; + switch (position) { + case Position::ear_left: + read = _ear_left.read(); + break; + case Position::ear_right: + read = _ear_right.read(); + break; + case Position::belt_left_back: + read = _belt_left_back.read(); + break; + case Position::belt_left_front: + read = _belt_left_front.read(); + break; + case Position::belt_right_back: + read = _belt_right_back.read(); + break; + case Position::belt_right_front: + read = _belt_right_front.read(); + break; + default: + break; + } + return read; +} + +void TouchSensorKit::setSensitivity(Position position, float value) +{ + switch (position) { + case Position::ear_left: + _ear_left.setSensitivity(value); + break; + case Position::ear_right: + _ear_right.setSensitivity(value); + break; + case Position::belt_left_back: + _belt_left_back.setSensitivity(value); + break; + case Position::belt_left_front: + _belt_left_front.setSensitivity(value); + break; + case Position::belt_right_back: + _belt_right_back.setSensitivity(value); + break; + case Position::belt_right_front: + _belt_right_front.setSensitivity(value); + break; + default: + break; + } +} diff --git a/libs/TouchSensorKit/tests/TouchSensorKit_test.cpp b/libs/TouchSensorKit/tests/TouchSensorKit_test.cpp new file mode 100644 index 0000000000..d5b77fc775 --- /dev/null +++ b/libs/TouchSensorKit/tests/TouchSensorKit_test.cpp @@ -0,0 +1,176 @@ +// Leka - LekaOS +// Copyright 2022 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "TouchSensorKit.h" + +#include "gtest/gtest.h" +#include "mocks/leka/CoreTouchSensor.h" +#include "mocks/leka/EventQueue.h" + +using namespace leka; + +using ::testing::MockFunction; +using ::testing::Return; + +class TouchSensorKitTest : public ::testing::Test +{ + protected: + TouchSensorKitTest() = default; + + void SetUp() override + { + expectedCallsInitialize(); + touch_sensor_kit.initialize(); + } + // void TearDown() override {} + + mock::CoreTouchSensor mock_ear_left; + mock::CoreTouchSensor mock_ear_right; + mock::CoreTouchSensor mock_belt_left_back; + mock::CoreTouchSensor mock_belt_left_front; + mock::CoreTouchSensor mock_belt_right_back; + mock::CoreTouchSensor mock_belt_right_front; + + mock::EventQueue event_queue {}; + + TouchSensorKit touch_sensor_kit {mock_ear_left, mock_ear_right, mock_belt_left_back, + mock_belt_left_front, mock_belt_right_back, mock_belt_right_front}; + + void expectedCallsInitialize() + { + EXPECT_CALL(mock_ear_left, init).Times(1); + EXPECT_CALL(mock_ear_right, init).Times(1); + EXPECT_CALL(mock_belt_left_front, init).Times(1); + EXPECT_CALL(mock_belt_left_back, init).Times(1); + EXPECT_CALL(mock_belt_right_front, init).Times(1); + EXPECT_CALL(mock_belt_right_back, init).Times(1); + + EXPECT_CALL(mock_ear_left, setSensitivity).Times(1); + EXPECT_CALL(mock_ear_right, setSensitivity).Times(1); + EXPECT_CALL(mock_belt_left_front, setSensitivity).Times(1); + EXPECT_CALL(mock_belt_left_back, setSensitivity).Times(1); + EXPECT_CALL(mock_belt_right_front, setSensitivity).Times(1); + EXPECT_CALL(mock_belt_right_back, setSensitivity).Times(1); + } +}; + +TEST_F(TouchSensorKitTest, initializationDefault) +{ + ASSERT_NE(&touch_sensor_kit, nullptr); +} + +TEST_F(TouchSensorKitTest, initialize) +{ + EXPECT_CALL(mock_ear_left, init).Times(1); + EXPECT_CALL(mock_ear_right, init).Times(1); + EXPECT_CALL(mock_belt_left_front, init).Times(1); + EXPECT_CALL(mock_belt_left_back, init).Times(1); + EXPECT_CALL(mock_belt_right_front, init).Times(1); + EXPECT_CALL(mock_belt_right_back, init).Times(1); + + EXPECT_CALL(mock_ear_left, setSensitivity).Times(1); + EXPECT_CALL(mock_ear_right, setSensitivity).Times(1); + EXPECT_CALL(mock_belt_left_front, setSensitivity).Times(1); + EXPECT_CALL(mock_belt_left_back, setSensitivity).Times(1); + EXPECT_CALL(mock_belt_right_front, setSensitivity).Times(1); + EXPECT_CALL(mock_belt_right_back, setSensitivity).Times(1); + + touch_sensor_kit.initialize(); +} + +TEST_F(TouchSensorKitTest, setRefreshDelay) +{ + touch_sensor_kit.setRefreshDelay(std::chrono::milliseconds {100}); + + // nothing expected +} + +TEST_F(TouchSensorKitTest, enable) +{ + EXPECT_CALL(mock_ear_left, read).Times(1); + EXPECT_CALL(mock_ear_right, read).Times(1); + EXPECT_CALL(mock_belt_left_front, read).Times(1); + EXPECT_CALL(mock_belt_left_back, read).Times(1); + EXPECT_CALL(mock_belt_right_front, read).Times(1); + EXPECT_CALL(mock_belt_right_back, read).Times(1); + + touch_sensor_kit.enable(); +} + +TEST_F(TouchSensorKitTest, disable) +{ + touch_sensor_kit.disable(); + + // nothing expected +} + +TEST_F(TouchSensorKitTest, registerOnSensorTouched) +{ + MockFunction mock_callback; + touch_sensor_kit.registerOnSensorTouched(mock_callback.AsStdFunction()); + + EXPECT_CALL(mock_ear_left, read).WillOnce(Return(false)); + EXPECT_CALL(mock_ear_right, read).WillOnce(Return(false)); + EXPECT_CALL(mock_belt_left_front, read).WillOnce(Return(false)); + EXPECT_CALL(mock_belt_left_back, read).WillOnce(Return(false)); + EXPECT_CALL(mock_belt_right_front, read).WillOnce(Return(false)); + EXPECT_CALL(mock_belt_right_back, read).WillOnce(Return(false)); + + touch_sensor_kit.enable(); + + EXPECT_CALL(mock_ear_left, read).WillOnce(Return(true)); + EXPECT_CALL(mock_callback, Call(Position::ear_left)).Times(1); + + EXPECT_CALL(mock_ear_right, read).WillOnce(Return(false)); + EXPECT_CALL(mock_callback, Call(Position::ear_right)).Times(0); + + EXPECT_CALL(mock_belt_left_front, read).WillOnce(Return(true)); + EXPECT_CALL(mock_callback, Call(Position::belt_left_front)).Times(1); + + EXPECT_CALL(mock_belt_left_back, read).WillOnce(Return(false)); + EXPECT_CALL(mock_callback, Call(Position::belt_left_back)).Times(0); + + EXPECT_CALL(mock_belt_right_front, read).WillOnce(Return(true)); + EXPECT_CALL(mock_callback, Call(Position::belt_right_front)).Times(1); + + EXPECT_CALL(mock_belt_right_back, read).WillOnce(Return(false)); + EXPECT_CALL(mock_callback, Call(Position::belt_right_back)).Times(0); + + touch_sensor_kit.enable(); +} + +TEST_F(TouchSensorKitTest, registerOnSensorReleased) +{ + MockFunction mock_callback; + touch_sensor_kit.registerOnSensorReleased(mock_callback.AsStdFunction()); + + EXPECT_CALL(mock_ear_left, read).WillOnce(Return(true)); + EXPECT_CALL(mock_ear_right, read).WillOnce(Return(true)); + EXPECT_CALL(mock_belt_left_front, read).WillOnce(Return(true)); + EXPECT_CALL(mock_belt_left_back, read).WillOnce(Return(true)); + EXPECT_CALL(mock_belt_right_front, read).WillOnce(Return(true)); + EXPECT_CALL(mock_belt_right_back, read).WillOnce(Return(true)); + + touch_sensor_kit.enable(); + + EXPECT_CALL(mock_ear_left, read).WillOnce(Return(true)); + EXPECT_CALL(mock_callback, Call(Position::ear_left)).Times(0); + + EXPECT_CALL(mock_ear_right, read).WillOnce(Return(true)); + EXPECT_CALL(mock_callback, Call(Position::ear_right)).Times(0); + + EXPECT_CALL(mock_belt_left_front, read).WillOnce(Return(false)); + EXPECT_CALL(mock_callback, Call(Position::belt_left_front)).Times(1); + + EXPECT_CALL(mock_belt_left_back, read).WillOnce(Return(true)); + EXPECT_CALL(mock_callback, Call(Position::belt_left_back)).Times(0); + + EXPECT_CALL(mock_belt_right_front, read).WillOnce(Return(false)); + EXPECT_CALL(mock_callback, Call(Position::belt_right_front)).Times(1); + + EXPECT_CALL(mock_belt_right_back, read).WillOnce(Return(false)); + EXPECT_CALL(mock_callback, Call(Position::belt_right_back)).Times(1); + + touch_sensor_kit.enable(); +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 8e785e109b..8ac438c4e7 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -298,6 +298,7 @@ leka_register_unit_tests_for_library(ReinforcerKit) leka_register_unit_tests_for_library(RobotKit) leka_register_unit_tests_for_library(RFIDKit) leka_register_unit_tests_for_library(SerialNumberKit) +leka_register_unit_tests_for_library(TouchSensorKit) leka_register_unit_tests_for_library(UIAnimationKit) leka_register_unit_tests_for_library(VideoKit) From dc052060e0fc3e5a1de309e1084617c02edecac8 Mon Sep 17 00:00:00 2001 From: Mourad Latoundji Date: Thu, 24 Nov 2022 16:28:36 +0100 Subject: [PATCH 023/143] :sparkles: (spikes): Add lk_touch_sensor_kit Co-Authored-By: YannL --- spikes/CMakeLists.txt | 2 + spikes/lk_touch_sensor_kit/CMakeLists.txt | 29 ++++ spikes/lk_touch_sensor_kit/main.cpp | 174 ++++++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 spikes/lk_touch_sensor_kit/CMakeLists.txt create mode 100644 spikes/lk_touch_sensor_kit/main.cpp diff --git a/spikes/CMakeLists.txt b/spikes/CMakeLists.txt index 6857152093..a050a6b220 100644 --- a/spikes/CMakeLists.txt +++ b/spikes/CMakeLists.txt @@ -34,6 +34,7 @@ add_subdirectory(${SPIKES_DIR}/lk_sensors_temperature_humidity) add_subdirectory(${SPIKES_DIR}/lk_sensors_touch) add_subdirectory(${SPIKES_DIR}/lk_serial_number) add_subdirectory(${SPIKES_DIR}/lk_ticker_timeout) +add_subdirectory(${SPIKES_DIR}/lk_touch_sensor_kit) add_subdirectory(${SPIKES_DIR}/lk_watchdog_isr) add_subdirectory(${SPIKES_DIR}/lk_wifi) @@ -75,6 +76,7 @@ add_dependencies(spikes_leka spike_lk_sensors_touch spike_lk_serial_number spike_lk_ticker_timeout + spike_lk_touch_sensor_kit spike_lk_wifi spike_lk_update_process_app_base diff --git a/spikes/lk_touch_sensor_kit/CMakeLists.txt b/spikes/lk_touch_sensor_kit/CMakeLists.txt new file mode 100644 index 0000000000..1487e6d6bd --- /dev/null +++ b/spikes/lk_touch_sensor_kit/CMakeLists.txt @@ -0,0 +1,29 @@ +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +add_mbed_executable(spike_lk_touch_sensor_kit) + +target_include_directories(spike_lk_touch_sensor_kit + PRIVATE + . +) + +target_sources(spike_lk_touch_sensor_kit + PRIVATE + main.cpp +) + +target_link_libraries(spike_lk_touch_sensor_kit + CoreI2C + CoreSPI + CoreIOExpander + IOKit + CoreQDAC + CoreTouchSensor + TouchSensorKit + CoreLED + LedKit +) + +target_link_custom_leka_targets(spike_lk_touch_sensor_kit) diff --git a/spikes/lk_touch_sensor_kit/main.cpp b/spikes/lk_touch_sensor_kit/main.cpp new file mode 100644 index 0000000000..2b1aa6ab61 --- /dev/null +++ b/spikes/lk_touch_sensor_kit/main.cpp @@ -0,0 +1,174 @@ +// Leka - LekaOS +// Copyright 2022 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "drivers/BufferedSerial.h" +#include "rtos/ThisThread.h" + +#include "CoreI2C.h" +#include "CoreIOExpander.h" +#include "CoreQDAC.h" +#include "CoreTouchSensor.h" +#include "DigitalOut.h" +#include "HelloWorld.h" +#include "IOKit/DigitalIn.h" +#include "IOKit/DigitalOut.h" +#include "LogKit.h" +#include "TouchSensorKit.h" + +using namespace leka; +using namespace std::chrono_literals; + +namespace touch { + +inline auto corei2c = CoreI2C {PinName::SENSOR_PROXIMITY_MUX_I2C_SDA, PinName::SENSOR_PROXIMITY_MUX_I2C_SCL}; +inline auto io_expander_reset = mbed::DigitalOut {PinName::SENSOR_PROXIMITY_MUX_RESET, 0}; +inline auto io_expander = CoreIOExpanderMCP23017 {corei2c, io_expander_reset}; + +namespace detect_pin { + + inline auto ear_left = io::expanded::DigitalIn<> {io_expander, mcp23017::pin::PB5}; + inline auto ear_right = io::expanded::DigitalIn<> {io_expander, mcp23017::pin::PB4}; + inline auto belt_left_front = io::expanded::DigitalIn<> {io_expander, mcp23017::pin::PB3}; + inline auto belt_left_back = io::expanded::DigitalIn<> {io_expander, mcp23017::pin::PB2}; + inline auto belt_right_back = io::expanded::DigitalIn<> {io_expander, mcp23017::pin::PB1}; + inline auto belt_right_front = io::expanded::DigitalIn<> {io_expander, mcp23017::pin::PB0}; + +} // namespace detect_pin + +namespace power_mode_pin { + + inline auto ear_left = io::expanded::DigitalOut<> {io_expander, mcp23017::pin::PA5}; + inline auto ear_right = io::expanded::DigitalOut<> {io_expander, mcp23017::pin::PA4}; + inline auto belt_left_front = io::expanded::DigitalOut<> {io_expander, mcp23017::pin::PA3}; + inline auto belt_left_back = io::expanded::DigitalOut<> {io_expander, mcp23017::pin::PA2}; + inline auto belt_right_back = io::expanded::DigitalOut<> {io_expander, mcp23017::pin::PA1}; + inline auto belt_right_front = io::expanded::DigitalOut<> {io_expander, mcp23017::pin::PA0}; + +} // namespace power_mode_pin + +namespace dac { + + inline auto left = CoreQDACMCP4728 {corei2c, 0xC2}; + inline auto right = CoreQDACMCP4728 {corei2c, 0xC0}; + + namespace channel { + + inline auto ear_left = mcp4728::channel::C; + inline auto ear_right = mcp4728::channel::C; + inline auto belt_left_back = mcp4728::channel::A; + inline auto belt_left_front = mcp4728::channel::B; + inline auto belt_right_back = mcp4728::channel::B; + inline auto belt_right_front = mcp4728::channel::A; + + } // namespace channel +} // namespace dac + +namespace sensor { + + inline auto ear_left = + CoreTouchSensor {detect_pin::ear_left, power_mode_pin::ear_left, dac::left, dac::channel::ear_left}; + inline auto ear_right = + CoreTouchSensor {detect_pin::ear_right, power_mode_pin::ear_right, dac::right, dac::channel::ear_right}; + inline auto belt_left_back = CoreTouchSensor {detect_pin::belt_left_back, power_mode_pin::belt_left_back, dac::left, + dac::channel::belt_left_back}; + inline auto belt_left_front = CoreTouchSensor {detect_pin::belt_left_front, power_mode_pin::belt_left_front, + dac::left, dac::channel::belt_left_front}; + inline auto belt_right_back = CoreTouchSensor {detect_pin::belt_right_back, power_mode_pin::belt_right_back, + dac::right, dac::channel::belt_right_back}; + inline auto belt_right_front = CoreTouchSensor {detect_pin::belt_right_front, power_mode_pin::belt_right_front, + dac::right, dac::channel::belt_right_front}; +} // namespace sensor + +} // namespace touch + +auto hello = HelloWorld {}; + +auto touch_sensor_kit = + TouchSensorKit {touch::sensor::ear_left, touch::sensor::ear_right, touch::sensor::belt_left_back, + touch::sensor::belt_left_front, touch::sensor::belt_right_back, touch::sensor::belt_right_front}; + +void printSensorTouched(Position position) +{ + switch (position) { + case Position::ear_left: + log_info("Ear left is touched"); + break; + case Position::ear_right: + log_info("Ear right is touched"); + break; + case Position::belt_left_front: + log_info("Belt left front is touched"); + break; + case Position::belt_left_back: + log_info("Belt left back is touched"); + break; + case Position::belt_right_front: + log_info("Belt right front is touched"); + break; + case Position::belt_right_back: + log_info("Belt right back is touched"); + break; + default: + break; + } +} + +void printSensorReleased(Position position) +{ + switch (position) { + case Position::ear_left: + log_info("Ear left is released"); + break; + case Position::ear_right: + log_info("Ear right is released"); + break; + case Position::belt_left_front: + log_info("Belt left front is released"); + break; + case Position::belt_left_back: + log_info("Belt left back is released"); + break; + case Position::belt_right_front: + log_info("Belt right front is released"); + break; + case Position::belt_right_back: + log_info("Belt right back is released"); + break; + default: + break; + } +} + +auto main() -> int +{ + logger::init(); + + HelloWorld hello {}; + hello.start(); + + log_info("Hello, World!\n\n"); + + rtos::ThisThread::sleep_for(2s); + + touch_sensor_kit.registerOnSensorTouched(printSensorTouched); + touch_sensor_kit.registerOnSensorReleased(printSensorReleased); + + touch_sensor_kit.initialize(); + touch_sensor_kit.setRefreshDelay(20ms); + + log_info("Enable touch for 10s"); + touch_sensor_kit.enable(); + rtos::ThisThread::sleep_for(10s); + + log_info("Disable touch for 10s"); + touch_sensor_kit.disable(); + rtos::ThisThread::sleep_for(10s); + + log_info("Enable Touch"); + touch_sensor_kit.enable(); + while (true) { + log_info("Still alive"); + rtos::ThisThread::sleep_for(5s); + } +} From 301d735e4f966c1e7bab3b53bc726e1d9dade82f Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 21:04:42 +0100 Subject: [PATCH 024/143] :recycle: (bufferedserial): Use std::function instead of mbed::Callback --- drivers/CoreBufferedSerial/include/CoreBufferedSerial.h | 4 +++- drivers/CoreBufferedSerial/source/CoreBufferedSerial.cpp | 5 +++-- include/interface/drivers/BufferedSerial.h | 2 +- tests/unit/mocks/mocks/leka/CoreBufferedSerial.h | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/drivers/CoreBufferedSerial/include/CoreBufferedSerial.h b/drivers/CoreBufferedSerial/include/CoreBufferedSerial.h index 30253bfcd0..0700eb5bee 100644 --- a/drivers/CoreBufferedSerial/include/CoreBufferedSerial.h +++ b/drivers/CoreBufferedSerial/include/CoreBufferedSerial.h @@ -26,10 +26,12 @@ class CoreBufferedSerial : public interface::BufferedSerial void enable_input() final; void disable_input() final; - void sigio(mbed::Callback func) final; + void sigio(std::function const &callback) final; private: mbed::BufferedSerial _serial; + + std::function _sigio_callback {}; }; } // namespace leka diff --git a/drivers/CoreBufferedSerial/source/CoreBufferedSerial.cpp b/drivers/CoreBufferedSerial/source/CoreBufferedSerial.cpp index d0a2838656..40160b097e 100644 --- a/drivers/CoreBufferedSerial/source/CoreBufferedSerial.cpp +++ b/drivers/CoreBufferedSerial/source/CoreBufferedSerial.cpp @@ -33,9 +33,10 @@ void CoreBufferedSerial::disable_input() _serial.enable_input(false); } -void CoreBufferedSerial::sigio(mbed::Callback func) +void CoreBufferedSerial::sigio(std::function const &callback) { - _serial.sigio(func); + _sigio_callback = callback; + _serial.sigio(mbed::Callback {[this] { _sigio_callback(); }}); } // LCOV_EXCL_STOP diff --git a/include/interface/drivers/BufferedSerial.h b/include/interface/drivers/BufferedSerial.h index 88e5e317f5..bb772402ef 100644 --- a/include/interface/drivers/BufferedSerial.h +++ b/include/interface/drivers/BufferedSerial.h @@ -24,7 +24,7 @@ class BufferedSerial virtual void disable_input() = 0; virtual void enable_input() = 0; - virtual void sigio(mbed::Callback func) = 0; // TODO (@HPezz) replace mbed callback by std function + virtual void sigio(std::function const &callback) = 0; }; } // namespace leka::interface diff --git a/tests/unit/mocks/mocks/leka/CoreBufferedSerial.h b/tests/unit/mocks/mocks/leka/CoreBufferedSerial.h index 8fd155380b..cf678664e3 100644 --- a/tests/unit/mocks/mocks/leka/CoreBufferedSerial.h +++ b/tests/unit/mocks/mocks/leka/CoreBufferedSerial.h @@ -17,7 +17,7 @@ class CoreBufferedSerial : public interface::BufferedSerial MOCK_METHOD(bool, readable, (), (override)); MOCK_METHOD(void, disable_input, (), (override)); MOCK_METHOD(void, enable_input, (), (override)); - MOCK_METHOD(void, sigio, (mbed::Callback), (override)); + MOCK_METHOD(void, sigio, (std::function const &), (override)); }; } // namespace leka::mock From 7681b1e56d9a90d2c7cc12edb31cecd1834dbff1 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 20:26:46 +0100 Subject: [PATCH 025/143] :recycle: (battery): Use std::function instead of mbed::Callback --- drivers/CoreBattery/include/CoreBattery.h | 8 +++-- drivers/CoreBattery/source/CoreBattery.cpp | 12 ++++--- .../CoreBattery/tests/CoreBattery_test.cpp | 31 ++++++++----------- include/interface/drivers/Battery.h | 7 ++--- libs/BatteryKit/include/BatteryKit.h | 4 +-- libs/BatteryKit/source/BatteryKit.cpp | 4 +-- libs/BatteryKit/tests/BatteryKit_test.cpp | 19 ++++++++---- libs/RobotKit/tests/RobotController_test.h | 9 +++--- tests/unit/mocks/mocks/leka/Battery.h | 4 +-- 9 files changed, 53 insertions(+), 45 deletions(-) diff --git a/drivers/CoreBattery/include/CoreBattery.h b/drivers/CoreBattery/include/CoreBattery.h index d19411b147..3623337ae0 100644 --- a/drivers/CoreBattery/include/CoreBattery.h +++ b/drivers/CoreBattery/include/CoreBattery.h @@ -6,7 +6,6 @@ #include "drivers/AnalogIn.h" #include "drivers/InterruptIn.h" -#include "platform/Callback.h" #include "interface/drivers/Battery.h" @@ -21,8 +20,8 @@ class CoreBattery : public interface::Battery // nothing do to } - void onChargeDidStart(mbed::Callback const &callback) final; - void onChargeDidStop(mbed::Callback const &callback) final; + void onChargeDidStart(std::function const &callback) final; + void onChargeDidStop(std::function const &callback) final; auto voltage() -> float final; auto level() -> uint8_t final; @@ -51,6 +50,9 @@ class CoreBattery : public interface::Battery mbed::AnalogIn _voltage_pin; mbed::InterruptIn &_charge_status_input; + + std::function _on_charge_did_start {}; + std::function _on_charge_did_stop {}; }; } // namespace leka diff --git a/drivers/CoreBattery/source/CoreBattery.cpp b/drivers/CoreBattery/source/CoreBattery.cpp index 008649986a..3426269aa0 100644 --- a/drivers/CoreBattery/source/CoreBattery.cpp +++ b/drivers/CoreBattery/source/CoreBattery.cpp @@ -4,18 +4,22 @@ #include "CoreBattery.h" +#include "platform/Callback.h" + #include "MathUtils.h" namespace leka { -void CoreBattery::onChargeDidStart(mbed::Callback const &callback) +void CoreBattery::onChargeDidStart(std::function const &callback) { - _charge_status_input.rise(callback); + _on_charge_did_start = callback; + _charge_status_input.rise(mbed::Callback {[this] { _on_charge_did_start(); }}); } -void CoreBattery::onChargeDidStop(mbed::Callback const &callback) +void CoreBattery::onChargeDidStop(std::function const &callback) { - _charge_status_input.fall(callback); + _on_charge_did_stop = callback; + _charge_status_input.fall(mbed::Callback {[this] { _on_charge_did_stop(); }}); } auto CoreBattery::voltage() -> float diff --git a/drivers/CoreBattery/tests/CoreBattery_test.cpp b/drivers/CoreBattery/tests/CoreBattery_test.cpp index ccddc763db..9b180913c7 100644 --- a/drivers/CoreBattery/tests/CoreBattery_test.cpp +++ b/drivers/CoreBattery/tests/CoreBattery_test.cpp @@ -4,12 +4,15 @@ #include "CoreBattery.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" #include "stubs/mbed/AnalogIn.h" #include "stubs/mbed/InterruptIn.h" using namespace leka; +using ::testing::MockFunction; + class CoreBatteryTest : public ::testing::Test { protected: @@ -109,32 +112,24 @@ TEST_F(CoreBatteryTest, voltageBelowEmpty) TEST_F(CoreBatteryTest, onChargeDidStart) { - auto lambda_dummy = []() {}; - auto lambda_impostor = []() {}; - - mbed::Callback callback_dummy(lambda_dummy); - mbed::Callback callback_impostor(lambda_impostor); + MockFunction callback; - battery.onChargeDidStart(callback_dummy); + EXPECT_CALL(callback, Call); + battery.onChargeDidStart(callback.AsStdFunction()); - ASSERT_NE(callback_impostor, callback_dummy); - ASSERT_NE(callback_impostor, spy_InterruptIn_getRiseCallback()); - ASSERT_EQ(callback_dummy, spy_InterruptIn_getRiseCallback()); + auto charge_did_start = spy_InterruptIn_getRiseCallback(); + charge_did_start(); } TEST_F(CoreBatteryTest, onChargeDidStop) { - auto lambda_dummy = []() {}; - auto lambda_impostor = []() {}; - - mbed::Callback callback_dummy(lambda_dummy); - mbed::Callback callback_impostor(lambda_impostor); + MockFunction callback; - battery.onChargeDidStop(callback_dummy); + EXPECT_CALL(callback, Call); + battery.onChargeDidStop(callback.AsStdFunction()); - ASSERT_NE(callback_impostor, callback_dummy); - ASSERT_NE(callback_impostor, spy_InterruptIn_getFallCallback()); - ASSERT_EQ(callback_dummy, spy_InterruptIn_getFallCallback()); + auto charge_did_stop = spy_InterruptIn_getFallCallback(); + charge_did_stop(); } TEST_F(CoreBatteryTest, isCharging) diff --git a/include/interface/drivers/Battery.h b/include/interface/drivers/Battery.h index 48667c274f..9d06198c4f 100644 --- a/include/interface/drivers/Battery.h +++ b/include/interface/drivers/Battery.h @@ -5,8 +5,7 @@ #pragma once #include - -#include "platform/Callback.h" +#include namespace leka::interface { @@ -15,8 +14,8 @@ class Battery public: virtual ~Battery() = default; - virtual void onChargeDidStart(mbed::Callback const &callback) = 0; - virtual void onChargeDidStop(mbed::Callback const &callback) = 0; + virtual void onChargeDidStart(std::function const &callback) = 0; + virtual void onChargeDidStop(std::function const &callback) = 0; virtual auto voltage() -> float = 0; virtual auto level() -> uint8_t = 0; diff --git a/libs/BatteryKit/include/BatteryKit.h b/libs/BatteryKit/include/BatteryKit.h index 4549ff5168..4615ee5d87 100644 --- a/libs/BatteryKit/include/BatteryKit.h +++ b/libs/BatteryKit/include/BatteryKit.h @@ -18,8 +18,8 @@ class BatteryKit auto level() -> uint8_t; auto isCharging() -> bool; - void onChargeDidStart(mbed::Callback const &callback); - void onChargeDidStop(mbed::Callback const &callback); + void onChargeDidStart(std::function const &callback); + void onChargeDidStop(std::function const &callback); void onDataUpdated(std::function const &callback); void onLowBattery(std::function const &callback); diff --git a/libs/BatteryKit/source/BatteryKit.cpp b/libs/BatteryKit/source/BatteryKit.cpp index a1961bcc0e..8b2a7a51cc 100644 --- a/libs/BatteryKit/source/BatteryKit.cpp +++ b/libs/BatteryKit/source/BatteryKit.cpp @@ -34,12 +34,12 @@ auto BatteryKit::isCharging() -> bool return _battery.isCharging(); } -void BatteryKit::onChargeDidStart(mbed::Callback const &callback) +void BatteryKit::onChargeDidStart(std::function const &callback) { _battery.onChargeDidStart(callback); } -void BatteryKit::onChargeDidStop(mbed::Callback const &callback) +void BatteryKit::onChargeDidStop(std::function const &callback) { _battery.onChargeDidStop(callback); } diff --git a/libs/BatteryKit/tests/BatteryKit_test.cpp b/libs/BatteryKit/tests/BatteryKit_test.cpp index 713f508d4d..c5bd95e677 100644 --- a/libs/BatteryKit/tests/BatteryKit_test.cpp +++ b/libs/BatteryKit/tests/BatteryKit_test.cpp @@ -13,6 +13,7 @@ using namespace leka; using ::testing::MockFunction; using ::testing::Return; +using ::testing::SaveArg; class BatteryKitTest : public ::testing::Test { @@ -57,20 +58,26 @@ TEST_F(BatteryKitTest, isCharging) TEST_F(BatteryKitTest, onChargeDidStart) { - mbed::Callback callback_dummy([] {}); + MockFunction callback {}; + std::function charge_did_start_callback {}; - EXPECT_CALL(mock_battery, onChargeDidStart(callback_dummy)).Times(1); + EXPECT_CALL(mock_battery, onChargeDidStart).WillOnce(SaveArg<0>(&charge_did_start_callback)); + batterykit.onChargeDidStart(callback.AsStdFunction()); - batterykit.onChargeDidStart(callback_dummy); + EXPECT_CALL(callback, Call); + charge_did_start_callback(); } TEST_F(BatteryKitTest, onChargeDidStop) { - mbed::Callback callback_dummy([] {}); + MockFunction callback; + std::function charge_did_stop_callback; - EXPECT_CALL(mock_battery, onChargeDidStop(callback_dummy)).Times(1); + EXPECT_CALL(mock_battery, onChargeDidStop).WillOnce(SaveArg<0>(&charge_did_stop_callback)); + batterykit.onChargeDidStop(callback.AsStdFunction()); - batterykit.onChargeDidStop(callback_dummy); + EXPECT_CALL(callback, Call); + charge_did_stop_callback(); } TEST_F(BatteryKitTest, onDataUpdated) diff --git a/libs/RobotKit/tests/RobotController_test.h b/libs/RobotKit/tests/RobotController_test.h index 238d35f664..f3ec24bf1d 100644 --- a/libs/RobotKit/tests/RobotController_test.h +++ b/libs/RobotKit/tests/RobotController_test.h @@ -49,6 +49,7 @@ using ::testing::AtLeast; using ::testing::InSequence; using ::testing::MockFunction; using ::testing::Return; +using ::testing::SaveArg; using ::testing::Sequence; ACTION_TEMPLATE(GetCallback, HAS_1_TEMPLATE_PARAMS(typename, callback_t), AND_1_VALUE_PARAMS(pointer)) @@ -136,8 +137,8 @@ class RobotControllerTest : public testing::Test interface::Timeout::callback_t on_sleeping_start_timeout = {}; interface::Timeout::callback_t on_charging_start_timeout = {}; - mbed::Callback on_charge_did_start {}; - mbed::Callback on_charge_did_stop {}; + std::function on_charge_did_start {}; + std::function on_charge_did_stop {}; bool spy_isCharging_return_value = false; @@ -215,9 +216,9 @@ class RobotControllerTest : public testing::Test EXPECT_CALL(mbed_mock_gap, setAdvertisingPayload).InSequence(on_data_updated_sequence); EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(2).InSequence(on_data_updated_sequence); - EXPECT_CALL(battery, onChargeDidStart).WillOnce(GetCallback>(&on_charge_did_start)); + EXPECT_CALL(battery, onChargeDidStart).WillOnce(SaveArg<0>(&on_charge_did_start)); - EXPECT_CALL(battery, onChargeDidStop).WillOnce(GetCallback>(&on_charge_did_stop)); + EXPECT_CALL(battery, onChargeDidStop).WillOnce(SaveArg<0>(&on_charge_did_stop)); { InSequence event_setup_complete; diff --git a/tests/unit/mocks/mocks/leka/Battery.h b/tests/unit/mocks/mocks/leka/Battery.h index 1e0f975866..92191c8db5 100644 --- a/tests/unit/mocks/mocks/leka/Battery.h +++ b/tests/unit/mocks/mocks/leka/Battery.h @@ -12,8 +12,8 @@ namespace leka::mock { class Battery : public interface::Battery { public: - MOCK_METHOD(void, onChargeDidStart, (mbed::Callback const &), (override)); - MOCK_METHOD(void, onChargeDidStop, (mbed::Callback const &), (override)); + MOCK_METHOD(void, onChargeDidStart, (std::function const &), (override)); + MOCK_METHOD(void, onChargeDidStop, (std::function const &), (override)); MOCK_METHOD(float, voltage, (), (override)); MOCK_METHOD(uint8_t, level, (), (override)); From c050bb4f7ad3da67aef9102ff3b75e39b8f0d207 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 20:55:40 +0100 Subject: [PATCH 026/143] :recycle: (ble): Use std::function instead of mbed::Callback --- libs/BLEKit/source/BLEKit.cpp | 4 +++- libs/BLEKit/tests/BLEKit_test.cpp | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libs/BLEKit/source/BLEKit.cpp b/libs/BLEKit/source/BLEKit.cpp index 131c0e0933..cc3421206e 100644 --- a/libs/BLEKit/source/BLEKit.cpp +++ b/libs/BLEKit/source/BLEKit.cpp @@ -4,6 +4,8 @@ #include "BLEKit.h" +#include "platform/Callback.h" + using namespace leka; void BLEKit::setServices(std::span const &services) @@ -31,7 +33,7 @@ void BLEKit::init() void BLEKit::processEvents(BLE::OnEventsToProcessCallbackContext *context) { - _event_queue.callMbedCallback(mbed::callback(&context->ble, &BLE::processEvents)); + _event_queue.call(mbed::Callback {&context->ble, &BLE::processEvents}); } void BLEKit::setAdvertisingData(const AdvertisingData &advertising_data) diff --git a/libs/BLEKit/tests/BLEKit_test.cpp b/libs/BLEKit/tests/BLEKit_test.cpp index 35f59b2036..68264849a5 100644 --- a/libs/BLEKit/tests/BLEKit_test.cpp +++ b/libs/BLEKit/tests/BLEKit_test.cpp @@ -93,20 +93,17 @@ TEST_F(BLEKitTest, setServices) TEST_F(BLEKitTest, callOnEventsToProcess) { - spy_ble_hasInitialized_return_value = false; - spy_CoreEventQueue_did_call_function = false; + spy_ble_hasInitialized_return_value = false; EXPECT_CALL(mbed_mock_gap, setEventHandler).Times(AnyNumber()); EXPECT_CALL(mbed_mock_gatt, setEventHandler).Times(AnyNumber()); ble.init(); - EXPECT_FALSE(spy_CoreEventQueue_did_call_function); - BLE::OnEventsToProcessCallbackContext context = {BLE::Instance()}; spy_ble_on_events_to_process_callback(&context); - EXPECT_TRUE(spy_CoreEventQueue_did_call_function); + // nothing expected } TEST_F(BLEKitTest, getAdvertisingDataThenSetAdvertisingData) From e6150643b1e44880ac4b169e1a8bea6680e70ccc Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 20:58:45 +0100 Subject: [PATCH 027/143] :recycle: (rc): Use std::function instead of mbed::Callback --- libs/RobotKit/tests/RobotController_test_registerEvents.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp index 9d0d35d66c..5449e41ddc 100644 --- a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp +++ b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp @@ -120,9 +120,9 @@ TEST_F(RobotControllerTest, registerEventsBatteryIsCharging) TEST_F(RobotControllerTest, registerOnFactoryResetNotificationCallback) { - mbed::Callback callback {}; + MockFunction callback {}; - rc.registerOnFactoryResetNotificationCallback(callback); + rc.registerOnFactoryResetNotificationCallback(callback.AsStdFunction()); // nothing can be expected } From ed538be49c4f5eeb717821c0b6057c3a78797309 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 21:17:50 +0100 Subject: [PATCH 028/143] :fire: (CoreEventQueue): Remove callMbedCallback --- drivers/CoreEventQueue/include/CoreEventQueue.h | 3 --- drivers/CoreEventQueue/source/CoreEventQueue.cpp | 5 ----- .../CoreEventQueue/tests/CoreEventQueue_test.cpp | 13 ------------- tests/unit/stubs/stubs/leka/CoreEventQueue.h | 2 -- .../unit/stubs/stubs/leka/source/CoreEventQueue.cpp | 7 ------- 5 files changed, 30 deletions(-) diff --git a/drivers/CoreEventQueue/include/CoreEventQueue.h b/drivers/CoreEventQueue/include/CoreEventQueue.h index f906c18e5a..28865f22c9 100644 --- a/drivers/CoreEventQueue/include/CoreEventQueue.h +++ b/drivers/CoreEventQueue/include/CoreEventQueue.h @@ -27,9 +27,6 @@ class CoreEventQueue : public interface::EventQueue void cancel(int id) { _event_queue.cancel(id); } - // ? Overload needed for mbed::BLE compatibility - void callMbedCallback(mbed::Callback const &f); - private: rtos::Thread _event_queue_thread {}; events::EventQueue _event_queue {}; diff --git a/drivers/CoreEventQueue/source/CoreEventQueue.cpp b/drivers/CoreEventQueue/source/CoreEventQueue.cpp index 697440f513..e216108600 100644 --- a/drivers/CoreEventQueue/source/CoreEventQueue.cpp +++ b/drivers/CoreEventQueue/source/CoreEventQueue.cpp @@ -6,8 +6,3 @@ void CoreEventQueue::dispatch_forever() { _event_queue_thread.start({&_event_queue, &events::EventQueue::dispatch_forever}); } - -void CoreEventQueue::callMbedCallback(mbed::Callback const &f) -{ - _event_queue.call(f); -} diff --git a/drivers/CoreEventQueue/tests/CoreEventQueue_test.cpp b/drivers/CoreEventQueue/tests/CoreEventQueue_test.cpp index f750353da6..8e0e376c8b 100644 --- a/drivers/CoreEventQueue/tests/CoreEventQueue_test.cpp +++ b/drivers/CoreEventQueue/tests/CoreEventQueue_test.cpp @@ -57,16 +57,3 @@ TEST_F(CoreEventQueueTest, callEvery) event_queue.call_every(2s, mock.AsStdFunction()); } - -TEST_F(CoreEventQueueTest, callMbedCallback) -{ - MockFunction mock; - - EXPECT_CALL(mock, Call()).Times(1); - - auto func = [&] { mock.Call(); }; - - event_queue.dispatch_forever(); - - event_queue.callMbedCallback(mbed::callback(func)); -} diff --git a/tests/unit/stubs/stubs/leka/CoreEventQueue.h b/tests/unit/stubs/stubs/leka/CoreEventQueue.h index b06d18de8e..377510f036 100644 --- a/tests/unit/stubs/stubs/leka/CoreEventQueue.h +++ b/tests/unit/stubs/stubs/leka/CoreEventQueue.h @@ -6,8 +6,6 @@ namespace leka { -extern bool spy_CoreEventQueue_did_call_function; - extern std::function spy_CoreEventQueue_on_dispatch_forever_call; } // namespace leka diff --git a/tests/unit/stubs/stubs/leka/source/CoreEventQueue.cpp b/tests/unit/stubs/stubs/leka/source/CoreEventQueue.cpp index fda46b5b2a..10b5ca8058 100644 --- a/tests/unit/stubs/stubs/leka/source/CoreEventQueue.cpp +++ b/tests/unit/stubs/stubs/leka/source/CoreEventQueue.cpp @@ -2,7 +2,6 @@ namespace leka { -bool spy_CoreEventQueue_did_call_function = false; std::function spy_CoreEventQueue_on_dispatch_forever_call; void CoreEventQueue::dispatch_forever() @@ -12,10 +11,4 @@ void CoreEventQueue::dispatch_forever() } } -void CoreEventQueue::callMbedCallback(mbed::Callback const &f) -{ - f(); - spy_CoreEventQueue_did_call_function = true; -} - } // namespace leka From 88b4d885cc856c39394658d85f27fe845284e3f9 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 21:19:11 +0100 Subject: [PATCH 029/143] :recycle: mbed::Callback related cleanup --- drivers/CoreWifi/source/CoreNetwork.cpp | 2 ++ include/interface/drivers/Network.h | 3 +-- spikes/lk_lcd/main.cpp | 1 - spikes/lk_motors/main.cpp | 1 - tests/unit/stubs/stubs/mbed/source/InterruptIn.cpp | 4 ++-- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/drivers/CoreWifi/source/CoreNetwork.cpp b/drivers/CoreWifi/source/CoreNetwork.cpp index 65b2b84636..6984d6608f 100644 --- a/drivers/CoreWifi/source/CoreNetwork.cpp +++ b/drivers/CoreWifi/source/CoreNetwork.cpp @@ -1,5 +1,7 @@ #include "CoreNetwork.h" +#include "platform/Callback.h" + using namespace leka; auto CoreNetwork::connect(const char *ssid, const char *pass) -> bool diff --git a/include/interface/drivers/Network.h b/include/interface/drivers/Network.h index 71d1cf10d9..7ff1ffdb69 100644 --- a/include/interface/drivers/Network.h +++ b/include/interface/drivers/Network.h @@ -4,11 +4,10 @@ #pragma once +#include #include #include -#include "platform/Callback.h" - namespace leka { struct HttpResponse { diff --git a/spikes/lk_lcd/main.cpp b/spikes/lk_lcd/main.cpp index 78e6889db0..b0e111ab6e 100644 --- a/spikes/lk_lcd/main.cpp +++ b/spikes/lk_lcd/main.cpp @@ -4,7 +4,6 @@ #include -#include "platform/Callback.h" #include "rtos/ThisThread.h" #include "rtos/Thread.h" diff --git a/spikes/lk_motors/main.cpp b/spikes/lk_motors/main.cpp index 0a083adb89..b824e5b375 100644 --- a/spikes/lk_motors/main.cpp +++ b/spikes/lk_motors/main.cpp @@ -7,7 +7,6 @@ #include "drivers/BufferedSerial.h" #include "drivers/DigitalOut.h" #include "drivers/PwmOut.h" -#include "platform/Callback.h" #include "rtos/ThisThread.h" #include "rtos/Thread.h" diff --git a/tests/unit/stubs/stubs/mbed/source/InterruptIn.cpp b/tests/unit/stubs/stubs/mbed/source/InterruptIn.cpp index 1ff2ec1e4f..df6b6123c3 100644 --- a/tests/unit/stubs/stubs/mbed/source/InterruptIn.cpp +++ b/tests/unit/stubs/stubs/mbed/source/InterruptIn.cpp @@ -20,12 +20,12 @@ auto InterruptIn::read() -> int return leka::spy_InterruptIn_value; } -void InterruptIn::rise(Callback func) +void InterruptIn::rise(mbed::Callback func) { leka::spy_InterruptIn_risecallback = func; } -void InterruptIn::fall(Callback func) +void InterruptIn::fall(mbed::Callback func) { leka::spy_InterruptIn_fallcallback = func; } From e434f68a1127814a710f18e75513ec3743042655 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 18 Jan 2023 20:08:39 +0100 Subject: [PATCH 030/143] :pushpin: (mbed): Pin to mbed-os@master+fixes+gcc-11-support --- config/mbed_version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/mbed_version b/config/mbed_version index 0ea0f61128..08322ee7d9 100644 --- a/config/mbed_version +++ b/config/mbed_version @@ -1 +1 @@ -mbed-os-6.15.1+fixes+gcc-11-support +mbed-os@master+fixes+gcc-11-support From 36bb259a93816224a0fdb61b0405602e06fc0a74 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 20 Jan 2023 10:32:15 +0100 Subject: [PATCH 031/143] :fire: (spikes): Remove spike lk_sensors_touch --- spikes/CMakeLists.txt | 2 -- spikes/lk_sensors_touch/CMakeLists.txt | 21 -------------- spikes/lk_sensors_touch/main.cpp | 39 -------------------------- 3 files changed, 62 deletions(-) delete mode 100644 spikes/lk_sensors_touch/CMakeLists.txt delete mode 100644 spikes/lk_sensors_touch/main.cpp diff --git a/spikes/CMakeLists.txt b/spikes/CMakeLists.txt index a050a6b220..38a5f895ba 100644 --- a/spikes/CMakeLists.txt +++ b/spikes/CMakeLists.txt @@ -31,7 +31,6 @@ add_subdirectory(${SPIKES_DIR}/lk_sensors_battery) add_subdirectory(${SPIKES_DIR}/lk_sensors_light) add_subdirectory(${SPIKES_DIR}/lk_sensors_microphone) add_subdirectory(${SPIKES_DIR}/lk_sensors_temperature_humidity) -add_subdirectory(${SPIKES_DIR}/lk_sensors_touch) add_subdirectory(${SPIKES_DIR}/lk_serial_number) add_subdirectory(${SPIKES_DIR}/lk_ticker_timeout) add_subdirectory(${SPIKES_DIR}/lk_touch_sensor_kit) @@ -73,7 +72,6 @@ add_dependencies(spikes_leka spike_lk_sensors_light spike_lk_sensors_microphone spike_lk_sensors_temperature_humidity - spike_lk_sensors_touch spike_lk_serial_number spike_lk_ticker_timeout spike_lk_touch_sensor_kit diff --git a/spikes/lk_sensors_touch/CMakeLists.txt b/spikes/lk_sensors_touch/CMakeLists.txt deleted file mode 100644 index a4a40795af..0000000000 --- a/spikes/lk_sensors_touch/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Leka - LekaOS -# Copyright 2020 APF France handicap -# SPDX-License-Identifier: Apache-2.0 - -add_mbed_executable(spike_lk_sensors_touch) - -target_include_directories(spike_lk_sensors_touch - PRIVATE - . -) - -target_sources(spike_lk_sensors_touch - PRIVATE - main.cpp -) - -target_link_libraries(spike_lk_sensors_touch - lib_LekaTouch -) - -target_link_custom_leka_targets(spike_lk_sensors_touch) diff --git a/spikes/lk_sensors_touch/main.cpp b/spikes/lk_sensors_touch/main.cpp deleted file mode 100644 index e7df4801d2..0000000000 --- a/spikes/lk_sensors_touch/main.cpp +++ /dev/null @@ -1,39 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "drivers/BufferedSerial.h" -#include "rtos/ThisThread.h" - -#include "HelloWorld.h" -#include "LekaTouch.h" -#include "LogKit.h" - -using namespace leka; -using namespace std::chrono; - -auto main() -> int -{ - logger::init(); - - HelloWorld hello; - hello.start(); - - log_info("Hello, World!\n\n"); - - auto start = rtos::Kernel::Clock::now(); - - rtos::ThisThread::sleep_for(2s); - - Touch touch_sensor; - rtos::Thread touch_thread; - touch_thread.start({&touch_sensor, &Touch::start}); - - while (true) { - auto t = rtos::Kernel::Clock::now() - start; - log_info("A message from your board %s --> \"%s\" at %i s\n", MBED_CONF_APP_TARGET_NAME, hello.world, - int(t.count() / 1000)); - - rtos::ThisThread::sleep_for(1s); - } -} From b4751477bca87f7afc043d11cbbd2ac2b7ce44ad Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 20 Jan 2023 10:30:31 +0100 Subject: [PATCH 032/143] :fire: (libs): Remove lib InvestigationDay --- libs/CMakeLists.txt | 6 - libs/InvestigationDay/BLE/CMakeLists.txt | 21 - libs/InvestigationDay/BLE/include/LKBLE.h | 145 ---- libs/InvestigationDay/LekaRFID/CMakeLists.txt | 17 - .../LekaRFID/include/LekaRFID.h | 59 -- .../LekaRFID/source/LekaRFID.cpp | 209 ----- .../LekaScreen/CMakeLists.txt | 19 - .../LekaScreen/include/LekaLCD.h | 101 --- .../LekaScreen/include/LekaScreen.h | 26 - .../LekaScreen/include/internal/otm8009a.h | 334 -------- .../LekaScreen/source/LekaLCD.cpp | 712 ------------------ .../LekaScreen/source/LekaScreen.cpp | 90 --- .../InvestigationDay/LekaTouch/CMakeLists.txt | 22 - .../LekaTouch/include/LekaTouch.h | 57 -- .../LekaTouch/include/internal/MCP23017.h | 145 ---- .../LekaTouch/source/LekaTouch.cpp | 261 ------- .../LekaTouch/source/MCP23017.cpp | 260 ------- libs/InvestigationDay/LekaWifi/CMakeLists.txt | 17 - .../LekaWifi/include/LekaWifi.h | 36 - .../LekaWifi/source/LekaWifi.cpp | 119 --- 20 files changed, 2656 deletions(-) delete mode 100644 libs/InvestigationDay/BLE/CMakeLists.txt delete mode 100644 libs/InvestigationDay/BLE/include/LKBLE.h delete mode 100644 libs/InvestigationDay/LekaRFID/CMakeLists.txt delete mode 100644 libs/InvestigationDay/LekaRFID/include/LekaRFID.h delete mode 100644 libs/InvestigationDay/LekaRFID/source/LekaRFID.cpp delete mode 100644 libs/InvestigationDay/LekaScreen/CMakeLists.txt delete mode 100644 libs/InvestigationDay/LekaScreen/include/LekaLCD.h delete mode 100644 libs/InvestigationDay/LekaScreen/include/LekaScreen.h delete mode 100644 libs/InvestigationDay/LekaScreen/include/internal/otm8009a.h delete mode 100644 libs/InvestigationDay/LekaScreen/source/LekaLCD.cpp delete mode 100644 libs/InvestigationDay/LekaScreen/source/LekaScreen.cpp delete mode 100644 libs/InvestigationDay/LekaTouch/CMakeLists.txt delete mode 100644 libs/InvestigationDay/LekaTouch/include/LekaTouch.h delete mode 100644 libs/InvestigationDay/LekaTouch/include/internal/MCP23017.h delete mode 100644 libs/InvestigationDay/LekaTouch/source/LekaTouch.cpp delete mode 100644 libs/InvestigationDay/LekaTouch/source/MCP23017.cpp delete mode 100644 libs/InvestigationDay/LekaWifi/CMakeLists.txt delete mode 100644 libs/InvestigationDay/LekaWifi/include/LekaWifi.h delete mode 100644 libs/InvestigationDay/LekaWifi/source/LekaWifi.cpp diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 2a0a9a1662..e91e96c36e 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -27,9 +27,3 @@ add_subdirectory(${LIBS_DIR}/VideoKit) add_subdirectory(${LIBS_DIR}/WebKit) add_subdirectory(${LIBS_DIR}/PrettyPrinter) - -add_subdirectory(${LIBS_DIR}/InvestigationDay/BLE) -add_subdirectory(${LIBS_DIR}/InvestigationDay/LekaRFID) -add_subdirectory(${LIBS_DIR}/InvestigationDay/LekaScreen) -add_subdirectory(${LIBS_DIR}/InvestigationDay/LekaTouch) -add_subdirectory(${LIBS_DIR}/InvestigationDay/LekaWifi) diff --git a/libs/InvestigationDay/BLE/CMakeLists.txt b/libs/InvestigationDay/BLE/CMakeLists.txt deleted file mode 100644 index 38e600f2e4..0000000000 --- a/libs/InvestigationDay/BLE/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Leka - LekaOS -# Copyright 2020 APF France handicap -# SPDX-License-Identifier: Apache-2.0 - -add_library(lib_BLE STATIC) - -target_include_directories(lib_BLE - PUBLIC - include -) - -target_sources(lib_BLE - PRIVATE - include/LKBLE.h -) - -target_link_libraries(lib_BLE - mbed-os - LogKit - lib_PrettyPrinter -) diff --git a/libs/InvestigationDay/BLE/include/LKBLE.h b/libs/InvestigationDay/BLE/include/LKBLE.h deleted file mode 100644 index e611d7fd11..0000000000 --- a/libs/InvestigationDay/BLE/include/LKBLE.h +++ /dev/null @@ -1,145 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "events/mbed_events.h" - -#include "ble/BLE.h" -#include "ble/Gap.h" -#include "ble/services/HeartRateService.h" - -#include "LogKit.h" -#include "PrettyPrinter.h" - -const static auto DEVICE_NAME = std::array {"Heartrate"}; -static events::EventQueue event_queue(/* event count */ 32 * EVENTS_EVENT_SIZE); - -class HeartrateDemo : ble::Gap::EventHandler -{ - public: - HeartrateDemo(BLE &ble, events::EventQueue &event_queue) - : _ble(ble), - _event_queue(event_queue), - // _led1(LED1, 1), - _connected(false), - _hr_uuid(GattService::UUID_HEART_RATE_SERVICE), - _hr_counter(100), - _hr_service(ble, _hr_counter, HeartRateService::LOCATION_FINGER), - _adv_data_builder(_adv_buffer.data(), std::size(_adv_buffer)) - { - } - - void start() - { - _ble.gap().setEventHandler(this); - - _ble.init(this, &HeartrateDemo::onInitComplete); - - // _event_queue.call_every(500ms, this, &HeartrateDemo::blink); - _event_queue.call_every(std::chrono::milliseconds(1000), this, &HeartrateDemo::update_sensor_value); - - // _event_queue.dispatch_forever(); - // printf("dispatch_forever\n"); - } - - private: - /** Callback triggered when the ble initialization process has finished */ - void onInitComplete(BLE::InitializationCompleteCallbackContext *params) - { - if (params->error != BLE_ERROR_NONE) { - log_error("Ble initialization failed."); - return; - } - - leka::ble::printMacAddress(); - - startAdvertising(); - } - - void startAdvertising() - { - /* Create advertising parameters and payload */ - - ble::AdvertisingParameters adv_parameters(ble::advertising_type_t::CONNECTABLE_UNDIRECTED, - ble::adv_interval_t(ble::millisecond_t(1000))); - - _adv_data_builder.setFlags(); - _adv_data_builder.setAppearance(ble::adv_data_appearance_t::GENERIC_HEART_RATE_SENSOR); - _adv_data_builder.setLocalServiceList(mbed::make_Span(&_hr_uuid, 1)); - _adv_data_builder.setName(DEVICE_NAME.data()); - - /* Setup advertising */ - - ble_error_t error = _ble.gap().setAdvertisingParameters(ble::LEGACY_ADVERTISING_HANDLE, adv_parameters); - - if (error) { - printf("_ble.gap().setAdvertisingParameters() failed\r\n"); - return; - } - - error = - _ble.gap().setAdvertisingPayload(ble::LEGACY_ADVERTISING_HANDLE, _adv_data_builder.getAdvertisingData()); - - if (error) { - printf("_ble.gap().setAdvertisingPayload() failed\r\n"); - return; - } - - /* Start advertising */ - - error = _ble.gap().startAdvertising(ble::LEGACY_ADVERTISING_HANDLE); - - if (error) { - printf("_ble.gap().startAdvertising() failed\r\n"); - return; - } - } - - void update_sensor_value() - { - if (_connected) { - // Do blocking calls or whatever is necessary for sensor polling. - // In our case, we simply update the HRM measurement. - _hr_counter++; - - // 100 <= HRM bps <=175 - if (_hr_counter == 175) { - _hr_counter = 100; - } - - _hr_service.updateHeartRate(_hr_counter); - } - } - - // void blink(void) { _led1 = !_led1; } - - private: - /* Event handler */ - - void onDisconnectionComplete(const ble::DisconnectionCompleteEvent &) override - { - _ble.gap().startAdvertising(ble::LEGACY_ADVERTISING_HANDLE); - _connected = false; - } - - void onConnectionComplete(const ble::ConnectionCompleteEvent &event) override - { - if (event.getStatus() == BLE_ERROR_NONE) { - _connected = true; - } - } - - private: - BLE &_ble; - events::EventQueue &_event_queue; - - bool _connected; - UUID _hr_uuid; - uint8_t _hr_counter; - HeartRateService _hr_service; - - std::array _adv_buffer; - ble::AdvertisingDataBuilder _adv_data_builder; -}; diff --git a/libs/InvestigationDay/LekaRFID/CMakeLists.txt b/libs/InvestigationDay/LekaRFID/CMakeLists.txt deleted file mode 100644 index 5c04895859..0000000000 --- a/libs/InvestigationDay/LekaRFID/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -# Leka - LekaOS -# Copyright 2020 APF France handicap -# SPDX-License-Identifier: Apache-2.0 - -add_library(lib_LekaRFID STATIC) - -target_include_directories(lib_LekaRFID - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include -) - -target_sources(lib_LekaRFID - PRIVATE - source/LekaRFID.cpp -) - -target_link_libraries(lib_LekaRFID mbed-os) diff --git a/libs/InvestigationDay/LekaRFID/include/LekaRFID.h b/libs/InvestigationDay/LekaRFID/include/LekaRFID.h deleted file mode 100644 index eca2b89dbd..0000000000 --- a/libs/InvestigationDay/LekaRFID/include/LekaRFID.h +++ /dev/null @@ -1,59 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "PinNames.h" - -#include "drivers/BufferedSerial.h" -#include "rtos/ThisThread.h" -#include "rtos/Thread.h" - -class RFID -{ - public: - RFID(); - ~RFID() {}; - - void start(void); - - bool checkConnected(); - - bool echo(); - bool getID(); - void fieldOff(); - bool setIEC15693(); - bool setIEC14443(); - bool setReceiverGain(); - void sendReceive(uint8_t val); - - size_t getAnswer(char *buffer); - - private: - mbed::BufferedSerial _interface; - - const uint8_t _echo_cmd[1] = {0x55}; - const uint8_t _echo_cmd_length = 1; - const uint8_t _idn_cmd[2] = {0x01, 0x00}; - const uint8_t _idn_cmd_length = 2; - const uint8_t _field_off_cmd[4] = {0x02, 0x02, 0x00, 0x00}; - const uint8_t _field_off_cmd_length = 4; - const uint8_t _iec_15693_cmd[4] = {0x02, 0x02, 0x01, 0x09}; - const uint8_t _iec_15693_cmd_length = 4; - const uint8_t _iec_14443_cmd[4] = {0x02, 0x02, 0x02, 0x00}; - const uint8_t _iec_14443_cmd_length = 4; - const uint8_t _set_receiver_gain_cmd[6] = {0x09, 0x04, 0x68, 0x01, 0x01, 0xD1}; - const uint8_t _set_receiver_gain_cmd_length = 6; - const uint8_t _send_receive_cmd[5] = {0x04, 0x03, 0x26, 0x01, 0x00}; - const uint8_t _send_receive_cmd_length = 5; - const uint8_t _send_receive2_cmd[4] = {0x04, 0x02, 0x26, 0x07}; - const uint8_t _send_receive2_cmd_length = 4; - const uint8_t _send_receive3_cmd[5] = {0x04, 0x03, 0x93, 0x20, 0x08}; - const uint8_t _send_receive3_cmd_length = 5; - - uint8_t _answer[64]; - size_t _answer_length = 0; -}; diff --git a/libs/InvestigationDay/LekaRFID/source/LekaRFID.cpp b/libs/InvestigationDay/LekaRFID/source/LekaRFID.cpp deleted file mode 100644 index 5e5ad9c159..0000000000 --- a/libs/InvestigationDay/LekaRFID/source/LekaRFID.cpp +++ /dev/null @@ -1,209 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "LekaRFID.h" - -using namespace mbed; -using namespace std::chrono; - -RFID::RFID() : _interface(RFID_UART_TX, RFID_UART_RX, 57600) {} - -bool RFID::echo() -{ - uint8_t buffer[1] = {0x00}; - const uint8_t aimed_buffer_length = 0x1; - uint8_t aimed_buffer[aimed_buffer_length] = {0x55}; - - _interface.write(_echo_cmd, _echo_cmd_length); - rtos::ThisThread::sleep_for(10ms); - - for (int i = 0; i < 10; i++) { - if (_interface.readable()) { - _interface.read(buffer, aimed_buffer_length); - if ((memcmp(aimed_buffer, buffer, aimed_buffer_length) == 0)) { - return true; - } - } - } - return false; -} - -bool RFID::getID() -{ - const uint8_t max_buffer_length = 0x20; // TODO: what is the maximum length that we can receive? - uint8_t buffer[max_buffer_length] = {0}; - - const uint8_t aimed_buffer_length = 0x11; - const uint8_t aimed_buffer[aimed_buffer_length] = {0x00, 0x0F, 0x4E, 0x46, 0x43, 0x20, 0x46, 0x53, 0x32, - 0x4A, 0x41, 0x53, 0x54, 0x34, 0x00, 0x2A, 0xCE}; - - _interface.write(_idn_cmd, _idn_cmd_length); - rtos::ThisThread::sleep_for(10ms); - - for (int i = 0; i < 10; i++) { - if (_interface.readable()) { - _interface.read(buffer, 2); - if (buffer[0] == 0x00) { - auto length = buffer[1]; - // TODO: shoudld we check that length < max_buffer_length? - _interface.read(&buffer[2], length); - // TODO: which id are we talking about? does it mean that the RFID id never changes? - if ((memcmp(aimed_buffer, buffer, aimed_buffer_length) == 0)) { - return true; - } - } - } - } - return false; -} - -bool RFID::checkConnected() -{ - return echo(); -} - -void RFID::fieldOff() -{ - _interface.write(_field_off_cmd, _field_off_cmd_length); - rtos::ThisThread::sleep_for(1ms); -} - -bool RFID::setIEC15693() -{ - uint8_t buffer[2] = {0x00}; - const uint8_t aimed_buffer_length = 0x02; - uint8_t aimed_buffer[aimed_buffer_length] = {0x00, 0x00}; - - _interface.write(_iec_15693_cmd, _iec_15693_cmd_length); - rtos::ThisThread::sleep_for(10ms); - - for (int i = 0; i < 10; i++) { - if (_interface.readable()) { - _interface.read(buffer, aimed_buffer_length); - if ((memcmp(aimed_buffer, buffer, aimed_buffer_length) == 0)) { - return true; - } - } - } - return false; -} - -bool RFID::setReceiverGain() -{ - uint8_t buffer[2] = {0x00}; - const uint8_t aimed_buffer_length = 0x2; - uint8_t aimed_buffer[aimed_buffer_length] = {0x00, 0x00}; - - _interface.write(_set_receiver_gain_cmd, _set_receiver_gain_cmd_length); - rtos::ThisThread::sleep_for(10ms); - - // TODO: why a for loop? why not while(!_interface.readable())? - for (int i = 0; i < 10; i++) { - if (_interface.readable()) { - _interface.read(buffer, aimed_buffer_length); - // TODO: check the second for loop and the use of the index - for (int j = 0; j < aimed_buffer_length; j++) { - printf("%X ", buffer[j]); - } - printf("\n"); - if ((memcmp(aimed_buffer, buffer, aimed_buffer_length) == 0)) { - return true; - } - } - } - return false; -} - -void RFID::sendReceive(uint8_t val) -{ - const uint8_t max_buffer_length = 0x20; // TODO: what is the maximum length that we can receive? - uint8_t buffer[max_buffer_length] = {0}; - - if (val == 1) { - _interface.write(_send_receive_cmd, _send_receive_cmd_length); - } else if (val == 2) { - _interface.write(_send_receive2_cmd, _send_receive2_cmd_length); - } else if (val == 3) { - _interface.write(_send_receive3_cmd, _send_receive3_cmd_length); - } - rtos::ThisThread::sleep_for(10ms); - - // TODO: why a for loop? why not while(!_interface.readable())? - for (int i = 0; i < 10; i++) { - if (_interface.readable()) { - _interface.read(buffer, 2); - auto length = buffer[1]; - _interface.read(&buffer[2], length); - - // TODO: check the second for loop and the use of the index - _answer_length = length + 2; - for (int j = 0; j < length + 2; j++) { - _answer[j] = buffer[j]; - printf("%X ", buffer[j]); - } - printf("\n"); - return; - } - } - printf("No answer received from reader...\n"); - return; -} -size_t RFID::getAnswer(char *buffer) -{ - for (uint16_t i = 0; i < _answer_length; i++) { - buffer[i] = _answer[i]; - } - return _answer_length; -} - -bool RFID::setIEC14443() -{ - uint8_t buffer[2] = {0x00}; - const uint8_t aimed_buffer_length = 0x02; - uint8_t aimed_buffer[aimed_buffer_length] = {0x00, 0x00}; - - _interface.write(_iec_14443_cmd, _iec_14443_cmd_length); - rtos::ThisThread::sleep_for(10ms); - - for (int i = 0; i < 10; i++) { - if (_interface.readable()) { - _interface.read(buffer, aimed_buffer_length); - if ((memcmp(aimed_buffer, buffer, aimed_buffer_length) == 0)) { - return true; - } - } - } - return false; -} - -void RFID::start() -{ - printf("RFID example\n\n"); - - while (!checkConnected()) { - printf("RFID reader is not connected...\n"); - rtos::ThisThread::sleep_for(1s); - } - printf("RFID reader detected!\n\n"); - - while (!setIEC14443()) { - printf("Attempt to enable RFID reader...\n"); - rtos::ThisThread::sleep_for(1s); - } - printf("RFID reader enable with IEC 14443!\n"); - - while (!setReceiverGain()) { - printf("Attempt to set RFID reader gain...\n"); - rtos::ThisThread::sleep_for(1s); - } - printf("RFID reader gain set!\n"); - - while (true) { - sendReceive(2); - sendReceive(3); - rtos::ThisThread::sleep_for(1s); - } - - printf("End of RFID example\n\n"); -} diff --git a/libs/InvestigationDay/LekaScreen/CMakeLists.txt b/libs/InvestigationDay/LekaScreen/CMakeLists.txt deleted file mode 100644 index e2c6e993f4..0000000000 --- a/libs/InvestigationDay/LekaScreen/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# Leka - LekaOS -# Copyright 2020 APF France handicap -# SPDX-License-Identifier: Apache-2.0 - -add_library(lib_LekaScreen STATIC) - -target_include_directories(lib_LekaScreen - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${CMAKE_CURRENT_SOURCE_DIR}/include/internal -) - -target_sources(lib_LekaScreen - PRIVATE - source/LekaLCD.cpp - source/LekaScreen.cpp -) - -target_link_libraries(lib_LekaScreen mbed-os) diff --git a/libs/InvestigationDay/LekaScreen/include/LekaLCD.h b/libs/InvestigationDay/LekaScreen/include/LekaLCD.h deleted file mode 100644 index 100ffa6236..0000000000 --- a/libs/InvestigationDay/LekaScreen/include/LekaLCD.h +++ /dev/null @@ -1,101 +0,0 @@ - - -#ifndef __LEKALCD_H__ -#define __LEKALCD_H__ - -#include "platform/Stream.h" - -#include "otm8009a.h" - -class LekaLCD -{ - public: - /** - * @brief Construct a new Leka LCD object and - * does all the initialization needed - */ - LekaLCD(); - - uint32_t getScreenWidth(); - uint32_t getScreenHeight(); - - void turnOff(); - void turnOn(); - - void LTDC_LayerInit(uint16_t layer_index); - void setActiveLayer(uint16_t layer_index); - - /** - * @brief Clears the active layer with a color - * - * @param color : Color - */ - void clear(uint32_t color); - - /** - * @brief Set the color of a pixel - * - * @param x : x position - * @param y : y position - * @param color : color of the pixel in ARGB8888 format - */ - void drawPixel(uint32_t x, uint32_t y, uint32_t color); - - /** - * @brief Returns the pixel color at the position (x, y) - * - * @param x : x position - * @param y : y position - * @return uint32_t : color of the pixel in ARGB8888 format - */ - uint32_t readPixel(uint16_t x, uint16_t y); - - /** - * @brief Fills a rectangle on the active layer with a color - * - * @param x : x position - * @param y : y position - * @param width : width - * @param height : height - * @param color : color - */ - void fillRect(uint32_t x, uint32_t y, uint32_t width, uint32_t height, uint32_t color); - - private: - DMA2D_HandleTypeDef _handle_dma2d; - LTDC_HandleTypeDef _handle_ltdc; - DSI_HandleTypeDef _handle_dsi; - DSI_VidCfgTypeDef _handle_dsivideo; - SDRAM_HandleTypeDef _handle_sdram; - - // using landscape orientation by default - const uint32_t _screen_width = 800; - const uint32_t _screen_height = 480; - - const uint32_t _frame_buffer_start_address = 0xC0000000; - - // active layer can be either 0 or 1 (LTDC supports 2 layers) - uint16_t _active_layer = 0; - - /** - * @brief Fills a rectangle in the frame buffer - * - * @param layer_index : Layer index where to draw (0 or 1) - * @param dest_addr : Frame buffer start address - * @param width : Width of the rectangle - * @param height : Height of the rectangle - * @param offset : offset = screen_width - rectangle_width - * @param color : Color in ARGB8888 format - */ - void fillBuffer(uint32_t layer_index, void *dest_addr, uint32_t width, uint32_t height, uint32_t offset, - uint32_t color); - - // internal init functions - void reset(); - void MspInit(); - void DSI_IO_WriteCmd(uint32_t NbrParams, uint8_t *pParams); - uint8_t OTM8009A_Init(uint32_t ColorCoding, uint32_t orientation); - void SDRAM_init(); -}; - -#endif diff --git a/libs/InvestigationDay/LekaScreen/include/LekaScreen.h b/libs/InvestigationDay/LekaScreen/include/LekaScreen.h deleted file mode 100644 index 8dde4b9df0..0000000000 --- a/libs/InvestigationDay/LekaScreen/include/LekaScreen.h +++ /dev/null @@ -1,26 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "PinNames.h" - -#include "drivers/PwmOut.h" -#include "rtos/ThisThread.h" -#include "rtos/Thread.h" - -#include "LekaLCD.h" - -class Screen -{ - public: - Screen(); - ~Screen() {}; - - void start(void); - - private: - mbed::PwmOut _brightness; - LekaLCD _lcd; -}; diff --git a/libs/InvestigationDay/LekaScreen/include/internal/otm8009a.h b/libs/InvestigationDay/LekaScreen/include/internal/otm8009a.h deleted file mode 100644 index 02e9890859..0000000000 --- a/libs/InvestigationDay/LekaScreen/include/internal/otm8009a.h +++ /dev/null @@ -1,334 +0,0 @@ -/** - ****************************************************************************** - * @file otm8009a.h - * @author MCD Application Team - * @brief This file contains all the constants parameters for the OTM8009A - * which is the LCD Driver for KoD KM-040TMP-02-0621 (WVGA) - * DSI LCD Display. - ****************************************************************************** - * @attention - * - *

© COPYRIGHT(c) 2017 STMicroelectronics

- * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of STMicroelectronics nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - ****************************************************************************** - */ - -/* Define to prevent recursive inclusion -------------------------------------*/ -#ifndef __OTM8009A_H - #define __OTM8009A_H - //#include "mbed-os/targets/TARGET_STM/TARGET_STM32F7/STM32Cube_FW/STM32F7xx_HAL_Driver/stm32f7xx_hal.h" - #include "stm32f7xx_hal.h" - - #ifdef __cplusplus -extern "C" { - #endif - /* Includes ------------------------------------------------------------------*/ - #include -/** @addtogroup BSP - * @{ - */ - -/** @addtogroup Components - * @{ - */ - -/** @addtogroup otm8009a - * @{ - */ - -/** @addtogroup OTM8009A_Exported_Variables - * @{ - */ - - #if defined(__GNUC__) - #ifndef __weak - #define __weak __attribute__((weak)) - #endif /* __weak */ - #endif /* __GNUC__ */ - - /** - * @brief LCD_OrientationTypeDef - * Possible values of Display Orientation - */ - #define OTM8009A_ORIENTATION_PORTRAIT ((uint32_t)0x00) /* Portrait orientation choice of LCD screen */ - #define OTM8009A_ORIENTATION_LANDSCAPE ((uint32_t)0x01) /* Landscape orientation choice of LCD screen */ - - /** - * @brief Possible values of - * pixel data format (ie color coding) transmitted on DSI Data lane in DSI packets - */ - #define OTM8009A_FORMAT_RGB888 ((uint32_t)0x00) /* Pixel format chosen is RGB888 : 24 bpp */ - #define OTM8009A_FORMAT_RBG565 ((uint32_t)0x02) /* Pixel format chosen is RGB565 : 16 bpp */ - - /** - * @brief otm8009a_480x800 Size - */ - - /* Width and Height in Portrait mode */ - #define OTM8009A_480X800_WIDTH ((uint16_t)480) /* LCD PIXEL WIDTH */ - #define OTM8009A_480X800_HEIGHT ((uint16_t)800) /* LCD PIXEL HEIGHT */ - - /* Width and Height in Landscape mode */ - #define OTM8009A_800X480_WIDTH ((uint16_t)800) /* LCD PIXEL WIDTH */ - #define OTM8009A_800X480_HEIGHT ((uint16_t)480) /* LCD PIXEL HEIGHT */ - - /** - * @brief OTM8009A_480X800 Timing parameters for Portrait orientation mode - */ - #define OTM8009A_480X800_HSYNC ((uint16_t)2) /* Horizontal synchronization */ - #define OTM8009A_480X800_HBP ((uint16_t)34) /* Horizontal back porch */ - #define OTM8009A_480X800_HFP ((uint16_t)34) /* Horizontal front porch */ - #define OTM8009A_480X800_VSYNC ((uint16_t)1) /* Vertical synchronization */ - #define OTM8009A_480X800_VBP ((uint16_t)15) /* Vertical back porch */ - #define OTM8009A_480X800_VFP ((uint16_t)16) /* Vertical front porch */ - - /** - * @brief OTM8009A_800X480 Timing parameters for Landscape orientation mode - * Same values as for Portrait mode in fact. - */ - #define OTM8009A_800X480_HSYNC OTM8009A_480X800_VSYNC /* Horizontal synchronization */ - #define OTM8009A_800X480_HBP OTM8009A_480X800_VBP /* Horizontal back porch */ - #define OTM8009A_800X480_HFP OTM8009A_480X800_VFP /* Horizontal front porch */ - #define OTM8009A_800X480_VSYNC OTM8009A_480X800_HSYNC /* Vertical synchronization */ - #define OTM8009A_800X480_VBP OTM8009A_480X800_HBP /* Vertical back porch */ - #define OTM8009A_800X480_VFP OTM8009A_480X800_HFP /* Vertical front porch */ - - /* List of OTM8009A used commands */ - /* Detailed in OTM8009A Data Sheet 'DATA_SHEET_OTM8009A_V0 92.pdf' */ - /* Version of 14 June 2012 */ - #define OTM8009A_CMD_NOP 0x00 /* NOP command */ - #define OTM8009A_CMD_SWRESET 0x01 /* Sw reset command */ - #define OTM8009A_CMD_RDDMADCTL 0x0B /* Read Display MADCTR command : read memory display access ctrl */ - #define OTM8009A_CMD_RDDCOLMOD 0x0C /* Read Display pixel format */ - #define OTM8009A_CMD_SLPIN 0x10 /* Sleep In command */ - #define OTM8009A_CMD_SLPOUT 0x11 /* Sleep Out command */ - #define OTM8009A_CMD_PTLON 0x12 /* Partial mode On command */ - - #define OTM8009A_CMD_DISPOFF 0x28 /* Display Off command */ - #define OTM8009A_CMD_DISPON 0x29 /* Display On command */ - - #define OTM8009A_CMD_CASET 0x2A /* Column address set command */ - #define OTM8009A_CMD_PASET 0x2B /* Page address set command */ - - #define OTM8009A_CMD_RAMWR 0x2C /* Memory (GRAM) write command */ - #define OTM8009A_CMD_RAMRD 0x2E /* Memory (GRAM) read command */ - - #define OTM8009A_CMD_PLTAR 0x30 /* Partial area command (4 parameters) */ - - #define OTM8009A_CMD_TEOFF 0x34 /* Tearing Effect Line Off command : command with no parameter */ - - #define OTM8009A_CMD_TEEON 0x35 /* Tearing Effect Line On command : command with 1 parameter 'TELOM' */ - - /* Parameter TELOM : Tearing Effect Line Output Mode : possible values */ - #define OTM8009A_TEEON_TELOM_VBLANKING_INFO_ONLY 0x00 - #define OTM8009A_TEEON_TELOM_VBLANKING_AND_HBLANKING_INFO 0x01 - - #define OTM8009A_CMD_MADCTR 0x36 /* Memory Access write control command */ - - /* Possible used values of MADCTR */ - #define OTM8009A_MADCTR_MODE_PORTRAIT 0x00 - #define OTM8009A_MADCTR_MODE_LANDSCAPE 0xA0 /* MY = 0, MX = 1, MV = 1, ML = 0, RGB = 0 */ - - #define OTM8009A_CMD_IDMOFF 0x38 /* Idle mode Off command */ - #define OTM8009A_CMD_IDMON 0x39 /* Idle mode On command */ - - #define OTM8009A_CMD_COLMOD 0x3A /* Interface Pixel format command */ - - /* Possible values of COLMOD parameter corresponding to used pixel formats */ - #define OTM8009A_COLMOD_RGB565 0x55 - #define OTM8009A_COLMOD_RGB888 0x77 - - #define OTM8009A_CMD_RAMWRC 0x3C /* Memory write continue command */ - #define OTM8009A_CMD_RAMRDC 0x3E /* Memory read continue command */ - - #define OTM8009A_CMD_WRTESCN 0x44 /* Write Tearing Effect Scan line command */ - #define OTM8009A_CMD_RDSCNL 0x45 /* Read Tearing Effect Scan line command */ - - /* CABC Management : ie : Content Adaptive Back light Control in IC OTM8009a */ - #define OTM8009A_CMD_WRDISBV 0x51 /* Write Display Brightness command */ - #define OTM8009A_CMD_WRCTRLD 0x53 /* Write CTRL Display command */ - #define OTM8009A_CMD_WRCABC 0x55 /* Write Content Adaptive Brightness command */ - #define OTM8009A_CMD_WRCABCMB 0x5E /* Write CABC Minimum Brightness command */ - - /** - * @brief OTM8009A_480X800 frequency divider - */ - #define OTM8009A_480X800_FREQUENCY_DIVIDER 2 /* LCD Frequency divider */ - -/* - * @brief Constant tables of register settings used to transmit DSI - * command packets as power up initialization sequence of the KoD LCD (OTM8009A LCD Driver) - */ -const uint8_t lcdRegData1[] = {0x80, 0x09, 0x01, 0xFF}; -const uint8_t lcdRegData2[] = {0x80, 0x09, 0xFF}; -const uint8_t lcdRegData3[] = {0x00, 0x09, 0x0F, 0x0E, 0x07, 0x10, 0x0B, 0x0A, 0x04, - 0x07, 0x0B, 0x08, 0x0F, 0x10, 0x0A, 0x01, 0xE1}; -const uint8_t lcdRegData4[] = {0x00, 0x09, 0x0F, 0x0E, 0x07, 0x10, 0x0B, 0x0A, 0x04, - 0x07, 0x0B, 0x08, 0x0F, 0x10, 0x0A, 0x01, 0xE2}; -const uint8_t lcdRegData5[] = {0x79, 0x79, 0xD8}; -const uint8_t lcdRegData6[] = {0x00, 0x01, 0xB3}; -const uint8_t lcdRegData7[] = {0x85, 0x01, 0x00, 0x84, 0x01, 0x00, 0xCE}; -const uint8_t lcdRegData8[] = {0x18, 0x04, 0x03, 0x39, 0x00, 0x00, 0x00, 0x18, - 0x03, 0x03, 0x3A, 0x00, 0x00, 0x00, 0xCE}; -const uint8_t lcdRegData9[] = {0x18, 0x02, 0x03, 0x3B, 0x00, 0x00, 0x00, 0x18, - 0x01, 0x03, 0x3C, 0x00, 0x00, 0x00, 0xCE}; -const uint8_t lcdRegData10[] = {0x01, 0x01, 0x20, 0x20, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0xCF}; -const uint8_t lcdRegData11[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCB}; -const uint8_t lcdRegData12[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCB}; -const uint8_t lcdRegData13[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCB}; -const uint8_t lcdRegData14[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCB}; -const uint8_t lcdRegData15[] = {0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCB}; -const uint8_t lcdRegData16[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, - 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0xCB}; -const uint8_t lcdRegData17[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCB}; -const uint8_t lcdRegData18[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCB}; -const uint8_t lcdRegData19[] = {0x00, 0x26, 0x09, 0x0B, 0x01, 0x25, 0x00, 0x00, 0x00, 0x00, 0xCC}; -const uint8_t lcdRegData20[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x26, 0x0A, 0x0C, 0x02, 0xCC}; -const uint8_t lcdRegData21[] = {0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCC}; -const uint8_t lcdRegData22[] = {0x00, 0x25, 0x0C, 0x0A, 0x02, 0x26, 0x00, 0x00, 0x00, 0x00, 0xCC}; -const uint8_t lcdRegData23[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x25, 0x0B, 0x09, 0x01, 0xCC}; -const uint8_t lcdRegData24[] = {0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCC}; -const uint8_t lcdRegData25[] = {0xFF, 0xFF, 0xFF, 0xFF}; -/* - * CASET value (Column Address Set) : X direction LCD GRAM boundaries - * depending on LCD orientation mode and PASET value (Page Address Set) : Y direction - * LCD GRAM boundaries depending on LCD orientation mode - * XS[15:0] = 0x000 = 0, XE[15:0] = 0x31F = 799 for landscape mode : apply to CASET - * YS[15:0] = 0x000 = 0, YE[15:0] = 0x31F = 799 for portrait mode : : apply to PASET - */ -const uint8_t lcdRegData27[] = {0x00, 0x00, 0x03, 0x1F, OTM8009A_CMD_CASET}; -// const uint8_t lcdRegData27[] = {0x03, 0x1F, 0x00, 0x00, OTM8009A_CMD_CASET}; - -/* - * XS[15:0] = 0x000 = 0, XE[15:0] = 0x1DF = 479 for portrait mode : apply to CASET - * YS[15:0] = 0x000 = 0, YE[15:0] = 0x1DF = 479 for landscape mode : apply to PASET - */ -const uint8_t lcdRegData28[] = {0x00, 0x00, 0x01, 0xDF, OTM8009A_CMD_PASET}; -// const uint8_t lcdRegData28[] = {0x01, 0xDF, 0x00, 0x00, OTM8009A_CMD_PASET}; - -const uint8_t ShortRegData1[] = {OTM8009A_CMD_NOP, 0x00}; -const uint8_t ShortRegData2[] = {OTM8009A_CMD_NOP, 0x80}; -const uint8_t ShortRegData3[] = {0xC4, 0x30}; -const uint8_t ShortRegData4[] = {OTM8009A_CMD_NOP, 0x8A}; -const uint8_t ShortRegData5[] = {0xC4, 0x40}; -const uint8_t ShortRegData6[] = {OTM8009A_CMD_NOP, 0xB1}; -const uint8_t ShortRegData7[] = {0xC5, 0xA9}; -const uint8_t ShortRegData8[] = {OTM8009A_CMD_NOP, 0x91}; -const uint8_t ShortRegData9[] = {0xC5, 0x34}; -const uint8_t ShortRegData10[] = {OTM8009A_CMD_NOP, 0xB4}; -const uint8_t ShortRegData11[] = {0xC0, 0x50}; -const uint8_t ShortRegData12[] = {0xD9, 0x4E}; -const uint8_t ShortRegData13[] = {OTM8009A_CMD_NOP, 0x81}; -const uint8_t ShortRegData14[] = {0xC1, 0x66}; -const uint8_t ShortRegData15[] = {OTM8009A_CMD_NOP, 0xA1}; -const uint8_t ShortRegData16[] = {0xC1, 0x08}; -const uint8_t ShortRegData17[] = {OTM8009A_CMD_NOP, 0x92}; -const uint8_t ShortRegData18[] = {0xC5, 0x01}; -const uint8_t ShortRegData19[] = {OTM8009A_CMD_NOP, 0x95}; -const uint8_t ShortRegData20[] = {OTM8009A_CMD_NOP, 0x94}; -const uint8_t ShortRegData21[] = {0xC5, 0x33}; -const uint8_t ShortRegData22[] = {OTM8009A_CMD_NOP, 0xA3}; -const uint8_t ShortRegData23[] = {0xC0, 0x1B}; -const uint8_t ShortRegData24[] = {OTM8009A_CMD_NOP, 0x82}; -const uint8_t ShortRegData25[] = {0xC5, 0x83}; -const uint8_t ShortRegData26[] = {0xC4, 0x83}; -const uint8_t ShortRegData27[] = {0xC1, 0x0E}; -const uint8_t ShortRegData28[] = {OTM8009A_CMD_NOP, 0xA6}; -const uint8_t ShortRegData29[] = {OTM8009A_CMD_NOP, 0xA0}; -const uint8_t ShortRegData30[] = {OTM8009A_CMD_NOP, 0xB0}; -const uint8_t ShortRegData31[] = {OTM8009A_CMD_NOP, 0xC0}; -const uint8_t ShortRegData32[] = {OTM8009A_CMD_NOP, 0xD0}; -const uint8_t ShortRegData33[] = {OTM8009A_CMD_NOP, 0x90}; -const uint8_t ShortRegData34[] = {OTM8009A_CMD_NOP, 0xE0}; -const uint8_t ShortRegData35[] = {OTM8009A_CMD_NOP, 0xF0}; -const uint8_t ShortRegData36[] = {OTM8009A_CMD_SLPOUT, 0x00}; -const uint8_t ShortRegData37[] = {OTM8009A_CMD_COLMOD, OTM8009A_COLMOD_RGB565}; -const uint8_t ShortRegData38[] = {OTM8009A_CMD_COLMOD, OTM8009A_COLMOD_RGB888}; -const uint8_t ShortRegData39[] = {OTM8009A_CMD_MADCTR, OTM8009A_MADCTR_MODE_LANDSCAPE}; -const uint8_t ShortRegData40[] = {OTM8009A_CMD_WRDISBV, 0x7F}; -const uint8_t ShortRegData41[] = {OTM8009A_CMD_WRCTRLD, 0x2C}; -const uint8_t ShortRegData42[] = {OTM8009A_CMD_WRCABC, 0x02}; -const uint8_t ShortRegData43[] = {OTM8009A_CMD_WRCABCMB, 0xFF}; -const uint8_t ShortRegData44[] = {OTM8009A_CMD_DISPON, 0x00}; -const uint8_t ShortRegData45[] = {OTM8009A_CMD_RAMWR, 0x00}; -const uint8_t ShortRegData46[] = {0xCF, 0x00}; -const uint8_t ShortRegData47[] = {0xC5, 0x66}; -const uint8_t ShortRegData48[] = {OTM8009A_CMD_NOP, 0xB6}; -const uint8_t ShortRegData49[] = {0xF5, 0x06}; -const uint8_t ShortRegData50[] = {OTM8009A_CMD_NOP, 0xB1}; -const uint8_t ShortRegData51[] = {0xC6, 0x06}; - /** - * @} - */ - /** - * @} - */ - - /* Exported macro ------------------------------------------------------------*/ - - /** @defgroup OTM8009A_Exported_Macros OTM8009A Exported Macros - * @{ - */ - - /** - * @} - */ - - /* Exported functions --------------------------------------------------------*/ - - /** @addtogroup OTM8009A_Exported_Functions - * @{ - */ - /*void DSI_IO_WriteCmd(uint32_t NbrParams, uint8_t *pParams); - uint8_t OTM8009A_Init(uint32_t ColorCoding, uint32_t orientation); - void OTM8009A_IO_Delay(uint32_t Delay);*/ - /** - * @} - */ - #ifdef __cplusplus -} - #endif - -#endif /* __OTM8009A_480X800_H */ -/** - * @} - */ - -/** - * @} - */ - -/** - * @} - */ - -/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ diff --git a/libs/InvestigationDay/LekaScreen/source/LekaLCD.cpp b/libs/InvestigationDay/LekaScreen/source/LekaLCD.cpp deleted file mode 100644 index 326db8728d..0000000000 --- a/libs/InvestigationDay/LekaScreen/source/LekaLCD.cpp +++ /dev/null @@ -1,712 +0,0 @@ -#include "LekaLCD.h" - -LekaLCD::LekaLCD() -{ - reset(); - MspInit(); - - // configuring clock for DSI (following BSP) - DSI_PLLInitTypeDef dsi_pll_init; - dsi_pll_init.PLLNDIV = 100; // clock loop division factor - dsi_pll_init.PLLIDF = DSI_PLL_IN_DIV5; // clock input division factor - dsi_pll_init.PLLODF = DSI_PLL_OUT_DIV1; // clock output division factor - uint32_t lane_byte_clock_kHz = 62500; // 500 MHz / 8 = 62.5 MHz = 62500 KHz - uint32_t lcd_clock = 27429; // 27429 kHz - - ///////////// DSI Initialization ///////////////////////// - printf("--DSI init--\n"); - _handle_dsi.Instance = DSI; - int val = HAL_DSI_DeInit(&_handle_dsi); - printf("HAL_DSI_DeInit %d\n", val); - _handle_dsi.Init.NumberOfLanes = DSI_TWO_DATA_LANES; // number of lanes -> 2 - // TXEscapeCKdiv = lane_byte_clock_kHz / 15620 = 4 - _handle_dsi.Init.TXEscapeCkdiv = lane_byte_clock_kHz / 15620; - val = HAL_DSI_Init(&_handle_dsi, &dsi_pll_init); - printf("HAL_DSI_Init %d\n", val); - - // video synchronisation parameters, all values in units of line (= in pixels ?) - // these values depend on orientation, I picked landscape values - uint32_t VSA = OTM8009A_480X800_VSYNC; // Vertical start active time - uint32_t VBP = OTM8009A_480X800_VBP; // Vertical back porch time - uint32_t VFP = OTM8009A_480X800_VFP; // Vertical front porch time - uint32_t VACT = _screen_height; // Vertical Active time - uint32_t HSA = OTM8009A_480X800_HSYNC; // Idem for horizontal - uint32_t HBP = OTM8009A_480X800_HBP; - uint32_t HFP = OTM8009A_480X800_HFP; - uint32_t HACT = _screen_width; - - _handle_dsivideo.VirtualChannelID = 0; // LCD_OTM8009A_ID = 0 - _handle_dsivideo.ColorCoding = DSI_RGB888; // = LCD_DSI_PIXEL_DATA_FMT_RGB888 - _handle_dsivideo.VSPolarity = DSI_VSYNC_ACTIVE_HIGH; - _handle_dsivideo.HSPolarity = DSI_HSYNC_ACTIVE_HIGH; - _handle_dsivideo.DEPolarity = DSI_DATA_ENABLE_ACTIVE_HIGH; - _handle_dsivideo.Mode = DSI_VID_MODE_BURST; - _handle_dsivideo.NullPacketSize = 0xFFF; - _handle_dsivideo.NumberOfChunks = 0; - _handle_dsivideo.PacketSize = HACT; - - _handle_dsivideo.HorizontalSyncActive = (HSA * lane_byte_clock_kHz) / lcd_clock; - _handle_dsivideo.HorizontalBackPorch = (HBP * lane_byte_clock_kHz) / lcd_clock; - _handle_dsivideo.HorizontalLine = ((HACT + HSA + HBP + HFP) * lane_byte_clock_kHz) / lcd_clock; - _handle_dsivideo.VerticalSyncActive = VSA; - _handle_dsivideo.VerticalBackPorch = VBP; - _handle_dsivideo.VerticalFrontPorch = VFP; - _handle_dsivideo.VerticalActive = VACT; - - // enable sending commands in low power mode - // maybe we want to disable it ? - _handle_dsivideo.LPCommandEnable = DSI_LP_COMMAND_ENABLE; - - _handle_dsivideo.LPLargestPacketSize = 16; // low power largest packet - _handle_dsivideo.LPVACTLargestPacketSize = 0; // low power largest packet during VACT period - - _handle_dsivideo.LPHorizontalFrontPorchEnable = DSI_LP_HFP_ENABLE; // Allow sending LP commands during HFP period - _handle_dsivideo.LPHorizontalBackPorchEnable = DSI_LP_HBP_ENABLE; // Allow sending LP commands during HBP period - _handle_dsivideo.LPVerticalActiveEnable = DSI_LP_VACT_ENABLE; // Allow sending LP commands during VACT period - _handle_dsivideo.LPVerticalFrontPorchEnable = DSI_LP_VFP_ENABLE; // Allow sending LP commands during VFP period - _handle_dsivideo.LPVerticalBackPorchEnable = DSI_LP_VBP_ENABLE; // Allow sending LP commands during VBP period - _handle_dsivideo.LPVerticalSyncActiveEnable = - DSI_LP_VSYNC_ENABLE; // Allow sending LP commands during VSync = VSA period - - // configure DSI Video Mode timings with all the settings we defined - val = HAL_DSI_ConfigVideoMode(&_handle_dsi, &_handle_dsivideo); - printf("HAL_DSI_ConfigVideoMode %d\n", val); - - printf("\r"); - /////////////////////////// End DSI Initialization /////////////////////////// - - /////////////////////////// LTDC Initialization /////////////////////////// - printf("--LTDC init--\n"); - - // timing configuration (LCD-TFT documentation section 3.2.2) - _handle_ltdc.Init.HorizontalSync = HSA - 1; - _handle_ltdc.Init.AccumulatedHBP = HSA + HBP - 1; - _handle_ltdc.Init.AccumulatedActiveW = _screen_width + HSA + HBP - 1; - _handle_ltdc.Init.TotalWidth = _screen_width + HSA + HBP + HFP - 1; - - _handle_ltdc.LayerCfg->ImageWidth = _screen_width; - _handle_ltdc.LayerCfg->ImageHeight = _screen_height; - - // LCD clock configuration - static RCC_PeriphCLKInitTypeDef periph_clk_init; - periph_clk_init.PeriphClockSelection = RCC_PERIPHCLK_LTDC; - periph_clk_init.PLLSAI.PLLSAIN = 384; - periph_clk_init.PLLSAI.PLLSAIR = 7; - periph_clk_init.PLLSAIDivR = RCC_PLLSAIDIVR_2; - val = HAL_RCCEx_PeriphCLKConfig(&periph_clk_init); - printf("HAL_RCCEx_PeriphCLKConfig %d\n", val); - - // background value - _handle_ltdc.Init.Backcolor.Blue = 0xff; - _handle_ltdc.Init.Backcolor.Green = 0xff; - _handle_ltdc.Init.Backcolor.Red = 0xff; - _handle_ltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC; - _handle_ltdc.Instance = LTDC; - - // init ltdc from dsivideo config - // (basically copy horizontal and vertical synchronization values from dsivideo to ltdc) - val = HAL_LTDC_StructInitFromVideoConfig(&_handle_ltdc, &_handle_dsivideo); - printf("HAL_LTDC_StructInitFromVideoConfig %d\n", val); - - val = HAL_LTDC_Init(&_handle_ltdc); - printf("HAL_LTDC_Init %d\n", val); - - val = HAL_DSI_Start(&_handle_dsi); - printf("HAL_DSI_Start %d\n", val); - -#if !defined(DATA_IN_ExtSDRAM) - SDRAM_init(); -#endif - - val = OTM8009A_Init(OTM8009A_FORMAT_RGB888, OTM8009A_ORIENTATION_LANDSCAPE); - printf("OTM8009A_Init %d\n", val); - - printf("\r"); -} - -uint32_t LekaLCD::getScreenWidth() -{ - return _screen_width; -} - -uint32_t LekaLCD::getScreenHeight() -{ - return _screen_height; -} - -void LekaLCD::turnOff() -{ - HAL_DSI_ShortWrite(&_handle_dsi, _handle_dsivideo.VirtualChannelID, DSI_DCS_SHORT_PKT_WRITE_P1, 0x28, 0x00); -} - -void LekaLCD::turnOn() -{ - HAL_DSI_ShortWrite(&_handle_dsi, _handle_dsivideo.VirtualChannelID, DSI_DCS_SHORT_PKT_WRITE_P1, 0x29, 0x00); -} - -// Layer init (copied from BSP) -void LekaLCD::LTDC_LayerInit(uint16_t layer_index) -{ - LTDC_LayerCfgTypeDef Layercfg; - Layercfg.WindowX0 = 0; - Layercfg.WindowX1 = _screen_width; - Layercfg.WindowY0 = 0; - Layercfg.WindowY1 = _screen_height; - Layercfg.PixelFormat = LTDC_PIXEL_FORMAT_ARGB8888; - Layercfg.FBStartAdress = _frame_buffer_start_address; - Layercfg.Alpha = 255; - Layercfg.Alpha0 = 0; - Layercfg.Backcolor.Blue = 0; - Layercfg.Backcolor.Green = 0; - Layercfg.Backcolor.Red = 0; - Layercfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_PAxCA; - Layercfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_PAxCA; - Layercfg.ImageWidth = _screen_width; - Layercfg.ImageHeight = _screen_height; - - int val = HAL_LTDC_ConfigLayer(&_handle_ltdc, &Layercfg, layer_index); - printf("HAL_LTDC_ConfigLayer %d\n\r", val); -} - -void LekaLCD::setActiveLayer(uint16_t layer_index) -{ - _active_layer = layer_index; -} - -void LekaLCD::clear(uint32_t color) -{ - fillBuffer(_active_layer, (uint32_t *)(_handle_ltdc.LayerCfg[_active_layer].FBStartAdress), _screen_width, - _screen_height, 0, color); -} - -void LekaLCD::drawPixel(uint32_t x, uint32_t y, uint32_t color) -{ - *(__IO uint32_t *)(_handle_ltdc.LayerCfg[_active_layer].FBStartAdress + (4 * (y * _screen_width + x))) = color; -} - -uint32_t LekaLCD::readPixel(uint16_t Xpos, uint16_t Ypos) -{ - uint32_t ret = 0; - // Read data value from SDRAM memory (in ARGB8888 format) - ret = *(__IO uint32_t *)(_handle_ltdc.LayerCfg[_active_layer].FBStartAdress + (4 * (Ypos * _screen_width + Xpos))); - return ret; -} - -void LekaLCD::fillRect(uint32_t x, uint32_t y, uint32_t width, uint32_t height, uint32_t color) -{ - uint32_t dest_address = (_handle_ltdc.LayerCfg[_active_layer].FBStartAdress) + 4 * (_screen_width * y + x); - uint32_t offset = _screen_width - width; - - fillBuffer(_active_layer, (uint32_t *)dest_address, width, height, offset, color); -} - -void LekaLCD::fillBuffer(uint32_t layer_index, void *dest_addr, uint32_t width, uint32_t height, uint32_t offset, - uint32_t color) -{ - _handle_dma2d.Init.Mode = DMA2D_R2M; - _handle_dma2d.Init.ColorMode = DMA2D_OUTPUT_ARGB8888; - _handle_dma2d.Init.OutputOffset = offset; - _handle_dma2d.Instance = DMA2D; - - if (HAL_DMA2D_Init(&_handle_dma2d) == HAL_OK) { - if (HAL_DMA2D_ConfigLayer(&_handle_dma2d, layer_index) == HAL_OK) { - if (HAL_DMA2D_Start(&_handle_dma2d, color, (uint32_t)dest_addr, width, height) == HAL_OK) { - HAL_DMA2D_PollForTransfer(&_handle_dma2d, 10); - } - } - } -} - -// copied from BSP -void LekaLCD::reset() -{ - printf("Reset LCD..."); - GPIO_InitTypeDef gpio_init_structure; - - __HAL_RCC_GPIOJ_CLK_ENABLE(); - - /* Configure the GPIO on PJ15 */ - gpio_init_structure.Pin = GPIO_PIN_15; - gpio_init_structure.Mode = GPIO_MODE_OUTPUT_PP; - gpio_init_structure.Pull = GPIO_PULLUP; - gpio_init_structure.Speed = GPIO_SPEED_HIGH; - - HAL_GPIO_Init(GPIOJ, &gpio_init_structure); - - /* Activate XRES active low */ - HAL_GPIO_WritePin(GPIOJ, GPIO_PIN_15, GPIO_PIN_RESET); - - HAL_Delay(20); /* wait 20 ms */ - - /* Desactivate XRES */ - HAL_GPIO_WritePin(GPIOJ, GPIO_PIN_15, GPIO_PIN_SET); - - /* Wait for 10ms after releasing XRES before sending commands */ - HAL_Delay(10); - printf("DONE\n\r"); -} - -// copied from BSP -void LekaLCD::MspInit() -{ - printf("MspInit..."); - /** @brief Enable the LTDC clock */ - __HAL_RCC_LTDC_CLK_ENABLE(); - - /** @brief Toggle Sw reset of LTDC IP */ - __HAL_RCC_LTDC_FORCE_RESET(); - __HAL_RCC_LTDC_RELEASE_RESET(); - - /** @brief Enable the DMA2D clock */ - __HAL_RCC_DMA2D_CLK_ENABLE(); - - /** @brief Toggle Sw reset of DMA2D IP */ - __HAL_RCC_DMA2D_FORCE_RESET(); - __HAL_RCC_DMA2D_RELEASE_RESET(); - - /** @brief Enable DSI Host and wrapper clocks */ - __HAL_RCC_DSI_CLK_ENABLE(); - - /** @brief Soft Reset the DSI Host and wrapper */ - __HAL_RCC_DSI_FORCE_RESET(); - __HAL_RCC_DSI_RELEASE_RESET(); - - /** @brief NVIC configuration for LTDC interrupt that is now enabled */ - HAL_NVIC_SetPriority(LTDC_IRQn, 3, 0); - HAL_NVIC_EnableIRQ(LTDC_IRQn); - - /** @brief NVIC configuration for DMA2D interrupt that is now enabled */ - HAL_NVIC_SetPriority(DMA2D_IRQn, 3, 0); - HAL_NVIC_EnableIRQ(DMA2D_IRQn); - - /** @brief NVIC configuration for DSI interrupt that is now enabled */ - HAL_NVIC_SetPriority(DSI_IRQn, 3, 0); - HAL_NVIC_EnableIRQ(DSI_IRQn); - printf("DONE\n\r"); -} - -// DSI write commands -void LekaLCD::DSI_IO_WriteCmd(uint32_t NbrParams, uint8_t *pParams) -{ - if (NbrParams <= 1) { - HAL_DSI_ShortWrite(&_handle_dsi, 0, DSI_DCS_SHORT_PKT_WRITE_P1, pParams[0], pParams[1]); - } else { - HAL_DSI_LongWrite(&_handle_dsi, 0, DSI_DCS_LONG_PKT_WRITE, NbrParams, pParams[NbrParams], pParams); - } -} - -// OTM8009A driver initialization -uint8_t LekaLCD::OTM8009A_Init(uint32_t ColorCoding, uint32_t orientation) -{ - /* Enable CMD2 to access vendor specific commands */ - /* Enter in command 2 mode and set EXTC to enable address shift function (0x00) */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData1); - DSI_IO_WriteCmd(3, (uint8_t *)lcdRegData1); - - /* Enter ORISE Command 2 */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData2); /* Shift address to 0x80 */ - DSI_IO_WriteCmd(2, (uint8_t *)lcdRegData2); - - ///////////////////////////////////////////////////////////////////// - /* SD_PCH_CTRL - 0xC480h - 129th parameter - Default 0x00 */ - /* Set SD_PT */ - /* -> Source output level during porch and non-display area to GND */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData2); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData3); - HAL_Delay(10); - /* Not documented */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData4); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData5); - HAL_Delay(10); - ///////////////////////////////////////////////////////////////////// - - /* PWR_CTRL4 - 0xC4B0h - 178th parameter - Default 0xA8 */ - /* Set gvdd_en_test */ - /* -> enable GVDD test mode !!! */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData6); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData7); - - /* PWR_CTRL2 - 0xC590h - 146th parameter - Default 0x79 */ - /* Set pump 4 vgh voltage */ - /* -> from 15.0v down to 13.0v */ - /* Set pump 5 vgh voltage */ - /* -> from -12.0v downto -9.0v */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData8); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData9); - - /* P_DRV_M - 0xC0B4h - 181th parameter - Default 0x00 */ - /* -> Column inversion */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData10); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData11); - - /* VCOMDC - 0xD900h - 1st parameter - Default 0x39h */ - /* VCOM Voltage settings */ - /* -> from -1.0000v downto -1.2625v */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData1); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData12); - - /* Oscillator adjustment for Idle/Normal mode (LPDT only) set to 65Hz (default is 60Hz) */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData13); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData14); - - /* Video mode internal */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData15); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData16); - - /* PWR_CTRL2 - 0xC590h - 147h parameter - Default 0x00 */ - /* Set pump 4&5 x6 */ - /* -> ONLY VALID when PUMP4_EN_ASDM_HV = "0" */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData17); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData18); - - /* PWR_CTRL2 - 0xC590h - 150th parameter - Default 0x33h */ - /* Change pump4 clock ratio */ - /* -> from 1 line to 1/2 line */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData19); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData9); - - /* GVDD/NGVDD settings */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData1); - DSI_IO_WriteCmd(2, (uint8_t *)lcdRegData5); - - /* PWR_CTRL2 - 0xC590h - 149th parameter - Default 0x33h */ - /* Rewrite the default value ! */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData20); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData21); - - /* Panel display timing Setting 3 */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData22); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData23); - - /* Power control 1 */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData24); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData25); - - /* Source driver precharge */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData13); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData26); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData15); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData27); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData28); - DSI_IO_WriteCmd(2, (uint8_t *)lcdRegData6); - - /* GOAVST */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData2); - DSI_IO_WriteCmd(6, (uint8_t *)lcdRegData7); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData29); - DSI_IO_WriteCmd(14, (uint8_t *)lcdRegData8); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData30); - DSI_IO_WriteCmd(14, (uint8_t *)lcdRegData9); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData31); - DSI_IO_WriteCmd(10, (uint8_t *)lcdRegData10); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData32); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData46); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData2); - DSI_IO_WriteCmd(10, (uint8_t *)lcdRegData11); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData33); - DSI_IO_WriteCmd(15, (uint8_t *)lcdRegData12); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData29); - DSI_IO_WriteCmd(15, (uint8_t *)lcdRegData13); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData30); - DSI_IO_WriteCmd(10, (uint8_t *)lcdRegData14); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData31); - DSI_IO_WriteCmd(15, (uint8_t *)lcdRegData15); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData32); - DSI_IO_WriteCmd(15, (uint8_t *)lcdRegData16); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData34); - DSI_IO_WriteCmd(10, (uint8_t *)lcdRegData17); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData35); - DSI_IO_WriteCmd(10, (uint8_t *)lcdRegData18); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData2); - DSI_IO_WriteCmd(10, (uint8_t *)lcdRegData19); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData33); - DSI_IO_WriteCmd(15, (uint8_t *)lcdRegData20); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData29); - DSI_IO_WriteCmd(15, (uint8_t *)lcdRegData21); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData30); - DSI_IO_WriteCmd(10, (uint8_t *)lcdRegData22); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData31); - DSI_IO_WriteCmd(15, (uint8_t *)lcdRegData23); - - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData32); - DSI_IO_WriteCmd(15, (uint8_t *)lcdRegData24); - - ///////////////////////////////////////////////////////////////////////////// - /* PWR_CTRL1 - 0xc580h - 130th parameter - default 0x00 */ - /* Pump 1 min and max DM */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData13); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData47); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData48); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData49); - ///////////////////////////////////////////////////////////////////////////// - - /* CABC LEDPWM frequency adjusted to 19,5kHz */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData50); - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData51); - - /* Exit CMD2 mode */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData1); - DSI_IO_WriteCmd(3, (uint8_t *)lcdRegData25); - - /*************************************************************************** */ - /* Standard DCS Initialization TO KEEP CAN BE DONE IN HSDT */ - /*************************************************************************** */ - - /* NOP - goes back to DCS std command ? */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData1); - - /* Gamma correction 2.2+ table (HSDT possible) */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData1); - DSI_IO_WriteCmd(16, (uint8_t *)lcdRegData3); - - /* Gamma correction 2.2- table (HSDT possible) */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData1); - DSI_IO_WriteCmd(16, (uint8_t *)lcdRegData4); - - /* Send Sleep Out command to display : no parameter */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData36); - - /* Wait for sleep out exit */ - HAL_Delay(120); - - switch (ColorCoding) { - case OTM8009A_FORMAT_RBG565: - /* Set Pixel color format to RGB565 */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData37); - break; - case OTM8009A_FORMAT_RGB888: - /* Set Pixel color format to RGB888 */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData38); - break; - default: - break; - } - - /* Send command to configure display in landscape orientation mode. By default - the orientation mode is portrait */ - if (orientation == OTM8009A_ORIENTATION_LANDSCAPE) { - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData39); - DSI_IO_WriteCmd(4, (uint8_t *)lcdRegData27); - DSI_IO_WriteCmd(4, (uint8_t *)lcdRegData28); - } - - /** CABC : Content Adaptive Backlight Control section start >> */ - /* Note : defaut is 0 (lowest Brightness), 0xFF is highest Brightness, try 0x7F : intermediate value */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData40); - - /* defaut is 0, try 0x2C - Brightness Control Block, Display Dimming & BackLight on */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData41); - - /* defaut is 0, try 0x02 - image Content based Adaptive Brightness [Still Picture] */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData42); - - /* defaut is 0 (lowest Brightness), 0xFF is highest Brightness */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData43); - - /** CABC : Content Adaptive Backlight Control section end << */ - - /* Send Command Display On */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData44); - - /* NOP command */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData1); - - /* Send Command GRAM memory write (no parameters) : this initiates frame write via other DSI commands sent by */ - /* DSI host from LTDC incoming pixels in video mode */ - DSI_IO_WriteCmd(0, (uint8_t *)ShortRegData45); - - return 0; -} - -// SDRAM initialization (copied from BSP) -void LekaLCD::SDRAM_init() -{ - _handle_sdram.Instance = FMC_SDRAM_DEVICE; - - FMC_SDRAM_TimingTypeDef timing; - timing.LoadToActiveDelay = 2; - timing.ExitSelfRefreshDelay = 7; - timing.SelfRefreshTime = 4; - timing.RowCycleDelay = 7; - timing.WriteRecoveryTime = 2; - timing.RPDelay = 2; - timing.RCDDelay = 2; - - _handle_sdram.Init.SDBank = FMC_SDRAM_BANK1; - _handle_sdram.Init.ColumnBitsNumber = FMC_SDRAM_COLUMN_BITS_NUM_8; - _handle_sdram.Init.RowBitsNumber = FMC_SDRAM_ROW_BITS_NUM_12; - _handle_sdram.Init.MemoryDataWidth = FMC_SDRAM_MEM_BUS_WIDTH_32; - _handle_sdram.Init.InternalBankNumber = FMC_SDRAM_INTERN_BANKS_NUM_4; - _handle_sdram.Init.CASLatency = FMC_SDRAM_CAS_LATENCY_3; - _handle_sdram.Init.WriteProtection = FMC_SDRAM_WRITE_PROTECTION_DISABLE; - _handle_sdram.Init.SDClockPeriod = FMC_SDRAM_CLOCK_PERIOD_2; - _handle_sdram.Init.ReadBurst = FMC_SDRAM_RBURST_ENABLE; - _handle_sdram.Init.ReadPipeDelay = FMC_SDRAM_RPIPE_DELAY_0; - - ////// SDRAM MspInit //////////////////////////////////////////// - static DMA_HandleTypeDef dma_handle; - GPIO_InitTypeDef gpio_init_structure; - - // Enable FMC clock - __HAL_RCC_FMC_CLK_ENABLE(); - - // Enable chosen DMAx clock - __HAL_RCC_DMA2_CLK_ENABLE(); - - // Enable GPIOs clock - __HAL_RCC_GPIOD_CLK_ENABLE(); - __HAL_RCC_GPIOE_CLK_ENABLE(); - __HAL_RCC_GPIOF_CLK_ENABLE(); - __HAL_RCC_GPIOG_CLK_ENABLE(); - __HAL_RCC_GPIOH_CLK_ENABLE(); - __HAL_RCC_GPIOI_CLK_ENABLE(); - - // Common GPIO configuration - gpio_init_structure.Mode = GPIO_MODE_AF_PP; - gpio_init_structure.Pull = GPIO_PULLUP; - gpio_init_structure.Speed = GPIO_SPEED_HIGH; - gpio_init_structure.Alternate = GPIO_AF12_FMC; - - // GPIOD configuration - gpio_init_structure.Pin = - GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15; - - HAL_GPIO_Init(GPIOD, &gpio_init_structure); - - // GPIOE configuration */ - gpio_init_structure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | - GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; - - HAL_GPIO_Init(GPIOE, &gpio_init_structure); - - // GPIOF configuration */ - gpio_init_structure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | - GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; - - HAL_GPIO_Init(GPIOF, &gpio_init_structure); - - // GPIOG configuration */ - gpio_init_structure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_15; - HAL_GPIO_Init(GPIOG, &gpio_init_structure); - - // GPIOH configuration */ - gpio_init_structure.Pin = GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | - GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; - HAL_GPIO_Init(GPIOH, &gpio_init_structure); - - // GPIOI configuration */ - gpio_init_structure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | - GPIO_PIN_7 | GPIO_PIN_9 | GPIO_PIN_10; - HAL_GPIO_Init(GPIOI, &gpio_init_structure); - - // Configure common DMA parameters - dma_handle.Init.Channel = DMA_CHANNEL_0; - dma_handle.Init.Direction = DMA_MEMORY_TO_MEMORY; - dma_handle.Init.PeriphInc = DMA_PINC_ENABLE; - dma_handle.Init.MemInc = DMA_MINC_ENABLE; - dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; - dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; - dma_handle.Init.Mode = DMA_NORMAL; - dma_handle.Init.Priority = DMA_PRIORITY_HIGH; - dma_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; - dma_handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; - dma_handle.Init.MemBurst = DMA_MBURST_SINGLE; - dma_handle.Init.PeriphBurst = DMA_PBURST_SINGLE; - - dma_handle.Instance = DMA2_Stream0; - - // Associate the DMA handle - __HAL_LINKDMA(&_handle_sdram, hdma, dma_handle); - - // Deinitialize the stream for new transfer - HAL_DMA_DeInit(&dma_handle); - - // Configure the DMA stream - HAL_DMA_Init(&dma_handle); - - // NVIC configuration for DMA transfer complete interrupt - HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0x0F, 0); - HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); - - HAL_SDRAM_Init(&_handle_sdram, &timing); -////// End SDRAM Msp Init ///////////////////////////////////////////// - -////// SDRAM Initialization sequence ////////////////////////////////// -#define REFRESH_COUNT ((uint32_t)0x0603) -#define SDRAM_TIMEOUT ((uint32_t)0xFFFF) - FMC_SDRAM_CommandTypeDef command; - __IO uint32_t tmpmrd = 0; - - // Step 1: Configure a clock configuration enable command - command.CommandMode = FMC_SDRAM_CMD_CLK_ENABLE; - command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1; - command.AutoRefreshNumber = 1; - command.ModeRegisterDefinition = 0; - - // Send the command - HAL_SDRAM_SendCommand(&_handle_sdram, &command, SDRAM_TIMEOUT); - - // Step 2: Insert 100 us minimum delay - // Inserted delay is equal to 1 ms due to systick time base unit (ms) - HAL_Delay(1); - - // Step 3: Configure a PALL (precharge all) command - command.CommandMode = FMC_SDRAM_CMD_PALL; - command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1; - command.AutoRefreshNumber = 1; - command.ModeRegisterDefinition = 0; - - // Send the command - HAL_SDRAM_SendCommand(&_handle_sdram, &command, SDRAM_TIMEOUT); - - // Step 4: Configure an Auto Refresh command - command.CommandMode = FMC_SDRAM_CMD_AUTOREFRESH_MODE; - command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1; - command.AutoRefreshNumber = 8; - command.ModeRegisterDefinition = 0; - - // Send the command - HAL_SDRAM_SendCommand(&_handle_sdram, &command, SDRAM_TIMEOUT); - - /* - // Step 5: Program the external memory mode register - // SDRAM_MODEREG_BURST_LENGTH_1 |\ - SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL |\ - SDRAM_MODEREG_CAS_LATENCY_3 |\ - SDRAM_MODEREG_OPERATING_MODE_STANDARD |\ - SDRAM_MODEREG_WRITEBURST_MODE_SINGLE - */ - - tmpmrd = (uint32_t)0x0000 | 0x0000 | 0x0030 | 0x0000 | 0x0200; - - command.CommandMode = FMC_SDRAM_CMD_LOAD_MODE; - command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1; - command.AutoRefreshNumber = 1; - command.ModeRegisterDefinition = tmpmrd; - - // Send the command - HAL_SDRAM_SendCommand(&_handle_sdram, &command, SDRAM_TIMEOUT); - - // Step 6: Set the refresh rate counter - // Set the device refresh rate - HAL_SDRAM_ProgramRefreshRate(&_handle_sdram, REFRESH_COUNT); -} diff --git a/libs/InvestigationDay/LekaScreen/source/LekaScreen.cpp b/libs/InvestigationDay/LekaScreen/source/LekaScreen.cpp deleted file mode 100644 index 1a1c3a7a45..0000000000 --- a/libs/InvestigationDay/LekaScreen/source/LekaScreen.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @file LekaScreen.cpp - * @author Yann Locatelli - * - * @version 0.1 - * @date 2020-09-26 - * - * @copyright Copyright (c) 2020 - */ - -#include "LekaScreen.h" - -using namespace mbed; -using namespace std::chrono; - -Screen::Screen() : _brightness(SCREEN_BACKLIGHT_PWM) -{ - _brightness.period(0.01f); // Set PWM at 1/(0.01 seconds) = 100Hz - _brightness = 0.50f; -} - -void squareBouncing(LekaLCD &lcd) -{ - uint32_t posx = 0; - uint32_t posy = 0; - uint32_t dirx = 1; - uint32_t diry = 1; - uint32_t sizex = 100; - uint32_t sizey = 100; - - uint32_t bg_color = 0xffffff00; - - // square color - uint8_t alpha = 0xff; - uint8_t red = 0xff; - uint8_t green = 0x00; - uint8_t blue = 0x00; - - // initialize and select layer 0 - lcd.LTDC_LayerInit(0); - lcd.setActiveLayer(0); - // clear layer 0 in yellow - lcd.clear(bg_color); - - while (true) { - // update position - posx = (posx + dirx); - posy = (posy + diry); - - // chek for screen limits - if (posx >= 800 - sizex || posx == 0) { - dirx *= -1; - } - if (posy >= 480 - sizey || posy == 0) { - diry *= -1; - } - - // draw the square - lcd.fillRect(posx, posy, sizex, sizey, (alpha << 24) | (red << 16) | (green << 8) | (blue)); - - // update colors - if (green == 0) { - red--; - blue++; - } - if (red == 0) { - green++; - blue--; - } - if (blue == 0) { - red++; - green--; - } - - // HAL_Delay(1); // ~2ms little delay to let things settle - rtos::ThisThread::sleep_for(1ms); - } -} - -void Screen::start() -{ - printf("Screen example\n\n"); - - while (true) { - squareBouncing(_lcd); - rtos::ThisThread::sleep_for(1ms); - } - - printf("End of Screen example\n\n"); -} diff --git a/libs/InvestigationDay/LekaTouch/CMakeLists.txt b/libs/InvestigationDay/LekaTouch/CMakeLists.txt deleted file mode 100644 index 9a73ca0246..0000000000 --- a/libs/InvestigationDay/LekaTouch/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -# Leka - LekaOS -# Copyright 2020 APF France handicap -# SPDX-License-Identifier: Apache-2.0 - -add_library(lib_LekaTouch STATIC) - -target_include_directories(lib_LekaTouch - PUBLIC - include - include/internal -) - -target_sources(lib_LekaTouch - PRIVATE - source/MCP23017.cpp - source/LekaTouch.cpp -) - -target_link_libraries(lib_LekaTouch - mbed-os - LogKit -) diff --git a/libs/InvestigationDay/LekaTouch/include/LekaTouch.h b/libs/InvestigationDay/LekaTouch/include/LekaTouch.h deleted file mode 100644 index c38062c507..0000000000 --- a/libs/InvestigationDay/LekaTouch/include/LekaTouch.h +++ /dev/null @@ -1,57 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -// #include "LekaTouchPins.h" - -#include - -#include "PinNames.h" - -#include "drivers/DigitalIn.h" -#include "drivers/DigitalOut.h" -#include "drivers/I2C.h" -#include "rtos/ThisThread.h" -#include "rtos/Thread.h" - -#include "MCP23017.h" - -class Touch -{ - public: - Touch(); - ~Touch() {}; - - void start(void); - void initReadInterface(); - void initWriteInterface(uint8_t address); - void calibrateTwoSensors(bool &sensor_left, bool &sensor_right, uint8_t channel); - void calibration(); - void printAllReadInterfaceRegisters(); - void printAllWriteInterfaceRegisters(uint8_t address); - uint8_t updateSensorsStatus(); - - void calibrateEars(); - void calibrateBeltRBLF(); - void calibrateBeltLBRF(); - - private: - mbed::I2C _write_interface; - MCP23017 _read_interface; - mbed::DigitalOut _mux_reset; - mbed::DigitalIn _mux_inta; - mbed::DigitalIn _mux_intb; - - bool _ear_left_touched; - bool _ear_right_touched; - bool _belt_left_back_touched; - bool _belt_left_front_touched; - bool _belt_right_back_touched; - bool _belt_right_front_touched; - - const uint8_t _write_address_left = 0xC0; - const uint8_t _write_address_right = 0xC2; - const uint8_t _read_address = 0x4E; -}; diff --git a/libs/InvestigationDay/LekaTouch/include/internal/MCP23017.h b/libs/InvestigationDay/LekaTouch/include/internal/MCP23017.h deleted file mode 100644 index caacda5f3a..0000000000 --- a/libs/InvestigationDay/LekaTouch/include/internal/MCP23017.h +++ /dev/null @@ -1,145 +0,0 @@ -/* MCP23017 library for Arduino - Copyright (C) 2009 David Pye . -*/ - -#ifndef MBED_MCP23017_H -#define MBED_MCP23017_H - -#include - -#include "drivers/I2C.h" - -// -// Register defines from data sheet - we set IOCON.BANK to 0 -// as it is easier to manage the registers sequentially. -// -#define IODIR 0x00 -#define IPOL 0x02 -#define GPINTEN 0x04 -#define DEFVAL 0x06 -#define INTCON 0x08 -#define IOCON 0x0A -#define GPPU 0x0C -#define INTF 0x0E -#define INTCAP 0x10 -#define GPIO 0x12 -#define OLAT 0x14 - -#define I2C_BASE_ADDRESS 0x40 - -#define DIR_OUTPUT 0 -#define DIR_INPUT 1 - -/** MCP23017 class - * - * Allow access to an I2C connected MCP23017 16-bit I/O extender chip - * Example: - * @code - * MCP23017 *par_port; - * @endcode - * - */ -class MCP23017 -{ - public: - /** Constructor for the MCP23017 connected to specified I2C pins at a specific address - * - * 16-bit I/O expander with I2C interface - * - * @param sda I2C data pin - * @param scl I2C clock pin - * @param i2cAddress I2C address - */ - MCP23017(PinName sda, PinName scl, int i2cAddress); - - /** Reset MCP23017 device to its power-on state - */ - void reset(void); - - /** Write a 0/1 value to an output bit - * - * @param value 0 or 1 - * @param bit_number bit number range 0 --> 15 - */ - void write_bit(int value, int bit_number); - - /** Write a masked 16-bit value to the device - * - * @param data 16-bit data value - * @param mask 16-bit mask value - */ - void write_mask(unsigned short data, unsigned short mask); - - /** Read a 0/1 value from an input bit - * - * @param bit_number bit number range 0 --> 15 - * @return 0/1 value read - */ - int read_bit(int bit_number); - - /** Read a 16-bit value from the device and apply mask - * - * @param mask 16-bit mask value - * @return 16-bit data with mask applied - */ - int read_mask(unsigned short mask); - - /** Configure an MCP23017 device - * - * @param dir_config data direction value (1 = input, 0 = output) - * @param pullup_config 100k pullup value (1 = enabled, 0 = disabled) - * @param polarity_config polarity value (1 = flip, 0 = normal) - */ - void config(unsigned short dir_config, unsigned short pullup_config, unsigned short polarity_config); - - void writeRegister(int regAddress, unsigned char val); - void writeRegister(int regAddress, unsigned short val); - int readRegister(int regAddress); - - /*----------------------------------------------------------------------------- - * pinmode - * Set units to sequential, bank0 mode - */ - void pinMode(int pin, int mode); - void digitalWrite(int pin, int val); - int digitalRead(int pin); - - // These provide a more advanced mapping of the chip functionality - // See the data sheet for more information on what they do - - // Returns a word with the current pin states (ie contents of the GPIO register) - unsigned short digitalWordRead(); - // Allows you to write a word to the GPIO register - void digitalWordWrite(unsigned short w); - // Sets up the polarity mask that the MCP23017 supports - // if set to 1, it will flip the actual pin value. - void inputPolarityMask(unsigned short mask); - // Sets which pins are inputs or outputs (1 = input, 0 = output) NB Opposite to arduino's - // definition for these - void inputOutputMask(unsigned short mask); - // Allows enabling of the internal 100k pullup resisters (1 = enabled, 0 = disabled) - void internalPullupMask(unsigned short mask); - int read(void); - void write(int data); - - protected: - mbed::I2C _i2c; - int MCP23017_i2cAddress; // physical I2C address - unsigned short shadow_GPIO, shadow_IODIR, shadow_GPPU, shadow_IPOL; // Cached copies of the register values -}; - -#endif diff --git a/libs/InvestigationDay/LekaTouch/source/LekaTouch.cpp b/libs/InvestigationDay/LekaTouch/source/LekaTouch.cpp deleted file mode 100644 index f6882670d2..0000000000 --- a/libs/InvestigationDay/LekaTouch/source/LekaTouch.cpp +++ /dev/null @@ -1,261 +0,0 @@ -/** - * @file LekaTouch.cpp - * @author Yann Locatelli - * - * @version 0.1 - * @date 2020-09-20 - * - * @copyright Copyright (c) 2020 - */ - -#include "LekaTouch.h" - -#include "LogKit.h" - -using namespace mbed; -using namespace std::chrono; - -Touch::Touch() - : _write_interface(SENSOR_PROXIMITY_MUX_I2C_SDA, SENSOR_PROXIMITY_MUX_I2C_SCL), - _read_interface(SENSOR_PROXIMITY_MUX_I2C_SDA, SENSOR_PROXIMITY_MUX_I2C_SCL, 0x4E), - _mux_reset(SENSOR_PROXIMITY_MUX_RESET, 0), - _mux_inta(SENSOR_PROXIMITY_MUX_IRQA), - _mux_intb(SENSOR_PROXIMITY_MUX_IRQB) -{ - /* Reset multiplexer before starting */ - - _mux_reset = 1; - - initReadInterface(); - initWriteInterface(_write_address_left); - initWriteInterface(_write_address_right); -} - -void Touch::initReadInterface() -{ - /* Reset multiplexer (read interface) registers */ - _read_interface.reset(); - rtos::ThisThread::sleep_for(1ms); - - /* NB. for following functions, 2bytes are sent in order to set both bank (A and B) */ - - /* Set direction of I/O of multiplexer - 0 = output (supply sensor), 1 = input (sensor data) */ - /* IODIRA register address 0x00 | IODIRB register address 0x01 (need pull-up) */ - _read_interface.inputOutputMask(0xFF00); // IODIRB << 8 + IODIRA - rtos::ThisThread::sleep_for(1ms); - - /* Set pull-up (value is 1), here to bank B */ - /* GPPUA register address 0x0C | GPPUB register addres 0x0D */ - _read_interface.internalPullupMask(0xFF00); // GPPUB << 8 + GPPUA - rtos::ThisThread::sleep_for(1ms); - - /* Define interesting pins to be read, here 6 first GPIO/bits containing touch sensor return */ - /* GPIOA register address 0x12 | GPIOB register addres 0x13 */ - _read_interface.digitalWordWrite(0x003F); // GPIOB << 8 + GPIOA - rtos::ThisThread::sleep_for(1ms); - - return; -} - -void Touch::initWriteInterface(uint8_t address) -{ - /* DAC (write interface) has 4 outputs. Due to the robot with its 6 sensors, 2 DACs are necessary. */ - /* There are two ways to write, one for configuration (presented here) and one for calibration */ - - /* NB. In following comments, different way to write depend on 3 bits symbolized by C2, C1 and C0 */ - /* x refers to bit that does not count. In our configuration, they are set to 0 */ - - /* Voltage reference, value set to VDD (0) instead of internal voltage (1) because it is higher */ - /* Structure of byte is : C2 C1 C0 x VrefA VrefB VrefC VrefD */ - const auto vref = std::array {0x80}; - _write_interface.write(address, vref.data(), 1); - rtos::ThisThread::sleep_for(1ms); - - /* Power down, set to Normal mode(00) */ - /* Structure of 2-byte is : C2 C1 C0 x PD1A PD0A PD1B PD0B PD1C PD0C PD1D PD0D x x x x */ - const auto pd = std::array {0xA0, 0x00}; - _write_interface.write(address, pd.data(), 2); - rtos::ThisThread::sleep_for(1ms); - - /* Gain, set to x1 (0) instead of x2 (1) */ - /* Structure of 2-byte is : C2 C1 C0 x GxA GxB GxC GxD */ - const auto gain = std::array {0xC0}; - _write_interface.write(address, gain.data(), 1); - rtos::ThisThread::sleep_for(1ms); - - return; -} - -void Touch::printAllReadInterfaceRegisters() -{ - for (uint16_t address = 0x00; address < 0x16; address += 0x01) { - log_info("Register address %X -> %X", address, _read_interface.readRegister(address)); - } -} - -void Touch::printAllWriteInterfaceRegisters(uint8_t address) -{ - /* Read DAC (write interface) is sequential, first register (3 bytes) then EEPROM (3 bytes) for each DAC */ - /* Structure is : CH_A_reg, CH_A_EEPROM, CH_B_reg, CH_B_EEPROM, CH_C_reg, CH_C_EEPROM, CH_D_reg, CH_D_EEPROM */ - const auto buffer_size = 24; // 3 bytes * ((1 register + 1 EEPROM) * 4 channels) = 24 bytes - auto buffer = std::array {}; - - _write_interface.read(address, buffer.data(), std::size(buffer)); - - /* Refer to datasheet of MCP4728, figure 5-15 for interpretation */ - log_info("Read DAC (channel 0=A, 1=B, 2=C, 3=D):"); - for (uint8_t ch = 0; ch < 4; ch++) { - log_info("Register of channel %d: %X %X %X", ch, buffer[ch * 6], buffer[ch * 6 + 1], buffer[ch * 6 + 2]); - log_info("EEPROM of channel %d: %X %X %X", ch, buffer[ch * 6 + 3], buffer[ch * 6 + 4], buffer[ch * 6 + 5]); - } - - return; -} - -void Touch::calibrateTwoSensors(bool &sensor_left, bool &sensor_right, uint8_t channel) -{ - uint16_t value_left_calib = 0x0FFF; - uint16_t value_right_calib = 0x0FFF; - uint16_t step = 0x0001; - - auto buffer = std::array {(uint8_t)(0x40 + (channel << 1 & 0x06)), 0x00, 0x00}; - buffer[0] = (uint8_t)(0x40 + ((channel & 0x03) << 1)); - - /* Reset calibration value to 0 on channel link to sensor */ - _write_interface.write(_write_address_left, buffer.data(), 3); - _write_interface.write(_write_address_right, buffer.data(), 3); - rtos::ThisThread::sleep_for(1s); - updateSensorsStatus(); - - while (!(sensor_left && sensor_right)) { - /* Decrement values if hands are not detected */ - if (!sensor_left) { - if (value_left_calib - step > 0x0FFF) { - value_left_calib = 0x0FFF; - } else { - value_left_calib -= step; - } - } - if (!sensor_right) { - if (value_right_calib - step > 0x0FFF) { - value_right_calib = 0x0FFF; - } else { - value_right_calib -= step; - } - } - - /* Multiple write mode and add channel information linked with sensor */ - /* Structure of multiple write is on 3 bytes, check datasheet for more information Figure 5-8 */ - buffer[1] = (uint8_t)((value_left_calib & 0x0F00) >> 8); - buffer[2] = (uint8_t)((value_left_calib & 0x00FF) >> 0); - _write_interface.write(_write_address_left, buffer.data(), 3); - - buffer[1] = (uint8_t)((value_right_calib & 0x0F00) >> 8); - buffer[2] = (uint8_t)((value_right_calib & 0x00FF) >> 0); - _write_interface.write(_write_address_right, buffer.data(), 3); - - /* Check sensors return */ - rtos::ThisThread::sleep_for(1ms); - updateSensorsStatus(); - } - - /* Write in EEPROM values of calibration of touch sensor */ - /* Byte structure of write in EEPROM is -> C2 C1 C0 W1 W0 DAC1 DAC0 0 = 0 1 0 1 1 DAC1 DAC0 0 */ - buffer[0] = 0x58 + (channel << 1 & 0x06); - buffer[1] = (uint8_t)(value_left_calib & 0x0F00 >> 8); - buffer[2] = (uint8_t)(value_left_calib & 0x00FF >> 0); - _write_interface.write(_write_address_left, buffer.data(), 3); - - buffer[1] = (uint8_t)(value_right_calib & 0x0F00 >> 8); - buffer[2] = (uint8_t)(value_right_calib & 0x00FF >> 0); - _write_interface.write(_write_address_right, buffer.data(), 3); - rtos::ThisThread::sleep_for(1ms); - - log_info("CALIBRATED!"); - rtos::ThisThread::sleep_for(100ms); - return; -} - -void Touch::calibrateEars() -{ - log_info("Place hands on EAR LEFT and EAR RIGHT"); - log_info("Calibration will start in 5 seconds"); - rtos::ThisThread::sleep_for(5s); - calibrateTwoSensors(_ear_left_touched, _ear_right_touched, 2); -} - -void Touch::calibrateBeltLBRF() -{ - log_info("Place hands on BELT LEFT BACK and BELT RIGHT FRONT"); - log_info("Calibration will start in 5 seconds"); - rtos::ThisThread::sleep_for(5s); - calibrateTwoSensors(_belt_left_back_touched, _belt_right_front_touched, 1); -} - -void Touch::calibrateBeltRBLF() -{ - log_info("Place hands on BELT LEFT FRONT and BELT RIGHT BACK"); - log_info("Calibration will start in 5 seconds"); - rtos::ThisThread::sleep_for(5s); - calibrateTwoSensors(_belt_left_front_touched, _belt_right_back_touched, 0); -} - -void Touch::calibration() -{ - log_info("Touch calibration"); - log_info("For each of 6 touch sensors, value of sensibility will change"); - log_info("Please keep your hands on 2 sensors until \"CALIBRATED !\" appears.\n"); - rtos::ThisThread::sleep_for(15s); - - calibrateEars(); - calibrateBeltLBRF(); - calibrateBeltRBLF(); - - return; -} - -auto Touch::updateSensorsStatus() -> uint8_t -{ - auto value = (uint8_t)(~(_read_interface.digitalWordRead() >> 8)); - - _ear_right_touched = (bool)((value >> 5) & 0x01); - _ear_left_touched = (bool)((value >> 4) & 0x01); - _belt_right_front_touched = (bool)((value >> 3) & 0x01); - _belt_right_back_touched = (bool)((value >> 2) & 0x01); - _belt_left_back_touched = (bool)((value >> 1) & 0x01); - _belt_left_front_touched = (bool)((value >> 0) & 0x01); - - return value; -} - -void Touch::start() -{ - log_info("Touch example"); - - // printAllReadInterfaceRegisters(); - // printAllWriteInterfaceRegisters(_write_address_left); - // printAllWriteInterfaceRegisters(_write_address_right); - // calibration(); - - int n_repetition = 10; - while (true) { - log_info("Start a cycle of %d checking of touch sensor every 1 second, then pause 10s", n_repetition); - - for (int i = 0; i < n_repetition; i++) { - updateSensorsStatus(); - - log_info("Ear left touched: %s", _ear_left_touched ? "true" : "false"); - log_info("Ear right touched: %s", _ear_right_touched ? "true" : "false"); - log_info("Belt left front touched: %s", _belt_left_front_touched ? "true" : "false"); - log_info("Belt left back touched: %s", _belt_left_back_touched ? "true" : "false"); - log_info("Belt right front touched: %s", _belt_right_front_touched ? "true" : "false"); - log_info("Belt right back touched: %s", _belt_right_back_touched ? "true" : "false"); - - rtos::ThisThread::sleep_for(1s); - } - rtos::ThisThread::sleep_for(10s); - } - - log_info("End of Touch example"); - return; -} diff --git a/libs/InvestigationDay/LekaTouch/source/MCP23017.cpp b/libs/InvestigationDay/LekaTouch/source/MCP23017.cpp deleted file mode 100644 index f4ee769acf..0000000000 --- a/libs/InvestigationDay/LekaTouch/source/MCP23017.cpp +++ /dev/null @@ -1,260 +0,0 @@ -/* MCP23017 library for Arduino - Copyright (C) 2009 David Pye . -*/ - -#include "MCP23017.h" - -union { - uint8_t value8[2]; - uint16_t value16; -} tmp_data; - -/*----------------------------------------------------------------------------- - * - */ -MCP23017::MCP23017(PinName sda, PinName scl, int i2cAddress) : _i2c(sda, scl) -{ - MCP23017_i2cAddress = i2cAddress; - reset(); // initialise chip to power-on condition -} - -/*----------------------------------------------------------------------------- - * reset - * Set configuration (IOCON) and direction(IODIR) registers to initial state - */ -void MCP23017::reset() -{ - // - // First make sure that the device is in BANK=0 mode - // - writeRegister(0x05, (unsigned char)0x00); - // - // set direction registers to inputs - // - writeRegister(IODIR, (unsigned short)0xFFFF); - // - // set all other registers to zero (last of 10 registers is OLAT) - // - for (int reg_addr = 2; reg_addr <= OLAT; reg_addr += 2) { - writeRegister(reg_addr, (unsigned short)0x0000); - } - // - // Set the shadow registers to power-on state - // - shadow_IODIR = 0xFFFF; - shadow_GPIO = 0; - shadow_GPPU = 0; - shadow_IPOL = 0; -} - -/*----------------------------------------------------------------------------- - * write_bit - * Write a 1/0 to a single bit of the 16-bit port - */ -void MCP23017::write_bit(int value, int bit_number) -{ - if (value == 0) { - shadow_GPIO &= ~(1 << bit_number); - } else { - shadow_GPIO |= 1 << bit_number; - } - writeRegister(GPIO, (unsigned short)shadow_GPIO); -} - -/*----------------------------------------------------------------------------- - * Write a combination of bits to the 16-bit port - */ -void MCP23017::write_mask(unsigned short data, unsigned short mask) -{ - shadow_GPIO = (shadow_GPIO & ~mask) | data; - writeRegister(GPIO, (unsigned short)shadow_GPIO); -} - -/*----------------------------------------------------------------------------- - * read_bit - * Read a single bit from the 16-bit port - */ -int MCP23017::read_bit(int bit_number) -{ - shadow_GPIO = readRegister(GPIO); - return ((shadow_GPIO >> bit_number) & 0x0001); -} - -/*----------------------------------------------------------------------------- - * read_mask - */ -int MCP23017::read_mask(unsigned short mask) -{ - shadow_GPIO = readRegister(GPIO); - return (shadow_GPIO & mask); -} - -/*----------------------------------------------------------------------------- - * Config - * set direction and pull-up registers - */ -void MCP23017::config(unsigned short dir_config, unsigned short pullup_config, unsigned short polarity_config) -{ - shadow_IODIR = dir_config; - writeRegister(IODIR, (unsigned short)shadow_IODIR); - shadow_GPPU = pullup_config; - writeRegister(GPPU, (unsigned short)shadow_GPPU); - shadow_IPOL = polarity_config; - writeRegister(IPOL, (unsigned short)shadow_IPOL); -} - -/*----------------------------------------------------------------------------- - * writeRegister - * write a byte - */ -void MCP23017::writeRegister(int regAddress, unsigned char data) -{ - char buffer[2]; - - buffer[0] = regAddress; - buffer[1] = data; - _i2c.write(MCP23017_i2cAddress, buffer, 2); -} - -/*---------------------------------------------------------------------------- - * write Register - * write two bytes - */ -void MCP23017::writeRegister(int regAddress, unsigned short data) -{ - char buffer[3]; - - buffer[0] = regAddress; - tmp_data.value16 = data; - buffer[1] = tmp_data.value8[0]; - buffer[2] = tmp_data.value8[1]; - - _i2c.write(MCP23017_i2cAddress, buffer, 3); -} - -/*----------------------------------------------------------------------------- - * readRegister - */ -int MCP23017::readRegister(int regAddress) -{ - char buffer[2]; - - buffer[0] = regAddress; - _i2c.write(MCP23017_i2cAddress, buffer, 1); - _i2c.read(MCP23017_i2cAddress, buffer, 2); - - return ((int)(buffer[0] + (buffer[1] << 8))); -} - -/*----------------------------------------------------------------------------- - * pinMode - */ -void MCP23017::pinMode(int pin, int mode) -{ - if (DIR_INPUT) { - shadow_IODIR |= 1 << pin; - } else { - shadow_IODIR &= ~(1 << pin); - } - writeRegister(IODIR, (unsigned short)shadow_IODIR); -} - -/*----------------------------------------------------------------------------- - * digitalRead - */ -int MCP23017::digitalRead(int pin) -{ - shadow_GPIO = readRegister(GPIO); - if (shadow_GPIO & (1 << pin)) { - return 1; - } else { - return 0; - } -} - -/*----------------------------------------------------------------------------- - * digitalWrite - */ -void MCP23017::digitalWrite(int pin, int val) -{ - // If this pin is an INPUT pin, a write here will - // enable the internal pullup - // otherwise, it will set the OUTPUT voltage - // as appropriate. - bool isOutput = !(shadow_IODIR & 1 << pin); - - if (isOutput) { - // This is an output pin so just write the value - if (val) - shadow_GPIO |= 1 << pin; - else - shadow_GPIO &= ~(1 << pin); - writeRegister(GPIO, (unsigned short)shadow_GPIO); - } else { - // This is an input pin, so we need to enable the pullup - if (val) { - shadow_GPPU |= 1 << pin; - } else { - shadow_GPPU &= ~(1 << pin); - } - writeRegister(GPPU, (unsigned short)shadow_GPPU); - } -} - -/*----------------------------------------------------------------------------- - * digitalWordRead - */ -unsigned short MCP23017::digitalWordRead() -{ - shadow_GPIO = readRegister(GPIO); - return shadow_GPIO; -} - -/*----------------------------------------------------------------------------- - * digitalWordWrite - */ -void MCP23017::digitalWordWrite(unsigned short w) -{ - shadow_GPIO = w; - writeRegister(GPIO, (unsigned short)shadow_GPIO); -} - -/*----------------------------------------------------------------------------- - * inputPolarityMask - */ -void MCP23017::inputPolarityMask(unsigned short mask) -{ - writeRegister(IPOL, mask); -} - -/*----------------------------------------------------------------------------- - * inputoutputMask - */ -void MCP23017::inputOutputMask(unsigned short mask) -{ - shadow_IODIR = mask; - writeRegister(IODIR, (unsigned short)shadow_IODIR); -} - -/*----------------------------------------------------------------------------- - * internalPullupMask - */ -void MCP23017::internalPullupMask(unsigned short mask) -{ - shadow_GPPU = mask; - writeRegister(GPPU, (unsigned short)shadow_GPPU); -} diff --git a/libs/InvestigationDay/LekaWifi/CMakeLists.txt b/libs/InvestigationDay/LekaWifi/CMakeLists.txt deleted file mode 100644 index 18ed905396..0000000000 --- a/libs/InvestigationDay/LekaWifi/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -# Leka - LekaOS -# Copyright 2020 APF France handicap -# SPDX-License-Identifier: Apache-2.0 - -add_library(lib_LekaWifi STATIC) - -target_include_directories(lib_LekaWifi - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include -) - -target_sources(lib_LekaWifi - PRIVATE - source/LekaWifi.cpp -) - -target_link_libraries(lib_LekaWifi mbed-os) diff --git a/libs/InvestigationDay/LekaWifi/include/LekaWifi.h b/libs/InvestigationDay/LekaWifi/include/LekaWifi.h deleted file mode 100644 index 16845e90d1..0000000000 --- a/libs/InvestigationDay/LekaWifi/include/LekaWifi.h +++ /dev/null @@ -1,36 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "drivers/DigitalOut.h" -#include "rtos/Thread.h" - -#include "connectivity/netsocket/include/netsocket/TCPSocket.h" - -#include "ESP8266Interface.h" - -class Wifi -{ - public: - Wifi(); - ~Wifi() {}; - - void start(void); - void connect_network(const char *network_name, const char *network_password); - - static const char *sec2str(nsapi_security_t security); - void scan_available_networks(WiFiInterface *wifi); - // void http_demo(NetworkInterface *net); - - private: - ESP8266Interface _interface; - mbed::DigitalOut _wifi_reset; - mbed::DigitalOut _wifi_enable; - - char const *_network_name = "HUAWEI P smart 2019"; - char const *_network_password; -}; diff --git a/libs/InvestigationDay/LekaWifi/source/LekaWifi.cpp b/libs/InvestigationDay/LekaWifi/source/LekaWifi.cpp deleted file mode 100644 index 072e02ca7c..0000000000 --- a/libs/InvestigationDay/LekaWifi/source/LekaWifi.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "LekaWifi.h" - -using namespace mbed; -using namespace std::chrono; - -Wifi::Wifi() : _interface(WIFI_USART_TX, WIFI_USART_RX), _wifi_reset(WIFI_RESET, 0), _wifi_enable(WIFI_ENABLE, 1) -{ - rtos::ThisThread::sleep_for(1s); - _wifi_reset = 1; -} - -const char *Wifi::sec2str(nsapi_security_t security) -{ - switch (security) { - case NSAPI_SECURITY_NONE: - return "None"; - case NSAPI_SECURITY_WEP: - return "WEP"; - case NSAPI_SECURITY_WPA: - return "WPA"; - case NSAPI_SECURITY_WPA2: - return "WPA2"; - case NSAPI_SECURITY_WPA_WPA2: - return "WPA/WPA2"; - case NSAPI_SECURITY_UNKNOWN: - default: - return "Unknown"; - } -} - -void Wifi::scan_available_networks(WiFiInterface *wifi) -{ - WiFiAccessPoint *ap; - - printf("Scan:\n"); - - int count = wifi->scan(NULL, 0); - - /* Limit number of network arbitrary to 15 */ - count = count < 15 ? count : 15; - - ap = new WiFiAccessPoint[15]; - - count = wifi->scan(ap, count); - for (int i = 0; i < count; i++) { - printf("Network: %s secured: %s BSSID: %hhX:%hhX:%hhX:%hhx:%hhx:%hhx RSSI: %hhd Ch: %hhd\n", ap[i].get_ssid(), - sec2str(ap[i].get_security()), ap[i].get_bssid()[0], ap[i].get_bssid()[1], ap[i].get_bssid()[2], - ap[i].get_bssid()[3], ap[i].get_bssid()[4], ap[i].get_bssid()[5], ap[i].get_rssi(), ap[i].get_channel()); - } - printf("%d networks available.\n", count); - - delete[] ap; -} - -void Wifi::connect_network(const char *network_name, const char *network_password) -{ - /* Connection to a specific network */ - printf("\nConnecting...\n"); - int ret = _interface.connect(network_name, network_password, NSAPI_SECURITY_WPA_WPA2); - if (ret != 0) { - printf("\nConnection error\n"); - return; - } - - /* Get information of device on specific network */ - SocketAddress socket_address; - - printf("Success\n\n"); - printf("MAC: %s\n", _interface.get_mac_address()); - _interface.get_ip_address(&socket_address); - printf("IP: %s\n", socket_address.get_ip_address()); - _interface.get_netmask(&socket_address); - printf("Netmask: %s\n", socket_address.get_ip_address()); - _interface.get_gateway(&socket_address); - printf("Gateway: %s\n", socket_address.get_ip_address()); - printf("RSSI: %d\n\n", _interface.get_rssi()); - - /* Get information online (_old http_demo) */ - // Open a socket on the network interface, and create a TCP connection to mbed.org - TCPSocket socket; - socket.open(&_interface); - - // SocketAddress a; - _interface.gethostbyname("ifconfig.io", &socket_address); - socket_address.set_port(80); - socket.connect(socket_address); - // Send a simple http request - char tx_buffer[] = "GET / HTTP/1.1\r\nHost: ifconfig.io\r\n\r\n"; - int tx_count = socket.send(tx_buffer, sizeof tx_buffer); - printf("sent %d [%.*s]\n", tx_count, strstr(tx_buffer, "\n") - tx_buffer, tx_buffer); - - // Recieve a simple http response and print out the response line - char rx_buffer[64]; - int rx_count = socket.recv(rx_buffer, sizeof rx_buffer); - printf("recv %d [%.*s]\n", rx_count, strstr(rx_buffer, "\n") - rx_buffer, rx_buffer); - - // Close the socket to return its memory and bring down the network interface - socket.close(); - - _interface.disconnect(); -} - -void Wifi::start() -{ - /* Test based on Wi-Fi example on os.mbed.com */ - printf("WiFi example\n\n"); - - while (true) { - scan_available_networks(&_interface); - // connect_network(_network_name, _network_password); - rtos::ThisThread::sleep_for(10s); - } - - printf("End of WiFi example\n\n"); -} From 218c6402752e17d20efcc4687a74c41d9a2df344 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 16 Jan 2023 18:06:16 +0100 Subject: [PATCH 033/143] :bento: (factory): Add LekaOS-factory.bin LekaOS-1.3.0+1673448777.bin --- fs/usr/os/LekaOS-factory.bin | Bin 0 -> 484004 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 fs/usr/os/LekaOS-factory.bin diff --git a/fs/usr/os/LekaOS-factory.bin b/fs/usr/os/LekaOS-factory.bin new file mode 100644 index 0000000000000000000000000000000000000000..dfdc543a15fc9337d861261829e26aa4331f8d1f GIT binary patch literal 484004 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 z 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%0AopcrnC*oiG!otXiVP< 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

(_!vgvef8gC~4 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 Date: Mon, 16 Jan 2023 18:09:13 +0100 Subject: [PATCH 034/143] :recycle: (firmware): Rename loadUpdate private method into load --- libs/FirmwareKit/include/FirmwareKit.h | 2 +- libs/FirmwareKit/source/FirmwareKit.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/FirmwareKit/include/FirmwareKit.h b/libs/FirmwareKit/include/FirmwareKit.h index f0aaa8b496..0131815abd 100644 --- a/libs/FirmwareKit/include/FirmwareKit.h +++ b/libs/FirmwareKit/include/FirmwareKit.h @@ -36,7 +36,7 @@ class FirmwareKit : public interface::FirmwareUpdate private: [[nodiscard]] auto getPathOfVersion(const Version &version) const -> std::filesystem::path; - auto loadUpdate(const std::filesystem::path &path) -> bool; + auto load(const std::filesystem::path &path) -> bool; interface::FlashMemory &_flash; FileManagerKit::File _file {}; diff --git a/libs/FirmwareKit/source/FirmwareKit.cpp b/libs/FirmwareKit/source/FirmwareKit.cpp index a82a205227..ad16f88bef 100644 --- a/libs/FirmwareKit/source/FirmwareKit.cpp +++ b/libs/FirmwareKit/source/FirmwareKit.cpp @@ -46,10 +46,10 @@ auto FirmwareKit::loadUpdate(const Version &version) -> bool { auto path = getPathOfVersion(version); - return loadUpdate(path); + return load(path); } -auto FirmwareKit::loadUpdate(const std::filesystem::path &path) -> bool +auto FirmwareKit::load(const std::filesystem::path &path) -> bool { if (auto is_open = _file.open(path); is_open) { auto address = uint32_t {0x0}; From 8b0cd85954a0cabd6c8e3c7f4febeca8b72a712e Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 20 Jan 2023 11:45:28 +0100 Subject: [PATCH 035/143] :recycle: (firmware): Rename loadUpdate into loadFirmware --- include/interface/drivers/FirmwareUpdate.h | 2 +- libs/FirmwareKit/include/FirmwareKit.h | 2 +- libs/FirmwareKit/source/FirmwareKit.cpp | 2 +- libs/FirmwareKit/tests/FirmwareKit_test.cpp | 8 ++++---- libs/RobotKit/include/RobotController.h | 2 +- .../tests/RobotController_test_stateCharging.cpp | 12 ++++++------ .../tests/RobotController_test_stateFileExchange.cpp | 8 ++++---- spikes/lk_update_process_app_base/main.cpp | 2 +- tests/unit/mocks/mocks/leka/FirmwareUpdate.h | 2 +- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/include/interface/drivers/FirmwareUpdate.h b/include/interface/drivers/FirmwareUpdate.h index 8be91b45af..75bb9cc637 100644 --- a/include/interface/drivers/FirmwareUpdate.h +++ b/include/interface/drivers/FirmwareUpdate.h @@ -15,7 +15,7 @@ class FirmwareUpdate virtual auto getCurrentVersion() -> Version = 0; virtual auto isVersionAvailable(const Version &version) -> bool = 0; - virtual auto loadUpdate(const Version &version) -> bool = 0; + virtual auto loadFirmware(const Version &version) -> bool = 0; }; } // namespace leka::interface diff --git a/libs/FirmwareKit/include/FirmwareKit.h b/libs/FirmwareKit/include/FirmwareKit.h index 0131815abd..96f7545287 100644 --- a/libs/FirmwareKit/include/FirmwareKit.h +++ b/libs/FirmwareKit/include/FirmwareKit.h @@ -31,7 +31,7 @@ class FirmwareKit : public interface::FirmwareUpdate auto getCurrentVersion() -> Version final; auto isVersionAvailable(const Version &version) -> bool final; - auto loadUpdate(const Version &version) -> bool final; + auto loadFirmware(const Version &version) -> bool final; private: [[nodiscard]] auto getPathOfVersion(const Version &version) const -> std::filesystem::path; diff --git a/libs/FirmwareKit/source/FirmwareKit.cpp b/libs/FirmwareKit/source/FirmwareKit.cpp index ad16f88bef..7643568d53 100644 --- a/libs/FirmwareKit/source/FirmwareKit.cpp +++ b/libs/FirmwareKit/source/FirmwareKit.cpp @@ -42,7 +42,7 @@ auto FirmwareKit::isVersionAvailable(const Version &version) -> bool return file_exists; } -auto FirmwareKit::loadUpdate(const Version &version) -> bool +auto FirmwareKit::loadFirmware(const Version &version) -> bool { auto path = getPathOfVersion(version); diff --git a/libs/FirmwareKit/tests/FirmwareKit_test.cpp b/libs/FirmwareKit/tests/FirmwareKit_test.cpp index bb3c2b6db5..2cd0c9afb9 100644 --- a/libs/FirmwareKit/tests/FirmwareKit_test.cpp +++ b/libs/FirmwareKit/tests/FirmwareKit_test.cpp @@ -53,7 +53,7 @@ TEST_F(FirmwareKitTest, getCurrentVersion) EXPECT_EQ(actual_version.revision, current_version.revision); } -TEST_F(FirmwareKitTest, loadUpdate) +TEST_F(FirmwareKitTest, loadFirmware) { auto version_to_load = Version {1, 0, 0}; { @@ -63,16 +63,16 @@ TEST_F(FirmwareKitTest, loadUpdate) EXPECT_CALL(mock_flash, write).Times(AtLeast(1)); } - auto did_load_firmware = firmwarekit.loadUpdate(version_to_load); + auto did_load_firmware = firmwarekit.loadFirmware(version_to_load); ASSERT_TRUE(did_load_firmware); } -TEST_F(FirmwareKitTest, loadUpdateFileNotFound) +TEST_F(FirmwareKitTest, loadFirmwareFileNotFound) { auto unexisting_version = Version {0, 0, 0}; - auto did_load_firmware = firmwarekit.loadUpdate(unexisting_version); + auto did_load_firmware = firmwarekit.loadFirmware(unexisting_version); ASSERT_FALSE(did_load_firmware); } diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index b945060eae..4bce3f6dcd 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -292,7 +292,7 @@ class RobotController : public interface::RobotController void applyUpdate() final { auto firmware_version = _service_update.getVersion(); - if (_firmware_update.loadUpdate(firmware_version) && _on_update_loaded_callback != nullptr) { + if (_firmware_update.loadFirmware(firmware_version) && _on_update_loaded_callback != nullptr) { _on_update_loaded_callback(); } diff --git a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp index 613d2d84c6..9e88f511e4 100644 --- a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp @@ -258,7 +258,7 @@ TEST_F(RobotControllerTest, stateChargingEventUpdateRequestedGuardIsReadyToUpdat EXPECT_CALL(battery, level).Times(AnyNumber()).WillRepeatedly(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).Times(AnyNumber()).WillRepeatedly(Return(true)); - EXPECT_CALL(firmware_update, loadUpdate).Times(0); + EXPECT_CALL(firmware_update, loadFirmware).Times(0); rc.isReadyToUpdate(); EXPECT_TRUE(rc.state_machine.is(lksm::state::charging)); @@ -275,7 +275,7 @@ TEST_F(RobotControllerTest, stateChargingEventUpdateRequestedGuardIsReadyToUpdat EXPECT_CALL(battery, level).WillOnce(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).Times(AnyNumber()).WillRepeatedly(Return(true)); - EXPECT_CALL(firmware_update, loadUpdate).Times(0); + EXPECT_CALL(firmware_update, loadFirmware).Times(0); rc.isReadyToUpdate(); EXPECT_TRUE(rc.state_machine.is(lksm::state::charging)); @@ -292,7 +292,7 @@ TEST_F(RobotControllerTest, stateChargingEventUpdateRequestedGuardIsReadyToUpdat EXPECT_CALL(battery, level).Times(AnyNumber()).WillRepeatedly(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).Times(AnyNumber()).WillRepeatedly(Return(false)); - EXPECT_CALL(firmware_update, loadUpdate).Times(0); + EXPECT_CALL(firmware_update, loadFirmware).Times(0); rc.isReadyToUpdate(); EXPECT_TRUE(rc.state_machine.is(lksm::state::charging)); @@ -313,7 +313,7 @@ TEST_F(RobotControllerTest, stateChargingEventUpdateRequestedGuardIsReadyToUpdat EXPECT_CALL(battery, level).WillOnce(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).WillOnce(Return(true)); - EXPECT_CALL(firmware_update, loadUpdate).WillOnce(Return(true)); + EXPECT_CALL(firmware_update, loadFirmware).WillOnce(Return(true)); EXPECT_CALL(mock_on_update_loaded_callback, Call).Times(1); rc.isReadyToUpdate(); rc.applyUpdate(); @@ -336,7 +336,7 @@ TEST_F(RobotControllerTest, EXPECT_CALL(battery, level).WillOnce(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).WillOnce(Return(true)); - EXPECT_CALL(firmware_update, loadUpdate).WillOnce(Return(true)); + EXPECT_CALL(firmware_update, loadFirmware).WillOnce(Return(true)); rc.isReadyToUpdate(); rc.applyUpdate(); @@ -358,7 +358,7 @@ TEST_F(RobotControllerTest, stateChargingEventUpdateRequestedGuardIsReadyToUpdat EXPECT_CALL(battery, level).WillOnce(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).WillOnce(Return(true)); - EXPECT_CALL(firmware_update, loadUpdate).WillOnce(Return(false)); + EXPECT_CALL(firmware_update, loadFirmware).WillOnce(Return(false)); EXPECT_CALL(mock_on_update_loaded_callback, Call).Times(0); rc.isReadyToUpdate(); rc.applyUpdate(); diff --git a/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp b/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp index 4a5b0f2b6a..095251a318 100644 --- a/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp @@ -179,7 +179,7 @@ TEST_F(RobotControllerTest, stateFileExchangeEventUpdateRequestedGuardIsReadyToU EXPECT_CALL(battery, level).InSequence(is_ready_to_update_sequence).WillRepeatedly(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).WillOnce(Return(true)); - EXPECT_CALL(firmware_update, loadUpdate).WillOnce(Return(true)); + EXPECT_CALL(firmware_update, loadFirmware).WillOnce(Return(true)); EXPECT_CALL(mock_on_update_loaded_callback, Call); rc.state_machine.process_event(lksm::event::update_requested {}); @@ -209,7 +209,7 @@ TEST_F(RobotControllerTest, stateFileExchangeEventUpdateRequestedGuardIsReadyToU EXPECT_CALL(battery, level).Times(0).InSequence(is_ready_to_update_sequence); EXPECT_CALL(firmware_update, isVersionAvailable).WillOnce(Return(returned_is_version_available)); - EXPECT_CALL(firmware_update, loadUpdate).Times(0); + EXPECT_CALL(firmware_update, loadFirmware).Times(0); EXPECT_CALL(mock_on_update_loaded_callback, Call).Times(0); rc.state_machine.process_event(lksm::event::update_requested {}); @@ -239,7 +239,7 @@ TEST_F(RobotControllerTest, stateFileExchangeEventUpdateRequestedGuardIsReadyToU EXPECT_CALL(battery, level).InSequence(is_ready_to_update_sequence).WillRepeatedly(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).WillOnce(Return(returned_is_version_available)); - EXPECT_CALL(firmware_update, loadUpdate).Times(0); + EXPECT_CALL(firmware_update, loadFirmware).Times(0); EXPECT_CALL(mock_on_update_loaded_callback, Call).Times(0); rc.state_machine.process_event(lksm::event::update_requested {}); @@ -269,7 +269,7 @@ TEST_F(RobotControllerTest, stateFileExchangeEventUpdateRequestedGuardIsReadyToU EXPECT_CALL(battery, level).InSequence(is_ready_to_update_sequence).WillRepeatedly(Return(returned_level)); EXPECT_CALL(firmware_update, isVersionAvailable).WillOnce(Return(returned_is_version_available)); - EXPECT_CALL(firmware_update, loadUpdate).Times(0); + EXPECT_CALL(firmware_update, loadFirmware).Times(0); EXPECT_CALL(mock_on_update_loaded_callback, Call).Times(0); rc.state_machine.process_event(lksm::event::update_requested {}); diff --git a/spikes/lk_update_process_app_base/main.cpp b/spikes/lk_update_process_app_base/main.cpp index e5c7a48068..fd6713011d 100644 --- a/spikes/lk_update_process_app_base/main.cpp +++ b/spikes/lk_update_process_app_base/main.cpp @@ -64,7 +64,7 @@ auto main() -> int // Load file auto version = Version {.major = 1, .minor = 2, .revision = 3}; - if (auto did_load = firmwarekit.loadUpdate(version); did_load) { + if (auto did_load = firmwarekit.loadFirmware(version); did_load) { log_info("New update was loaded in external flash"); } diff --git a/tests/unit/mocks/mocks/leka/FirmwareUpdate.h b/tests/unit/mocks/mocks/leka/FirmwareUpdate.h index a3d50b8e8b..c793ca97cb 100644 --- a/tests/unit/mocks/mocks/leka/FirmwareUpdate.h +++ b/tests/unit/mocks/mocks/leka/FirmwareUpdate.h @@ -13,7 +13,7 @@ class FirmwareUpdate : public interface::FirmwareUpdate public: MOCK_METHOD(Version, getCurrentVersion, (), (override)); MOCK_METHOD(bool, isVersionAvailable, (const Version &), (override)); - MOCK_METHOD(bool, loadUpdate, (const Version &), (override)); + MOCK_METHOD(bool, loadFirmware, (const Version &), (override)); }; } // namespace leka::mock From 8fb35cccb631c24e8e8a7611be2074e291342e9c Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 16 Jan 2023 17:56:52 +0100 Subject: [PATCH 036/143] :sparkles: (firmware): Add loadFactory --- include/interface/drivers/FirmwareUpdate.h | 1 + libs/FirmwareKit/include/FirmwareKit.h | 5 +- libs/FirmwareKit/source/FirmwareKit.cpp | 16 ++++++ libs/FirmwareKit/tests/FirmwareKit_test.cpp | 56 +++++++++++++++++++- tests/unit/mocks/mocks/leka/FirmwareUpdate.h | 1 + 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/include/interface/drivers/FirmwareUpdate.h b/include/interface/drivers/FirmwareUpdate.h index 75bb9cc637..cdc6e8e130 100644 --- a/include/interface/drivers/FirmwareUpdate.h +++ b/include/interface/drivers/FirmwareUpdate.h @@ -16,6 +16,7 @@ class FirmwareUpdate virtual auto getCurrentVersion() -> Version = 0; virtual auto isVersionAvailable(const Version &version) -> bool = 0; virtual auto loadFirmware(const Version &version) -> bool = 0; + virtual auto loadFactoryFirmware() -> bool = 0; }; } // namespace leka::interface diff --git a/libs/FirmwareKit/include/FirmwareKit.h b/libs/FirmwareKit/include/FirmwareKit.h index 96f7545287..4fe2e62fc4 100644 --- a/libs/FirmwareKit/include/FirmwareKit.h +++ b/libs/FirmwareKit/include/FirmwareKit.h @@ -19,9 +19,11 @@ class FirmwareKit : public interface::FirmwareUpdate public: struct Config { const char *bin_path_format; + const char *factory_path; }; - static constexpr auto DEFAULT_CONFIG = Config {.bin_path_format = "/fs/usr/os/LekaOS-%i.%i.%i.bin"}; + static constexpr auto DEFAULT_CONFIG = + Config {.bin_path_format = "/fs/usr/os/LekaOS-%i.%i.%i.bin", .factory_path = "/fs/usr/os/LekaOS-factory.bin"}; explicit FirmwareKit(interface::FlashMemory &flash, Config config) : _flash(flash), _config(config) { @@ -32,6 +34,7 @@ class FirmwareKit : public interface::FirmwareUpdate auto isVersionAvailable(const Version &version) -> bool final; auto loadFirmware(const Version &version) -> bool final; + auto loadFactoryFirmware() -> bool final; private: [[nodiscard]] auto getPathOfVersion(const Version &version) const -> std::filesystem::path; diff --git a/libs/FirmwareKit/source/FirmwareKit.cpp b/libs/FirmwareKit/source/FirmwareKit.cpp index 7643568d53..a71c374736 100644 --- a/libs/FirmwareKit/source/FirmwareKit.cpp +++ b/libs/FirmwareKit/source/FirmwareKit.cpp @@ -49,6 +49,22 @@ auto FirmwareKit::loadFirmware(const Version &version) -> bool return load(path); } +auto FirmwareKit::loadFactoryFirmware() -> bool +{ + if (auto factory_firmware_exists = _file.open(_config.factory_path); factory_firmware_exists) { + return load(_config.factory_path); + } + // ! IMPORTANT: BACKWARD COMPATIBILITY + // ? This path is kept to handle the case where the bootloader has + // ? been updated but not the SD card, in which case the factory + // ? firmware might not exist. If it's the case, we must load the + // ? previous factory firmware which was LekaOS-1.0.0.bin + // ? This should only happen when a robot is sent back to be fixed + // ? and used internally, but never for products in the field as + // ? the bootloader cannot change. + return loadFirmware({.major = 1, .minor = 0, .revision = 0}); +} + auto FirmwareKit::load(const std::filesystem::path &path) -> bool { if (auto is_open = _file.open(path); is_open) { diff --git a/libs/FirmwareKit/tests/FirmwareKit_test.cpp b/libs/FirmwareKit/tests/FirmwareKit_test.cpp index 2cd0c9afb9..657f0d3338 100644 --- a/libs/FirmwareKit/tests/FirmwareKit_test.cpp +++ b/libs/FirmwareKit/tests/FirmwareKit_test.cpp @@ -34,7 +34,10 @@ class FirmwareKitTest : public ::testing::Test }; mock::FlashMemory mock_flash {}; - FirmwareKit::Config config = {.bin_path_format = "fs/usr/os/LekaOS-%i.%i.%i.bin"}; + FirmwareKit::Config config = { + .bin_path_format = "fs/usr/os/LekaOS-%i.%i.%i.bin", + .factory_path = "fs/usr/os/LekaOS-factory.bin", + }; FirmwareKit firmwarekit = FirmwareKit {mock_flash, config}; }; @@ -115,3 +118,54 @@ TEST_F(FirmwareKitTest, isVersionAvailableFileTooSmall) std::filesystem::remove(bin_dummy_version_path.c_str()); } + +TEST_F(FirmwareKitTest, loadFactoryFirmware) +{ + { + InSequence seq; + + EXPECT_CALL(mock_flash, erase).Times(1); + EXPECT_CALL(mock_flash, write).Times(AtLeast(1)); + } + + auto did_load_firmware = firmwarekit.loadFactoryFirmware(); + + EXPECT_TRUE(did_load_firmware); +} + +TEST_F(FirmwareKitTest, loadFactoryFirmwareDoesNotExistLoadV100) +{ + auto _config = FirmwareKit::Config { + .factory_path = "/tmp/LekaOS-factory.bin", + }; + FileManagerKit::remove("/tmp/LekaOS-factory.bin"); + + auto _firmwarekit = FirmwareKit {mock_flash, _config}; + + { + InSequence seq; + + EXPECT_CALL(mock_flash, erase).Times(1); + EXPECT_CALL(mock_flash, write).Times(AtLeast(1)); + } + + auto did_load_firmware = firmwarekit.loadFactoryFirmware(); + + EXPECT_TRUE(did_load_firmware); +} + +TEST_F(FirmwareKitTest, loadFactoryFirmwareDoesNotExistAndV100DoesNotExist) +{ + auto _config = FirmwareKit::Config { + .bin_path_format = "/tmp/LekaOS-%i.%i.%i.bin", + .factory_path = "/tmp/LekaOS-factory.bin", + }; + FileManagerKit::remove("/tmp/LekaOS-factory.bin"); + FileManagerKit::remove("/tmp/LekaOS-1.0.0.bin"); + + auto _firmwarekit = FirmwareKit {mock_flash, _config}; + + auto did_load_firmware = _firmwarekit.loadFactoryFirmware(); + + EXPECT_FALSE(did_load_firmware); +} diff --git a/tests/unit/mocks/mocks/leka/FirmwareUpdate.h b/tests/unit/mocks/mocks/leka/FirmwareUpdate.h index c793ca97cb..99c49a184c 100644 --- a/tests/unit/mocks/mocks/leka/FirmwareUpdate.h +++ b/tests/unit/mocks/mocks/leka/FirmwareUpdate.h @@ -14,6 +14,7 @@ class FirmwareUpdate : public interface::FirmwareUpdate MOCK_METHOD(Version, getCurrentVersion, (), (override)); MOCK_METHOD(bool, isVersionAvailable, (const Version &), (override)); MOCK_METHOD(bool, loadFirmware, (const Version &), (override)); + MOCK_METHOD(bool, loadFactoryFirmware, (), (override)); }; } // namespace leka::mock From d6abcdab18f9f8af2906efdeb03a9ce9a5f642cf Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 16 Jan 2023 18:11:38 +0100 Subject: [PATCH 037/143] :recycle: (bootloader): Use loadFactory instead of loadUpdate for factory --- app/bootloader/main.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/bootloader/main.cpp b/app/bootloader/main.cpp index c46066fd5d..75540fe9d9 100644 --- a/app/bootloader/main.cpp +++ b/app/bootloader/main.cpp @@ -89,8 +89,7 @@ namespace sd { namespace factory_reset { - constexpr auto default_limit = uint8_t {10}; - constexpr auto firmware_version = Version {.major = 1, .minor = 0, .revision = 0}; + constexpr auto default_limit = uint8_t {10}; namespace internal { @@ -151,7 +150,7 @@ namespace factory_reset { void applyFactoryReset() { - firmwarekit.loadUpdate(firmware_version); + firmwarekit.loadFactoryFirmware(); boot_set_pending(1); } @@ -431,8 +430,6 @@ auto main() -> int factory_reset::initializeExternalFlash(); factory_reset::applyFactoryReset(); - config::setOSVersion(factory_reset::firmware_version.major, factory_reset::firmware_version.minor, - factory_reset::firmware_version.revision); factory_reset::resetCounter(); } else { From c630e9765a587b7840577611e82e2b19cf802124 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 16 Jan 2023 18:15:49 +0100 Subject: [PATCH 038/143] :bookmark: (bootloader): Bump bootloader to v2 --- app/bootloader/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bootloader/main.cpp b/app/bootloader/main.cpp index 75540fe9d9..e908ad2c90 100644 --- a/app/bootloader/main.cpp +++ b/app/bootloader/main.cpp @@ -40,7 +40,7 @@ namespace { namespace bootloader { - constexpr auto version = uint8_t {1}; + constexpr auto version = uint8_t {2}; } From b25ea3504e09340b69a503cab06535e06e506d4f Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Thu, 19 Jan 2023 12:16:34 +0100 Subject: [PATCH 039/143] :fire: (os): Remove LekaOS-1.0.0.bin Deprecated, use LekaOS-factory.bin instead Fix UT related to this file --- fs/usr/os/LekaOS-1.0.0.bin | Bin 402148 -> 0 bytes .../FileManagerKit/tests/File_sha256_test.cpp | 8 +++---- libs/FirmwareKit/tests/FirmwareKit_test.cpp | 22 ++++++++++++------ 3 files changed, 19 insertions(+), 11 deletions(-) delete mode 100644 fs/usr/os/LekaOS-1.0.0.bin diff --git a/fs/usr/os/LekaOS-1.0.0.bin b/fs/usr/os/LekaOS-1.0.0.bin deleted file mode 100644 index 2194849f8c6f9370438a22c96ebd332537108254..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 402148 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⁢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%0DJgidIVIP#B6R;nGopzbJ z9M(hILRb{=AKOP*MEWF!pjv`HT8(MS-ouPqJRe}7OHw1&45IXOQ~8A3w4bFjoQRCu zsP>#h{p21lUj@7=;%@-@;S<_a0@qWJ!F{j2c#5=-YmDOl&ebijsQfaQW z*e9XB^h~w*bI6uMvQ>g5@x{#a^}CZ7FBQjX_XuaR_{AUr`4imi_Q6-3}kwe2sm9J+hAYkcax)5Wg$FKC(_>jLKH_ zX}IPXi?F#(AwC*BV^@n`4j+MCislBx+~`?Hyh`F2Yjq}gZWLncn9Uk+CpZ%nyx=duRwOFyMIp!cNPuY=}b0_wQ{DST4<+{(yQ z{^U!26L^g!zUO~5mf{v?teQO$F>4|yDRJF|?Hh+2?QNCivb7~RqExMCN~+p}6^T`N znMRY4R7?_6d9PA^d#cJ~#B~(Xxv@Czc*3Wfe#~c>?)B;Oj{eM+qw1L>EC43Rx#7cr z7Zk=5J|*~o=~l+_wU-xEMmmFG1g^vNkk(Yf z&=cJ0``tlZ==-s=J1PJEk^Fz?);F}m>#|PJ!rvZ;^6G~FE#otN`)l|(K_tgUHSD(d zo$OZl=aQ7zk=E9?+aiy}V|S4WI+Q8BS~X78C^!#JC?-sCsl_T-oRS@bEz?xxf%#04#hcP-N#!s3DdHI? zc_-!(d&~sid3u@BwAd*vI@b)2NhRwKF8dPjOsSCnbkm)9jZxTxyRg4lbyTfA&LxL= zb@0D4U3?}O(w;&am3#@hhiHL2V@MwGPJ%3y>}+#8xnyYFyo__Kv(BdSu5d1Q9*!a2 z>|h(pCJq^UfVrJoc!@kJ8CgRI_5uyip!Q@pJcrXiEisycOgrr83|RT#A(GlctvA9t zUEdikM4X9hSZ7n5++br*#@a7oZTVTldPFu`Wmm-Fbi}xE1tGlB+sb8+6mf#m%O*p1 zY~+pPL&lJ->5kKm6URib@Q(v;o0~bYB#1GLV4E3Fom(O5cpZFovPt@rMe2W@AUIiP z&%+9X7*n7Or0-V3ce=*-k=!=1V?FF5#zPK}z_2E8E^N?0c@X>x;z{XFsLY&+xF*sI z8!;Ql4AVYgSLMT%9$SS^! z1-;D__*deEoqY;5WT-)pn{R2UI<1~{LW%RyzqdsqVvRzVPB9udHDrE|IrtX%O8UVG z>Bt*Gxv%%?5|S`G3kQh<_~+2G=-VUvh&CgNVe#IW&EOs$kYrl?8*fgYaJGlnY3cl6 zjuuiyDmm7vg>;ynR^f@wP{X^8wojqm+P2R2%F&6!`@W^ZUwka=cE`eQmx0}G7Gze| zV$z7_4SAt+CO#SK6~->naA##2{3-G~n<_`otKs~sta-f4V=PV)zO7r7H@BL~Jtx;)Zdao2gSkh2dNaPcsHIbn8Kw5= zA`_^L3)Ql@%~(9_{~-8@k2N10p@D=4eDOr;pHU&yLt;z%RYam;lucBpJLtklKjvX? z#!egds9K%=hFxtZg`YvH3%JVhND4hO&LA>-#zcv`mmtHpY@700+tWDDr`}1j-&h$& z4}T9v3j&HpCaJ*NUbRvkcS7BK6nrCjZJ6LO7aPIzZLL(aFwgen*5Z71rVt-L3~#j- z!6?rZV=-3O-S7!AXg$-Sntfc^(%P%#+U?q#V(Xfr7mg8@18y~ghrU(E<7}!WT4<11 z!%CslehSpdIgFfr99NSaTNgV}b8%G;^7hA`jE9tuh0aZ5-Q1QnGAhHsVJ)roztIxB z-PFQAUjy%++}C~F4WCGc-A;J+Wc54c`A+;(vk;R%2K>0@_X#&pbOj*O2y7+q{AjtTx)Ch-V|v)4z`u+l?{Ms~OS@-n998 zZpZ4I4i8*ei0$yXN82*FKtAXE6w$w>-x-$^j{vQQ)pnWX)v!=eSW#>FR-4j``=cS+ zBvxm~P0D|%4Q-OKr_ZxTGA!F8MOt6ArFW+atv2Ja2V#5h?Jcl*QjSN&XhcJuj`^oS ztsi^YSLio}*iTA3v<&-^79^QtjT2pho^_?R7t1eQb{-(_d#$JD*ptKohN9GM6Vd8%JK zGP7<0hB`Y#TpihQ&fO$ySmd^p$TRCNm|5ZGy=|d+Rcv37JzZQrL>QAM<4S05O$mJL zV~q!VYUBQE=GTmWaZfi@K1H;)F?7aU+sh|U#fep_<=3FMfW~%RFP{SHI^fND^eWAz znW&#S@U%GW|5}F6gC3H@spqRrn2QQT3BHM>lw4VNo95q*H^IiQSI_fJ%3Yl4Hppfb z;0_Q`_@0tx25`)KZE;@g@azUZvzC!#>%~YT*Muf^}7|lOu|cTyk~Ep0iJzz)l4cQYO7^f zHjct~aSn4a=(4~+tI%~P z)U*o8^mUfURjDyCO=!KjCwE!b7YhM1xeGf;dHst8Gelbx!wpVgE|n?CR&acwo*jZFlS#_)Zk5Iu zZ&RC3D6fpqWRD5)O&cr6n6SpVdVpPx`;@&pE-&ZplP_YG>%sc8Pyl^4OsU)4_AGoL zP5__h1~m#B*zIdloj*a5FG4HT=W?w?%R{j30SvJ7Z5m?=(JbNIR7w9GN?!yJ4~inBIVj&hE?XP-O|-;jA7Mtt2hw(1ym zoh;<=TXVQ3=Hhkk!K=&mO?fK8Kg_QYo)+#gHEPNb7f}UEnC?# z;_s;h#w2L2%*`nSG?y~PXIz+HyrQWK?}z<&4~{j{=pjOd3-jw1O-qZN^zZA+?!pYa zi)Pq>vTuw^*QemK%J?QTBm=a6|Jjgc8>dSuS2kLO2g@X0@wpv@pu~3VzZEtXpgS1T3vdyloyDi7!rB*|7T;-}%LVI$g->{E# zM~dcV+s((6%~8N=MXCWf&1ZmD8Ul`oaXM-Pusju%Y8$mI>YZx*Np31^=Srcyh%1KPnfXb00&Vswyv zAB7{Pmr9Hbny=~vt7u_>l?x0>^ zcQy)FXHP)FDLqeh)lyZ)2~hE+EtRkg>E)wV+FxuxR23;UMp!*tRP2e^s-pIteGxjH zy0)IyQ1kKT9J5yo?YOH=C4LF~IpekzYx*>-sya`|Dj;1o|0*OTGM7AEd{`oBNxZbTc^^<^03TB%48Srb2HCAowUR{3@v?m69-ObrM3O)gi7=;DV z(~$qjFcZZNgp?HCq;9?hd9kdYmZaPYsCzrSfJ_2ycG{c9#j1Nw z?6XgCoxpyRO8v~6FGR{~XA<_C0e|Fh52QhTK6-b|TbXJpMlD82)kbs`kVrcIJjzpJ z@BY;8J1SK|10X@xXyXA1z3Zhm;-|@vcA*RlMtsV380)Da{#oF6!2cvXl-!FIRhzW0 z3LNzy?PpleN!8jDufUQcMop09j_<3_&w2P5eQ{&H(aQH8WK?3p&<(IoJq^8G(MesO zN}g9Y4$|88Gta)N@atOCQ;z3B)1m3pF+1ROXNRQ9+yS0r8cym^Nz?Kd2%}}S3__A1 z?`UfzwQ#;Ov;rwxPTQXoa7xm{m?yw$fW9IrD#qjP{}J}&GD&~)FpXwmf{Ek+R;*nW zL!bF3nzPFiba?__Mx17{uns=+^t;35c|A?63Cshp7s@~EUy6uMtnp)?9{%;ijXgfC z@gLB|9+zQCHhI9Y1kS4g@g*)@2iykPU*@^B~Z60c|&g=jlQ~HU|<2YU0rXLFKT&g3%VG z2OG@q#{w}_YHMq4cU9T|t)kej7_AdPcd|M5_x{Xm0$88_|M$vkv$Heb`QG2peOx%* z&|{c$hA0Ss#W?k#7MNFBgz_o!$3v*wL6XahA=Hys*b3T1NGZ28osV#NCn9`a zdxB0#b;qq)8D>Z^-+XZFCS;gpq5pMoz4N)nSLKywn~dVSn;=sxXT-7%+u%tBsa^L| zIMWP#IuXeT(#rSaBy~L>*WrH;qKEU9nGIuWk+GqcV*gP&Dlfs~pFEWEU>Cq1nj*gp zud%d+2H5u*Bo@|^rX|p7ODUPm06!MGL1-23kfzOm#}MwPNd~!jfIXy>U&H>8x~M+V z;ibb{4jwU}d~DDg?f`~cu+E@Hanq}IQtV!X{QUtvbXXY;nmz?3VS5jKirO#YxyJxU zH^BzU#8BiecBeregx$~)b*>?QreIik*SRuLpiqu3RQhzpmLwIeb5Y&6&Ojaq1gj|668|dxCSm4SK;| zil|5;WFDCcFR-&_uKyG+9>gZy*UxFf)c`gP9S{PJd>=sFB3fnKex z6xcZsMC_0h9X^*SdNtl9Ik`bF$UnlHjsP$6=m+N}d2U97LEeivSXvuAR@5LjqEu-> zFH#xmp$T<9j<{7?8L}r#6=0kIuG)_iBQw#RvM}Lm$Y0 z38Nh#I~m>$TV)UFyWeOcsjET$(+OHvE#8|!eq(^EC@BBUCekArrs&v(w3vF?F&`_`RZm$iP``hDw{srV{{ zHVmSn68n9n`o=xj=c|?>1IxZ9!e=o&N3N0#W7S?KNs!fe9d=u}c?(;hgm~nFfG;!7 z4)v9FB~_wCr>P2dg|OOZL%dxVzPI5?Cl6U=`pljV4mm{4UDW0wd?+XPKjONAlpcdS zZcDdv{m2nm<|-xwAri z^aztT<}Nd8F-SP!jtQ8he{^I`);#s7HulidN)mJ>yjmN{rZVGP@u!eNgjGp8)BUSN zee-jfx>)qsYsC9KdBjk+&BY37h=205lH!R+r$D+&yu?qN_9=Qtj_m{Gk6Qs}in&9u z*53iTEqP*hx;WLv4H%lg>z^X88Dwr|8*%Su90v-G~Gpu2B@3inc^}9;SKTz%qoZqq^ zmI_b9tBi8Rr+`m*TFKh+w35jzZ+u(HIq$VOgS_F*FxZ|I~8N z#iBnOzPXlrS#rgqw+yRg70!X4!TLKXykF5voP3Hq1-($gLMoF!JYJ5CnF)^v9lKag zXHvUa5%vs_wSo(Q^yueD^~JM7Z1MDvZSvjDyXWhQ*^sd~IXoZbnS6HYJK{yhdH5>L zN2Vj)1t6Qwk=@9xv{+6yvA=}1+l3F5c{^shKI=FD3tkrc zAr@UctkNURE55z>+q2fqQ*LtH>dOnT^IjcLuClPuTU@@OiM_=z`*B6Q@KJ>7mpjMa z_4pA_@G`DX$iFm(p2`PKPlhN@K=E0>YB%~E$y3u7PO?ZAx2qRWwwgwDt5+--I95ltc% z{lRO9or_(cGYqJ3HmS~F)4M@G!zxr~TL!-flOq;=1kt0l_q4Kx>&h}}S#GqfceE_o zBWHw<1=AC7#1z)x2#@c4rfiaCO+~SY#Bx9yHP0*Q%S`3Es;$n;fO21`^CPMHFEz)I zi#d^Bw=x&9hMkBp9*2E4VkE?)-k}4*6gbi^*<#tkIdimE^f0!|QOMJQ^l-6fT{a((4SRuanY6n~L7p%?9#9m@;mQKEo z{C~}WVn#gFy1C!-p+Nk%3f{V(YI==-g~zDTnl z*wM~(?mv3V{qTtWEJDxkQlG;kIPv^n=<^61HFZu%tRVawR7{XcD~PjfLi?YAI}8Pl z@Mkk&G<#nodThb~{ogo9V=)iV{~rtzZI(Ph|Ety9GO=h9VusT?)OM!B=7Fgb+|IHe z);*uGZ^c9YDlad`^WG1>`lVwFA9^Oh7Nv*yqSR1&9{gQrv7lg>wzuk5)i1;jt{P;; zi|c6>#>qotVj*J@>?hUtbn+0tIlvUl;ct5He!M5veiUJfzrZ{1he@xm;ysn7oS^pJ zqW0UI$cFRgagE*-)MvSPrj0t|B;kYy7XA3h169ma&U?r;1+Xlh{DHC}z_k(1iFNS6 z&Qp3wYdb+PJ^u@3+<~Vd&w5%JD=>C<4&Q_uKvCp~J%`e8E)11y-aU z1tSLZKKm>?JN*`R7B~IRVRrxA0H6~%58j$CWVu-Rh1ku3Y`5AwAMLAL6>_6iaI9!i z)wSe?m*OlLWAy)f16IGGa4hb`qF)WTUCV$w($SYkVwBH`-g{I@YoZ>Vh76W%yAq)u zb|uzm1SdE9r>k)XribsWi;KAn(}dj0!t(SjDVrE^D_}~x6&7{23hTYK@H3E?#NyMZ zgxKlUI&+Aho*t?Y+kJ(>3$W=~IA98Jjbc+eeD(Lka)@;BtqCu1`0fU8w)5eIphpcj zGhak*1?JNH6***^pgm0chVTTzs+$l??rB&P0#n0xb_Jw)juWJ{r*p7z$b6KY#=Szn zv0^xph>0SmFM_n|Fyg7gLu!mLO*9C}d(8tnsi0ghR{#R!NpwHm`Y1RO4w{K$~8E37#^00(XO3F|H$Cg<@E1 zd`iCj;qdjmk6wOJvn5mciIMgmRolDgYwi7Lw7rpX>(p}heyv=?XgQ5;*UOiNcz3$M z4CtkDAJOi5`6HG79eI9s^!fJ0^LNzeTAcCmF1`F_qJ#mlO(^=l4|SYU%Mg!066e`a zrI(KmQA;Mr0z_AS11078digNsiPmJKy@R9eQ63BG&%PV_vvc%$dE)sKiT;q)2V_-p z(~tqy-k!3oj7j(0uktE-+22L*d8CaWpbmpNW-VKQUiPZbO`UqgoJD@H$}CT6<-S$% z=#HUy^!Xw9`h#!C@(d9Cwk^KuSi)HQqb(|&)611z9>_CR0B8B}8BAq`H;RzM`Wxu? z?Rd_)PSt^XW%t*m1pA|tyZfVWb*I9opSTBo5g%fUQp4F>K>3A?6?*w612&O8MD#eX z;vd+uDB5nMb3|u|=q$@3(yCDWlq*3y-pvL?$_$NJY@BuJ30K_?P}Ij7IuU_czmt{S z{q>EUOAo0(x9ig_Z(m||>vqj_^+%(_ynB*QqsB(+PQ_R>zGhW0YYU4g0DO@##Hk#R zu9X$eZSRjBd2NKRN$@{mDrrPXeMoO(V5cbx{m~zzly-ia{kM?KBVy+ihYPEAqtBc@ z4{$gC>>K`RCpdfl;^gp$Pgw&-QE;`d)63ZdbNvKgt@(^-1^gxG$tiRF;wBclPElGE z$U?54PC!xGPZOJ4$CYf zG}R(v2O%2~xbJ>YjO@*bO4v6o0mc5goqDgAzld#D?pUJnVl?k2WCIf6lc}>OgX?63 zY+x8bB2f?OOqBYgN|!d@)F0wxX;e=kYn%-@kw&r+BM|}5sdj=vR43vebkp9xGGKHY zvA1FCJ4|(Fc=YmFSl^j&$|5J2ZraB$CiXDx-$K#kdeTQ$vCpggrx|S~)it&DM_)T( zfej_QLofe+fZfB&Ws0#fRb1)iK+7|#n#PZ16v{W(}&o(61);9>l)>K)5=VSOoM45o6e}c8fwof8#qgeM5N-}1r73J}!4mI-9WVaUIlv>sQrh^ArioR&Oc)h%8fAH?LyFCZhBkKlGx!^q=AZeCs3;yxmBDbceaq zp$OMV&zB+!%Z*RPdW=T;Swvk^$o07Vq$*@u3$nLMvu={(@V_X=3i`UVb!>*Vu`e{T zPQK1Z5euPa5^I-wn7(8^aU_3_hk~3fgEEpmym#|$fd@A03Ufnzp)S0)HQpv`jB&Qc^Qjn3==RmgM%cQhmk4na4W&uWXv7K@pvW9&XaB#FUNUM#Fd~s zs-!7q?~8qJ>hJbG=Vj;W`(D)LLI0e33+=O?HgT^{^jrz+EAQQSFX#;!XOG&ufb-OY zZ-O0csbQgHJKdoh;KuFPoDtx9%Q)EnnMd{~;cki-(Hj>6R@Hu!kxP&bN8Pu${{d|L z+Wl7S&IxKXCN62>q$$&yTFFYy?C6i`x-Pahwd&g1TR&=H+g7(8ghX1zX?;FMyV{DA zJ5uuZ$3JR0*mCXnyT`BpHMQ&7li$IXodaytSMJ=L6hM^X4xFr+G~%zqBmE{?K)*&e3%Fhi&y9;)S3lAa1Rfv?xQ&qq2jkIS z#@PaPKOfF=O$Cl%<_UX(6DwkZ-n;s zk(xw(jC=;QJr0jJA*gc|0H!kX9|sEv7y00!vi>4=82N!bmWWToHWr}7pF!ETZ(RLI zSrGi%4e$OE?^63j$;6{I-3&uvteCKvN2+$RxxmDv!mT1&s^D*t*QTC=hfo#Tkxfj0vO9Pqks3A!@@BWSN&UGXy z_c?g^GIC+jScx{wgS?(chQCl?%_Pa1rt%ItMDSYv2m>7K6W4*Ku_3m%=q>ZC_TKAh zM+SN5T2NCdYI++Pr?j?5>O;I8`8EanOg=gYoo(NPI#C(7gnaV;hJ8e|ZSgUJ5#&#V zICRLfvCCELVy9Z}s)*^YoHu5ixeXeIIm-AW>@0SdUbZO4m|p(4<5UMQ)*$So`yS+` zC>80zcoW_N?VtpU0{~IK{pkjWc^@-$d@n|0L-t(D!%1;H3%2bT}E7Vf* z8Tbpb>@1U7zp-~KdkUVpX@l2l`xHDJXex&0$*j(k1G5SbAeZeMW<}FP(5yIVRuc1T zP~XeLdldmuL`+>(uQ4)z)71I9v240pHXmiT!xQRStrJJfvMysKC$w!A18uEOLIYrK z^erfxq(Ug>3Lq5CFXxtQY^e?s`=-q!FzedAQ+Pw0Bi(4ZufNB9!+Qp4l^N!8m*7^X(~L7csVZ){3#kMxB)%WjPd-sb#~DDDbN3AtF}`obf=anD$a#e4CPbf zlBlmhY}EmtztGC=;AMTB-9xgl8^`Q;WTc<`jHXs4Y60w`2<}^35lf<;m9Gv}dk?hI zepI6RZgZys&ppVKRY?UwJH($lE}_LokC?z=!B!j5I@XKq%erTt2IT_W3CQ*-r2i74 zQZadt9eMi_Hw$`XrwG3*vh99s$UuEk``j_CbFWZ!LA?CS*J!N^-NeBxYTOAcm}Q%n zHegpKjj2nS1zH5>=4uRWEMf9>yBEu;*sYWAdRozgu7bm*x^E|)Ny3h$^Pn3rALe?u z>*!3F1{lw5#bTy&@uGCT#imF(Hf8!e^I>>Vm9Vq&=6>8^e3RlMUEAM(2^(MGUuF;W zM-jh6Bq>cMsOdlTkEJt|(CN{YDvW=Deonh8d(hfL?iRO5bT z_k;oc8+Vrf{gU2_`P7%Xl<0%-r_=hTT9wRaqihSXjqh|cwSpEwj^@(N<~5`v z-e*iY(;vNOs2N&?!rYO)^g-9^#sjS-K~^lxU9D#RWaS@rSsPOmam-%pCLRJgykn(- zTCtjJs6mP8KzW@Wq+QG~4z;gh!#nSspnWnIdS`v*K6Rh$OYD=9#99c6waCCX zyi(D(%EqR;B+Tsl$BdomKWvBk735|&t7GkrM-QDCxzF`qe}Bp7{gfN-?@;^D56tuR zKJ+0jA?*k0x;{MVcWx$KBVzRRzRo2t(!(eFCdDG%<1gDdKFJ8bvpmTj(8*(T(n&-u z0zXJ``aV16Kpj-qHb3VYUou2ZxC!xdbjy97S!4H0ftj_ zLqDQb)!Qg1um`h%oJH|1q*AUcwrmkqfnQISz#9)eA;pCJ57<%u>x?$vH1 zB9!VSBlfKhyJIN=@J$G@gT|MMhQI0^JQE9;WR zjOU>1O@lp#a|3JTE^tL-VMma)(FC59Eh35IvW=G`q%GzbKDk+xSuU&WiH*pqZY8@= zs+aOA(P+yn>pGO^37sG5~rA}+W;;#)n z>3qIJ_bz-4T~gQm{zsj$(8pzLiM2edytU)=NS5nz1bhPg`Z8*{3gk}Tz{-CNQXMbt zqga;<1A2FMV@dEyKVNy?M3m$H>-wzDzx(`Vdmtl#NN3#~daomC0p8c+en~)A0ICL0 zSh`Ceb*yXwAJP>-Pa~+MasS`%H2aI}8ul`W@3e8?aSdvuDkDJ>LFEB4evOAVT_6wl zbKt(({e=`6#XccKc&*1FOB<~3c93N~^(LdU``T+y5(c-IS4`W@ninS@x(wK&sV#jHIncx1q z?LaE-F`};WT^~LheG4*Z&GGrVjX?p*IlW2lu`OGhb|L?bvGVTL1B%f+wqgGIu-^*3 za@bdDb2ibM()(18+&K|?THZBb@t6LaWR}ehH$QPFbj?SUi9$(J^X6Yb9+GA|>MMH$ zRsonzL_?D|A+v&C{L-g;nQ5ETAOQ-*qV5>!bRCy(tu1Rh;Z^06&^9pLBYmCk$?>a0?mF#?cwOVhRPj^U9!NJuSCht-r0os zGdi+cP@>1XsD9SPxo0SP`?)5%cR{_UMEkm!I-PszdL_!^>{Ged>$SJCUSpUj#;tcv zT-@g$E9>~lusujd{jSx! zzxKbgIk_we+F|PN&xiO6b5UY~`+N)6G$EnAfmP9hdANJZ&lFq@n{nP4yBtb$|08lc z=B+pKEpZ@n2cA<_g^$C=O4Gu7>Y1WHY|L`qgqRr&c;r;Tr`v}95&_O0om<+IC;H(R zTe)u~j|k@4UVJr3dvPB2lY~z#Xx}U~f;A^+BI?1OfCRlrvrka?8eT?>a#%Z$XL$MB z$~F5fpM#S#x=DYj!2s}1Wky#qYN2>=1x?6JxP#rv$$j9aKMBmBNb5U{>oj{O$Y14K z6~wu~j^h!>fa=f2)e^-3E2!;?y(wXLR>sNyd`+G4X4=ox&(uUe-$1=hftlbX_1D#! z3NDna*637I4}XWqTN+gs>`f2|b3!MBMN-YV@Qgt?r z$Tf}Fd~xzm5GPF=uM++1N%Evk6<|q+ac)-obBkygwd&&JH{yEtKM*zUT7R~T_J{b5 z@!*R&*@HK|f!VO*)MlKM{}|Usp^f1oI#N((egDIv~2E_Be3yex%T zfU~JW8fu<95Q$VKAOz`X8PT%ovxqx!2d7p1Dnj$M6rWcK7vb9komci29W>?oo@ogk z7jn$T5&06)<1~G%wy)%}sVXM9WeYQHe)wipZn0Q4B#Cu%=btxDJY$L(!2!mj%D}2j z!(o$jPhHZ0zJ$GnLpFrZJ7z0p$VVKEsEjXuy#?}@V&l!AC8insSmWQr;}(4aud`nd zu!n7Rg`1V=kwKC!j}1SD`2j9O`ot?jLWfyR&#R2?EG7pS_I`_w&h zqs^ZO_sB0gw@R-{a2_3)vW0U!a>7(nl##i37PwpUyR02!I~<4CN#S(}vT zlpxV=^c=C;x|{t5)gB=~T&SLg6!kP54K&yDuwA5`-wYdJ+V#FJr-PH}g6%nN_4HIuocj~%X`B;!>Ti7y$iZNRh7hwl-rLxE^#O@(& zvBozNtwek>ts%v;Aquw|*4J;MUuJtrxVP2VcsOXX*%uzl73KeiZ>}#`jktrXd>pn) zdl0LYk$$>Ce7K+`8odiqKjkiZc3_Y#&?DxyMwLXPGrEOF8ryIbI1F>zzCmoXH%a1Q zz!BoJK8mE{q<_%EB*wpeSX6aOy?)Y>wFiq~Q4Sr`BNsQeLwm+cUcjLpaj{XAPDolQ z77+_yi%e5X{ws3g&l-AN8oJwCS*z;`#HL!bp?gX&MSlFsSrON=cJ%Xpv4L;10`D=?4L zHo<;Qt{j3q3!J@~*1wCMY4^W{`#FjGbJY7IacO1@=_{gAB9mry&lGh`1s8-Rz%lr?#IsVhqm$oB@CEcuvjY<_ zKeXaL>=Ek;7XIsqvxDczvCvi4u&qJUn7-b?G{6R|624Naj}BZW2_xp-;T*%L`CL|M zdty8GgsKbQH5?rigFn2Df0*6BZ3DlsDZ%4Kqft;_M8DC#0i9IV1kZrhkqE{fPTt?& z1Prg2je3J@G*E7!W@Mn@cPv=V1%2>Y>+!rfn`Kr^GzsQxe zb%ntQ&6By)Shc#LBp~{mom|+H!^z$Ki1G;k2*EWNfz1`lkUly+p^skDkP$eI96pF~ z$Pf&o=a2<9Jmsgm#znkruq3vfG#OCXWFAe6nmo+#8Sq)r~KAhP~p1wPsrtS>aMgP6Q<(LKY z(dK&6=WFlBqsP0K0SnKWW=1WDL)buZk$(tTtT`xve*3TZMSn{0JK{%?Z;KZqMXJvK z1L*wWJEhwF(cIH`W+vpx2DCmWkljkyg)Ai)$ea%Xw-ME;&7G!)|G}sp9`R40y`*!0 zE^+Q(#dzrqO@O6OD>S*BTppwN&he-doU9Pkt_55Z+?{uHoS>6`2Cs*nKzscRmvNl| znp(rWr6Shxd4@9N%AX=(y&%1W$Y+JF-mS>ix4{VAqE0>-KTuC+pOY=IhiVaDJ2tAN z#h6qc1a;TN$qw-WkyrI(D%{7r&7^Zp$R}4GQ=$*!^W-tsy~jgqd;#yX?w;$e!P6nu z7u_dWUb7-FgaM<`Pma+&C7K2Ljs6&hvWW~w9kC`vfae*1z_f^bLkx~yKxcckcD-jo z&FB@a1fNcHB#&r(3@8O#tR*lUJsg8P%UT*Vy7yPR5y4)Qi)BEPhd8|M^VL@)8tkNZ zeVwnvb_CKX*t6{K;OvvmI_uavEw^lPZkuyLxXjP^UsAI^UaX_&IH#_pFlp{zbm*t) zLxyQ7A%0p)INQry*pC#(0s|D3t+>)5?X6W+K|}+bm_`&?VbEzAtxq22l06bpF(`5;=>nkZew?* z=bYyOPu5D`0;=EqYNqeKj-=#0>zL%6br1NgxjpMLTzh?EbH^o_8Wa3+U~5bJbzh>b?0^C1lWIW} zHJqNEsHG)fQENfmUqqoVucbQJy;?0JC+`>Cv~qIi8Th|a46E_5A<~OW>i2i#=Dv(v z2jhz8g-!N(@S+;qu)BdGS-snEQzLoF-rM-s#?}s6g|>UKrRUWzD}G?=>HIRbMgLCnT*p@rJ1Oq#n6oV6 z&ybG++1Rkf1BiLX*kCgPNF!t1Yr5H2W?*OjwEtfb1>+q9OgS9=44$Rm3azYX3g;r? zY;MJr`qwkYZGOU8j2sCmr(US1*z&tNbbWm4_wTfYEz>i?x*{P=UX%+Oxs59u@*2z) zd@q})lm9Y2?&cRelEduu0@wI%c(KD$P(hrvy~o+<|A=T&cIo#){MLsMEg#x0%2YS0 zTj?ssnv=ifxs{BKbMM=>4>DKU2VtxYJ=5$W>0Iqbe{T;>UVP`I#qv1QBEZFhYfbTJ0CBH8M>7^8258ex+JQyR^+g*g);()`r(-MbP@HsaSJ+x~wYHXV z$}Lax7WUB(9A z0=k#tDh#yxS=eV~fSbrU?ptuz1vFW%*j0%Lm5N8#2emT4htPp@Wo?!-4 zBgzGe2pbh+xS%GDC{2mp7UdGe>7+5lqo2ghR-s0M-Q^dGwMKxRPSO!vvg<{8qf<6ZZm%=6CqTFDyeBqJ9<=XSbV?)(mX>c~IyIYp6{A8lTn z?JqmJLn&J_?Zghu$8eOv{vkQIP9$Gln{+e&8>HRJ?WFZUZpU8^wXcOI7o!73Ei6GK zh5Pqbn`h|cw}#k5&@(AU*pHS;%ydTH-o#GdjcYz^ge{NcO2Mh)rn5as_J>2s_6?!k z$~?rNW~TH-XtWQjag(CBTPD>6Wn<|XL=u;yg9hZDwbt-65EBV|V9(Il3kG{p*oYV} zV$J?1%hdnbi1u!)xwrb1k|MR?{}{<$Ux@ipdm`(RA8?+Bng07QFX&_)cvGg}bSM>c z291hqdQSNaHKz>zA`&o~&ZGp4$lzr{tFWtrPVe=I33-%~)ZeT6y|{Zn?!uQF?@S;1 zLu9=%^-K~hZXK7m+H5fn7BT#pWVQ4y{U1b#J2*4IPT2yy@I3ZEL1S}$D&n>q3Xrz~ z(LIG+a5h7Xb>anM!PM~1`g_%AEbYzjU@WUwzJsx}uY5*vz#^3&GhSVli<_`E*7i-$ zVBZ^x*pLbEhMftY1%b)825wgOGU5}$BFoohf;WCrK~nhMdSvv0CER$X{b8N_&LA)T zJ(65-T5bJbgC_AWkt5K+8<8i_1gXg#(%DF@`kM_UAyQN_q$X^BD5CA`AK)C`&M@*@ z{brGMwQszTp_3ni<->NxDS^_#9(EG9^OfCVa=Dv^kyG`q%OlsPaf)`}Iqi9p&>#W> zenFEzOy!Yl807UF_9Y_~sl5Ggv<+Tj zpj*J}@Q9UnJCla|h|djM+`E-;nSbej_tI@5iyU)VE{peG#cED*W%>iiO8j^;o?J5oZ^MRhfy3DrR}suzZ%zZxdX3*JUBhu{wf zD-S${)^$9_YmR>rIbBy0Jna>OOe<5WWevpXTA*NEhN9Dl0LKtL7;7>W>oYVF5>WCk z{71YX$leT|mro6`Q@@O~*By0!g;?5zWfnmLD2D`>(v?yXM-*^>AQ|6(jC+Hostnb> z+$NU~Q`w)c)5$Xi5D%^Dazt14kH`v~>kasnnbFjd=?mv?wdPP z;IVK&)|^9*rwYK1KgX7$R(*v{&V=1foAMbj+I^CMQ*;+j5xqn3g8ayziJ$U9Ho;Kr zVd7nWvo0r~qbMKVvXw-m)4Wo-t?^TJTDf?%A&v}{uGGqd0gHG46P5w?%KnbA$dve| zcUjps=bmnHYfAM~-4{F`dBl~7O^(>fbaFqBjMTL}PAz0tb1O|)XGqueGhb=viHT?LOUGz4D&mOs~+o6LEi! z4D>|{y2n*d-CEFXTge44BCiy(A|aaw;yb~bEkm^i|6Ld|wQiaV>I21aAnE8GcOCYZ zPnv4KKRgtDd9Vup$NXx}eU`&@J}?x07&6P$?kQrPh&sP- zwmzZ1En=i`VCIvykWSG*@6)-RisRy45((SugJ;03Z;$vA7RN5()-1GN(d5cNwZcfxZ29^i6<*L8@oqFuG%$ss)XZsN(Zff83*-#FHSNEr7!;48UgqyJ6^ z*=xgJa|yBs=@H-JEo4csA9im0G*!pq>PwuTsf>AcT3;sKW0%v@qj;)MJWUZZEBTj^ zPpFM>!!G4BvagB73$`os8DrS-6~}S#G$v^vY>)<%h!e>F71nAF`nC*GEUl+$qkS&L z{k!_5#C^-aOTdJEfM^FJGzUa?Lq2tcvWi`sg{7E<9Mk|f(Jv(4gr8hu7GQ6ICwZe! zK8zV${Gk)Mb6~FnAJ(_VDQxdkpQfkobArQv>&vhaQ3licCNMW)q%Az2I=M#~>G3o8 zt&Mh`*Zfiiq0v2uH3FRJ6GT*a_J1vcjoWui?i9=t^iOvW;0pQ3Pa!k8RBl6TnBB?@VK*$(A?pP%-`@P5 zG81&td&+FkNswfLD!P$Yx*0Md!sf%#M^6x4ume;l_17M7)R+RSY=}R!8Zo}>Q(W9a zD)+<+_D2RG9=+=r?Rlls;IGQlyEaW4cNafkr1L9X48SuS8a#@f0GJ|;53Q`Rf|e0n zt5?IfG)@r7_`PQVBjyHDp(7VmD}Dv^%&R8|p3%NR#0vQh$Oph5A?w0t3@g9&8r>)C zxLn2Dc{ovap$(A(EiNzq{DX>nJivrXP1U84kx>6kzV@|ys?Eq@RRvE*T}eg_gLo%xibEKu(U{<=l6@I9 zg7mq%#>fc({J9;yM1BW&F^aBzO;shLQd-Q;T^-0vxq>gc5;h>pkD(GAbX>oX zg*2hmF%+#hMty5NX)vAbhFs%>0s4jDS;DTO@pM9w#Vyx!T@|}UUC(E}# zbdxNZAXBPqF#2uyLL3V`2UzvJoTCAzFN>v|9<2P`xB$JN0rCsc$6YYk?3-WC5Pe2O zJYnTV(4wkbxlc%xu0U+ANT|2b0G)&g8<*=_DE?XtBkxSVp@ormwHDl?EsXSMqS}Ie zv%x0%Ue4$RtrZ3B$h$OJ5ftPS3nG-<5hB_v+c@_O{G4%3^yIuS_GEsFFU7wf88t-d zyPySkqtth>qjoD+hAS}Q{3d9@f~h^M-O+<`KJ4mKbB&I^KQ^H>W5Ct#n(9H5D4u6e zXmln)O#YNcZQdP|)Hz|N@xWr#?r+1d11g@5@C6e<1jIMw~DJ#AW zXnPI=JF5g0$>g8H*pPciycB6)>$uELnA9}RpS3Q@A2^Db7|^p&n~Z!PD6?gu_OE=Mr{4Q2GEp`g$ArwXnMn_^ut@y{o*h<4 z#}*h-HVyg3+9S8S4o38@pG0nQ{WwDShGtERVex7|X4_0F!hXu>9y&ppZMNgwsd9~? zjP4Y`@^RkWqZ+M08EdA|`o;?ri5f$0bQ6~nqFK4NDh7#}0>7jFS&PtfBUCGPG|zwn@6hnPTA?%(P7w-ll!h;JN@;uL_dA_>UIRMMbM<9g|Z2G=Jc8}Lw&dP_t z<7;!(zIM4ES{1ulwQbX-r0Fc`Dr+lnMIo8wP2!i4@z4s4td1Pnf)*fIvz~+Y><%;L zz^rm4@RnrE)2-RC{UonRJu>I5!iY%P-@NwL&UY{A?pcnw`*R%T61Hs~Z~(<|D}vmj zHAd~&;l-dtYCDsGA=81~mk5l!7FhMo0IhiY+T($<7!xC(iqUBHdXf7FR}Vm*!pL2* z18bRQMb{F*8AmlEOEIzw+pCs8q?SMH{b!_kt+{i#A6D!x9@aRzLkqpYXzE;$KO!&P zf)z8M*G7f-DOy{N)>OS0K>^h$myz9jk+--$y*NEw18R_$sFbEXpN+S5hz$A(G#pYF zS3D4&WUCz2tSmYPD}Jm?nrpxRH01GF!^wcTyC7e21Q7dYy}hn1A$vnC^piB#wAM9E znw)L=G1B#gl!{Yw=id~wv=?JYbD4k1^$Erduc-vN!I6dZgp+%Hg6pgB-(WAh2C>_O z^og#KrxRSYcXGd-sXax6C?QqXCvc9LBhSC*YIAbGeHG7>7G@%s(CtF0Yp3#=IU0Rr zSPI^Cd89wX7+)snK*Aw!`DN@uimN*J1ldmuSg|&6l5${zTlWewAf)deZGR%Q|GPP; zU(B5eO={i^`OGN_{tTA~JAjez=_gCn6dPuiVdQX(q>7q7>M;BYjlY9^*V-t~$e!wS zM6)#`DoNfT<%YQht>1^7k{L&tsXXONQrHGYez~9a2%_++-?UeL28k5m_7==NeRm?_ z$^zdlHqKY*B3v&bUZxqcyL8nqG-I^AbhJo2=JSD()^gR>ShO}S(c1b%Ya2&f^Ceol zJJH%Bko_3ZTG`iH`zy|x40=av&uptzN0>$<#EJd$05Unb=3|5x73AnTs*TAx+RE+f zm4wli64aD_ox=g2*rYQ2w@Rdiha4c$gWCsjR-O@E^D*O1n3;VFofYlerC}ARl_R6A z(2S)gMwP673%H|4D@BP`3e{E$60M{sS~)v#V=LcR+SgWlmsS&3M)m-eCrUdX#d)Zy z&JKD67Wx0YD_>fB*qa@6;Mc?bhdlU&X99lxp#Kn_^(LNq`#-@md*a#3{(_b5YnN76 z1on7K1AD3q0zKZ0Kt{DD=P2!>Gtax0R)hOPKBRWQtz{^`$4kE-Qh)FB(5ln#Z)?Bt zyhnY0zxw+eo@c1f7pcFqNRGhNcILQpL}-lv7pw9O$a|s6C8FSeJ+NtF<=Opoipe*- zwn-cHaI{jj52dr?L!KR2mdY$N_EJzg?~*FfD}|Me51A_||1!mKq?0vY$f4X=JN~ZZ zH9>YCmjj4?2Dz%h0(&o4?GTptLuV^+;0JY(XJ_wDV06f=kU0Rhq0c$6;^4cH5BFd< zz8}~0ZMwc3cHe`gGfykKk=^S8DALo4tp2|WzPwpcKtof1_l#kMZ@+FZz403Zd_rN{ z>axnW4;Ex<5_}d>1W4}yxrhy13E(%K-m#z~%b-Ocjs?KO2~v^muHl9yp=G!dBvuWw_l+QzzpOtf(cXVM!; z6K``RNoE^hz&sY%K&z3sif3-9ku*p!W&XDsKY`!LaI`|L@swI4e?yJN>uOy4Z#Dks zH`Lgz);L+M@j10d?uHuQ@4wc^vVW^_I`;87{{z+Unsl;JcN*Cd?v6EIX#=-shHo0n zIOHaVXCy+-zOQdPYg?{j|P?iR{jusxRA~Ru3K<@8?GI=-iGTL zxRxZ+qp@;*knV0ul$LP4E>W6lsY#R;B-rsW3V4)>u$>r=ZjY0^8Wxl#B&n8MQ<@^a z;pl%sW|!rC26x`6YlD?Z8lnre`xI4H##8JuS6Z)*r5%G<1974~h5?l|*)(g@sGpJj zls%vW?+7gx@U%bX0Ic%Fh1FxQ@}DUbQIrKPMeGr8Rvq+ob=%gjse66>{yMJqa9vq# zZ(Vb3vsYZz+}ggqxq)h#g3*%f{0YLZs%&tFGFK>qbx{+(iMCn`nIsE)2K<)X-sNAz z`7>3S&rtMV0|{GN#_heebDx*yKYz(7WvZ|b9vqPJHZ&>Igk=q{l<|-&O%e7tw3V4c zIQ1BdyGhdl4@c*oOj?Ag7X4muQ=Qa^V59;3h1lmb< z!QKLz(cM@P+DE&w3oMQvWBQqN#Dq}5R4>k%@k-Azv&XBh!Bm9bCJx8rIk|VfVsP%ZPKP?hqO`=E^1!qydb3^vK9)MARm@_CY(K)lsF^ zVV-egEw`z)RC+NAtigA{VG-qGwqfT2c3S1f733~;jaYI#+tuqQ7{->I@37c=ea}Hx zP5EN(kg`55v%TD3r*H3-p&=A8KAZ9`iwS^M8ih*Mg1{mR7SxOrOLg zhxdD-+uH%Vv|TuP7-27RlEX7?AtN6oO{1jAYg45hw3QAXRFB-VV`87FcD&7NeVyM? z^M2WoO%-&w-Pc4@stWN|EENix(xA*-CwmA|5}0K<^YlvlWSv?Mezgg^ZlT zNq0gU$c#cT>ww8!{cjK~SB2@r(GAc@Yd8UM`#}|b0!aG36Lii$0Z%|a>2!}W1IX(C ztDuxLI8gP6B$M?RY?6XRD^U!9p{OmMEKsgu;`dT+;P-~3ZfI_~8p`{c=Bfq#%gA31 z2&*epDu8CbW)wpd!FQKNGuAZ_eKuYL$hpsJTeYwCh3)$qfX#$`z)yPN9APP7oBfb% zQzUUC!EGA3G$Vnpo>e(}WON2zn~J?4x_Os)7h%mNNOzL)zfgd^CiJX?kM`rrETn;ua4|AfZc?d=1yQZw~FClam%PskfI#`@5Ak`+vY^YmWh!0iQ|N;j;-R zsjmdZ?Mn#f=SfV>kDUv>pu`z%w+iq{6KZI`GFAbx69xHLoK-29a?H@g;Y`fXj|TSu z!xDZbJB|MtLk~x1!_r=p%irOl-SSUhjVZty(||RG5a0L!MwXA&E+)+8`TxdhzJJ4N z*D#I`wO!R@@6g4=6A$Z2+WT&jmIz`EyZ4)LY#szSc3~TEtPZ=Duq^RCB(wghYRQnV z;>Kd&I}JAuMbqOq;yLJ5kQtYxMay7;2y6JFN#Ui{V^lmbW3YW~S^`hl2MJG@6L^AE z@x<+TPI!U^p3q=1qB@-R_Vsr)PG(%aW2C7`b|(7x7? zXz}vEH5{qo?`K}4dI>9-fCpirv34QoKEjoRC%+n?lRHup&qZpV=CK(V);yDP=3Fb$ z@fzVL!pTW9mH_Vm^#tKcdIMP6iijZS*{WeaLHV|?y@vI!^b^+mI>x%75M?wXHw<5g zv){!^P`&YJ)pd8@gxCANZV7Wi5(kPwLOlWhB=5o;T#q47sO{Z|A755mi*`N+m=CFx zZ4;;kW8V@+kYj@*m{GNGi1R|Ta}8_8 zk*nkrp@lc#P#SIX;58iD7EBX04C>4HD!LsKSQ?LpLuvKjI5tKE z6#_ov4k>9yGblG&_w&coY|U$NPgv$h-r%sdQ+{=fVt|ZLifOQ;yK%RiN^qk@Cw{Kd zg(N*%TCGZtQ0wm4PqDk!RkP}vNRZHkf;g#w6M3-HDd^^0N1)+4jXBQ%eDV$J{mwx<>xb(=MuHjPhHPv!>zuCmdhZKG9o%}R6k8EbC$xQEdez9yyc@y|O*Vm&k zK1PPbL`LkH@{>;PEF%z1K$NCwb3(Hb1KU_Mxu-};Jvm27ZJeO!isPnaCAm1xR9@1R zvqgA+$HX2+Zd7aFXD6eDt>smBp6_9AXT;AtM9;kPKSu5~=5*V@kDF$qrsA{6sSA5W z^$jZfMi0C1!%TLL)syOar{lc{>SQ>#kd@&HVlQ2VJVnOmGklcJbhptp$(!cR@uvp! zU8xsJT246A-eu(9#yY*X2khRX9!9M3knFnvHaCoXGRDqJo&9k~Btq{~U4Sr^dRLlz zyg${J?S=Kktkik!PUdWB*DCKTP6cB*5Hq-I5KGJMdC5a?3Gw@mYyF@)(n5!0tXuPi z`q;+8UR}GsfNPUv>TK$WM-RuD0Is`C<@D=nx2cq@Bz2HIzko=qjGTFla0R=Qkzb6l z`>cR5eQ_4CCN(U8ygi8KOR;$-D&_!JXOcvp888abngKOZFuKJt(Q6Opdmc*kZ{hLP z-ds;KLJ*6Q9kG0GF6Kh?aKY6mJqL3^92C9H$TR3&JbB2IpLo0IIQj69eFuX)C?#G? zpw`I^Mcx)bMY@DR7bw2FG4ab@~MHO6ZIZ;CWWcp)wnWse-HK3 zo3S7>{mYQuTjIGKp>Z?v7yXP)^zP_jXWE184rE>I*WyHFd#|e2fQRMRIn`wQualX;Ny1}CA zrauB5!Ps8%tgbad-eC#s^}g&hp0&F4cyk*@M=N{;@%FNjO&Y!lsDpS#tWCe6(t>#w zeL48`lYZ(2VvhSP-sfSRPvx*8m9Jl7O1MgkhdxBt(tQQ{WDp-%G1Wd$#?F9_zMK9B z5Fv)fy9wisgD%xPA{lud@GIGHGV+>!UyW6xju-o(KdFNE-WPbuduNd3-|=Wk{GnQKjOa7ypgin}{{F<+QoNk!$(H6O z-vWcp)G5ea{aU_m*m#Ks$pIe_0a=qV63B84wrW_s5XKRpAJEEa@*8j&9Oz8@UZ(^9 z`w`cS>UARy9o__dGCne)D`KdtD^ z`t5yRManXm-n&_IXN!|TDcGY-+H+v7%`hZ0#%%YpN#DBb10{9q zqB~jG49Jg{_+br`T@t*la;)#RN=p-Yr^^x6eoy!VrRTtG_i=@BWZv(|nXzzNN0R>) zg%K(yokEV5XS<~;CNFQr3(f-_Jz-rT;u05553z*{!$0wtsxaX4Q1XSd{yl!Q2`i1q zJI2MDWP%O<8X`Chh$l516)JPeb1N9D#q+##Y?uv<_n-yeb3qQhKnZhZz~avEUxgnk zBX&f->kmgT$Jrb{to0x82tE{n2RI^wTD(hhGJR%GQl+JZox8uo(!|*R7J4dD;^*w= zLM;(y>Z{lbnb#=0{?}+<^JyFRX+6LVS?E=D4ZSbc_ z$naEuopIpibU2%*l>@k9x+Nn^Vhn3&Q=by)r69IJ7SECfw7x82_FONZvCa)k! zq^~Zai1zrER)(xTOXATB>bXpoe|jtj{n2a`--D+*-Y}<@p(LoJKcS4-ku2|4%aDI% zBg*oSO;t+|Zt2HYNQb3MH%xKp~p5GuJW7-MZs(f4- z?zYIK!}Q7D>hbW9hPFZfEk z#OnVtv2e@%D>S&s7zHdfSzZml@lr%qghet&dOW^%C2|k%TC7|!iWA-fgh4=l)75p-&zrF$- zrida_XH-CD#Oc``7I-Zf`ZBmztJys(f>$8r5PN2=S)RQr^U6wAoQYAluQOh-d7p7w zdOq^54`cy*w|OjxoXLpj)69!2e{)GcGjA4QSYOu*-YkzCK~@p&MekoD7Wvmhmm_y8 zrg>^q6<12Y1W&A9k-OT*;X?!J|=x7WqeAh_SBQ??ognk)B(;VpQt#*$kfD!1j zR&wwhW2{ak^^U#p<2t;C_XsO%0?247+!HABz5qP_k+&$gvig%Xt=ZO3KU@b{IbfmU zu*i2(pP08J*XlwnTjX|BIuS)58nqWNev5q5(0z(|p3XJH#g&&Ud}D#D9KYky1M$M@ z@yets=uMcX9GAQgXIbRJp;={Q>rC_~uSWD_Ocwd-AohVl??QIXB<#Z`$z_4}#A+}5 z^73rPE_!>qAM&tm*@0!%_hd6gxBgx9aDkkazlQxR`{SDeVk>szR(j|2LH6Z`POv{- zj9eMav{_+`{5xb}+U8nu{^edq&rDnPxAryr5$)H4C>HH&$^tg;%)s5%7uFo?O=h)` zvyj%I2a@_G{8Keb7qh_b_r4Z67R^W8q{E@~X0W*KZ>BHD?slk&)f{ zN;OlIa=xb8QeLytB0o9^J6&)A@#w_(w_P2Pi{9G6VP|TOxCYeFyNBy+)hZ_!(_J3l zeR$oYis1nEJWJ)@82%^n#>a1=9&a8Ly>kN(V4bzTJ`HXo4S3BWuSfmb`Pn?!Uf-aO z?|ZnK3t743N{nH#MgGo7D!qKrBF{ti;;M+2P1B*I=D=%0G2y>&!53G|&FlZSzH+R8 zM(}(YzmS97eTt72Ek56ZR=}7Rf-`{SS%?ah?dJlk{`a-}pmSB5Kq_aZ%)OnH)G`;G-fxH^km*4hi1x1lkdk zodxBKtaFL-3d;Yi>!O$8`y!y=k?-QquvMh*_q*wTKlwcX)`x(v$eKJQpsS#0bH^ZG zqI;7&%W}+N&2zfhnH+BQayyLjebAP0d(^wVM)}O~)ER+(Mp~BhIV5B{(2$6&iO9|K zhk?IXwyDKw?9{oEk%P??VrS?>kPatGNbmqNL4vKtgQ2ooJ**nly&3UdD-b0ib;gy5 zGlyv1j=&!?l&*-k1Ty0}t^#>*V7tpW zU|!-`)f?I7G@O0HnR2$pIg($lMZNPB;z6*W3+O{0^RTvG;PM(>CGd6IQdxv7JCw)P z8-@*Nyw>|yoO#CnAE9?IG0wkSO12zyw$0bS+XL?mf}`?+j0Ky$XV8m}vwEvn^|GmF zF6no@;H*Xe(%$X$wh!?0xq;NN&-9m^B3PLTs&6ZDK5umutwC0KX=iy+b<(Fgo!Q0i z+OuG;I$P{M#P?nkkNzYs9mSpMKSW;fE}6t+(kxLOsptO)JAt2Lo`hJc%wkq(p3G$# zWw3+89MliI9jUFB*37KVD5o~ej**_K-;Xm)nKz1D8j=8dv*;0IUYZFXocb{5%_hHO z@A+y_Q{XWuHqUJpvhX2-O4;$mIzTjk$Z=nC3$CO!8xmTE+Q=mT!zsCwS zhHTp(BLcG48Cm!<7%9sZR=+Q`8<41^-LdZflJ+LxQB`;U_+4hpLNdt!h6&4LZUP|# zI3z(qP$qMeAqyBFYBgw`C6PNbL8ACp+S)P@w1`-NXbXw90kn1Lt1Qx**V3?Qt=n&g zkd_3O0M(JC+8Z)40|Dmu`QDkJ_U-#V|NrxQ<~eiDJ@@SQEZ_58U0EIcK&)u#sNBD`024b^KS@Ru>`&l+$j$N`yZ=ct&EAY=e45@0#>IUor+Y|S?jgK`& z3mcD}Pi6^L*n1Uc7=1gyYb+#pBvxxi|(q$B17d z9Vc|3`yKGvbf5awNEbA(p2k>Xjaaky#cx&5y-Bt@W?OMjg_R)4hFtYXnCULeJMJ-* z&T&bdKG3?BOCG0yv!E}y6(xXz4^1+Z53}`ZH*Tt=TUeFDH|w6|aI2F>INa)PysF;G z?O>k%_&g8n)z4#O%~g@ld^6rw;ym?9HMn6Xg-%QPjv8ld_@S%uRxf)KJDnvO?7_Kx z5$ASK;>H{s{^%+=mcSVS`%7qRCq|cOcNF(AeA-R0zLb6O^1tkhf5g5>aUFyAK9z;N z9a?$xdIz{zOh*Q_Ih*ZMK-)SNeb$r-`b?Y2poH~GI711iDPj75)-*;5YZ3pnsbjGE zsgA+bPc<$&7TsZD!57oU{0;Cb^CiLawVFL3Q&Or5RD;eQvbcgjs zw9QJI5VORi$n{N0l$X*$?`p*nrPL5ostPEr*vIfk@N^;c-SkuBTE%|X6Sv0om6M%% z-BMzheLKpr8?XYF8OlPdmg9Cl*I|~=UZuF(LPdVYvMgxD zE>PsZyL`u@w*imZn7UxArnKv>N=iFL?!B_ne)Gpn{``-%rMbp826XAmgEyUeV}MIv z5PSoc#VUeK-YbqZKJ+kWxr1tX@=JEJ4z;{?m6@4ey2h8)p~}lI-La^wl2_`VBLDtM zo1O1r@{e~c%!-$}DmY6ibGA~ZCpbS*`>rdqdYKu=J{t5{y|0Gf7|^HxIW(t=>77$m zm`Je|WdWmx(paU1fX`C?5S~@_72H2ypY64edc#cKppv%}cCQO6+1@J0^hE9lkUNOH zAAX!?)RxA_Xu2{+ORqj^KZbrE`FJMYFf$K*e89H`Uvw;bK`ExlHCG!6f{)E--}oFgsU7Gm4Cs%zVhubeXDhm8Lt^IPY+{Vl$=P?G287z9V-yIJ5egjo3c!ju^B%pJUY@>~kza{^5l&M=zlokhC=GGD zqMyL3PB{EN+IbB1-onNL|A0RM-!2zUgd4&0Ld2^Ow!u$_e=~d@{BNCKRHq;NqU3t$ zWS<@do+|joy$e@l9sXkZLZ%=Z+OaE@p9}_P>vE4 z$7J3gJ&FA{z8618+Od_r7I_lriTy$l9;a9={tR z?u}&FRUHExRjgt|lH^gfxV>H)VM#+3@(zt4#|+pyJh*2s=SXpEgavG~eE4!xzNT9X zI&Ah2KZrIV*Uzj>KKC(6%pZ*j2^h5oW06jC_9fzcB260~CGd_a%vszB0*6K7jH9qR zFe>$mbLPJoMzJ@1kpo7FEiyQDhi=0+P~l0~$Idr6Q(#@>3q^k(#p@1|gsUT#56s57 z812!fjw|8V@JU7b@(0kP_!mf^Tnw`4&&QXUj;E0)zGkK`d1`MO_~PdDnbi3mYKIjP ztlW!JMoV_qK=;xZ(_;t;bs#bV={HkX4_F8|o zU$b3{HTirjOKCCW+MG$DsgChW(mzapYt$71bDg;){e8qFoFKh`JFLZ-hTDXQ{UtX1 zBqX2H0jqGo)II$H`tV8&lIoaGN3SyLXntAMjOk9a-wC$^?ru05V>U-q{y|jL8ygN^ zCCF0W&SOk|j=nd>X#O(DOHeS~r4D1}DRBog91l8hpMXa7AL4EU%q2?s0q#>Y`p2%C z+DR)Mo%}wOvsfu7sr{XNH6%v(bka3VC6fMb1+NKN)-=bw1J(nJyuTfA=-wFElx{lp zJE=P2@&uu~-6Jj_8=OVii|0C0O3zE|JQZxkW}x)0SZeNc?}yjpt$u99vZzU)QkoPp zw`W#7Vb^vtrU|{!0vgg+42K=prw7+g4(QtkGFW5#JJ-L{(FSQt+l5t`+=wniRmq{m zA}87fM2;{cV~ez=W~VR-Px9gZ!%UXZA#8QkxNi3n)bAG7?5!exCnZd;fSPA~$kmi>82A9E` z3M-S0_4X(@JxzZ%s;gcf8HBChlg`FNY*DJ^i_n*0y(1bP?E~qtfC{4VaTNC9pey5xzDCfgO zIXC~ea%vOh-1H|(qd%x#EJy~l<)Ns_X@b=`;v@L`fA!)W=?w%-%btKfyTe&@hihE) z1>#S^k?s+;ir&WKM#J>ZfIhOgm6L5=9+*!^z-={eqPe_Rxw)P|8UgUT;b2lC-91-G z@7E5ya?jnQQtr9XiIceJQuvdtLhQ)%0$g`IZN zx%l)xJ+;#DdcTgluV1UlL_ahvG#@Dz@t}Y6kXGIXxq0nk?8H;|_v?5{m93;Yex8{P zNw*H_6ZM9 zh2dQA`UjZF@<-rGEV7 zs487I!0HBu-v+lY2quF`D%PCj7|2D zPORw;CY7-f4^_5=d*0xkVs+$_XYa_M%vEJAQfV0z-KCCTZPEJI9Zag?O-9KcVIDL( z$q&G1iw(N~>xmbHr9y>#UVrQnFZ!+fj~};sOs+oV|}V?~*8FmTgK*vUmp1 z;Ju14?iTpTVsS-_bGL7^eR*&vcxD@2JDocbd&Wm^k!yK875(q>nq`iokiD-2K4f-H zOu;Qd)a^qL-RwN2rijxXue${8VoW{0GhTC~kz?oexO;qP(efU*QvZWg5@dY!D9!Bh z(i7UPw94MM-rVAJ)}g<^t&6yA2z7BO+_{ISRgFq2YTf>Dtp8B@4xi=hb#r^0rRfz^U$ZkG zr_X_rfkDLvTQl3rSk496R(5_g%;PKcdXzg*B(R3BtC3cy)m;>8s}UoC8orKiFu>=6 zBHFLvMYMf9^e`>3u9nTb0aUqD8ck@cq?c_=#|9Y_!K&$1Ex-&@yKMLJ@I^TwHY4R} z%~vOr@UB1fd3ZGT^Dt-ndzj`K%{S^Va5^?XHa1>rks1%)p(aQ$rK?psrPg%`4Y1E5 zo34!@@a6KyS2PYmGD_Pq7OUk<&ge8Z&>S__wMdz0!F6a=U)VeG%V2%G0L+L1v+eVc zxq%0F)&hk-WUlK8tL649f$SIUU!FP@-VGe=-$OmV%t&K87i`6de0&mL591@$nHuzw zz|yXe&=CKUhr+ycs)6P^$-It!qyNA+qA?mH@zSgNlV)y~H2n_MP2C3vnCcsYlNSI( zlKQEXdP#M8T3@R6Q|B$!hk$wiNGk2)>OOLU8+1C`zn-(6Qr|Qs_b&g_HVxs1`c&*) zp1b^58J?obX1gv#rP)I#qN*Z%E&eEc;>cTI%>q_LKJ=-?T~b9Bh<^nhSerVSrrYbYlAc}H_2PGE0=fr5^J~6(^ zdhjFZ_#l^id_b!fCaGrT<0h)H5Uy!U2fu*HtmP z`*_3Y{hHIvfYZhdxom+bQv@D%Pz?c(-Uezm${z~{$=}ngU{7M?DGU!hCR-&&%?#>4 z!}RhJ=g2a3@7K@U2MB^r%6q4q?he)qk#&tGnP)b2KrXRff9PG>KXEpwlFI7yq`XKV zFVE?5Hc2*UG1=`r<2;Te)L3_Otc+>&TYj@2=*%m?)%)nGDblf3v!oVVp&fdB zB%Ndf6<8;kWEG=xzU*|bqCMMP4+(9g&;@g(3U87|)fIok;f?CrnD|+i0$!CJi_WYvnKz^Sn~*k3dRub?(moxX zV^S9wrD-9vJRV=87UyDst96+hlG}GT828av0wvJ7POY?xO@B1Jl%J-wVfIN+qJGaR{{auM2NSxgBjan#(jP_!ht=O%12~h4 z-n6sL+$n=}8j>F0uJ2sK+hL=W31~nUnt;J%#iAqo7*;+eJFV*;CBSlOfM4UV#-n^^|5A>Rf6)4hSn=$Mp2^sAGJhR9Z*> zTaDArZ=r^rsNpwC4MEhfE%X7tFR(X&Qoavv-AU2~?N|_vGP~%D5^QW&R<)g0HQI}> zf!5+%SdV#mU;_4Q#&^A<8jF#k^bAJVgmj)&1Py!YRn+E0@6LJS>({Cs-!)x*Er;9a z)FpDL{)Ze7LVC)tPC^~T6G{-N(TSbJNqi`K(b|{L+C=?mFQ=5|si##nTKAqr-4`^5 zu6|X&`Iw%X**rjuqnciuxyIY;5vWmJtdF9{r9jOd!Kl;O0)`B;$)o5e~bnk z^QGF=Ctw-0v`>?(*~yqn``FUbzDLJ?bRS_E)bh*GwtgM67h1#4ck?FbPG3ZJet~De z4d*uj@e#sT4MEDw^AKrN5TwqiHP+CMUIp);B_Hb-8_9)nNeWD%j< zW6|TxvFJMt_9<7J%kz*H(1$u2$fj{z_w^&MK|>IB0hK%wi^o0JJp`yB5@wQa29#w5 z)BuckzC8$?qPgNjfEu3ejH+_!L^=ka!W_cPcBX}Wx>kWF=(fh8U$n|;6wHbKbI55$ zPU^ApMUa!_xb0jEpLp}#Z^bwBt+C-l(Erikn(L&V9?nnuD5@Jsw7END3w1FZ1K ztvp(_SgQ4ZAn`M(^jc46lu6XX#IQX>p}l`uE*Jf8!K`tC$$zQ^*!Fud7ayi)E|23 zpWfEGj5x>B!NFrhoCslRpv;{ZkE?^#(IRsn-?4FYgd_VwfHX(pY+$(x8SJ- z{{>=8eGkI#`+LY2d+Lq%kM`eqpYi@wl(@LTcpt_o!q{n?%;J^?__$@S<62xT)O!2- z4HQ!5BKczKDm+t;$&#^SdA6lH%qh5hZZ zsJ;#OqNlQ;ONA}0?mL>wUrHUU=;M=4hE>?rnbcoFPe?xK`l_@{@Ksdz9Zq7cx=TsX z$I2G>=~5%1J^g9?gH-e3Be)-)!n#SW0H>ulH)4Az)s^Zk9ra~amFnW0MC}#vU;Pxx z$Px}oSB3c)u|25FVZu4lT*eABx>zM0VC;XB4(IMS=>k;VXgbg`u9f$#G>zX*GrqD@ zY6WyXuHp&Y2u%DFu1?@LR9zXB&3M-H&8{@0*ol}iN(!Blq886dN(vQH04pkGBL+4u zk%vQy3qmK|S^n4qJ4HOR;EHP`YPKoO(+XNxNnb`2cBmh?-f zdF{zQU`H-+Y2YE+7-LO*uY2N!U}`(8QDQ$X0xqP@-q$@G{sNdCmWg1l=^g#xY=kXU z^nuzrd~jkl__Cn==%%0G3@Vrj3e(~XORYEk2e{8-JKEV+ewG#*jWjZ6$eMJ1Kiv~= zj!_C|dx?RQ7`Ij2dx0^Z{~rXozoewdihUW5QzT+nzX%s7w28A<&tVRM+Iy|;F;)EZ zZ@#7~WXJA7oW6W7H~o_Gqdj6)79Ue4macqDhLPv(W2ff?x#{3= zi7*dTM{EzISEb||y>#~dmSV#Uu-8V$9$(n!fp`qO8JkF2JZOSYGdOoA=~E_ zB8(_NUoDL=r>U2(uPt8VmE#%ve##v`kAEdQz!o)NqZ@6cu`-n_GivT(zHHJ^6^3Dx?kHU zW|8yH(jUQpKkmhR8a@uBLz&SY5aO{tdCQ?Lc5rb>EiMckoU- z^z4n4MvZv(8TPrfOQ}PDi|X16`qXK&m%M0S98?bl>R+^1Cj9b*?@0Ki!Sb?Fgc+o< zC*o`g|CWTG+L4r(oTo;~Vn9C2)fSvbyl`?aUQl-r%p zB%Yq-X(OXW50@0pHt!UEd(#?jl>D z2^4ob;7+x?CZzYCkv7@K1T$P&z{RNKi6gdbYyMZ=0=Sx1X#0Y^X)s**^~*UK&aVK? zeHrE@JmL0d#C|pUlMk|U*{Mt(qztOBcjT2G915&6pGq&it)0CgW7c43@$Kx?Nlw8r zX07g?ws!dtyFd#YF#inO!C}i5>+YF9;YE8iIAx~pl;)CJ-eFgDuVC0YUxcS*L#D#? zd00O9pBKj7Ll8Rqiuf}3QVbN5x$W$)Qo!%3XZ1G}m+rE2p)p7!AA%%_KV??(DRuv2 zTIl3>@6XZH!;ljBF}v`!+o9{v)F0--mnlO&Xxm=ZP}I|6&kL!BM(S9U`d;jdcJ}4f zvMfA5kfj;t04~Yd`6J!U(CMk?Zp!AKi7sg5BaNO3qT$Ec9EIkKVjnAl+xh0#?dL-o z*!fgLmr*8%GGldwHH+Z9hmKb1Kfj(k20a*R!`pEo)rHbJzhlgWVmtd1Q7FF;iuXMs zrZo*3i1ew~IrK%rSVR3airb>Qt>5&WOY}V)8iW4P z?P{0sr?9w6p*YmrR{^gIIF~D^#hkYTQX*)-7MKTuu7QC7MEz6F=z$+OyqtXO{Rux{ zHv+!1%TBJvV^nT8bO)zhTvwn9A*FmjayBlC$E6lrRN|amBy9IZ4|2e`AA^PWW_tlF zKMN9D_?dS_!#2CqXSuE3*XzwM#pvagHC_vs;o0+O;`vyk$Hx!g9_~I9^7zjBpiw;J z_MN-`jF&KPkRWqM57~Xq_K|M8k1@3xTXb9PXS|wF*yr*6+HO?Ro)4o|>xzbYU)AmP zpfpV{os_%;tL1I9!ji1nIZ^>eTNyILpp>$A-?`Z*sUHB|$Y> zCY{daEk{rFFfO#8Et(1{g8eqPk1ce&A&3 zE`)%a@dq_ix>hut@w$Ca+~)F~^(L22NM6>M2%%lu65-B7yUzLC=)W3$GC61jb}XuQ zv$ud|BDq)H<^K!R>TFl#Dwsn0q8?w-zIdxCCAHhL3Vm3GJ(}kt8|<9LT~83Lu0yzg zV|1|z9AI2l$GKG-qf1Q0aS8tHL!$qvFI&1x%BkY}*7Kdl#)G08c1M*~9J}A{JCZ0_ zknZu1llAIcd?U-gZ@14349O_=*0bvWPwk#ntt!>YI5&E1*a;~yrsHyqX99k4Rkn)l z0$sZUFdFnTHDPw~mTBEbeEjs>Zu=^nM7(hc$=T5DC+pS;)%JQ$()(?NrpV!?_NYD; zItx7Y{WB8TT$|2Ms``TL7*|#S6eer}dumLu^@@>lkG3USX$#~sjDma5{W)mM9v^SX z?xu0wgIRQ1HTmQo-?@EejLa@CMinFT_mQ>#W6NOiIj9oOgySt_Jl}2MX#1`X2QPE^ zS-_zyIJJMfD-i~52{7dG{*U&Y_-N@zb0Lm7vaa8Xbc7k+)BcO90A_bn{WDd^;Q0HV zsS@Fu;EvtLuTE)yx{6;Ztft+>EOEm23aWt{TG43mb3@@C))!>DezR z-=F3IgApe`AR0W)ei7r|>{(xR4vy&2$IoXJJ@I4JybmAA2ChxxNqeM7y!8=;1jt~{ zd(ke3)>d&t?jNkJ;D)~c@X_enqCEqU3WF5M(da`;_-M4T=#?LTtlQyHnAXQKMU z@cCz=8RQENl=LV2KR~gZQOpfJTA!VYbdeu{vUd>}tr^AJqOV$2^R`6~IOA>D7TxPC z!s)TkN$n^@?nkfXexm-4^49&LYy;M82C!xeih0{n{IkN*=-guL5k*I%#l>+v*@Cf) zs~+E9A_pEo3f#iW$FjDPh}BghC*xjTKK5v|w3vExG-^XhBXz88BH&q>t-lqI7EuM$)V&uECnA#ISd+tO}_9{I&c&#VdJ-Se ze}uT9Hg_(#6u}R7S-+1pnd+m5Xdg0TjNf<1hA%^l@E}&P$uS-=QN+B7m_8+D5@IeR zWGLa}=0((&@I?g!|lm(Xjo#ufyC* zoW3hzl>)eOJ;&951?t)oYj(S8`an<(H(Hb6I0GrvKZj?3r|!l@@wzi`Lqb024Myo! z*x(h`*T-x6$F-7Cm+;yD_x$($zvVyjKjc^V=&G4~UyzwD1yzy}iILW>-pzZW!W?mmMh(_(K5GPbQ8DVMhr_n8k zCjswMmsWCN)7;yl>aD5$YArK$`X$YU6Z=_M+o>zk-N4>xngv?~b8Iue47DaPrW6;T zucthK>UsWfFy0d0GARUOAYpzc8)St46E=w1+Djo;csHCj!j1eg%CsHzXGW9T-W<7f zp|0SGjyE75a&+Jwt~HDAU(Uaum~Y_oP-x!e2TBgi2lZ?5Qg&uOuv_>#OKFaPy$ zlCs#!nJxlyTW7 zbLuQnC8z3IPK~fxd!F*i`FuRzS;#FuiqkvIG3QM-hwvMoRm2rDIrQbe-}gV0UfuVM z)_Up%?=;B-C~A6E{@uT_Z(h4C${w!n zX7`)8gS?5`pSX%_H_}W57l5?FU#rn~v6H}qc$I;^8;@(xsvV2s(1HyxG(p)mK-qN0 zYEKf3_>8u+OQmr2`7ky+6S0}vTN1IiLW4uCpOfcurh<7PV*;9YC!lklPId*ZABErl z9aPBXxdBrst^5{TmptR$KM7yMU9>vOnCIiem+r5O%8{;U(e2vfi zTWJ1jAW?`r6wUl5AGN2+-3psN%zZCvle#>3!_-iVC#a0DQWhIdy+lyXWN|!Jg1(mn zgtQ(I663#uHZ%EWg97?Suw@)}qZRjU2=B_U4XOSSazp}m^dwfG&eDBt9mU(B5njgn zb1UlzaNBZFZn6FhVv6T@c)e=_Y>OUo9mT&M)WRjuN5#Wx_HCy|SLLXsg?)b-?WZF| zp>O8sU5Pn*2XN*8^Bkplw_TIpW%Qd8cScZ^0dKqOM`z)`&b>H)2Mb$0$uXxt!R-MZ z4tnV0Vmk=ZCob5xperP~s`P7ogR)uOnvm}dA?EJBk4jA~^eJU09QOnoRJ3)L2 z-@tz5m~XQ%%i=r%Grq)?POys@w;3%CvOZ7Fqt z)!rlFUYE95dV4U%p^h|`g`!zor;Nc`z1yPJrDc6d^<{m!ys|!R z*>j+m<;gMiJ+39*dt4uT^CG?Vl=EX(=)CrOH;W&;p7Sm79gF7oKIfi%fSn}-4^HN0 zn!B~LU|%qg&HKdne1ED+Blk{i5I=GA#XtA`#dp65s&VN@?uq`~NUSawwu2{nCU~0e zB$&KuDbCm?Ro+?Pw1GZA6*HUT$wgd@p!Tdk`0=4>ER>4{3e$osw+{T)4draYo>^ zGi-Oc&iHCUTVw4x#82qz_B|cFfdP+83HX7Q5|`i8;Hyc-Nuj25%MQ+0cP;jOP$WT{ zqwn5=Z+krYJa)BR(9BBQCrp%)*WTlQ5IhE6NDkFvcUK{=%G~1#MxVFR z$Q*`Gr}!>Y0Frm4PkTT38(u*Ux>->U4mmjFIEj=75w*dp1Jy@+BIS4|CTsOw=b&J;yKo**s^7;7wHaL&$SjSf0zS#3Ln@k`-;}lod<%qk+lpL7XL}kuKlm4z zDZ%1g+$VHn9FIkJF|brq_gJ(oFt<4UgY@I0&LUYIZpS=_0M3fXuR_;`=@?EKq=#!` zFay?CL3%F>^fIRAm@@@ol8Ft7jL_tK&l`VY@FY5`{I!!exHkI@rL~hecX9*y8{F^u z*_#ZdyaKh5#0(&uHHt={M#iR7C@Hksh{{29GoDPm`KHKCjv?6^?4DU- zYLy=Ig6Ji2>O(w_8hMCLL9wHGp^2!2K^?>oIic#tgT{sli|sP70d?68yd0<@-UIN< zMFK^Flb5riYKKSa;Zrfv=}U0qRAR?OpN8UV~i5h z59&YaBiuCV#J5;XDC#z|10S&V0 z)^dMkAD0|DpHe~c)l-8{NQ?WDQ`5ZtuHWt}@6+-EaIZ-`FTMjFC?4T7p6;hqgWtsl ztE5*5BK}!9cunHzims*cy9UB5|D}x45|a6k!{6kImzvCf7N$1L0F3{Hr1PG0IZ)HB zUU0UkL($^HzgMA^O~2hYSESl)eL99@P~QQ+M=I)O;J|AQ4o{EkAlkLZwLeU0sK!*j z2>hNN*Gu8Q?Q@_{@B0{u${@Z^;*v=PSB;8ato*-^@(@O?8%5Ez6(u#fo(wayo(R5& zTt#A<-zZjj3&mUgg;?uNcDw7R3hw#iXjXur-Eod7!235#MfS0fS*2*7YAuF!K?t!(GF5=3Rw zljH#IEropR?mCmSOX46oWR66a6UVDTESb53yNCxjL>! zdt-54J!u~&(K?~D#L>f;l=Rr=Y@}Bo`y8EL)T8ShkAsr`E3)iqf-GP`QIIurQ{%RA zN=$6Qy_Ysbr-c|ZdI`ALUB~mg=gIb-j!~)aJSos^=@wI9|RrbM2#$o`~a>&-&P#&bt2^ra7SFJ#{$qOrOAg`kfZ@ zSMwy^(G|@)OrysZEcusKi-}g>6f|FJ^=?mrcbW&dYme8PBin$z3Antjw@}7z|F>KJ z>;I+onrp4!qO|@QNTQkjd}~Id+0Pc5J-e~b?)Ln<*3-Rx5O(v3Ms`}O$DQZb+B*DC z&@uxr2LNNje=k6i12(yMJ!7);{-UY{l#EnP1?VXr(iNnHYNb0kUDs2P$9RtasLKQS z|Lzd!C)m)dnhcfQ8SUxPQD z%m2E2bJ*i`ENR;3Urp=Qzmk*BU)DJMt6Q}Thsu%e>}4wJ>+TJZ$thQKI^C5zrEi2o@-_COo<0oNkY-NAwzWn*< z8M$D|E5IM6@;ZwhIuN-6vR#ROm+wyKv~)S79&uGNX+1(a$+KUm4NP?mr@IhSn~N)XJ_K zdK{^;6RBSNoGtK63nRaHm5&6tGv3Yib&;*F^59|4fmGJI$mUlUeSP>rm(OV1-@ks! zy2yrCcl-E=Z8o)JUq4s9F7iX9ZgijV?eAw-uZuj0r(V(QKhV#uTNt_RRqy9<_`p!1 zJNSgsbCo4KsIz2;Dy1r^0_$xd2fPe_WN=f4u_L9N5mL%`jWBGkd@`!3S|J@9WJR`U z1w%4!xVn6=)^gFgm1=f|8nfcs=1Y|sF^o(-hIk;Lsga& z4lVuh2xIy%OlM6xPv_QXEyqIdg~_Vn^eP@wY$cK2Wjme3r%YjI$xf$G(FTY@kO9hx zPdMKQyQC>q>~zR!hcdxA@I3Y!ns3Hx;uZw&8fNBVSnqsOax3F?pnpSp8%}Qf2+g@c z>?d2o9BkWX%FMc6)XSxRoM@BfTANnb@ z_ge;TRA(tYlZ+_!eS^?>H(#ljPT2sBBK@t2^rP?XMgRVNy1+Z9K?Z#pe+y&=+RS#) zfjQhhj>11WcpTh0#?{NUH+HE3M^CD%T}|)oE=UQ;fpud$I9P?;;Hs54%*CVRV|H-v zTIAf6X@}2&@17e96rM=gpv3a-qwZ?C*S1Ve{iVmg{z1|iY_{<-QZZUyd zVhZ?+GIbs7RO0s>-*NELqeD#Is-z=$+s)~_epQ~+5JY(2P#_&C3G>Ju#xpE$ttYG4 zY@PvQgdR+7(ai4JUe#P}Z1~r`alUfTn2#A`xkMb&r`5KB&FgMHOFJSd0kh1Mek^AW<0h9 zvCaz9>O=`#Joa9VuB*1f*`UM{XFbWMtfn8$KDJ0U{*z2 zyccpHQNj)`wQG9$nbkpz$-V(*wOMA|^c9oE!-yfB|M-@9cVPskR5%lPN84oNlDdo) zC!+x--h)7*{(IS^uCa*Un22W*@mYvZL;NPhZvh82qx22)ib?9yA%06DKHj!1CUzDB zp05^`8Q2@(dzE>O`x`rRb?7;qu5?<``IrPr4~z6`h$!c*H%sGrw(FcfdtZ>72&vA_ z_O9qAE1Vwlsfd+(OMuS;{J!3>i=cSVTguxl)?Gd!60Kq9Ax?NJy2ZMU8`pJi^#P1W zMrcDt2&v#0oFE=X?zQp2mVk@iV8W@#;QMZ}WlxHV?=Uy5en`RNgsc(Jj~8&u6{J?iHgG(^n38(gsdwQ#Y*FL`>hjlp?SGYz zYq@SR`?143*lb@I@xMBM^-Zf$-Wr1LZd!TMYO~_cUpasEDZnc$aEAUcihX+?*4gTv zfN_>1rco)gylkkuD@?mWG~FrqZ_8?D>fFg<7-95_l_RluzZMRC{3vIukzehn%J zkJLTV^2B@aSQg?AB>ygvZ-r*?BrCs=$TNgI|4{hm-qWQ$T2TZj+$b%E|*H z-R$h7xsGqwjGiZ-B(Aq{pr}}+_&8qwu>au?0D;H*9Ix?hQ0n+~(26sawaiA}91rsk zk6HW9+N!my_@~w~1?8b0$dw78UMCk-HO7WlL2p9&+Ssr+#wtCmkJ)gZLXHCcWAg-w zzU1gv{O47(q=Ds_cn>}3q3|#}6*j*Q<;m;H^5nzn+yAA-GbHhtj&=4H+#6QmAjSQ&ZCRks8_Yl)E$T?J+bc$*ms zJTBQdAnPDw9GvCU{xLyVdQ0T#ziI(y6MCRK)B<4!Ci zE{_I8;685;*>T(5u#O2Hb3GlsM`@+dPPm}EP0;#-cABL#sPi7kM(-Qy#huB9yC!g_ z{C%eYf8EXm_qaXMZy_JPVNIJ?8&NI#jc>j<%?pahy;_;miolAk^lD{R2{94oVTuVO zX08%5&8L+EN=Or7zDMs2&Ww=YZ+??%OYY_b0h}}<=*I@9x|b0SL;vy1u=y-_z#|uF zbghKuEFLo`UY}0OK#G@2f!rq~LK(KKkHt)2LwlYyxH`RzNkB=Rz?Yluqdr9sah9IY zhhY}9>q(A%LyfC{5#F$ulT-qd@BT=Hh8Y}w6`AVf@*b|xMV`$y45h9;s z6{>|NLFG^vQtL-cRfW*+(j{J!&?|Kwb5%)v9p|6mb6|e?VI))zEfW2X#0Aua)L0!T8;8Ac4Kr;!0y|v(u;MjuU5F^nkB1lQzTB2OLYf0KJ5>R}? zkuUG16#u+JDTq3!QzYkzl0$NHzyY|r!jxh&pb#Ym%1|WgPzL!_2Bd3{f>mniQOf9X z^?0ca@&Q@64k#(|M^m5-B?ZbL|C=%vj<%HewyC8kgM82#>$d?DydBWm6qfgaN}jvf zw#6p&sd(Dcj3Va-A1FiLdpay?-4f?1GnMwLrfXrZb0MCG!SO`4dZza3lsH&NwS0*< znoSdXQ>UjRU5S;QPd9VM(i_rEmxH9?gyYR2nr+Fwdf+<9I_f{bXC_+DgC{INHJMH5 z{Q->66lf$H8=MUNDjC58Se@fSJEe)0$u>>F3{XB`>kRp6&VClAS$j5I+wg=P-^5;9 z*BAUeoQZs&E32*9em3EM0)MT&+1>-cae1@-uULgo7dG2Z1cB=XzQX7{0Ss+m;V{mG z(LGqpunePOY@n%kjE!Y@HWPwwjtiD@#v9J8HwwZJ*PRbt2Eo=yGpaNZ&Ckf-=hBU ziT-IZa$05NG(qYgf33fa>1Be^8P-Pqj150aG)hHZ)M0Oqcef-+$+-PZ!vD4kPMmJb zgU*>Vq#Xgg2e##yN6y{eol=3A10GJoa6OJTW`{OaLHjKBARf{UoQ1jYMc~m&Xf>(s zyxS(f5ztwtf)mjoKh2zBo@Mfw({S{Aj(G&RHK89?{kSroKb1ch{vqPBLJuWMwDKm% z`yLEvPmvDSqX9ks4+Ovy(`&Vigs6_jAUJJoZ}v7IUwY`iM83m(04a9{R7$?z1ZRcqS16UNO$iFC7qYj-TMNiyZ5)@a-`c8x_LC6h;-G9lytXK zy6SC|uDUsVbvWm0emgLF;AxsrH22W_a<^2Y-%40cQ20$&?WPhL#x{cw+F7IOso54!ly8~YPhSz z(JNWKSBEpMjO(Q_eJeP*JW~lpLRW`1SN;&D*nWiner2B${#_8Z3$HNwuLRu^1MWl8 zVb=Pcmd$((QcKQO*nf!q6Si{{T;(j_9+^_cHkC`XHprNa3eEOHkd@1#hk^MuK{HjH`z#%&e{c;C?KlKq zHaShgo|WfT;y!^>=pifJ-fFd7UB|rH>Jz}hGC0S0)cxOc9FGR9=fc*5;N1je@^Cb; zl$Fz?X3+UEr3~#j;3zVs<={nIE&tRGD4qo!QK9L!#oF3UfDZPoM19P;m8Sc~^impr zD{9N2R0X(2JsIO$nf#A}yoqSGwC~vtG_59mDh~m^`1Fcd$^{SDo|Oir7eu@L+Z8r{ ze9+^I+iA+eo<{Nu=EzoiEikiYxd6AP_zq0`Tt?*B)j>Nbjb^0}JO*OuJbP#8JpT`z zSi$qsFJ$KWHD^kwN)y6Y;lDw-8qgUT`nw3#u%aaAeK z%&M^oD#(euQ>0ECVZbx0E`)2Or4lGM={2~CE@ZMgQY()QZZhaPbd~#|RZCSVipzno zXYFj^a)#D-w}Nk1Xbo7SM0X&%&ANlT24tS@tgZXgE%UnzEAkx|p?lFW>_UCI_m;d zfL${?G^L78MQ9_eThYMdjzruqCdVbfRZ9;{D8Dp3<4TrNuQfJ4QEwHn3vWYLTC+Wk zpaI|lKM#$Gm*P&8;vr2Fr^7lWmPwS&tl^ru)?gS~bPu)lV4pI@_pUq8-*-O3a}H~> z9wZyiq(f42OpupeVa+`33{~o((^<%zzC_`>Pf4CSZMtRV^|OoS+&FLk%?nE$YL;dk5In6Fc!1vBpGf zR>FU}BiWQfa}f^b^CAuo677praB4UWoEA<8mjnlk5ISgb(7_rEd{)OCe0=Za;mWAw zxIEk+y*&KAsJgY4;ZA}|IDGSEV0#mKj~nRhKz>*p6CT?O%XiJ}z*;&>rz|~jc{mLm zPfUJQ=+f|$Bi5>XhgZ?P0}TeW-vDk-_;`*D2XK$!w`v^fX3V!2*{4;_mxr%LnYqAr zc+3vKv8FQnFYE;&e%1}$D)1m;Pb|dzH50FwvBF3i779&8K_j#ue>BSfHR?0Mk?7LG z(WgtpOGie4iPVuLr8)Ty4dxbKpzBr@D5cIusW~a7Y~i27sH5Z}{oCIgrSV~H6YumC z2lX?}vDHC+oqu9vcnA1^ZwyUc(hiC#aQ(m)#CZ%DPr}FZ90zHz=YpjNP;)VJz5Zj3 zJca9=KrNe4UQ~KG+UPdj&;t8B|Gf_x&|YvXx>V^e@u`grKM?N$>@5+0^bXib6BHfB zBjEJQZ%-PC&12_~PSoRM|EBneB2!O$lkOn3U8l4>`2=Y{=JS)fRryNqt>`^;mm-IC zgnDixe(T0X_zF(^M;LwMqL}i%S$RxMHM#J#@zy(Fx0A`A8QdsjE%Au`clLNQG)Y}O z-b|d(+0X#NcNwbK9J0i*QHa0arb)s3?Vvm%R-^7psbX@d6eW$RRJ7cMGN63|Wx&F{ z&TJ&COou*`HCf?O;n;HMr0Kxf0T_kCf1og677Fj9Fkl!8zd~WaG!%Y;!hmro{A&sW z=ArOT3Ih(Ja5IGoKKftu9Wwc2PK!ybnRFqVinvR_dR3Agw9)y6)zkstx|gq-;4G1N zXiCh%h%p`1Zlw>egO|9txV*_;5wx6Y8?r8KvfmOkA;dVF>_tIlC;-1O$UvrOV_t(; zuw=$vOu;AkSAs@U`n{ui0v3Qwq7ZaNya1JnL4 zFtiozBz+?|q5T+k9g;#+(@!Ch5lF2Av2cma3<~vw< zJ)qo6m}z&5MR(oln`?_6GPWDNh3zr(7vcQ_fqT`+o7}$6d?vim{Arjme~fiYZ6Z4* zsSXD$fw1!GL>b2ib*k@+Tvh8`&noka(v7Lq9c;Rv?sUMDxizPfp`~>UINo)zWjpB& zUBzZ#6}33Uh$o-ogg4aK5nqRuBRZD=FE52(%-TNsD`dzHVlH0Rezi6E*I%lB6#Z)D zk5+#5vmafDXWb!I7nry0(yqY+mzoCQ4_FVbVEDX!m(~xcBsFZ`R4}=KTQ}PbBJU(@ z9&@m)>dC#y`OJl;z^7j@vc@oZdhx=_+nxR&Fh>8va^?Q)$@cSrT8vpBCRK-Ii>aI;V~HZD9#7Y#xQUyI2~LnoE{E3 z9hfY*iEt*kT)2GB_BFl>!OYc>#s?Vr@@3Wp3yG>W8{u23CN|@gpGuADi>o8GiHLPpLQ_0?6k>Cw9v)+QIv5O!HQIk|9R5Nso0tlZJ1<5}pdVQo?tYr5`lNjgoA4t?9(p;r9gp&XS7a!&Ah z_|H?h@Y~>1x@X~&wZ$DB>~#IXv7x5N$1SE2*=cKn1vpSJG4nFSUCt)l!+^QKlNqhu z?3Gn)8l(DrkcRf z#U35xGueSa3Gnccj#IE2L(k4mpW4c7k$z?4Fm4%@xzLLzfD?xmGNe~*6T}SXM(Hzf z>TQ!Ifb(J(LLn(d>~wC$I%O=-WNW@FiI=DFl$*!ionxi*MbQa@eVcT;>~vI3q#a|c zr85 zV0WroUYuH+^}F?{BPo~fzH|FUorx84Dhe8*!T0L2XsT6T_!i2iJ68#w447SjSNb>d z&;_^kl!0BM2=`xb zZ@~RNoL;^sobG@|cdV^C%$3*aZh`qS{dTR6<`B&v+T(NZJOM5PE(wl>yLg*c=SA8L zkEaTG6&y6L)rj)J_wW253sxp_R!P5-$hsG_4-Zf6<21i=ej<_oiE}dfoMEcbsTon1 zPN?KyKfBJq%XYUn)y@R(axE6_5o_*xQ=Aftf5S1SYm+cf@eNIdu*EevrlKTjR!%~K zjpAvNuBCA*X;exY`4IJozO}L*@V%or8!iqnxSDEyNW1Y;}i0^-lx=f(+-p)Gdgw#mQV`afhV8M~oiqW9B~k zhG5U$i#pcedW_}^zWmN&UN~2G<)@V!9btG|fyZLI4)u0K;42;82pKahgL$zImuRqh zbf!ISI^#l88@`RBDB~EFgYlxaz7|PwrB^%JlIMe_0c*R1pXf}B-Be2A0t@^-h2S3q zJY6qiNtmDFrHjgqN4a&z5jcsc4R|+&q`LMuQ1+9!eluJT)kk&NUE@4rYiqN~H?tYL zn>`9Y?QGU_=A?t{_Dpx0c;ArC%O$}*B!tv6-)=T z?L8ip3rY(#nmlw`D>_TWp>PjMe;l-Ue(3%I$?MTbnwMiF<1rUAVH1bxsK!{-W9|;& zqHmJM>3w>}H`ulUFc#J_jUUbBnL!QaG6VLCXuMT4-lo_r{`n;3qcNpZE+m~Aj7sv* zWS3gVbWfg-)Qh*fC*7S_eYLG@;-VYQK)((4vOkU$X0UM}{Zq zLn`qF$Ea>#71ZBqFZHbz7p=e7agD4K zFZx?Hc&qEy1?km!J_fb3uvW9&Eld9e&P%%I6Hp)bc*tHZixHLKO^!m7v{?vE)|xQR zrUf+8MvQc9&CZ98uf;naE4aZoe&P5MYc)7|nvdXXr(#HBiw8}2mt)jNQE3&@S+TY) z-Z3uVL>!IA^0JVWbyDZi6p+X1g|Ob1xX@Z%h`jTL;yvIUBA;{UR`0o>&O`R!{uZP) zKGGNJBlVB^sq`CjN_|D1xACPivtjej+q#o~zIn{Y%PS$@nwNCbL)&+;!sW1~#$a4W zzOmf-ODntiDIYJb1Z^qb5+Dg9iu({W%EB9GAM3jI9_ZC~=dWP-3n~pO2p2Xy&ff7m z*}Ww6p>a-1pD{7!2@baOkbmjTa>p(hl72B1l4^!5kkV3rJP7a27RV>0@s-P0=ejVZV!0VY2W3Upp!Rs{Ek9#z5aRq-U z;OytI#*bqR{ym($`JdqAO@NdCeach+l(Gvc35PGr94-9Z4=EGwook!YzUP`aP#fyR z>CTE;?NZrX3ixl}cwb^7xNt&32XA)Z1Z|6Qx7qysZEk zyOuEQZH(fffMZ8u7F}>~`~Y_iQZ&Gr)ZG=HJa`JT79J_AaR1>No6P2LehwD*7C2vu+m&~TKrmh$t#Il?Q7FBF1Zq4%DdAk0xlW4wz-AvOYC zq2Yd9V?E0m-qK(n=~ujc3GNEPA8QZ;aTfNzdE-+z?pl3>fEbA$Y^y?J6u1r`gGaaZj}qZ${F~0&1Snf zmmDM-(_@H#8khZQ*YxI)iit{@9l);AXSn(=cdip#`z{*>w4N=^8eyV)_Q$`y%&fb9 zxp*Cg{&Z-{x)(3;>yG37fy+}A9Ly;GI>MMIyNu$(L6dk!c3V_J-p_1hYy)F~m%;{B zgSO!q{3D(2QRgq6!0m)Ri0+4>`#xPz_ij`ZG<7ICr^*H_O&j=`Pc_XDI9ZW!_HUO` z*1ZR7jqIUB=aBTgT>|G50oal3xr^$!D&h}qYRUHQ=DPR6^)gORO{&O!0yYv?S z?Gn2Vz8mldoCDH-M4KyGK$31ugV`OGx}!-|bFU_)Y!t@9dqz@~{c2I+IN-D+JOlP% zr4W(Eg|a=2d6p~C!sacNr+c4)w@gFZ4D{#D0lxPgKBI<#O^%4PUH(EgoMr04W7&Pg z4QNcvb&+0yFMO@AcOeJK^o)7fDitN))f4%&^VKqWwr2J zb6E+b*(87XMO z`RxwORZ;^pS5n6?^SOxc4$M(v?87m3p0Mb9;4_j+2R~g&AEO7f25m|xXaP!Sj218* zrI-R!V|4huVLE(1Ve?kR+!nA<9>UTo53qE?ytB@ZS4sl?Kq)CkKQJQwC_nxB0%Kz3 znTN}>5XP@RYlzYHhihT-i8?^I^=#NIfbD>?Y~hXC-}mtOtmTM10W<8y?y>LfkBP?5 zfTjX@ECtuk1dT<7ONR?EE|Z4KjyNOkMYub`OTTPL{~IVS^rc5G2kC21hrg+}nXJc3 zAQP$B0D-5w^<0hiiKjh9HMjaX*z94fw!k9vaVubwcc7uau*_BQPEUOPU2lF+_JNx$ zUC%>0C(^knU12<=OSzvjImLc(AIsy*3L%|Xi4-tMVqF~|D3md}%Ije54iLiHk3dVc zul0l04|%toyWNm+%1Q7)3cD1w6(5xGxq4uQDlvYPAqA-``YCnA)!-C-YfsAztO6q4 z(i8Y)mL8|LrLFXgHgJ}c^o}Q$<+mt3zWi8l?VFA#Ti{)d52t2*-E!xNdnep5JeH}- zHGlrQ-cyi1jdBJ*;;-#>jQglJ-BngtiSy~|vu?6do25F?UpphA?4@8~A1p=JBnhW60P=Jt@gUY0%xJyOEe(@yY*(8DE6eKwxn z2|ZK-AGmR)cs>zYgXj9VWq584Rh85VPCPY-m_1yh9%+(fwJiPL|e( z#$ly$^>)P6gqX&3fot63u!{$3H{$oz+NEfZEbR|1QEH*OUk;U)@Jih;g%&CGQQg}^ z3(+>J`MHp*gleO@pAMDaxgO91qpMrZHj=H~x{A8h4DRZB^{%=~C8Vsb(nv97jHy64 zAeVX}9b|wMk%2V<*GV#%z9dXyF3~6n{$TGgm0>NGm6g5Ot7Dl)@MTOE_p4>-QTnMK zZCzAa5}q5AkPh>EY$$Pwn=E1!Bb2`O55hD8*$BS}@YS6NrIWNX%r%xG?B0Fj@HQQx zrMS0-X?&I;d|7i;nvBq82tAC@`gA8km)A$741_un`bsz*X^nyw9)mUHF?bQ|h`jx< z$TBxWezD-JUiT!f*KzrAF<-*BDX!Q$dLBGP9rU~_VGjp7y^;F1e9|AwBMxC4pw+t2 zvNn1rC~Z(W6&hJj1hLY39Du#Ze=Os@e<|a~|4>Hw-;_Z#!snamr2e8OwVLytYa8n$ z%Q&|LDwXkjC884CZLB5MveP|!J{zr>9b&S{ra~0FpwV5xbf_NC`{;vX#e={RCwQ1! zivyDuu#;ekc1-|O;gqGRLrtF12cf5u(&V;0 zT!>xJ@_^AYx`Fn?bd0DrqVpv?F9a z{NQ!PIX?~>0hfejolocdQ!o!BuN%i2Y5vY)A7akHZ!o=QGxn)?F2ZGWnVhB)8!Xmt zYyN>pWl#5vvUB$Rj$G*U#@P=$vIA;x+auDuvZns`*Hp%c)Frb8+WPE-i1aq(z;yMe zuBl<|<~S^gow}w${F|V$K~^7m-jG@7v{wrJL0RgL6!^$5?I`evGd)z(dw^LPlVUdl z?ODHmJ#!ZrtYRyZwY@pbtR*`0L6cxy$m|>|-iXN3;NS$8_Hcnk1WOa9B%zlXRdK?#MM4rxT#OVVa32pDJ z?7UWsH{P=hoDZU&7sF!T&b-A{bK!evart6cA5iXVl>4ehNVO)g$hEwRcm&JKmsgo% zcj#x}eN}AtPs7fBXDIfZj_2D$7RXyOplj8xR|jI@SqRTj!fO@Tu#xb|2%iut_b{g0 z$q$*q&^{A&jwQ*0@&<>=PhR2X`1A04JTT2g6rI}SbcLd$+#y4R`>MBsljj2tzRpZf zDo?6{t|8smDECz_2PZ0VwWe5{az`BQtHz_;ae=W^t_VxJRBr5!{?$4@3>spk4(DX( zPNgEJ0Xf%SU;k@Z^haK1IInU?UdC}OsPG<1{t9cFc!k85yeXFj{u=Q+H=7GROABFL zADq?^{%AMm$c{N89#)}eGrY_3h^Np_WgaP<7a%QfG8~&)=4U{O@%Gn4T zD8GXj4d{?oZ7gHV`(*I{%J#uO%|3ZKu4Tr~9-aBkP&)k2s{=Xt=a~Zc;yXDVCM!4JcTIBdg{%wTzf*zm z4a0D}L4ok^hL>bd;BE!Nr!`FwzEOej$DO4U_isTbQaF?z?FFBA9^QVa{C*j*$DnGg zKn1E!Ax>Miuo$P#s;JvtAPh;(Q5L6hRGJ}|ptVEN!Ke-Xilb64U_p2cTVfO}6bK({TY>Ov3kf4Q8cdFrGMpZAao`)KBK^KVf|4HVSV_+m5^VoM zP^teJrsLzk>vb=Gt=Bz=z2&>>`@3tq8vAxG?cKv`e3DZK9W>G{R{5jS+YxxPy|zMc za&n#zAr}DH06bi1eOksNB%p-6J{*#MGfmTQ$SpV1>=_P8xtV7Ba7g0KG7R8saETz81c)=rY-41x> zdF4!A8jyka=PYEz^g-gYg{9`ONtj{g59YeZiWdg5T>L==S6nhY7fWfD1|>=6EZ~0} z)bHdQ3>KWbDPbvp2vShKaah4?23R50%+~Wbzuz5T5Ag>FrMyt0h20Ixc!PGQX6NZ1 zRT0VO=zOD{-@pQPcyQFPN8C622i6aK#E2Kdti|e1!Wy#KW$CwVNjTY<4x-1=-t-RU z@*A2@;9$+r70UG|b~pF$dpH~N`^}Ko&GI)xia~)%Y~JGL{!X_HNmcv9=}J1|!?z%v z@p`QnlB^HMzIX8GkCP(q_1aQrxw8smPWLs+`>K*P?Dg8k+;(oa#k5&oW$>14b1>ptwMnRj zZ5Wc)L^zWj`;Qp8PuYi7MHmz9N9QApQRk4f67O_B8)?I+9FW(z56BPI9+17>1M;Kw zjCiP)ZFHhHLsBtPI&uFX0_?%kRHn03RIs-&kUBQgo+ucHJsH zjnrIzJHI>2R2j@s(lNv7lsnSluB2n;s%eN1@&+9uE_ei;1jdOLLv-=YnB(>(+`U~hs9 ziXw|ba0I>^-Nc0K7d!MEMzHMF{@O>_oG;zCfd*moGda@(=Cbm#{go|{zE^{KkqTNg zX34z2Y!mqUjD;~B3q8ajc2R{}*U{w0{10*nTf+=V=LRj6G;?IRjWe;PA?ee>5xbBx z-)2mvvOv%bnBq@C&TKAY&F_OMN$rV9^M}q=7Fb{G;O9qS6NAl9n~U$bBP;e@7OR9# za7jLl>=K$D8YAdl(U9aI$(FcZ^Wo`wk;8G_?oi|hl~Bff*wOS*#%FsxNt!{$qCTa4 z#0)UALqsQ~eMFM1(WZy?;7l6)V_uk91l&COlmonqDWlIGbJ&Gr9phw`;XXhr zN29Mhj(5b#Fp)juq(l5{M8-htf>l1im)l_z=2Q5#R6**UgIzsIa-UG}3!EQM(zy@* zbS?U7gWRcQ0i9T3=}aUnRYXiKww_KvqJM%*i~BH_mO>QRo9<2h@%frP*gxP6=$w0u zEA9j{iD=Ml!KAX++-~=1CwR5rAC#sICb|+89<5H{(T)QDt`^)ujdLna4FS^gA7O4e z{v)>@=bfGT40E8g58n~dh>17#gP(%u3O=gJ$(XpC=?A4LSih8(buFtJ>6_X`{mt+! zt4N3TI#I+vbyFOr(+tOvB}#=({GAjO&%@r!NNYy&a5wXKi(_SI;Zgd&sw8+!RlCyN zYS*V8x{sv)Bg%Of7oF37rA|tp4BcrBIQPpdkQV8`ZPQ44N=td@j8u7;$?$KcgA~H! zx&cie!Jq>UspXwscKWMfH9Ju}Jh;#CNFT$iKV)y$-U@r}iu6Aqs96cFXiH}gb31Yr zt}L{}cI%+M%dNT^oN)~Fy3LSUD29iIpr3b2kb?oNNcGwndmYvS3+m^OPsYNgLqaqL z2iaQFSG6@~tbyL(B<>(TOoPZlKVmxq?8<;1N|3?n9&T}1dO6G}evq{5)PW9B%O?R68dbRHdiUDXJ&8;8fTWUVgr(;}!C;JcU`0Pp!hN=+DWMg zQyZ|GROBOXCvuFN$y+iTAUBRad=p5-xa?b^s-G%w2T}J7l)n^sxFswt4+DM`?fOp? znHt8+*uirq4!{yGdb1O~VJ5MYW4%G{tl`$*AC~?jY_Mn#YInvrB%;plwQ(=%UO3%j zm_aQaH80lMSxReDK@Ay@e6Z#w-)xE2^v^9BGu)CO?M+zIkU=pWdTaJqd0AtVYF&PoM0bP2U3|%0PrOej zq(R>pa-g1g;pyfaSifLjf6=1?w@>@t=JQ&{1Q<)*&|FN?;0$zvKJ%m%)4j1WcF(99 zejmPnhC7{Pk~DG9^7D)~l4g-4%SlKD{Di`c$V*`vCtweLNJ+Q_N} zV_sgC{_{NFa*x@E89$U!nGK&7WC@fYT}iqYw*1`UBP(}Q@{JR$Qu5Vsg~vD^-nlE; zL+s97#WDDoTEdv&ODRKD;sA8SnjK|q8TS)9Q_o?3X0?u8Fm|D(y5ri4UjVCu951Mv z;=oY04|;i#Y*?0z6XT**kF_lw@Y-l6(;!I?4vvB(1m*Zl8wYC}apG67;4syrb4I2A zhCk3av2RH2r1ml$=keX@(m{J*(`vuQ^F{iOJt{H#ZN{S!ziAtLY>mKpHlI&mcH-jc z#ByoGStl5eT1@Jo-($z=w-D)7;+&I|%ZzydFTR~cPFB2%7*>4nq~X|JS!HK;QVEQQ z6K5fxR{RRFTJgWz==V~)qLHBJJY0}Aw`-u|S7+M;Ide6vPLa)%3xMMWXo_^K;1O4d3bc|GH)(-Q24zYIphPUe)9NKY=9SA?FS0MHw>) zD<>xKkeh0)o)zRtD7(zR^E}UR;7J^n6Aa{CtSEOBj;w;QX@IQ+@~S?NxDxaW6iCA7tE2oXRt$Tl(*Yr?~uQJyKJ3h-B&gH zke4+vy_|7NOKrh!mgsJBM5ZzLpoqDkIiY=ZaVk&xYQ>F1|Zb5^dK%}?=O8}7#q z>7(OJ$1bezdR(;56R;jT-vRFf*SolA-QSQpkJ~E^$5fuI$^!y@Q|@9OT5u{M!>6)g zhAoyd>>=>eL+ez#Xr(WjI~hCW98Y#fbn=0WzLuFqyNVCATn)lK~X>xP2g?#m=Ae>6gSJT!sY31Z&oG5XY%{h-Ue2HHVEdm-ciwZN!H z2`SLVC25LOW509={17JlhW{bqh4YDw>gZKi9>2Y;dWVf7r0T_+i2e$#J78KcrPGpECtXCixa&=6kE`$ zaluZ%+wRFmD^pEz;;JF=1i_yuX#X5}^eCI}tL1oieZER?Kq|Vk%HV@P3G64Tz66FW z3Q;*TF2?p6vM7|GlyCoE%Aaww{FiUG>Edvn$^T29^qYBJ|Cc-o|4Sa`W}cJ(lIJS$ z?SI$zZz3ao|0D9mQ{UaG=(i1|que`5eqPl_wETB*C%XS(+-VG+Q^w$Y@5mT@INYDN zhwCAkamWJ;Bd3r~IfY&c8+|Iw)_8ZE*c|4ZBs(OU;om2VV41Hw)XKc_ZoSwHTvRXW zm7X#k^_TTJSYEDfte#@!{MlCCkF{vJBt_o3B+WU&m^t^xS})FO#aH4Et@x`b+n9s> zj#s{V;9GASTD9WK(Qo&C?CkYgu>m3M?u^RV&Yo6GYq@wZwwBXR(%X$FAS&R!aW~J? z1f?gl!*(BRZw--zzE+%tUdM^WVb*DAA4GpjC$YwY*3ppL8R>trl51kv>m#n|Utp<8 z>(q!oXf4K(XUh4S65bgnh6iF}0!-2PB+8IE#<+Ns7JT}+1+>n-EiX2n(!jUlsT&^8 zi>Tj^@hoUf{7(I2l=}ZRQ0L>tKgsGocyER#McG0iOI}FYn$}^sVjOH)#fiTeIOyZVX<;6Au*s(kzem-lV|6a3UguPayEt~qt*O1+& zaRJml#7u&RO!OrxJ%AJYg!1$;o|?mG6V|LRqH;}oBn{Yh2<<9p*OSHnj#QLIvdK=! zD|=hxJ$22+cQ<-e2X_?<3)L^H8a2E3%9%{QJJ;PWog2vaY-~vZMqu^qbTsvHyv=QO zYtAs1{poX@<8Z#-kbV{Imo^RdO53Bo(yO?nNH3s=UTI2nhO=L?4_%dV2J`SVXNa#C z;lrd4JOfc$5D_Ig+6TSKtI{sHPr5vCRXPyrlV%L^yZR*Sz!c;#BX2UU`oXKxvB*`> za?A)FJ9JU{g?tIR;+LdZgA<*Xq@l=V=?@5fHhf9CN6tVR_0ToxFN6Kk!vpX65$6@B0pX#v;%4|2qbPlZOlrSFG0e2q+pjJBo?e@meaS{*FGb;RJhKVxm=4kk^^Uq=S2*fC(Nt};YW?f1ll=(6 zNEz&$_}8~i_b^y*<^iVug7nH@mrDD|704X+VU9%Uw?m@z9%_CGwLK1h6E%>b-3|>} zaHAg??d(EJ2roDXx$1#{xz2HQBEQGcA84v6uQAndy}ViDB595tEoq(-eXPc)!~@q( z!x+&x=~`95tfJBr1GJCzNvntFZEdTnhNKhN+ziNbX86Bu0|&5PFZK_xyY%AI1M#j) z?q514bi4>0XA`cg(6~$M=<4CKqS7bSUtm}IELa-Ol7sPCH-c237pMJm&66tU`K-Wz zEM>iDeYVgqeG$4I#Cf4s9h{0?{P2Kkw|dXVJqflIX^vio%cgnG(~HLk$lnZOK@2-V zynm47_q@J-DK1RmuZ6j#Tv#(5v3WnpDDP!CAfxzsn_sTbo0 zXa#JRGv;TMjKgkokE}hY7q^D{rR{_7`bPClWcsE31AFC(wiCWom+o+Ci4Hhq{B~wL zq%rbQ=h}T?o(KNV>z$+D*NZQODF51U0`*+{X;_bOC9HB+G0C`J$GYmqK1=&o0Bih5 zo3*+tSt?y@KY6r+AQaMUA0vLPoQY&V?slhI**(XIta?=-P8=&U*=|2CPKOmt+P4vB-B>}8hy{?ykIT#5s=gUa6Mmo5OmV#i84G^qWEUiNX3=llOw9P8 zs&6*d-A{41Qda_!rhJgZ*ceG{s7{Cg8Lg$=+aeemY*#>AU{po!)p%!?)8pK zov9JmhE`1r1&mdOg}mprzD)MDvc`vZ6+`;lxmkAG^kN_yH$T1vF^pc^7iFDtJLE`= zXYrj8Ttsk`8&@j!_`%_C@B15Ld;GU&SUhG=FTM~hP|n1Mqgc1VX7yqd%7ag4l=nIE z0#`J5^)Ti=?({NlevIBlYwrns%kYs}rxNB`M+I217vB>TsRg9=RoxFfrn-tPuY>dB zwY%|Ue;EPp1IiXjCXRjE+rHs}?@|^vICz1mAcW!5%KysqDKN@(K+oW0R?lgRtKQRQ zf7bPES2{t#&;Yf@2jZ()t1e(Lv(s7I81cw3Z3e#OjhBNA!>mTDA=i^8j8I{ijxT}B z+t>X2aM0=sYlUZ;nJH`*`Aa|HuzxYo!p&%K9B3&x(=DgjmdaDySF2u>vu$xcHvhAh z%%T@%qb)_WB_4o%LuS&S<=lG28c7;g>tCArS<4ZJx|@Xt2RrMNV2U6Gdt2bo)}Pa{ zwhDAG{Ze^Sz2gR##2)-WdMwNyB1x<|(941Wc4jwZ1wX5b{#B95g6E%tR##<$uw6Em zu5F9oTG7T$;{!URe=b_@8zU}AElb#Tk!vpE5pH?y!3G-iZPKPE5ZxG?Y7FF(m7i3Wz~>igxNGtWXtU1O%07WwCf zx!_hI)bOIb%%CwFjTHXQHI`NN7V;bAT0_fLOG%TQ32fj$@lDR~TRk83uwT6iS?4*` zNp+4)-uuL}g&X0mwa)+z1J%|O4hMci0cwH9+zDxBlvubAo*dqkpR|ePn>KZAox90$ zef?64co$!I-B8iBi7`@Y7CK2q)2$vuz7<+p;s&%okDUWaxjZfGw=x}{?~f?B`?GMw~&_Yl2@{Pe~Y(E-n_U=zK3-< zo8-T}!}ni)vP)jY+MV{hyJQbi9QQWKpC)z5Ygm6~m%Ixp{7*K?pRn4z>^i>x(&J6? zS4lfsn&iJA?FE$Pg*Ia~o;&gU*xWApN%l5pm%I~CwQN^U`Iy{=e1G>NU2^?mZc2=V}n!845l7oo3iheAEO@-rlKKsZf z`Rm0^^50Ee@~N^-^2c~CH+A(qY`CRF2YdSbsWk%s$1eF2(w;|&J!sQGZ&%Nwk0h3K z$^Im+Sv+}3s%l%(rPtk!>&zalZUOF>c58JjalZ!_2hyb)m(1wc<#>M#IIs@y80X|W z=9BjEK3SE=OqvAXey0rk`8eDa+II@TSY7-vorI5oXA{4R5J= zA7oB1YHXpkzfUg7Gt`}uXDm4&ySS;&Q*s)o>QDBbk~4AF<8I`#zz^Aiw^5It>e;%) zJpYtjK;MCh>s!aDVB6}Hd?#X)kk-y+poAjM2rEcVPGe-koP8fw=RW!8?poM3!uJFZ zjQixwBvto z#w06XzuY@q`=t)~JcfszzO(ZrCXsjo9^MKKD0h z6~KaAOLd{r0?s@}{G%NE;$_rmFw=KV<3{`b1nN;9cE8$Zn%R6cWSUb^_n&2^p6YMd zl!CfRrkLwX=1p6(h4&ywc!(%=kg0wkn(ry!`kt&~c6m^vb(y=Ya*X(*4DVg0Jsv~d zo-Hdp$?0iDW5oR-#**h@4>qhdRkN_2A1~1E->~)r>2aAoMEm!a!P+-pz|NFn{Ym$^ z^`B}V|0G4!Bt9BIF4O47ao&a}3Lz)euvTNHRIRaHcM0vfY1nlk(IcdZ#zdV!yKH6$ zB$So?R^403wQXhd_hXekg?vrxr+YGdTTzyd-@2Bue6aT1`oF>MH>*5#U7HKJE$%)O5sdz2g; zau}vF#_4WI65||On)yT6_xS`ieLf17i;K7({3deW1hqJ`Rj`8{Urpze2Gp`KblQ18 zgoG_)tqQQp$lHW>G4cn8NB%JE2$yet_a8<+RXl?g@y{dwcX=#$hgQ!m9V*kFwY%4j z6&ElQI(9!XS4?JiE3?5VZ!F$kT|dr8F*-cgXdFto9nVWne7d&UcLp&A?$fnjt^Xag z&qxA*&w+jD94=5MKFpq3``Hs((Wd3aB7RPx4qO5S2Zl{z*olFMab-%?_p0W@WPFuff7lJfmq_dK-|5{YO#w@JR$ z2#Buy&$83>s4q#_QN`qh16#|fg>3%Q_$4dNbV42q{y}z5C$Fc`VhwolW6^@&qBjoz zZ0Ppc1b9D!EV*eY_7;`KLpIBMqE;Byml(HAPGl}ry&z{B-txHs5f#oGbLK|_b=S7S zm*k5btE(9EQb0^|lyV?Vy)P=w8mew1yF2HC@j@^-PPFM0{Cnj@o3(AE{9lGufY4R6 zo}O7Cgo3IpSc3vwa}DxHYP4-*B+aN7O@rAkX0l4y1kWEDcNeIbQE60|Wr_B)E#h`N z+el6^4OBzDIBC$XV2~XM%Z|^u! z$(WYPlMPYnCuG;$7b}Aw{5;6sem+RK6sZYa#l}xyH|h6ssiUIoBJ|TW^VQhX@#V4? zr6r-?WB(c1%W1#%%T53qo3?Fru>RVrP!QBLi>+0YmEui-&P~zcy$;w|3iIk#`N`64 z7(Jp^Ls#8si@|jk$Zo|_&VAXTJp-%Xs$+8I+~;6Nlg+1eF|T8D!C4{tu-!8X(rz&h z&ACcTxv{Fi*9A-7;Mc&icE3NP@;a!--%a@$@XDsDbLD4mrabpADPz=Jz4$ZGcH|(W zF>zk-!}P?cbO{)&p?wX`Z8~G%ky)Wv>ct;}N~1q!5QSr*!jet z`VY`m3R)+-$W9^eo+@Oy=DF1yI)M>dT<|_U(~Mue@|!U8P8)0sMxm*w;DVnjPpeS= zCwQthK#XAo%g65R|+0i?P=LizpHq!oXXUmxFvU*|GHE-_$lOG z9Pne8U6d*g4DOLf+tRg;j!$KsP2CIWl-lc3{@{{an?JofE%!FRt@{AZg5{3obJ-WE z~& z7dY%D{$k$zXGmFn9MqUUgykc@;@@C2c-`X#t?-3xt0m6_)6q*S?!Pl7a>QIX3EA2^M?9@fGL;E^jSgM0%Vz&Dn2jw2af?Kx+rZ4ACpzwMd_(9yvDGp z;s!Ze(9S1FIJUNnl$B3kKrdu95C=XM?Va?EkAlz8Cigzi$UfHO`F0=Ur_O2}4@pvB z-F%8)HFb)gojNLD_t>xlR>Csq3eQnl1)txr;iG)_e~A)b-tMvcjzS9rR6!5YRL0Vr zTTSD@STFgT)=VAdZlo3eW7@{yw3Y|yi)XBV@t=F}e6Xu7MdTAnIw7@#H>pg-*S>wI zsm@%-?jE5c9tO>d=qa~Y4 zfaeL8LL~vM!q^!u0q=Hom{xG|H8&=eiq>~HU_ToteU$Sa&LczHV ztBnJ}wcy^6zFFI#4VKt%wP(ILB&j3JWZ23bVh>Lhpd;gsNIlVa+qm3#Kc73w&*rKF zdr59Bd#6HWAPObD!>3-5?)*fe*IWy-#=f8pa=tjFi^l*m&~mka@g_h*F53%>;)BgT z?$(8YX?1*-QMe+#Gr(r6138G*8jM1(WEo=bOckRl=wKOXq{L@(6fc(-rRN9sonUUe z7N8PMkh_JZDx_$7{rr?44LlQaqO3-gH9la5S6zyIK6>0or8-e6H)Mb;$3{Wzw+eaS zR5^u<(pEVgzRSKF0@(bn?!fN=oxT&~jU7RM4{s!!qTb?8PpuBtPXQ%uZ0376aQToz zthkoQEUl}!HqnleDCVE+^wdL+4iZ4E2~TeGloxV$9!fjBe6yRI&ehI!+uFZP;T=w) z8cuO{oeF6Lr1HY^1<7P=Znm|mUT6Ar>dogzLlPv$rzM^`?0epfP5Za8BRpk>A=yIlkLpfy!^x8(wG5MwPt3))s4)&jeOu_PyKfwW_*xr(ya8DcGm#l*Q z&q<)o&+C;Y43IZcTl>mPgXMaVO6zRVwf0JBN}K3CT?t_u~=|9aQFyI$djqrx&_#N~yy&jf5V6M?IY)MWnO=ePec%dm1rXR!9$lmX=@P zCjJ0fPK}L1x}a<9Y=OnpqVXQ)1xk4cFbVbT+g^dcf>H_cNj2%ji`czmIJg0Cf4})g zaew`Xx4$TF@vvXA96l$&uQwh^d%Ag_ci+at%gwd(YuKF3zz^zJ`ZgJZc~qa8QNNKY zt{FHg-%{F?$sWxSbou=IEe>Ga35BvMbBxd*$o3GGmu84H@6JqyKAg&koZMnr6~CLP zYVn~!yPRZWkE-85Iku+Ee^<(9L4qcsLwxrPg`%D+vZ|zjP0)!C4tjjLg?MtUDO*$i zd)RB#iR%V6;IL*RcK#si^tLasf=7!>1dKxP#r>~nb%m9XG_#SU8DzeM&gxw_um2}{ zq`Gnon{}aj1#?v94Aj;tVQL+8`Rt^}2c9)x7e$VWv0?9g2K-UZnt?sCC0g#WY*9~2 zme~vzT7J4xcB+2iHd${l)v=HRjYJjp^7BI^y&apeSPcnE4Zj&0 z_njI(L8^bQVYD?FHSAYv7_ZczLAhuVpf_j+VNTxqPT3>v8l~iVM#+`&Z*#r!om_=g z@xC-=EgM?9s#U@xRt@U(4AH9TL$7Jg5Ix?jjLj`rXG7AWP(<1ad59Gc?kYAa=Y9sb zD0l{ij`${hCpJ~bVdjRQvm62T8A}QO$$s8bzJ)k0=c*4D2ZQRnM^Cy}R?RU2+JjW; zf5?F^f?mJ`1wF9U0sh$vNL8{pQM|Bat5d;hu#ou2#NS)seV*uXbk^{~vs>ysdur3% zY48TsfsimRvV%Nw3OWDnIYT>w-9x7hadXbYZWwF%x&@Z^t{d&_g|8ngg_jQ4Tr|=N z1I~`1^Xzi#glFsaY`7v_Q1*RoD+7D(@d9r$2$!UNH_sexYdjz|-o%<2<0Wb5&G3R& z{X#bD$j`oP$@i`P8EfSOkg+vhmUc(j8Mc|K+&;hzB;#U8V2vNP@J47Dv|g5;jnHm9 zt97rORtnhyJK#Jcdw9dzQGFVgHDaCFu1M98%ef-`3VbWpct!d|sWB#9dI<2m0Zt3z zOs+y$LVE;0T|v!bOpPr}!$&PlUm{og+9)fVN>AT)E2D>Fyl$rgb| zM|6buhm5}SEmx$k(QdMC5zi96tpnq54YKt8!IakI>UUc*s$prWZ!a{#OZ4$=#Ebg6 zh1q#U`YUqLEYZoFiPLwYLQSA^Ti1F?8c-mF6pT+Q<_mM)z|>!o9*?qnz|GusN%~=w zej9*mQW+DGZX8nbNHaI8ZNy64^(EE>R+;URG#h2nS@GRew;~lZQofUF%5bU}{6nji zYrF*8ZVL2(HjVSe>eIDflA4AFg6E|LkdittEgIyrvpaYrMhQ>{wf!eC=mt+uDNhVD z3ossIjKjAz9xMA1nMNFNz@8qok!C@ZY>@QfU1@*-=|oMytUZY{`dzd_lvc%Hu$yq1 z2w4_tRjj2nJEG(nhSNmpe&y-Bq(3>aHAQ&DxA6(a#2P^l1f-G30Rl|ABCQ8Z6r0~G z(yzmEP}i!jPOgS*1*HAs+Q`Dj6)6Gpql3k<=v<~RRm1e%qhh9v>eJ~AxBr(T&F_I` z&^v=w!hgY@>^4~gYQQKjG@kPL)AQzyKs-6a5YOSdJ+ph}_Ox7oV-VbH)(zyc8AjD-RL|(;8(DGnOP_lY;{{CG*2Ma*Eq5 zC#;#R+~d~Vq1<(AGJbix@}yalIsBwrGhTUO^UX51=C6>pye#bqr#4<_fs`J0j<*I! zc8++$v3^nbV7$+%oM5R=NU+|NnwS{Sicdz*`V0&B$p)7SlxC9cI@QC>gmsDVYinP7 ztUxf#ib(xo_Ip}6ZlzY{SM~>ZNG%k(e|*1I#Mc8^g=(y;T3q!wlf12)s-MHjKN+6W zNzSop9q@2ggE3}oqxv}J+`0$xOt4-S!5{1Bl&)B(eI0fkYy=gh*|aWoL*nVgQ&yZ| znnM;htRLZwC8-gD%&JibjrXSN2GlaeT8vs=h+67YeQ_*oXTPC^RlS|Es`;3#+XuQN zXdh!)^}%DHRUMO)Yk&1D!5XT*@hs8a&keDS+Ffi`9KIN%?KEs#9+mTK>Rq&}ZIiPA zlTKpi$k8RhFWRH3RG$nreU( zvK1&ph?;85b(VGT8YZg^>>;c+JGXyFp{?W*P z4Nwl9Ei|$mq}si$J7kkPQ~2cPSogQ81BYM>E!D>KstoW9VCA5_av5i^tfVziT057k z34C1n>To*V0NQlEA&<8beh+JPB%`oMCA`$kWZ&cG>f(1mj%KK-dY;ELCw|XN>s(lS zN)fj+yBIrjmN|ppC)xjG?YRH{|HIIglKOW1E@0kZ-hv0Gv&=>CP$_&dt}RMDz5RrN zY?Wt%r(#{UvLY?(zN~I8Zn|4@6UNdhFxJ@EOit^ALXjF_HuKf6;-?Ig=Y+c0Ny@oVU0U&PGSf6)`4cf1(3P^SGi zO3D_#^8B!}VjZ_*|2q0I*d04VTHDT$#`r1(CVzue$COmB){%VF8+d;m?~PSN^ZOOv zUc=jGRp6CO+lQxD$4{wAJ-Aa=*Zy)f{DVbOk2pXh$rhT6wV{E`iF-hSZRB^%7UK6}w`s=c?>pV2-2jZgqC`kOa6t$&!@tX{+W1Ium?zoSC0{&X-w>mb~EQ0J1 zu-qLffNY_eSHYT9=F8Go1N+>@H?jM!1vM8C0&rPIbL1$bbG3&tGK~5Vw3nnSfT75i ztqA=Bn~z<03n21KutIeI+H=8elvYWl8|)1x7(Wa0IrP166t*^R7aE0!yP2sA{Y+Ne zWU?{5Pk43S_s*okzEuWrzkJZY*A|`tv^@vfX5g1c)s1T5tdDzwt7mdzZ6#ufYV+niJ8kF=wbvWNYh5$P5rS`L!S7sKej8_@J(D-U;|x695DvDn z+1pBd7V4=h0xYEzu*nHJgGNPpVV951W&Nt$Q33PBy^q}E_ySh*@0pwC-d&ps+j)d9 z5(RSZT6iE)(p?8-hP+Di;8?O|2WCQJr19?|PDe3tk$q}FxViO=@$;6rU+$H&ynDg* zxG234E=9YKg-ZZAUWDC+xwu~jjX57Sut=isqV)Oz&OPs&b!O}yjOi#&#eA}*51GGT zcF@d{rMOMa^};U&vhlVz?HkJ@Ztm5pAovi&rF5R{v`FJi387FH3}04 zp6_EDE=uFV%h2zF@M6G&7hykbKKe!w)1L>@VL{CZ+|(Xo8(}vshb*emyO}J~<)Cly zHg-wU!&;p|3IAF~o0YIDGL0J9K)Wa(F2dr;sUAZo+ z&5YRJ0hco8;Qc{hP#VnYdGJ3g zYDl*HF23RsLp$kMrD+-CEI?PBkWo@#UEoRdtGj1eiGyVFGsc9ov*8_bp!-fA={J&| zBX<^Z%#WtFLGv7~vcQU27i5p@o|G*N;6N3)NJimc@s%L?gz^kYLD!>9?s@+^)v!k7 zq4O^TvgEsp{}!ZriQcdC#^^0Xb20f?%R6rVQHo(71=QtYj2WJBk04z~k%lpTFi~&S zc5jEYDP&s_bKfI-5J%psV(}wX8SsD}?Q-*v72dz;{;vCpvO_aWnA;`zzN7Upf@fcW z<#&1)VNrmw0EW{-YW;QW^ER^RnLZU!aXro6r;~u2HRaiBT|HDkAzt5w7JwDy0urN2bFdahPxNg(tT|s=~K{;IM6_@NGp{TJbZXCebads@MjuUF=*Sa z?Vy6IT%iDTGk}GG0wrt|S(d(r_wi2jAW(7o=Bkn~SCBI0(7zQ84(J812nATI&sU;D z(ldkX?jdQT@=e(w-yi{}P8VDgMuX~{+s*WD(Ei7RdwnY4qv&nM%Z_>Y4Pk8Vc#y5{ zl|GEHyL7vIrEX>QitX_w1|th?slN`ed+a_Q5&{Mm`BpqfEk&&b_cPgN16t#GoEqN* zNgfrFz!S))dLq@OGCpYZ8BAL7)ByDL?q|&BTav)pdIhCJdK;@e#>Gj;&Yo%mAEV(z zjIjZ-aX3uM0}41_@Fy4eH`&%9Fo|}(u0U0Oh;^OXdVN-36ka49LyAw z#lZ-7Bu7wVbxjlu9$nrNtgP`u`8@vjR;(Haf6>>`KCZ_axe#f|bm`rz|rm5ptzQGf8^Pb09efn24%{+THhYzgq zfTqUv2fxe-1sau@K0w>BGQ>4jn(BI#XFyeza?nyUxh?}((Tdg6g$u!onkU|jO0bi; zPHH)T(2pQVNwVzIh2No+PCx=1qr8a$y`li)>c^-g60WoTaxhH`ug(aveKIYkh54Q=92{^(6da;rjgv zt!_$t>`u>gJ%D%gvoAYg%6QNzxQ}FKZG}+a(ari#te0Ej3(klV((-5~o@`+!jS#+Po1MH0j#A(slE~2s`f7Ii>6TJJH#sQ#zm!(5$glvujE%M% zI=DvPi2V8Ak=M{JVQe16UXT2*j1!(hP2)eGUCqw^5V|v(0JHmeX0jN>2~Vx!*X>C! z^u!gx%iQNAfp!WzjuE=-WFrIn3)(ZkLj??nH3q?X;a(@*yI9*TQF|MGF=S7J=)uHY zo6*74kA#sQ7NT(xwO0px?-n6N%o$iM08%YY_~3i+eW?eRfn-rAX8<-gb?gmbQc>96 znt)XU&n)nM1q{kd>n9!dt5Z#d^Wy!mtv@&h8ZD$(!v(lJHvd}C0AG$OmE1)dSYlGda#8=-Xu$n2dt!jZNXoR>))XIYFmqM(9CI>a63g=Lrl&HIp09CE|& zC96-Jc`o>tV6a>WCcIB+qtdU2`hY1abn|Capj6^S0)~1cNHE?+j1JcF8&6{-I9$Y4r9M6 zrQO9yycSzB)^Jq%e$?4!t*-E?59yKfMbrYS*%0GPP@qiJkv|7jhu{^%C0@0IPP z@A}?rQR=WUQ%5zfs)WB|6AyayouEKNLIAuu4LB`&FUH<|R8B@{YU#dWl9scftfx?x zv9wlcB@a8uM`Z?-UJhKUEZc!fSVcx&RVm-Fub8;&TI9$EFB&)uO8yL##Ta~-f^a*^ z$U`ALt4pVo#!K;}(*S93}XG9KyK!U5HQWyZ_eFQl&yL*3E|W(TyuNBAK`g zEodFZ|LJR)v945kCM^rmPu)0|D$l3c)a&PQy(SIudpLt%yR#-u>kH;DIS3uPDrdLO zLyz>0bY&WFrw%L3+eW3DgqVVb#I->JEl908F(wC8ioy58pt9u|&j zPZ?$eqOcmKS&BJ~1Y&*Lc#HsKhUtByu;mlPmhq6DD-!>83~b8&L*G;i?LwZnR7n@I z@P7jMBiMpki7QmOL7e%LPY&9oReTmaGI*DMOeEyvti(s&7GWO3m)Im8Pk20`k~S#@ zzhZbfK-YR)>-kn(XCZ3a74^*W$SKonv#pojAv-3?sCv(8F(7DWm?D$MAZ=CU0hz@- z=)FVG6Oql1$+=}?w8)e?K{=6q$0I#8o*l|oVIDYG#<~u9lWfipmF>dbjc+JB1>%&Q z#n3)EY4(`p%l;1RlE`u zwNNYOEruN?&Xww!BH&(FT#@4jzRhw%4mCwDZmx+I^J=3?CLb}qBVc{)>)C-86&A-I zi9)O*Lv=-t0eYOFLb0>fZ7QAY+_Mr^!fq-3w)VHu)XJGbKHjCNiUFd}|O*=6t}4-VJdY^LVT!*sbo=*3#d4 z?{mn5&iJNaU;P}1*5hbkt`XvX&qn9sF{={R!aBs1DT9w0jSfk+ zU@HtC*0Fx=+8(Ydgq5t5sQIMEPggRj&3;oFIL2+TG45t}G09)~_pdzSW#K=!4RFe2 zWl}Hr`Tg7<8jIO&h)<=kyK-y!$}zwQCM(rm7AAWF7VsOR2S^2*>6C99{uIz!@Ks=B zq63`NJ)x>Xqiu|s>e9YVqd9;pgzbwqmR|&Dr0>H>f8P)@ZKTy84GXQsoHY6Ggl&85 z-(&ww$7E+Z{9YFeTG!hJ>G#id(rdSQz>F9B!A3~nw$(T4{MMLLDwoVdZ`<<3{i3Oo{9q}CI$4sWOqSA zTS~A^sjwzMAM$q}uFS3+s-5hICqp0dAx8ElLo1pZ3GwFTFsg~Pr3 zELBXwSVrVBJKAJ#cP8K~=$Z6SH?d_~HbKUrROjbyirh5KS)KE3AQv$*(5|fYz@gFN zPiyZlaf<-Jz!cVBJdVLdH{X4X31X{HPtzrZYTocw=K6X7? zqJz#i8k|yp7q}(i7_ch#!y2h>9sIL5AY$=gaa8Ts2UDG(0muHX2=ya&gNS-2U_8U} z0<}i;zCQ;nZ59w28CE{1Jrh}xcP9Jd0aN;`U!mF$t2kTLhhpw`PuYyUHXLu{y6cr0 zrCV#)7G~GloN=PA`jj%VDlGR0{YxXnB^@-{gVqpPRU%?9PW>5NjHS(Ve$H>OjfR+Op{*rL2{#g$e7 zFUd*%w+pw{vhUiQ{5$#s86rsnLb4PhXnO^|HzE1^P>hWMA30PABe&7a_ElNG1#CT_ zq#*i1Zb1Y_HrY%X^#jV?*4Q$FyHU6&n0MeRrj&Y;VK%;XEXS-uT!K{h^kjN%D_B_2 z?Ety?Bm`0pcC_Uy$&A}Q(c_&U~j|%NL?X!gzqN$(uP<^+Am(k z6yNv#cGA-(iiDKJx$MB@1bZQy9W~Xl_lXB{`&Gg&+~5%QDUaljMtq9xhAPJcxriy9 ze^ObC*b*D_Pd1Mr?qKOj!0nlcGjXzcX#agmsel+3dYYZ#h|u#o1Gt3JyYts7ui*R| z&Qu7FvVA)eM_ICh*x&UN6?PfduT$R2yd zG=*${$a^jt9589*s_go6iK1TZ-{mlD3;|yWD^et2y|_^<6H~oR>vkPkCj&;je5{z2 z{@kS#zr*=t*ADC$BYY=It9B^ADP>ac#p+zGmc8oHB6GLZgUOb4ZgcM ztS@DfANErlR(9!k)p*L${sgazbwJHIirK_N%4=5gohesdvA+7$A!WBv4Jz+$eDsqT+y4awuJ>-&kw1?`| zSFH5d+ssc#Jz#jtP`)?Bjc0a7fIc$L0qh#z?-QN$bPueyt%P@d(0tZ|&O8zr_96KW zX}J3UsO!pEZ+NQEF2b`_%5Jk8U$&`~EB1kqQ6H9Hz}Q^(2URR>f^8u%3+DP|)sAbZ z8Y{b88DClst$n$Y4lfhRdfBh@7_eyn&itv|If`t3C9p%O7T8oq%3B?Qm^$|C&kqiP zzEYKmz6`&W(2>?~>~%kOE3!3S8;shlR2ymr?HCABZ7spfspHPk*dt$%pliuw1jS;G z2Fy>xI)+VrF!ILT7~IvXch3c5ZrlY%Vx|EHOhah~zG#9#<2M_bM7*Dq{`&-ER~&q{ zU@6t(#)vc0bx^|9n6rBu_P27#L78xNl2tD~*<*tT9pd7!klB5@g=BW01{)GK#=AdM z-e%S*Z@SV!@xLzAg5rNeU~TJA2h}hyA`?vOOd60z`2m16No4l(1*>^uSuIgC& zF71nXYJ8f!5Mq3!a`1(tTGn9)ox@l@lZMb`r*~lGoHy!DJBz3E@)KySl}(9kU`8S? z5-=L$BD5*P&S{T(DPCwPkMCG;cQEW=JzqpvjWU`ZH{1l)G&3Ys(09q9I8lw-g z!r~rNn#G`{RT=oY&ds-`un&SCNX(Ad#coO4LhF!-5BxLm>V^ za3(uyH~z3>zgy!EaBm95f-MCXz@@e~5UiZk$t3^ahaHt_54KBOQ>at3&E0W@+wxM) zMa5CWU=RJ%O)!_PK6h{4xCk7m=>YgLD|SyLQySB1HkE;LSeR!56*1CmEc;Bkt?Ga> zq>AVX9qtVkwI`2SQySJPk21G6GUL!c zgH~%T>?+K6XE?e2?{AE0wMKL3RkW3Qot&5KAbdzY$9s-fa1Rj_Ky3{$V;pKQ4mGi# zZbkpgl}ChHyMTT_+S~7Qg<8KK5t!un{Ry?y??-Rx_fYiv5n+(pZ`56Lq43mv>T^H; zEZ8%c@1Cx%)_?Wn!%E?sF!fq5E$P}HxY75Q8Q<*BjHanLs#IDh+NWVpT609{GO5e_ zv>upLm;X~_OMTLBLS6U5?#bzAY=(Eq3V4iYKt5!<*jP-Q;AD}Nh6fE|uE%f-$M{N* z#+}d1)0OJWfDiI%#y4oawqAo=d8yj(40XQG6-KD@{V4i9&rh>@0WnV(_%p#J>ZQmo z!lVE2zYVY8qo^VFqgQ(CVr)MYrY~Ube8fKqnFr4(7PnsNK=e&v0h9b7A|*@_&C-^h zKQ|kGPGcRLn%B!w{$oCk;{nVy;e?3|S7E94rtq45A28hO95kG)8T!>QGDNbwndDu5 z&PI~nn>*}-uA4jTPa$oGtq-v2_zKp}1@sQHBhZR^lvy7bgOOsT6X1Jy`j-Z@y?4Q> zj?WZJ&NcM(T|eb0iN=fxqIO>|?Le&O0?ZTXp9$j`#BJcEm{DJXdg-%&sJ2f&XLG8y z#MMM`)fT?0#FVbAB|lU4-B*J{Yjmj@V!3aK!n+Bxb;{p#*Mrx&sPw~n94VNwLn9&A zcu5)UhW#wKgO`-sOJ7p%6!gf-Hm-)agnr1R6phR+u&p(l-iifO{I-XkSRbG~I}+9t z%Y;`g2JMDuG?R7Ekbn6dFBy1KbR?s@3;sSn(4pX_mBT&q-#&ov)?uQe^`N5NXlur? z*jHdzNWQhe{f133Mq^^PhxnQL5YA_r8nn9I#MuD1~A?Q@9KN?SR z@x%x|p&zgv*5fha;Rd1g5t#-^SLfnstjma2Z|NaQGo19-z^%~<^wL-GR{ZbcCJ;?Z zvbVmt33}--on((QBF}|*v2zhGcKH2k91JM&VR<~Vj0}U{V5TDdsz8hZcoXUsSsIsynNhSl-@qDo&04FxlusOlh7A^V5(P(` z1o0fiXRHPNrIVfsI!J~GxrN3}?3}d^bUq~T;0TDL_zY*_BtF5JIExQ)CJOgYI1>j^ zk27%)UYv>3sKc44S`RGrYZ#q074nvh`oEZ18`1S!GFLy6_&v|Ae#n*6caKNb?=_DM z&Z2v&-@&L-W#6sUK(w~lJFYi4igg{IJgw_E@hl5Y zlZCA)v-9F2UB^dDt8CgBL5L)HIQ(5;Z{aD=D6#LI0q3-*#6?vV;yRIk3tyV}5}-ZFtYcgiIp2Z9+!lrm z5)!7JX{yEsvO1D~=wM%omYYd0wF|4L3~kSp-gU!PK^?a(>hCu}6OHKHDEG)0daCWZ zvpPsGRg1|rRBj2z@z*GK4bnYrbX~y=4_(s;7X={|9`!Bd?}kkkY+M0}ndA;|2t9HU zp!U7$TQp~Lg8ASR(t7DMNAY+_b0EJ3*MPpJ=~74dsn2^YC&sT)elu$%Z1C48+qcm( za0|02dfU!4=8gv&ti^`6#(?`va7JvW%npXB6**W<>^}H5ATB|e-e6MC`i-N&z1&fu z@)}uQ;x#USQoOYVG-o6oAl_mfATOi_;4SV{^c5RC{qy=yH6;-Q z`75}khW-6M#fnnOmDQ@gVJ*(Y!DK-Ou@+}XKtJl2*j0}}E;|Ccj)CAON$)}Xcvr6u zqeuRt+j^v2S(Qlb>JNJ)9&f_~-J|hBx1BN9??s!RC(Q*P+pxrw1P#Vu6$h+J9D;tD z(a$x#-&}+5Q*`&QG3r`ufKlENsy;*~mf{;W)ba0HkyFOOXNcpe^=dV{QI6{U8nEKW z^-hMSkk*tr6Y+ZwR~4GPWSv8LlikR4MAGP)$PvPql~Qz0C!$9nuRvIS^aG)eymHAR zu?8~VjdosLSLAI=Yl>vSGk}rW{p>DnFHgmr8=@vN0mo| z2BXl%U9*@we!yDb-ZHCeLBCyxwZN%sVHb;>Nn<|3?z}m_e^Td{mohPKH|Od-%+(>x z)21&eb{ee^?hM&QzwRR_AZ0Nlx6~L#rfuoK(cmSLv8LO?xt_@Vbe*2fPU02wjO*ZC zVBs|(cI=gg+iFXSnUOKA7SnjF?L^WXXJ1^D1sg!U==Z!X9xmkHg1?WIoxo%7Mq4YL zpTXY*7y~%(HXMTxkK*_Cma~j)gR(14+k?io=LfJ8E$}U(_5Eh|J9hajVy9t@-|^A^ z{XW|3;hctM=4a5-v^`@PmyJC?m}y%xs1BJu$CPB^y)E6)2q)y?>N&c4I|JDhdm!ty zdMGbhSYFaikcUbA#y@zO4XgG zLYxhQvDSZrH6DZ5A#o)=@;iw5iYO+45@SSg_h-WLv~JScAcJ*xr^-Vk?nKG98iUA= zfz9CA*)LQY9y$|S-6L=7EO^0HeBxSW{}b1au|{Wqcv3_k3m4kIpMpE?xR~GCzhB)S zc7x_8Rwha=mXlmuxzevOGK&XvKXHwTFAolKmn*TQQDu5$NGVq=Rgq=7);L_5YjkCM z(L0JEqeIJLe#ex(1X*1H?v1R01g{E`zS7S?$-SfuXv7>HgFIHW!WNdIUQsNG3{j&z zbhig#Pg<%hNsEMR1pAbYXdq2RGWP9~-krH54H9}th@`q$$cgo^>+V5r?!)$n&fTTT zXnNqs2!g$Vem!IEkx$@PHe^Cug?LW^bVr%$lO=fa9^{?Oof!?Z@>J#j>j8DjC<@0=FTUICt$@7@Cmk&wh`m*hTad-{*e!YE=>ZB?o|x?PqvQW z#)y`|X6fUg;Sa3`Ghq1z>5Rs+y-YQJ6s)oT^a7xjN(uDHm%B@k5FVtN`?lM0#CL>B z9Y(1!;@I<)c8|# zzc=uA!|wP9U|^42ih4ggud^NsXjr%+bP0LoCq#V};45qGL(ai>gL4!|wwQPpSswYy zJji+S#0#+hoTWqwW1tZv4()IbU-?!GAHgJ-HdJJP8Xzk%t^u>cHKLxpsj!Dh;*!qe znn^v=&<6iB?rpN3fm9<2JAORb&Ojc~LpY$9a|4yJ4$+60)L(*gdr4^W94ls|8OUw# z6oZKWMcJ5pWOtX_+lc#i+~=x;@>gLFdY>M7S(nud8j-1jHhxM2;%c=$KL{gg0o)io zM$MRbmpWE7niIOKF@m|cy0d$|J+^^Hb~VO6xjWa>e2}2yhp3J25>41YL^RqL!2hMf z8eqWJ?EP?Ek%m8QtZ-BDL{>GAu36-?>}~Gt+?k7bv5|25c4;7)TuzfQ4N78Q0lu>IBfne;P5V%#_&r&MK^j? z5rA6&s^ufTb0fb*s-gq4buV!|ar4&%zHYSm$OulsIh z8Tp^B9bAX>u!UIboacPptZ#XY`P!K6d;8(0xvry@oJ8VLknaE<*6GZs%{57m0(kuo z%X=G+J#g<_$t3>ek8Xh(5P zIXAxg26hbz#rt=P4ZQ0@AH7hj!&Vw7zI;x#RUgVswEbFDU zFb2%*(j210UG+v-ev(Wk8u8xeeNS|2iICvQDvpBv!0u(*Y&(jRJ%0FR&x1^6;S(~V zIx-M8lNAB)AWgqRjR#VZV}vJnGP8F9@*YdILAHXIqC;fi2&%S zy6V$lVPL(b?uEqz0l!P~$!45rlS+J%>_g8WgCTu=oM)og3OkK)kY1Gtk3Vryg{xV*5~=~9YwFgKAj|orxa7w zT94JacVju|ZLYK$doB+!m@Gg)gVm2eN26sk#n*wmUha}$Ur3RWm+O%WFKI7qbM=VP zw%5TxGaAhjzw|Nus$KQzVq1U@af!biTKEY3&it&n*FkVyfj7Yq0Sh$0$VVFDfza>73rjnqY>AvQO6<ss54Co#5&Sil6~ zV?pyidI2x#g1S~G?hHos%cpQ2*!B1m`;-!g1zs1)SpRm!LkY@}UDd;a@`GW^L_MMi z=oNNxz5NO}s&)e+QI;^bPgK1$sC_TMI+Z+bqis9mQbsxdjNL_#E$4tVq8!?|w?9L$gUUO0*G*6Ji zN^e7(^h?CUU7?51CL~L35&OkYr`r6L${=BjrxG!&W>1Z2FeKe2&PB_1gfhJ5q%<+A z)qr<@gLkL+qgpLd-vqFh;>I;%UB#t0(yaZg$7oG&v`oBzYNx;Dc_ua3uui$njh1qg zovE;z+V9>!43Tycc9Opi*{Xv!YI&wySuT*2VmY)R?T|p6^Pg(w%yZ$H_&5JH$7w~0 z+l5uDq%kR<__sOZu|A{37HB0^eE%|8m7gpkx$2`&k_B6;IAy370muTZO!i|x_}qn{ z2{_<3rd9y@Me?rE ziLj@iV-kY$c}O+CSvqQ2Mm;aW!YtifR{^i`lPiCj17KNA*7xT66GgOhw|KD2!fp@$oVBQ2S#Jd&>YU-T zOFE8|Lfi#pD0Bg*qdltM6h*@$nIr5CpL^`u6rNu;)O+FJW7np}^UJhYF-#J94zNj8 zy_pQ>fodXOFgA%|BvXd6=;?Y}r1em!t(8R-PA(Le9i#9rcJjkRw}+&*wuYKQ!iI=t zCX(cf?AfcGCq1vw8jODl)0Yfnd(P2C0OJUEs~^WR-|^jREv6gw&#}j zD%JN0){_RI2=@B9p2i|}H|w0S_+cmXwyG@0qT1|gE34T@U%3tKpAA?edA=)}k3CC} z{Fuk~-0|h$#Ch1wag}TVEKcLI#)~JG^p=5z-*-jhMJi8YN#91~!rt1(qPD7AYIEJF zt>D`F{>PV$7qwc{H(_IlRf<*Pjl*=Ul(1(y?wnccc?Az`$i|?6FG1B zMh33`y*)Q;MI4WtwHE&Owf_BotTkZ~wUuD30dQ#}#_f+idQtI6MPzG8iRHgA@TZfXGsBYD=ua^4XLPW2u(`es@V3qXXS2D!3dBn$ zhV`?ywt&@jJ`iC&3oBc7J%O%#6k|ancj+KosdFMuQrk4=V~ZY)*r!^q2*}?;(i+-D z1Cb}SA9`8bQ-u|{P+En3{Q1FoXb)+n5SyUK={d2Imn^&m)>l=96k(MhRYIY*zg+O; zBUvErkfa{B=i8#_>F|0+#4mUbZI8^eqj#Y1iOLzzLArW42b>K2=>RR9nydqKldtxQ z(ew!p4MJ4Fv*-BA&$09E&ds#*1$ce}u8=BOYWDe8-YXhPX%?OI!eO9;}l~Jk}zN`IE4Z!b(PjD(5`p9wM7wEz9~T zUq&y@{f&>-5lQlZn{U~Db7A+rUPv%ToYA-5&34P{x@^xmuIb`!%V;gxp3}FwAfIz{ z3)Fk;wr=qMf)_GhV7PAiDa0cB-fw%3Y}&s8){xLLO|tNiAJ#0Y{q!to2{ysv2+&*@ zVw`aXw$e$o1`}be9+nS5y1T)e`W#n{=dKIJ_xk&i@Hz_-aV@x~xTMV#;&8^Y}HU{})cu zaYB*?`4qjuKo3p66B4r9g18P6jr_i*b@Jc8pkjZ3FO)RW$jvQD5n!mc`@Q*8%O$wxcu;L=*g-VlBYNeGq6} zShK>;EVDLrkV9Uk6Dw^}2KsL0o0e7=5{27q%MA#9emI+=KT9RWX-xqFLNi)&36wqlk&4#{rHhzZL_$j`{B+vF!$zYG|In2B3ym5!J*}P|^ zHrgB{uN*KfGiI`XhqA#Ol}Te;_8n`7YfuW1oeRF&9a_b1@8>+<%`7NZc_OycEfw0NhhBBjl+?S&%llKu<$L zqQ(Gp-04k#CV{BcgQ zcp8=+WWS2GXCWs|{jI-Ny&qvc8K`VCBEQZ@MI>?f6=;frPIxw=HA?-hExGPLzPxGW zEJNg(-NqrODMJkL!?f>}OxSq&lAfDqq&uL)xuj)0)*#)#vtBz-$Cv&A+x zC)x-Mf;B4>n}bof{YRrfJ)DBxXcWygP%^uW!_MqBFsHxt_Kkz(N8?~E9nd=t$9n#A z98Lv(0=e+bky!7$wvv9&GwROtjn3JleOGPX2X}vc0h5G zqUDP30;U~+#om5kNutomSJj63T^dZTJ+U&uXg&S(a9zp>NC`QKkLnj^VTc~1F(Ha9 zEUzJ7Q|>zS2Rug{)6FNtVQnj;cK9ztZ$*Beib{vXVhv_vZ06QVMOO~?Q9;()f#{{l zUy7JyOzIzeJAsuM8?<(OAtG5WOyfL`WW#X){r3TCY(~tRS|hQ>}-mdG^Qt_ zGFN2F8M62)*@=D9=ik8iTCf)gLK8d!r>5w|Y7bz<+fgcvumVO5aGP$x2wl&j8!)2$ z78s$`v-bZ-RQA5OEN+5yz#(+LfHT1vchLc8g%JbmX&ofM{j5Vu9xMEXw|_bZ-HUF# zZa=@9f%k-Uc2y3+ST^09)E00T!Dk(|CkcgIO>b?$@q-Szi`eb(Jw=Xk)qe-xr|u#( zYk5(hH!6L39{8@_`!~E)wcq5)N;1p7_rAX6J>d{CD>&YCpgk(V1{# z-}z>I@0-z)|KGI=uDW$a>p|UO6lNjjTJ-^FF<&d9ICO{@Eu579(#h^;5T})*1W>GV zD!&osktqSOF!*YA`)h?Kp=IIsQ#4m9W%^Mmm^j}kweR0bF^DbD9}z)Fn_+`4W8wVR z>0J3j$I0f1Sy9g6i(_ulQ;;U2s>o2zJAb*@lt(rsMm#0kks449q?b4g`RW}#$X|-% zCpf0yfJ6p=z)~CBz`&PepXw7U%Z1QrY7&Th5;^H(^%v7o=isGpz5koPDn)z3cwLXq zR9NgnAX`7p^KHrD6_Cdrh@m$Fhy$UKd&V5RI3@^V0? z<+jdj?{p1u{2+HiR`?3TX{8j%kjwRiSt|@>dh}6;KC;qS#Y9v<{}s%-u+fBA>ZTKg zFBT=}=qvG#%(gG`e~|ytRZw)Pkjou{e2nJIALPFsC0U3e54PFDZiD`eJ}<#p37Y4U z{KWehi}x9yU}UXl^^A3CU6kr8+7B7pvQ|ed|NXG06267C?K)_08wz40Vk3z8)mW;`;oYV=jJf~-UU?Fn44so%xzTmZTw|N$* z%R<<=-7V;BcR)AD2=F!Nc^b8{`IqEKSmfN-zW7~@lf5LH5A%s%$zj;j-`76>-Pok# zF6fGv*q2;|OdsFP{0cG7>Dlae-FVg%n33D}=95RJ-?%P&zwhoP?^nzBET59cwXjKh zl^3i@E7mA$tZS4P;TuVK>XO_HdceReaZb+TPxReu@$OkZ0i(3`MybDeFD+*h_b%sK zINP}C$Ca(-hYiQEhD>M1VFk@I=+5-Ld*}#bOBP>*6h16}+SBvx{1vtpmzG!IO*RqH z7Oa~Nx)1i1y?!`h`sWyP8>}I=}&ySN1Lv}sG|C1SWlYWx-Fs$y7rE&61DV)xf{T%j4<*^Xa8CVV_|^X+%Ta z!Ak=bDqZK`q+PJ2Jc`ox@bJ75$jXD~sxFqKH9YjKycDHSo7ROa(+5w3?ns&wvfQ7l zw)lc^(CK8{afB*717<|wsVslp)j!WFW+i4SmdPjNm&5e@iBQD!e(JkF2}R}-)Xv6u zti|EH5@BlpAwq>8uj)-|&WLR~lPMF=a&ctf`J;d>(Y?OwjM~;@^oZI%4_c1P3@_zE zBT1w1Lbr7oE8VVI&@w)%o1hVCSZ1~#olk8~(0txi--9oenwkxWbfFh3v41v#!h)Tz zZ~>O|`nIFLKt35kKT=nkkGlf|_;2@|fycPMRTpta*=kMD#XEHS6Q-+KDDKG>`7&-h zD_vIj$^pn|!|mo3HcD!sco$sifCkFCQENA!n*zH4RywELBVHT4*Upt0`~y^*Emr!w z!c}$zCcwgcqmYmPZtD>A@0A{A?0c}>R%K)_uSS2?;{c6=IJzflfFBAmcR$aCw&aya zPO9(8Ko1{>Z9XIYwwq19w;?g_De*K~s~=@?>RNTCgyml<%W6ob0cbBUrPPZAv;ceu z@-?yd8qn(NXf=G7Q{vHT-T|Fj)O#K7RpeRhe)bOs586$ihZCv%vkE&g3gz1azR0iWU*bo-feU0LAPE| zfA__ZX$czN%_~9SJG;h zY>qI2R)TIT0W=c)%aRfb<3vkNu{4l@C4@2|o@9t_CBHwN5qW{9|M}d_xC_P7P?V%R za=arK_SeUhuLZx7!IQq3B%->GOzf!uzQ;<@>Pjm|->O=_1~2(_ub(!px0y?5y){AG zN_uR1+Uon?FgY}hXeoW4T1Q_s)mc4^)%IH^w^mq@`G0g=DHS!N_fUhXxw7@ok zfz<>nJs(`FOqxwnKcSAbX-`SFOj1`?dJ0b`s!wTOT$N|^oNmsvA-*ppQ1FBtxfZ!z zuF838ZW@woF&&xDK>xRl_VC@R{H+ObTj55zlIg%&alOk-OfB@6*KC0C|%=8^Oh}{b=L#hTi^KBB08#G)W5Co znD#8?L%NFC#FP_hJ&rTzd!8o;nHfkX^>@@ocLey?6E4x!ATL4hNPmH^6VG%N_F1PD zCNZ@noU->Ju9f8xMc9VECZ1H7v=q-tMVCet(p9;k9^S)=yX`t3*)*8at|4m{)B2rW z2MQATJ>X|qu1IN1VN1kHZy~!Lo$m`GKNSm@!BCk$!-}I^nUqczzEWB|N`Qk7KLAa6 zSk8sbAgxc0Hyh?7y2OPR_CpIdcWZON!FI_s>lsheBkZVj7qupw($`{YC)s|R(h$E6 z`d2N2QK{1PAk)$LEVad@%=J)fNJIQNELXG~TdX80KcH0;(B-wL`<=fg%hBeZOwXHxkJ=+tXqAkweW=K)SJ~0b zOa#^HLYZh0<;|$@?8LhnBI5i?e-0H(k&Gr);3OaN0&s{vLFHc!eH(~q(Xp9od<=UJ zD|W^JB| zTQ?(mG0EPw9Tb4AaNqbo3aNL!5W@IQOm9-h_v;>Q6)-(jH^!J9jWHI;vVw9D-a5*R z^s_MFMZs0su8#5R7-Qn!5lI7M!lY#R5z!S=V!@LF>r~?XDLQcD2ZoN!5<^Gl@=UaN zL}({g*;4PDExO1z?2#=xv+&H*i*5%9}aTT4%9ao?@h)|zD7AFhNHHT zRL@m;M7PuVnBC+752#Q*0hHebECc?{NA)bNGe1BUx`L`jJt_Ya&!0qntzB2;_-=;s z#h|}e&@O`SLs8@PE&=%Q1j;>wdT4)BJw!toF^jvP-6Xr;H8t(>4h8Y9(T|^Loe2$iNS81J7OOLm&6<;fIat(3!y+;pqZF6-eR_*E>y z>)1S^+WLvG0GvEjwHu0&Mu#YFtFG0`#7I_nD3YeQsi5gGvfWFD91~e|&<0m;8`eNa zk>wm>*Rn*pa#ZUDM0|{*{Rt|rB}mxO3``vTzDffefM!s`|3TAke8upzN0_h!RKQvs zY?1(Pq;@&Gvk&vPdM_2U2Y&qK#T##C{^(5(Z{B|6&C}jXkF^K>y?w?yP)+8m@{u5H z2+Zw>SEtI@TCU1Ry@;xV74;6R>Du9!M%M?)vQCYF*7x4;)b~2%tYF`JRo*o(&`czF zugY72d-gtSuJ65r_3&GaAl;!7S7mpw>4iM+p`gWV6+7hnRA}D_Ys;o*wJ}=Zg_UU% zm1WmttvNoQUjW^jIE^>wtd-;*sE&1-~k+k0k8_ zKYmi#mmect55k)uo$?<)mT!C9Sb377ouIYGJhTy4TLnGrh*RKG_bPfq@G%qLqxhiB zaT=aNJUqANTb_vsnuvo}MgEQE+V?}!H6|2aWtPGb2@QRouE^dckO%H#@PJ+@L{+X; z94>rQusPQ%^BouPe*t_M3*01~C!B*lh}}YQCdvh(HOs&s5Kl7(G{x3@>#z=aa*)Ze zIRV?P7o1b=l(j7x?ZkT%N#?G#j7hX6IR)ejl3oawBYNxdp=;r95vfw4g|4+qk?{1; za%DdBVWXcQkMwe70qB?~1&vN!hTjGFU4rvtI4>6#4W)O#L0LC=yp6cG7S9&qyiQOM zg%i9zJjTj}+dxrVY&I>6s=e$9cx=v=-qxps4#0{}gzr(<2D?c;tZtLhmxN9oVhbk| z-Wz!V7>csjk@m0{w0%&8N7oUl^#8v0RX?izFTwx4b^*D5*VSmF4T>c()Z1c=Csql? zn-9wmg(HChVeiPYW!7O4(Egb!JLE6_i+P|Jh%^(($$;FceC`Q(d{6y%rc74)^9P53 zAGLeQz4y+l_cH2`j|gy(pdffXK)DV%O5K~^sVk7yU*F!UQDN=ZDipTEUU)MsYWWE| z*yY*04}0dDXU+X|F5AeA2{x>Da4&&Hc+Mg0== zIXv_7EQolE)~9_b-+siEV}dM+xTY)eW8uiWneDrmM9vrj&%NFDsEde9Dj6Pr$F6J8 zWm8@=g2-y-_>pp8z}<*LjlSxp;~iJL^WmX!4sJRV;<0!8o@J@=bk^9ZUuJ(6tVJBC z8S;bXLH#@AM_^|>|0$htT^^(qI8P`aXOiEb3uO}uNn=H&{fNi2*M72j(9fb87#sP7 z>IA^FFzvf}z4k){qy7@^2;6*^-dy;&_HG3Ef#98Q{^Ok*yi*g*hgBx;=NHF$UR)gS zaOE6doS>tfiTFXPHmy8Zn7;`7s!*5;It}_xUxHSD489hF=8SgG3Nd-UMBX=|Kxut2HWTp0vF1TGem%Un z^RgZf+o}L*uE^7Sti@`SScer~f~7|>C|Ji8Ik(5*{44nPqhX>W2(A(=kI!XBSshL5 z`{xoguY&FIJtF(luB2TUHx~kDtTGNxxyf1$r zICz?^eaZ)}eabQL*?-MntN3yMb71t-!bznOYx0h{C!5p2cTbu_G9ZmwW_%?Wr%i)R zrk~^JlMBw6oS@19JLKDfnp}`%ZiF}gv&6uEE3vkQxYLMtDC5lp*LqNoyz?HG>f3=< z@}8GryH+p2i{j2`|JdxEpd%|L5H(47jd<62P95~Cy4DKQ733L--mPz)5mB9e7M4dZ zdZN5nkemM2w^&%6=Yk8_(|JWc*!4vq3K<*^^)N{vL%-V!oB#DSae2&W$mS044$zX1 zlO0M}J^(6?;xsb_+V3|2CyoN*5T~91Tb&t9yL={WMI=^6I)zn3vT_k z!$De4GlXk$^ZS&Wg{~~x>T6-m6@sbmgfq(1Ba7XpJ_zHC0W_v>?YF1;7eXdbkNC5D zoCVHVSUnU2H`}*h?y0#IImmI@rLR4$>_c=Wj0Mvp+q$pF={+P_%>TjGEqkQTX4}Ln|DWVoeR75t;vQyDe1-L zXlTGl3do`je^U7>l8$O#QkUh|x{(P(#azD)bI{b-90cDf&PUcF!eQ%bR(hD$Wd2Dp z_S1FP9n`bF)@MUv!6S|iQBRsa`Lg`%jrxjjsqYnxL)P=OY7;#C^VIXvke--T`)_L3 zlfi=LXM1hx6MeU}@G|+?*vNmS;2CsBLXkTfSq91ot`7CoQmNk;jaFg%dP z9~&~ig=6VGdTTmHoNzH^9f3_D#?+}s2)htwcCoupL(VnNVsBx%lZYNy@O*+t`~JBu z=(ej1o?)bvAbFip26&ROPz{5FV_*q~%+X<{lHGj<{vKKld^Nr~o{{44MH-F1IcI~^ zmj%c+0vkKD3SYu}Gg7xgv+XDEz|qX@pe5#W&tyFV?}#UkHFHl8&ukU_D))C;ei;4- z+6)D7SK|pUBTrM`ynUhG)od`#btbY`NHVFb@%8!oY4k<`@LxR|ze;!L_tg&=*!R(V5uR&Tzd-L1A4mDBiX{s}*Pt|!E1Ip=)wr<| z@eFY*$)bU>IS~bF>tw6h@(%e)$OVx<(U$6>dxtvLfFmM)hFZIO*SobO5`YA8?)@i0XR&qe| zWvV}nezQ{HVK$MK9zBfvwkh9#_4Htol~yQO!{^%PPBGPcg3(q_PzT9C?gt!n(7cEA zF^tcGx=B88MSkrTnE?H+y7BvpyxZ$WHf?4l^)@CCd4HDDxHGn6&1_m^?s4)!kzQ2h zpf^LDLmdkodfdB&d*$FZGU^wS7aiJY#kYpy7=U@>9YmWQhn$?17Akw4sh+rL3!G8T zz4rbMk&f8B=-eSrjb<6*I1ZW)x*)~2GTv()!ALvQRWcZGa<4tqFx`RJY`$CT&&4-1 z{PUNv8Tf~hO4K*VZfsC)jCdSzCt*#Z49pwq+~;}Ixxtg6^0U?UsD>0sf-=Nt=_4iC z8;$m@Ldi5GuJJKvbHI2ihcWLw~6qeY!mt zRx04o@R$oWC-NwuRb)435N*ye;zaC+ghGwqhWrUM6d(vNar89ox5tywHZwf4le0l# zI){|pnPNZ&0UK8RZ^I7w^x=&Z#m}a*X*S2zz**V@m@A?Rt?)G(74BOB+K60(J_Q_+ zN0(u)?<*4@*5n~3b+y7a@JD4dO&;m_evbWf>wl`zgf*YlO^}0jz6N7qC)X1RNd0;t z$@UO45WDj(T0t?*}bLrT};WeLD6AJYcmp#`KWRE7Q7nLyCt=kPHdD z;QJb6ffo~V88k=VH|d>Cg%qiXTHtSvMm7t3E5jYbmA^T1e0;}>rP}Yv@z7DC4gEV- zJe}3b@L~CH^<3Ue$-Uqg4%!RM6{ZopCow~~4B5R2qbI>DlC_!Z+{%66x7I9*!aXfF z%LQ;5rLg;@ujj#L#f|GyMBfE3hU&&U@dZS0|0 zX@q2jVl70Yr(S55SHLO`s`{C^ExZm@gz6!v#+SFF|{@E6V$uE>LflSj<{ z-o&Gnp!4hQEWs*bPHpipNo$p#T5XtFt(-oXBBqPcjr99o)$&GK&D!cZRxCg{;c=`i z(p!K+haLW_N0qvP7>Qk;S&Heo9nh{uNwY!}8}MB_Wv)#2rQptSz^5qbe#EC%>SN)9 zIZg})rf0?1QOvz4=`L8QTRe!nml!3T!1^J~oKc6I*P{9U1gAN-1rRc|8?;k*3cR{4 z6aOpU8Y{e&2`UfjCjBuZeFPe6>txnC12D>ogJ2Hw_Mn8Jd*}>Cs_Y)v8WEd%7G43+ z(MCzeP;%`aC$O*Kw!D;VJ$SA?j-*1HlO6whVQO~L{QDD-5!+E_&sTI~H~I=3{0JuH zu&>gR;G|ej!?NF)Z$RB=yBKM6H}Uudh0JI+y*Q(wDuv?ep}vd`>2M8kR^3rYc&6mVj9bgIqaKf0T#6(N8UY&kXiME zJi$H0;;+wcL} zV3=WZPWI~W2c4s^s@CzP?oke_GozmHaw!h-rM7v=Hn2>&moe2J0|%$Y=Vg3UhP)uK zLnjtXjCk8j+W+5%g(IhXSMVR;E0%$%NgI18GW%VSz)>9f3h&M5#)d00tI*T&UA}9@ zFJBBy#+PVEv^87{(EZU}v|s33WML6=Klv1ttc@f)T3f-9PWe)~$Gsjl&wwD+u#7hkLW}I(xUybs)@5y9v~LJkUC?rA zY06;mg->XZ^hB7eU=j#sNUNzGnpKEKC1G``_96HWnypV3_%AuptH}a6Rr>QUIj#4% zwqstl=pQ(WELLQ3B#3Q@4oo_H=4wBrL4(k1;+;({`v5qoBvbhYhwjS&r!?JE#8I05wG)^~7i+5VGH`>?u z40+K>1g*`Et-c`+%6-ZBMuKW(x{TKRdy%^abK%47{U;6--l^y<>?YBJ` zIV}RCv;Y!9!XhU0R*&Di!`~wQAJJwbjmYMUHL(%!#)+|`Jc4hG(PStL@)CH%QF&R) zs7%2dA%Y;lXhkc?R^bHy}Ck9l3*H4RTrD*L7Jw z2YbnOHJWWm))cKTzy9QJF63oXJ7j)Tg*e`q$g|ckYEXdHp@?zZgUub}*NDPnr!XQOo z%1%5NsG9oG#p)Ivr@j3!O4VN&X&K?WZ!n)XNMXnBp7Te{-JMv+R?OP~%o`%L`Bq`o zkfi*w{CtS|l_U=D?N6hW}JOdtJlVVz9K`X*j6JPw{ zLZtsyC02-mf693DD}ByYf257Irn(p5_ zp)~pUw*?{rS50-P&I)IQ^sBB^=UJTRb+K5B7s4F=pAAQQF3USRdoXUf*wd8N@0&0< zCiFH3dUX2ESDm-6Z(2P}vK3LlpgWw6BWn?8{7U$+C(mcBSJ1{r>{aA%DvgvTh56k) zBBHU#YsBYXmffMg99APRNANH9MM@)aB;i0jGHJB>TZMbTb_qd7@&qHNXWE!VWGRiR z0_}tt=|kDcas!X&Q9+`z8R>`@IovVdw8zYK{d4(|$K`H?k;;PRyxGb~>k1D$A#-f7 zAwqht(_*6$<{c)y--0r)s?Zhp#_MvkQ1-aoh`ZZ^gyZ|_?01MT%!V4alx(DQ!{aYN zzO+kMo%jH0AzlpXsu43Q2roGreBKYeN;TrFV2qv)##C}5k2qh))EJvheu6B6I`#Aw z*QdR(iq;gZc~@OOao{vP@Wvs`o(0;5#~>4O)yGXED((+Gm!)Se}M5x?(MUDyQ zcn_!m7Eu}S^lm)83><8(FGt%1tgmDoh=zu!9ZaH=Fn$~$8>n>XY*gJo$^XoFa~R&N zRbo+Js;45y;sv$=)Vu_!3tM#DVGzTLVc$^cLnF$1m0rECz={&aguXWx-9b@NWD;qNS1V2t_UJ7RI&mo!eEh8DTP;qjBg;zx%nb?a(o@XfZjU*SyV0{$!e5NR`+`n zN?(>Y1huvfLU|fXD*LXgu_gH-V>63IB&kog4bw*cqWrg8MxK1Q`m87z##qxJVFX%-+M#y0@pp>MSG&cj7{fGS5ZdA^YMzpruYe4$PNWNm$jp-h~pr8Sc& zn6Gh01X1AEMjqig(B1!h7kIfv#*K*92(FQ}_(n-a!%QigV_Hj?Xx})|G$~4&9jxnI zca2HBKiir#FGANkl$k&OK*Cy3yWq9Vctam0O@TZcc92!fohIZ*`ewoJ7rZ(DBywE< zb8cwBoItku3dUhVJiMxDZhT>N+?cgB3T0w7>O>=Q0+F>2{6ND6g`~~6SIB}Ch_F(z z#6yCvs1oSCMU>;uHtkYTZOisp%jbKF?266I!qTd-XhhzKX%+TzpB3dXmf+W?bXOtD z`nL^K&mh#JW6lS%ZKH=3EYP)<@U{hH4McXqh)TvK!dmZN^5{;-{8D`V9wF6J1sw|H zHQ@gDE}$B&_t03vuN^gUOdaggv2zfgsV4BoxL;ffg~O{T=GHUmWbZ`zWi`uRpZeD0 zkcT8;HVWob&k*ZoB++J{VfLU+SGiVZTs!Ra|0Pd^|1I`7)90^1SnSsJT!gH7FWOJ@ z`(ba36m=CaKJtU0bx1npqmXDJCWX2h`)FcN$B86yF|eR=k;}dd<%u@hB}P~3MX?Z` zS;7hDAn~Fyp@uE%CYz#4otOgc)*x{2+ZRo1iHa=E$(*{RC6Z?=`OiVqs?^ch!70u> z=o=yox`enz$s#yz!`Q>h}H6|(#Ta(Det2{>JoMIM-7ah*Y^`sQ0i~O$SVS6lkYDH$Z zyzNMF`w9nn_5b(p-naO_e)qRG-j(UQX40x)k36x&!_qgOn6`LhPNru};dn85*nErH zZ+x-;QTihBdM0`co4Q8ORk7FZ~WrrG`}J*MkvBZIRewGF0SL*_X5! zc*OU@xHbK#_R9aLebv9!PPEO<+Fk!^?OKmgJuKi%x4ib>dYZ1@kIU|se|_T#jSD14 zh|ssm+Gd z&4%&TLUvnx3lmXLm?7F*_{eT~dZ!y>(hVCv*g|LLPJITeQvbOiVn!CW$Sb?Q44iJ( zRltk?|FHJv@lBO!-}rr&ENw%Zwk)MBkTwNEgOIkUNL51G9##iLaRHraL7bF|6i}?V zkOC?dXDoJbx`=={4v0w8f>MDg4m#sHPr@=&s7sY4(z=`gC8tfB{Jz&oi|F$_@B9Ao zO5JA<(~0r>hL#vi!#eNN77a4$a7`o0?e4ZXQs4H zq8N2^i1(t%bXbQeK#cjqdk{9ooaY|tKd=<%IA%HSv8s|q(VJVA2LzDZQrXAfG7qxG z@A1YAY_d+O4KbmJo(`3a@J&j};&PI`V(0o5yqu;uQcoy_Sy3KSK9|N2mGMqa5823y z>^qfoTat8Y2-ZaQbS7p_Z@B1%7yV_|9F}&O_U2TLlLH>=bm#2$2cltNSQtS?g>_-+ zwB~tAv>kSYN4`|XwoPj;x(B*gNQ0yQ^||^Nf$L#qpoiCvu!Uu$mX0lbp=_XRiW}sa zvHFP8X5!$v8M9iDM*z#cH!EiK0vNEllwcMQ33wOQR49v?Y=@1RabHG1c}#0K`N`A$ zxiU*PKHZ;h8esZK;dypN z{Zg6e`cko1a+?_RX!kVbAlf6@{zm`#^9wN*Dn-ug4HyooPwOz!dCa^Krw5SvAK{=g z#cG3AgGC{J2xRlPI|K*#M+>x;G--qmU;%H5Z#jq;@O!ME3bMi4g0zIM4}`3n&wAI% z9E%{X%iyaKHvy$mX;g+-rA>?ECw^Nk0b1Fuu+~y7?QAhPBj`kK#4Y=ZhlAEdgWb=7 zajiA?w?pi!pY*wFk?vb~(v4;=a2xhZcJ`$YSe8b_R)PwQRW%mO zWN(0s$aGd+r_a_`;(UsejCB}!M97&T#nq{CG^A=ors&NYH*B-g1zR;6qE&>lHUqto zuiA{&hJ9z=taNAf2JQzhvsP+@ok1{7vzt6TBA6~nv2LzoCid)qkb=_A1@bm|B=Cs! z&ED({*b(C?saXW~ph_^GQ{!#;59vwPF$B0b6dN=FbQ4?w@WI(CN zyM$=S88``GO`eQ&DrRSSV88y=K~fZWnrtb0D_P^+6{dAWV@E6LbMVCdCwtR-bgGl| zmR6qzY3*TqlYqN^rlhIqAK#PRB&j9J_Z%=IHh^as0i4-G(O+IL-aD^7N|@>Sw<3dM zHNn4SJ}B&wu3brUr0K8^xwnAE9qSQezq^Bv*$sR%ApO?^YpRQ+ME?r0gW_(7#3RCr ziljf4R+HQWiLbdDvVy9DByUQGDB7f!{HcCiA39<*#eRzNzBW|qo`Vuz?KWGffRGqkxZ=O-GvLi+ik4>4?hpF3hT?@;i1 zWJfc!q~z*}Gflb|zfZ*PUx21ot`z-;VzgCvw?_-}JfOAlGgI@u+B(<|S7UD1VUF)Y zBsF<$uh`*Y9_2thEIT*dBk3?NvFKema6_oQ^xcjq z9eB-XD&OeQ`>%tZBF7|l9Od40%yc$RncPgV?L;7>Q$ICTRN_~?rS5z1w_9l>`hD*| zecD;{{i3qoN8_mYgjw+L$au=P0Au*`SeDM-1Rw6_>o>uh8`dkF0EDc(_Jl;e^-j;w zFL-PG7gbmhKrbE{p&JwX=msc30s2}H4y^Cx+C=L(@PHI)sq{r7yd0r5jK*q-iQ0qz zRXWB-QGR@K7N?1uDe5yJToc`TpqFB{)2~Ho0`M5=SHv!$H$>gkRM~CUw&D6;wc!v> zzyDs}fEmpE>$=9Nb?Ig-Ps@|-S8JlEACY=Y(#t-90k~-9)-=!llnu|<3!s`&Z&GY0 zl1+#&Q9BXc30?tx?fX5^RrCZ@Fx)feAoZo$I?$tO;jBrXEr^O8UGF*kcc1|iHzu?7 z@Wd2=M|IXV2>*X?`^mRn;koaDC`|9Z^KDT^!W;Q4Xz3`EDYFgy8JG2?db*#6EM*~h zn8}&612l+)W52Z1wF$ajVDRus9s#rT45$-PD8Xv*vUJO^c%o8-bD!P=7*-4%M+gO{5h-y5rqIaq7{#&9h!Z z-$&al66qP}oC*haK$5ZTpjb&cD5o0;zSpjtsc6x+UIB3wyR+62HY7jLMSsI4*J(5&0SNly6GfV6qg(4 z;U=hMW}j5PH;TYH(AsEzsTR7|zaFGGULNH;*p)-~Pf=f?{ZI8mDiySl-94n>geO+| zBF#OQCRVC*V5ZV(Bv2OpRm*xBx@MA{{zL6g|J?Vlho5JX`_)V?L_TKu#7phjiM&V2 zOH7^flgf8!^Ut&XsV^7Sh$$OZ?gvs1tSSf#WD~F_{-E%tqnCK#>}gFDH|>5EQC|4X zTo^ZU;LjA;=JvoxM6unQ8U>o&W1Nwy@$hdZ=5y)K+Xd)>xm`4CzX2CT7>I~5fS4fM z9Fvrjw*_Sriog~{W3%S{0sn7Nw6;V%X|YNd;-(ZvPiMY$Bt0EL`zFICYSJCM!PWer?)mnLMvJE;ld1a8d+|8joHEH^iHgo~u z4Yic`ka)CppiQ*#hygIzgjLcQ$ zQq%)G7IE#pG}6U*|CNW{qx?BYI|n#np@^N4-ErJU?O0zg@85zpjDFR;Q@@97_f|l^ zuTK-e!HVYjXbG3brH`s%FJ(J7OtsM}w+&O}GGsZ5Thn6}@Iaj0hB#qzE5(rE?UAmjDUAMC+m^+MC$8k!CUC6|niDcR|n> z*|QyYzW+BozahD*4g746vTsfm#%7*-`i$u__k*)GG!MdU+#)5arvf;_s8Pcxy2k;u zpgW5!n^kUi9?p?@J%NvVEN+}4iz&s?o-^>b7qerUfG{u@*aIuL8@cg#Uh$KcnF)d1 z1|N@L)_QQ@J%Khzw6Vt2S$q~FG}}Y{n2vt@8)KEE|5|yyKK5;l{^~Sv&7WV0-T%P_ z$Gn$;%J{*BX|Sb#KKwc2H&NV6CMV-G<4z~vcF;G#Z_!g^s@VN@iPDAxtHJ~7K3BwT zH1gG|oHl^b{jSvtt~O2WiwND?BDFNKi)V1;W|o##YRwFFr5(e$E3Wv1Pq=NPZ$9Ym zz4-Lg;7d0Fw`G6QG=r3M+mXN@gP}l1Po^^})AYq4Xhdp;!>`t#pv~&y*ZkEn)Z=z^ zc?afj5^r;Nfu`IL?KQZ#Y?hH(sGD{qunf7=AoWxZS3AB(bed(bO1MZnf5K0`51>zw z@%bK~HT%avdw96ZQV49E1uost^zF~V zc#MhQABfljd`?3bW7{0jUh<$wbj8d_M3e;XOK2`2NfO#NVg#t?0>L+6LIfGbxb8l&yh*q$=f_(5kG*5N|#R=is{$~_V2W+99- z;zAPVf)O4!LEVn;WSZ-l85$kGgAwVhpCMuyN3Q37Akj&CzrR@%bXI|2_j+250x{lT-tWyM;6E@nptl@bwPz6d4@3e&ipS?%_pL-7r zAMIkJlBTiI&(%XOmdR~M@kVMEyy*xD=l z$va^SWRK*@<-rMyek~_I)#&Td?v?P4u~F15*8CCLN?2$VnS>;N9_6!2#8pJT8K5o+ z^2YAOJjk!h2771VEy<1(=WurNDBB%DIc^f>lmlIJb~yv<*HJV$%fFCFMuY{5$Ki?*iSfQ ziLnE4I*8ySI9s9t+=?wR`VWd=8|v1NN%w@!tZlP-8Ri0g5Q)R!Zh#d+@RdTV zNMjWliNAtYkPN#gP+-CqcNG2#C|9xil`%GV%r!9EA}A~#Wr@*qu61d>yy${BqT z)=pCn_XM5|hSMgZ-nF116f=RDh^Hz2J{%_h2-T)*QwZ9?IpIn-c$^7B9bqf8%{_tD z$Vn|;-1iqT;Z3T?$h4r3^21LU?Ie3R$}s{d=fL}&5Ud$)?mdC|KvG#Vg`V<*Xk!*V zP3|iz7kn`!g3GB*)5D@Xg3&y~XFx5HQV#CfwS@kj>SplK(WhaCAwde`?{xGe8ZEvb zb3(R?J%Ol0kujt6-F-DgW8|;~kS8*JfrHqiv&nLDlT-`26WO2`#)_W)R%pYJZtE#V zc9iEv$dajL&xiVd4TNTpRVv=OLSc(mS^?|~PVe*T+!G7GRxZ2f7jxK-x|A0kKvk#j zDPi#5UeRn_{O?!pGo{O(^pFa#^!mFzR-`o(PJ1p(Zj~)d!ueX z+h824YZ*P$+$~-{19a8{Xp>jvelLbT_aUmY3w9G@(>#;Lp7&w}#DvrCK~G=mUW?JW zN1ELpy`(3wu{)eLqn*v5U!K9U7LiqY0r`wGFe66Iq5T`fvg6~nxJ-35nEAe*~J`dxw3?GsbZ;&{6 z>ka`WgiNp)d{uE2GSg4R&bAObz4!!_;s0hdLm6$d-nv z`S`FED|(VNRQ(oIIiXfBpu^zYA>merrT25%#Y7*zjqWY+0e(2?!xQ? z!)j?XJlmTK+CQ1cjmwl?Xii9r$GYB)_D#In%YNxD0;xu$@0Yu_AaW&?VfjC}|x7D{VT=Zy9&(NLU64@8iA(QZg)w}Vf$dsH1K z+PWF1LtYboOP!^*J)-?9f2WhJvOYdrHgOgy-w$qv^hN^B)hhHtY@XWw(cMz}2X{?E zYi7VM$b?8v(2b(RFA?vFRlYYg4OA{1xEa5JzxM*21aC*F@3QgE=i_F5F@|l| zn{=;1+LvZuKDmOdphkr5X)-@fn9V}n(MvHxW9P{=!<$7jH_g19>}ZxFAI0p_WC(Ao zdW4rrPY0~5kWj}Vh8t(<4O|!IZbseJDpzMF?F6FpC|=uh_0Tgw(@BwGILl)7zK&Sc zlF^+tvlabsfv}m^Mn%)z5u!l2)CqNlgLQ}5>^6%5ozuYA8J9`4VdVqXjW>$T3B~B)XDwRIp zN_6|F9!;GYnrNaUpZYQ%=Q9U-lO<{km9x3mgmx@o+(`kyGjndFjTo&BXk#Pxz$M6; z_xfm`()iGxb-i4Tvs9kKMm}2~1%2J;*f-Y%J`W<-YZVU&81@(TOLtL?R z9U^xN-K>+}xjm>Uh?gFS&Ts$l&ZW`eKupj?t9#`?z*l*EqtmZb%V9yzX6Nya{ljNV zFHqlI>E>+f`R{yVrIFG>pWr0BqW=9xceN9cgn;MF;jHvL9w#(P4{L_QngPBw#LI~) zC8BjXK()VI9iG$9L95Jjdav0@{Bb|eM7Bv*;1{zMs<#wI=qv1$@6|H1V86;Nr;r0@ z+JoslZT??x!y*wdgX&{ceRAt~D=A%<@smeX&iLyrs}5oVe1>7WQ((U5yok~C4AG#`>s zE{iiq)lfU%ECwwuxV81hvH^a8%OXzj5SiDJXYB@j9{0NQCrAVQ#U3VTrq6pH&NdQD!Nn}J8n~44UIv3>lS6aZ4Da;=~_y= z5EpJRmn9)`38y=z6CmfAae`TodC>O>liRqQxlFVkYsmKA)JAVM`wV_gh_+f6{`kc* zd6r(Bpz&wK!be@F@fWhVG-lq}q0y&12P$G+x*X=Cy_f3#Mo{O{d z)5+FlLYTvuu7RAv86ocIHzRO{`hI7DWLWr@J~k7&X79)q14qPdz|jJQ$i zWq&Vc*7!Yagp|HU*Uew#G9~oKaH?pHqzp{mwe)!hJI*z|xp=cO%aok=4E|rho^^vn$A+p(ut&7_tWJKY%t=DV(J7Y8*M>Vh)Sc0>aXLX6~`nY7z0$3Hmjs@D)Nr)X& z9waLluI`}Yah%pM$5#%A2Q)VWE53CodE`ZI3&Jt(=GpD{=)XADZ~dO6`Nij$_&nf6S*4tKwSlCX(1h-ACKD#%)^%94&J<2L0zN^%Ib|{ra zc37_`ktf?p__e(T|F?)WR^%4=e=CVN)&GA=qW=BAl|-{f$o}+~q$^Y4>ucPXTH--( z^tf@5HLo6Tjr;J%o-owl%qX#==Rg0ZmH;$=pdP>`0jV;o;Y%-aW zk{7}o5#yfC(x*H`at@-0deXivCQcQyGFG}j{x_gaMJx?k@&d%^UBRt_Jry(n`U2hQ zxwx-#+@g*=fm=auxv`I87wF(qfcL`UjslH<)cS(b!xL~`lFx2TG*_Be8e@oxU03=( zD2}j%zlqP?2(O{M3VR8Vks$X2TF@n(`T13!u7dZEm1|BgL_eAkJ>yZFjs6&pr61?= z+UE*Jg|p7>UUzAO<8(u(*9b2oUX@_YcgIWd3YR;=8h>gMw7iLBw%nG6C<}jz=!CY7 z$4$P=D_E1HD_^8RA_io%GFTPfO#W7cFpz{uCCqFsB;OonRXoXMEVI*7zTxzSO~3`c zrwo|7VI%Yw9pD!_KsV&6Y_vyVkQ5oD7av_$%&bg)*>w~a(WAj>jvJXQ<+brQx)3|V zXlq$8t&whk8ZYZK-BH)j*vXd3$L$yQ$J^WWc3yrb7`A-q4@VC;^`4)LOE|S?C?DPO zXzioCd00L(wfYW7w@9abHaFdD2Y40@t$vg#9X!ya^?=J;7Q791+x73mKj&(nYTb9rB6?uF8b#( zy*#pOPUAjAdf(B>Ry1@LE*sWT^u#Im{Nc&=nZsl3iFt#(XDaFtfe}cU*0`2^z#L!X z-lrtm)>ySIKv2X;JQAw!bWP@tpLss73FVufaTuC;lVF;tM#rSMn}5aL;l_|uvd>oM zVg+pJH#+PWpK%Eoh3Ou_`a2J2eag#q+^%0&@&1wn!AqBh=96SeE0rDGQ87s7G~blK z9wW*m;kS!IFkwSnbFd*Eu|&Wj@wr@TvmnbtVjhP#7||tXN_SK=^_6lLN-1pGQPEa{ zHZ$^`6Qcneyu_l|m;8etYIy}Y^WOv$|TDQPPZ z@gS7;X8JlMoHp0|ZAoJ39i>HPZ)sKOw*pq$mJ}(uq7_U zJN(GStn#h!Ga6-QV-X{u{N_T99MYJ}O5C)HhxER$)1MgTHp-%gd7`loR6DEZm|1Qx zmlgu$T;Kb59rGua4{+z{emsGQ$UL7$Ip-o$pWe-lJm=e1v8|J>Y{Y5|wB!vJ9GAUk z5Ida7pSuW?0kI83yu~j6+jXX7nIv?cfgD1(sD)>R5fMPI>zxA6sai+C%SPReIJ;~N zd}Ume>!A>5VHfuy*FMEi*{H;rW)#@*+o|W%ngliWcw8?h6)jAD@`N&R+nnLAl~Vd$ zuHQZJ`*Jp($*;T0hEH_Z5?K<;qn+XF zX;fm590F})@-r?j$cG-xUzK0)c>1_2<$we7)r*fGzirxD$EuduS2|1AVn00Z5=Qgc zhK^`${#ta#T)JCIO_lLUgNO7Llg%|{S307+g1T)K&9TPhnB|jM1fu8@QahhD6sH z7f?WpVtl+jxTkFqv#hy%-QM_-f=@7UH;CibT)t(mXwiBTeIg=!#P8)SYpzV*J7DCc z^F;BxJ*D~lc+;ZU?GBdG>`DSUGtWzW#~gl>FJ|O7=M5vjJ}->?^1S^@=VR|VUTEe= zbEC;_M1c;dGaQbaJGW@cWjqU(nH$CC=bz7O=kv&0mm5PUA_l4Ol(3}6asM%*F*eYc zDWEZT5slFhjoIHv9j5jgRn4?=*j=$K)h6}t)JrAr2L1l~@4pn;|Ij~ocpEF;jZl!s zE|s+0e@0i(&P=nKS=5nBG3={fQH&JiIOctTxhwWX=tcgelB5Nn{)$fMk*e8SU-3F4 zj9!MwzjBEQ^ycHB3G`u~k25s@sevTL@e((-06lue;W##-WXnQD<__#kNDWw3v}zvj{M%-Q`vNPQ>5v+Pneq<{5sxx0%xe24=yf_jM9sKc3j(Ev%oc#UY z2<4^1;{tVmeky0lI>#rc&ZhjN;*f{y zb0usP<+yN)Gru=^6AXt0iW z)eFXv%6U1&OU=b-CA1JVIqNAZHeoD@*YaR3v}65XHsQGebO3NfJ{3EBaS{Bty0;Y_ z#N3Q3dMC1*9}JcZ&p%&u%%?MXpy`~MN-Y;9jZ<5kH{BJLjGe6Z%vflvF^L*6ZQG0QLHV3Ivrl`^7vEUM#y{=3xcc09#LyOYEEhaR#3hV_Q%WQ!yMHS+Xgjij1DqoqS3}W0SP8LIa?OI0)l zX4Vv(@#d6KWTa#_t3x{5q9$(g@4BDsik!da;G*{*IL|ISOb0V2t?D$E{C-u zET*5uXsPAShSlKTrb{tp6%v=#pKpZq1z3lR8QgfA_aJhPvu5M=qxGA3t1`s46{pX* zG*cRv6JzCZgSg_&mrby;uSI%Z+@_n{`NZBNnTtzm&X|yd6&8^dKyPtcdpmB_J%QQ$; z%{qA&e4074()1VH-YOb~jv3xEP3HARK<2iex_9{V1;TNvvsQY+r|pR5STy*bXzrDA zcwo_Y`iAE@Px&a-??zts{l#CTmh)<}>6U5O>3bSr516TM)|f{5=A(BRdmA5b)n0#+ z)nygeRp3TIH;8zwv67+rtS+exzN^9d!W<_Xji{3K*jF(W&CIZu`o_t(!Mb**dj$9% zVefdnKl=84`2Wb;Yb;_5Cq+%u$!8&-504i-m~T6G<5xbdsr|gh^xb*Z`~mJw75KRD zq4i&>SnJ6mKSkcAvCEY%oin!>yU-Nv=79cvWa|XY+!{>m+I?_gD<^5djA-^fkOP(gO$3NhIAg&f`T^BVCR!E?*FRqY|`&>Zor*!}5DM#$j zCchZ>gXc%c`?XOW;5;vQ(fj%onI{}g zc}V0c^u5JAIPhbTQochLS7kO>rWnFp z<#zZah66*@GIpVi3k_T?&^-+QQH*W#SI)+r^=oD4w$m$S3{P5V2=W)#OVz7dR@n=Z zAy0=)-jLLMtCGNkj;I*4E?ZEd*z9)W7?N(;r9;3K*{E=pKCiui_aL1qx@n4R=Z4}o zOO!YW9)612n>C2T$K7(kORWh3y_4Fx=JLY5ru6Aj8%~UeyU!F0|4J&nx3HlsYeVgd zq2+X@d|OO0y%Se*=3wbe@Y@mktK@}8Yv~koi6d`DWXSO2?qW7(vB!>*|Dsa^In6m5zp9hzGGm|Hw?nRf5J+8a*_alnqF0QH*oUu;u}lN*k9uGmlFfj@4pQ3`e3G!`5LfBbCG>MWBRS_JB;kmoJ+O zETJ@a*lKKH>G;hAkE{lw^2ob6Hhq9@B6Ia8&9|I=B7|!Qs{s~ z@RFQ{ESzvwP}cMNq?~y)N_tac1z2t4S&%M2#mvPfSf)x^iK~cA#0C zF<4HGd&I-WPV?zF5jraBh3Xxn>Zs#(ooJiXE@yb@Dm z0_`g5GQpN#D}A~G{t7jrz-zrRb?8A_@?-NKt9|VBV?y1j(&3F+l30Bl9$&b3BD$`< zX}Tm-hXQYgxLXBij(=NJ>9)fjcSx_nO_E1dPX?(y*%~Lx@p0q4rMi1gWa-{LA-pnC z7gMvNgkrthb;S#<3#knwx^@-{5(l|j2*_98ZfMeKr8mJJG5)*4v4!IwS@KAG(UPJ8 zCs@X(;OakCZJ1AMvrfFD`}fH&bSF;gG7zlrd{Iu{imOa3!%H$$YYZlQW%@w$qJ;3dBhjE9!}x86K(xiNNDHOT8|+UDde z(JlO_2OqKZaY5Tvu6DC>(U+GhcsVoo>Y_(v-?pMJ6+=C7jiJC{+*{6hSlzIsgTR$O zh}fkEm1t9)!ZF-dyIGxpb8nMlinO8mHF!y9O5`BFfDYqDifJKWln+a z7|JlUDmNSXAaiM9vy1O@qJx^wQ|RTIl%4t3eCqEaAj&N&`m^s`kx;#;_?e1tiW`HS z7d63zoF9@8qo=o^UA4Gxz5)KRsJOPGwZt9a_v15Rbv!g*3)|w8KHaE|xZQHQG)TP- zE4%RE*e3_dcG2QN^upknB|3@2I$MPmtu#Hn29KtKRp>RuM z2hC5`>g@i*u0^EPjk$!re3i$>F?px!*E9E|cWMPUInJKqU5j}`1TKm0A7$9bOy1<; zEIKbgaxvoEMY!4R#Xn&N!QFx9*x`gcrDRj&V(8VmkOghsYt#Tb68u^ zQo;wha;^M(*qeM^D%5SQhzaTf-~?i59xTwRdhm+sMNZp9`Z$-Kn zkgiaj_4?pNKW@Kl60A*=FzX}G9<+i#UHYDn;(6N<&zn54CtohvODDJzdeEmE`2Y~W zND}G?Z$#c6s6EJ+~ySTb$o>evQi@Xm%$;K6WJAgGS9?N_(0Vln_ITidy z32x&1ARB3SmK0Yx%9}Hv>?m}=k1O*LuJ)yh#z@<8nAHIt{yIle^LItJv~jg}SG-hl zcizfIE_dPZ#$dqT07>2Rj#0-~J9ZT(+RrAJ6tOWUJck@5#fDS4`LB2A^cH7HkrnG^ z%*hZizCa-lauVbg)#m(?qIHn}s5~R0FEx2Wf%Dzd+{XOy`bUSnl^Xg&SKTK?gn?8bW*6hP@Gg2 z3fvn4Ev{p*+-cpg+Z779&>O@>lE-w5V|AW>`c9{s1}JKpj4PmuS3ngnW5zhp!cgE) z_q8(x+cb5i-0S{&Zryw`~BfP0_ZdLN?2)qtj@=;Ty2wx8f)=~goDTSWH?1$KAY1R$yQHWcc?fIF{(P5VEY1*RXBBuA06DiusC%{d(=$@(b=zx?1uOV1k zr_v3eT-X{@GZAYy*pnn1`@jEqjaV0d#0rHs7Nj`;<^9UX!*WxGRjO5aOJ3y&%~)3|g2_t9j~`O_ud61ZiU(+vd2D9=fW<( z*f*vI@KNX0I|h^g*f*vI%<*M){>m_(3&3e^i+kLgSSP-Perge`CI>xDO_v%S!ruo< zSGGB z%yGVVM9l#IFkyiII>z}2@F;)gItLK?W_Ed#HgKbiuDY_S(k)7gO?c(B_MT?qRX$yD zzVy@8t=eHHV{4O9?of{jEA@>a$qjqSD?rjW()lK;G?`{=ladCxkj9&g+2{?N4hwZ( zl~P|Du|}X-!kYXZX<>0G{|+|i8GY1>&*=2U^ig8saUynY23W#JRYvV(FZBWoj>foT z-JhYmGr{MA?kpcR{Angi8mpd`a&DM3bN8*w!~YA`EpoeJv|X@Qn1C$T*$&hK4Rb^@9* z*ziG@%-3yz6(nD!$FEf0uw&4uB^(G|yu=3+*D-4n$*#-JJKq5sMMBHU;W;>+*{G(1 zcY|G*v{f;u;&F5P*&2`1!ee3-5U=at^|!6DNn0lbqhuz3s;&?p?tz9kIkb{i(lF=> zSRJ$Og!Lqo4~1U$u~&ZdCChq2R~6?q)J?X=o|>FHd9{+{#* g-CFv#aP92&p*P1( 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 z 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 Z>)XO1XK#%hn*Y({ - 0x02, 0x4d, 0x59, 0x65, 0x4e, 0xca, 0x26, 0xf4, 0xfa, 0x21, 0xb7, 0x29, 0x26, 0x81, 0xf6, 0x9f, - 0xdb, 0xd2, 0x3f, 0x3a, 0x82, 0xb7, 0xbc, 0x8f, 0x1f, 0x66, 0xbf, 0xf9, 0x64, 0x84, 0x2c, 0xa8, + 0xd5, 0x70, 0x15, 0x4c, 0x5a, 0x2d, 0x46, 0x72, 0x46, 0x01, 0xa1, 0x40, 0x3d, 0xcb, 0x0e, 0xe8, + 0x7a, 0x15, 0x0c, 0x2c, 0xb5, 0x34, 0xa3, 0xec, 0x6e, 0x53, 0x02, 0xff, 0x9f, 0x2e, 0x1a, 0xa1, }); auto actual_sha256 = file.getSHA256(); diff --git a/libs/FirmwareKit/tests/FirmwareKit_test.cpp b/libs/FirmwareKit/tests/FirmwareKit_test.cpp index 657f0d3338..c9bf60b68e 100644 --- a/libs/FirmwareKit/tests/FirmwareKit_test.cpp +++ b/libs/FirmwareKit/tests/FirmwareKit_test.cpp @@ -24,8 +24,19 @@ class FirmwareKitTest : public ::testing::Test protected: FirmwareKitTest() = default; - // void SetUp() override {} - // void TearDown() override {} + void SetUp() override + { + auto dummy_version = Version {1, 0, 0}; + std::string bin_dummy_version_path = "/tmp/LekaOS-1.0.0.bin"; + + auto k_minimal_expected_file_size = 300'000; + std::ofstream update_stream {bin_dummy_version_path.c_str(), std::ios::binary}; + for (auto i = 0; i < k_minimal_expected_file_size; i++) { + update_stream << static_cast(i); + } + update_stream.close(); + } + void TearDown() override { std::filesystem::remove("/tmp/LekaOS-1.0.0.bin"); } Version current_version = Version { semver::version {OS_VERSION}.major, @@ -35,7 +46,7 @@ class FirmwareKitTest : public ::testing::Test mock::FlashMemory mock_flash {}; FirmwareKit::Config config = { - .bin_path_format = "fs/usr/os/LekaOS-%i.%i.%i.bin", + .bin_path_format = "/tmp/LekaOS-%i.%i.%i.bin", .factory_path = "fs/usr/os/LekaOS-factory.bin", }; @@ -99,9 +110,6 @@ TEST_F(FirmwareKitTest, isVersionAvailableFileNotFound) TEST_F(FirmwareKitTest, isVersionAvailableFileTooSmall) { - auto _config = FirmwareKit::Config {.bin_path_format = "/tmp/LekaOS-%i.%i.%i.bin"}; - auto _firmwarekit = FirmwareKit {mock_flash, _config}; - auto dummy_version = Version {99, 99, 9999}; std::string bin_dummy_version_path = "/tmp/LekaOS-99.99.9999.bin"; @@ -112,7 +120,7 @@ TEST_F(FirmwareKitTest, isVersionAvailableFileTooSmall) } update_stream.close(); - auto is_version_available = _firmwarekit.isVersionAvailable(dummy_version); + auto is_version_available = firmwarekit.isVersionAvailable(dummy_version); EXPECT_FALSE(is_version_available); From 1ab268a1a9633375aac0288af5ffc43fea32b6d7 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Thu, 19 Jan 2023 18:04:03 +0100 Subject: [PATCH 040/143] :construction_worker: (release): Check version is correct --- .github/actions/check_version/action.yml | 69 ++++++++++++++ .../check_versions_are_all_identical.sh | 23 +++++ .../check_version/compare_with_file_name.py | 46 ++++++++++ .../check_version/compare_with_os_version.py | 62 +++++++++++++ .../actions/check_version/generate_report.sh | 49 ++++++++++ .../get_version_from_firmware_hex.py | 92 +++++++++++++++++++ .../check_version/get_version_from_os_bin.py | 54 +++++++++++ .github/actions/check_version/utils.py | 25 +++++ .github/workflows/ci-create_release.yml | 11 +++ 9 files changed, 431 insertions(+) create mode 100644 .github/actions/check_version/action.yml create mode 100755 .github/actions/check_version/check_versions_are_all_identical.sh create mode 100755 .github/actions/check_version/compare_with_file_name.py create mode 100755 .github/actions/check_version/compare_with_os_version.py create mode 100755 .github/actions/check_version/generate_report.sh create mode 100755 .github/actions/check_version/get_version_from_firmware_hex.py create mode 100755 .github/actions/check_version/get_version_from_os_bin.py create mode 100755 .github/actions/check_version/utils.py diff --git a/.github/actions/check_version/action.yml b/.github/actions/check_version/action.yml new file mode 100644 index 0000000000..588de253df --- /dev/null +++ b/.github/actions/check_version/action.yml @@ -0,0 +1,69 @@ +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: "Get os/firmware version" +description: "" + +inputs: + firmware_hex_path: + description: "Path to Firmware hex file" + required: true + + os_bin_path: + description: "Path to LekaOS bin file" + required: true + +runs: + using: "composite" + steps: + - name: Get version from firmware hex + id: get_version_from_firmware_hex + shell: bash + run: | + python ${{ github.action_path }}/get_version_from_firmware_hex.py ${{ inputs.firmware_hex_path }} + + - name: Get version from OS bin + id: get_version_from_os_bin + shell: bash + run: | + python ${{ github.action_path }}/get_version_from_os_bin.py ${{ inputs.os_bin_path }} + + - name: Compare firmware version with firmware file + id: compare_firmware_version_with_firmware_file + shell: bash + run: | + python ${{ github.action_path }}/compare_with_file_name.py ${{ inputs.firmware_hex_path }} ${{ env.FIRMWARE_VERSION_FILE }} + + - name: Compare OS version with OS file + id: compare_os_version_with_os_file + shell: bash + run: | + python ${{ github.action_path }}/compare_with_file_name.py ${{ inputs.os_bin_path }} ${{ env.OS_VERSION_FILE }} + + - name: Compare versions from files with os_version + id: compare_versions_from_files_with_os_version + shell: bash + run: | + python ${{ github.action_path }}/compare_with_os_version.py config/os_version ${{ env.FIRMWARE_VERSION_FILE }} ${{ env.OS_VERSION_FILE }} + + - name: Generate version comparison report + id: generate_version_comparison_report + shell: bash + run: | + ${{ github.action_path }}/generate_report.sh + + - name: Publish version + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: publish_version + message: | + # Version comparison + + ${{ env.VERSION_COMPARISON_OUTPUT }} + + - name: Check versions are all identical + id: check_versions_are_all_identical + shell: bash + run: | + ${{ github.action_path }}/check_versions_are_all_identical.sh diff --git a/.github/actions/check_version/check_versions_are_all_identical.sh b/.github/actions/check_version/check_versions_are_all_identical.sh new file mode 100755 index 0000000000..ecb0826706 --- /dev/null +++ b/.github/actions/check_version/check_versions_are_all_identical.sh @@ -0,0 +1,23 @@ +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +shopt -s xpg_echo + +if [ $OS_VERSION_FILE_ARE_SAME != "True" ]; then + exit 1 +fi + +if [ $OS_VERSION_ARE_SAME != "True" ]; then + exit 1 +fi + +if [ $FIRMWARE_VERSION_FILE_ARE_SAME != "True" ]; then + exit 1 +fi + +if [ $FIRMWARE_VERSION_ARE_SAME != "True" ]; then + exit 1 +fi + +exit 0 diff --git a/.github/actions/check_version/compare_with_file_name.py b/.github/actions/check_version/compare_with_file_name.py new file mode 100755 index 0000000000..9f3eb38d62 --- /dev/null +++ b/.github/actions/check_version/compare_with_file_name.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys +import argparse +import utils + + +# +# MARK: - argparse +# + + +parser = argparse.ArgumentParser( + description="Check filename contains version number" +) + +parser.add_argument("file_path", type=str, + help="path of file to compare with version") +parser.add_argument("version", type=str, help="version to compare with") + +args = parser.parse_args() + + +# +# MARK: - Main +# + + +def main(): + are_version_same: bool = args.version in args.file_path + + if "Firmware" in args.file_path: + utils.addToEnvironement( + f"FIRMWARE_VERSION_FILE_ARE_SAME={are_version_same}") + elif "LekaOS" in args.file_path: + utils.addToEnvironement(f"OS_VERSION_FILE_ARE_SAME={are_version_same}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/actions/check_version/compare_with_os_version.py b/.github/actions/check_version/compare_with_os_version.py new file mode 100755 index 0000000000..65a5d8b97a --- /dev/null +++ b/.github/actions/check_version/compare_with_os_version.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys +import argparse +import utils + + +# +# MARK: - argparse +# + + +parser = argparse.ArgumentParser( + description="Check os & firmware version are equal to os_version config" +) + +parser.add_argument( + "version_config_path", type=str, help="version config filepath" +) +parser.add_argument("firmware_version", type=str, help="firmware_version") +parser.add_argument("os_version", type=str, help="os_version") + +args = parser.parse_args() + + +# +# MARK: - Functions +# + + +def checkVersionsAreEquals(config_version: str, actual_version: str) -> bool: + return config_version in actual_version + + +# +# MARK: - Main +# + + +def main(): + config_version = utils.getDataFromFile( + args.version_config_path).replace("\n", "") + + is_firmware_version_correct = checkVersionsAreEquals( + config_version, args.firmware_version) + + is_os_version_correct = checkVersionsAreEquals( + config_version, args.os_version) + + utils.addToEnvironement( + f"FIRMWARE_VERSION_ARE_SAME={is_firmware_version_correct}") + utils.addToEnvironement(f"OS_VERSION_ARE_SAME={is_os_version_correct}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/actions/check_version/generate_report.sh b/.github/actions/check_version/generate_report.sh new file mode 100755 index 0000000000..3ef258494a --- /dev/null +++ b/.github/actions/check_version/generate_report.sh @@ -0,0 +1,49 @@ +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +shopt -s xpg_echo + +OUTPUT_OS_VERSION="`(echo $OS_VERSION_FILE)`" +OUTPUT_FIRMWARE_VERSION="`(echo $FIRMWARE_VERSION_FILE)`" + +if [ $OS_VERSION_FILE_ARE_SAME = "True" ]; then + OUTPUT_OS_VERSION_SAME_AS_FILE="✔️" +else + OUTPUT_OS_VERSION_SAME_AS_FILE="❌" +fi + +if [ $OS_VERSION_ARE_SAME = "True" ]; then + OUTPUT_OS_VERSION_SAME_AS_OS_VERSION_CONFIG="✔️" +else + OUTPUT_OS_VERSION_SAME_AS_OS_VERSION_CONFIG="❌" +fi + +if [ $FIRMWARE_VERSION_FILE_ARE_SAME = "True" ]; then + OUTPUT_FIRMWARE_VERSION_SAME_AS_FILE="✔️" +else + OUTPUT_FIRMWARE_VERSION_SAME_AS_FILE="❌" +fi + +if [ $FIRMWARE_VERSION_ARE_SAME = "True" ]; then + OUTPUT_FIRMWARE_VERSION_SAME_AS_OS_VERSION_CONFIG="✔️" +else + OUTPUT_FIRMWARE_VERSION_SAME_AS_OS_VERSION_CONFIG="❌" +fi + +# +# MARK: - markdown output +# + +echo "Creating markdown output" + +echo 'VERSION_COMPARISON_OUTPUT<> $GITHUB_ENV + +echo -n "| - | Version | Same as filename | Same as os_version |\n" >> $GITHUB_ENV +echo -n "|:---------------------------------:|:--------------------------:|:----------------:|:--------------------:|\n" >> $GITHUB_ENV + +echo -n "| **os** |\`$OUTPUT_OS_VERSION\` |$OUTPUT_OS_VERSION_SAME_AS_FILE |$OUTPUT_OS_VERSION_SAME_AS_OS_VERSION_CONFIG |\n" >> $GITHUB_ENV +echo -n "| **firmware**
(os + bootloader) |\`$OUTPUT_FIRMWARE_VERSION\`|$OUTPUT_FIRMWARE_VERSION_SAME_AS_FILE|$OUTPUT_FIRMWARE_VERSION_SAME_AS_OS_VERSION_CONFIG|\n" >> $GITHUB_ENV + + +echo 'EOF_VERSION_COMPARISON_OUTPUT' >> $GITHUB_ENV diff --git a/.github/actions/check_version/get_version_from_firmware_hex.py b/.github/actions/check_version/get_version_from_firmware_hex.py new file mode 100755 index 0000000000..0f58fc3d2c --- /dev/null +++ b/.github/actions/check_version/get_version_from_firmware_hex.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys +import argparse +import utils + +# +# MARK: - argparse +# + + +parser = argparse.ArgumentParser( + description="Get version from firmware hex file") + +parser.add_argument("firmware_hex_path", type=str, + help="path of firmware hex file") + +args = parser.parse_args() + + +# +# MARK: - Functions +# + + +def getLineOfVersion() -> str: + START_OS_MARKER = ":020000040804EE\n" + + firmware_raw = utils.getDataFromFile(args.firmware_hex_path) + + pos_os_start = firmware_raw.find(START_OS_MARKER) + pos_line_previous_version_line = firmware_raw.find( + "\n", pos_os_start + len(START_OS_MARKER) + 1 + ) + pos_version_start_line = firmware_raw.find( + "\n", pos_line_previous_version_line) + pos_version_end_line = firmware_raw.find("\n", pos_version_start_line + 1) + + return firmware_raw[pos_version_start_line:pos_version_end_line] + + +def getFirmwareVersionRaw() -> str: + START_VERSION_MARKER = 18 + VERSION_SIZE = 16 + + version_line = getLineOfVersion() + + version_raw = version_line[ + START_VERSION_MARKER: START_VERSION_MARKER + VERSION_SIZE + ] + + return version_raw + + +def getFirmwareVersion() -> str: + raw = getFirmwareVersionRaw() + + major = int(raw[0:2], 16) + minor = int(raw[2:4], 16) + revision = int( + raw[6:8] + raw[4:6], 16 + ) + build = int( + raw[14:16] + + raw[12:14] + + raw[10:12] + + raw[8:10], + 16, + ) + + version = f"{major}.{minor}.{revision}+{build}" + return version + + +# +# MARK: - Main +# + + +def main(): + firmware_version = getFirmwareVersion() + utils.addToEnvironement(f"FIRMWARE_VERSION_FILE={firmware_version}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/actions/check_version/get_version_from_os_bin.py b/.github/actions/check_version/get_version_from_os_bin.py new file mode 100755 index 0000000000..2f975a8e04 --- /dev/null +++ b/.github/actions/check_version/get_version_from_os_bin.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys +import argparse +import struct +import utils + +# +# MARK: - argparse +# + + +parser = argparse.ArgumentParser(description="Get version from os bin file") + +parser.add_argument("os_bin_path", type=str, help="path of os bin file") + +args = parser.parse_args() + + +# +# MARK: - Functions +# + + +def getOSVersion() -> str: + raw = utils.getDataFromFile(args.os_bin_path, mode="rb")[20:28] + + major = raw[0] + minor = raw[1] + revision = struct.unpack("h", raw[2:4])[0] + build = struct.unpack("I", raw[4:8])[0] + + version = f"{major}.{minor}.{revision}+{build}" + return version + + +# +# MARK: - Main +# + + +def main(): + os_version = getOSVersion() + utils.addToEnvironement(f"OS_VERSION_FILE={os_version}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/actions/check_version/utils.py b/.github/actions/check_version/utils.py new file mode 100755 index 0000000000..a4ca93a96a --- /dev/null +++ b/.github/actions/check_version/utils.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import os +import sys + + +def addToEnvironement(data: str): + env_file = os.getenv("GITHUB_ENV") + with open(env_file, "a") as env: + env.write(data + "\n") + + +def getDataFromFile(file_path: str, mode="r"): + try: + f = open(file_path, mode) + data = f.read() + f.close() + return data + except BaseException as error: + print(f"{error}") + sys.exit(1) diff --git a/.github/workflows/ci-create_release.yml b/.github/workflows/ci-create_release.yml index 807d7960cb..bd59787532 100644 --- a/.github/workflows/ci-create_release.yml +++ b/.github/workflows/ci-create_release.yml @@ -72,3 +72,14 @@ jobs: path: | _release/Firmware-*.hex _release/LekaOS-*.bin + + # + # Mark: - Check versions from files and os_version are identical + # + + - name: Check os/firmware version + id: check_os_firmware_version + uses: ./.github/actions/check_version + with: + firmware_hex_path: _release/Firmware-*.hex + os_bin_path: _release/LekaOS-*.bin From eecfe7a35a2a61d7cd1e8f17f4fc631b43b4821f Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 16 Jan 2023 13:49:51 +0100 Subject: [PATCH 041/143] :truck: (config): Rename Config.cpp into ConfigKit.cpp --- libs/ConfigKit/CMakeLists.txt | 2 +- libs/ConfigKit/source/{Config.cpp => ConfigKit.cpp} | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename libs/ConfigKit/source/{Config.cpp => ConfigKit.cpp} (99%) diff --git a/libs/ConfigKit/CMakeLists.txt b/libs/ConfigKit/CMakeLists.txt index 427c94e420..cecee17a73 100644 --- a/libs/ConfigKit/CMakeLists.txt +++ b/libs/ConfigKit/CMakeLists.txt @@ -11,7 +11,7 @@ target_include_directories(ConfigKit target_sources(ConfigKit PRIVATE - source/Config.cpp + source/ConfigKit.cpp ) target_link_libraries(ConfigKit diff --git a/libs/ConfigKit/source/Config.cpp b/libs/ConfigKit/source/ConfigKit.cpp similarity index 99% rename from libs/ConfigKit/source/Config.cpp rename to libs/ConfigKit/source/ConfigKit.cpp index 0bf6216fc0..686e3cb1fd 100644 --- a/libs/ConfigKit/source/Config.cpp +++ b/libs/ConfigKit/source/ConfigKit.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "ConfigKit.h" + #include "FileManagerKit.h" using namespace leka; From 119ab50264cd216c8319c488baa98eda6fd87461 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 16 Jan 2023 13:15:36 +0100 Subject: [PATCH 042/143] :sparkles: (config): Use Config with multiple bytes --- app/bootloader/main.cpp | 6 +- libs/ConfigKit/include/Config.h | 11 +- libs/ConfigKit/include/ConfigKit.h | 35 ++++- libs/ConfigKit/include/ConfigList.h | 2 +- libs/ConfigKit/source/ConfigKit.cpp | 32 ---- libs/ConfigKit/tests/Config_test.cpp | 209 ++++++++++++++++++++------- spikes/lk_config_kit/main.cpp | 12 +- 7 files changed, 208 insertions(+), 99 deletions(-) diff --git a/app/bootloader/main.cpp b/app/bootloader/main.cpp index e908ad2c90..31a9d15630 100644 --- a/app/bootloader/main.cpp +++ b/app/bootloader/main.cpp @@ -158,10 +158,10 @@ namespace factory_reset { namespace config { - auto bootloader_version = Config {"/fs/sys/bootloader-version", bootloader::version}; + auto bootloader_version = Config {"/fs/sys/bootloader-version", {bootloader::version}}; auto battery_hysteresis_offset = - Config {"/fs/etc/bootloader-battery_hysteresis", battery::default_hysteresis_offset}; - auto factory_reset_limit = Config {"/fs/etc/bootloader-reboots_limit", factory_reset::default_limit}; + Config {"/fs/etc/bootloader-battery_hysteresis", {battery::default_hysteresis_offset}}; + auto factory_reset_limit = Config {"/fs/etc/bootloader-reboots_limit", {factory_reset::default_limit}}; auto configkit = ConfigKit {}; diff --git a/libs/ConfigKit/include/Config.h b/libs/ConfigKit/include/Config.h index 0b33ada78b..c0278de5e2 100644 --- a/libs/ConfigKit/include/Config.h +++ b/libs/ConfigKit/include/Config.h @@ -4,25 +4,28 @@ #pragma once +#include +#include #include namespace leka { +template struct Config { public: - explicit Config(const std::filesystem::path &path, uint8_t default_value = 0xFF) - : _path(_default_parent_path / path), _default_value(default_value) + explicit Config(const std::filesystem::path &path, std::array default_value = {}) + : _path(_default_parent_path / path), kDefaultValue(default_value) { // nothing to do } [[nodiscard]] auto path() const -> std::filesystem::path { return _path; } - [[nodiscard]] auto default_value() const -> uint8_t { return _default_value; } + [[nodiscard]] auto default_value() const -> std::array { return kDefaultValue; } private: const std::filesystem::path _default_parent_path = "/fs/etc"; const std::filesystem::path _path; - const uint8_t _default_value; + const std::array kDefaultValue {}; }; } // namespace leka diff --git a/libs/ConfigKit/include/ConfigKit.h b/libs/ConfigKit/include/ConfigKit.h index 8d25fb4581..0140632898 100644 --- a/libs/ConfigKit/include/ConfigKit.h +++ b/libs/ConfigKit/include/ConfigKit.h @@ -7,6 +7,7 @@ #include #include "Config.h" +#include "FileManagerKit.h" namespace leka { @@ -14,8 +15,38 @@ class ConfigKit { public: explicit ConfigKit() = default; - [[nodiscard]] auto read(Config const &config) const -> uint8_t; - [[nodiscard]] auto write(Config const &config, uint8_t data) const -> bool; + template + [[nodiscard]] auto read(Config const &config) const + { + if (FileManagerKit::File file {config.path(), "r"}; file.is_open()) { + auto data = std::array {}; + file.read(data); + + if constexpr (SIZE == 1) { + return data[0]; + } else { + return data; + } + } + + if constexpr (SIZE == 1) { + return config.default_value()[0]; + } else { + return config.default_value(); + } + } + + template + [[nodiscard]] auto write(Config const &config, std::array data) const -> bool + { + if (FileManagerKit::File file {config.path(), "w+"}; file.is_open()) { + file.write(data); + + return true; + } + + return false; + } }; } // namespace leka diff --git a/libs/ConfigKit/include/ConfigList.h b/libs/ConfigKit/include/ConfigList.h index 961fd9baa8..2965d38790 100644 --- a/libs/ConfigKit/include/ConfigList.h +++ b/libs/ConfigKit/include/ConfigList.h @@ -10,6 +10,6 @@ namespace leka::config::bootloader { -inline const auto battery_level_hysteresis = Config {"bootloader_battery_level_hysteresis", 42}; +inline const auto battery_level_hysteresis = Config {"bootloader_battery_level_hysteresis", {42}}; } // namespace leka::config::bootloader diff --git a/libs/ConfigKit/source/ConfigKit.cpp b/libs/ConfigKit/source/ConfigKit.cpp index 686e3cb1fd..1098c3f152 100644 --- a/libs/ConfigKit/source/ConfigKit.cpp +++ b/libs/ConfigKit/source/ConfigKit.cpp @@ -3,35 +3,3 @@ // SPDX-License-Identifier: Apache-2.0 #include "ConfigKit.h" - -#include "FileManagerKit.h" - -using namespace leka; - -auto ConfigKit::read(Config const &config) const -> uint8_t -{ - FileManagerKit::File file {config.path(), "r"}; - - if (!file.is_open()) { - return config.default_value(); - } - - auto data = std::array {}; - file.read(data); - - return data.front(); -} - -auto ConfigKit::write(Config const &config, uint8_t data) const -> bool -{ - FileManagerKit::File file {config.path(), "w+"}; - - if (!file.is_open()) { - return false; - } - - auto output = std::to_array({data}); - file.write(output); - - return true; -} diff --git a/libs/ConfigKit/tests/Config_test.cpp b/libs/ConfigKit/tests/Config_test.cpp index c74c2ba0b1..66c2f4186a 100644 --- a/libs/ConfigKit/tests/Config_test.cpp +++ b/libs/ConfigKit/tests/Config_test.cpp @@ -22,21 +22,35 @@ class ConfigTest : public ::testing::Test // void SetUp() override {} // void TearDown() override {} + void spy_readConfigValue(std::span input_data) + { + if (FileManagerKit::File file {config_path.c_str()}; file.is_open()) { + file.read(input_data); + } + } + auto spy_readConfigValue() -> uint8_t { - auto input_data = std::array {}; - FileManagerKit::File file {config_path.c_str()}; - file.read(input_data); - file.close(); - return input_data.front(); + auto data = std::array {}; + if (FileManagerKit::File file {config_path.c_str()}; file.is_open()) { + file.read(data); + } + return data[0]; } - void spy_writeConfigValue(uint8_t data) + void spy_writeConfigValue(std::span data) { - auto output_data = std::array {data}; - FileManagerKit::File file {config_path.c_str(), "r+"}; - file.write(output_data); - file.close(); + if (FileManagerKit::File file {config_path.c_str(), "r+"}; file.is_open()) { + file.write(data); + } + } + + void spy_writeConfigValue(uint8_t input_data) + { + if (FileManagerKit::File file {config_path.c_str(), "r+"}; file.is_open()) { + auto data = std::array {input_data}; + file.write(data); + } } void spy_touchConfigFile() @@ -46,92 +60,183 @@ class ConfigTest : public ::testing::Test } void spy_removeConfigFile() { std::remove(config_path.c_str()); } + ConfigKit configkit {}; const std::filesystem::path config_path = "/tmp/test_config.conf"; }; TEST_F(ConfigTest, initializationConfigFullPath) { - const std::filesystem::path custom_full_path = "/tmp/test_config.conf"; - Config config {custom_full_path}; - ASSERT_NE(nullptr, &config); - const std::filesystem::path expected_path = "/tmp/test_config.conf"; - ASSERT_EQ(expected_path, config.path()); + auto custom_full_path = std::filesystem::path {"/tmp/test_config.conf"}; + auto config = Config<5> {custom_full_path}; + auto expected_path = std::filesystem::path {"/tmp/test_config.conf"}; + + EXPECT_NE(nullptr, &config); + + EXPECT_EQ(expected_path, config.path()); } TEST_F(ConfigTest, initializationWithDefaultParentPathConfig) { - const std::filesystem::path custom_filename = "test_config.conf"; - Config config {custom_filename}; - ASSERT_NE(nullptr, &config); - const std::filesystem::path expected_path = "/fs/etc/test_config.conf"; - ASSERT_EQ(expected_path, config.path()); + auto custom_filename = std::filesystem::path {"test_config.conf"}; + auto config = Config<5> {custom_filename}; + auto expected_path = std::filesystem::path {"/fs/etc/test_config.conf"}; + + EXPECT_NE(nullptr, &config); + EXPECT_EQ(expected_path, config.path()); } TEST_F(ConfigTest, initializationConfigKit) { - auto configkit = ConfigKit(); - ASSERT_NE(nullptr, &configkit); + EXPECT_NE(nullptr, &configkit); } TEST_F(ConfigTest, readNotOpenFile) { - Config config {config_path}; - auto configkit = ConfigKit(); + auto config_buffer = std::array {"Leka"}; + auto config = Config {config_path, config_buffer}; + spy_removeConfigFile(); - auto data = configkit.read(config); - ASSERT_EQ(config.default_value(), data); + + auto actual_data_config = configkit.read(config); + EXPECT_EQ(config.default_value(), actual_data_config); } TEST_F(ConfigTest, readEmptyFile) { - Config config {config_path}; - auto configkit = ConfigKit(); + auto config = Config<5> {config_path}; + auto expected_data_config = std::array {}; + spy_removeConfigFile(); spy_touchConfigFile(); - auto data = configkit.read(config); - ASSERT_EQ(0, data); + + auto actual_data_config = configkit.read(config); + EXPECT_EQ(actual_data_config, expected_data_config); } TEST_F(ConfigTest, read) { - Config config {config_path}; - auto configkit = ConfigKit(); - spy_writeConfigValue(5); - auto data = configkit.read(config); - ASSERT_EQ(5, data); + auto config = Config<5> {config_path}; + auto expected_data_config = std::array {"Leka"}; + + spy_writeConfigValue(expected_data_config); + + auto actual_data_config = configkit.read(config); + EXPECT_EQ(actual_data_config, expected_data_config); } TEST_F(ConfigTest, writeCreatingFile) { - Config config {config_path}; - auto configkit = ConfigKit(); + auto config = Config<5> {config_path}; + + auto expected_data_config = std::array {"Leka"}; + auto actual_data_config = std::array {}; spy_removeConfigFile(); - auto write = configkit.write(config, 5); - ASSERT_TRUE(write); + auto write = configkit.write(config, expected_data_config); + EXPECT_TRUE(write); - auto data = spy_readConfigValue(); - ASSERT_EQ(5, data); + spy_readConfigValue(actual_data_config); + EXPECT_EQ(actual_data_config, expected_data_config); } TEST_F(ConfigTest, writeNotOpenFile) { - auto unreachable_config_path = std::filesystem::path {"/tmp/unnexisting_directory/test_config.conf"}; - Config config {unreachable_config_path}; - auto configkit = ConfigKit(); + auto config_buffer = std::array {"Leka"}; + auto unreachable_config_path = std::filesystem::path {"/tmp/unexisting_directory/test_config.conf"}; + auto config = Config {unreachable_config_path, config_buffer}; + spy_removeConfigFile(); - auto write = configkit.write(config, 5); - ASSERT_FALSE(write); + + auto write = configkit.write(config, {}); + EXPECT_FALSE(write); } TEST_F(ConfigTest, write) { - Config config {config_path}; - auto configkit = ConfigKit(); + auto config = Config<5> {config_path}; + + auto expected_data_config = std::array {"Leka"}; + auto actual_data_config = std::array {}; + + spy_touchConfigFile(); + auto write = configkit.write(config, expected_data_config); + EXPECT_TRUE(write); + + spy_readConfigValue(actual_data_config); + EXPECT_EQ(actual_data_config, expected_data_config); +} + +TEST_F(ConfigTest, readNotOpenFileOneByteConfig) +{ + auto expected_data_config = uint8_t {0x2A}; + auto config = Config {config_path, {expected_data_config}}; + + spy_removeConfigFile(); + + auto actual_data_config = configkit.read(config); + EXPECT_EQ(actual_data_config, expected_data_config); +} + +TEST_F(ConfigTest, readEmptyFileOneByteConfig) +{ + auto expected_data_config = uint8_t {}; + auto config = Config {config_path}; + + spy_removeConfigFile(); spy_touchConfigFile(); - auto write = configkit.write(config, 5); - ASSERT_TRUE(write); - auto data = spy_readConfigValue(); - ASSERT_EQ(5, data); + + auto actual_data_config = configkit.read(config); + EXPECT_EQ(actual_data_config, expected_data_config); +} + +TEST_F(ConfigTest, readOneByteConfig) +{ + auto expected_data_config = uint8_t {0x2A}; + auto config = Config {config_path}; + + spy_writeConfigValue(expected_data_config); + + auto actual_data_config = configkit.read(config); + EXPECT_EQ(actual_data_config, expected_data_config); +} + +TEST_F(ConfigTest, writeCreatingFileOneByteConfig) +{ + auto config = Config {config_path}; + + auto expected_data_config = uint8_t {0x2A}; + + spy_removeConfigFile(); + + auto write = configkit.write(config, {expected_data_config}); + EXPECT_TRUE(write); + + auto actual_data_config = spy_readConfigValue(); + EXPECT_EQ(actual_data_config, expected_data_config); +} + +TEST_F(ConfigTest, writeNotOpenFileOneByteConfig) +{ + auto unreachable_config_path = std::filesystem::path {"/tmp/unexisting_directory/test_config.conf"}; + auto config = Config {unreachable_config_path, {0x2A}}; + + spy_removeConfigFile(); + + auto write = configkit.write(config, {}); + EXPECT_FALSE(write); +} + +TEST_F(ConfigTest, writeOneByteConfig) +{ + auto config = Config {config_path}; + + auto expected_data_config = uint8_t {0x2A}; + + spy_touchConfigFile(); + auto write = configkit.write(config, {expected_data_config}); + EXPECT_TRUE(write); + + auto actual_data_config = spy_readConfigValue(); + EXPECT_EQ(actual_data_config, expected_data_config); } diff --git a/spikes/lk_config_kit/main.cpp b/spikes/lk_config_kit/main.cpp index 8cce4c022e..aa16f78801 100644 --- a/spikes/lk_config_kit/main.cpp +++ b/spikes/lk_config_kit/main.cpp @@ -22,7 +22,8 @@ using namespace std::chrono_literals; SDBlockDevice sd_blockdevice(SD_SPI_MOSI, SD_SPI_MISO, SD_SPI_SCK); FATFileSystem fatfs("fs"); -auto configkit = ConfigKit(); +auto configkit = ConfigKit(); +auto config_to_edit = Config {"/fs/var/tmp/config"}; void initializeSD() { @@ -60,12 +61,13 @@ auto main() -> int int(t.count() / 1000)); ++custom_data; - if (auto write = configkit.write(config::bootloader::battery_level_hysteresis, custom_data); !write) { - log_error("Fail to write in hysteresis config file"); + if (auto write = configkit.write(config_to_edit, {custom_data}); !write) { + log_error("Fail to write config file"); return EXIT_FAILURE; } - auto battery_level_hysteresis = configkit.read(config::bootloader::battery_level_hysteresis); - log_info("Battery level hysteresis : %d", battery_level_hysteresis); + auto config_value = configkit.read(config_to_edit); + log_info("Config value : %d", config_value); + rtos::ThisThread::sleep_for(10s); } } From 7062bd656550fe571902393e75d2ff713f4ce6d1 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Thu, 19 Jan 2023 15:26:43 +0100 Subject: [PATCH 043/143] :building_construction: (Interface): Add InterruptIn interface --- include/interface/drivers/InterruptIn.h | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 include/interface/drivers/InterruptIn.h diff --git a/include/interface/drivers/InterruptIn.h b/include/interface/drivers/InterruptIn.h new file mode 100644 index 0000000000..416ae49967 --- /dev/null +++ b/include/interface/drivers/InterruptIn.h @@ -0,0 +1,27 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "drivers/InterruptIn.h" + +#include "Callback.h" +#include "PinNamesTypes.h" + +namespace leka::interface { + +class InterruptIn +{ + public: + virtual ~InterruptIn() = default; + + virtual auto read() -> int = 0; + + virtual void onRise(std::function const &callback) = 0; + virtual void onFall(std::function const &callback) = 0; +}; + +} // namespace leka::interface From bfcae5bcd78c2e497d992abcd9ffa400ec95cead Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Thu, 19 Jan 2023 15:27:34 +0100 Subject: [PATCH 044/143] :sparkles: (CoreInterruptIn): Create leka CoreInterruptIn wrapper --- drivers/CMakeLists.txt | 1 + drivers/CoreInterruptIn/CMakeLists.txt | 20 ++++++++++++++ .../CoreInterruptIn/include/CoreInterruptIn.h | 27 +++++++++++++++++++ .../source/CoreInterruptIn.cpp | 24 +++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 drivers/CoreInterruptIn/CMakeLists.txt create mode 100644 drivers/CoreInterruptIn/include/CoreInterruptIn.h create mode 100644 drivers/CoreInterruptIn/source/CoreInterruptIn.cpp diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt index c018297f2e..4282cae5cf 100644 --- a/drivers/CMakeLists.txt +++ b/drivers/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(${DRIVERS_DIR}/CoreBufferedSerial) add_subdirectory(${DRIVERS_DIR}/CoreEventFlags) add_subdirectory(${DRIVERS_DIR}/CoreEventQueue) add_subdirectory(${DRIVERS_DIR}/CoreI2C) +add_subdirectory(${DRIVERS_DIR}/CoreInterruptIn) add_subdirectory(${DRIVERS_DIR}/CoreLL) add_subdirectory(${DRIVERS_DIR}/CoreMCU) add_subdirectory(${DRIVERS_DIR}/CoreMutex) diff --git a/drivers/CoreInterruptIn/CMakeLists.txt b/drivers/CoreInterruptIn/CMakeLists.txt new file mode 100644 index 0000000000..88ceda7827 --- /dev/null +++ b/drivers/CoreInterruptIn/CMakeLists.txt @@ -0,0 +1,20 @@ +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +add_library(CoreInterruptIn STATIC) + +target_include_directories(CoreInterruptIn + PUBLIC + include +) + +target_sources(CoreInterruptIn + PRIVATE + source/CoreInterruptIn.cpp +) + +target_link_libraries(CoreInterruptIn + mbed-os +) + diff --git a/drivers/CoreInterruptIn/include/CoreInterruptIn.h b/drivers/CoreInterruptIn/include/CoreInterruptIn.h new file mode 100644 index 0000000000..f07113a2d1 --- /dev/null +++ b/drivers/CoreInterruptIn/include/CoreInterruptIn.h @@ -0,0 +1,27 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "interface/drivers/InterruptIn.h" + +namespace leka { + +class CoreInterruptIn : public interface::InterruptIn +{ + public: + explicit CoreInterruptIn(PinName pin) : _irq(pin) {}; + + auto read() -> int final; + + void onRise(std::function const &callback) final; + void onFall(std::function const &callback) final; + + private: + mbed::InterruptIn _irq; + std::function _on_rise_callback {}; + std::function _on_fall_callback {}; +}; + +} // namespace leka diff --git a/drivers/CoreInterruptIn/source/CoreInterruptIn.cpp b/drivers/CoreInterruptIn/source/CoreInterruptIn.cpp new file mode 100644 index 0000000000..a730751537 --- /dev/null +++ b/drivers/CoreInterruptIn/source/CoreInterruptIn.cpp @@ -0,0 +1,24 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "CoreInterruptIn.h" + +using namespace leka; + +auto CoreInterruptIn::read() -> int +{ + return _irq.read(); +} + +void CoreInterruptIn::onRise(std::function const &callback) +{ + _on_rise_callback = callback; + _irq.rise(mbed::Callback {[this] { _on_rise_callback(); }}); +} + +void CoreInterruptIn::onFall(std::function const &callback) +{ + _on_fall_callback = callback; + _irq.fall(mbed::Callback {[this] { _on_fall_callback(); }}); +} From bb417c4fb07d2efe1e08cc28d53d1b0eeda89f95 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Thu, 19 Jan 2023 15:28:29 +0100 Subject: [PATCH 045/143] :white_check_mark: (CoreInterruptIn): Add CoreInterruptIn_test --- drivers/CoreInterruptIn/CMakeLists.txt | 5 ++ .../tests/CoreInterruptIn_test.cpp | 65 +++++++++++++++++++ tests/unit/CMakeLists.txt | 1 + 3 files changed, 71 insertions(+) create mode 100644 drivers/CoreInterruptIn/tests/CoreInterruptIn_test.cpp diff --git a/drivers/CoreInterruptIn/CMakeLists.txt b/drivers/CoreInterruptIn/CMakeLists.txt index 88ceda7827..b6fc09ecdf 100644 --- a/drivers/CoreInterruptIn/CMakeLists.txt +++ b/drivers/CoreInterruptIn/CMakeLists.txt @@ -18,3 +18,8 @@ target_link_libraries(CoreInterruptIn mbed-os ) +if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") + leka_unit_tests_sources( + tests/CoreInterruptIn_test.cpp + ) +endif() diff --git a/drivers/CoreInterruptIn/tests/CoreInterruptIn_test.cpp b/drivers/CoreInterruptIn/tests/CoreInterruptIn_test.cpp new file mode 100644 index 0000000000..516e434c73 --- /dev/null +++ b/drivers/CoreInterruptIn/tests/CoreInterruptIn_test.cpp @@ -0,0 +1,65 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "CoreInterruptIn.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "stubs/mbed/InterruptIn.h" + +using namespace leka; +using ::testing::MockFunction; + +class CoreInterruptInTest : public ::testing::Test +{ + protected: + // void SetUp() override {} + // void TearDown() override {} + + CoreInterruptIn interrupt_in {NC}; +}; + +TEST_F(CoreInterruptInTest, initialisation) +{ + ASSERT_NE(&interrupt_in, nullptr); +} + +TEST_F(CoreInterruptInTest, readPinDown) +{ + auto expected_pin_down_value = 0; + auto pin_value = interrupt_in.read(); + + EXPECT_EQ(pin_value, expected_pin_down_value); +} + +TEST_F(CoreInterruptInTest, readPinUp) +{ + auto expected_pin_up_value = 1; + spy_InterruptIn_setValue(1); + auto pin_value = interrupt_in.read(); + + EXPECT_EQ(pin_value, expected_pin_up_value); +} + +TEST_F(CoreInterruptInTest, onRise) +{ + MockFunction mock_function {}; + EXPECT_CALL(mock_function, Call).Times(1); + + interrupt_in.onRise(mock_function.AsStdFunction()); + + auto on_rise_callback = spy_InterruptIn_getRiseCallback(); + on_rise_callback(); +} + +TEST_F(CoreInterruptInTest, fall) +{ + MockFunction mock_function {}; + EXPECT_CALL(mock_function, Call).Times(1); + + interrupt_in.onFall(mock_function.AsStdFunction()); + + auto on_fall_callback = spy_InterruptIn_getFallCallback(); + on_fall_callback(); +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 8ac438c4e7..ac790e5550 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -259,6 +259,7 @@ leka_register_unit_tests_for_driver(CoreEventQueue) leka_register_unit_tests_for_driver(CoreFlashMemory) leka_register_unit_tests_for_driver(CoreHTS) leka_register_unit_tests_for_driver(CoreI2C) +leka_register_unit_tests_for_driver(CoreInterruptIn) leka_register_unit_tests_for_driver(CoreIMU) leka_register_unit_tests_for_driver(CoreIOExpander) leka_register_unit_tests_for_driver(CoreLED) From c2cb8812f958d2fd4e0f531c2a47377fc2078899 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Mon, 23 Jan 2023 14:54:57 +0100 Subject: [PATCH 046/143] :hammer: (fs): Add script to translate fs structure to namespaces This script recursively walks the content of `fs/` and generates the equivalent namespace strcture: - directories --> namespace - files --> std::string_view --- include/fs_structure.hpp | 492 ++++++++++++++++++++++++++++++++++ tools/generate_fs_strcture.rb | 105 ++++++++ 2 files changed, 597 insertions(+) create mode 100644 include/fs_structure.hpp create mode 100755 tools/generate_fs_strcture.rb diff --git a/include/fs_structure.hpp b/include/fs_structure.hpp new file mode 100644 index 0000000000..1f382c674d --- /dev/null +++ b/include/fs_structure.hpp @@ -0,0 +1,492 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +// NOLINTBEGIN(modernize-concat-nested-namespaces) + +namespace leka::fs { + +namespace home { + + namespace vid { + + namespace system { + + inline constexpr auto robot_system_sleep_wake_up_no_eyebrows = + std::string_view {"/fs/home/vid/system/robot-system-sleep-wake_up-no_eyebrows.avi"}; + inline constexpr auto robot_system_reinforcer_happy_no_eyebrows = + std::string_view {"/fs/home/vid/system/robot-system-reinforcer-happy-no_eyebrows.avi"}; + inline constexpr auto robot_system_idle_looking_top_right_left_no_eyebrows = + std::string_view {"/fs/home/vid/system/robot-system-idle-looking_top_right_left-no_eyebrows.avi"}; + inline constexpr auto robot_system_sleep_yawn_then_sleep_no_eyebrows = + std::string_view {"/fs/home/vid/system/robot-system-sleep-yawn_then_sleep-no_eyebrows.avi"}; + inline constexpr auto robot_system_ble_connection_wink_no_eyebrows = + std::string_view {"/fs/home/vid/system/robot-system-ble_connection-wink-no_eyebrows.avi"}; + + } // namespace system + + namespace states { + + inline constexpr auto robot_state_looking_top_right_left_no_eyebrows = + std::string_view {"/fs/home/vid/states/robot-state-looking_top_right_left-no_eyebrows.avi"}; + inline constexpr auto robot_state_looking_center_front_winking_no_eyebrows = + std::string_view {"/fs/home/vid/states/robot-state-looking_center_front_winking-no_eyebrows.avi"}; + inline constexpr auto robot_state_looking_center_right_no_eyebrows = + std::string_view {"/fs/home/vid/states/robot-state-looking_center_right-no_eyebrows.avi"}; + inline constexpr auto robot_state_looking_center_left_no_eyebrows = + std::string_view {"/fs/home/vid/states/robot-state-looking_center_left-no_eyebrows.avi"}; + inline constexpr auto robot_state_looking_top_right_no_eyebrows = + std::string_view {"/fs/home/vid/states/robot-state-looking_top_right-no_eyebrows.avi"}; + + } // namespace states + + namespace actions { + + inline constexpr auto robot_animation_action_fly_landing_on_nose_no_eyebrows = + std::string_view {"/fs/home/vid/actions/robot-animation-action-fly_landing_on_nose-no_eyebrows.avi"}; + inline constexpr auto robot_animation_action_blowing_bubbles_no_eyebrows = + std::string_view {"/fs/home/vid/actions/robot-animation-action-blowing_bubbles-no_eyebrows.avi"}; + inline constexpr auto robot_animation_action_singing_colored_notes_no_eyebrows = + std::string_view {"/fs/home/vid/actions/robot-animation-action-singing-colored_notes-no_eyebrows.avi"}; + inline constexpr auto robot_animation_action_sneezing_no_eyebrows = + std::string_view {"/fs/home/vid/actions/robot-animation-action-sneezing-no_eyebrows.avi"}; + inline constexpr auto robot_animation_action_diving_under_water_no_eyebrows = + std::string_view {"/fs/home/vid/actions/robot-animation-action-diving_under_water-no_eyebrows.avi"}; + inline constexpr auto robot_animation_action_yawning_no_eyebrows = + std::string_view {"/fs/home/vid/actions/robot-animation-action-yawning-no_eyebrows.avi"}; + + } // namespace actions + + namespace emotions { + + inline constexpr auto robot_emotion_afraid_blue_forehead_sweatdrop_no_eyebrows = + std::string_view {"/fs/home/vid/emotions/robot-emotion-afraid_blue_forehead_sweatdrop-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_amazed_sparkly_eyes_looking_top_trembling_mouth_no_eyebrows = + std::string_view { + "/fs/home/vid/emotions/" + "robot-emotion-amazed_sparkly_eyes_looking_top_trembling_mouth-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_angry_no_eyebrows = + std::string_view {"/fs/home/vid/emotions/robot-emotion-angry-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_angry_breathing_nose_no_eyebrows = + std::string_view {"/fs/home/vid/emotions/robot-emotion-angry_breathing_nose-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_amazed_sparkly_eyes_trembling_mouth_no_eyebrows = std::string_view { + "/fs/home/vid/emotions/robot-emotion-amazed_sparkly_eyes_trembling_mouth-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_sad_trembling_mouth_cry_tears_no_eyebrows = + std::string_view {"/fs/home/vid/emotions/robot-emotion-sad_trembling_mouth_cry_tears-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_happy_no_eyebrows = + std::string_view {"/fs/home/vid/emotions/robot-emotion-happy-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_sad_no_tears_no_eyebrows = + std::string_view {"/fs/home/vid/emotions/robot-emotion-sad-no_tears-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_afraid_trembling_eyes_sweatdrop_no_eyebrows = std::string_view { + "/fs/home/vid/emotions/robot-emotion-afraid_trembling_eyes_sweatdrop-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_afraid_trembling_eyes_no_eyebrows = + std::string_view {"/fs/home/vid/emotions/robot-emotion-afraid_trembling_eyes-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_disgusted_green_cheeks_pull_out_the_tang_no_eyebrows = + std::string_view { + "/fs/home/vid/emotions/robot-emotion-disgusted_green_cheeks_pull_out_the_tang-no_eyebrows.avi"}; + inline constexpr auto robot_emotion_sick_green_cheeks_no_eyebrows = + std::string_view {"/fs/home/vid/emotions/robot-emotion-sick_green_cheeks-no_eyebrows.avi"}; + + } // namespace emotions + + } // namespace vid + + namespace wav { + + inline constexpr auto fur_elise = std::string_view {"/fs/home/wav/fur-elise.wav"}; + + } // namespace wav + + namespace img { + + namespace system { + + inline constexpr auto robot_battery_charging_quarter_1_orange = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_1-orange.jpg"}; + inline constexpr auto robot_face_tired = std::string_view {"/fs/home/img/system/robot-face-tired.jpg"}; + inline constexpr auto robot_misc_splash_screen_medium_300 = + std::string_view {"/fs/home/img/system/robot-misc-splash_screen-medium-300.jpg"}; + inline constexpr auto robot_misc_splash_screen_small_200 = + std::string_view {"/fs/home/img/system/robot-misc-splash_screen-small-200.jpg"}; + inline constexpr auto robot_battery_empty_must_be_charged = + std::string_view {"/fs/home/img/system/robot-battery-empty-must_be_charged.jpg"}; + inline constexpr auto robot_battery_charging_full_green = + std::string_view {"/fs/home/img/system/robot-battery-charging-full_green.jpg"}; + inline constexpr auto robot_battery_charging_quarter_2_yellow = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_2-yellow.jpg"}; + inline constexpr auto robot_face_smiling = std::string_view {"/fs/home/img/system/robot-face-smiling.jpg"}; + inline constexpr auto robot_battery_charging_quarter_4_orange = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_4-orange.jpg"}; + inline constexpr auto robot_misc_choose_activity_fr_FR = + std::string_view {"/fs/home/img/system/robot-misc-choose_activity-fr_FR.jpg"}; + inline constexpr auto robot_face_angry = std::string_view {"/fs/home/img/system/robot-face-angry.jpg"}; + inline constexpr auto robot_battery_charging_quarter_1_red = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_1-red.jpg"}; + inline constexpr auto robot_battery_charging_quarter_1_green = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_1-green.jpg"}; + inline constexpr auto robot_face_sad = std::string_view {"/fs/home/img/system/robot-face-sad.jpg"}; + inline constexpr auto robot_battery_charging_quarter_3_orange = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_3-orange.jpg"}; + inline constexpr auto robot_face_neutral = std::string_view {"/fs/home/img/system/robot-face-neutral.jpg"}; + inline constexpr auto robot_battery_charging_quarter_3_red = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_3-red.jpg"}; + inline constexpr auto robot_misc_choose_activity_en_US = + std::string_view {"/fs/home/img/system/robot-misc-choose_activity-en_US.jpg"}; + inline constexpr auto robot_misc_missing_resource = + std::string_view {"/fs/home/img/system/robot-misc-missing_resource.jpg"}; + inline constexpr auto robot_battery_charging_quarter_2_red = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_2-red.jpg"}; + inline constexpr auto robot_misc_robot_misc_screen_empty_white = + std::string_view {"/fs/home/img/system/robot-misc-robot-misc-screen_empty_white.jpg"}; + inline constexpr auto robot_face_smiling_slightly = + std::string_view {"/fs/home/img/system/robot-face-smiling-slightly.jpg"}; + inline constexpr auto robot_battery_charging_quarter_3_yellow = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_3-yellow.jpg"}; + inline constexpr auto robot_face_smiling_smiling_eyes = + std::string_view {"/fs/home/img/system/robot-face-smiling-smiling_eyes.jpg"}; + inline constexpr auto robot_misc_splash_screen_large_400 = + std::string_view {"/fs/home/img/system/robot-misc-splash_screen-large-400.jpg"}; + inline constexpr auto robot_battery_charging_empty_no_color = + std::string_view {"/fs/home/img/system/robot-battery-charging-empty_no_color.jpg"}; + inline constexpr auto robot_face_happy = std::string_view {"/fs/home/img/system/robot-face-happy.jpg"}; + inline constexpr auto robot_battery_charging_quarter_3_green = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_3-green.jpg"}; + inline constexpr auto robot_battery_charging_quarter_1_yellow = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_1-yellow.jpg"}; + inline constexpr auto robot_battery_charging_quarter_4_red = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_4-red.jpg"}; + inline constexpr auto robot_face_disgusted = + std::string_view {"/fs/home/img/system/robot-face-disgusted.jpg"}; + inline constexpr auto robot_face_smiling_no_cheeks = + std::string_view {"/fs/home/img/system/robot-face-smiling-no_cheeks.jpg"}; + inline constexpr auto robot_face_tired_with_tears = + std::string_view {"/fs/home/img/system/robot-face-tired-with_tears.jpg"}; + inline constexpr auto robot_battery_charging_quarter_4_green = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_4-green.jpg"}; + inline constexpr auto robot_face_afraid = std::string_view {"/fs/home/img/system/robot-face-afraid.jpg"}; + inline constexpr auto robot_battery_charging_quarter_2_orange = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_2-orange.jpg"}; + inline constexpr auto robot_battery_charging_quarter_4_yellow = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_4-yellow.jpg"}; + inline constexpr auto robot_battery_charging_quarter_2_green = + std::string_view {"/fs/home/img/system/robot-battery-charging-quarter_2-green.jpg"}; + inline constexpr auto robot_face_sad_with_tears = + std::string_view {"/fs/home/img/system/robot-face-sad-with_tears.jpg"}; + inline constexpr auto robot_battery_charging_empty_red = + std::string_view {"/fs/home/img/system/robot-battery-charging-empty_red.jpg"}; + inline constexpr auto robot_misc_robot_misc_screen_empty_black = + std::string_view {"/fs/home/img/system/robot-misc-robot-misc-screen_empty_black.jpg"}; + + } // namespace system + + namespace id { + + inline constexpr auto _0071 = std::string_view {"/fs/home/img/id/0071.jpg"}; + inline constexpr auto _0065 = std::string_view {"/fs/home/img/id/0065.jpg"}; + inline constexpr auto _00A6 = std::string_view {"/fs/home/img/id/00A6.jpg"}; + inline constexpr auto _0059 = std::string_view {"/fs/home/img/id/0059.jpg"}; + inline constexpr auto _006C = std::string_view {"/fs/home/img/id/006C.jpg"}; + inline constexpr auto _006B = std::string_view {"/fs/home/img/id/006B.jpg"}; + inline constexpr auto _0058 = std::string_view {"/fs/home/img/id/0058.jpg"}; + inline constexpr auto _007F = std::string_view {"/fs/home/img/id/007F.jpg"}; + inline constexpr auto _00A7 = std::string_view {"/fs/home/img/id/00A7.jpg"}; + inline constexpr auto _0064 = std::string_view {"/fs/home/img/id/0064.jpg"}; + inline constexpr auto _0070 = std::string_view {"/fs/home/img/id/0070.jpg"}; + inline constexpr auto _00AA = std::string_view {"/fs/home/img/id/00AA.jpg"}; + inline constexpr auto _0066 = std::string_view {"/fs/home/img/id/0066.jpg"}; + inline constexpr auto _00B9 = std::string_view {"/fs/home/img/id/00B9.jpg"}; + inline constexpr auto _00AC = std::string_view {"/fs/home/img/id/00AC.jpg"}; + inline constexpr auto _0072 = std::string_view {"/fs/home/img/id/0072.jpg"}; + inline constexpr auto _00A5 = std::string_view {"/fs/home/img/id/00A5.jpg"}; + inline constexpr auto _007D = std::string_view {"/fs/home/img/id/007D.jpg"}; + inline constexpr auto _0099 = std::string_view {"/fs/home/img/id/0099.jpg"}; + inline constexpr auto _0098 = std::string_view {"/fs/home/img/id/0098.jpg"}; + inline constexpr auto _00A4 = std::string_view {"/fs/home/img/id/00A4.jpg"}; + inline constexpr auto _007E = std::string_view {"/fs/home/img/id/007E.jpg"}; + inline constexpr auto _006A = std::string_view {"/fs/home/img/id/006A.jpg"}; + inline constexpr auto _00AB = std::string_view {"/fs/home/img/id/00AB.jpg"}; + inline constexpr auto _00B8 = std::string_view {"/fs/home/img/id/00B8.jpg"}; + inline constexpr auto _0073 = std::string_view {"/fs/home/img/id/0073.jpg"}; + inline constexpr auto _0067 = std::string_view {"/fs/home/img/id/0067.jpg"}; + inline constexpr auto _006E = std::string_view {"/fs/home/img/id/006E.jpg"}; + inline constexpr auto _007A = std::string_view {"/fs/home/img/id/007A.jpg"}; + inline constexpr auto _00A0 = std::string_view {"/fs/home/img/id/00A0.jpg"}; + inline constexpr auto _00C8 = std::string_view {"/fs/home/img/id/00C8.jpg"}; + inline constexpr auto _0063 = std::string_view {"/fs/home/img/id/0063.jpg"}; + inline constexpr auto _0077 = std::string_view {"/fs/home/img/id/0077.jpg"}; + inline constexpr auto _00AF = std::string_view {"/fs/home/img/id/00AF.jpg"}; + inline constexpr auto _0088 = std::string_view {"/fs/home/img/id/0088.jpg"}; + inline constexpr auto _0089 = std::string_view {"/fs/home/img/id/0089.jpg"}; + inline constexpr auto _0076 = std::string_view {"/fs/home/img/id/0076.jpg"}; + inline constexpr auto _00C9 = std::string_view {"/fs/home/img/id/00C9.jpg"}; + inline constexpr auto _0062 = std::string_view {"/fs/home/img/id/0062.jpg"}; + inline constexpr auto _00A1 = std::string_view {"/fs/home/img/id/00A1.jpg"}; + inline constexpr auto _006D = std::string_view {"/fs/home/img/id/006D.jpg"}; + inline constexpr auto _00A3 = std::string_view {"/fs/home/img/id/00A3.jpg"}; + inline constexpr auto _007B = std::string_view {"/fs/home/img/id/007B.jpg"}; + inline constexpr auto _0048 = std::string_view {"/fs/home/img/id/0048.jpg"}; + inline constexpr auto _006F = std::string_view {"/fs/home/img/id/006F.jpg"}; + inline constexpr auto _00AE = std::string_view {"/fs/home/img/id/00AE.jpg"}; + inline constexpr auto _0074 = std::string_view {"/fs/home/img/id/0074.jpg"}; + inline constexpr auto _0060 = std::string_view {"/fs/home/img/id/0060.jpg"}; + inline constexpr auto _0061 = std::string_view {"/fs/home/img/id/0061.jpg"}; + inline constexpr auto _00AD = std::string_view {"/fs/home/img/id/00AD.jpg"}; + inline constexpr auto _0075 = std::string_view {"/fs/home/img/id/0075.jpg"}; + inline constexpr auto _00A2 = std::string_view {"/fs/home/img/id/00A2.jpg"}; + inline constexpr auto _0049 = std::string_view {"/fs/home/img/id/0049.jpg"}; + inline constexpr auto _007C = std::string_view {"/fs/home/img/id/007C.jpg"}; + inline constexpr auto _00D9 = std::string_view {"/fs/home/img/id/00D9.jpg"}; + inline constexpr auto _0012 = std::string_view {"/fs/home/img/id/0012.jpg"}; + inline constexpr auto _0006 = std::string_view {"/fs/home/img/id/0006.jpg"}; + inline constexpr auto _001D = std::string_view {"/fs/home/img/id/001D.jpg"}; + inline constexpr auto _000A = std::string_view {"/fs/home/img/id/000A.jpg"}; + inline constexpr auto _001E = std::string_view {"/fs/home/img/id/001E.jpg"}; + inline constexpr auto _0007 = std::string_view {"/fs/home/img/id/0007.jpg"}; + inline constexpr auto _00D8 = std::string_view {"/fs/home/img/id/00D8.jpg"}; + inline constexpr auto _0013 = std::string_view {"/fs/home/img/id/0013.jpg"}; + inline constexpr auto _0005 = std::string_view {"/fs/home/img/id/0005.jpg"}; + inline constexpr auto _0011 = std::string_view {"/fs/home/img/id/0011.jpg"}; + inline constexpr auto _000C = std::string_view {"/fs/home/img/id/000C.jpg"}; + inline constexpr auto _0039 = std::string_view {"/fs/home/img/id/0039.jpg"}; + inline constexpr auto _001F = std::string_view {"/fs/home/img/id/001F.jpg"}; + inline constexpr auto _0038 = std::string_view {"/fs/home/img/id/0038.jpg"}; + inline constexpr auto _000B = std::string_view {"/fs/home/img/id/000B.jpg"}; + inline constexpr auto _0010 = std::string_view {"/fs/home/img/id/0010.jpg"}; + inline constexpr auto _0004 = std::string_view {"/fs/home/img/id/0004.jpg"}; + inline constexpr auto _000F = std::string_view {"/fs/home/img/id/000F.jpg"}; + inline constexpr auto _0028 = std::string_view {"/fs/home/img/id/0028.jpg"}; + inline constexpr auto _001B = std::string_view {"/fs/home/img/id/001B.jpg"}; + inline constexpr auto _0014 = std::string_view {"/fs/home/img/id/0014.jpg"}; + inline constexpr auto _0015 = std::string_view {"/fs/home/img/id/0015.jpg"}; + inline constexpr auto _0001 = std::string_view {"/fs/home/img/id/0001.jpg"}; + inline constexpr auto _001C = std::string_view {"/fs/home/img/id/001C.jpg"}; + inline constexpr auto _0029 = std::string_view {"/fs/home/img/id/0029.jpg"}; + inline constexpr auto _001A = std::string_view {"/fs/home/img/id/001A.jpg"}; + inline constexpr auto _000E = std::string_view {"/fs/home/img/id/000E.jpg"}; + inline constexpr auto _0017 = std::string_view {"/fs/home/img/id/0017.jpg"}; + inline constexpr auto _0003 = std::string_view {"/fs/home/img/id/0003.jpg"}; + inline constexpr auto _0002 = std::string_view {"/fs/home/img/id/0002.jpg"}; + inline constexpr auto _0016 = std::string_view {"/fs/home/img/id/0016.jpg"}; + inline constexpr auto _000D = std::string_view {"/fs/home/img/id/000D.jpg"}; + inline constexpr auto _0033 = std::string_view {"/fs/home/img/id/0033.jpg"}; + inline constexpr auto _0027 = std::string_view {"/fs/home/img/id/0027.jpg"}; + inline constexpr auto _003E = std::string_view {"/fs/home/img/id/003E.jpg"}; + inline constexpr auto _002A = std::string_view {"/fs/home/img/id/002A.jpg"}; + inline constexpr auto _00D0 = std::string_view {"/fs/home/img/id/00D0.jpg"}; + inline constexpr auto _00D1 = std::string_view {"/fs/home/img/id/00D1.jpg"}; + inline constexpr auto _003D = std::string_view {"/fs/home/img/id/003D.jpg"}; + inline constexpr auto _0026 = std::string_view {"/fs/home/img/id/0026.jpg"}; + inline constexpr auto _0032 = std::string_view {"/fs/home/img/id/0032.jpg"}; + inline constexpr auto _0024 = std::string_view {"/fs/home/img/id/0024.jpg"}; + inline constexpr auto _0030 = std::string_view {"/fs/home/img/id/0030.jpg"}; + inline constexpr auto _00D3 = std::string_view {"/fs/home/img/id/00D3.jpg"}; + inline constexpr auto _002B = std::string_view {"/fs/home/img/id/002B.jpg"}; + inline constexpr auto _0018 = std::string_view {"/fs/home/img/id/0018.jpg"}; + inline constexpr auto _003F = std::string_view {"/fs/home/img/id/003F.jpg"}; + inline constexpr auto _00D2 = std::string_view {"/fs/home/img/id/00D2.jpg"}; + inline constexpr auto _0019 = std::string_view {"/fs/home/img/id/0019.jpg"}; + inline constexpr auto _002C = std::string_view {"/fs/home/img/id/002C.jpg"}; + inline constexpr auto _0031 = std::string_view {"/fs/home/img/id/0031.jpg"}; + inline constexpr auto _0025 = std::string_view {"/fs/home/img/id/0025.jpg"}; + inline constexpr auto _00D6 = std::string_view {"/fs/home/img/id/00D6.jpg"}; + inline constexpr auto _0009 = std::string_view {"/fs/home/img/id/0009.jpg"}; + inline constexpr auto _003C = std::string_view {"/fs/home/img/id/003C.jpg"}; + inline constexpr auto _0021 = std::string_view {"/fs/home/img/id/0021.jpg"}; + inline constexpr auto _0035 = std::string_view {"/fs/home/img/id/0035.jpg"}; + inline constexpr auto _0034 = std::string_view {"/fs/home/img/id/0034.jpg"}; + inline constexpr auto _0020 = std::string_view {"/fs/home/img/id/0020.jpg"}; + inline constexpr auto _00DA = std::string_view {"/fs/home/img/id/00DA.jpg"}; + inline constexpr auto _003B = std::string_view {"/fs/home/img/id/003B.jpg"}; + inline constexpr auto _0008 = std::string_view {"/fs/home/img/id/0008.jpg"}; + inline constexpr auto _002F = std::string_view {"/fs/home/img/id/002F.jpg"}; + inline constexpr auto _00D7 = std::string_view {"/fs/home/img/id/00D7.jpg"}; + inline constexpr auto _00D5 = std::string_view {"/fs/home/img/id/00D5.jpg"}; + inline constexpr auto _002D = std::string_view {"/fs/home/img/id/002D.jpg"}; + inline constexpr auto _0036 = std::string_view {"/fs/home/img/id/0036.jpg"}; + inline constexpr auto _0022 = std::string_view {"/fs/home/img/id/0022.jpg"}; + inline constexpr auto _00DB = std::string_view {"/fs/home/img/id/00DB.jpg"}; + inline constexpr auto _0023 = std::string_view {"/fs/home/img/id/0023.jpg"}; + inline constexpr auto _0037 = std::string_view {"/fs/home/img/id/0037.jpg"}; + inline constexpr auto _00D4 = std::string_view {"/fs/home/img/id/00D4.jpg"}; + inline constexpr auto _002E = std::string_view {"/fs/home/img/id/002E.jpg"}; + inline constexpr auto _003A = std::string_view {"/fs/home/img/id/003A.jpg"}; + inline constexpr auto _0050 = std::string_view {"/fs/home/img/id/0050.jpg"}; + inline constexpr auto _00CA = std::string_view {"/fs/home/img/id/00CA.jpg"}; + inline constexpr auto _00BE = std::string_view {"/fs/home/img/id/00BE.jpg"}; + inline constexpr auto _0044 = std::string_view {"/fs/home/img/id/0044.jpg"}; + inline constexpr auto _005F = std::string_view {"/fs/home/img/id/005F.jpg"}; + inline constexpr auto _00C7 = std::string_view {"/fs/home/img/id/00C7.jpg"}; + inline constexpr auto _00B3 = std::string_view {"/fs/home/img/id/00B3.jpg"}; + inline constexpr auto _0078 = std::string_view {"/fs/home/img/id/0078.jpg"}; + inline constexpr auto _004B = std::string_view {"/fs/home/img/id/004B.jpg"}; + inline constexpr auto _0093 = std::string_view {"/fs/home/img/id/0093.jpg"}; + inline constexpr auto _0087 = std::string_view {"/fs/home/img/id/0087.jpg"}; + inline constexpr auto _009E = std::string_view {"/fs/home/img/id/009E.jpg"}; + inline constexpr auto _008A = std::string_view {"/fs/home/img/id/008A.jpg"}; + inline constexpr auto _009D = std::string_view {"/fs/home/img/id/009D.jpg"}; + inline constexpr auto _0086 = std::string_view {"/fs/home/img/id/0086.jpg"}; + inline constexpr auto _0092 = std::string_view {"/fs/home/img/id/0092.jpg"}; + inline constexpr auto _00B2 = std::string_view {"/fs/home/img/id/00B2.jpg"}; + inline constexpr auto _004C = std::string_view {"/fs/home/img/id/004C.jpg"}; + inline constexpr auto _0079 = std::string_view {"/fs/home/img/id/0079.jpg"}; + inline constexpr auto _00C6 = std::string_view {"/fs/home/img/id/00C6.jpg"}; + inline constexpr auto _00BD = std::string_view {"/fs/home/img/id/00BD.jpg"}; + inline constexpr auto _0045 = std::string_view {"/fs/home/img/id/0045.jpg"}; + inline constexpr auto _0051 = std::string_view {"/fs/home/img/id/0051.jpg"}; + inline constexpr auto _0047 = std::string_view {"/fs/home/img/id/0047.jpg"}; + inline constexpr auto _00BF = std::string_view {"/fs/home/img/id/00BF.jpg"}; + inline constexpr auto _00CB = std::string_view {"/fs/home/img/id/00CB.jpg"}; + inline constexpr auto _0053 = std::string_view {"/fs/home/img/id/0053.jpg"}; + inline constexpr auto _004A = std::string_view {"/fs/home/img/id/004A.jpg"}; + inline constexpr auto _00B0 = std::string_view {"/fs/home/img/id/00B0.jpg"}; + inline constexpr auto _00C4 = std::string_view {"/fs/home/img/id/00C4.jpg"}; + inline constexpr auto _005E = std::string_view {"/fs/home/img/id/005E.jpg"}; + inline constexpr auto _0084 = std::string_view {"/fs/home/img/id/0084.jpg"}; + inline constexpr auto _0090 = std::string_view {"/fs/home/img/id/0090.jpg"}; + inline constexpr auto _008B = std::string_view {"/fs/home/img/id/008B.jpg"}; + inline constexpr auto _009F = std::string_view {"/fs/home/img/id/009F.jpg"}; + inline constexpr auto _008C = std::string_view {"/fs/home/img/id/008C.jpg"}; + inline constexpr auto _0091 = std::string_view {"/fs/home/img/id/0091.jpg"}; + inline constexpr auto _0085 = std::string_view {"/fs/home/img/id/0085.jpg"}; + inline constexpr auto _00C5 = std::string_view {"/fs/home/img/id/00C5.jpg"}; + inline constexpr auto _005D = std::string_view {"/fs/home/img/id/005D.jpg"}; + inline constexpr auto _00B1 = std::string_view {"/fs/home/img/id/00B1.jpg"}; + inline constexpr auto _00CC = std::string_view {"/fs/home/img/id/00CC.jpg"}; + inline constexpr auto _0052 = std::string_view {"/fs/home/img/id/0052.jpg"}; + inline constexpr auto _0046 = std::string_view {"/fs/home/img/id/0046.jpg"}; + inline constexpr auto _00B5 = std::string_view {"/fs/home/img/id/00B5.jpg"}; + inline constexpr auto _004D = std::string_view {"/fs/home/img/id/004D.jpg"}; + inline constexpr auto _00C1 = std::string_view {"/fs/home/img/id/00C1.jpg"}; + inline constexpr auto _00BC = std::string_view {"/fs/home/img/id/00BC.jpg"}; + inline constexpr auto _00A9 = std::string_view {"/fs/home/img/id/00A9.jpg"}; + inline constexpr auto _0042 = std::string_view {"/fs/home/img/id/0042.jpg"}; + inline constexpr auto _0056 = std::string_view {"/fs/home/img/id/0056.jpg"}; + inline constexpr auto _009C = std::string_view {"/fs/home/img/id/009C.jpg"}; + inline constexpr auto _0081 = std::string_view {"/fs/home/img/id/0081.jpg"}; + inline constexpr auto _0095 = std::string_view {"/fs/home/img/id/0095.jpg"}; + inline constexpr auto _0094 = std::string_view {"/fs/home/img/id/0094.jpg"}; + inline constexpr auto _0080 = std::string_view {"/fs/home/img/id/0080.jpg"}; + inline constexpr auto _009B = std::string_view {"/fs/home/img/id/009B.jpg"}; + inline constexpr auto _008F = std::string_view {"/fs/home/img/id/008F.jpg"}; + inline constexpr auto _0057 = std::string_view {"/fs/home/img/id/0057.jpg"}; + inline constexpr auto _00CF = std::string_view {"/fs/home/img/id/00CF.jpg"}; + inline constexpr auto _00A8 = std::string_view {"/fs/home/img/id/00A8.jpg"}; + inline constexpr auto _00BB = std::string_view {"/fs/home/img/id/00BB.jpg"}; + inline constexpr auto _0043 = std::string_view {"/fs/home/img/id/0043.jpg"}; + inline constexpr auto _005A = std::string_view {"/fs/home/img/id/005A.jpg"}; + inline constexpr auto _00C0 = std::string_view {"/fs/home/img/id/00C0.jpg"}; + inline constexpr auto _00B4 = std::string_view {"/fs/home/img/id/00B4.jpg"}; + inline constexpr auto _004E = std::string_view {"/fs/home/img/id/004E.jpg"}; + inline constexpr auto _00C2 = std::string_view {"/fs/home/img/id/00C2.jpg"}; + inline constexpr auto _005C = std::string_view {"/fs/home/img/id/005C.jpg"}; + inline constexpr auto _0069 = std::string_view {"/fs/home/img/id/0069.jpg"}; + inline constexpr auto _00B6 = std::string_view {"/fs/home/img/id/00B6.jpg"}; + inline constexpr auto _00CD = std::string_view {"/fs/home/img/id/00CD.jpg"}; + inline constexpr auto _0055 = std::string_view {"/fs/home/img/id/0055.jpg"}; + inline constexpr auto _0041 = std::string_view {"/fs/home/img/id/0041.jpg"}; + inline constexpr auto _008D = std::string_view {"/fs/home/img/id/008D.jpg"}; + inline constexpr auto _0096 = std::string_view {"/fs/home/img/id/0096.jpg"}; + inline constexpr auto _0082 = std::string_view {"/fs/home/img/id/0082.jpg"}; + inline constexpr auto _0083 = std::string_view {"/fs/home/img/id/0083.jpg"}; + inline constexpr auto _0097 = std::string_view {"/fs/home/img/id/0097.jpg"}; + inline constexpr auto _008E = std::string_view {"/fs/home/img/id/008E.jpg"}; + inline constexpr auto _009A = std::string_view {"/fs/home/img/id/009A.jpg"}; + inline constexpr auto _0040 = std::string_view {"/fs/home/img/id/0040.jpg"}; + inline constexpr auto _00BA = std::string_view {"/fs/home/img/id/00BA.jpg"}; + inline constexpr auto _00CE = std::string_view {"/fs/home/img/id/00CE.jpg"}; + inline constexpr auto _0054 = std::string_view {"/fs/home/img/id/0054.jpg"}; + inline constexpr auto _004F = std::string_view {"/fs/home/img/id/004F.jpg"}; + inline constexpr auto _00B7 = std::string_view {"/fs/home/img/id/00B7.jpg"}; + inline constexpr auto _00C3 = std::string_view {"/fs/home/img/id/00C3.jpg"}; + inline constexpr auto _0068 = std::string_view {"/fs/home/img/id/0068.jpg"}; + inline constexpr auto _005B = std::string_view {"/fs/home/img/id/005B.jpg"}; + + } // namespace id + + } // namespace img + +} // namespace home + +namespace usr { + + namespace test { + + inline constexpr auto vid_2_ok = std::string_view {"/fs/usr/test/vid-2-ok.avi"}; + inline constexpr auto img_9_ok = std::string_view {"/fs/usr/test/img-9-ok.jpg"}; + inline constexpr auto file_1_ok = std::string_view {"/fs/usr/test/file-1-ok.txt"}; + inline constexpr auto img_1_ok = std::string_view {"/fs/usr/test/img-1-ok.jpg"}; + inline constexpr auto vid_0_ok_pre_image = std::string_view {"/fs/usr/test/vid-0-ok-pre_image.jpg"}; + inline constexpr auto img_3_ok = std::string_view {"/fs/usr/test/img-3-ok.jpg"}; + inline constexpr auto file_2_ko_empty = std::string_view {"/fs/usr/test/file-2-ko-empty.txt"}; + inline constexpr auto vid_1_ok = std::string_view {"/fs/usr/test/vid-1-ok.avi"}; + inline constexpr auto img_4_ko_broken_content = std::string_view {"/fs/usr/test/img-4-ko-broken_content.jpg"}; + inline constexpr auto img_6_ko_empty = std::string_view {"/fs/usr/test/img-6-ko-empty.jpg"}; + inline constexpr auto file_3_ko_need_reboot = std::string_view {"/fs/usr/test/file-3-ko-need_reboot"}; + inline constexpr auto vid_4_ko_empty = std::string_view {"/fs/usr/test/vid-4-ko-empty.avi"}; + inline constexpr auto img_5_ko_size_800x481 = std::string_view {"/fs/usr/test/img-5-ko-size_800x481.jpg"}; + inline constexpr auto vid_5_ok = std::string_view {"/fs/usr/test/vid-5-ok.avi"}; + inline constexpr auto conf = std::string_view {"/fs/usr/test/conf"}; + inline constexpr auto vid_3_ko_broken_content = std::string_view {"/fs/usr/test/vid-3-ko-broken_content.avi"}; + inline constexpr auto img_2_ok = std::string_view {"/fs/usr/test/img-2-ok.jpg"}; + + } // namespace test + + namespace certs { + + namespace ca { + + inline constexpr auto DigiCert_SHA2_High_Assurance_Server_CA_04E1E7A4DC5CF2F36DC02B42B85D159F = + std::string_view { + "/fs/usr/certs/ca/DigiCert_SHA2_High_Assurance_Server_CA-04E1E7A4DC5CF2F36DC02B42B85D159F.txt"}; + inline constexpr auto + DigiCert_High_Assurance_TLS_Hybrid_ECC_SHA256_2020_CA1_0667035BBB14FD63AFC0D6A8534EFE16 = + std::string_view { + "/fs/usr/certs/ca/" + "DigiCert_High_Assurance_TLS_Hybrid_ECC_SHA256_2020_CA1-0667035BBB14FD63AFC0D6A8534EFE16.txt"}; + + } // namespace ca + + } // namespace certs + + namespace os { + + inline constexpr auto LekaOS_factory = std::string_view {"/fs/usr/os/LekaOS-factory.bin"}; + + } // namespace os + + namespace share { + + inline constexpr auto global_reboots_counter = std::string_view {"/fs/usr/share/global_reboots_counter"}; + inline constexpr auto factory_reset_counter = std::string_view {"/fs/usr/share/factory_reset_counter"}; + + } // namespace share + +} // namespace usr + +namespace etc { + + inline constexpr auto bootloader_battery_hysteresis = std::string_view {"/fs/etc/bootloader-battery_hysteresis"}; + inline constexpr auto bootloader_reboots_limit = std::string_view {"/fs/etc/bootloader-reboots_limit"}; + +} // namespace etc + +namespace var { + +} // namespace var + +namespace sys { + + inline constexpr auto date_of_test = std::string_view {"/fs/sys/date_of_test"}; + inline constexpr auto os_version = std::string_view {"/fs/sys/os-version"}; + inline constexpr auto bootloader_version = std::string_view {"/fs/sys/bootloader-version"}; + inline constexpr auto hardware_version = std::string_view {"/fs/sys/hardware-version"}; + +} // namespace sys + +} // namespace leka::fs + +// NOLINTEND(modernize-concat-nested-namespaces) diff --git a/tools/generate_fs_strcture.rb b/tools/generate_fs_strcture.rb new file mode 100755 index 0000000000..58c7922a57 --- /dev/null +++ b/tools/generate_fs_strcture.rb @@ -0,0 +1,105 @@ +#!/usr/bin/env ruby + +# Leka - LekaOS +# Copyright 2020 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +require 'pathname' + +$ROOT_PATH = Pathname.new('fs') +$OUTPUT_PATH = 'include/fs_structure.hpp' +$DIR_LEVEL = 0 + +$output = File.new($OUTPUT_PATH, "w") + +def level + return " " * $DIR_LEVEL * 2 +end + +def uninteresting?(file) + name = "#{file.basename}" + + if name[0] == '.' or name == "README.md" + return true + else + return false + end +end + +def make_var_name(name) + raw = "#{name.sub_ext('').basename}" + var = raw.gsub(/[\-]/, '_') + return var +end + +def write(str) + puts str + $output.puts str +end + +def is_id_dir?(path) + parent = "#{path.parent.basename}" + return parent == "id" +end + +def recurse(dir) + + dir.each_child do |child| + + next if "#{child.basename}"[0] == '.' + + if child.directory? and not child.empty? + + next if child.children.length() == 1 and uninteresting?(child.children[0]) + + write "#{level}namespace #{child.basename} {\n\n" + + $DIR_LEVEL += 1 + recurse(child) + $DIR_LEVEL -= 1 + + write "\n#{level}} // namespace #{child}\n\n" + + else + + if is_id_dir?(child) + write "#{level}inline constexpr auto _#{make_var_name(child)} = std::string_view {\"/#{child}\"};" unless uninteresting?(child) + else + write "#{level}inline constexpr auto #{make_var_name(child)} = std::string_view {\"/#{child}\"};" unless uninteresting?(child) + end + + end + + end + +end + + +write <<~EOS +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +// NOLINTBEGIN(modernize-concat-nested-namespaces) + +namespace leka::fs { + +EOS + +recurse($ROOT_PATH) + +write <<~EOS + +} // namespace leka::fs + +// NOLINTEND(modernize-concat-nested-namespaces) + +EOS + +$output.close + +system("clang-format -i #{$OUTPUT_PATH}") From d71751a70ef628055a69552fa6c67de1ef7d9dc2 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Mon, 23 Jan 2023 15:01:09 +0100 Subject: [PATCH 047/143] :recycle: (BehaviorKit): Replace img/vid paths w/ namespaces --- libs/BehaviorKit/source/BehaviorKit.cpp | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/libs/BehaviorKit/source/BehaviorKit.cpp b/libs/BehaviorKit/source/BehaviorKit.cpp index 485ab71f1e..776fa5b97e 100644 --- a/libs/BehaviorKit/source/BehaviorKit.cpp +++ b/libs/BehaviorKit/source/BehaviorKit.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "BehaviorKit.h" +#include #include "rtos/ThisThread.h" @@ -26,19 +27,19 @@ void BehaviorKit::spinRight(float speed) void BehaviorKit::launching() { - _videokit.displayImage("/fs/home/img/system/robot-misc-splash_screen-large-400.jpg"); + _videokit.displayImage(fs::home::img::system::robot_misc_splash_screen_large_400); } void BehaviorKit::sleeping() { _ledkit.start(&led::animation::sleeping); - _videokit.playVideoOnce("/fs/home/vid/system/robot-system-sleep-yawn_then_sleep-no_eyebrows.avi"); + _videokit.playVideoOnce(fs::home::vid::system::robot_system_sleep_yawn_then_sleep_no_eyebrows); } void BehaviorKit::waiting() { _ledkit.stop(); - _videokit.playVideoOnRepeat("/fs/home/vid/system/robot-system-idle-looking_top_right_left-no_eyebrows.avi"); + _videokit.playVideoOnRepeat(fs::home::vid::system::robot_system_idle_looking_top_right_left_no_eyebrows); } void BehaviorKit::blinkOnCharge() @@ -49,34 +50,34 @@ void BehaviorKit::blinkOnCharge() void BehaviorKit::lowBattery() { _ledkit.stop(); - _videokit.displayImage("/fs/home/img/system/robot-battery-empty-must_be_charged.jpg"); + _videokit.displayImage(fs::home::img::system::robot_battery_empty_must_be_charged); _motor_left.stop(); _motor_right.stop(); } void BehaviorKit::chargingEmpty() { - _videokit.displayImage("/fs/home/img/system/robot-battery-charging-empty_red.jpg"); + _videokit.displayImage(fs::home::img::system::robot_battery_charging_empty_red); } void BehaviorKit::chargingLow() { - _videokit.displayImage("/fs/home/img/system/robot-battery-charging-quarter_1-red.jpg"); + _videokit.displayImage(fs::home::img::system::robot_battery_charging_quarter_1_red); } void BehaviorKit::chargingMedium() { - _videokit.displayImage("/fs/home/img/system/robot-battery-charging-quarter_2-orange.jpg"); + _videokit.displayImage(fs::home::img::system::robot_battery_charging_quarter_2_orange); } void BehaviorKit::chargingHigh() { - _videokit.displayImage("/fs/home/img/system/robot-battery-charging-quarter_3-green.jpg"); + _videokit.displayImage(fs::home::img::system::robot_battery_charging_quarter_3_green); } void BehaviorKit::chargingFull() { - _videokit.displayImage("/fs/home/img/system/robot-battery-charging-quarter_4-green.jpg"); + _videokit.displayImage(fs::home::img::system::robot_battery_charging_quarter_4_green); } void BehaviorKit::bleConnectionWithoutVideo() @@ -87,16 +88,17 @@ void BehaviorKit::bleConnectionWithoutVideo() void BehaviorKit::bleConnectionWithVideo() { _ledkit.start(&led::animation::ble_connection); - _videokit.playVideoOnce("/fs/home/vid/system/robot-system-ble_connection-wink-no_eyebrows.avi"); + _videokit.playVideoOnce(fs::home::vid::system::robot_system_ble_connection_wink_no_eyebrows); } void BehaviorKit::working() { - _videokit.displayImage("/fs/home/img/system/robot-face-smiling-slightly.jpg"); + _videokit.displayImage(fs::home::img::system::robot_face_smiling_slightly); } void BehaviorKit::fileExchange() { + // TODO(@ladislas): add file exchange image _videokit.displayImage("/fs/home/img/system/robot-file_exchange.jpg"); } From fcb87938d172168bd276696933e84881ab60cf7f Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 23 Jan 2023 16:05:02 +0100 Subject: [PATCH 048/143] :white_check_mark: (jpeg): Improve CoreJPEG coverage Expect calls from callback --- drivers/CoreVideo/tests/CoreJPEG_test.cpp | 54 ++++++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/drivers/CoreVideo/tests/CoreJPEG_test.cpp b/drivers/CoreVideo/tests/CoreJPEG_test.cpp index 3a8a0218f1..d6c4a9ba36 100644 --- a/drivers/CoreVideo/tests/CoreJPEG_test.cpp +++ b/drivers/CoreVideo/tests/CoreJPEG_test.cpp @@ -18,6 +18,7 @@ using ::testing::DoAll; using ::testing::InSequence; using ::testing::Matcher; using ::testing::Return; +using ::testing::SaveArg; using ::testing::SetArgPointee; using ::testing::SetArrayArgument; @@ -65,6 +66,16 @@ TEST_F(CoreJPEGTest, getImageProperties) TEST_F(CoreJPEGTest, initializationSequence) { + std::function hal_jpeg_mspinit_callback = [](JPEG_HandleTypeDef *) {}; + std::function hal_jpeg_info_ready_callback = + [](JPEG_HandleTypeDef *, JPEG_ConfTypeDef *) {}; + std::function hal_jpeg_get_data_callback = [](JPEG_HandleTypeDef *, + uint32_t) {}; + std::function hal_jpeg_data_ready_callback = + [](JPEG_HandleTypeDef *, uint8_t *, uint32_t) {}; + std::function hal_jpeg_decode_complete_callback = [](JPEG_HandleTypeDef *) {}; + std::function hal_jpeg_error_callback = [](JPEG_HandleTypeDef *) {}; + { InSequence seq; @@ -75,17 +86,48 @@ TEST_F(CoreJPEGTest, initializationSequence) EXPECT_CALL(halmock, HAL_NVIC_SetPriority).Times(1); EXPECT_CALL(halmock, HAL_NVIC_EnableIRQ).Times(1); - EXPECT_CALL(halmock, HAL_JPEG_RegisterCallback(_, HAL_JPEG_MSPINIT_CB_ID, _)).Times(1); + EXPECT_CALL(halmock, HAL_JPEG_RegisterCallback(_, HAL_JPEG_MSPINIT_CB_ID, _)) + .WillOnce(DoAll(SaveArg<2>(&hal_jpeg_mspinit_callback), Return(HAL_StatusTypeDef::HAL_OK))); EXPECT_CALL(halmock, HAL_JPEG_Init).Times(1); - EXPECT_CALL(halmock, HAL_JPEG_RegisterInfoReadyCallback).Times(1); - EXPECT_CALL(halmock, HAL_JPEG_RegisterGetDataCallback).Times(1); - EXPECT_CALL(halmock, HAL_JPEG_RegisterDataReadyCallback).Times(1); - EXPECT_CALL(halmock, HAL_JPEG_RegisterCallback(_, HAL_JPEG_DECODE_CPLT_CB_ID, _)).Times(1); - EXPECT_CALL(halmock, HAL_JPEG_RegisterCallback(_, HAL_JPEG_ERROR_CB_ID, _)).Times(1); + EXPECT_CALL(halmock, HAL_JPEG_RegisterInfoReadyCallback) + .WillOnce(DoAll(SaveArg<1>(&hal_jpeg_info_ready_callback), Return(HAL_StatusTypeDef::HAL_OK))); + EXPECT_CALL(halmock, HAL_JPEG_RegisterGetDataCallback) + .WillOnce(DoAll(SaveArg<1>(&hal_jpeg_get_data_callback), Return(HAL_StatusTypeDef::HAL_OK))); + EXPECT_CALL(halmock, HAL_JPEG_RegisterDataReadyCallback) + .WillOnce(DoAll(SaveArg<1>(&hal_jpeg_data_ready_callback), Return(HAL_StatusTypeDef::HAL_OK))); + EXPECT_CALL(halmock, HAL_JPEG_RegisterCallback(_, HAL_JPEG_DECODE_CPLT_CB_ID, _)) + .WillOnce(DoAll(SaveArg<2>(&hal_jpeg_decode_complete_callback), Return(HAL_StatusTypeDef::HAL_OK))); + EXPECT_CALL(halmock, HAL_JPEG_RegisterCallback(_, HAL_JPEG_ERROR_CB_ID, _)) + .WillOnce(DoAll(SaveArg<2>(&hal_jpeg_error_callback), Return(HAL_StatusTypeDef::HAL_OK))); } corejpeg.initialize(); + + // ? - Verification of callback registered + + auto jpeg_handle = JPEG_HandleTypeDef {}; + + EXPECT_CALL(modemock, onMspInitCallback(&jpeg_handle)); + hal_jpeg_mspinit_callback(&jpeg_handle); + + auto jpeg_conf = JPEG_ConfTypeDef {}; + EXPECT_CALL(modemock, onInfoReadyCallback(&jpeg_handle, &jpeg_conf)); + hal_jpeg_info_ready_callback(&jpeg_handle, &jpeg_conf); + + auto any_datasize = uint8_t {42}; + EXPECT_CALL(modemock, onDataAvailableCallback(&jpeg_handle, any_datasize)); + hal_jpeg_get_data_callback(&jpeg_handle, any_datasize); + + auto output_data = std::array {}; + EXPECT_CALL(modemock, onDataReadyCallback(&jpeg_handle, output_data.data(), any_datasize)); + hal_jpeg_data_ready_callback(&jpeg_handle, output_data.data(), any_datasize); + + EXPECT_CALL(modemock, onDecodeCompleteCallback(&jpeg_handle)); + hal_jpeg_decode_complete_callback(&jpeg_handle); + + EXPECT_CALL(modemock, onErrorCallback(&jpeg_handle)); + hal_jpeg_error_callback(&jpeg_handle); } TEST_F(CoreJPEGTest, decodeImage) From a4b9c3b2771fe94a592d8d4459e7e642757205a4 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 23 Jan 2023 17:02:56 +0100 Subject: [PATCH 049/143] :white_check_mark: (jpeg): Improve CoreJPEGModeDMA coverage --- .../CoreVideo/tests/CoreJPEGModeDMA_test.cpp | 143 +++++++++++------- 1 file changed, 85 insertions(+), 58 deletions(-) diff --git a/drivers/CoreVideo/tests/CoreJPEGModeDMA_test.cpp b/drivers/CoreVideo/tests/CoreJPEGModeDMA_test.cpp index 8f0b1ed96d..a26f2d1c58 100644 --- a/drivers/CoreVideo/tests/CoreJPEGModeDMA_test.cpp +++ b/drivers/CoreVideo/tests/CoreJPEGModeDMA_test.cpp @@ -21,11 +21,7 @@ class CoreJPEGModeDMATest : public ::testing::Test protected: CoreJPEGModeDMATest() : corejpegmode(halmock) {} - void SetUp() override - { - // EXPECT_CALL(filemock, read(Matcher(_), _)).Times(1); - // corejpegmode.decode(&hjpeg, &filemock); - } + // void SetUp() override {} // void TearDown() override {} mock::CoreSTM32Hal halmock; @@ -174,78 +170,109 @@ TEST_F(CoreJPEGModeDMATest, onDataReadyCallback) corejpegmode.onDataReadyCallback(&hjpeg, &pDataOut, size); } -// TEST_F(CoreJPEGModeDMATest, onDataAvailableCallback) -// { -// auto size = uint32_t {2}; +TEST_F(CoreJPEGModeDMATest, onDataAvailableCallbackSizeDifferent) +{ + auto decoded_datasize = 1; -// { -// InSequence seq; + EXPECT_CALL(halmock, HAL_JPEG_ConfigInputBuffer).Times(1); -// EXPECT_CALL(filemock, seek).Times(1); -// EXPECT_CALL(filemock, read(Matcher(_), _)).WillOnce(Return(42)); -// EXPECT_CALL(halmock, HAL_JPEG_ConfigInputBuffer).Times(1); -// } + corejpegmode.onDataAvailableCallback(&hjpeg, decoded_datasize); +} -// corejpegmode.onDataAvailableCallback(&hjpeg, size); -// } +TEST_F(CoreJPEGModeDMATest, onDataAvailableCallbackSizeEqualNextBufferEmpty) +{ + auto decoded_datasize = 0; -// TEST_F(CoreJPEGModeDMATest, onDataAvailableCallbackSizeEqual) -// { -// auto size = uint32_t {42}; + EXPECT_CALL(halmock, HAL_JPEG_Pause).Times(1); -// EXPECT_CALL(filemock, read(Matcher(_), _)).WillOnce(Return(42)); -// EXPECT_CALL(halmock, HAL_JPEG_Decode).Times(1); -// corejpegmode.decode(&hjpeg, &filemock); + corejpegmode.onDataAvailableCallback(&hjpeg, decoded_datasize); +} -// { -// InSequence seq; +TEST_F(CoreJPEGModeDMATest, onDecodeCompleteCallback) +{ + corejpegmode.onDecodeCompleteCallback(&hjpeg); -// EXPECT_CALL(filemock, seek).Times(0); -// EXPECT_CALL(filemock, read(Matcher(_), _)).WillOnce(Return(42)); -// EXPECT_CALL(halmock, HAL_JPEG_ConfigInputBuffer).Times(1); -// } + // nothing expected +} -// corejpegmode.onDataAvailableCallback(&hjpeg, size); -// } +TEST_F(CoreJPEGModeDMATest, decodeBufferFull) +{ + auto buffer_state_full = 1; -// TEST_F(CoreJPEGModeDMATest, onDataAvailableCallbackCannotReadFile) -// { -// auto size = uint32_t {2}; + { + InSequence seq; -// { -// InSequence seq; + // read file and fill input buffers + EXPECT_CALL(filemock, read(Matcher(_), jpeg::input_chunk_size)) + .Times(jpeg::input_buffers_nb) + .WillRepeatedly(Return(buffer_state_full)); -// EXPECT_CALL(filemock, seek).Times(1); -// EXPECT_CALL(filemock, read(Matcher(_), _)).WillOnce(Return(0)); -// EXPECT_CALL(halmock, HAL_JPEG_ConfigInputBuffer).Times(0); -// } + // start JPEG decoding with DMA method + EXPECT_CALL(halmock, HAL_JPEG_Decode_DMA).Times(1); -// corejpegmode.onDataAvailableCallback(&hjpeg, size); -// } + // loop until decode process ends + // nothing expected + } -// TEST_F(CoreJPEGModeDMATest, decodeSuccess) -// { -// EXPECT_CALL(filemock, read(Matcher(_), _)).WillOnce(Return(42)); -// EXPECT_CALL(halmock, HAL_JPEG_Decode).Times(1); + auto image_size = corejpegmode.decode(&hjpeg, filemock); -// auto status = corejpegmode.decode(&hjpeg, &filemock); + EXPECT_EQ(image_size, 0); +} -// EXPECT_EQ(status, HAL_OK); -// } +TEST_F(CoreJPEGModeDMATest, decodeBufferEmptyNotPausedWriteBufferFull) +{ + auto buffer_state_full = 1; + auto buffer_state_empty = 0; -// TEST_F(CoreJPEGModeDMATest, decodeFailed) -// { -// EXPECT_CALL(filemock, read(Matcher(_), _)).WillOnce(Return(0)); -// EXPECT_CALL(halmock, HAL_JPEG_Decode).Times(0); + { + InSequence seq; -// auto status = corejpegmode.decode(&hjpeg, &filemock); + // read file and fill input buffers + EXPECT_CALL(filemock, read(Matcher(_), jpeg::input_chunk_size)) + .Times(jpeg::input_buffers_nb) + .WillRepeatedly(Return(buffer_state_empty)); -// EXPECT_NE(status, HAL_OK); -// } + // start JPEG decoding with DMA method + EXPECT_CALL(halmock, HAL_JPEG_Decode_DMA).Times(1); -TEST_F(CoreJPEGModeDMATest, onDecodeCompleteCallback) + // loop until decode process ends + // // decodeInputHandler + EXPECT_CALL(filemock, read(Matcher(_), jpeg::input_chunk_size)) + .Times(jpeg::input_buffers_nb) + .WillRepeatedly(Return(buffer_state_full)); + // // decoderOutputHandler + } + + auto image_size = corejpegmode.decode(&hjpeg, filemock); + + EXPECT_EQ(image_size, 0); +} + +TEST_F(CoreJPEGModeDMATest, decodeBufferEmptyNotPausedWriteBufferEmpty) { - corejpegmode.onDecodeCompleteCallback(&hjpeg); + auto buffer_state_full = 1; + auto buffer_state_empty = 0; - // nothing expected + { + InSequence seq; + + // read file and fill input buffers + EXPECT_CALL(filemock, read(Matcher(_), jpeg::input_chunk_size)) + .Times(jpeg::input_buffers_nb) + .WillRepeatedly(Return(buffer_state_empty)); + + // start JPEG decoding with DMA method + EXPECT_CALL(halmock, HAL_JPEG_Decode_DMA).Times(1); + + // loop until decode process ends + // // decodeInputHandler + EXPECT_CALL(filemock, read(Matcher(_), jpeg::input_chunk_size)) + .Times(jpeg::input_buffers_nb) + .WillRepeatedly(Return(buffer_state_empty)); + // // decoderOutputHandler + } + + auto image_size = corejpegmode.decode(&hjpeg, filemock); + + EXPECT_EQ(image_size, 0); } From 71ec9063429d9345c4c06bd02c37d6cec864f7d8 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 13:15:12 +0100 Subject: [PATCH 050/143] :white_check_mark: (file): Move initialization UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + .../tests/File_initialization_test.cpp | 109 ++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 84 -------------- 3 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_initialization_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index 9208d515c4..54e91822ae 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -25,6 +25,7 @@ target_link_libraries(FileManagerKit if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") leka_unit_tests_sources( tests/File_test.cpp + tests/File_initialization_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_initialization_test.cpp b/libs/FileManagerKit/tests/File_initialization_test.cpp new file mode 100644 index 0000000000..2a365a99db --- /dev/null +++ b/libs/FileManagerKit/tests/File_initialization_test.cpp @@ -0,0 +1,109 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestInitialization : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path {}; +}; + +TEST_F(FileTestInitialization, initializationDefault) +{ + auto new_file = FileManagerKit::File {}; + ASSERT_NE(&new_file, nullptr); + ASSERT_FALSE(new_file.is_open()); +} + +TEST_F(FileTestInitialization, initializationWithPath) +{ + auto new_file = FileManagerKit::File {tempFilename}; + ASSERT_NE(&new_file, nullptr); + ASSERT_TRUE(new_file.is_open()); +} + +TEST_F(FileTestInitialization, initializationWithPathAndMode) +{ + auto new_file = FileManagerKit::File {tempFilename, "r"}; + + ASSERT_NE(&new_file, nullptr); + ASSERT_TRUE(new_file.is_open()); +} + +TEST_F(FileTestInitialization, initializationWithFileSystemPath) +{ + auto new_file = FileManagerKit::File {tempFilename_filesystem_path}; + ASSERT_NE(&new_file, nullptr); + ASSERT_TRUE(new_file.is_open()); +} + +TEST_F(FileTestInitialization, initializationWithFileSystemPathAndMode) +{ + auto new_file = FileManagerKit::File {tempFilename_filesystem_path, "r"}; + + ASSERT_NE(&new_file, nullptr); + ASSERT_TRUE(new_file.is_open()); +} + +TEST_F(FileTestInitialization, initializationWithFileSystemPathAndNullPtrMode) +{ + auto new_file = FileManagerKit::File {tempFilename_filesystem_path, nullptr}; + + ASSERT_NE(&new_file, nullptr); + ASSERT_FALSE(new_file.is_open()); +} + +TEST_F(FileTestInitialization, initializationWithEmptyAndNullptr) +{ + { + auto f = FileManagerKit::File {"tempFilename", "r"}; + ASSERT_FALSE(f.is_open()); + } + { + auto f = FileManagerKit::File {nullptr, nullptr}; + ASSERT_FALSE(f.is_open()); + } + { + auto f = FileManagerKit::File {"nullptr", nullptr}; + ASSERT_FALSE(f.is_open()); + } + { + auto f = FileManagerKit::File {nullptr, "r"}; + ASSERT_FALSE(f.is_open()); + } + { + auto f = FileManagerKit::File {nullptr}; + ASSERT_FALSE(f.is_open()); + } + { + auto empty_path = std::filesystem::path {}; + auto f = FileManagerKit::File {empty_path}; + ASSERT_FALSE(f.is_open()); + } + { + auto empty_path = std::filesystem::path {}; + auto f = FileManagerKit::File {empty_path, "r"}; + ASSERT_FALSE(f.is_open()); + } + { + auto empty_path = std::filesystem::path {}; + auto f = FileManagerKit::File {empty_path, nullptr}; + ASSERT_FALSE(f.is_open()); + } +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index 269323dd82..9243ea2bd0 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,90 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, initializationDefault) -{ - auto new_file = FileManagerKit::File {}; - ASSERT_NE(&new_file, nullptr); - ASSERT_FALSE(new_file.is_open()); -} - -TEST_F(FileTest, initializationWithPath) -{ - auto new_file = FileManagerKit::File {tempFilename}; - ASSERT_NE(&new_file, nullptr); - ASSERT_TRUE(new_file.is_open()); -} - -TEST_F(FileTest, initializationWithPathAndMode) -{ - auto new_file = FileManagerKit::File {tempFilename, "r"}; - - ASSERT_NE(&new_file, nullptr); - ASSERT_TRUE(new_file.is_open()); -} - -TEST_F(FileTest, initializationWithFileSystemPath) -{ - auto new_file = FileManagerKit::File {tempFilename_filesystem_path}; - ASSERT_NE(&new_file, nullptr); - ASSERT_TRUE(new_file.is_open()); -} - -TEST_F(FileTest, initializationWithFileSystemPathAndMode) -{ - auto new_file = FileManagerKit::File {tempFilename_filesystem_path, "r"}; - - ASSERT_NE(&new_file, nullptr); - ASSERT_TRUE(new_file.is_open()); -} - -TEST_F(FileTest, initializationWithFileSystemPathAndNullPtrMode) -{ - auto new_file = FileManagerKit::File {tempFilename_filesystem_path, nullptr}; - - ASSERT_NE(&new_file, nullptr); - ASSERT_FALSE(new_file.is_open()); -} - -TEST_F(FileTest, initializationWithEmptyAndNullptr) -{ - { - auto f = FileManagerKit::File {"tempFilename", "r"}; - ASSERT_FALSE(f.is_open()); - } - { - auto f = FileManagerKit::File {nullptr, nullptr}; - ASSERT_FALSE(f.is_open()); - } - { - auto f = FileManagerKit::File {"nullptr", nullptr}; - ASSERT_FALSE(f.is_open()); - } - { - auto f = FileManagerKit::File {nullptr, "r"}; - ASSERT_FALSE(f.is_open()); - } - { - auto f = FileManagerKit::File {nullptr}; - ASSERT_FALSE(f.is_open()); - } - { - auto empty_path = std::filesystem::path {}; - auto f = FileManagerKit::File {empty_path}; - ASSERT_FALSE(f.is_open()); - } - { - auto empty_path = std::filesystem::path {}; - auto f = FileManagerKit::File {empty_path, "r"}; - ASSERT_FALSE(f.is_open()); - } - { - auto empty_path = std::filesystem::path {}; - auto f = FileManagerKit::File {empty_path, nullptr}; - ASSERT_FALSE(f.is_open()); - } -} - TEST_F(FileTest, open) { file.open(tempFilename); From 93e665f18fb7f5371412abec3cc2e8af064c0739 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 13:18:59 +0100 Subject: [PATCH 051/143] :white_check_mark: (file): Move open/close UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + .../tests/File_open_close_test.cpp | 55 +++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 29 ---------- 3 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_open_close_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index 54e91822ae..d4aece2d06 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -26,6 +26,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") leka_unit_tests_sources( tests/File_test.cpp tests/File_initialization_test.cpp + tests/File_open_close_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_open_close_test.cpp b/libs/FileManagerKit/tests/File_open_close_test.cpp new file mode 100644 index 0000000000..ffe4762a7b --- /dev/null +++ b/libs/FileManagerKit/tests/File_open_close_test.cpp @@ -0,0 +1,55 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestOpenClose : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path {}; +}; + +TEST_F(FileTestOpenClose, open) +{ + file.open(tempFilename); + + ASSERT_TRUE(file.is_open()); +} + +TEST_F(FileTestOpenClose, openByFileSystemPath) +{ + file.open(tempFilename_filesystem_path); + + ASSERT_TRUE(file.is_open()); +} + +TEST_F(FileTestOpenClose, close) +{ + file.open(tempFilename); + file.close(); + + ASSERT_FALSE(file.is_open()); +} + +TEST_F(FileTestOpenClose, closeNoFile) +{ + file.close(); + + ASSERT_FALSE(file.is_open()); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index 9243ea2bd0..3df00a393e 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,35 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, open) -{ - file.open(tempFilename); - - ASSERT_TRUE(file.is_open()); -} - -TEST_F(FileTest, openByFileSystemPath) -{ - file.open(tempFilename_filesystem_path); - - ASSERT_TRUE(file.is_open()); -} - -TEST_F(FileTest, close) -{ - file.open(tempFilename); - file.close(); - - ASSERT_FALSE(file.is_open()); -} - -TEST_F(FileTest, closeNoFile) -{ - file.close(); - - ASSERT_FALSE(file.is_open()); -} - TEST_F(FileTest, writeSpan) { auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" From b2532a269753999856d0fc3a5f6734617db2b9a9 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 13:26:08 +0100 Subject: [PATCH 052/143] :white_check_mark: (file): Move write UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + libs/FileManagerKit/tests/File_test.cpp | 128 ------------- libs/FileManagerKit/tests/File_write_test.cpp | 169 ++++++++++++++++++ 3 files changed, 170 insertions(+), 128 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_write_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index d4aece2d06..8824da46c7 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -27,6 +27,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_test.cpp tests/File_initialization_test.cpp tests/File_open_close_test.cpp + tests/File_write_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index 3df00a393e..d6ae78e2aa 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,134 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, writeSpan) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w"); - auto size = file.write(input_data); - - ASSERT_EQ(size, std::size(input_data)); - - file.close(); - - auto actual_data = readTempFile(); - - ASSERT_EQ("abcdef", actual_data); -} - -TEST_F(FileTest, writeSpanBinary) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "wb"); - auto size = file.write(input_data); - - ASSERT_EQ(size, std::size(input_data)); - - file.close(); - - auto actual_data = readTempFile(); - - ASSERT_EQ("abcdef", actual_data); -} - -TEST_F(FileTest, writeCharSpan) -{ - auto input_data = std::to_array({'a', 'b', 'c', 'd', 'e', 'f'}); - - file.open(tempFilename, "w"); - auto size = file.write(input_data); - - ASSERT_EQ(size, std::size(input_data)); - - file.close(); - - auto actual_data = readTempFile(); - - ASSERT_EQ("abcdef", actual_data); -} - -TEST_F(FileTest, writeCharSpanBinary) -{ - auto input_data = std::to_array({'a', 'b', 'c', 'd', 'e', 'f'}); - - file.open(tempFilename, "wb"); - auto size = file.write(input_data); - - ASSERT_EQ(size, std::size(input_data)); - - file.close(); - - auto actual_data = readTempFile(); - - ASSERT_EQ("abcdef", actual_data); -} - -TEST_F(FileTest, writeBufferAndSize) -{ - uint8_t input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" - - file.open(tempFilename, "w"); - auto size = file.write(input_data, std::size(input_data)); - - ASSERT_EQ(size, std::size(input_data)); - - file.close(); - - auto actual_data = readTempFile(); - - ASSERT_EQ("abcdef", actual_data); -} - -TEST_F(FileTest, writeBufferAndSizeBinary) -{ - uint8_t input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" - - file.open(tempFilename, "wb"); - auto size = file.write(input_data, std::size(input_data)); - - ASSERT_EQ(size, std::size(input_data)); - - file.close(); - - auto actual_data = readTempFile(); - - ASSERT_EQ("abcdef", actual_data); -} - -TEST_F(FileTest, writeCharBufferAndSize) -{ - char input_data[] = {'a', 'b', 'c', 'd', 'e', 'f'}; - - file.open(tempFilename, "w"); - auto size = file.write(input_data, std::size(input_data)); - - ASSERT_EQ(size, std::size(input_data)); - - file.close(); - - auto actual_data = readTempFile(); - - ASSERT_EQ("abcdef", actual_data); -} - -TEST_F(FileTest, writeCharBufferAndSizeBinary) -{ - char input_data[] = {'a', 'b', 'c', 'd', 'e', 'f'}; - - file.open(tempFilename, "wb"); - auto size = file.write(input_data, std::size(input_data)); - - ASSERT_EQ(size, std::size(input_data)); - - file.close(); - - auto actual_data = readTempFile(); - - ASSERT_EQ("abcdef", actual_data); -} - TEST_F(FileTest, readSpan) { auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" diff --git a/libs/FileManagerKit/tests/File_write_test.cpp b/libs/FileManagerKit/tests/File_write_test.cpp new file mode 100644 index 0000000000..7c539a5d8d --- /dev/null +++ b/libs/FileManagerKit/tests/File_write_test.cpp @@ -0,0 +1,169 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestWrite : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + auto readTempFile() -> std::string + { + std::fstream f {}; + f.open(tempFilename); + + std::stringstream out {}; + out << f.rdbuf(); + f.close(); + + return out.str(); + } + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestWrite, writeSpan) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w"); + auto size = file.write(input_data); + + ASSERT_EQ(size, std::size(input_data)); + + file.close(); + + auto actual_data = readTempFile(); + + ASSERT_EQ("abcdef", actual_data); +} + +TEST_F(FileTestWrite, writeSpanBinary) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "wb"); + auto size = file.write(input_data); + + ASSERT_EQ(size, std::size(input_data)); + + file.close(); + + auto actual_data = readTempFile(); + + ASSERT_EQ("abcdef", actual_data); +} + +TEST_F(FileTestWrite, writeCharSpan) +{ + auto input_data = std::to_array({'a', 'b', 'c', 'd', 'e', 'f'}); + + file.open(tempFilename, "w"); + auto size = file.write(input_data); + + ASSERT_EQ(size, std::size(input_data)); + + file.close(); + + auto actual_data = readTempFile(); + + ASSERT_EQ("abcdef", actual_data); +} + +TEST_F(FileTestWrite, writeCharSpanBinary) +{ + auto input_data = std::to_array({'a', 'b', 'c', 'd', 'e', 'f'}); + + file.open(tempFilename, "wb"); + auto size = file.write(input_data); + + ASSERT_EQ(size, std::size(input_data)); + + file.close(); + + auto actual_data = readTempFile(); + + ASSERT_EQ("abcdef", actual_data); +} + +TEST_F(FileTestWrite, writeBufferAndSize) +{ + uint8_t input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" + + file.open(tempFilename, "w"); + auto size = file.write(input_data, std::size(input_data)); + + ASSERT_EQ(size, std::size(input_data)); + + file.close(); + + auto actual_data = readTempFile(); + + ASSERT_EQ("abcdef", actual_data); +} + +TEST_F(FileTestWrite, writeBufferAndSizeBinary) +{ + uint8_t input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" + + file.open(tempFilename, "wb"); + auto size = file.write(input_data, std::size(input_data)); + + ASSERT_EQ(size, std::size(input_data)); + + file.close(); + + auto actual_data = readTempFile(); + + ASSERT_EQ("abcdef", actual_data); +} + +TEST_F(FileTestWrite, writeCharBufferAndSize) +{ + char input_data[] = {'a', 'b', 'c', 'd', 'e', 'f'}; + + file.open(tempFilename, "w"); + auto size = file.write(input_data, std::size(input_data)); + + ASSERT_EQ(size, std::size(input_data)); + + file.close(); + + auto actual_data = readTempFile(); + + ASSERT_EQ("abcdef", actual_data); +} + +TEST_F(FileTestWrite, writeCharBufferAndSizeBinary) +{ + char input_data[] = {'a', 'b', 'c', 'd', 'e', 'f'}; + + file.open(tempFilename, "wb"); + auto size = file.write(input_data, std::size(input_data)); + + ASSERT_EQ(size, std::size(input_data)); + + file.close(); + + auto actual_data = readTempFile(); + + ASSERT_EQ("abcdef", actual_data); +} From 74228d81a52667ecf1da3448c6c47f6a0654ff71 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:02:10 +0100 Subject: [PATCH 053/143] :white_check_mark: (file): Move read UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + libs/FileManagerKit/tests/File_read_test.cpp | 193 +++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 151 --------------- 3 files changed, 194 insertions(+), 151 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_read_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index 8824da46c7..45d9c89e30 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -28,6 +28,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_initialization_test.cpp tests/File_open_close_test.cpp tests/File_write_test.cpp + tests/File_read_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_read_test.cpp b/libs/FileManagerKit/tests/File_read_test.cpp new file mode 100644 index 0000000000..0b6482ba68 --- /dev/null +++ b/libs/FileManagerKit/tests/File_read_test.cpp @@ -0,0 +1,193 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestRead : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + void writeTempFile(std::span data) + { + auto *file = fopen(tempFilename, "wb"); + fwrite(data.data(), sizeof(uint8_t), data.size(), file); + fclose(file); + } + + void writeTempFile(std::span data) + { + auto *file = fopen(tempFilename, "w"); + fwrite(data.data(), sizeof(char), data.size(), file); + fclose(file); + } + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestRead, readSpan) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + writeTempFile(input_data); + + file.open(tempFilename, "r"); + + auto output_data = std::array {}; + + auto size = file.read(output_data); + + ASSERT_EQ(std::size(output_data), size); + ASSERT_EQ(input_data, output_data); +} + +TEST_F(FileTestRead, readSpanBinary) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + writeTempFile(input_data); + + file.open(tempFilename, "rb"); + + auto output_data = std::array {}; + + auto size = file.read(output_data); + + ASSERT_EQ(std::size(output_data), size); + ASSERT_EQ(input_data, output_data); +} + +TEST_F(FileTestRead, readCharSpan) +{ + auto input_data = std::to_array({'a', 'b', 'c', 'd', 'e', 'f'}); + + writeTempFile(input_data); + + file.open(tempFilename, "r"); + + auto output_data = std::array {}; + + auto size = file.read(output_data); + + ASSERT_EQ(std::size(output_data), size); + ASSERT_EQ(input_data, output_data); +} + +TEST_F(FileTestRead, readCharSpanBinary) +{ + auto input_data = std::to_array({'a', 'b', 'c', 'd', 'e', 'f'}); + + writeTempFile(input_data); + + file.open(tempFilename, "rb"); + + auto output_data = std::array {}; + + auto size = file.read(output_data); + + ASSERT_EQ(std::size(output_data), size); + ASSERT_EQ(input_data, output_data); +} + +TEST_F(FileTestRead, readBufferAndSize) +{ + uint8_t input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" + + writeTempFile(input_data); + + file.open(tempFilename, "r"); + + uint8_t output_data[6]; + + auto size = file.read(output_data, std::size(output_data)); + + ASSERT_EQ(std::size(output_data), size); + for (uint32_t i = 0; i < size; ++i) { + ASSERT_EQ(input_data[i], output_data[i]); + } +} + +TEST_F(FileTestRead, readBufferAndSizeBinary) +{ + uint8_t input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" + + writeTempFile(input_data); + + file.open(tempFilename, "rb"); + + uint8_t output_data[6]; + + auto size = file.read(output_data, std::size(output_data)); + + ASSERT_EQ(std::size(output_data), size); + for (uint32_t i = 0; i < size; ++i) { + ASSERT_EQ(input_data[i], output_data[i]); + } +} + +TEST_F(FileTestRead, readCharBufferAndSize) +{ + char input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" + + writeTempFile(input_data); + + file.open(tempFilename, "r"); + + char output_data[6]; + + auto size = file.read(output_data, std::size(output_data)); + + ASSERT_EQ(std::size(output_data), size); + for (uint32_t i = 0; i < size; ++i) { + ASSERT_EQ(input_data[i], output_data[i]); + } +} + +TEST_F(FileTestRead, readCharBufferAndSizeBinary) +{ + char input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" + + writeTempFile(input_data); + + file.open(tempFilename, "rb"); + + char output_data[6]; + + auto size = file.read(output_data, std::size(output_data)); + + ASSERT_EQ(std::size(output_data), size); + for (uint32_t i = 0; i < size; ++i) { + ASSERT_EQ(input_data[i], output_data[i]); + } +} + +TEST_F(FileTestRead, writeThenRead) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + auto output_data = std::array {}; + + file.open(tempFilename, "w+"); + + auto bytes_written = file.write(input_data); + file.rewind(); + auto bytes_read = file.read(output_data); + + ASSERT_EQ(bytes_written, bytes_read); + ASSERT_EQ(input_data, output_data); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index d6ae78e2aa..dd379ea76c 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,142 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, readSpan) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - writeTempFile(input_data); - - file.open(tempFilename, "r"); - - auto output_data = std::array {}; - - auto size = file.read(output_data); - - ASSERT_EQ(std::size(output_data), size); - ASSERT_EQ(input_data, output_data); -} - -TEST_F(FileTest, readSpanBinary) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - writeTempFile(input_data); - - file.open(tempFilename, "rb"); - - auto output_data = std::array {}; - - auto size = file.read(output_data); - - ASSERT_EQ(std::size(output_data), size); - ASSERT_EQ(input_data, output_data); -} - -TEST_F(FileTest, readCharSpan) -{ - auto input_data = std::to_array({'a', 'b', 'c', 'd', 'e', 'f'}); - - writeTempFile(input_data); - - file.open(tempFilename, "r"); - - auto output_data = std::array {}; - - auto size = file.read(output_data); - - ASSERT_EQ(std::size(output_data), size); - ASSERT_EQ(input_data, output_data); -} - -TEST_F(FileTest, readCharSpanBinary) -{ - auto input_data = std::to_array({'a', 'b', 'c', 'd', 'e', 'f'}); - - writeTempFile(input_data); - - file.open(tempFilename, "rb"); - - auto output_data = std::array {}; - - auto size = file.read(output_data); - - ASSERT_EQ(std::size(output_data), size); - ASSERT_EQ(input_data, output_data); -} - -TEST_F(FileTest, readBufferAndSize) -{ - uint8_t input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" - - writeTempFile(input_data); - - file.open(tempFilename, "r"); - - uint8_t output_data[6]; - - auto size = file.read(output_data, std::size(output_data)); - - ASSERT_EQ(std::size(output_data), size); - for (uint32_t i = 0; i < size; ++i) { - ASSERT_EQ(input_data[i], output_data[i]); - } -} - -TEST_F(FileTest, readBufferAndSizeBinary) -{ - uint8_t input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" - - writeTempFile(input_data); - - file.open(tempFilename, "rb"); - - uint8_t output_data[6]; - - auto size = file.read(output_data, std::size(output_data)); - - ASSERT_EQ(std::size(output_data), size); - for (uint32_t i = 0; i < size; ++i) { - ASSERT_EQ(input_data[i], output_data[i]); - } -} - -TEST_F(FileTest, readCharBufferAndSize) -{ - char input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" - - writeTempFile(input_data); - - file.open(tempFilename, "r"); - - char output_data[6]; - - auto size = file.read(output_data, std::size(output_data)); - - ASSERT_EQ(std::size(output_data), size); - for (uint32_t i = 0; i < size; ++i) { - ASSERT_EQ(input_data[i], output_data[i]); - } -} - -TEST_F(FileTest, readCharBufferAndSizeBinary) -{ - char input_data[] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; // "abcdef" - - writeTempFile(input_data); - - file.open(tempFilename, "rb"); - - char output_data[6]; - - auto size = file.read(output_data, std::size(output_data)); - - ASSERT_EQ(std::size(output_data), size); - for (uint32_t i = 0; i < size; ++i) { - ASSERT_EQ(input_data[i], output_data[i]); - } -} - TEST_F(FileTest, sizeNoFile) { auto size = file.size(); @@ -218,21 +82,6 @@ TEST_F(FileTest, sizeFile) ASSERT_EQ(expected_size, actual_size); } -TEST_F(FileTest, writeThenRead) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - auto output_data = std::array {}; - - file.open(tempFilename, "w+"); - - auto bytes_written = file.write(input_data); - file.rewind(); - auto bytes_read = file.read(output_data); - - ASSERT_EQ(bytes_written, bytes_read); - ASSERT_EQ(input_data, output_data); -} - TEST_F(FileTest, seek) { auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" From 5b11f89af8989719c299565000493ce498b7b7ad Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:05:40 +0100 Subject: [PATCH 054/143] :white_check_mark: (file): Move size UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + libs/FileManagerKit/tests/File_size_test.cpp | 52 ++++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 25 ---------- 3 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_size_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index 45d9c89e30..4c5bbade56 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -29,6 +29,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_open_close_test.cpp tests/File_write_test.cpp tests/File_read_test.cpp + tests/File_size_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_size_test.cpp b/libs/FileManagerKit/tests/File_size_test.cpp new file mode 100644 index 0000000000..a38c25d29e --- /dev/null +++ b/libs/FileManagerKit/tests/File_size_test.cpp @@ -0,0 +1,52 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestSize : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestSize, sizeNoFile) +{ + auto size = file.size(); + ASSERT_EQ(0, size); +} + +TEST_F(FileTestSize, sizeEmptyFile) +{ + file.open(tempFilename); + auto size = file.size(); + ASSERT_EQ(0, size); +} + +TEST_F(FileTestSize, sizeFile) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename); + auto expected_size = file.write(input_data); + + auto actual_size = file.size(); + + ASSERT_EQ(expected_size, actual_size); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index dd379ea76c..d535288f6b 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,31 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, sizeNoFile) -{ - auto size = file.size(); - ASSERT_EQ(0, size); -} - -TEST_F(FileTest, sizeEmptyFile) -{ - file.open(tempFilename); - auto size = file.size(); - ASSERT_EQ(0, size); -} - -TEST_F(FileTest, sizeFile) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename); - auto expected_size = file.write(input_data); - - auto actual_size = file.size(); - - ASSERT_EQ(expected_size, actual_size); -} - TEST_F(FileTest, seek) { auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" From 66505c6739f3f79a077763702bf5ee0e562937cf Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:07:30 +0100 Subject: [PATCH 055/143] :white_check_mark: (file): Move seek UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + libs/FileManagerKit/tests/File_seek_test.cpp | 43 ++++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 16 -------- 3 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_seek_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index 4c5bbade56..d68a31f740 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -30,6 +30,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_write_test.cpp tests/File_read_test.cpp tests/File_size_test.cpp + tests/File_seek_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_seek_test.cpp b/libs/FileManagerKit/tests/File_seek_test.cpp new file mode 100644 index 0000000000..3579f36a54 --- /dev/null +++ b/libs/FileManagerKit/tests/File_seek_test.cpp @@ -0,0 +1,43 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestSeek : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestSeek, seek) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + auto output_data = std::array {}; + auto expected_data = std::array {0x64, 0x65, 0x66, 0x00, 0x00, 0x00}; + + file.open(tempFilename, "w+"); + + std::ignore = file.write(input_data); + file.seek(3); + auto bytes_read = file.read(output_data); + + ASSERT_EQ(3, bytes_read); + ASSERT_EQ(expected_data, output_data); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index d535288f6b..ae8c7a8393 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,22 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, seek) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - auto output_data = std::array {}; - auto expected_data = std::array {0x64, 0x65, 0x66, 0x00, 0x00, 0x00}; - - file.open(tempFilename, "w+"); - - std::ignore = file.write(input_data); - file.seek(3); - auto bytes_read = file.read(output_data); - - ASSERT_EQ(3, bytes_read); - ASSERT_EQ(expected_data, output_data); -} - TEST_F(FileTest, tellNoFile) { auto pos = file.tell(); From 1e9fbdfd641b8a48403470dd5f6fe15c489c221e Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:08:57 +0100 Subject: [PATCH 056/143] :white_check_mark: (file): Move tell UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + libs/FileManagerKit/tests/File_tell_test.cpp | 53 ++++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 26 ---------- 3 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_tell_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index d68a31f740..ba351c5e7b 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -31,6 +31,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_read_test.cpp tests/File_size_test.cpp tests/File_seek_test.cpp + tests/File_tell_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_tell_test.cpp b/libs/FileManagerKit/tests/File_tell_test.cpp new file mode 100644 index 0000000000..4421f366b9 --- /dev/null +++ b/libs/FileManagerKit/tests/File_tell_test.cpp @@ -0,0 +1,53 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestTell : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestTell, tellNoFile) +{ + auto pos = file.tell(); + ASSERT_EQ(0, pos); +} + +TEST_F(FileTestTell, tellEmptyFile) +{ + file.open(tempFilename); + auto pos = file.tell(); + ASSERT_EQ(0, pos); +} + +TEST_F(FileTestTell, tellFile) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w"); + file.write(input_data); + file.seek(3); + + auto actual_pos = file.tell(); + + ASSERT_EQ(3, actual_pos); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index ae8c7a8393..d51a2f6e24 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,32 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, tellNoFile) -{ - auto pos = file.tell(); - ASSERT_EQ(0, pos); -} - -TEST_F(FileTest, tellEmptyFile) -{ - file.open(tempFilename); - auto pos = file.tell(); - ASSERT_EQ(0, pos); -} - -TEST_F(FileTest, tellFile) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w"); - file.write(input_data); - file.seek(3); - - auto actual_pos = file.tell(); - - ASSERT_EQ(3, actual_pos); -} - TEST_F(FileTest, reopenNoFile) { auto reopen = file.reopen(tempFilename, "w"); From 5af757c33c20e33a91cf068a2f8b93a4d7409908 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:12:33 +0100 Subject: [PATCH 057/143] :white_check_mark: (file): Move reopen UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + .../FileManagerKit/tests/File_reopen_test.cpp | 98 +++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 57 ----------- 3 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_reopen_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index ba351c5e7b..b398b80a6e 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -32,6 +32,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_size_test.cpp tests/File_seek_test.cpp tests/File_tell_test.cpp + tests/File_reopen_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_reopen_test.cpp b/libs/FileManagerKit/tests/File_reopen_test.cpp new file mode 100644 index 0000000000..b5f669e126 --- /dev/null +++ b/libs/FileManagerKit/tests/File_reopen_test.cpp @@ -0,0 +1,98 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestReopen : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + auto readTempFile() -> std::string + { + std::fstream f {}; + f.open(tempFilename); + + std::stringstream out {}; + out << f.rdbuf(); + f.close(); + + return out.str(); + } + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestReopen, reopenNoFile) +{ + auto reopen = file.reopen(tempFilename, "w"); + + ASSERT_FALSE(reopen); +} + +TEST_F(FileTestReopen, openThenReopenFile) +{ + file.open(tempFilename, "r"); + + auto reopen = file.reopen(tempFilename, "w"); + + ASSERT_TRUE(reopen); +} + +TEST_F(FileTestReopen, reopenNoFileWithFileSystemPath) +{ + auto reopen = file.reopen(tempFilename_filesystem_path, "w"); + + ASSERT_FALSE(reopen); +} + +TEST_F(FileTestReopen, openThenReopenFileWithFileSystemPath) +{ + file.open(tempFilename_filesystem_path, "r"); + + auto reopen = file.reopen(tempFilename_filesystem_path, "w"); + + ASSERT_TRUE(reopen); +} + +TEST_F(FileTestReopen, closeThenReopenFile) +{ + file.open(tempFilename, "r"); + + file.close(); + + auto reopen = file.reopen(tempFilename, "w"); + + ASSERT_FALSE(reopen); +} + +TEST_F(FileTestReopen, reopen) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "r"); + file.reopen(tempFilename, "w"); + file.write(input_data); + file.close(); + + auto output_data_w = readTempFile(); + + ASSERT_EQ("abcdef", output_data_w); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index d51a2f6e24..4724636bf6 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,63 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, reopenNoFile) -{ - auto reopen = file.reopen(tempFilename, "w"); - - ASSERT_FALSE(reopen); -} - -TEST_F(FileTest, openThenReopenFile) -{ - file.open(tempFilename, "r"); - - auto reopen = file.reopen(tempFilename, "w"); - - ASSERT_TRUE(reopen); -} - -TEST_F(FileTest, reopenNoFileWithFileSystemPath) -{ - auto reopen = file.reopen(tempFilename_filesystem_path, "w"); - - ASSERT_FALSE(reopen); -} - -TEST_F(FileTest, openThenReopenFileWithFileSystemPath) -{ - file.open(tempFilename_filesystem_path, "r"); - - auto reopen = file.reopen(tempFilename_filesystem_path, "w"); - - ASSERT_TRUE(reopen); -} - -TEST_F(FileTest, closeThenReopenFile) -{ - file.open(tempFilename, "r"); - - file.close(); - - auto reopen = file.reopen(tempFilename, "w"); - - ASSERT_FALSE(reopen); -} - -TEST_F(FileTest, reopen) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "r"); - file.reopen(tempFilename, "w"); - file.write(input_data); - file.close(); - - auto output_data_w = readTempFile(); - - ASSERT_EQ("abcdef", output_data_w); -} - TEST_F(FileTest, setBufferSpanNoFile) { auto buffer = std::array {}; From 0ffeb497874a02521ff33123c37f27e36438afd1 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:15:29 +0100 Subject: [PATCH 058/143] :white_check_mark: (file): Move set/unset buffer UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + .../FileManagerKit/tests/File_buffer_test.cpp | 170 ++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 129 ------------- 3 files changed, 171 insertions(+), 129 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_buffer_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index b398b80a6e..b931cae771 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -33,6 +33,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_seek_test.cpp tests/File_tell_test.cpp tests/File_reopen_test.cpp + tests/File_buffer_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_buffer_test.cpp b/libs/FileManagerKit/tests/File_buffer_test.cpp new file mode 100644 index 0000000000..23a55d98e6 --- /dev/null +++ b/libs/FileManagerKit/tests/File_buffer_test.cpp @@ -0,0 +1,170 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestBuffer : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + auto readTempFile() -> std::string + { + std::fstream f {}; + f.open(tempFilename); + + std::stringstream out {}; + out << f.rdbuf(); + f.close(); + + return out.str(); + } + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestBuffer, setBufferSpanNoFile) +{ + auto buffer = std::array {}; + + auto buffering = file.setBuffer(buffer); + + ASSERT_FALSE(buffering); +} + +TEST_F(FileTestBuffer, setBufferSpan) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w"); + + auto buffer = std::array {}; + + auto buffering = file.setBuffer(buffer); + + ASSERT_TRUE(buffering); + + file.write(input_data); + + for (int i = 0; i < input_data.size(); i++) { + ASSERT_EQ(input_data[i], buffer[i]); + } + + auto output_data = readTempFile(); + + ASSERT_EQ("", output_data); +} + +TEST_F(FileTestBuffer, setBufferCharNoFile) +{ + char buffer[BUFSIZ]; + + auto buffering = file.setBuffer(buffer, static_cast(BUFSIZ)); + + ASSERT_FALSE(buffering); +} + +TEST_F(FileTestBuffer, setBufferChar) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w"); + + char buffer[BUFSIZ]; + + auto buffering = file.setBuffer(buffer, static_cast(BUFSIZ)); + + ASSERT_TRUE(buffering); + + file.write(input_data); + + for (int i = 0; i < input_data.size(); i++) { + ASSERT_EQ(input_data[i], buffer[i]); + } + + auto output_data = readTempFile(); + + ASSERT_EQ("", output_data); +} + +TEST_F(FileTestBuffer, unsetBufferNoFile) +{ + auto noBuffering = file.unsetBuffer(); + + ASSERT_FALSE(noBuffering); +} + +TEST_F(FileTestBuffer, unsetBufferFile) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w"); + + auto noBuffering = file.unsetBuffer(); + + ASSERT_TRUE(noBuffering); + + file.write(input_data); + + auto output_data = readTempFile(); + + ASSERT_EQ("abcdef", output_data); +} + +TEST_F(FileTestBuffer, setBufferThenUnsetBuffer) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w"); + + auto buffer = std::array {}; + + auto buffering = file.setBuffer(buffer); + + ASSERT_TRUE(buffering); + + file.write(input_data); + + for (int i = 0; i < input_data.size(); i++) { + ASSERT_EQ(input_data[i], buffer[i]); + } + + auto output_data_buffer_set = readTempFile(); + + ASSERT_EQ("", output_data_buffer_set); + + std::fill(std::begin(buffer), std::end(buffer), 0); + + auto noBuffering = file.unsetBuffer(); + + ASSERT_TRUE(noBuffering); + + file.rewind(); + + file.write(input_data); + + for (int i = 0; i < input_data.size(); i++) { + ASSERT_EQ(0, buffer[i]); + } + + auto output_data_buffer_no_set = readTempFile(); + + ASSERT_EQ("abcdef", output_data_buffer_no_set); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index 4724636bf6..3fb33e76c4 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,135 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, setBufferSpanNoFile) -{ - auto buffer = std::array {}; - - auto buffering = file.setBuffer(buffer); - - ASSERT_FALSE(buffering); -} - -TEST_F(FileTest, setBufferSpan) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w"); - - auto buffer = std::array {}; - - auto buffering = file.setBuffer(buffer); - - ASSERT_TRUE(buffering); - - file.write(input_data); - - for (int i = 0; i < input_data.size(); i++) { - ASSERT_EQ(input_data[i], buffer[i]); - } - - auto output_data = readTempFile(); - - ASSERT_EQ("", output_data); -} - -TEST_F(FileTest, setBufferCharNoFile) -{ - char buffer[BUFSIZ]; - - auto buffering = file.setBuffer(buffer, static_cast(BUFSIZ)); - - ASSERT_FALSE(buffering); -} - -TEST_F(FileTest, setBufferChar) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w"); - - char buffer[BUFSIZ]; - - auto buffering = file.setBuffer(buffer, static_cast(BUFSIZ)); - - ASSERT_TRUE(buffering); - - file.write(input_data); - - for (int i = 0; i < input_data.size(); i++) { - ASSERT_EQ(input_data[i], buffer[i]); - } - - auto output_data = readTempFile(); - - ASSERT_EQ("", output_data); -} - -TEST_F(FileTest, unsetBufferNoFile) -{ - auto noBuffering = file.unsetBuffer(); - - ASSERT_FALSE(noBuffering); -} - -TEST_F(FileTest, unsetBufferFile) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w"); - - auto noBuffering = file.unsetBuffer(); - - ASSERT_TRUE(noBuffering); - - file.write(input_data); - - auto output_data = readTempFile(); - - ASSERT_EQ("abcdef", output_data); -} - -TEST_F(FileTest, setBufferThenUnsetBuffer) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w"); - - auto buffer = std::array {}; - - auto buffering = file.setBuffer(buffer); - - ASSERT_TRUE(buffering); - - file.write(input_data); - - for (int i = 0; i < input_data.size(); i++) { - ASSERT_EQ(input_data[i], buffer[i]); - } - - auto output_data_buffer_set = readTempFile(); - - ASSERT_EQ("", output_data_buffer_set); - - std::fill(std::begin(buffer), std::end(buffer), 0); - - auto noBuffering = file.unsetBuffer(); - - ASSERT_TRUE(noBuffering); - - file.rewind(); - - file.write(input_data); - - for (int i = 0; i < input_data.size(); i++) { - ASSERT_EQ(0, buffer[i]); - } - - auto output_data_buffer_no_set = readTempFile(); - - ASSERT_EQ("abcdef", output_data_buffer_no_set); -} - TEST_F(FileTest, flushNoFile) { auto flush = file.flush(); From 8b86dad6ef9d71021b54633f10d14b6134fb91cd Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:17:36 +0100 Subject: [PATCH 059/143] :white_check_mark: (file): Move flush UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + libs/FileManagerKit/tests/File_flush_test.cpp | 109 ++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 68 ----------- 3 files changed, 110 insertions(+), 68 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_flush_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index b931cae771..a4a0c7e0a8 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -34,6 +34,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_tell_test.cpp tests/File_reopen_test.cpp tests/File_buffer_test.cpp + tests/File_flush_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_flush_test.cpp b/libs/FileManagerKit/tests/File_flush_test.cpp new file mode 100644 index 0000000000..667a179aac --- /dev/null +++ b/libs/FileManagerKit/tests/File_flush_test.cpp @@ -0,0 +1,109 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestFlush : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + auto readTempFile() -> std::string + { + std::fstream f {}; + f.open(tempFilename); + + std::stringstream out {}; + out << f.rdbuf(); + f.close(); + + return out.str(); + } + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestFlush, flushNoFile) +{ + auto flush = file.flush(); + + ASSERT_FALSE(flush); +} + +TEST_F(FileTestFlush, flush) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w+"); + + file.write(input_data); + + auto flush = file.flush(); + + auto output_data = readTempFile(); + + ASSERT_EQ("abcdef", output_data); +} + +TEST_F(FileTestFlush, setBufferThenFlush) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w+"); + + auto buffer = std::array {}; + + auto buffering = file.setBuffer(buffer); + + ASSERT_TRUE(buffering); + + file.write(input_data); + + auto flush = file.flush(); + + auto output_data = readTempFile(); + + ASSERT_EQ("abcdef", output_data); +} + +TEST_F(FileTestFlush, setBufferNoFlushThenFlush) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w+"); + + auto buffer = std::array {}; + + auto buffering = file.setBuffer(buffer); + + ASSERT_TRUE(buffering); + + file.write(input_data); + + auto output_data_no_flush = readTempFile(); + + ASSERT_EQ("", output_data_no_flush); + + auto flush = file.flush(); + + auto output_data_flush = readTempFile(); + + ASSERT_EQ("abcdef", output_data_flush); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index 3fb33e76c4..2f91aa65f6 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,74 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, flushNoFile) -{ - auto flush = file.flush(); - - ASSERT_FALSE(flush); -} - -TEST_F(FileTest, flush) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w+"); - - file.write(input_data); - - auto flush = file.flush(); - - auto output_data = readTempFile(); - - ASSERT_EQ("abcdef", output_data); -} - -TEST_F(FileTest, setBufferThenFlush) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w+"); - - auto buffer = std::array {}; - - auto buffering = file.setBuffer(buffer); - - ASSERT_TRUE(buffering); - - file.write(input_data); - - auto flush = file.flush(); - - auto output_data = readTempFile(); - - ASSERT_EQ("abcdef", output_data); -} - -TEST_F(FileTest, setBufferNoFlushThenFlush) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w+"); - - auto buffer = std::array {}; - - auto buffering = file.setBuffer(buffer); - - ASSERT_TRUE(buffering); - - file.write(input_data); - - auto output_data_no_flush = readTempFile(); - - ASSERT_EQ("", output_data_no_flush); - - auto flush = file.flush(); - - auto output_data_flush = readTempFile(); - - ASSERT_EQ("abcdef", output_data_flush); -} - TEST_F(FileTest, errorNoFile) { auto error = file.error(); From 6f6cae3bef85ee74f9b500b62be46c380522ef0d Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:19:15 +0100 Subject: [PATCH 060/143] :white_check_mark: (file): Move error UTs --- libs/FileManagerKit/CMakeLists.txt | 1 + libs/FileManagerKit/tests/File_error_test.cpp | 137 ++++++++++++++++++ libs/FileManagerKit/tests/File_test.cpp | 95 ------------ 3 files changed, 138 insertions(+), 95 deletions(-) create mode 100644 libs/FileManagerKit/tests/File_error_test.cpp diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index a4a0c7e0a8..3365506bee 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -35,6 +35,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_reopen_test.cpp tests/File_buffer_test.cpp tests/File_flush_test.cpp + tests/File_error_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_error_test.cpp b/libs/FileManagerKit/tests/File_error_test.cpp new file mode 100644 index 0000000000..068a0173be --- /dev/null +++ b/libs/FileManagerKit/tests/File_error_test.cpp @@ -0,0 +1,137 @@ +// Leka - LekaOS +// Copyright 2021 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "FileManagerKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class FileTestError : public ::testing::Test +{ + protected: + void SetUp() override + { + strcpy(tempFilename, "/tmp/XXXXXX"); + mkstemp(tempFilename); + tempFilename_filesystem_path = tempFilename; + } + // void TearDown() override {} + + void writeTempFile(std::span data) + { + auto *file = fopen(tempFilename, "wb"); + fwrite(data.data(), sizeof(uint8_t), data.size(), file); + fclose(file); + } + + void writeTempFile(std::span data) + { + auto *file = fopen(tempFilename, "w"); + fwrite(data.data(), sizeof(char), data.size(), file); + fclose(file); + } + + FileManagerKit::File file {}; + char tempFilename[L_tmpnam]; // NOLINT + std::filesystem::path tempFilename_filesystem_path; +}; + +TEST_F(FileTestError, errorNoFile) +{ + auto error = file.error(); + + ASSERT_FALSE(error); +} + +TEST_F(FileTestError, errorWriting) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "w"); + + file.write(input_data); + + auto error = file.error(); + + ASSERT_FALSE(error); +} + +TEST_F(FileTestError, errorWritingModeRead) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "r"); + + file.write(input_data); + + auto error = file.error(); + + ASSERT_TRUE(error); +} + +TEST_F(FileTestError, errorReading) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + writeTempFile(input_data); + + file.open(tempFilename, "r"); + + auto output_data = std::array {}; + + file.read(output_data); + + auto error = file.error(); + + ASSERT_FALSE(error); +} + +TEST_F(FileTestError, errorReadingModeWrite) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + writeTempFile(input_data); + + file.open(tempFilename, "w"); + + auto output_data = std::array {}; + + file.read(output_data); + + auto error = file.error(); + + ASSERT_TRUE(error); +} + +TEST_F(FileTestError, clearErrorNoFile) +{ + file.clearerr(); + + auto error = file.error(); + + ASSERT_FALSE(error); +} + +TEST_F(FileTestError, clearError) +{ + auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" + + file.open(tempFilename, "r"); + + file.write(input_data); + + auto error = file.error(); + + ASSERT_TRUE(error); + + file.clearerr(); + + auto error_after_clear = file.error(); + + ASSERT_FALSE(error_after_clear); +} diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_test.cpp index 2f91aa65f6..0ad1cb8772 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_test.cpp @@ -57,101 +57,6 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, errorNoFile) -{ - auto error = file.error(); - - ASSERT_FALSE(error); -} - -TEST_F(FileTest, errorWriting) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "w"); - - file.write(input_data); - - auto error = file.error(); - - ASSERT_FALSE(error); -} - -TEST_F(FileTest, errorWritingModeRead) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "r"); - - file.write(input_data); - - auto error = file.error(); - - ASSERT_TRUE(error); -} - -TEST_F(FileTest, errorReading) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - writeTempFile(input_data); - - file.open(tempFilename, "r"); - - auto output_data = std::array {}; - - file.read(output_data); - - auto error = file.error(); - - ASSERT_FALSE(error); -} - -TEST_F(FileTest, errorReadingModeWrite) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - writeTempFile(input_data); - - file.open(tempFilename, "w"); - - auto output_data = std::array {}; - - file.read(output_data); - - auto error = file.error(); - - ASSERT_TRUE(error); -} - -TEST_F(FileTest, clearErrorNoFile) -{ - file.clearerr(); - - auto error = file.error(); - - ASSERT_FALSE(error); -} - -TEST_F(FileTest, clearError) -{ - auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" - - file.open(tempFilename, "r"); - - file.write(input_data); - - auto error = file.error(); - - ASSERT_TRUE(error); - - file.clearerr(); - - auto error_after_clear = file.error(); - - ASSERT_FALSE(error_after_clear); -} - TEST_F(FileTest, clear) { auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" From a99ab15c23b0885aa916f61050f51182a9f7b9ad Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 14:20:17 +0100 Subject: [PATCH 061/143] :white_check_mark: (file): Move clear UTs --- libs/FileManagerKit/CMakeLists.txt | 2 +- .../{File_test.cpp => File_clear_test.cpp} | 24 ++++--------------- 2 files changed, 5 insertions(+), 21 deletions(-) rename libs/FileManagerKit/tests/{File_test.cpp => File_clear_test.cpp} (76%) diff --git a/libs/FileManagerKit/CMakeLists.txt b/libs/FileManagerKit/CMakeLists.txt index 3365506bee..59b60b3e21 100644 --- a/libs/FileManagerKit/CMakeLists.txt +++ b/libs/FileManagerKit/CMakeLists.txt @@ -24,7 +24,6 @@ target_link_libraries(FileManagerKit if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") leka_unit_tests_sources( - tests/File_test.cpp tests/File_initialization_test.cpp tests/File_open_close_test.cpp tests/File_write_test.cpp @@ -36,6 +35,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/File_buffer_test.cpp tests/File_flush_test.cpp tests/File_error_test.cpp + tests/File_clear_test.cpp tests/File_sha256_test.cpp tests/FileReception_test.cpp tests/FileManagerKit_test.cpp diff --git a/libs/FileManagerKit/tests/File_test.cpp b/libs/FileManagerKit/tests/File_clear_test.cpp similarity index 76% rename from libs/FileManagerKit/tests/File_test.cpp rename to libs/FileManagerKit/tests/File_clear_test.cpp index 0ad1cb8772..7b249a69b2 100644 --- a/libs/FileManagerKit/tests/File_test.cpp +++ b/libs/FileManagerKit/tests/File_clear_test.cpp @@ -3,19 +3,15 @@ // SPDX-License-Identifier: Apache-2.0 #include -#include -#include +#include #include -#include #include "FileManagerKit.h" -#include "LogKit.h" -#include "filesystem" #include "gtest/gtest.h" using namespace leka; -class FileTest : public ::testing::Test +class FileTestClear : public ::testing::Test { protected: void SetUp() override @@ -26,18 +22,6 @@ class FileTest : public ::testing::Test } // void TearDown() override {} - auto readTempFile() -> std::string - { - std::fstream f {}; - f.open(tempFilename); - - std::stringstream out {}; - out << f.rdbuf(); - f.close(); - - return out.str(); - } - void writeTempFile(std::span data) { auto *file = fopen(tempFilename, "wb"); @@ -57,7 +41,7 @@ class FileTest : public ::testing::Test std::filesystem::path tempFilename_filesystem_path; }; -TEST_F(FileTest, clear) +TEST_F(FileTestClear, clear) { auto input_data = std::to_array({0x61, 0x62, 0x63, 0x64, 0x65, 0x66}); // "abcdef" @@ -75,7 +59,7 @@ TEST_F(FileTest, clear) file.close(); } -TEST_F(FileTest, clearNotExistingFile) +TEST_F(FileTestClear, clearNotExistingFile) { ASSERT_FALSE(file.is_open()); From ec7ab575e561c53c6bf4629bc758224871250670 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 25 Jan 2023 19:21:35 +0100 Subject: [PATCH 062/143] :recycle: (Graphics): Forward declare CGColor and CGPixel struct --- drivers/CoreVideo/include/CoreGraphics.hpp | 1 + drivers/CoreVideo/include/CoreVideo.hpp | 3 ++- drivers/CoreVideo/include/FilledRectangle.hpp | 19 +++++++++++++++++++ .../CoreVideo/include/interface/Graphics.hpp | 19 ++++++++++--------- drivers/CoreVideo/source/CoreGraphics.cpp | 2 +- drivers/CoreVideo/source/CoreVideo.cpp | 2 +- drivers/CoreVideo/tests/CoreGraphics_test.cpp | 2 +- drivers/CoreVideo/tests/CoreVideo_test.cpp | 4 ++-- .../include/internal/BouncingSquare.h | 3 ++- 9 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 drivers/CoreVideo/include/FilledRectangle.hpp diff --git a/drivers/CoreVideo/include/CoreGraphics.hpp b/drivers/CoreVideo/include/CoreGraphics.hpp index 6806dc1aa2..3daff71d26 100644 --- a/drivers/CoreVideo/include/CoreGraphics.hpp +++ b/drivers/CoreVideo/include/CoreGraphics.hpp @@ -6,6 +6,7 @@ #include "CGColor.hpp" #include "CoreLL.h" +#include "FilledRectangle.hpp" #include "interface/DMA2D.hpp" #include "interface/Graphics.hpp" #include "internal/corevideo_config.h" diff --git a/drivers/CoreVideo/include/CoreVideo.hpp b/drivers/CoreVideo/include/CoreVideo.hpp index cd59469fb4..b0f92e108f 100644 --- a/drivers/CoreVideo/include/CoreVideo.hpp +++ b/drivers/CoreVideo/include/CoreVideo.hpp @@ -4,6 +4,7 @@ #pragma once +#include "FilledRectangle.hpp" #include "interface/DMA2D.hpp" #include "interface/DSI.hpp" #include "interface/Font.hpp" @@ -34,7 +35,7 @@ class CoreVideo : public interface::Video void clearScreen() final; void clearScreen(CGColor color); - void displayRectangle(interface::Graphics::FilledRectangle rectangle, CGColor color); + void displayRectangle(FilledRectangle rectangle, CGColor color); void displayImage(interface::File &file, JPEGImageProperties *image_properties = nullptr) final; void setVideo(interface::File &file) final; diff --git a/drivers/CoreVideo/include/FilledRectangle.hpp b/drivers/CoreVideo/include/FilledRectangle.hpp new file mode 100644 index 0000000000..5f4a540a77 --- /dev/null +++ b/drivers/CoreVideo/include/FilledRectangle.hpp @@ -0,0 +1,19 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "CGPixel.hpp" + +namespace leka { + +struct FilledRectangle { + CGPoint origin {0, 0}; // * Top left corner by convention + uint16_t width {}; + uint16_t height {}; +}; + +} // namespace leka diff --git a/drivers/CoreVideo/include/interface/Graphics.hpp b/drivers/CoreVideo/include/interface/Graphics.hpp index f083d72cd3..c3035f2f09 100644 --- a/drivers/CoreVideo/include/interface/Graphics.hpp +++ b/drivers/CoreVideo/include/interface/Graphics.hpp @@ -4,23 +4,24 @@ #pragma once -#include "CGColor.hpp" -#include "CGPixel.hpp" +#include + +namespace leka { + +struct CGColor; +struct CGPoint; +struct FilledRectangle; + +} // namespace leka namespace leka::interface { class Graphics { public: - struct FilledRectangle { - CGPoint origin {0, 0}; // * Top left corner by convention - uint16_t width {}; - uint16_t height {}; - }; - virtual ~Graphics() = default; - virtual void clearScreen(CGColor color = CGColor::white) = 0; + virtual void clearScreen(CGColor color) = 0; virtual void drawRectangle(FilledRectangle rectangle, CGColor color) = 0; }; diff --git a/drivers/CoreVideo/source/CoreGraphics.cpp b/drivers/CoreVideo/source/CoreGraphics.cpp index 780c79d9b0..eb725789e9 100644 --- a/drivers/CoreVideo/source/CoreGraphics.cpp +++ b/drivers/CoreVideo/source/CoreGraphics.cpp @@ -10,7 +10,7 @@ CoreGraphics::CoreGraphics(interface::DMA2DBase &dma2d) : _dma2d(dma2d) {} void CoreGraphics::clearScreen(CGColor color) { - FilledRectangle rect; + FilledRectangle rect {}; rect.width = lcd::dimension::width; rect.height = lcd::dimension::height; diff --git a/drivers/CoreVideo/source/CoreVideo.cpp b/drivers/CoreVideo/source/CoreVideo.cpp index 83c0bb38b1..a5c57a9a62 100644 --- a/drivers/CoreVideo/source/CoreVideo.cpp +++ b/drivers/CoreVideo/source/CoreVideo.cpp @@ -70,7 +70,7 @@ void CoreVideo::clearScreen(CGColor color) _coregraphics.clearScreen(color); } -void CoreVideo::displayRectangle(interface::Graphics::FilledRectangle rectangle, CGColor color) +void CoreVideo::displayRectangle(FilledRectangle rectangle, CGColor color) { _coregraphics.drawRectangle(rectangle, color); } diff --git a/drivers/CoreVideo/tests/CoreGraphics_test.cpp b/drivers/CoreVideo/tests/CoreGraphics_test.cpp index 3cad87fd30..e70a1a7e0d 100644 --- a/drivers/CoreVideo/tests/CoreGraphics_test.cpp +++ b/drivers/CoreVideo/tests/CoreGraphics_test.cpp @@ -39,7 +39,7 @@ TEST_F(CoreGraphicsTest, drawRectangle) auto rectangle_height = 40; auto expected_color = CGColor::magenta; - CoreGraphics::FilledRectangle rectangle; + FilledRectangle rectangle {}; rectangle.origin.x = starting_pixel_column; rectangle.origin.y = starting_pixel_line; rectangle.width = rectangle_width; diff --git a/drivers/CoreVideo/tests/CoreVideo_test.cpp b/drivers/CoreVideo/tests/CoreVideo_test.cpp index c1c7db116b..70e402bdcf 100644 --- a/drivers/CoreVideo/tests/CoreVideo_test.cpp +++ b/drivers/CoreVideo/tests/CoreVideo_test.cpp @@ -132,7 +132,7 @@ TEST_F(CoreVideoTest, clearScreenWithColor) TEST_F(CoreVideoTest, drawRectangle) { - interface::Graphics::FilledRectangle rectangle; + FilledRectangle rectangle {}; rectangle.origin.x = 200; rectangle.origin.y = 369; rectangle.width = 11; @@ -147,7 +147,7 @@ TEST_F(CoreVideoTest, drawRectangle) TEST_F(CoreVideoTest, drawRectangleWithColor) { - interface::Graphics::FilledRectangle rectangle; + FilledRectangle rectangle {}; CGColor rectangle_color {0x2A, 0x2B, 0x2C}; EXPECT_CALL(graphicsmock, drawRectangle(_, compareColor(rectangle_color))).Times(1); diff --git a/libs/UIAnimationKit/include/internal/BouncingSquare.h b/libs/UIAnimationKit/include/internal/BouncingSquare.h index 6a040f3c73..b4b7911a2f 100644 --- a/libs/UIAnimationKit/include/internal/BouncingSquare.h +++ b/libs/UIAnimationKit/include/internal/BouncingSquare.h @@ -9,6 +9,7 @@ #include #include "CGAnimation.h" +#include "FilledRectangle.hpp" #include "interface/Graphics.hpp" namespace leka::animation { @@ -32,7 +33,7 @@ class BouncingSquare : public interface::CGAnimation interface::Graphics &_coregraphics; - interface::Graphics::FilledRectangle _square = {{0, 0}, 100, 100}; + FilledRectangle _square = {{0, 0}, 100, 100}; CGColor _color {.red = 0xFF, .green = 0x00, .blue = 0x00}; struct Shift { From aec856f2f093a3ff5a71255d22748a3eaef429b1 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 25 Jan 2023 19:28:24 +0100 Subject: [PATCH 063/143] :recycle: (Font): Forward declare CGColor and CGPixel struct --- drivers/CoreVideo/include/Character.hpp | 18 ++++++++++++++++ drivers/CoreVideo/include/CoreFont.hpp | 1 + drivers/CoreVideo/include/interface/Font.hpp | 22 +++++++++----------- drivers/CoreVideo/tests/CoreFont_test.cpp | 5 +++-- drivers/CoreVideo/tests/CoreVideo_test.cpp | 1 + 5 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 drivers/CoreVideo/include/Character.hpp diff --git a/drivers/CoreVideo/include/Character.hpp b/drivers/CoreVideo/include/Character.hpp new file mode 100644 index 0000000000..74c73e6c11 --- /dev/null +++ b/drivers/CoreVideo/include/Character.hpp @@ -0,0 +1,18 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "CGPixel.hpp" + +namespace leka { + +struct Character { + CGPoint origin {}; // Top left corner by convention + uint8_t ascii {}; // From 0x20 to 0x7F +}; + +} // namespace leka diff --git a/drivers/CoreVideo/include/CoreFont.hpp b/drivers/CoreVideo/include/CoreFont.hpp index 6be4ff3c84..2071475e03 100644 --- a/drivers/CoreVideo/include/CoreFont.hpp +++ b/drivers/CoreVideo/include/CoreFont.hpp @@ -6,6 +6,7 @@ #include "CGColor.hpp" #include "CGPixel.hpp" +#include "Character.hpp" #include "interface/Font.hpp" namespace leka { diff --git a/drivers/CoreVideo/include/interface/Font.hpp b/drivers/CoreVideo/include/interface/Font.hpp index ef93f63dbf..1a34f452f3 100644 --- a/drivers/CoreVideo/include/interface/Font.hpp +++ b/drivers/CoreVideo/include/interface/Font.hpp @@ -4,27 +4,25 @@ #pragma once -// #include +#include -#include "CGColor.hpp" -#include "CGPixel.hpp" +namespace leka { + +struct CGColor; +struct Character; + +} // namespace leka namespace leka::interface { class Font { public: - struct Character { - CGPoint origin {}; // Top left corner by convention - uint8_t ascii {}; // From 0x20 to 0x7F - }; - virtual ~Font() = default; - virtual void drawChar(Character character, CGColor foreground = CGColor::black, - CGColor background = CGColor::white) = 0; - virtual void display(const char *text, uint32_t size, uint32_t starting_line, CGColor foreground = CGColor::black, - CGColor background = CGColor::white) = 0; + virtual void drawChar(Character character, CGColor foreground, CGColor background) = 0; + virtual void display(const char *text, uint32_t size, uint32_t starting_line, CGColor foreground, + CGColor background) = 0; virtual auto fontGetFirstPixelAddress(char character) -> const uint8_t * = 0; virtual auto fontGetPixelBytes(const uint8_t *line_address) -> uint32_t = 0; diff --git a/drivers/CoreVideo/tests/CoreFont_test.cpp b/drivers/CoreVideo/tests/CoreFont_test.cpp index 64df6b39cb..d0bdd9c1f7 100644 --- a/drivers/CoreVideo/tests/CoreFont_test.cpp +++ b/drivers/CoreVideo/tests/CoreFont_test.cpp @@ -5,6 +5,7 @@ #include "CoreFont.hpp" #include "CGFont.hpp" +#include "Character.hpp" #include "gtest/gtest.h" #include "internal/corevideo_config.h" #include "mocks/leka/CoreLL.h" @@ -129,7 +130,7 @@ TEST_F(CoreFontTest, fontPixelIsOnWithC00010FF) TEST_F(CoreFontTest, drawCharacter) { - CoreFont::Character character; + Character character {}; character.ascii = '.'; auto pixels_per_char = graphics::font_pixel_width * graphics::font_pixel_height; // 17 * 24 = 408 @@ -143,7 +144,7 @@ TEST_F(CoreFontTest, drawCharacterWithColor) { // ENHANCEMENT: Set pixels_lit by checking number of bit set. - CoreFont::Character character; + Character character {}; character.ascii = '.'; CGColor foreground_color = CGColor::pure_red; CGColor background_color = CGColor::black; diff --git a/drivers/CoreVideo/tests/CoreVideo_test.cpp b/drivers/CoreVideo/tests/CoreVideo_test.cpp index 70e402bdcf..944b677a4b 100644 --- a/drivers/CoreVideo/tests/CoreVideo_test.cpp +++ b/drivers/CoreVideo/tests/CoreVideo_test.cpp @@ -4,6 +4,7 @@ #include "CoreVideo.hpp" +#include "Character.hpp" #include "gtest/gtest.h" #include "mocks/leka/CoreDMA2D.h" #include "mocks/leka/CoreDSI.h" From d832ec89d5166ceb32799432654e9818025bdfb3 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 12:31:29 +0100 Subject: [PATCH 064/143] :recycle: (video): Set CGGraphics with CGColor+CGPoint+CGPixel+Character+FilledRectangle --- .../include/{CGColor.hpp => CGGraphics.hpp} | 37 ++++++++++++++++++- drivers/CoreVideo/include/CGPixel.hpp | 36 ------------------ drivers/CoreVideo/include/Character.hpp | 18 --------- drivers/CoreVideo/include/CoreFont.hpp | 4 +- drivers/CoreVideo/include/CoreGraphics.hpp | 3 +- drivers/CoreVideo/include/CoreVideo.hpp | 2 +- drivers/CoreVideo/include/FilledRectangle.hpp | 19 ---------- drivers/CoreVideo/tests/CGColor_test.cpp | 3 +- drivers/CoreVideo/tests/CGPixel_test.cpp | 3 +- drivers/CoreVideo/tests/CoreFont_test.cpp | 1 - drivers/CoreVideo/tests/CoreVideo_test.cpp | 1 - .../include/internal/BouncingSquare.h | 2 +- spikes/lk_reinforcer/main.cpp | 2 +- 13 files changed, 43 insertions(+), 88 deletions(-) rename drivers/CoreVideo/include/{CGColor.hpp => CGGraphics.hpp} (56%) delete mode 100644 drivers/CoreVideo/include/CGPixel.hpp delete mode 100644 drivers/CoreVideo/include/Character.hpp delete mode 100644 drivers/CoreVideo/include/FilledRectangle.hpp diff --git a/drivers/CoreVideo/include/CGColor.hpp b/drivers/CoreVideo/include/CGGraphics.hpp similarity index 56% rename from drivers/CoreVideo/include/CGColor.hpp rename to drivers/CoreVideo/include/CGGraphics.hpp index 249fc8a58e..bc13bd00ca 100644 --- a/drivers/CoreVideo/include/CGColor.hpp +++ b/drivers/CoreVideo/include/CGGraphics.hpp @@ -1,11 +1,14 @@ // Leka - LekaOS -// Copyright 2021 APF France handicap +// Copyright 2023 APF France handicap // SPDX-License-Identifier: Apache-2.0 #pragma once #include +#include "CoreLL.h" +#include "internal/corevideo_config.h" + namespace leka { struct CGColor { @@ -37,4 +40,36 @@ constexpr CGColor CGColor::yellow {0xFF, 0xFF, 0x00}; constexpr CGColor CGColor::cyan {0x00, 0xFF, 0xFF}; constexpr CGColor CGColor::magenta {0xFF, 0x00, 0xFF}; +struct CGPoint { + uint32_t x = 0; + uint32_t y = 0; +}; + +struct CGPixel { + explicit CGPixel(CoreLL &ll) : corell(ll) {} + + CGPoint coordinates {0, 0}; + CoreLL &corell; + + void draw(CGColor color) + { + uintptr_t destination_address = + lcd::frame_buffer_address + (4 * (coordinates.y * lcd::dimension::width + coordinates.x)); + uint32_t destinationColor = color.getARGB(); + + corell.rawMemoryWrite(destination_address, destinationColor); + } +}; + +struct Character { + CGPoint origin {}; // Top left corner by convention + uint8_t ascii {}; // From 0x20 to 0x7F +}; + +struct FilledRectangle { + CGPoint origin {0, 0}; // * Top left corner by convention + uint16_t width {}; + uint16_t height {}; +}; + } // namespace leka diff --git a/drivers/CoreVideo/include/CGPixel.hpp b/drivers/CoreVideo/include/CGPixel.hpp deleted file mode 100644 index ab16fe439a..0000000000 --- a/drivers/CoreVideo/include/CGPixel.hpp +++ /dev/null @@ -1,36 +0,0 @@ -// Leka - LekaOS -// Copyright 2021 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "CGColor.hpp" -#include "CoreLL.h" -#include "internal/corevideo_config.h" - -namespace leka { - -struct CGPoint { - uint32_t x = 0; - uint32_t y = 0; -}; - -struct CGPixel { - explicit CGPixel(CoreLL &ll) : corell(ll) {} - - CGPoint coordinates {0, 0}; - CoreLL &corell; - - void draw(CGColor color) - { - uintptr_t destination_address = - lcd::frame_buffer_address + (4 * (coordinates.y * lcd::dimension::width + coordinates.x)); - uint32_t destinationColor = color.getARGB(); - - corell.rawMemoryWrite(destination_address, destinationColor); - } -}; - -} // namespace leka diff --git a/drivers/CoreVideo/include/Character.hpp b/drivers/CoreVideo/include/Character.hpp deleted file mode 100644 index 74c73e6c11..0000000000 --- a/drivers/CoreVideo/include/Character.hpp +++ /dev/null @@ -1,18 +0,0 @@ -// Leka - LekaOS -// Copyright 2023 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "CGPixel.hpp" - -namespace leka { - -struct Character { - CGPoint origin {}; // Top left corner by convention - uint8_t ascii {}; // From 0x20 to 0x7F -}; - -} // namespace leka diff --git a/drivers/CoreVideo/include/CoreFont.hpp b/drivers/CoreVideo/include/CoreFont.hpp index 2071475e03..6333d9873c 100644 --- a/drivers/CoreVideo/include/CoreFont.hpp +++ b/drivers/CoreVideo/include/CoreFont.hpp @@ -4,9 +4,7 @@ #pragma once -#include "CGColor.hpp" -#include "CGPixel.hpp" -#include "Character.hpp" +#include "CGGraphics.hpp" #include "interface/Font.hpp" namespace leka { diff --git a/drivers/CoreVideo/include/CoreGraphics.hpp b/drivers/CoreVideo/include/CoreGraphics.hpp index 3daff71d26..36568f7cca 100644 --- a/drivers/CoreVideo/include/CoreGraphics.hpp +++ b/drivers/CoreVideo/include/CoreGraphics.hpp @@ -4,9 +4,8 @@ #pragma once -#include "CGColor.hpp" +#include "CGGraphics.hpp" #include "CoreLL.h" -#include "FilledRectangle.hpp" #include "interface/DMA2D.hpp" #include "interface/Graphics.hpp" #include "internal/corevideo_config.h" diff --git a/drivers/CoreVideo/include/CoreVideo.hpp b/drivers/CoreVideo/include/CoreVideo.hpp index b0f92e108f..bf159facb8 100644 --- a/drivers/CoreVideo/include/CoreVideo.hpp +++ b/drivers/CoreVideo/include/CoreVideo.hpp @@ -4,7 +4,7 @@ #pragma once -#include "FilledRectangle.hpp" +#include "CGGraphics.hpp" #include "interface/DMA2D.hpp" #include "interface/DSI.hpp" #include "interface/Font.hpp" diff --git a/drivers/CoreVideo/include/FilledRectangle.hpp b/drivers/CoreVideo/include/FilledRectangle.hpp deleted file mode 100644 index 5f4a540a77..0000000000 --- a/drivers/CoreVideo/include/FilledRectangle.hpp +++ /dev/null @@ -1,19 +0,0 @@ -// Leka - LekaOS -// Copyright 2023 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "CGPixel.hpp" - -namespace leka { - -struct FilledRectangle { - CGPoint origin {0, 0}; // * Top left corner by convention - uint16_t width {}; - uint16_t height {}; -}; - -} // namespace leka diff --git a/drivers/CoreVideo/tests/CGColor_test.cpp b/drivers/CoreVideo/tests/CGColor_test.cpp index f432154e11..ccdc398cb3 100644 --- a/drivers/CoreVideo/tests/CGColor_test.cpp +++ b/drivers/CoreVideo/tests/CGColor_test.cpp @@ -2,8 +2,7 @@ // Copyright 2021 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "CGColor.hpp" - +#include "CGGraphics.hpp" #include "gtest/gtest.h" using namespace leka; diff --git a/drivers/CoreVideo/tests/CGPixel_test.cpp b/drivers/CoreVideo/tests/CGPixel_test.cpp index 5a47020abc..c492d22710 100644 --- a/drivers/CoreVideo/tests/CGPixel_test.cpp +++ b/drivers/CoreVideo/tests/CGPixel_test.cpp @@ -2,8 +2,7 @@ // Copyright 2021 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "CGPixel.hpp" - +#include "CGGraphics.hpp" #include "gtest/gtest.h" #include "mocks/leka/CoreLL.h" diff --git a/drivers/CoreVideo/tests/CoreFont_test.cpp b/drivers/CoreVideo/tests/CoreFont_test.cpp index d0bdd9c1f7..9b6d14c714 100644 --- a/drivers/CoreVideo/tests/CoreFont_test.cpp +++ b/drivers/CoreVideo/tests/CoreFont_test.cpp @@ -5,7 +5,6 @@ #include "CoreFont.hpp" #include "CGFont.hpp" -#include "Character.hpp" #include "gtest/gtest.h" #include "internal/corevideo_config.h" #include "mocks/leka/CoreLL.h" diff --git a/drivers/CoreVideo/tests/CoreVideo_test.cpp b/drivers/CoreVideo/tests/CoreVideo_test.cpp index 944b677a4b..70e402bdcf 100644 --- a/drivers/CoreVideo/tests/CoreVideo_test.cpp +++ b/drivers/CoreVideo/tests/CoreVideo_test.cpp @@ -4,7 +4,6 @@ #include "CoreVideo.hpp" -#include "Character.hpp" #include "gtest/gtest.h" #include "mocks/leka/CoreDMA2D.h" #include "mocks/leka/CoreDSI.h" diff --git a/libs/UIAnimationKit/include/internal/BouncingSquare.h b/libs/UIAnimationKit/include/internal/BouncingSquare.h index b4b7911a2f..427ee81947 100644 --- a/libs/UIAnimationKit/include/internal/BouncingSquare.h +++ b/libs/UIAnimationKit/include/internal/BouncingSquare.h @@ -9,7 +9,7 @@ #include #include "CGAnimation.h" -#include "FilledRectangle.hpp" +#include "CGGraphics.hpp" #include "interface/Graphics.hpp" namespace leka::animation { diff --git a/spikes/lk_reinforcer/main.cpp b/spikes/lk_reinforcer/main.cpp index 654aec28e6..2106890414 100644 --- a/spikes/lk_reinforcer/main.cpp +++ b/spikes/lk_reinforcer/main.cpp @@ -7,7 +7,7 @@ #include "drivers/HighResClock.h" #include "rtos/ThisThread.h" -#include "CGPixel.hpp" +#include "CGGraphics.hpp" #include "CoreAccelerometer.h" #include "CoreDMA2D.hpp" #include "CoreDSI.hpp" From 5034aa8339938b9573a16257218d0b8148d68422 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 12:42:13 +0100 Subject: [PATCH 065/143] :recycle: (cggraphics): Rename Character into CGCharacter --- drivers/CoreVideo/include/CGGraphics.hpp | 2 +- drivers/CoreVideo/include/CoreFont.hpp | 3 ++- drivers/CoreVideo/include/interface/Font.hpp | 6 +++--- drivers/CoreVideo/source/CoreFont.cpp | 4 ++-- drivers/CoreVideo/tests/CoreFont_test.cpp | 4 ++-- tests/unit/mocks/mocks/leka/CoreFont.h | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/drivers/CoreVideo/include/CGGraphics.hpp b/drivers/CoreVideo/include/CGGraphics.hpp index bc13bd00ca..85a8566ac0 100644 --- a/drivers/CoreVideo/include/CGGraphics.hpp +++ b/drivers/CoreVideo/include/CGGraphics.hpp @@ -61,7 +61,7 @@ struct CGPixel { } }; -struct Character { +struct CGCharacter { CGPoint origin {}; // Top left corner by convention uint8_t ascii {}; // From 0x20 to 0x7F }; diff --git a/drivers/CoreVideo/include/CoreFont.hpp b/drivers/CoreVideo/include/CoreFont.hpp index 6333d9873c..b2fb74f0ba 100644 --- a/drivers/CoreVideo/include/CoreFont.hpp +++ b/drivers/CoreVideo/include/CoreFont.hpp @@ -14,7 +14,8 @@ class CoreFont : public interface::Font public: explicit CoreFont(CGPixel &pixel_to_draw); - void drawChar(Character character, CGColor foreground = CGColor::black, CGColor background = CGColor::white) final; + void drawChar(CGCharacter character, CGColor foreground = CGColor::black, + CGColor background = CGColor::white) final; void display(const char *text, uint32_t size, uint32_t starting_line, CGColor foreground = CGColor::black, CGColor background = CGColor::white) final; diff --git a/drivers/CoreVideo/include/interface/Font.hpp b/drivers/CoreVideo/include/interface/Font.hpp index 1a34f452f3..0e214b59a6 100644 --- a/drivers/CoreVideo/include/interface/Font.hpp +++ b/drivers/CoreVideo/include/interface/Font.hpp @@ -9,7 +9,7 @@ namespace leka { struct CGColor; -struct Character; +struct CGCharacter; } // namespace leka @@ -20,9 +20,9 @@ class Font public: virtual ~Font() = default; - virtual void drawChar(Character character, CGColor foreground, CGColor background) = 0; + virtual void drawChar(CGCharacter character, CGColor foreground, CGColor background) = 0; virtual void display(const char *text, uint32_t size, uint32_t starting_line, CGColor foreground, - CGColor background) = 0; + CGColor background) = 0; virtual auto fontGetFirstPixelAddress(char character) -> const uint8_t * = 0; virtual auto fontGetPixelBytes(const uint8_t *line_address) -> uint32_t = 0; diff --git a/drivers/CoreVideo/source/CoreFont.cpp b/drivers/CoreVideo/source/CoreFont.cpp index 0a7838956c..5fa65424c7 100644 --- a/drivers/CoreVideo/source/CoreFont.cpp +++ b/drivers/CoreVideo/source/CoreFont.cpp @@ -33,7 +33,7 @@ auto CoreFont::fontPixelIsOn(uint32_t byte_of_line, uint8_t pixel_id) -> bool return is_on; } -void CoreFont::drawChar(Character character, CGColor foreground, CGColor background) +void CoreFont::drawChar(CGCharacter character, CGColor foreground, CGColor background) { _pixel_to_draw.coordinates.x = character.origin.x; _pixel_to_draw.coordinates.y = character.origin.y; @@ -62,7 +62,7 @@ void CoreFont::display(const char *text, uint32_t size, uint32_t starting_line, return; } - Character character; + auto character = CGCharacter {}; character.origin.x = 0; character.origin.y = (starting_line - 1) * graphics::font_pixel_height; diff --git a/drivers/CoreVideo/tests/CoreFont_test.cpp b/drivers/CoreVideo/tests/CoreFont_test.cpp index 9b6d14c714..7afbd1cd3f 100644 --- a/drivers/CoreVideo/tests/CoreFont_test.cpp +++ b/drivers/CoreVideo/tests/CoreFont_test.cpp @@ -129,7 +129,7 @@ TEST_F(CoreFontTest, fontPixelIsOnWithC00010FF) TEST_F(CoreFontTest, drawCharacter) { - Character character {}; + auto character = CGCharacter {}; character.ascii = '.'; auto pixels_per_char = graphics::font_pixel_width * graphics::font_pixel_height; // 17 * 24 = 408 @@ -143,7 +143,7 @@ TEST_F(CoreFontTest, drawCharacterWithColor) { // ENHANCEMENT: Set pixels_lit by checking number of bit set. - Character character {}; + auto character = CGCharacter {}; character.ascii = '.'; CGColor foreground_color = CGColor::pure_red; CGColor background_color = CGColor::black; diff --git a/tests/unit/mocks/mocks/leka/CoreFont.h b/tests/unit/mocks/mocks/leka/CoreFont.h index ca0317dfea..55fdbb2de2 100644 --- a/tests/unit/mocks/mocks/leka/CoreFont.h +++ b/tests/unit/mocks/mocks/leka/CoreFont.h @@ -12,7 +12,7 @@ namespace leka::mock { class CoreFont : public interface::Font { public: - MOCK_METHOD(void, drawChar, (Character character, CGColor foreground, CGColor background), (override)); + MOCK_METHOD(void, drawChar, (CGCharacter character, CGColor foreground, CGColor background), (override)); MOCK_METHOD(void, display, (const char *text, uint32_t size, uint32_t starting_line, CGColor foreground, CGColor background), (override)); From 1f7304fb07203551f9e8bd3e16f8145b31439b62 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 27 Jan 2023 12:53:02 +0100 Subject: [PATCH 066/143] :recycle: (cggraphics): Rename FilledRectangle into CGRectangle --- drivers/CoreVideo/include/CGGraphics.hpp | 2 +- drivers/CoreVideo/include/CoreGraphics.hpp | 2 +- drivers/CoreVideo/include/CoreVideo.hpp | 2 +- drivers/CoreVideo/include/interface/Graphics.hpp | 4 ++-- drivers/CoreVideo/source/CoreGraphics.cpp | 4 ++-- drivers/CoreVideo/source/CoreVideo.cpp | 2 +- drivers/CoreVideo/tests/CoreGraphics_test.cpp | 2 +- drivers/CoreVideo/tests/CoreVideo_test.cpp | 8 ++++---- libs/UIAnimationKit/include/internal/BouncingSquare.h | 2 +- tests/unit/mocks/mocks/leka/CoreGraphics.h | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/drivers/CoreVideo/include/CGGraphics.hpp b/drivers/CoreVideo/include/CGGraphics.hpp index 85a8566ac0..eda65985a1 100644 --- a/drivers/CoreVideo/include/CGGraphics.hpp +++ b/drivers/CoreVideo/include/CGGraphics.hpp @@ -66,7 +66,7 @@ struct CGCharacter { uint8_t ascii {}; // From 0x20 to 0x7F }; -struct FilledRectangle { +struct CGRectangle { CGPoint origin {0, 0}; // * Top left corner by convention uint16_t width {}; uint16_t height {}; diff --git a/drivers/CoreVideo/include/CoreGraphics.hpp b/drivers/CoreVideo/include/CoreGraphics.hpp index 36568f7cca..9202baf90a 100644 --- a/drivers/CoreVideo/include/CoreGraphics.hpp +++ b/drivers/CoreVideo/include/CoreGraphics.hpp @@ -19,7 +19,7 @@ class CoreGraphics : public interface::Graphics void clearScreen(CGColor color = CGColor::white) final; - void drawRectangle(FilledRectangle rectangle, CGColor color) final; + void drawRectangle(CGRectangle rectangle, CGColor color) final; private: interface::DMA2DBase &_dma2d; diff --git a/drivers/CoreVideo/include/CoreVideo.hpp b/drivers/CoreVideo/include/CoreVideo.hpp index bf159facb8..8f08c47eb2 100644 --- a/drivers/CoreVideo/include/CoreVideo.hpp +++ b/drivers/CoreVideo/include/CoreVideo.hpp @@ -35,7 +35,7 @@ class CoreVideo : public interface::Video void clearScreen() final; void clearScreen(CGColor color); - void displayRectangle(FilledRectangle rectangle, CGColor color); + void displayRectangle(CGRectangle rectangle, CGColor color); void displayImage(interface::File &file, JPEGImageProperties *image_properties = nullptr) final; void setVideo(interface::File &file) final; diff --git a/drivers/CoreVideo/include/interface/Graphics.hpp b/drivers/CoreVideo/include/interface/Graphics.hpp index c3035f2f09..5e48570eb0 100644 --- a/drivers/CoreVideo/include/interface/Graphics.hpp +++ b/drivers/CoreVideo/include/interface/Graphics.hpp @@ -10,7 +10,7 @@ namespace leka { struct CGColor; struct CGPoint; -struct FilledRectangle; +struct CGRectangle; } // namespace leka @@ -23,7 +23,7 @@ class Graphics virtual void clearScreen(CGColor color) = 0; - virtual void drawRectangle(FilledRectangle rectangle, CGColor color) = 0; + virtual void drawRectangle(CGRectangle rectangle, CGColor color) = 0; }; } // namespace leka::interface diff --git a/drivers/CoreVideo/source/CoreGraphics.cpp b/drivers/CoreVideo/source/CoreGraphics.cpp index eb725789e9..db28a3ac32 100644 --- a/drivers/CoreVideo/source/CoreGraphics.cpp +++ b/drivers/CoreVideo/source/CoreGraphics.cpp @@ -10,14 +10,14 @@ CoreGraphics::CoreGraphics(interface::DMA2DBase &dma2d) : _dma2d(dma2d) {} void CoreGraphics::clearScreen(CGColor color) { - FilledRectangle rect {}; + auto rect = CGRectangle {}; rect.width = lcd::dimension::width; rect.height = lcd::dimension::height; drawRectangle(rect, color); } -void CoreGraphics::drawRectangle(FilledRectangle rectangle, CGColor color) +void CoreGraphics::drawRectangle(CGRectangle rectangle, CGColor color) { uintptr_t destination_address = lcd::frame_buffer_address + 4 * (lcd::dimension::width * rectangle.origin.y + rectangle.origin.x); diff --git a/drivers/CoreVideo/source/CoreVideo.cpp b/drivers/CoreVideo/source/CoreVideo.cpp index a5c57a9a62..6ed561c6f6 100644 --- a/drivers/CoreVideo/source/CoreVideo.cpp +++ b/drivers/CoreVideo/source/CoreVideo.cpp @@ -70,7 +70,7 @@ void CoreVideo::clearScreen(CGColor color) _coregraphics.clearScreen(color); } -void CoreVideo::displayRectangle(FilledRectangle rectangle, CGColor color) +void CoreVideo::displayRectangle(CGRectangle rectangle, CGColor color) { _coregraphics.drawRectangle(rectangle, color); } diff --git a/drivers/CoreVideo/tests/CoreGraphics_test.cpp b/drivers/CoreVideo/tests/CoreGraphics_test.cpp index e70a1a7e0d..35faa5fffc 100644 --- a/drivers/CoreVideo/tests/CoreGraphics_test.cpp +++ b/drivers/CoreVideo/tests/CoreGraphics_test.cpp @@ -39,7 +39,7 @@ TEST_F(CoreGraphicsTest, drawRectangle) auto rectangle_height = 40; auto expected_color = CGColor::magenta; - FilledRectangle rectangle {}; + auto rectangle = CGRectangle {}; rectangle.origin.x = starting_pixel_column; rectangle.origin.y = starting_pixel_line; rectangle.width = rectangle_width; diff --git a/drivers/CoreVideo/tests/CoreVideo_test.cpp b/drivers/CoreVideo/tests/CoreVideo_test.cpp index 70e402bdcf..c6da7b4d72 100644 --- a/drivers/CoreVideo/tests/CoreVideo_test.cpp +++ b/drivers/CoreVideo/tests/CoreVideo_test.cpp @@ -54,7 +54,7 @@ MATCHER_P(compareColor, expected_color, "") return (same_red && same_green && same_blue); } -MATCHER_P(compareFilledRectangle, expected_rectangle, "") +MATCHER_P(compareCGRectangle, expected_rectangle, "") { bool same_origin_x = arg.origin.x == expected_rectangle.origin.x; bool same_origin_y = arg.origin.y == expected_rectangle.origin.y; @@ -132,7 +132,7 @@ TEST_F(CoreVideoTest, clearScreenWithColor) TEST_F(CoreVideoTest, drawRectangle) { - FilledRectangle rectangle {}; + auto rectangle = CGRectangle {}; rectangle.origin.x = 200; rectangle.origin.y = 369; rectangle.width = 11; @@ -140,14 +140,14 @@ TEST_F(CoreVideoTest, drawRectangle) CGColor rectangle_color; - EXPECT_CALL(graphicsmock, drawRectangle(compareFilledRectangle(rectangle), _)).Times(1); + EXPECT_CALL(graphicsmock, drawRectangle(compareCGRectangle(rectangle), _)).Times(1); corevideo.displayRectangle(rectangle, rectangle_color); } TEST_F(CoreVideoTest, drawRectangleWithColor) { - FilledRectangle rectangle {}; + auto rectangle = CGRectangle {}; CGColor rectangle_color {0x2A, 0x2B, 0x2C}; EXPECT_CALL(graphicsmock, drawRectangle(_, compareColor(rectangle_color))).Times(1); diff --git a/libs/UIAnimationKit/include/internal/BouncingSquare.h b/libs/UIAnimationKit/include/internal/BouncingSquare.h index 427ee81947..6d757af20a 100644 --- a/libs/UIAnimationKit/include/internal/BouncingSquare.h +++ b/libs/UIAnimationKit/include/internal/BouncingSquare.h @@ -33,7 +33,7 @@ class BouncingSquare : public interface::CGAnimation interface::Graphics &_coregraphics; - FilledRectangle _square = {{0, 0}, 100, 100}; + CGRectangle _square {{0, 0}, 100, 100}; CGColor _color {.red = 0xFF, .green = 0x00, .blue = 0x00}; struct Shift { diff --git a/tests/unit/mocks/mocks/leka/CoreGraphics.h b/tests/unit/mocks/mocks/leka/CoreGraphics.h index 3eb6b2b690..d5e4705d7e 100644 --- a/tests/unit/mocks/mocks/leka/CoreGraphics.h +++ b/tests/unit/mocks/mocks/leka/CoreGraphics.h @@ -13,7 +13,7 @@ class CoreGraphics : public interface::Graphics { public: MOCK_METHOD(void, clearScreen, (CGColor color), (override)); - MOCK_METHOD(void, drawRectangle, (FilledRectangle rectangle, CGColor color), (override)); + MOCK_METHOD(void, drawRectangle, (CGRectangle rectangle, CGColor color), (override)); }; } // namespace leka::mock From 61ca03c63cb3c117b70f24274789b47efefd0d8d Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 25 Jan 2023 15:36:28 +0100 Subject: [PATCH 067/143] :recycle: (eventflags): Remove unused EventFlags related code --- libs/LedKit/include/LedKit.h | 4 ---- libs/RobotKit/tests/RobotController_test.h | 1 - 2 files changed, 5 deletions(-) diff --git a/libs/LedKit/include/LedKit.h b/libs/LedKit/include/LedKit.h index 9d60005392..627b146467 100644 --- a/libs/LedKit/include/LedKit.h +++ b/libs/LedKit/include/LedKit.h @@ -18,10 +18,6 @@ class LedKit : public interface::LedKit static constexpr auto kNumberOfLedsEars = 2; static constexpr auto kNumberOfLedsBelt = 20; - struct flags { - static constexpr uint32_t START_LED_ANIMATION_FLAG = (1UL << 1); - }; - LedKit(interface::EventLoop &event_loop, interface::LED &ears, interface::LED &belt) : _event_loop(event_loop), _ears(ears), _belt(belt) {}; diff --git a/libs/RobotKit/tests/RobotController_test.h b/libs/RobotKit/tests/RobotController_test.h index f3ec24bf1d..c75205ab04 100644 --- a/libs/RobotKit/tests/RobotController_test.h +++ b/libs/RobotKit/tests/RobotController_test.h @@ -34,7 +34,6 @@ #include "mocks/leka/Timeout.h" #include "mocks/leka/VideoKit.h" #include "mocks/mbed/DigitalOut.h" -#include "mocks/mbed/EventFlags.h" #include "stubs/leka/EventLoopKit.h" #include "stubs/mbed/Kernel.h" From 7b09e5aaf8ea02ba5d4899d2e2b07d6844112330 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 25 Jan 2023 11:25:57 +0100 Subject: [PATCH 068/143] :recycle: (video): Use EventLoop in VideoKit --- app/os/CMakeLists.txt | 1 + app/os/main.cpp | 3 ++- libs/VideoKit/include/VideoKit.h | 10 ++++---- libs/VideoKit/source/VideoKit.cpp | 36 +++++++++++++-------------- libs/VideoKit/tests/VideoKit_test.cpp | 6 ++--- spikes/lk_behavior_kit/CMakeLists.txt | 1 + spikes/lk_behavior_kit/main.cpp | 3 ++- spikes/lk_command_kit/CMakeLists.txt | 1 + spikes/lk_command_kit/main.cpp | 3 ++- spikes/lk_fs/main.cpp | 3 ++- spikes/lk_lcd/CMakeLists.txt | 1 + spikes/lk_lcd/main.cpp | 4 ++- spikes/lk_reinforcer/CMakeLists.txt | 1 + spikes/lk_reinforcer/main.cpp | 3 ++- 14 files changed, 43 insertions(+), 33 deletions(-) diff --git a/app/os/CMakeLists.txt b/app/os/CMakeLists.txt index 386f41c177..44e5ad1eb1 100644 --- a/app/os/CMakeLists.txt +++ b/app/os/CMakeLists.txt @@ -40,6 +40,7 @@ target_link_libraries(LekaOS CoreIMU IMUKit MotionKit + EventLoopKit ) target_link_custom_leka_targets(LekaOS) diff --git a/app/os/main.cpp b/app/os/main.cpp index 96e7768a03..90ddfc1e38 100644 --- a/app/os/main.cpp +++ b/app/os/main.cpp @@ -232,6 +232,7 @@ namespace motors { namespace display::internal { + auto event_loop = EventLoopKit {}; auto event_flags = CoreEventFlags {}; auto corell = CoreLL {}; @@ -253,7 +254,7 @@ namespace display::internal { } // namespace display::internal -auto videokit = VideoKit {display::internal::event_flags, display::internal::corevideo}; +auto videokit = VideoKit {display::internal::event_loop, display::internal::event_flags, display::internal::corevideo}; namespace imu { diff --git a/libs/VideoKit/include/VideoKit.h b/libs/VideoKit/include/VideoKit.h index a24eba42cc..4727f8ddda 100644 --- a/libs/VideoKit/include/VideoKit.h +++ b/libs/VideoKit/include/VideoKit.h @@ -8,6 +8,7 @@ #include "interface/drivers/EventFlags.h" #include "interface/drivers/Video.h" +#include "interface/libs/EventLoop.h" #include "interface/libs/VideoKit.h" namespace leka { @@ -15,8 +16,8 @@ namespace leka { class VideoKit : public interface::VideoKit { public: - explicit VideoKit(interface::EventFlags &event_flags, interface::Video &video) - : _event_flags(event_flags), _video {video} + explicit VideoKit(interface::EventLoop &event_loop, interface::EventFlags &event_flags, interface::Video &video) + : _event_loop(event_loop), _event_flags(event_flags), _video {video} { // nothing to do } @@ -36,12 +37,11 @@ class VideoKit : public interface::VideoKit [[noreturn]] void run(); struct flags { - static constexpr uint32_t START_VIDEO_FLAG = (1UL << 1); - static constexpr uint32_t STOP_VIDEO_FLAG = (1UL << 2); + static constexpr uint32_t STOP_VIDEO_FLAG = (1UL << 2); }; private: - rtos::Thread _thread {}; + interface::EventLoop &_event_loop; interface::EventFlags &_event_flags; interface::Video &_video; diff --git a/libs/VideoKit/source/VideoKit.cpp b/libs/VideoKit/source/VideoKit.cpp index 048966d485..d6a919034a 100644 --- a/libs/VideoKit/source/VideoKit.cpp +++ b/libs/VideoKit/source/VideoKit.cpp @@ -26,7 +26,7 @@ void VideoKit::initializeScreen() _video.setBrightness(1.F); _video.clearScreen(); - _thread.start([this] { run(); }); + _event_loop.registerCallback([this] { run(); }); } void VideoKit::displayImage(const std::filesystem::path &path) @@ -39,6 +39,7 @@ void VideoKit::displayImage(const std::filesystem::path &path) if (auto file = FileManagerKit::File {path}; file.is_open()) { _event_flags.set(flags::STOP_VIDEO_FLAG); + _event_loop.stop(); _current_path = path; @@ -60,6 +61,7 @@ void VideoKit::fillWhiteBackgroundAndDisplayImage(const std::filesystem::path &p if (auto file = FileManagerKit::File {path}; file.is_open()) { _event_flags.set(flags::STOP_VIDEO_FLAG); + _event_loop.stop(); _current_path = path; @@ -80,14 +82,15 @@ void VideoKit::playVideoOnce(const std::filesystem::path &path, const std::funct file.close(); _event_flags.set(flags::STOP_VIDEO_FLAG); + _event_loop.stop(); _current_path = path; _must_loop = false; rtos::ThisThread::sleep_for(100ms); - _event_flags.set(flags::START_VIDEO_FLAG); _on_video_ended_callback = on_video_ended_callback; + _event_loop.start(); } } @@ -100,14 +103,15 @@ void VideoKit::playVideoOnRepeat(const std::filesystem::path &path, file.close(); _event_flags.set(flags::STOP_VIDEO_FLAG); + _event_loop.stop(); _current_path = path; _must_loop = true; rtos::ThisThread::sleep_for(100ms); - _event_flags.set(flags::START_VIDEO_FLAG); _on_video_ended_callback = on_video_ended_callback; + _event_loop.start(); } } @@ -118,8 +122,6 @@ void VideoKit::stopVideo() void VideoKit::run() { - auto file = FileManagerKit::File {}; - auto keep_running = [this] { auto must_not_stop = !((_event_flags.get() & flags::STOP_VIDEO_FLAG) == flags::STOP_VIDEO_FLAG); auto is_still_playing = !_video.isLastFrame(); @@ -127,22 +129,18 @@ void VideoKit::run() return must_not_stop && (_must_loop || is_still_playing); }; - while (true) { - _event_flags.wait_any(flags::START_VIDEO_FLAG); - _event_flags.clear(flags::STOP_VIDEO_FLAG); - - file.open(_current_path); - _video.setVideo(file); + _event_flags.clear(flags::STOP_VIDEO_FLAG); - while (keep_running()) { - _video.displayNextFrameVideo(file); + auto file = FileManagerKit::File {_current_path}; + _video.setVideo(file); - rtos::ThisThread::sleep_for(1ms); - } + while (keep_running()) { + _video.displayNextFrameVideo(file); + rtos::ThisThread::sleep_for(1ms); + } - file.close(); - if (_on_video_ended_callback != nullptr) { - _on_video_ended_callback(); - } + file.close(); + if (_on_video_ended_callback != nullptr) { + _on_video_ended_callback(); } } diff --git a/libs/VideoKit/tests/VideoKit_test.cpp b/libs/VideoKit/tests/VideoKit_test.cpp index d5dde3d3ff..1b580ceb3c 100644 --- a/libs/VideoKit/tests/VideoKit_test.cpp +++ b/libs/VideoKit/tests/VideoKit_test.cpp @@ -7,6 +7,7 @@ #include "gtest/gtest.h" #include "mocks/leka/CoreVideo.h" #include "mocks/leka/EventFlags.h" +#include "stubs/leka/EventLoopKit.h" using namespace leka; @@ -24,9 +25,10 @@ class VideoKitTest : public ::testing::Test char temp_file_path[L_tmpnam]; // NOLINT + stub::EventLoopKit stub_event_loop {}; mock::EventFlags mock_event_flags {}; mock::CoreVideo mock_corevideo {}; - VideoKit video_kit {mock_event_flags, mock_corevideo}; + VideoKit video_kit {stub_event_loop, mock_event_flags, mock_corevideo}; }; TEST_F(VideoKitTest, initialization) @@ -99,7 +101,6 @@ TEST_F(VideoKitTest, fillWhiteBackgroundDisplayImageSamePathTwice) TEST_F(VideoKitTest, playVideoOnce) { EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::START_VIDEO_FLAG)); video_kit.playVideoOnce(temp_file_path); } @@ -107,7 +108,6 @@ TEST_F(VideoKitTest, playVideoOnce) TEST_F(VideoKitTest, playVideoOnRepeat) { EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::START_VIDEO_FLAG)); video_kit.playVideoOnRepeat(temp_file_path); } diff --git a/spikes/lk_behavior_kit/CMakeLists.txt b/spikes/lk_behavior_kit/CMakeLists.txt index 9b1aa8a6df..9a20949c37 100644 --- a/spikes/lk_behavior_kit/CMakeLists.txt +++ b/spikes/lk_behavior_kit/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries(spike_lk_behavior_kit BehaviorKit CorePwm CoreEventFlags + EventLoopKit ) target_link_custom_leka_targets(spike_lk_behavior_kit) diff --git a/spikes/lk_behavior_kit/main.cpp b/spikes/lk_behavior_kit/main.cpp index dd8d2a7dc9..71de821ffc 100644 --- a/spikes/lk_behavior_kit/main.cpp +++ b/spikes/lk_behavior_kit/main.cpp @@ -147,6 +147,7 @@ namespace display { namespace internal { + auto event_loop = EventLoopKit {}; auto event_flags = CoreEventFlags {}; auto corell = CoreLL {}; @@ -168,7 +169,7 @@ namespace display { } // namespace internal - auto videokit = VideoKit {internal::event_flags, internal::corevideo}; + auto videokit = VideoKit {internal::event_loop, internal::event_flags, internal::corevideo}; } // namespace display diff --git a/spikes/lk_command_kit/CMakeLists.txt b/spikes/lk_command_kit/CMakeLists.txt index aeb0a52c1e..df0d6a8c1f 100644 --- a/spikes/lk_command_kit/CMakeLists.txt +++ b/spikes/lk_command_kit/CMakeLists.txt @@ -29,6 +29,7 @@ target_link_libraries(spike_lk_command_kit IMUKit MotionKit CoreTimeout + EventLoopKit ) target_link_custom_leka_targets(spike_lk_command_kit) diff --git a/spikes/lk_command_kit/main.cpp b/spikes/lk_command_kit/main.cpp index a41dfae438..34fe66aaab 100644 --- a/spikes/lk_command_kit/main.cpp +++ b/spikes/lk_command_kit/main.cpp @@ -127,6 +127,7 @@ namespace display { namespace internal { + auto event_loop = EventLoopKit {}; auto event_flags = CoreEventFlags {}; auto corell = CoreLL {}; @@ -148,7 +149,7 @@ namespace internal { } // namespace internal -auto videokit = VideoKit {internal::event_flags, internal::corevideo}; +auto videokit = VideoKit {internal::event_loop, internal::event_flags, internal::corevideo}; } // namespace display diff --git a/spikes/lk_fs/main.cpp b/spikes/lk_fs/main.cpp index 70f2189ca6..205189e44f 100644 --- a/spikes/lk_fs/main.cpp +++ b/spikes/lk_fs/main.cpp @@ -60,6 +60,7 @@ namespace sd { namespace display::internal { + auto event_loop = EventLoopKit {}; auto event_flags = CoreEventFlags {}; auto corell = CoreLL {}; @@ -81,7 +82,7 @@ namespace display::internal { } // namespace display::internal -auto videokit = VideoKit {display::internal::event_flags, display::internal::corevideo}; +auto videokit = VideoKit {display::internal::event_loop, display::internal::event_flags, display::internal::corevideo}; } // namespace diff --git a/spikes/lk_lcd/CMakeLists.txt b/spikes/lk_lcd/CMakeLists.txt index 9b6b7df6aa..d6c90a323d 100644 --- a/spikes/lk_lcd/CMakeLists.txt +++ b/spikes/lk_lcd/CMakeLists.txt @@ -19,6 +19,7 @@ target_link_libraries(spike_lk_lcd CoreLL CoreSTM32Hal CoreEventFlags + EventLoopKit FileManagerKit VideoKit ) diff --git a/spikes/lk_lcd/main.cpp b/spikes/lk_lcd/main.cpp index b0e111ab6e..4af64f8d5e 100644 --- a/spikes/lk_lcd/main.cpp +++ b/spikes/lk_lcd/main.cpp @@ -24,6 +24,7 @@ #include "CoreSDRAM.hpp" #include "CoreSTM32Hal.h" #include "CoreVideo.hpp" +#include "EventLoopKit.h" #include "FATFileSystem.h" #include "FileManagerKit.h" #include "HelloWorld.h" @@ -39,6 +40,7 @@ FATFileSystem fatfs("fs"); namespace display::internal { +auto event_loop = EventLoopKit {}; auto event_flags = CoreEventFlags {}; auto corell = CoreLL {}; @@ -60,7 +62,7 @@ extern "C" auto corevideo = } // namespace display::internal -auto videokit = VideoKit {display::internal::event_flags, display::internal::corevideo}; +auto videokit = VideoKit {display::internal::event_loop, display::internal::event_flags, display::internal::corevideo}; auto file = FileManagerKit::File {}; diff --git a/spikes/lk_reinforcer/CMakeLists.txt b/spikes/lk_reinforcer/CMakeLists.txt index 19866582ca..c78ddce17a 100644 --- a/spikes/lk_reinforcer/CMakeLists.txt +++ b/spikes/lk_reinforcer/CMakeLists.txt @@ -22,6 +22,7 @@ target_link_libraries(spike_lk_reinforcer CorePwm CoreI2C CoreIMU + EventLoopKit IMUKit MotionKit ReinforcerKit diff --git a/spikes/lk_reinforcer/main.cpp b/spikes/lk_reinforcer/main.cpp index 2106890414..84fab1d3dd 100644 --- a/spikes/lk_reinforcer/main.cpp +++ b/spikes/lk_reinforcer/main.cpp @@ -163,6 +163,7 @@ auto motionkit = MotionKit {motor::left, motor::right, imukit, motion::internal: namespace display::internal { + auto event_loop = EventLoopKit {}; auto event_flags = CoreEventFlags {}; auto corell = CoreLL {}; @@ -184,7 +185,7 @@ namespace display::internal { } // namespace display::internal -auto videokit = VideoKit {display::internal::event_flags, display::internal::corevideo}; +auto videokit = VideoKit {display::internal::event_loop, display::internal::event_flags, display::internal::corevideo}; auto reinforcerkit = ReinforcerKit {videokit, ledkit, motionkit}; auto hello = HelloWorld {}; From b6832a9d88ce409d639a3f3865ed2f6bb649f882 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 25 Jan 2023 11:43:14 +0100 Subject: [PATCH 069/143] :recycle: (video): Use internal bool instead of EventFlags in VideoKit --- app/os/CMakeLists.txt | 1 - app/os/main.cpp | 6 ++---- libs/VideoKit/include/VideoKit.h | 11 +++-------- libs/VideoKit/source/VideoKit.cpp | 14 +++++++------- libs/VideoKit/tests/VideoKit_test.cpp | 14 +------------- spikes/lk_behavior_kit/CMakeLists.txt | 1 - spikes/lk_behavior_kit/main.cpp | 6 ++---- spikes/lk_command_kit/CMakeLists.txt | 1 - spikes/lk_command_kit/main.cpp | 6 ++---- spikes/lk_fs/CMakeLists.txt | 1 - spikes/lk_fs/main.cpp | 6 ++---- spikes/lk_lcd/CMakeLists.txt | 1 - spikes/lk_lcd/main.cpp | 6 ++---- spikes/lk_led_kit/CMakeLists.txt | 1 - spikes/lk_reinforcer/main.cpp | 5 ++--- 15 files changed, 23 insertions(+), 57 deletions(-) diff --git a/app/os/CMakeLists.txt b/app/os/CMakeLists.txt index 44e5ad1eb1..0e6ba00395 100644 --- a/app/os/CMakeLists.txt +++ b/app/os/CMakeLists.txt @@ -31,7 +31,6 @@ target_link_libraries(LekaOS VideoKit LedKit CoreLED - CoreEventFlags CoreBufferedSerial CoreRFIDReader RFIDKit diff --git a/app/os/main.cpp b/app/os/main.cpp index 90ddfc1e38..4e0b9e4441 100644 --- a/app/os/main.cpp +++ b/app/os/main.cpp @@ -16,7 +16,6 @@ #include "CoreBufferedSerial.h" #include "CoreDMA2D.hpp" #include "CoreDSI.hpp" -#include "CoreEventFlags.h" #include "CoreFlashIS25LP016D.h" #include "CoreFlashManagerIS25LP016D.h" #include "CoreFont.hpp" @@ -232,8 +231,7 @@ namespace motors { namespace display::internal { - auto event_loop = EventLoopKit {}; - auto event_flags = CoreEventFlags {}; + auto event_loop = EventLoopKit {}; auto corell = CoreLL {}; auto pixel = CGPixel {corell}; @@ -254,7 +252,7 @@ namespace display::internal { } // namespace display::internal -auto videokit = VideoKit {display::internal::event_loop, display::internal::event_flags, display::internal::corevideo}; +auto videokit = VideoKit {display::internal::event_loop, display::internal::corevideo}; namespace imu { diff --git a/libs/VideoKit/include/VideoKit.h b/libs/VideoKit/include/VideoKit.h index 4727f8ddda..17294b67cb 100644 --- a/libs/VideoKit/include/VideoKit.h +++ b/libs/VideoKit/include/VideoKit.h @@ -6,7 +6,6 @@ #include "rtos/Thread.h" -#include "interface/drivers/EventFlags.h" #include "interface/drivers/Video.h" #include "interface/libs/EventLoop.h" #include "interface/libs/VideoKit.h" @@ -16,8 +15,8 @@ namespace leka { class VideoKit : public interface::VideoKit { public: - explicit VideoKit(interface::EventLoop &event_loop, interface::EventFlags &event_flags, interface::Video &video) - : _event_loop(event_loop), _event_flags(event_flags), _video {video} + explicit VideoKit(interface::EventLoop &event_loop, interface::Video &video) + : _event_loop(event_loop), _video {video} { // nothing to do } @@ -36,18 +35,14 @@ class VideoKit : public interface::VideoKit [[noreturn]] void run(); - struct flags { - static constexpr uint32_t STOP_VIDEO_FLAG = (1UL << 2); - }; - private: interface::EventLoop &_event_loop; - interface::EventFlags &_event_flags; interface::Video &_video; std::filesystem::path _current_path {}; std::function _on_video_ended_callback {}; + bool _must_stop {false}; bool _must_loop {false}; }; diff --git a/libs/VideoKit/source/VideoKit.cpp b/libs/VideoKit/source/VideoKit.cpp index d6a919034a..13f0add6a9 100644 --- a/libs/VideoKit/source/VideoKit.cpp +++ b/libs/VideoKit/source/VideoKit.cpp @@ -38,7 +38,7 @@ void VideoKit::displayImage(const std::filesystem::path &path) } if (auto file = FileManagerKit::File {path}; file.is_open()) { - _event_flags.set(flags::STOP_VIDEO_FLAG); + _must_stop = true; _event_loop.stop(); _current_path = path; @@ -60,7 +60,7 @@ void VideoKit::fillWhiteBackgroundAndDisplayImage(const std::filesystem::path &p } if (auto file = FileManagerKit::File {path}; file.is_open()) { - _event_flags.set(flags::STOP_VIDEO_FLAG); + _must_stop = true; _event_loop.stop(); _current_path = path; @@ -81,7 +81,7 @@ void VideoKit::playVideoOnce(const std::filesystem::path &path, const std::funct if (auto file = FileManagerKit::File {path}; file.is_open()) { file.close(); - _event_flags.set(flags::STOP_VIDEO_FLAG); + _must_stop = true; _event_loop.stop(); _current_path = path; @@ -102,7 +102,7 @@ void VideoKit::playVideoOnRepeat(const std::filesystem::path &path, if (auto file = FileManagerKit::File {path}; file.is_open()) { file.close(); - _event_flags.set(flags::STOP_VIDEO_FLAG); + _must_stop = true; _event_loop.stop(); _current_path = path; @@ -117,19 +117,19 @@ void VideoKit::playVideoOnRepeat(const std::filesystem::path &path, void VideoKit::stopVideo() { - _event_flags.set(flags::STOP_VIDEO_FLAG); + _must_stop = true; } void VideoKit::run() { auto keep_running = [this] { - auto must_not_stop = !((_event_flags.get() & flags::STOP_VIDEO_FLAG) == flags::STOP_VIDEO_FLAG); + auto must_not_stop = !_must_stop; auto is_still_playing = !_video.isLastFrame(); return must_not_stop && (_must_loop || is_still_playing); }; - _event_flags.clear(flags::STOP_VIDEO_FLAG); + _must_stop = false; auto file = FileManagerKit::File {_current_path}; _video.setVideo(file); diff --git a/libs/VideoKit/tests/VideoKit_test.cpp b/libs/VideoKit/tests/VideoKit_test.cpp index 1b580ceb3c..7be74abb3a 100644 --- a/libs/VideoKit/tests/VideoKit_test.cpp +++ b/libs/VideoKit/tests/VideoKit_test.cpp @@ -6,7 +6,6 @@ #include "gtest/gtest.h" #include "mocks/leka/CoreVideo.h" -#include "mocks/leka/EventFlags.h" #include "stubs/leka/EventLoopKit.h" using namespace leka; @@ -26,9 +25,8 @@ class VideoKitTest : public ::testing::Test char temp_file_path[L_tmpnam]; // NOLINT stub::EventLoopKit stub_event_loop {}; - mock::EventFlags mock_event_flags {}; mock::CoreVideo mock_corevideo {}; - VideoKit video_kit {stub_event_loop, mock_event_flags, mock_corevideo}; + VideoKit video_kit {stub_event_loop, mock_corevideo}; }; TEST_F(VideoKitTest, initialization) @@ -47,7 +45,6 @@ TEST_F(VideoKitTest, initializeScreen) TEST_F(VideoKitTest, displayImage) { - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); EXPECT_CALL(mock_corevideo, displayImage); video_kit.displayImage(temp_file_path); @@ -60,7 +57,6 @@ TEST_F(VideoKitTest, displayImageFileDoesNotExist) TEST_F(VideoKitTest, displayImageSamePathTwice) { - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); EXPECT_CALL(mock_corevideo, displayImage).Times(1); video_kit.displayImage(temp_file_path); @@ -72,7 +68,6 @@ TEST_F(VideoKitTest, displayImageSamePathTwice) TEST_F(VideoKitTest, fillWhiteBackgroundDisplayImage) { - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); EXPECT_CALL(mock_corevideo, clearScreen); EXPECT_CALL(mock_corevideo, displayImage); @@ -86,7 +81,6 @@ TEST_F(VideoKitTest, fillWhiteBackgroundDisplayImageFileDoesNotExist) TEST_F(VideoKitTest, fillWhiteBackgroundDisplayImageSamePathTwice) { - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); EXPECT_CALL(mock_corevideo, clearScreen).Times(1); EXPECT_CALL(mock_corevideo, displayImage).Times(1); @@ -100,15 +94,11 @@ TEST_F(VideoKitTest, fillWhiteBackgroundDisplayImageSamePathTwice) TEST_F(VideoKitTest, playVideoOnce) { - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); - video_kit.playVideoOnce(temp_file_path); } TEST_F(VideoKitTest, playVideoOnRepeat) { - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); - video_kit.playVideoOnRepeat(temp_file_path); } @@ -124,7 +114,5 @@ TEST_F(VideoKitTest, playVideoOnRepeatFileDoesNotExist) TEST_F(VideoKitTest, stopVideo) { - EXPECT_CALL(mock_event_flags, set(VideoKit::flags::STOP_VIDEO_FLAG)); - video_kit.stopVideo(); } diff --git a/spikes/lk_behavior_kit/CMakeLists.txt b/spikes/lk_behavior_kit/CMakeLists.txt index 9a20949c37..346086848f 100644 --- a/spikes/lk_behavior_kit/CMakeLists.txt +++ b/spikes/lk_behavior_kit/CMakeLists.txt @@ -17,7 +17,6 @@ target_sources(spike_lk_behavior_kit target_link_libraries(spike_lk_behavior_kit BehaviorKit CorePwm - CoreEventFlags EventLoopKit ) diff --git a/spikes/lk_behavior_kit/main.cpp b/spikes/lk_behavior_kit/main.cpp index 71de821ffc..fbcb10f2e2 100644 --- a/spikes/lk_behavior_kit/main.cpp +++ b/spikes/lk_behavior_kit/main.cpp @@ -10,7 +10,6 @@ #include "BehaviorKit.h" #include "CoreDMA2D.hpp" #include "CoreDSI.hpp" -#include "CoreEventFlags.h" #include "CoreFont.hpp" #include "CoreGraphics.hpp" #include "CoreJPEG.hpp" @@ -147,8 +146,7 @@ namespace display { namespace internal { - auto event_loop = EventLoopKit {}; - auto event_flags = CoreEventFlags {}; + auto event_loop = EventLoopKit {}; auto corell = CoreLL {}; auto pixel = CGPixel {corell}; @@ -169,7 +167,7 @@ namespace display { } // namespace internal - auto videokit = VideoKit {internal::event_loop, internal::event_flags, internal::corevideo}; + auto videokit = VideoKit {internal::event_loop, internal::corevideo}; } // namespace display diff --git a/spikes/lk_command_kit/CMakeLists.txt b/spikes/lk_command_kit/CMakeLists.txt index df0d6a8c1f..1e9bf3303a 100644 --- a/spikes/lk_command_kit/CMakeLists.txt +++ b/spikes/lk_command_kit/CMakeLists.txt @@ -19,7 +19,6 @@ target_link_libraries(spike_lk_command_kit CoreMotor CorePwm CoreSPI - CoreEventFlags BehaviorKit CommandKit LedKit diff --git a/spikes/lk_command_kit/main.cpp b/spikes/lk_command_kit/main.cpp index 34fe66aaab..43665117e9 100644 --- a/spikes/lk_command_kit/main.cpp +++ b/spikes/lk_command_kit/main.cpp @@ -11,7 +11,6 @@ #include "CoreAccelerometer.h" #include "CoreDMA2D.hpp" #include "CoreDSI.hpp" -#include "CoreEventFlags.h" #include "CoreFont.hpp" #include "CoreGraphics.hpp" #include "CoreGyroscope.h" @@ -127,8 +126,7 @@ namespace display { namespace internal { - auto event_loop = EventLoopKit {}; - auto event_flags = CoreEventFlags {}; + auto event_loop = EventLoopKit {}; auto corell = CoreLL {}; auto pixel = CGPixel {corell}; @@ -149,7 +147,7 @@ namespace internal { } // namespace internal -auto videokit = VideoKit {internal::event_loop, internal::event_flags, internal::corevideo}; +auto videokit = VideoKit {internal::event_loop, internal::corevideo}; } // namespace display diff --git a/spikes/lk_fs/CMakeLists.txt b/spikes/lk_fs/CMakeLists.txt index 05638ad6b8..dcdb03f137 100644 --- a/spikes/lk_fs/CMakeLists.txt +++ b/spikes/lk_fs/CMakeLists.txt @@ -17,7 +17,6 @@ target_sources(spike_lk_fs ) target_link_libraries(spike_lk_fs - CoreEventFlags CorePwm CoreVideo EventLoopKit diff --git a/spikes/lk_fs/main.cpp b/spikes/lk_fs/main.cpp index 205189e44f..d122856396 100644 --- a/spikes/lk_fs/main.cpp +++ b/spikes/lk_fs/main.cpp @@ -9,7 +9,6 @@ #include "ComUtils.h" #include "CoreDMA2D.hpp" #include "CoreDSI.hpp" -#include "CoreEventFlags.h" #include "CoreFont.hpp" #include "CoreGraphics.hpp" #include "CoreJPEG.hpp" @@ -60,8 +59,7 @@ namespace sd { namespace display::internal { - auto event_loop = EventLoopKit {}; - auto event_flags = CoreEventFlags {}; + auto event_loop = EventLoopKit {}; auto corell = CoreLL {}; auto pixel = CGPixel {corell}; @@ -82,7 +80,7 @@ namespace display::internal { } // namespace display::internal -auto videokit = VideoKit {display::internal::event_loop, display::internal::event_flags, display::internal::corevideo}; +auto videokit = VideoKit {display::internal::event_loop, display::internal::corevideo}; } // namespace diff --git a/spikes/lk_lcd/CMakeLists.txt b/spikes/lk_lcd/CMakeLists.txt index d6c90a323d..05a7c65b86 100644 --- a/spikes/lk_lcd/CMakeLists.txt +++ b/spikes/lk_lcd/CMakeLists.txt @@ -18,7 +18,6 @@ target_link_libraries(spike_lk_lcd CoreVideo CoreLL CoreSTM32Hal - CoreEventFlags EventLoopKit FileManagerKit VideoKit diff --git a/spikes/lk_lcd/main.cpp b/spikes/lk_lcd/main.cpp index 4af64f8d5e..0599d37ce3 100644 --- a/spikes/lk_lcd/main.cpp +++ b/spikes/lk_lcd/main.cpp @@ -11,7 +11,6 @@ #include "./Videos.h" #include "CoreDMA2D.hpp" #include "CoreDSI.hpp" -#include "CoreEventFlags.h" #include "CoreFont.hpp" #include "CoreGraphics.hpp" #include "CoreJPEG.hpp" @@ -40,8 +39,7 @@ FATFileSystem fatfs("fs"); namespace display::internal { -auto event_loop = EventLoopKit {}; -auto event_flags = CoreEventFlags {}; +auto event_loop = EventLoopKit {}; auto corell = CoreLL {}; auto pixel = CGPixel {corell}; @@ -62,7 +60,7 @@ extern "C" auto corevideo = } // namespace display::internal -auto videokit = VideoKit {display::internal::event_loop, display::internal::event_flags, display::internal::corevideo}; +auto videokit = VideoKit {display::internal::event_loop, display::internal::corevideo}; auto file = FileManagerKit::File {}; diff --git a/spikes/lk_led_kit/CMakeLists.txt b/spikes/lk_led_kit/CMakeLists.txt index 0ff71a3475..02bf2170ad 100644 --- a/spikes/lk_led_kit/CMakeLists.txt +++ b/spikes/lk_led_kit/CMakeLists.txt @@ -18,7 +18,6 @@ target_link_libraries(spike_lk_led_kit CoreLED ColorKit LedKit - CoreEventFlags ) target_link_custom_leka_targets(spike_lk_led_kit) diff --git a/spikes/lk_reinforcer/main.cpp b/spikes/lk_reinforcer/main.cpp index 84fab1d3dd..af20d17b51 100644 --- a/spikes/lk_reinforcer/main.cpp +++ b/spikes/lk_reinforcer/main.cpp @@ -163,8 +163,7 @@ auto motionkit = MotionKit {motor::left, motor::right, imukit, motion::internal: namespace display::internal { - auto event_loop = EventLoopKit {}; - auto event_flags = CoreEventFlags {}; + auto event_loop = EventLoopKit {}; auto corell = CoreLL {}; auto pixel = CGPixel {corell}; @@ -185,7 +184,7 @@ namespace display::internal { } // namespace display::internal -auto videokit = VideoKit {display::internal::event_loop, display::internal::event_flags, display::internal::corevideo}; +auto videokit = VideoKit {display::internal::event_loop, display::internal::corevideo}; auto reinforcerkit = ReinforcerKit {videokit, ledkit, motionkit}; auto hello = HelloWorld {}; From fe27c7d8cd14db8d61386ff8c9a7184485763ae6 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 25 Jan 2023 15:27:06 +0100 Subject: [PATCH 070/143] :bug: (video): Prevent loop of VideoKit.run method --- libs/VideoKit/include/VideoKit.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/VideoKit/include/VideoKit.h b/libs/VideoKit/include/VideoKit.h index 17294b67cb..8b9d64d2de 100644 --- a/libs/VideoKit/include/VideoKit.h +++ b/libs/VideoKit/include/VideoKit.h @@ -33,7 +33,7 @@ class VideoKit : public interface::VideoKit void stopVideo() final; - [[noreturn]] void run(); + void run(); private: interface::EventLoop &_event_loop; From 16dbb6ee64b7f67dd48118550ce8efff5b0dfbf7 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 25 Jan 2023 15:57:05 +0100 Subject: [PATCH 071/143] :white_check_mark: (video): Add VideoKit.run method UTs --- libs/VideoKit/tests/VideoKit_test.cpp | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/libs/VideoKit/tests/VideoKit_test.cpp b/libs/VideoKit/tests/VideoKit_test.cpp index 7be74abb3a..61484eb576 100644 --- a/libs/VideoKit/tests/VideoKit_test.cpp +++ b/libs/VideoKit/tests/VideoKit_test.cpp @@ -10,6 +10,10 @@ using namespace leka; +using ::testing::InSequence; +using ::testing::MockFunction; +using ::testing::Return; + class VideoKitTest : public ::testing::Test { protected: @@ -116,3 +120,45 @@ TEST_F(VideoKitTest, stopVideo) { video_kit.stopVideo(); } + +TEST_F(VideoKitTest, run) +{ + auto n_frames = uint8_t {99}; + { + InSequence seq; + + EXPECT_CALL(mock_corevideo, setVideo).Times(1); + + for (auto i = 0; i < n_frames; i++) { + EXPECT_CALL(mock_corevideo, isLastFrame).WillOnce(Return(false)); + EXPECT_CALL(mock_corevideo, displayNextFrameVideo); + } + + EXPECT_CALL(mock_corevideo, isLastFrame).WillOnce(Return(true)); + } + + video_kit.run(); +} + +TEST_F(VideoKitTest, runVideoEndedCallback) +{ + auto dummy_function = MockFunction {}; + video_kit.playVideoOnce(temp_file_path, dummy_function.AsStdFunction()); + + auto n_frames = uint8_t {99}; + { + InSequence seq; + + EXPECT_CALL(mock_corevideo, setVideo).Times(1); + + for (auto i = 0; i < n_frames; i++) { + EXPECT_CALL(mock_corevideo, isLastFrame).WillOnce(Return(false)); + EXPECT_CALL(mock_corevideo, displayNextFrameVideo); + } + + EXPECT_CALL(mock_corevideo, isLastFrame).WillOnce(Return(true)); + EXPECT_CALL(dummy_function, Call).Times(1); + } + + video_kit.run(); +} From 6b307334a62961ee7db1f1533fa916d66afb1c75 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Thu, 26 Jan 2023 16:42:29 +0100 Subject: [PATCH 072/143] :sparkles: (ble): Add MagicCard service --- libs/BLEKit/CMakeLists.txt | 2 + libs/BLEKit/include/BLEServiceMagicCard.h | 68 +++++++++++ .../internal/ServicesCharacteristics.h | 10 ++ .../BLEKit/tests/BLEServiceMagicCard_test.cpp | 114 ++++++++++++++++++ 4 files changed, 194 insertions(+) create mode 100644 libs/BLEKit/include/BLEServiceMagicCard.h create mode 100644 libs/BLEKit/tests/BLEServiceMagicCard_test.cpp diff --git a/libs/BLEKit/CMakeLists.txt b/libs/BLEKit/CMakeLists.txt index 1982d5443d..1feb327bc5 100644 --- a/libs/BLEKit/CMakeLists.txt +++ b/libs/BLEKit/CMakeLists.txt @@ -21,6 +21,7 @@ target_sources(BLEKit target_link_libraries(BLEKit mbed-os CoreEventQueue + RFIDKit ) if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") @@ -34,6 +35,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/BLEServiceBattery_test.cpp tests/BLEServiceDeviceInformation_test.cpp tests/BLEServiceMonitoring_test.cpp + tests/BLEServiceMagicCard_test.cpp tests/BLEServiceFileExchange_test.cpp tests/BLEServiceUpdate_test.cpp ) diff --git a/libs/BLEKit/include/BLEServiceMagicCard.h b/libs/BLEKit/include/BLEServiceMagicCard.h new file mode 100644 index 0000000000..bb2a9e9add --- /dev/null +++ b/libs/BLEKit/include/BLEServiceMagicCard.h @@ -0,0 +1,68 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include + +#include "MagicCard.h" +#include "MemoryUtils.h" +#include "internal/BLEService.h" +#include "internal/ServicesCharacteristics.h" + +namespace leka { + +class BLEServiceMagicCard : public interface::BLEService +{ + public: + BLEServiceMagicCard() : interface::BLEService(service::magic_card::uuid, _characteristic_table) {}; + + void setMagicCard(MagicCard const &card) + { + sendID(card.getId()); + sendLanguage(card.getLanguage()); + } + + void onDataReceived(const data_received_handle_t ¶ms) final + { + // do nothing + } + + void onDataRequested(const data_requested_handle_t ¶ms) final + { + // do nothing + } + + private: + void sendID(uint16_t id) + { + _id[0] = utils::memory::getHighByte(id); + _id[1] = utils::memory::getLowByte(id); + + auto data = std::make_tuple(_id_characteristic.getValueHandle(), _id); + sendData(data); + } + + void sendLanguage(MagicCard::Language language) + { + _language = static_cast(language); + + auto data = std::make_tuple(_language_characteristic.getValueHandle(), std::span(&_language, 1)); + sendData(data); + } + + std::array _id {}; + ReadOnlyArrayGattCharacteristic _id_characteristic { + service::magic_card::characteristic::id, _id.begin(), GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY}; + + uint8_t _language {0x00}; + ReadOnlyGattCharacteristic _language_characteristic { + service::magic_card::characteristic::language, &_language, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY}; + + std::array _characteristic_table {&_id_characteristic, &_language_characteristic}; +}; + +} // namespace leka diff --git a/libs/BLEKit/include/internal/ServicesCharacteristics.h b/libs/BLEKit/include/internal/ServicesCharacteristics.h index 5959f24d62..2732ed367b 100644 --- a/libs/BLEKit/include/internal/ServicesCharacteristics.h +++ b/libs/BLEKit/include/internal/ServicesCharacteristics.h @@ -45,6 +45,16 @@ namespace monitoring { } // namespace monitoring +namespace magic_card { + inline constexpr UUID::LongUUIDBytes_t uuid = {"Magic Card"}; + + namespace characteristic { + inline constexpr UUID::LongUUIDBytes_t id = {"ID"}; + inline constexpr UUID::LongUUIDBytes_t language = {"Language"}; + } // namespace characteristic + +} // namespace magic_card + namespace file_exchange { inline constexpr uint16_t uuid = 0x8270; diff --git a/libs/BLEKit/tests/BLEServiceMagicCard_test.cpp b/libs/BLEKit/tests/BLEServiceMagicCard_test.cpp new file mode 100644 index 0000000000..621d60c153 --- /dev/null +++ b/libs/BLEKit/tests/BLEServiceMagicCard_test.cpp @@ -0,0 +1,114 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "BLEServiceMagicCard.h" +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace leka; + +using ::testing::MockFunction; + +class BLEServiceMagicCardTest : public testing::Test +{ + protected: + // void SetUp() override {} + // void TearDown() override {} + + BLEServiceMagicCard service_magic_card {}; + + std::array default_expected_file_path {}; + + BLEServiceMagicCard::data_received_handle_t data_received_handle {}; + BLEServiceMagicCard::data_requested_handle_t data_requested_handle {}; + + void onDataReceivedProcess(std::span data) + { + const auto packet_size = 100; + auto expected_complete_packet = static_cast(std::size(data) / packet_size); + + std::array packet_array; + + for (auto packet = 0; packet < expected_complete_packet; packet++) { + std::copy(std::begin(data) + (packet * packet_size), std::begin(data) + ((packet + 1) * packet_size), + packet_array.begin()); + + data_received_handle.len = packet_size; + data_received_handle.offset = packet * packet_size; + data_received_handle.data = packet_array.data(); + + service_magic_card.onDataReceived(data_received_handle); + } + + const auto remaining_bytes = static_cast(std::size(data) % packet_size); + + std::fill(std::begin(packet_array), std::end(packet_array), 0); + std::copy(std::end(data) - remaining_bytes, std::end(data), packet_array.begin()); + + data_received_handle.len = remaining_bytes; + data_received_handle.offset = expected_complete_packet * packet_size; + data_received_handle.data = packet_array.data(); + + service_magic_card.onDataReceived(data_received_handle); + } +}; + +TEST_F(BLEServiceMagicCardTest, initialisation) +{ + EXPECT_NE(&service_magic_card, nullptr); +} + +TEST_F(BLEServiceMagicCardTest, setMagicCard) +{ + auto expected_id = uint16_t {0x1234}; + auto expected_language = uint8_t {0x56}; + + auto rfid_tag = rfid::Tag {.data = { + 0x4C, + 0x45, + 0x4B, + 0x41, + utils::memory::getHighByte(expected_id), + utils::memory::getLowByte(expected_id), + expected_language, + }}; + auto magic_card = MagicCard {rfid_tag}; + + auto spy_callback = [&expected_id, &expected_language](const BLEServiceMagicCard::data_to_send_handle_t &handle) { + auto data = std::get<1>(handle); + + auto is_id = std::size(data) == 2; + auto is_language = std::size(data) == 1; + + if (is_id) { + auto actual_id = utils::memory::combineBytes({.high = data[0], .low = data[1]}); + EXPECT_EQ(actual_id, expected_id); + } + if (is_language) { + auto actual_language = data[0]; + EXPECT_EQ(actual_language, expected_language); + } + }; + service_magic_card.onDataReadyToSend(spy_callback); + + service_magic_card.setMagicCard(magic_card); +} + +TEST_F(BLEServiceMagicCardTest, onDataReceived) +{ + auto dummy_params = BLEServiceMagicCard::data_received_handle_t {}; + service_magic_card.onDataReceived(dummy_params); + + // nothing expected +} + +TEST_F(BLEServiceMagicCardTest, onDataRequested) +{ + auto dummy_params = BLEServiceMagicCard::data_requested_handle_t {}; + service_magic_card.onDataRequested(dummy_params); + + // nothing expected +} From 1f366b4ab5d5c34c8dea4020613b63fc32bacc99 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Thu, 26 Jan 2023 16:43:26 +0100 Subject: [PATCH 073/143] :children_crossing: (rc): Set MagicCard via BLE --- libs/RobotKit/include/RobotController.h | 14 ++++++++++---- libs/RobotKit/tests/RobotController_test.h | 2 +- .../RobotController_test_initializeComponents.cpp | 2 +- .../tests/RobotController_test_registerEvents.cpp | 10 ++++++++++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index 4bce3f6dcd..09d7f37715 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -12,6 +12,7 @@ #include "BLEServiceCommands.h" #include "BLEServiceDeviceInformation.h" #include "BLEServiceFileExchange.h" +#include "BLEServiceMagicCard.h" #include "BLEServiceMonitoring.h" #include "BLEServiceUpdate.h" @@ -423,7 +424,10 @@ class RobotController : public interface::RobotController // Setup callbacks for monitoring - _rfidkit.onTagActivated([this](const MagicCard &card) { onMagicCardAvailable(card); }); + _rfidkit.onTagActivated([this](const MagicCard &card) { + onMagicCardAvailable(card); + _service_magic_card.setMagicCard(card); + }); _battery_kit.onDataUpdated([this](uint8_t level) { auto is_charging = _battery.isCharging(); @@ -555,12 +559,14 @@ class RobotController : public interface::RobotController BLEServiceCommands _service_commands {}; BLEServiceDeviceInformation _service_device_information {}; BLEServiceMonitoring _service_monitoring {}; + BLEServiceMagicCard _service_magic_card {}; BLEServiceFileExchange _service_file_exchange {}; BLEServiceUpdate _service_update {}; - std::array services = { - &_service_battery, &_service_commands, &_service_device_information, - &_service_monitoring, &_service_file_exchange, &_service_update, + std::array services = { + &_service_battery, &_service_commands, &_service_device_information, + &_service_monitoring, &_service_magic_card, &_service_file_exchange, + &_service_update, }; uint8_t _emergency_stop_counter {0}; diff --git a/libs/RobotKit/tests/RobotController_test.h b/libs/RobotKit/tests/RobotController_test.h index c75205ab04..b94920d9eb 100644 --- a/libs/RobotKit/tests/RobotController_test.h +++ b/libs/RobotKit/tests/RobotController_test.h @@ -162,7 +162,7 @@ class RobotControllerTest : public testing::Test { InSequence seq; - EXPECT_CALL(mbed_mock_gatt, addService).Times(6); + EXPECT_CALL(mbed_mock_gatt, addService).Times(7); EXPECT_CALL(mbed_mock_gap, setEventHandler).Times(1); EXPECT_CALL(mbed_mock_gatt, setEventHandler).Times(1); diff --git a/libs/RobotKit/tests/RobotController_test_initializeComponents.cpp b/libs/RobotKit/tests/RobotController_test_initializeComponents.cpp index 32d6a8ef76..8ca125bf82 100644 --- a/libs/RobotKit/tests/RobotController_test_initializeComponents.cpp +++ b/libs/RobotKit/tests/RobotController_test_initializeComponents.cpp @@ -14,7 +14,7 @@ TEST_F(RobotControllerTest, initializeComponents) { InSequence seq; - EXPECT_CALL(mbed_mock_gatt, addService).Times(6); + EXPECT_CALL(mbed_mock_gatt, addService).Times(7); EXPECT_CALL(mbed_mock_gap, setEventHandler).Times(1); EXPECT_CALL(mbed_mock_gatt, setEventHandler).Times(1); diff --git a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp index 5449e41ddc..e6dd286b78 100644 --- a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp +++ b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp @@ -181,3 +181,13 @@ TEST_F(RobotControllerTest, onChargingBehaviorLevelAbove90) rc.onChargingBehavior(battery_level); } + +TEST_F(RobotControllerTest, onTagActivated) +{ + auto onTagActivated = rfidkit.getCallback(); + + // TODO: Specify which BLE service and what is expected if necessary + EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(2); + + onTagActivated(MagicCard::none); +} From 81468ddac217bc37265d033b2302e9e814f3ab71 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 13 Jan 2023 23:54:50 +0100 Subject: [PATCH 074/143] :sparkles: (ble): Add RobotName characteristic --- libs/BLEKit/CMakeLists.txt | 1 + libs/BLEKit/include/BLEServiceConfig.h | 63 +++++++ .../internal/ServicesCharacteristics.h | 10 ++ libs/BLEKit/tests/BLEServiceConfig_test.cpp | 161 ++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 libs/BLEKit/include/BLEServiceConfig.h create mode 100644 libs/BLEKit/tests/BLEServiceConfig_test.cpp diff --git a/libs/BLEKit/CMakeLists.txt b/libs/BLEKit/CMakeLists.txt index 1feb327bc5..71cfe413a3 100644 --- a/libs/BLEKit/CMakeLists.txt +++ b/libs/BLEKit/CMakeLists.txt @@ -35,6 +35,7 @@ if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") tests/BLEServiceBattery_test.cpp tests/BLEServiceDeviceInformation_test.cpp tests/BLEServiceMonitoring_test.cpp + tests/BLEServiceConfig_test.cpp tests/BLEServiceMagicCard_test.cpp tests/BLEServiceFileExchange_test.cpp tests/BLEServiceUpdate_test.cpp diff --git a/libs/BLEKit/include/BLEServiceConfig.h b/libs/BLEKit/include/BLEServiceConfig.h new file mode 100644 index 0000000000..2ade43a14d --- /dev/null +++ b/libs/BLEKit/include/BLEServiceConfig.h @@ -0,0 +1,63 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include + +#include "internal/BLEService.h" +#include "internal/ServicesCharacteristics.h" + +namespace leka { + +class BLEServiceConfig : public interface::BLEService +{ + public: + static constexpr uint8_t kMaxRobotNameSize {18}; + + BLEServiceConfig() : interface::BLEService(service::config::uuid, _characteristic_table) {} + + void setRobotName(std::span robot_name) + { + std::copy(std::begin(robot_name), std::begin(robot_name) + std::size(_robot_name), _robot_name.begin()); + + auto data = std::make_tuple(_robot_name_characteristic.getValueHandle(), _robot_name); + sendData(data); + } + + void onDataReceived(const data_received_handle_t ¶ms) final + { + if (params.handle == _robot_name_characteristic.getValueHandle() && params.len <= _robot_name.size()) { + _robot_name.fill('\0'); + std::copy(params.data, params.data + params.len, _robot_name.begin() + params.offset); + if (_on_robot_name_updated != nullptr) { + _on_robot_name_updated(_robot_name); + } + } + } + + void onRobotNameUpdated(const std::function &)> &callback) + { + _on_robot_name_updated = callback; + } + + void onDataRequested(const data_requested_handle_t ¶ms) final + { + // do nothing + } + + private: + std::array _robot_name {}; + ReadWriteArrayGattCharacteristic _robot_name_characteristic { + service::config::characteristic::robot_name, _robot_name.begin(), + GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY}; + std::function &)> _on_robot_name_updated {}; + + std::array _characteristic_table {&_robot_name_characteristic}; +}; + +} // namespace leka diff --git a/libs/BLEKit/include/internal/ServicesCharacteristics.h b/libs/BLEKit/include/internal/ServicesCharacteristics.h index 2732ed367b..dedab2fc6b 100644 --- a/libs/BLEKit/include/internal/ServicesCharacteristics.h +++ b/libs/BLEKit/include/internal/ServicesCharacteristics.h @@ -45,6 +45,16 @@ namespace monitoring { } // namespace monitoring +namespace config { + + inline constexpr uint16_t uuid = 0x6770; + + namespace characteristic { + inline constexpr uint16_t robot_name = 0x8278; + } // namespace characteristic + +} // namespace config + namespace magic_card { inline constexpr UUID::LongUUIDBytes_t uuid = {"Magic Card"}; diff --git a/libs/BLEKit/tests/BLEServiceConfig_test.cpp b/libs/BLEKit/tests/BLEServiceConfig_test.cpp new file mode 100644 index 0000000000..86738f7c2f --- /dev/null +++ b/libs/BLEKit/tests/BLEServiceConfig_test.cpp @@ -0,0 +1,161 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "BLEServiceConfig.h" +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace leka; + +using ::testing::MockFunction; + +MATCHER_P(compareArray, expected_array, "") +{ + bool same_content = true; + for (int i = 0; i < std::size(expected_array); i++) { + same_content &= expected_array[i] == arg[i]; + } + return same_content; +} + +class BLEServiceConfigTest : public testing::Test +{ + protected: + // void SetUp() override {} + // void TearDown() override {} + + BLEServiceConfig service_config {}; + + BLEServiceConfig::data_received_handle_t data_received_handle {}; + BLEServiceConfig::data_requested_handle_t data_requested_handle {}; + + void onDataReceivedProcess(std::span data) + { + const auto packet_size = 100; + auto expected_complete_packet = static_cast(std::size(data) / packet_size); + + std::array packet_array; + + for (auto packet = 0; packet < expected_complete_packet; packet++) { + std::copy(std::begin(data) + (packet * packet_size), std::begin(data) + ((packet + 1) * packet_size), + packet_array.begin()); + + data_received_handle.len = packet_size; + data_received_handle.offset = packet * packet_size; + data_received_handle.data = packet_array.data(); + + service_config.onDataReceived(data_received_handle); + } + + const auto remaining_bytes = static_cast(std::size(data) % packet_size); + + std::fill(std::begin(packet_array), std::end(packet_array), 0); + std::copy(std::end(data) - remaining_bytes, std::end(data), packet_array.begin()); + + data_received_handle.len = remaining_bytes; + data_received_handle.offset = expected_complete_packet * packet_size; + data_received_handle.data = packet_array.data(); + + service_config.onDataReceived(data_received_handle); + } +}; + +TEST_F(BLEServiceConfigTest, initialisation) +{ + EXPECT_NE(&service_config, nullptr); +} + +TEST_F(BLEServiceConfigTest, setRobotName) +{ + auto name = std::array {"Leka"}; + auto expected_name = std::array {"Leka"}; + auto actual_name = std::array {}; + + auto spy_callback = [&actual_name](const BLEServiceConfig::data_to_send_handle_t &handle) { + for (auto i = 0; i < std::size(actual_name); i++) { + actual_name.at(i) = std::get<1>(handle)[i]; + } + }; + service_config.onDataReadyToSend(spy_callback); + + service_config.setRobotName(name); + EXPECT_EQ(actual_name, expected_name); +} + +TEST_F(BLEServiceConfigTest, setRobotNameOversize) +{ + auto name = std::array { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + }; + auto expected_name = std::array { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + }; + auto actual_name = std::array {}; + + auto spy_callback = [&actual_name](const BLEServiceConfig::data_to_send_handle_t &handle) { + for (auto i = 0; i < std::size(actual_name); i++) { + actual_name.at(i) = std::get<1>(handle)[i]; + } + }; + service_config.onDataReadyToSend(spy_callback); + + service_config.setRobotName(name); + EXPECT_EQ(actual_name, expected_name); +} + +TEST_F(BLEServiceConfigTest, onRobotNameUpdated) +{ + auto name = std::array {"Leka"}; + auto expected_name = std::array {"Leka"}; + + MockFunction)> mock_callback {}; + service_config.onRobotNameUpdated(mock_callback.AsStdFunction()); + + EXPECT_CALL(mock_callback, Call(compareArray(expected_name))).Times(1); + onDataReceivedProcess(name); +} + +TEST_F(BLEServiceConfigTest, onRobotNameUpdatedOversize) +{ + auto name = std::array { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + }; + + MockFunction)> mock_callback {}; + service_config.onRobotNameUpdated(mock_callback.AsStdFunction()); + + EXPECT_CALL(mock_callback, Call).Times(0); + onDataReceivedProcess(name); +} + +TEST_F(BLEServiceConfigTest, onRobotNameUpdatedUnset) +{ + auto name = std::array {"Leka"}; + + onDataReceivedProcess(name); + + // nothing expected +} + +TEST_F(BLEServiceConfigTest, onRobotNameUpdatedNotSameHandle) +{ + auto name = std::array {"Leka"}; + + data_received_handle.handle = 0xFFFF; + + MockFunction)> mock_callback {}; + service_config.onRobotNameUpdated(mock_callback.AsStdFunction()); + + EXPECT_CALL(mock_callback, Call).Times(0); + onDataReceivedProcess(name); +} + +TEST_F(BLEServiceConfigTest, onDataRequested) +{ + service_config.onDataRequested(data_requested_handle); + + // nothing expected +} From ed68e0380721b61b1e53e11ca2628ee085c2a285 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 16 Jan 2023 12:51:55 +0100 Subject: [PATCH 075/143] :children_crossing: (rc): Store and set RobotName via BLE --- libs/RobotKit/CMakeLists.txt | 1 + libs/RobotKit/include/RobotController.h | 36 ++++++++++++++----- libs/RobotKit/tests/RobotController_test.h | 3 +- ...otController_test_initializeComponents.cpp | 3 +- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/libs/RobotKit/CMakeLists.txt b/libs/RobotKit/CMakeLists.txt index 140b0c7d9f..007a52b1f9 100644 --- a/libs/RobotKit/CMakeLists.txt +++ b/libs/RobotKit/CMakeLists.txt @@ -30,6 +30,7 @@ target_link_libraries(RobotKit CommandKit RFIDKit ActivityKit + ConfigKit ) if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index 09d7f37715..f4f83b8783 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -10,6 +10,7 @@ #include "BLEKit.h" #include "BLEServiceBattery.h" #include "BLEServiceCommands.h" +#include "BLEServiceConfig.h" #include "BLEServiceDeviceInformation.h" #include "BLEServiceFileExchange.h" #include "BLEServiceMagicCard.h" @@ -20,6 +21,7 @@ #include "BatteryKit.h" #include "BehaviorKit.h" #include "CommandKit.h" +#include "ConfigKit.h" #include "CoreMutex.h" #include "FileReception.h" #include "MagicCard.h" @@ -330,10 +332,19 @@ class RobotController : public interface::RobotController auto _os_version = _firmware_update.getCurrentVersion(); _service_device_information.setOSVersion(_os_version); - auto advertising_data = _ble.getAdvertisingData(); - advertising_data.name = reinterpret_cast(_serialnumberkit.getShortSerialNumber().data()); - advertising_data.version_major = _os_version.major; - advertising_data.version_minor = _os_version.minor; + auto short_serial_number_span = _serialnumberkit.getShortSerialNumber(); + auto short_serial_number_array = std::array {}; + std::copy_n(std::begin(short_serial_number_span), std::size(short_serial_number_span), + std::begin(short_serial_number_array)); + auto config_robot_name = Config {"robot_name", short_serial_number_array}; + _robot_name = _configkit.read(config_robot_name); + + _service_config.setRobotName(_robot_name); + + auto advertising_data = _ble.getAdvertisingData(); + advertising_data.name = reinterpret_cast(_robot_name.data()); + advertising_data.version_major = _os_version.major; + advertising_data.version_minor = _os_version.minor; advertising_data.version_revision = _os_version.revision; _ble.setAdvertisingData(advertising_data); @@ -473,6 +484,12 @@ class RobotController : public interface::RobotController _service_monitoring.onSoftReboot([] { system_reset(); }); + _service_config.onRobotNameUpdated( + [this](const std::array &robot_name) { + auto config_robot_name = Config {"robot_name"}; + std::ignore = _configkit.write(config_robot_name, robot_name); + }); + auto on_commands_received = [this](std::span _buffer) { raise(event::command_received {}); @@ -532,6 +549,9 @@ class RobotController : public interface::RobotController SerialNumberKit &_serialnumberkit; + ConfigKit _configkit {}; + std::array _robot_name {}; + interface::FirmwareUpdate &_firmware_update; std::function _on_update_loaded_callback {}; @@ -559,14 +579,14 @@ class RobotController : public interface::RobotController BLEServiceCommands _service_commands {}; BLEServiceDeviceInformation _service_device_information {}; BLEServiceMonitoring _service_monitoring {}; + BLEServiceConfig _service_config {}; BLEServiceMagicCard _service_magic_card {}; BLEServiceFileExchange _service_file_exchange {}; BLEServiceUpdate _service_update {}; - std::array services = { - &_service_battery, &_service_commands, &_service_device_information, - &_service_monitoring, &_service_magic_card, &_service_file_exchange, - &_service_update, + std::array services = { + &_service_battery, &_service_commands, &_service_device_information, &_service_monitoring, + &_service_config, &_service_magic_card, &_service_file_exchange, &_service_update, }; uint8_t _emergency_stop_counter {0}; diff --git a/libs/RobotKit/tests/RobotController_test.h b/libs/RobotKit/tests/RobotController_test.h index b94920d9eb..0b87015de0 100644 --- a/libs/RobotKit/tests/RobotController_test.h +++ b/libs/RobotKit/tests/RobotController_test.h @@ -162,7 +162,7 @@ class RobotControllerTest : public testing::Test { InSequence seq; - EXPECT_CALL(mbed_mock_gatt, addService).Times(7); + EXPECT_CALL(mbed_mock_gatt, addService).Times(8); EXPECT_CALL(mbed_mock_gap, setEventHandler).Times(1); EXPECT_CALL(mbed_mock_gatt, setEventHandler).Times(1); @@ -174,6 +174,7 @@ class RobotControllerTest : public testing::Test Sequence set_serial_number_as_ble_device_name; EXPECT_CALL(mock_mcu, getID).InSequence(set_serial_number_as_ble_device_name); + EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(1).InSequence(set_serial_number_as_ble_device_name); EXPECT_CALL(mbed_mock_gap, setAdvertisingPayload).InSequence(set_serial_number_as_ble_device_name); expectedCallsStopMotors(); diff --git a/libs/RobotKit/tests/RobotController_test_initializeComponents.cpp b/libs/RobotKit/tests/RobotController_test_initializeComponents.cpp index 8ca125bf82..f3f0ff3b89 100644 --- a/libs/RobotKit/tests/RobotController_test_initializeComponents.cpp +++ b/libs/RobotKit/tests/RobotController_test_initializeComponents.cpp @@ -14,7 +14,7 @@ TEST_F(RobotControllerTest, initializeComponents) { InSequence seq; - EXPECT_CALL(mbed_mock_gatt, addService).Times(7); + EXPECT_CALL(mbed_mock_gatt, addService).Times(8); EXPECT_CALL(mbed_mock_gap, setEventHandler).Times(1); EXPECT_CALL(mbed_mock_gatt, setEventHandler).Times(1); @@ -27,6 +27,7 @@ TEST_F(RobotControllerTest, initializeComponents) Sequence set_serial_number_as_ble_device_name; EXPECT_CALL(mock_mcu, getID).InSequence(set_serial_number_as_ble_device_name); + EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(1).InSequence(set_serial_number_as_ble_device_name); EXPECT_CALL(mbed_mock_gap, setAdvertisingPayload).InSequence(set_serial_number_as_ble_device_name); expectedCallsStopMotors(); From 95de17cc1430cb50dcf234d464db6822dda8b947 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Wed, 18 Jan 2023 18:22:20 +0100 Subject: [PATCH 076/143] :recycle: (LSM6DSOX): Get data on INT1 rise --- drivers/CoreIMU/CMakeLists.txt | 5 +- drivers/CoreIMU/include/CoreLSM6DSOX.h | 17 ++++++- drivers/CoreIMU/include/interface/LSM6DSOX.h | 9 ++-- drivers/CoreIMU/source/CoreLSM6DSOX.cpp | 48 ++++++++++++++++---- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/drivers/CoreIMU/CMakeLists.txt b/drivers/CoreIMU/CMakeLists.txt index 3d7b775e62..d38fec9739 100644 --- a/drivers/CoreIMU/CMakeLists.txt +++ b/drivers/CoreIMU/CMakeLists.txt @@ -18,7 +18,10 @@ target_sources(CoreIMU source/CoreGyroscope.cpp ) -target_link_libraries(CoreIMU) +target_link_libraries(CoreIMU + CoreEventQueue + CoreInterruptIn +) if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") leka_unit_tests_sources( diff --git a/drivers/CoreIMU/include/CoreLSM6DSOX.h b/drivers/CoreIMU/include/CoreLSM6DSOX.h index 285ee3285b..266eed72b4 100644 --- a/drivers/CoreIMU/include/CoreLSM6DSOX.h +++ b/drivers/CoreIMU/include/CoreLSM6DSOX.h @@ -6,6 +6,8 @@ #include +#include "CoreEventQueue.h" +#include "CoreInterruptIn.h" #include "interface/LSM6DSOX.h" #include "interface/drivers/I2C.h" #include "lsm6dsox_reg.h" @@ -15,10 +17,12 @@ namespace leka { class CoreLSM6DSOX : public interface::LSM6DSOX { public: - explicit CoreLSM6DSOX(interface::I2C &i2c); + explicit CoreLSM6DSOX(interface::I2C &i2c, CoreInterruptIn &drdy_irq); void init() final; - auto getData() -> SensorData & final; + + void registerOnGyDataReadyCallback(drdy_callback_t const &callback) final; + void setPowerMode(PowerMode mode) final; private: @@ -30,12 +34,21 @@ class CoreLSM6DSOX : public interface::LSM6DSOX static auto ptr_io_read(CoreLSM6DSOX *handle, uint8_t read_address, uint8_t *p_buffer, uint16_t number_bytes_to_read) -> int32_t; + void onGyrDataReadyHandler(); + void setGyrDataReadyInterrupt(); + interface::I2C &_i2c; + CoreEventQueue _event_queue {}; lsm6dsox_md_t _config {}; stmdev_ctx_t _register_io_function {}; SensorData _sensor_data {}; + CoreInterruptIn &_drdy_irq; const char _address = LSM6DSOX_I2C_ADD_L; + std::array data_raw_xl {}; + std::array data_raw_gy {}; + drdy_callback_t _on_gy_data_ready_callback; + static constexpr uint8_t kMaxBufferLength = 32; std::array _rx_buffer {}; }; diff --git a/drivers/CoreIMU/include/interface/LSM6DSOX.h b/drivers/CoreIMU/include/interface/LSM6DSOX.h index 64dffab0a5..cdc136e197 100644 --- a/drivers/CoreIMU/include/interface/LSM6DSOX.h +++ b/drivers/CoreIMU/include/interface/LSM6DSOX.h @@ -4,6 +4,7 @@ #pragma once +#include namespace leka::interface { class LSM6DSOX @@ -37,8 +38,10 @@ class LSM6DSOX Gyroscope gy = {0, 0, 0}; }; - virtual void init() = 0; - virtual auto getData() -> SensorData & = 0; - virtual void setPowerMode(PowerMode) = 0; + using drdy_callback_t = std::function; + + virtual void init() = 0; + virtual void registerOnGyDataReadyCallback(drdy_callback_t const &callback) = 0; + virtual void setPowerMode(PowerMode) = 0; }; } // namespace leka::interface diff --git a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp index 2d27595713..e5c8cfb7e8 100644 --- a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp +++ b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp @@ -6,7 +6,7 @@ namespace leka { -CoreLSM6DSOX::CoreLSM6DSOX(interface::I2C &i2c) : _i2c(i2c) +CoreLSM6DSOX::CoreLSM6DSOX(interface::I2C &i2c, CoreInterruptIn &drdy_irq) : _i2c(i2c), _drdy_irq(drdy_irq) { // ? NOLINTNEXTLINE - allow reinterpret_cast as there are no alternatives _register_io_function.write_reg = reinterpret_cast(ptr_io_write); @@ -17,6 +17,8 @@ CoreLSM6DSOX::CoreLSM6DSOX(interface::I2C &i2c) : _i2c(i2c) void CoreLSM6DSOX::init() { + _event_queue.dispatch_forever(); + lsm6dsox_init_set(&_register_io_function, LSM6DSOX_DRV_RDY); lsm6dsox_i3c_disable_set(&_register_io_function, LSM6DSOX_I3C_DISABLE); lsm6dsox_mode_get(&_register_io_function, nullptr, &_config); @@ -27,6 +29,8 @@ void CoreLSM6DSOX::init() _config.ui.gy.fs = _config.ui.gy.LSM6DSOX_GY_UI_500dps; lsm6dsox_mode_set(&_register_io_function, nullptr, &_config); + + setGyrDataReadyInterrupt(); } void CoreLSM6DSOX::setPowerMode(PowerMode mode) @@ -42,6 +46,7 @@ void CoreLSM6DSOX::setPowerMode(PowerMode mode) xl_power_mode = LSM6DSOX_ULTRA_LOW_POWER_MD; gy_power_mode = LSM6DSOX_GY_NORMAL; break; + default: case PowerMode::Normal: xl_power_mode = LSM6DSOX_LOW_NORMAL_POWER_MD; gy_power_mode = LSM6DSOX_GY_NORMAL; @@ -50,8 +55,6 @@ void CoreLSM6DSOX::setPowerMode(PowerMode mode) xl_power_mode = LSM6DSOX_HIGH_PERFORMANCE_MD; gy_power_mode = LSM6DSOX_GY_HIGH_PERFORMANCE; break; - default: - return; } lsm6dsox_xl_power_mode_set(&_register_io_function, xl_power_mode); lsm6dsox_gy_power_mode_set(&_register_io_function, gy_power_mode); @@ -60,13 +63,24 @@ void CoreLSM6DSOX::setPowerMode(PowerMode mode) } } -auto CoreLSM6DSOX::getData() -> SensorData & +void CoreLSM6DSOX::registerOnGyDataReadyCallback(drdy_callback_t const &callback) { - lsm6dsox_data_t data; - lsm6dsox_data_get(&_register_io_function, nullptr, &_config, &data); - _sensor_data.xl = {data.ui.xl.mg[0], data.ui.xl.mg[1], data.ui.xl.mg[2]}; - _sensor_data.gy = {data.ui.gy.mdps[0] / 1000, data.ui.gy.mdps[1] / 1000, data.ui.gy.mdps[2] / 1000}; - return _sensor_data; + _on_gy_data_ready_callback = callback; +} + +void CoreLSM6DSOX::onGyrDataReadyHandler() +{ + lsm6dsox_angular_rate_raw_get(&_register_io_function, data_raw_gy.data()); + _sensor_data.gy.x = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(0)) / 1000.F; + _sensor_data.gy.y = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(1)) / 1000.F; + _sensor_data.gy.z = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(2)) / 1000.F; + + lsm6dsox_acceleration_raw_get(&_register_io_function, data_raw_xl.data()); + _sensor_data.xl.x = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(0)); + _sensor_data.xl.y = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(1)); + _sensor_data.xl.z = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(2)); + + _on_gy_data_ready_callback(_sensor_data); } auto CoreLSM6DSOX::read(uint8_t register_address, uint16_t number_bytes_to_read, uint8_t *p_buffer) -> int32_t @@ -105,4 +119,20 @@ auto CoreLSM6DSOX::ptr_io_read(CoreLSM6DSOX *handle, uint8_t read_address, uint8 return handle->read(read_address, number_bytes_to_read, p_buffer); } +void CoreLSM6DSOX::setGyrDataReadyInterrupt() +{ + lsm6dsox_dataready_pulsed_t drdy_pulsed {LSM6DSOX_DRDY_PULSED}; + lsm6dsox_data_ready_mode_set(&_register_io_function, drdy_pulsed); + + lsm6dsox_pin_int1_route_t gyro_int1 { + .drdy_xl = PROPERTY_ENABLE, + .den_flag = PROPERTY_ENABLE, + }; + lsm6dsox_pin_int1_route_set(&_register_io_function, gyro_int1); + + auto gyr_drdy_callback = [this] { _event_queue.call([this] { onGyrDataReadyHandler(); }); }; + + _drdy_irq.onRise(gyr_drdy_callback); +} + } // namespace leka From 767c11a84ddbd0d6a3f98fd35fc1cffc52a24e20 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Wed, 18 Jan 2023 18:23:52 +0100 Subject: [PATCH 077/143] :fire: (Accel & Gyro): Remove CoreAccel & CoreGyro --- drivers/CoreIMU/CMakeLists.txt | 4 -- drivers/CoreIMU/include/CoreAccelerometer.h | 25 ----------- drivers/CoreIMU/include/CoreGyroscope.h | 25 ----------- .../CoreIMU/include/interface/Accelerometer.h | 17 ------- drivers/CoreIMU/include/interface/Gyroscope.h | 17 ------- drivers/CoreIMU/source/CoreAccelerometer.cpp | 15 ------- drivers/CoreIMU/source/CoreGyroscope.cpp | 15 ------- .../CoreIMU/tests/CoreAccelerometer_test.cpp | 44 ------------------- drivers/CoreIMU/tests/CoreGyroscope_test.cpp | 44 ------------------- 9 files changed, 206 deletions(-) delete mode 100644 drivers/CoreIMU/include/CoreAccelerometer.h delete mode 100644 drivers/CoreIMU/include/CoreGyroscope.h delete mode 100644 drivers/CoreIMU/include/interface/Accelerometer.h delete mode 100644 drivers/CoreIMU/include/interface/Gyroscope.h delete mode 100644 drivers/CoreIMU/source/CoreAccelerometer.cpp delete mode 100644 drivers/CoreIMU/source/CoreGyroscope.cpp delete mode 100644 drivers/CoreIMU/tests/CoreAccelerometer_test.cpp delete mode 100644 drivers/CoreIMU/tests/CoreGyroscope_test.cpp diff --git a/drivers/CoreIMU/CMakeLists.txt b/drivers/CoreIMU/CMakeLists.txt index d38fec9739..edad0f0d94 100644 --- a/drivers/CoreIMU/CMakeLists.txt +++ b/drivers/CoreIMU/CMakeLists.txt @@ -14,8 +14,6 @@ target_sources(CoreIMU PRIVATE extern/source/lsm6dsox_reg.c source/CoreLSM6DSOX.cpp - source/CoreAccelerometer.cpp - source/CoreGyroscope.cpp ) target_link_libraries(CoreIMU @@ -25,8 +23,6 @@ target_link_libraries(CoreIMU if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") leka_unit_tests_sources( - tests/CoreAccelerometer_test.cpp - tests/CoreGyroscope_test.cpp tests/CoreLSM6DSOX_test.cpp ) endif() diff --git a/drivers/CoreIMU/include/CoreAccelerometer.h b/drivers/CoreIMU/include/CoreAccelerometer.h deleted file mode 100644 index 5c22ed0d6b..0000000000 --- a/drivers/CoreIMU/include/CoreAccelerometer.h +++ /dev/null @@ -1,25 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "interface/Accelerometer.h" -#include "interface/LSM6DSOX.h" - -namespace leka { - -using mg_t = float; - -class CoreAccelerometer : public interface::Accelerometer -{ - public: - explicit CoreAccelerometer(interface::LSM6DSOX &lsm6dsox) : _lsm6dsox(lsm6dsox) {}; - - auto getXYZ() -> std::tuple final; - - private: - interface::LSM6DSOX &_lsm6dsox; -}; - -} // namespace leka diff --git a/drivers/CoreIMU/include/CoreGyroscope.h b/drivers/CoreIMU/include/CoreGyroscope.h deleted file mode 100644 index 5bcb4a2af0..0000000000 --- a/drivers/CoreIMU/include/CoreGyroscope.h +++ /dev/null @@ -1,25 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "interface/Gyroscope.h" -#include "interface/LSM6DSOX.h" - -namespace leka { - -using dps_t = float; - -class CoreGyroscope : public interface::Gyroscope -{ - public: - explicit CoreGyroscope(interface::LSM6DSOX &lsm6dsox) : _lsm6dsox(lsm6dsox) {}; - - auto getXYZ() -> std::tuple final; - - private: - interface::LSM6DSOX &_lsm6dsox; -}; - -} // namespace leka diff --git a/drivers/CoreIMU/include/interface/Accelerometer.h b/drivers/CoreIMU/include/interface/Accelerometer.h deleted file mode 100644 index ba8747c7ee..0000000000 --- a/drivers/CoreIMU/include/interface/Accelerometer.h +++ /dev/null @@ -1,17 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include -namespace leka::interface { - -class Accelerometer -{ - public: - virtual ~Accelerometer() = default; - - virtual auto getXYZ() -> std::tuple = 0; -}; -} // namespace leka::interface diff --git a/drivers/CoreIMU/include/interface/Gyroscope.h b/drivers/CoreIMU/include/interface/Gyroscope.h deleted file mode 100644 index 59c1963b44..0000000000 --- a/drivers/CoreIMU/include/interface/Gyroscope.h +++ /dev/null @@ -1,17 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include -namespace leka::interface { - -class Gyroscope -{ - public: - virtual ~Gyroscope() = default; - - virtual auto getXYZ() -> std::tuple = 0; -}; -} // namespace leka::interface diff --git a/drivers/CoreIMU/source/CoreAccelerometer.cpp b/drivers/CoreIMU/source/CoreAccelerometer.cpp deleted file mode 100644 index 432e8f78da..0000000000 --- a/drivers/CoreIMU/source/CoreAccelerometer.cpp +++ /dev/null @@ -1,15 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "CoreAccelerometer.h" - -namespace leka { - -auto CoreAccelerometer::getXYZ() -> std::tuple -{ - auto data = _lsm6dsox.getData(); - return std::tuple {data.xl.x, data.xl.y, data.xl.z}; -} - -} // namespace leka diff --git a/drivers/CoreIMU/source/CoreGyroscope.cpp b/drivers/CoreIMU/source/CoreGyroscope.cpp deleted file mode 100644 index ff5092046e..0000000000 --- a/drivers/CoreIMU/source/CoreGyroscope.cpp +++ /dev/null @@ -1,15 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "CoreGyroscope.h" - -namespace leka { - -auto CoreGyroscope::getXYZ() -> std::tuple -{ - auto data = _lsm6dsox.getData(); - return std::tuple {data.gy.x, data.gy.y, data.gy.z}; -} - -} // namespace leka diff --git a/drivers/CoreIMU/tests/CoreAccelerometer_test.cpp b/drivers/CoreIMU/tests/CoreAccelerometer_test.cpp deleted file mode 100644 index 631572890a..0000000000 --- a/drivers/CoreIMU/tests/CoreAccelerometer_test.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "CoreAccelerometer.h" - -#include "gtest/gtest.h" -#include "mocks/leka/LSM6DSOX.h" - -using namespace leka; - -using ::testing::ReturnRef; - -class CoreAccelerometerTest : public ::testing::Test -{ - protected: - CoreAccelerometerTest() = default; - - // void SetUp() override {} - // void TearDown() override {} - - mock::LSM6DSOX mock_lsm6dsox; - CoreAccelerometer accel {mock_lsm6dsox}; -}; - -TEST_F(CoreAccelerometerTest, initialization) -{ - ASSERT_NE(&accel, nullptr); -} - -TEST_F(CoreAccelerometerTest, getXYZ) -{ - leka::interface::LSM6DSOX::SensorData data { - {12.f, 24.f, 36.f}, { 48.f, 60.f, 72.f } - }; - - EXPECT_CALL(mock_lsm6dsox, getData).WillOnce(ReturnRef(data)); - - auto [ax, ay, az] = accel.getXYZ(); - - EXPECT_EQ(data.xl.x, ax); - EXPECT_EQ(data.xl.y, ay); - EXPECT_EQ(data.xl.z, az); -} diff --git a/drivers/CoreIMU/tests/CoreGyroscope_test.cpp b/drivers/CoreIMU/tests/CoreGyroscope_test.cpp deleted file mode 100644 index 23788c2a3e..0000000000 --- a/drivers/CoreIMU/tests/CoreGyroscope_test.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "CoreGyroscope.h" - -#include "gtest/gtest.h" -#include "mocks/leka/LSM6DSOX.h" - -using namespace leka; - -using testing::ReturnRef; - -class CoreGyroscopeTest : public ::testing::Test -{ - protected: - CoreGyroscopeTest() = default; - - // void SetUp() override {} - // void TearDown() override {} - - mock::LSM6DSOX mock_lsm6dsox; - CoreGyroscope gyro {mock_lsm6dsox}; -}; - -TEST_F(CoreGyroscopeTest, initialization) -{ - ASSERT_NE(&gyro, nullptr); -} - -TEST_F(CoreGyroscopeTest, getXYZ) -{ - leka::interface::LSM6DSOX::SensorData data { - {12.F, 24.F, 36.F}, { 48.F, 60.F, 72.F } - }; - - EXPECT_CALL(mock_lsm6dsox, getData).WillOnce(ReturnRef(data)); - - auto [gx, gy, gz] = gyro.getXYZ(); - - EXPECT_EQ(data.gy.x, gx); - EXPECT_EQ(data.gy.y, gy); - EXPECT_EQ(data.gy.z, gz); -} From 52b37a6abff757853a14ebe62e6a9648b6cad561 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Wed, 18 Jan 2023 18:24:55 +0100 Subject: [PATCH 078/143] :recycle: (IMUKit): IMUKit depends now on LSM6DSOX --- libs/IMUKit/include/IMUKit.h | 24 ++++++++--------------- libs/IMUKit/source/IMUKit.cpp | 36 +++++++++++++---------------------- 2 files changed, 21 insertions(+), 39 deletions(-) diff --git a/libs/IMUKit/include/IMUKit.h b/libs/IMUKit/include/IMUKit.h index e8f44b0b83..16b0f2a03e 100644 --- a/libs/IMUKit/include/IMUKit.h +++ b/libs/IMUKit/include/IMUKit.h @@ -6,9 +6,7 @@ #include -#include "interface/Accelerometer.h" -#include "interface/Gyroscope.h" -#include "interface/libs/EventLoop.h" +#include "interface/LSM6DSOX.h" #include "internal/Mahony.h" namespace leka { @@ -16,36 +14,30 @@ namespace leka { class IMUKit { public: - IMUKit(interface::EventLoop &event_loop, interface::Accelerometer &accel, interface::Gyroscope &gyro) - : _event_loop(event_loop), - _accel(accel), - _gyro(gyro) { + explicit IMUKit(interface::LSM6DSOX &lsm6dsox) + : _lsm6dsox(lsm6dsox) { // nothing to do }; void init(); void start(); - void run(); void stop(); + void setOrigin(); auto getAngles() -> std::array; - void reset(); - - void computeAngles(); - private: - interface::EventLoop &_event_loop; - interface::Accelerometer &_accel; - interface::Gyroscope &_gyro; + void computeAngles(const interface::LSM6DSOX::SensorData &imu_data); + interface::LSM6DSOX &_lsm6dsox; ahrs::Mahony _mahony {}; struct SamplingConfig { const std::chrono::milliseconds delay {}; const float frequency {}; }; + // ? Sampling config deprecated. + // TODO(@ladislas @hugo): Use dynamic sampling frequency. const SamplingConfig kDefaultSamplingConfig {.delay = std::chrono::milliseconds {70}, .frequency = 13.F}; - bool _is_running {false}; }; } // namespace leka diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index 4c4acc7ac6..ed21de9f5f 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -4,34 +4,29 @@ #include "IMUKit.h" -#include "rtos/ThisThread.h" - using namespace leka; void IMUKit::init() { _mahony.begin(kDefaultSamplingConfig.frequency); - _event_loop.registerCallback([this] { run(); }); + + auto on_drdy_callback = [this](const interface::LSM6DSOX::SensorData &imu_data) { computeAngles(imu_data); }; + _lsm6dsox.registerOnGyDataReadyCallback(on_drdy_callback); } void IMUKit::start() { - _is_running = true; - _event_loop.start(); + _lsm6dsox.setPowerMode(interface::LSM6DSOX::PowerMode::Normal); } -void IMUKit::run() +void IMUKit::stop() { - while (_is_running) { - computeAngles(); - rtos::ThisThread::sleep_for(kDefaultSamplingConfig.delay); - } + _lsm6dsox.setPowerMode(interface::LSM6DSOX::PowerMode::Off); } -void IMUKit::stop() +void IMUKit::setOrigin() { - _is_running = false; - _event_loop.stop(); + _mahony.setOrigin(); } auto IMUKit::getAngles() -> std::array @@ -39,15 +34,10 @@ auto IMUKit::getAngles() -> std::array return {_mahony.getPitch(), _mahony.getRoll(), _mahony.getYaw()}; } -void IMUKit::reset() -{ - _mahony.setOrigin(); -} - -void IMUKit::computeAngles() +void IMUKit::computeAngles(const interface::LSM6DSOX::SensorData &imu_data) { - auto accel = _accel.getXYZ(); - auto gyro = _gyro.getXYZ(); - auto mag = std::make_tuple(0.0F, 0.0F, 0.0F); - _mahony.update(accel, gyro, mag); + auto xl_data = std::make_tuple(imu_data.xl.x, imu_data.xl.y, imu_data.xl.z); + auto gy_data = std::make_tuple(imu_data.gy.x, imu_data.gy.y, imu_data.gy.z); + constexpr auto mag_data = std::make_tuple(0.0F, 0.0F, 0.0F); + _mahony.update(xl_data, gy_data, mag_data); } From 3a226c72d5bd1a6af1b26225f88dd537cf8f452b Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Wed, 18 Jan 2023 18:25:48 +0100 Subject: [PATCH 079/143] :heavy_plus_sign: (MotionKit): Update typos & imports --- libs/MotionKit/include/MotionKit.h | 1 + libs/MotionKit/source/MotionKit.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/MotionKit/include/MotionKit.h b/libs/MotionKit/include/MotionKit.h index a2ac10e638..35a7b2bae8 100644 --- a/libs/MotionKit/include/MotionKit.h +++ b/libs/MotionKit/include/MotionKit.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include "IMUKit.h" #include "PID.h" diff --git a/libs/MotionKit/source/MotionKit.cpp b/libs/MotionKit/source/MotionKit.cpp index ded8317bc9..ead7cb8fab 100644 --- a/libs/MotionKit/source/MotionKit.cpp +++ b/libs/MotionKit/source/MotionKit.cpp @@ -33,7 +33,7 @@ void MotionKit::rotate(uint8_t number_of_rotations, Rotation direction, stop(); _imukit.start(); - _imukit.reset(); + _imukit.setOrigin(); _target_not_reached = true; _stabilisation_requested = false; @@ -59,7 +59,7 @@ void MotionKit::startStabilisation() stop(); _imukit.start(); - _imukit.reset(); + _imukit.setOrigin(); _target_not_reached = false; _stabilisation_requested = true; From 7e2a2521838c11e695c5c86603953b340e98446f Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Wed, 18 Jan 2023 18:31:34 +0100 Subject: [PATCH 080/143] :recycle: (Spikes & OS): Update dependencies to LSM6DSOX changes --- app/os/main.cpp | 13 +++++-------- spikes/lk_accel_gyro/main.cpp | 29 +++++++++++++++++------------ spikes/lk_command_kit/main.cpp | 13 ++++--------- spikes/lk_imu_kit/main.cpp | 23 +++++++++-------------- spikes/lk_motion_kit/main.cpp | 11 ++++------- spikes/lk_reinforcer/main.cpp | 12 ++++-------- 6 files changed, 43 insertions(+), 58 deletions(-) diff --git a/app/os/main.cpp b/app/os/main.cpp index 4e0b9e4441..396d69d9ed 100644 --- a/app/os/main.cpp +++ b/app/os/main.cpp @@ -11,7 +11,6 @@ #include "ActivityKit.h" #include "ChooseReinforcer.h" -#include "CoreAccelerometer.h" #include "CoreBattery.h" #include "CoreBufferedSerial.h" #include "CoreDMA2D.hpp" @@ -20,8 +19,8 @@ #include "CoreFlashManagerIS25LP016D.h" #include "CoreFont.hpp" #include "CoreGraphics.hpp" -#include "CoreGyroscope.h" #include "CoreI2C.h" +#include "CoreInterruptIn.h" #include "CoreJPEG.hpp" #include "CoreJPEGModeDMA.hpp" #include "CoreJPEGModePolling.hpp" @@ -258,18 +257,16 @@ namespace imu { namespace internal { - CoreI2C i2c(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - EventLoopKit event_loop {}; + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); } // namespace internal - CoreLSM6DSOX lsm6dsox(internal::i2c); - CoreAccelerometer accel(lsm6dsox); - CoreGyroscope gyro(lsm6dsox); + auto lsm6dsox = CoreLSM6DSOX(internal::i2c, internal::drdy_irq); } // namespace imu -auto imukit = IMUKit {imu::internal::event_loop, imu::accel, imu::gyro}; +auto imukit = IMUKit {imu::lsm6dsox}; namespace motion::internal { diff --git a/spikes/lk_accel_gyro/main.cpp b/spikes/lk_accel_gyro/main.cpp index 01e69165bd..849196804e 100644 --- a/spikes/lk_accel_gyro/main.cpp +++ b/spikes/lk_accel_gyro/main.cpp @@ -4,8 +4,6 @@ #include "rtos/ThisThread.h" -#include "CoreAccelerometer.h" -#include "CoreGyroscope.h" #include "CoreI2C.h" #include "CoreLSM6DSOX.h" #include "HelloWorld.h" @@ -18,10 +16,14 @@ namespace { namespace imu { - CoreI2C i2c(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - CoreLSM6DSOX lsm6dsox(i2c); - CoreAccelerometer accel(lsm6dsox); - CoreGyroscope gyro(lsm6dsox); + namespace internal { + + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); + + } // namespace internal + + CoreLSM6DSOX lsm6dsox(internal::i2c, internal::drdy_irq); } // namespace imu @@ -37,15 +39,18 @@ auto main() -> int imu::lsm6dsox.init(); imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Off); - imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Normal); - while (true) { - auto [xlx, xly, xlz] = imu::accel.getXYZ(); - auto [gyx, gyy, gyz] = imu::gyro.getXYZ(); + auto callback = [](const interface::LSM6DSOX::SensorData &imu_data) { + const auto &[xlx, xly, xlz] = imu_data.xl; + const auto &[gx, gy, gz] = imu_data.gy; + log_info("Xl : x: %7.2f, y: %7.2f, z: %7.2f\n", xlx, xly, xlz); + log_info("Gy : x: %7.2f, y: %7.2f, z: %7.2f\n", gx, gy, gz); + }; - log_info("Ax: %f, Ay: %f, Az: %f, Gx: %f, Gy: %f, Gz: %f)", xlx, xly, xlz, gyx, gyy, gyz); + imu::lsm6dsox.registerOnGyDataReadyCallback(callback); - rtos::ThisThread::sleep_for(250ms); + while (true) { + rtos::ThisThread::sleep_for(1s); } } diff --git a/spikes/lk_command_kit/main.cpp b/spikes/lk_command_kit/main.cpp index 43665117e9..00c7422337 100644 --- a/spikes/lk_command_kit/main.cpp +++ b/spikes/lk_command_kit/main.cpp @@ -8,12 +8,10 @@ #include "rtos/ThisThread.h" #include "CommandKit.h" -#include "CoreAccelerometer.h" #include "CoreDMA2D.hpp" #include "CoreDSI.hpp" #include "CoreFont.hpp" #include "CoreGraphics.hpp" -#include "CoreGyroscope.h" #include "CoreI2C.h" #include "CoreJPEG.hpp" #include "CoreJPEGModeDMA.hpp" @@ -97,21 +95,18 @@ auto right = CoreMotor {internal::right::dir_1, internal::right::dir_2, internal } // namespace motor namespace imu { - namespace internal { - CoreI2C i2c(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - EventLoopKit event_loop {}; + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); } // namespace internal -CoreLSM6DSOX lsm6dsox(internal::i2c); -CoreAccelerometer accel(lsm6dsox); -CoreGyroscope gyro(lsm6dsox); +CoreLSM6DSOX lsm6dsox(internal::i2c, internal::drdy_irq); } // namespace imu -auto imukit = IMUKit {imu::internal::event_loop, imu::accel, imu::gyro}; +auto imukit = IMUKit {imu::lsm6dsox}; namespace motion::internal { diff --git a/spikes/lk_imu_kit/main.cpp b/spikes/lk_imu_kit/main.cpp index d2ca68776d..40c338b7b3 100644 --- a/spikes/lk_imu_kit/main.cpp +++ b/spikes/lk_imu_kit/main.cpp @@ -4,11 +4,8 @@ #include "rtos/ThisThread.h" -#include "CoreAccelerometer.h" -#include "CoreGyroscope.h" #include "CoreI2C.h" #include "CoreLSM6DSOX.h" -#include "EventLoopKit.h" #include "HelloWorld.h" #include "IMUKit.h" #include "LogKit.h" @@ -22,19 +19,17 @@ namespace imu { namespace internal { - CoreI2C i2c(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - CoreLSM6DSOX lsm6dsox(internal::i2c); - EventLoopKit event_loop {}; + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); } // namespace internal - CoreAccelerometer accel(internal::lsm6dsox); - CoreGyroscope gyro(internal::lsm6dsox); - - IMUKit kit(internal::event_loop, accel, gyro); + CoreLSM6DSOX lsm6dsox(internal::i2c, internal::drdy_irq); } // namespace imu +IMUKit imukit(imu::lsm6dsox); + } // namespace auto main() -> int @@ -44,13 +39,13 @@ auto main() -> int HelloWorld hello; hello.start(); - imu::internal::lsm6dsox.init(); + imu::lsm6dsox.init(); - imu::kit.init(); - imu::kit.start(); + imukit.init(); + imukit.start(); while (true) { - auto [pitch, roll, yaw] = imu::kit.getAngles(); + auto [pitch, roll, yaw] = imukit.getAngles(); log_info("Pitch : %7.2f, Roll : %7.2f Yaw : %7.2f", pitch, roll, yaw); rtos::ThisThread::sleep_for(140ms); diff --git a/spikes/lk_motion_kit/main.cpp b/spikes/lk_motion_kit/main.cpp index 2644902900..a318cc27aa 100644 --- a/spikes/lk_motion_kit/main.cpp +++ b/spikes/lk_motion_kit/main.cpp @@ -4,9 +4,7 @@ #include "rtos/ThisThread.h" -#include "CoreAccelerometer.h" #include "CoreBufferedSerial.h" -#include "CoreGyroscope.h" #include "CoreI2C.h" #include "CoreLSM6DSOX.h" #include "CoreMotor.h" @@ -61,18 +59,17 @@ namespace imu { namespace internal { + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; CoreI2C i2c(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - EventLoopKit event_loop {}; + auto event_queue = CoreEventQueue(); } // namespace internal - CoreLSM6DSOX lsm6dsox(internal::i2c); - CoreAccelerometer accel(lsm6dsox); - CoreGyroscope gyro(lsm6dsox); + CoreLSM6DSOX lsm6dsox(internal::i2c, internal::drdy_irq); } // namespace imu -auto imukit = IMUKit {imu::internal::event_loop, imu::accel, imu::gyro}; +auto imukit = IMUKit {imu::lsm6dsox}; namespace motion::internal { diff --git a/spikes/lk_reinforcer/main.cpp b/spikes/lk_reinforcer/main.cpp index af20d17b51..959b1fb5c2 100644 --- a/spikes/lk_reinforcer/main.cpp +++ b/spikes/lk_reinforcer/main.cpp @@ -8,12 +8,10 @@ #include "rtos/ThisThread.h" #include "CGGraphics.hpp" -#include "CoreAccelerometer.h" #include "CoreDMA2D.hpp" #include "CoreDSI.hpp" #include "CoreFont.hpp" #include "CoreGraphics.hpp" -#include "CoreGyroscope.h" #include "CoreI2C.h" #include "CoreJPEG.hpp" #include "CoreJPEGModeDMA.hpp" @@ -139,18 +137,16 @@ namespace imu { namespace internal { - CoreI2C i2c(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - EventLoopKit event_loop {}; + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); } // namespace internal - CoreLSM6DSOX lsm6dsox(internal::i2c); - CoreAccelerometer accel(lsm6dsox); - CoreGyroscope gyro(lsm6dsox); + CoreLSM6DSOX lsm6dsox(internal::i2c, internal::drdy_irq); } // namespace imu -auto imukit = IMUKit {imu::internal::event_loop, imu::accel, imu::gyro}; +auto imukit = IMUKit {imu::lsm6dsox}; namespace motion::internal { From ab02dc5fc4212e29b8adc30d6809c0da23faf7aa Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Wed, 18 Jan 2023 18:32:26 +0100 Subject: [PATCH 081/143] :white_check_mark: (functional tests): Update functional tests --- .../functional/tests/core_imu/CMakeLists.txt | 1 - .../tests/core_imu/suite_core_imu.cpp | 51 ------------------- .../tests/core_imu/suite_core_lsm6dsox.cpp | 23 ++++----- tests/functional/tests/core_imu/utils.h | 45 +++++++++------- tests/functional/tests/imu_kit/CMakeLists.txt | 1 - .../tests/imu_kit/suite_imu_kit.cpp | 16 ++---- 6 files changed, 43 insertions(+), 94 deletions(-) delete mode 100644 tests/functional/tests/core_imu/suite_core_imu.cpp diff --git a/tests/functional/tests/core_imu/CMakeLists.txt b/tests/functional/tests/core_imu/CMakeLists.txt index ee2411e1e7..49964ee343 100644 --- a/tests/functional/tests/core_imu/CMakeLists.txt +++ b/tests/functional/tests/core_imu/CMakeLists.txt @@ -11,7 +11,6 @@ register_functional_test( SOURCES suite_core_lsm6dsox.cpp - suite_core_imu.cpp LINK_LIBRARIES CoreI2C diff --git a/tests/functional/tests/core_imu/suite_core_imu.cpp b/tests/functional/tests/core_imu/suite_core_imu.cpp deleted file mode 100644 index 13a017cbd5..0000000000 --- a/tests/functional/tests/core_imu/suite_core_imu.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "rtos/ThisThread.h" - -#include "./utils.h" -#include "CoreAccelerometer.h" -#include "CoreGyroscope.h" -#include "CoreI2C.h" -#include "CoreLSM6DSOX.h" -#include "tests/config.h" - -using namespace leka; -using namespace std::chrono; -using namespace boost::ut; -using namespace boost::ut::bdd; - -suite suite_core_imu = [] { - constexpr auto kAccelZAxisDefaultMinBound = 800._f; - constexpr auto kAccelZAxisDefaultMaxBound = 1200._f; - - auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - auto lsm6dsox = CoreLSM6DSOX {i2c}; - auto accel = CoreAccelerometer {lsm6dsox}; - auto gyro = CoreGyroscope {lsm6dsox}; - - "initialization"_test = [&] { - expect(neq(&lsm6dsox, nullptr)); - lsm6dsox.init(); - }; - - "accelerometer"_test = [&] { - "z accel != 0"_test = [&] { - auto [ax, ay, az] = accel.getXYZ(); - expect(az != 0._f); - }; - - "z accel between min/max bounds"_test = [&] { - auto [ax, ay, az] = accel.getXYZ(); - expect(az >= kAccelZAxisDefaultMinBound); - expect(az <= kAccelZAxisDefaultMaxBound); - }; - - "x,y,z accel values change over time"_test = [&] { expect(values_did_change_over_time(accel)); }; - }; - - "gyroscope"_test = [&] { - "x,y,z gyro values change over time"_test = [&] { expect(values_did_change_over_time(gyro)); }; - }; -}; diff --git a/tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp b/tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp index fb0da7a144..cc44f37def 100644 --- a/tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp +++ b/tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp @@ -5,8 +5,6 @@ #include "rtos/ThisThread.h" #include "./utils.h" -#include "CoreAccelerometer.h" -#include "CoreGyroscope.h" #include "CoreI2C.h" #include "CoreLSM6DSOX.h" #include "tests/config.h" @@ -17,33 +15,32 @@ using namespace boost::ut; using namespace boost::ut::bdd; suite suite_lsm6dsox = [] { - auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - auto lsm6dsox = CoreLSM6DSOX {i2c}; - auto accel = CoreAccelerometer {lsm6dsox}; - auto gyro = CoreGyroscope {lsm6dsox}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto lsm6dsox = CoreLSM6DSOX {i2c, drdy_irq}; + auto sensor_data = leka::interface::LSM6DSOX::SensorData(); "initialization"_test = [&] { expect(neq(&lsm6dsox, nullptr)); lsm6dsox.init(); + + auto sensor_callback = [&](const leka::interface::LSM6DSOX::SensorData &data) { sensor_data = data; }; + lsm6dsox.registerOnGyDataReadyCallback(sensor_callback); }; scenario("lsm6dsox - power mode") = [&] { given("powermode is set to off") = [&] { lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Off); - then("I expect accel data to not change over time") = [&] { - expect(not values_did_change_over_time(accel)); + then("I expect imu data to not change over time") = [&] { + expect(not values_did_change_over_time(lsm6dsox)); }; - - then("I expect gyro data to not change over time") = [&] { expect(not values_did_change_over_time(gyro)); }; }; given("powermode is set to normal again") = [&] { lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Normal); - then("I expect accel data to change over time") = [&] { expect(values_did_change_over_time(accel)); }; - - then("I expect gyro data to change over time") = [&] { expect(values_did_change_over_time(gyro)); }; + then("I expect imu data to change over time") = [&] { expect(values_did_change_over_time(lsm6dsox)); }; }; }; }; diff --git a/tests/functional/tests/core_imu/utils.h b/tests/functional/tests/core_imu/utils.h index 2b3a054f4a..60ed1cafcc 100644 --- a/tests/functional/tests/core_imu/utils.h +++ b/tests/functional/tests/core_imu/utils.h @@ -4,34 +4,39 @@ #pragma once +#include #include #include "rtos/ThisThread.h" #include "boost/ut.hpp" +#include "interface/LSM6DSOX.h" -// clang-format off -template -concept CanGetDataXYZ = requires { - &T::getXYZ; -}; -// clang-format on - -template -auto values_did_change_over_time(T &sensor) +inline auto values_did_change_over_time(leka::interface::LSM6DSOX &lsm6dsox) { using namespace std::chrono; using namespace boost::ut; + auto sensor_data = leka::interface::LSM6DSOX::SensorData(); + auto i_batch = std::vector {}; auto f_batch = std::vector {}; + auto sensor_callback = [&](const leka::interface::LSM6DSOX::SensorData &data) { sensor_data = data; }; + lsm6dsox.registerOnGyDataReadyCallback(sensor_callback); + for (auto i = 0; i < 10; ++i) { - auto [x, y, z] = sensor.getXYZ(); + auto [xlx, xly, xlz] = sensor_data.xl; + + i_batch.push_back(xlx); + i_batch.push_back(xly); + i_batch.push_back(xlz); - i_batch.push_back(x); - i_batch.push_back(y); - i_batch.push_back(z); + auto [gyx, gyy, gyz] = sensor_data.gy; + + i_batch.push_back(gyx); + i_batch.push_back(gyy); + i_batch.push_back(gyz); rtos::ThisThread::sleep_for(25ms); } @@ -39,11 +44,17 @@ auto values_did_change_over_time(T &sensor) rtos::ThisThread::sleep_for(100ms); for (auto i = 0; i < 10; ++i) { - auto [x, y, z] = sensor.getXYZ(); + auto [xlx, xly, xlz] = sensor_data.xl; + + f_batch.push_back(xlx); + f_batch.push_back(xly); + f_batch.push_back(xlz); + + auto [gyx, gyy, gyz] = sensor_data.gy; - f_batch.push_back(x); - f_batch.push_back(y); - f_batch.push_back(z); + f_batch.push_back(gyx); + f_batch.push_back(gyy); + f_batch.push_back(gyz); rtos::ThisThread::sleep_for(25ms); } diff --git a/tests/functional/tests/imu_kit/CMakeLists.txt b/tests/functional/tests/imu_kit/CMakeLists.txt index fea81abb35..d213a0a659 100644 --- a/tests/functional/tests/imu_kit/CMakeLists.txt +++ b/tests/functional/tests/imu_kit/CMakeLists.txt @@ -15,5 +15,4 @@ register_functional_test( CoreI2C CoreIMU IMUKit - EventLoopKit ) diff --git a/tests/functional/tests/imu_kit/suite_imu_kit.cpp b/tests/functional/tests/imu_kit/suite_imu_kit.cpp index 68c5bf3432..b153ae97d3 100644 --- a/tests/functional/tests/imu_kit/suite_imu_kit.cpp +++ b/tests/functional/tests/imu_kit/suite_imu_kit.cpp @@ -4,11 +4,8 @@ #include "rtos/ThisThread.h" -#include "CoreAccelerometer.h" -#include "CoreGyroscope.h" #include "CoreI2C.h" #include "CoreLSM6DSOX.h" -#include "EventLoopKit.h" #include "IMUKit.h" #include "tests/config.h" @@ -29,13 +26,10 @@ suite suite_imu_kit = [] { constexpr auto maximal_roll_noise_amplitude = 0.5F; constexpr auto maximal_yaw_drift = 15.F; - auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); - auto lsm6dsox = CoreLSM6DSOX {i2c}; - auto accel = CoreAccelerometer {lsm6dsox}; - auto gyro = CoreGyroscope {lsm6dsox}; - auto event_loop = EventLoopKit {}; - - auto imukit = IMUKit {event_loop, accel, gyro}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto lsm6dsox = CoreLSM6DSOX {i2c, drdy_irq}; + auto imukit = IMUKit {lsm6dsox}; lsm6dsox.init(); @@ -69,7 +63,7 @@ suite suite_imu_kit = [] { scenario("imu - measurement stability") = [&] { given("a new origin is set") = [&] { - imukit.reset(); + imukit.setOrigin(); then("I expect yaw to be reset to 180 degrees") = [&] { auto [pitch, roll, yaw] = imukit.getAngles(); From ce73e1921f18186db78a8050307a257bd3290854 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 20 Jan 2023 12:35:29 +0100 Subject: [PATCH 082/143] :white_check_mark: (LSM6DSOX): Update LSM6DSOX & heritating libs unit tests --- drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp | 39 ++- libs/IMUKit/tests/IMUKit_test.cpp | 242 +++++++++--------- libs/MotionKit/tests/MotionKit_test.cpp | 43 +--- .../tests/ReinforcerKit_test.cpp | 10 +- tests/unit/mocks/mocks/leka/Accelerometer.h | 18 -- tests/unit/mocks/mocks/leka/Gyroscope.h | 18 -- tests/unit/mocks/mocks/leka/LSM6DSOX.h | 2 +- 7 files changed, 165 insertions(+), 207 deletions(-) delete mode 100644 tests/unit/mocks/mocks/leka/Accelerometer.h delete mode 100644 tests/unit/mocks/mocks/leka/Gyroscope.h diff --git a/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp b/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp index 4d3a5d6413..402449e468 100644 --- a/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp +++ b/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp @@ -7,21 +7,38 @@ #include "CoreLSM6DSOX.h" #include "gtest/gtest.h" #include "mocks/leka/CoreI2C.h" +#include "mocks/leka/EventQueue.h" +#include "stubs/mbed/InterruptIn.h" using namespace leka; using testing::_; using testing::AtLeast; +using testing::MockFunction; class CoreLSM6DSOXTest : public ::testing::Test { protected: CoreLSM6DSOXTest() = default; - // void SetUp() override {} + + void SetUp() override + { + EXPECT_CALL(mocki2c, write).Times(AtLeast(1)); + EXPECT_CALL(mocki2c, read).Times(AtLeast(1)); + + lsm6dsox.init(); + } // void TearDown() override {} mock::CoreI2C mocki2c {}; - CoreLSM6DSOX lsm6dsox {mocki2c}; + CoreInterruptIn drdy_irq {NC}; + + CoreLSM6DSOX lsm6dsox {mocki2c, drdy_irq}; + + // ? Instantiation of mock::EventQueue is needed to setup the underlying stubs that will make the mock work + // ? correctly. Without it UT are failing + // TODO (@ladislas) - review mocks/stubs to remove the need of the object, replace with setup/teardown functions + mock::EventQueue _ {}; }; TEST_F(CoreLSM6DSOXTest, initialization) @@ -29,14 +46,6 @@ TEST_F(CoreLSM6DSOXTest, initialization) ASSERT_NE(&lsm6dsox, nullptr); } -TEST_F(CoreLSM6DSOXTest, i2cCommunicationWorking) -{ - EXPECT_CALL(mocki2c, write).Times(AtLeast(1)); - EXPECT_CALL(mocki2c, read).Times(AtLeast(1)); - - lsm6dsox.init(); -} - TEST_F(CoreLSM6DSOXTest, setPowerMode) { EXPECT_CALL(mocki2c, write).Times(AtLeast(1)); @@ -56,10 +65,16 @@ TEST_F(CoreLSM6DSOXTest, setPowerMode) lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::High); } -TEST_F(CoreLSM6DSOXTest, getData) +TEST_F(CoreLSM6DSOXTest, onGyrDRDY) { + MockFunction mock_callback; + EXPECT_CALL(mocki2c, write).Times(AtLeast(1)); EXPECT_CALL(mocki2c, read).Times(AtLeast(1)); + EXPECT_CALL(mock_callback, Call).Times(1); + + lsm6dsox.registerOnGyDataReadyCallback(mock_callback.AsStdFunction()); - lsm6dsox.getData(); + auto on_rise_callback = spy_InterruptIn_getRiseCallback(); + on_rise_callback(); } diff --git a/libs/IMUKit/tests/IMUKit_test.cpp b/libs/IMUKit/tests/IMUKit_test.cpp index 1fddc9c979..df92c3d379 100644 --- a/libs/IMUKit/tests/IMUKit_test.cpp +++ b/libs/IMUKit/tests/IMUKit_test.cpp @@ -4,17 +4,21 @@ #include "IMUKit.h" +#include "CoreLSM6DSOX.h" #include "ThisThread.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" -#include "mocks/leka/Accelerometer.h" -#include "mocks/leka/Gyroscope.h" +#include "mocks/leka/CoreI2C.h" +#include "mocks/leka/LSM6DSOX.h" #include "stubs/leka/EventLoopKit.h" +#include "stubs/mbed/InterruptIn.h" using namespace leka; +using ::testing::_; +using ::testing::AtLeast; +using ::testing::SaveArg; -using ::testing::AnyNumber; -using ::testing::MockFunction; -using testing::Return; +// TODO(@leka/dev-embedded): temporary fix, changes are needed when updating fusion algorithm class IMUKitTest : public ::testing::Test { @@ -24,15 +28,9 @@ class IMUKitTest : public ::testing::Test void SetUp() override { imukit.init(); } // void TearDown() override {} - stub::EventLoopKit stub_event_loop {}; + mock::LSM6DSOX mock_lsm6dox {}; - mock::Accelerometer accel {}; - mock::Gyroscope gyro {}; - - std::tuple accel_data = {0.F, 0.F, 0.F}; - std::tuple gyro_data = {0.F, 0.F, 0.F}; - - IMUKit imukit {stub_event_loop, accel, gyro}; + IMUKit imukit {mock_lsm6dox}; }; TEST_F(IMUKitTest, initialization) @@ -40,73 +38,63 @@ TEST_F(IMUKitTest, initialization) ASSERT_NE(&imukit, nullptr); } -TEST_F(IMUKitTest, registerMockCallbackAndStart) +TEST_F(IMUKitTest, start) { - auto mock_function = MockFunction {}; - auto loop = [&] { mock_function.Call(); }; + EXPECT_CALL(mock_lsm6dox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); - EXPECT_CALL(mock_function, Call()).Times(1); - - stub_event_loop.registerCallback(loop); imukit.start(); } -TEST_F(IMUKitTest, start10Loops) +TEST_F(IMUKitTest, stop) { - auto mock_function = MockFunction {}; - auto loop = [&] { mock_function.Call(); }; + EXPECT_CALL(mock_lsm6dox, setPowerMode(interface::LSM6DSOX::PowerMode::Off)).Times(1); - stub_event_loop.spy_setNumberOfLoops(10); - EXPECT_CALL(mock_function, Call()).Times(10); - - stub_event_loop.registerCallback(loop); - imukit.start(); + imukit.stop(); } -TEST_F(IMUKitTest, stop) +TEST_F(IMUKitTest, computeAnglesNullAccelerations) { - auto mock_function = MockFunction {}; - auto loop = [&] { - mock_function.Call(); - imukit.stop(); - }; + std::function callback {}; - stub_event_loop.spy_setNumberOfLoops(2); - EXPECT_CALL(mock_function, Call()).Times(1); + EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - stub_event_loop.registerCallback(loop); - imukit.start(); -} + imukit.init(); -TEST_F(IMUKitTest, run) -{ - auto mock_function = MockFunction {}; - auto loop = [&] { - mock_function.Call(); - imukit.stop(); - imukit.run(); + interface::LSM6DSOX::SensorData imu_data + { + {0, 0, 0}, { 0, 0, 0 } }; - stub_event_loop.spy_setNumberOfLoops(2); - EXPECT_CALL(mock_function, Call()).Times(1); + callback(imu_data); - stub_event_loop.registerCallback(loop); - imukit.start(); + auto [pitch, roll, yaw] = imukit.getAngles(); + + for (auto i = 0; i < 100; ++i) { + auto [pitch, roll, yaw] = imukit.getAngles(); + EXPECT_EQ(pitch, 0); + EXPECT_EQ(roll, 0); + EXPECT_EQ(yaw, 180); + } } -TEST_F(IMUKitTest, computeAnglesNullAccelerations) +TEST_F(IMUKitTest, defaultPosition) { - accel_data = {0.F, 0.F, 0.F}; - gyro_data = {0.F, 0.F, 0.F}; + std::function callback {}; + + EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - EXPECT_CALL(accel, getXYZ).WillRepeatedly(Return(accel_data)); - EXPECT_CALL(gyro, getXYZ).WillRepeatedly(Return(gyro_data)); + imukit.init(); + + interface::LSM6DSOX::SensorData imu_data + { + {0, 0, 1000}, { 0, 0, 0 } + }; + + callback(imu_data); auto [pitch, roll, yaw] = imukit.getAngles(); for (auto i = 0; i < 100; ++i) { - imukit.computeAngles(); - auto [pitch, roll, yaw] = imukit.getAngles(); EXPECT_EQ(pitch, 0); EXPECT_EQ(roll, 0); @@ -114,92 +102,104 @@ TEST_F(IMUKitTest, computeAnglesNullAccelerations) } } -TEST_F(IMUKitTest, defaultPosition) -{ - accel_data = {0.F, 0.F, 1000.F}; +// TODO (@ladislas, @hugo) Go further with numeric testing when new fusion is implemented - EXPECT_CALL(accel, getXYZ).WillRepeatedly(Return(accel_data)); - EXPECT_CALL(gyro, getXYZ).Times(AnyNumber()); +// TEST_F(IMUKitTest, robotRolled90DegreesOnTheTop) +// { +// std::function callback {}; - for (auto i = 0; i < 100; ++i) { - imukit.computeAngles(); - } +// EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - auto [pitch, roll, yaw] = imukit.getAngles(); +// imukit.init(); - EXPECT_EQ(pitch, 0); - EXPECT_EQ(roll, 0); - EXPECT_EQ(yaw, 180); -} +// interface::LSM6DSOX::SensorData imu_data +// { +// {1000, 0, 0}, { 0, 0, 0 } +// }; -TEST_F(IMUKitTest, robotRolled90DegreesOnTheTop) -{ - accel_data = {1000.F, 0.F, 0.F}; +// callback(imu_data); - EXPECT_CALL(accel, getXYZ).WillRepeatedly(Return(accel_data)); - EXPECT_CALL(gyro, getXYZ).Times(AnyNumber()); +// auto [pitch, roll, yaw] = imukit.getAngles(); - for (auto i = 0; i < 100; ++i) { - imukit.computeAngles(); - } +// for (auto i = 0; i < 100; ++i) { +// auto [pitch, roll, yaw] = imukit.getAngles(); +// EXPECT_TRUE(-100 < pitch && pitch < -80); +// EXPECT_EQ(roll, 0); +// EXPECT_EQ(yaw, 180); +// } +// } - auto [pitch, roll, yaw] = imukit.getAngles(); +// TEST_F(IMUKitTest, robotRolled90DegreesOnTheBottom) +// { +// std::function callback {}; - EXPECT_TRUE(-100 < pitch && pitch < -80); - EXPECT_EQ(roll, 0); - EXPECT_EQ(yaw, 180); -} +// EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); -TEST_F(IMUKitTest, robotRolled90DegreesOnTheBottom) -{ - accel_data = {-1000.F, 0.F, 0.F}; +// imukit.init(); - EXPECT_CALL(accel, getXYZ).WillRepeatedly(Return(accel_data)); - EXPECT_CALL(gyro, getXYZ).Times(AnyNumber()); +// interface::LSM6DSOX::SensorData imu_data +// { +// {-1000, 0, 0}, { 0, 0, 0 } +// }; - for (auto i = 0; i < 100; ++i) { - imukit.computeAngles(); - } +// callback(imu_data); - auto [pitch, roll, yaw] = imukit.getAngles(); +// auto [pitch, roll, yaw] = imukit.getAngles(); - EXPECT_TRUE(80 < pitch && pitch < 100); - EXPECT_EQ(roll, 0); - EXPECT_EQ(yaw, 180); -} +// for (auto i = 0; i < 100; ++i) { +// auto [pitch, roll, yaw] = imukit.getAngles(); +// EXPECT_TRUE(80 < pitch && pitch < 100); +// EXPECT_EQ(roll, 0); +// EXPECT_EQ(yaw, 180); +// } +// } -TEST_F(IMUKitTest, robotRolled90DegreesOnItsLeft) -{ - accel_data = {0.F, 1000.F, 0.F}; +// TEST_F(IMUKitTest, robotRolled90DegreesOnItsLeft) +// { +// std::function callback {}; - EXPECT_CALL(accel, getXYZ).WillRepeatedly(Return(accel_data)); - EXPECT_CALL(gyro, getXYZ).Times(AnyNumber()); +// EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - for (auto i = 0; i < 100; ++i) { - imukit.computeAngles(); - } +// imukit.init(); - auto [pitch, roll, yaw] = imukit.getAngles(); +// interface::LSM6DSOX::SensorData imu_data +// { +// {0, 1000, 0}, { 0, 0, 0 } +// }; - EXPECT_EQ(pitch, 0); - EXPECT_TRUE(80 < roll && roll < 100); - EXPECT_EQ(yaw, 180); -} +// callback(imu_data); -TEST_F(IMUKitTest, robotRolled90DegreesOnItsRight) -{ - accel_data = {0.F, -1000.F, 0.F}; +// auto [pitch, roll, yaw] = imukit.getAngles(); - EXPECT_CALL(accel, getXYZ).WillRepeatedly(Return(accel_data)); - EXPECT_CALL(gyro, getXYZ).Times(AnyNumber()); +// for (auto i = 0; i < 100; ++i) { +// auto [pitch, roll, yaw] = imukit.getAngles(); +// EXPECT_EQ(pitch, 0); +// EXPECT_TRUE(80 < roll && roll < 100); +// EXPECT_EQ(yaw, 180); +// } +// } - for (auto i = 0; i < 100; ++i) { - imukit.computeAngles(); - } +// TEST_F(IMUKitTest, robotRolled90DegreesOnItsRight) +// { +// std::function callback {}; - auto [pitch, roll, yaw] = imukit.getAngles(); +// EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - EXPECT_EQ(pitch, 0); - EXPECT_TRUE(-100 < roll && roll < -80); - EXPECT_EQ(yaw, 180); -} +// imukit.init(); + +// interface::LSM6DSOX::SensorData imu_data +// { +// {0, -1000, 0}, { 0, 0, 0 } +// }; + +// callback(imu_data); + +// auto [pitch, roll, yaw] = imukit.getAngles(); + +// for (auto i = 0; i < 100; ++i) { +// auto [pitch, roll, yaw] = imukit.getAngles(); +// EXPECT_EQ(pitch, 0); +// EXPECT_TRUE(-100 < roll && roll < -80); +// EXPECT_EQ(yaw, 180); +// } +// } diff --git a/libs/MotionKit/tests/MotionKit_test.cpp b/libs/MotionKit/tests/MotionKit_test.cpp index dff7547aa0..4a8fc1ced2 100644 --- a/libs/MotionKit/tests/MotionKit_test.cpp +++ b/libs/MotionKit/tests/MotionKit_test.cpp @@ -6,9 +6,8 @@ #include "IMUKit.h" #include "gtest/gtest.h" -#include "mocks/leka/Accelerometer.h" #include "mocks/leka/CoreMotor.h" -#include "mocks/leka/Gyroscope.h" +#include "mocks/leka/LSM6DSOX.h" #include "mocks/leka/Timeout.h" #include "stubs/leka/EventLoopKit.h" @@ -16,6 +15,8 @@ using namespace leka; using ::testing::MockFunction; +// TODO(@leka/dev-embedded): temporary fix, changes are needed when updating fusion algorithm + ACTION_TEMPLATE(GetCallback, HAS_1_TEMPLATE_PARAMS(typename, callback_t), AND_1_VALUE_PARAMS(pointer)) { *pointer = callback_t(arg0); @@ -28,26 +29,26 @@ class MotionKitTest : public ::testing::Test void SetUp() override { + EXPECT_CALL(lsm6dsox, registerOnGyDataReadyCallback).Times(1); + imukit.init(); motion.init(); } // void TearDown() override {} - stub::EventLoopKit stub_event_loop_imu {}; stub::EventLoopKit stub_event_loop_motion {}; mock::CoreMotor mock_motor_left {}; mock::CoreMotor mock_motor_right {}; - mock::Accelerometer accel {}; - mock::Gyroscope gyro {}; + mock::LSM6DSOX lsm6dsox {}; mock::Timeout mock_timeout {}; std::tuple accel_data = {0.F, 0.F, 0.F}; std::tuple gyro_data = {0.F, 0.F, 0.F}; - IMUKit imukit {stub_event_loop_imu, accel, gyro}; + IMUKit imukit {lsm6dsox}; MotionKit motion {mock_motor_left, mock_motor_right, imukit, stub_event_loop_motion, mock_timeout}; }; @@ -59,13 +60,10 @@ TEST_F(MotionKitTest, initialization) TEST_F(MotionKitTest, registerMockCallbackAndRotate) { - auto mock_function_imu = MockFunction {}; - auto loop_imu = [&] { mock_function_imu.Call(); }; - auto mock_function_motion = MockFunction {}; auto loop_motion = [&] { mock_function_motion.Call(); }; - EXPECT_CALL(mock_function_imu, Call()).Times(1); + EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); EXPECT_CALL(mock_function_motion, Call()).Times(1); EXPECT_CALL(mock_timeout, stop).Times(1); @@ -78,43 +76,35 @@ TEST_F(MotionKitTest, registerMockCallbackAndRotate) EXPECT_CALL(mock_motor_left, spin(Rotation::clockwise, 1)).Times(1); EXPECT_CALL(mock_motor_right, spin(Rotation::clockwise, 1)).Times(1); - stub_event_loop_imu.registerCallback(loop_imu); stub_event_loop_motion.registerCallback(loop_motion); motion.rotate(1, Rotation::clockwise); } TEST_F(MotionKitTest, registerMockCallbackAndStartStabilisation) { - auto mock_function_imu = MockFunction {}; - auto loop_imu = [&] { mock_function_imu.Call(); }; - auto mock_function_motion = MockFunction {}; auto loop_motion = [&] { mock_function_motion.Call(); }; - EXPECT_CALL(mock_function_imu, Call()).Times(1); + EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); EXPECT_CALL(mock_function_motion, Call()).Times(1); EXPECT_CALL(mock_timeout, stop).Times(1); EXPECT_CALL(mock_motor_left, stop).Times(1); EXPECT_CALL(mock_motor_right, stop).Times(1); - stub_event_loop_imu.registerCallback(loop_imu); stub_event_loop_motion.registerCallback(loop_motion); motion.startStabilisation(); } TEST_F(MotionKitTest, rotateAndStop) { - auto mock_function_imu = MockFunction {}; - auto loop_imu = [&] { mock_function_imu.Call(); }; - auto mock_function_motion = MockFunction {}; auto loop_motion = [&] { mock_function_motion.Call(); motion.stop(); }; - EXPECT_CALL(mock_function_imu, Call()).Times(1); + EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); EXPECT_CALL(mock_function_motion, Call()).Times(1); EXPECT_CALL(mock_timeout, stop).Times(2); @@ -127,22 +117,18 @@ TEST_F(MotionKitTest, rotateAndStop) EXPECT_CALL(mock_timeout, onTimeout).Times(1); EXPECT_CALL(mock_timeout, start).Times(1); - stub_event_loop_imu.registerCallback(loop_imu); stub_event_loop_motion.registerCallback(loop_motion); motion.rotate(1, Rotation::clockwise); } TEST_F(MotionKitTest, rotateAndTimeOutOver) { - auto mock_function_imu = MockFunction {}; - auto loop_imu = [&] { mock_function_imu.Call(); }; - auto mock_function_motion = MockFunction {}; auto loop_motion = [&] { mock_function_motion.Call(); }; interface::Timeout::callback_t on_timeout_callback = {}; - EXPECT_CALL(mock_function_imu, Call()).Times(1); + EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); EXPECT_CALL(mock_function_motion, Call()).Times(1); EXPECT_CALL(mock_timeout, stop).Times(1); @@ -155,7 +141,6 @@ TEST_F(MotionKitTest, rotateAndTimeOutOver) EXPECT_CALL(mock_timeout, onTimeout).WillOnce(GetCallback(&on_timeout_callback)); EXPECT_CALL(mock_timeout, start).Times(1); - stub_event_loop_imu.registerCallback(loop_imu); stub_event_loop_motion.registerCallback(loop_motion); motion.rotate(1, Rotation::clockwise); @@ -168,23 +153,19 @@ TEST_F(MotionKitTest, rotateAndTimeOutOver) TEST_F(MotionKitTest, startStabilisationAndStop) { - auto mock_function_imu = MockFunction {}; - auto loop_imu = [&] { mock_function_imu.Call(); }; - auto mock_function_motion = MockFunction {}; auto loop_motion = [&] { mock_function_motion.Call(); motion.stop(); }; - EXPECT_CALL(mock_function_imu, Call()).Times(1); + EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); EXPECT_CALL(mock_function_motion, Call()).Times(1); EXPECT_CALL(mock_timeout, stop).Times(2); EXPECT_CALL(mock_motor_left, stop).Times(2); EXPECT_CALL(mock_motor_right, stop).Times(2); - stub_event_loop_imu.registerCallback(loop_imu); stub_event_loop_motion.registerCallback(loop_motion); motion.startStabilisation(); } diff --git a/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp b/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp index 181153866d..04722f9ee0 100644 --- a/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp +++ b/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp @@ -8,9 +8,8 @@ #include "MotionKit.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "mocks/leka/Accelerometer.h" #include "mocks/leka/CoreMotor.h" -#include "mocks/leka/Gyroscope.h" +#include "mocks/leka/LSM6DSOX.h" #include "mocks/leka/LedKit.h" #include "mocks/leka/Timeout.h" #include "mocks/leka/VideoKit.h" @@ -40,18 +39,16 @@ class ReinforcerkitTest : public ::testing::Test mock::LedKit mock_ledkit; - stub::EventLoopKit stub_event_loop_imu {}; stub::EventLoopKit stub_event_loop_motion {}; mock::CoreMotor mock_motor_left {}; mock::CoreMotor mock_motor_right {}; - mock::Accelerometer accel {}; - mock::Gyroscope gyro {}; + mock::LSM6DSOX lsm6dsox {}; mock::Timeout mock_timeout {}; - IMUKit imukit {stub_event_loop_imu, accel, gyro}; + IMUKit imukit {lsm6dsox}; MotionKit motion {mock_motor_left, mock_motor_right, imukit, stub_event_loop_motion, mock_timeout}; @@ -64,6 +61,7 @@ class ReinforcerkitTest : public ::testing::Test EXPECT_CALL(mock_motor_right, stop).Times(1); EXPECT_CALL(mock_motor_left, spin).Times(1); EXPECT_CALL(mock_motor_right, spin).Times(1); + EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); EXPECT_CALL(mock_timeout, stop).Times(AtMost(1)); EXPECT_CALL(mock_timeout, onTimeout).Times(AtMost(1)); EXPECT_CALL(mock_timeout, start).Times(AtMost(1)); diff --git a/tests/unit/mocks/mocks/leka/Accelerometer.h b/tests/unit/mocks/mocks/leka/Accelerometer.h deleted file mode 100644 index 0809b9b6c3..0000000000 --- a/tests/unit/mocks/mocks/leka/Accelerometer.h +++ /dev/null @@ -1,18 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "gmock/gmock.h" -#include "interface/Accelerometer.h" - -namespace leka::mock { - -class Accelerometer : public interface::Accelerometer -{ - public: - MOCK_METHOD((std::tuple), getXYZ, (), (override)); -}; - -} // namespace leka::mock diff --git a/tests/unit/mocks/mocks/leka/Gyroscope.h b/tests/unit/mocks/mocks/leka/Gyroscope.h deleted file mode 100644 index 44666cb919..0000000000 --- a/tests/unit/mocks/mocks/leka/Gyroscope.h +++ /dev/null @@ -1,18 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "gmock/gmock.h" -#include "interface/Gyroscope.h" - -namespace leka::mock { - -class Gyroscope : public interface::Gyroscope -{ - public: - MOCK_METHOD((std::tuple), getXYZ, (), (override)); -}; - -} // namespace leka::mock diff --git a/tests/unit/mocks/mocks/leka/LSM6DSOX.h b/tests/unit/mocks/mocks/leka/LSM6DSOX.h index 99f073468e..eb2b115e12 100644 --- a/tests/unit/mocks/mocks/leka/LSM6DSOX.h +++ b/tests/unit/mocks/mocks/leka/LSM6DSOX.h @@ -13,8 +13,8 @@ class LSM6DSOX : public interface::LSM6DSOX { public: MOCK_METHOD(void, init, (), (override)); + MOCK_METHOD(void, registerOnGyDataReadyCallback, (std::function const &), (override)); MOCK_METHOD(void, setPowerMode, (PowerMode), (override)); - MOCK_METHOD(SensorData &, getData, (), (override)); }; } // namespace leka::mock From 440001b9a667ec1650d0dfd1d5753b2e9c4264f1 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 31 Jan 2023 17:10:48 +0100 Subject: [PATCH 083/143] :truck: (imu): Rename C++ headers with .hpp (instead of .h) --- app/os/main.cpp | 2 +- .../CoreIMU/include/{CoreLSM6DSOX.h => CoreLSM6DSOX.hpp} | 2 +- .../CoreIMU/include/interface/{LSM6DSOX.h => LSM6DSOX.hpp} | 1 + drivers/CoreIMU/source/CoreLSM6DSOX.cpp | 2 +- drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp | 2 +- libs/IMUKit/include/{IMUKit.h => IMUKit.hpp} | 4 ++-- libs/IMUKit/include/internal/Mahony.cpp | 2 +- libs/IMUKit/include/internal/{Mahony.h => Mahony.hpp} | 0 libs/IMUKit/source/IMUKit.cpp | 2 +- libs/IMUKit/tests/IMUKit_test.cpp | 4 ++-- libs/MotionKit/include/{MotionKit.h => MotionKit.hpp} | 4 ++-- libs/MotionKit/include/{PID.h => PID.hpp} | 0 libs/MotionKit/source/MotionKit.cpp | 2 +- libs/MotionKit/source/PID.cpp | 2 +- libs/MotionKit/tests/MotionKit_test.cpp | 4 ++-- libs/MotionKit/tests/PID_test.cpp | 2 +- libs/ReinforcerKit/include/ReinforcerKit.h | 2 +- libs/ReinforcerKit/tests/ReinforcerKit_test.cpp | 2 +- spikes/lk_accel_gyro/main.cpp | 2 +- spikes/lk_command_kit/main.cpp | 2 +- spikes/lk_imu_kit/main.cpp | 4 ++-- spikes/lk_motion_kit/main.cpp | 6 +++--- spikes/lk_reinforcer/main.cpp | 6 +++--- tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp | 2 +- tests/functional/tests/core_imu/utils.h | 2 +- tests/functional/tests/imu_kit/suite_imu_kit.cpp | 4 ++-- tests/unit/mocks/mocks/leka/LSM6DSOX.h | 2 +- 27 files changed, 35 insertions(+), 34 deletions(-) rename drivers/CoreIMU/include/{CoreLSM6DSOX.h => CoreLSM6DSOX.hpp} (97%) rename drivers/CoreIMU/include/interface/{LSM6DSOX.h => LSM6DSOX.hpp} (99%) rename libs/IMUKit/include/{IMUKit.h => IMUKit.hpp} (92%) rename libs/IMUKit/include/internal/{Mahony.h => Mahony.hpp} (100%) rename libs/MotionKit/include/{MotionKit.h => MotionKit.hpp} (97%) rename libs/MotionKit/include/{PID.h => PID.hpp} (100%) diff --git a/app/os/main.cpp b/app/os/main.cpp index 396d69d9ed..3ac5081c44 100644 --- a/app/os/main.cpp +++ b/app/os/main.cpp @@ -27,7 +27,7 @@ #include "CoreLCD.hpp" #include "CoreLCDDriverOTM8009A.hpp" #include "CoreLL.h" -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "CoreLTDC.hpp" #include "CoreMCU.h" #include "CoreMotor.h" diff --git a/drivers/CoreIMU/include/CoreLSM6DSOX.h b/drivers/CoreIMU/include/CoreLSM6DSOX.hpp similarity index 97% rename from drivers/CoreIMU/include/CoreLSM6DSOX.h rename to drivers/CoreIMU/include/CoreLSM6DSOX.hpp index 266eed72b4..5081254e49 100644 --- a/drivers/CoreIMU/include/CoreLSM6DSOX.h +++ b/drivers/CoreIMU/include/CoreLSM6DSOX.hpp @@ -8,7 +8,7 @@ #include "CoreEventQueue.h" #include "CoreInterruptIn.h" -#include "interface/LSM6DSOX.h" +#include "interface/LSM6DSOX.hpp" #include "interface/drivers/I2C.h" #include "lsm6dsox_reg.h" diff --git a/drivers/CoreIMU/include/interface/LSM6DSOX.h b/drivers/CoreIMU/include/interface/LSM6DSOX.hpp similarity index 99% rename from drivers/CoreIMU/include/interface/LSM6DSOX.h rename to drivers/CoreIMU/include/interface/LSM6DSOX.hpp index cdc136e197..c408568944 100644 --- a/drivers/CoreIMU/include/interface/LSM6DSOX.h +++ b/drivers/CoreIMU/include/interface/LSM6DSOX.hpp @@ -5,6 +5,7 @@ #pragma once #include + namespace leka::interface { class LSM6DSOX diff --git a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp index e5c8cfb7e8..825dd0f2a8 100644 --- a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp +++ b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp @@ -2,7 +2,7 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" namespace leka { diff --git a/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp b/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp index 402449e468..6e32c3af81 100644 --- a/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp +++ b/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp @@ -4,7 +4,7 @@ #include -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "gtest/gtest.h" #include "mocks/leka/CoreI2C.h" #include "mocks/leka/EventQueue.h" diff --git a/libs/IMUKit/include/IMUKit.h b/libs/IMUKit/include/IMUKit.hpp similarity index 92% rename from libs/IMUKit/include/IMUKit.h rename to libs/IMUKit/include/IMUKit.hpp index 16b0f2a03e..28b723d9bc 100644 --- a/libs/IMUKit/include/IMUKit.h +++ b/libs/IMUKit/include/IMUKit.hpp @@ -6,8 +6,8 @@ #include -#include "interface/LSM6DSOX.h" -#include "internal/Mahony.h" +#include "interface/LSM6DSOX.hpp" +#include "internal/Mahony.hpp" namespace leka { diff --git a/libs/IMUKit/include/internal/Mahony.cpp b/libs/IMUKit/include/internal/Mahony.cpp index b5dd0d06a6..389da6ddac 100644 --- a/libs/IMUKit/include/internal/Mahony.cpp +++ b/libs/IMUKit/include/internal/Mahony.cpp @@ -3,7 +3,7 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "Mahony.h" +#include "Mahony.hpp" #include #include diff --git a/libs/IMUKit/include/internal/Mahony.h b/libs/IMUKit/include/internal/Mahony.hpp similarity index 100% rename from libs/IMUKit/include/internal/Mahony.h rename to libs/IMUKit/include/internal/Mahony.hpp diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index ed21de9f5f..aec8d633d5 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -2,7 +2,7 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "IMUKit.h" +#include "IMUKit.hpp" using namespace leka; diff --git a/libs/IMUKit/tests/IMUKit_test.cpp b/libs/IMUKit/tests/IMUKit_test.cpp index df92c3d379..98ae3ceba5 100644 --- a/libs/IMUKit/tests/IMUKit_test.cpp +++ b/libs/IMUKit/tests/IMUKit_test.cpp @@ -2,9 +2,9 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "IMUKit.h" +#include "IMUKit.hpp" -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "ThisThread.h" #include "gmock/gmock.h" #include "gtest/gtest.h" diff --git a/libs/MotionKit/include/MotionKit.h b/libs/MotionKit/include/MotionKit.hpp similarity index 97% rename from libs/MotionKit/include/MotionKit.h rename to libs/MotionKit/include/MotionKit.hpp index 35a7b2bae8..ffb7fcc985 100644 --- a/libs/MotionKit/include/MotionKit.h +++ b/libs/MotionKit/include/MotionKit.hpp @@ -7,8 +7,8 @@ #include #include -#include "IMUKit.h" -#include "PID.h" +#include "IMUKit.hpp" +#include "PID.hpp" #include "interface/drivers/Timeout.h" namespace leka { diff --git a/libs/MotionKit/include/PID.h b/libs/MotionKit/include/PID.hpp similarity index 100% rename from libs/MotionKit/include/PID.h rename to libs/MotionKit/include/PID.hpp diff --git a/libs/MotionKit/source/MotionKit.cpp b/libs/MotionKit/source/MotionKit.cpp index ead7cb8fab..e40a63b0ab 100644 --- a/libs/MotionKit/source/MotionKit.cpp +++ b/libs/MotionKit/source/MotionKit.cpp @@ -2,7 +2,7 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "MotionKit.h" +#include "MotionKit.hpp" #include "MathUtils.h" #include "ThisThread.h" diff --git a/libs/MotionKit/source/PID.cpp b/libs/MotionKit/source/PID.cpp index a0dc76e00c..2317d2a10b 100644 --- a/libs/MotionKit/source/PID.cpp +++ b/libs/MotionKit/source/PID.cpp @@ -2,7 +2,7 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "PID.h" +#include "PID.hpp" #include using namespace leka; diff --git a/libs/MotionKit/tests/MotionKit_test.cpp b/libs/MotionKit/tests/MotionKit_test.cpp index 4a8fc1ced2..33a2e787a4 100644 --- a/libs/MotionKit/tests/MotionKit_test.cpp +++ b/libs/MotionKit/tests/MotionKit_test.cpp @@ -2,9 +2,9 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "MotionKit.h" +#include "MotionKit.hpp" -#include "IMUKit.h" +#include "IMUKit.hpp" #include "gtest/gtest.h" #include "mocks/leka/CoreMotor.h" #include "mocks/leka/LSM6DSOX.h" diff --git a/libs/MotionKit/tests/PID_test.cpp b/libs/MotionKit/tests/PID_test.cpp index 3ed4f4cf60..69e33374fb 100644 --- a/libs/MotionKit/tests/PID_test.cpp +++ b/libs/MotionKit/tests/PID_test.cpp @@ -2,7 +2,7 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include "PID.h" +#include "PID.hpp" #include "gtest/gtest.h" diff --git a/libs/ReinforcerKit/include/ReinforcerKit.h b/libs/ReinforcerKit/include/ReinforcerKit.h index 3e00764a50..38e7ef8335 100644 --- a/libs/ReinforcerKit/include/ReinforcerKit.h +++ b/libs/ReinforcerKit/include/ReinforcerKit.h @@ -8,7 +8,7 @@ #include #include -#include "MotionKit.h" +#include "MotionKit.hpp" namespace leka { diff --git a/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp b/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp index 04722f9ee0..6559cba720 100644 --- a/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp +++ b/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp @@ -5,7 +5,7 @@ #include "ReinforcerKit.h" #include "LedKitAnimations.h" -#include "MotionKit.h" +#include "MotionKit.hpp" #include "gmock/gmock.h" #include "gtest/gtest.h" #include "mocks/leka/CoreMotor.h" diff --git a/spikes/lk_accel_gyro/main.cpp b/spikes/lk_accel_gyro/main.cpp index 849196804e..a578246893 100644 --- a/spikes/lk_accel_gyro/main.cpp +++ b/spikes/lk_accel_gyro/main.cpp @@ -5,7 +5,7 @@ #include "rtos/ThisThread.h" #include "CoreI2C.h" -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "HelloWorld.h" #include "LogKit.h" diff --git a/spikes/lk_command_kit/main.cpp b/spikes/lk_command_kit/main.cpp index 00c7422337..8282dfe00c 100644 --- a/spikes/lk_command_kit/main.cpp +++ b/spikes/lk_command_kit/main.cpp @@ -20,7 +20,7 @@ #include "CoreLCDDriverOTM8009A.hpp" #include "CoreLED.h" #include "CoreLL.h" -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "CoreLTDC.hpp" #include "CoreMotor.h" #include "CorePwm.h" diff --git a/spikes/lk_imu_kit/main.cpp b/spikes/lk_imu_kit/main.cpp index 40c338b7b3..6d38341829 100644 --- a/spikes/lk_imu_kit/main.cpp +++ b/spikes/lk_imu_kit/main.cpp @@ -5,9 +5,9 @@ #include "rtos/ThisThread.h" #include "CoreI2C.h" -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "HelloWorld.h" -#include "IMUKit.h" +#include "IMUKit.hpp" #include "LogKit.h" using namespace std::chrono; diff --git a/spikes/lk_motion_kit/main.cpp b/spikes/lk_motion_kit/main.cpp index a318cc27aa..f3d92971da 100644 --- a/spikes/lk_motion_kit/main.cpp +++ b/spikes/lk_motion_kit/main.cpp @@ -6,16 +6,16 @@ #include "CoreBufferedSerial.h" #include "CoreI2C.h" -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "CoreMotor.h" #include "CorePwm.h" #include "CoreRFIDReaderCR95HF.h" #include "CoreTimeout.h" #include "EventLoopKit.h" #include "HelloWorld.h" -#include "IMUKit.h" +#include "IMUKit.hpp" #include "LogKit.h" -#include "MotionKit.h" +#include "MotionKit.hpp" #include "RFIDKit.h" using namespace std::chrono; diff --git a/spikes/lk_reinforcer/main.cpp b/spikes/lk_reinforcer/main.cpp index 959b1fb5c2..c2a0262ff1 100644 --- a/spikes/lk_reinforcer/main.cpp +++ b/spikes/lk_reinforcer/main.cpp @@ -18,7 +18,7 @@ #include "CoreLCD.hpp" #include "CoreLED.h" #include "CoreLL.h" -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "CoreLTDC.hpp" #include "CoreMotor.h" #include "CorePwm.h" @@ -30,10 +30,10 @@ #include "EventLoopKit.h" #include "FATFileSystem.h" #include "HelloWorld.h" -#include "IMUKit.h" +#include "IMUKit.hpp" #include "LedKit.h" #include "LogKit.h" -#include "MotionKit.h" +#include "MotionKit.hpp" #include "ReinforcerKit.h" #include "SDBlockDevice.h" #include "VideoKit.h" diff --git a/tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp b/tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp index cc44f37def..ca848ed83a 100644 --- a/tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp +++ b/tests/functional/tests/core_imu/suite_core_lsm6dsox.cpp @@ -6,7 +6,7 @@ #include "./utils.h" #include "CoreI2C.h" -#include "CoreLSM6DSOX.h" +#include "CoreLSM6DSOX.hpp" #include "tests/config.h" using namespace leka; diff --git a/tests/functional/tests/core_imu/utils.h b/tests/functional/tests/core_imu/utils.h index 60ed1cafcc..e2e829cfdf 100644 --- a/tests/functional/tests/core_imu/utils.h +++ b/tests/functional/tests/core_imu/utils.h @@ -10,7 +10,7 @@ #include "rtos/ThisThread.h" #include "boost/ut.hpp" -#include "interface/LSM6DSOX.h" +#include "interface/LSM6DSOX.hpp" inline auto values_did_change_over_time(leka::interface::LSM6DSOX &lsm6dsox) { diff --git a/tests/functional/tests/imu_kit/suite_imu_kit.cpp b/tests/functional/tests/imu_kit/suite_imu_kit.cpp index b153ae97d3..482671647a 100644 --- a/tests/functional/tests/imu_kit/suite_imu_kit.cpp +++ b/tests/functional/tests/imu_kit/suite_imu_kit.cpp @@ -5,8 +5,8 @@ #include "rtos/ThisThread.h" #include "CoreI2C.h" -#include "CoreLSM6DSOX.h" -#include "IMUKit.h" +#include "CoreLSM6DSOX.hpp" +#include "IMUKit.hpp" #include "tests/config.h" using namespace leka; diff --git a/tests/unit/mocks/mocks/leka/LSM6DSOX.h b/tests/unit/mocks/mocks/leka/LSM6DSOX.h index eb2b115e12..a06ea5cea0 100644 --- a/tests/unit/mocks/mocks/leka/LSM6DSOX.h +++ b/tests/unit/mocks/mocks/leka/LSM6DSOX.h @@ -5,7 +5,7 @@ #pragma once #include "gmock/gmock.h" -#include "interface/LSM6DSOX.h" +#include "interface/LSM6DSOX.hpp" namespace leka::mock { From b9408bfa815db0e835d7f9725756b59e79966c82 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 31 Jan 2023 23:06:19 +0100 Subject: [PATCH 084/143] :recycle: (CoreLSM6DSOX): Refactor setPowerMode method --- drivers/CoreIMU/source/CoreLSM6DSOX.cpp | 58 ++++++++++++++----------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp index 825dd0f2a8..59c971341c 100644 --- a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp +++ b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp @@ -35,32 +35,40 @@ void CoreLSM6DSOX::init() void CoreLSM6DSOX::setPowerMode(PowerMode mode) { - if (mode == PowerMode::Off) { - lsm6dsox_xl_data_rate_set(&_register_io_function, LSM6DSOX_XL_ODR_OFF); - lsm6dsox_gy_data_rate_set(&_register_io_function, LSM6DSOX_GY_ODR_OFF); - } else { - lsm6dsox_xl_hm_mode_t xl_power_mode {}; - lsm6dsox_g_hm_mode_t gy_power_mode {}; - switch (mode) { - case PowerMode::UltraLow: - xl_power_mode = LSM6DSOX_ULTRA_LOW_POWER_MD; - gy_power_mode = LSM6DSOX_GY_NORMAL; - break; - default: - case PowerMode::Normal: - xl_power_mode = LSM6DSOX_LOW_NORMAL_POWER_MD; - gy_power_mode = LSM6DSOX_GY_NORMAL; - break; - case PowerMode::High: - xl_power_mode = LSM6DSOX_HIGH_PERFORMANCE_MD; - gy_power_mode = LSM6DSOX_GY_HIGH_PERFORMANCE; - break; - } - lsm6dsox_xl_power_mode_set(&_register_io_function, xl_power_mode); - lsm6dsox_gy_power_mode_set(&_register_io_function, gy_power_mode); - lsm6dsox_xl_data_rate_set(&_register_io_function, lsm6dsox_odr_xl_t::LSM6DSOX_XL_ODR_52Hz); - lsm6dsox_gy_data_rate_set(&_register_io_function, lsm6dsox_odr_g_t::LSM6DSOX_GY_ODR_52Hz); + auto xl_power_mode = lsm6dsox_xl_hm_mode_t {}; + auto gy_power_mode = lsm6dsox_g_hm_mode_t {}; + + auto xl_odr = lsm6dsox_odr_xl_t {LSM6DSOX_XL_ODR_52Hz}; + auto gy_odr = lsm6dsox_odr_g_t {LSM6DSOX_GY_ODR_52Hz}; + + switch (mode) { + case PowerMode::Off: + xl_odr = LSM6DSOX_XL_ODR_OFF; + gy_odr = LSM6DSOX_GY_ODR_OFF; + xl_power_mode = LSM6DSOX_ULTRA_LOW_POWER_MD; + gy_power_mode = LSM6DSOX_GY_NORMAL; + break; + + case PowerMode::UltraLow: + xl_power_mode = LSM6DSOX_ULTRA_LOW_POWER_MD; + gy_power_mode = LSM6DSOX_GY_NORMAL; + break; + + case PowerMode::Normal: + xl_power_mode = LSM6DSOX_LOW_NORMAL_POWER_MD; + gy_power_mode = LSM6DSOX_GY_NORMAL; + break; + + case PowerMode::High: + xl_power_mode = LSM6DSOX_HIGH_PERFORMANCE_MD; + gy_power_mode = LSM6DSOX_GY_HIGH_PERFORMANCE; + break; } + + lsm6dsox_xl_power_mode_set(&_register_io_function, xl_power_mode); + lsm6dsox_gy_power_mode_set(&_register_io_function, gy_power_mode); + lsm6dsox_xl_data_rate_set(&_register_io_function, xl_odr); + lsm6dsox_gy_data_rate_set(&_register_io_function, gy_odr); } void CoreLSM6DSOX::registerOnGyDataReadyCallback(drdy_callback_t const &callback) From 67f01fa91f24c47d2626a480e04f49ec42b9da56 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 10 Jan 2023 17:48:02 +0100 Subject: [PATCH 085/143] :recycle: (CoreIMU): Return g intead of mg As we are also returning dps, it makes more sense to return g instead of mg --- drivers/CoreIMU/source/CoreLSM6DSOX.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp index 59c971341c..528168a546 100644 --- a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp +++ b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp @@ -78,15 +78,17 @@ void CoreLSM6DSOX::registerOnGyDataReadyCallback(drdy_callback_t const &callback void CoreLSM6DSOX::onGyrDataReadyHandler() { + static constexpr auto _1k = float {1000.F}; + lsm6dsox_angular_rate_raw_get(&_register_io_function, data_raw_gy.data()); - _sensor_data.gy.x = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(0)) / 1000.F; - _sensor_data.gy.y = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(1)) / 1000.F; - _sensor_data.gy.z = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(2)) / 1000.F; + _sensor_data.gy.x = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(0)) / _1k; + _sensor_data.gy.y = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(1)) / _1k; + _sensor_data.gy.z = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(2)) / _1k; lsm6dsox_acceleration_raw_get(&_register_io_function, data_raw_xl.data()); - _sensor_data.xl.x = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(0)); - _sensor_data.xl.y = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(1)); - _sensor_data.xl.z = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(2)); + _sensor_data.xl.x = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(0)) / _1k; + _sensor_data.xl.y = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(1)) / _1k; + _sensor_data.xl.z = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(2)) / _1k; _on_gy_data_ready_callback(_sensor_data); } From db08b03ac1376419a915fd3cc9048812e46b5b4f Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 31 Jan 2023 23:25:13 +0100 Subject: [PATCH 086/143] :recycle: (spike): CoreLSM6DSOX - Make output clearer and alternate on/off mode --- spikes/lk_accel_gyro/main.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/spikes/lk_accel_gyro/main.cpp b/spikes/lk_accel_gyro/main.cpp index a578246893..d0e5b3218c 100644 --- a/spikes/lk_accel_gyro/main.cpp +++ b/spikes/lk_accel_gyro/main.cpp @@ -39,18 +39,28 @@ auto main() -> int imu::lsm6dsox.init(); imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Off); - imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Normal); auto callback = [](const interface::LSM6DSOX::SensorData &imu_data) { const auto &[xlx, xly, xlz] = imu_data.xl; const auto &[gx, gy, gz] = imu_data.gy; - log_info("Xl : x: %7.2f, y: %7.2f, z: %7.2f\n", xlx, xly, xlz); - log_info("Gy : x: %7.2f, y: %7.2f, z: %7.2f\n", gx, gy, gz); + + log_debug("xl.x: %7.2f, xl.y: %7.2f, xl.z: %7.2f, gy.x: %7.2f, gy.y: %7.2f, gy.z: %7.2f", xlx, xly, xlz, gx, gy, + gz); }; imu::lsm6dsox.registerOnGyDataReadyCallback(callback); while (true) { + log_info("Setting normal power mode for 5s"); rtos::ThisThread::sleep_for(1s); + imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Normal); + + rtos::ThisThread::sleep_for(5s); + + imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Off); + rtos::ThisThread::sleep_for(500ms); + log_info("Turning off for 5s"); + + rtos::ThisThread::sleep_for(5s); } } From 3e1be1420ed47771d7c8e71b6c6de29b557e6d24 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 24 Jan 2023 18:52:29 +0100 Subject: [PATCH 087/143] :wrench: (ci): apt-fast install zsh --- .github/actions/setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 5faa4a1f08..7b47ae8add 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -98,7 +98,7 @@ runs: shell: bash run: | sudo apt-fast update - sudo apt-fast install -y --no-install-recommends ninja-build ccache lcov gcovr + sudo apt-fast install -y --no-install-recommends ninja-build ccache lcov gcovr zsh - name: Cache ccache id: global_cache-ccache From 2b436e3fcd71f3a358089e5b7428cf5e29dd8258 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 24 Jan 2023 10:12:16 +0100 Subject: [PATCH 088/143] :art: (makefile): Clean up, rename, rearrange --- Makefile | 63 ++++++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index 96c5246cf5..4ad446837c 100644 --- a/Makefile +++ b/Makefile @@ -49,9 +49,12 @@ BUILD_TARGETS_TO_USE_WITH_BOOTLOADER ?= OFF # MARK: - Build dirs # -PROJECT_BUILD_DIR := $(ROOT_DIR)/_build -TARGET_BUILD_DIR := $(PROJECT_BUILD_DIR)/${TARGET_BOARD} -CMAKE_CONFIG_DIR := $(TARGET_BUILD_DIR)/cmake_config +# Global - os + bootloader + spikes + functional tests +GLOBAL_BUILD_DIR := $(ROOT_DIR)/_build +TARGET_BOARD_BUILD_DIR := $(GLOBAL_BUILD_DIR)/${TARGET_BOARD} +TARGET_BOARD_CMAKE_CONFIG_DIR := $(TARGET_BOARD_BUILD_DIR)/cmake_config + +# Unit tests UNIT_TESTS_BUILD_DIR := $(ROOT_DIR)/_build_unit_tests UNIT_TESTS_COVERAGE_DIR := $(UNIT_TESTS_BUILD_DIR)/_coverage @@ -59,7 +62,7 @@ UNIT_TESTS_COVERAGE_DIR := $(UNIT_TESTS_BUILD_DIR)/_coverage # MARK: - VSCode CMake Tools # -CMAKE_TOOLS_BUILD_DIR := $(ROOT_DIR)/_build_cmake_tools +CMAKE_TOOLS_BUILD_DIR := $(ROOT_DIR)/_build_cmake_tools CMAKE_TOOLS_CONFIG_DIR := $(CMAKE_TOOLS_BUILD_DIR)/cmake_config # @@ -73,7 +76,7 @@ EXCLUDE_FROM_LCOV_COVERAGE = '*Xcode*' '*_build*' '*extern*' # MARK: - .bin path # -LEKA_OS_BIN_PATH := $(TARGET_BUILD_DIR)/app/os/LekaOS.bin +LEKA_OS_BIN_PATH := $(TARGET_BOARD_BUILD_DIR)/app/os/LekaOS.bin BIN_PATH ?= $(LEKA_OS_BIN_PATH) # @@ -85,27 +88,27 @@ BIN_PATH ?= $(LEKA_OS_BIN_PATH) all: @echo "" @echo "🏗️ Building everything! 🌈" - cmake --build $(TARGET_BUILD_DIR) + cmake --build $(TARGET_BOARD_BUILD_DIR) os: @echo "" @echo "🏗️ Building LekaOS 🤖" - cmake --build $(TARGET_BUILD_DIR) -t LekaOS + cmake --build $(TARGET_BOARD_BUILD_DIR) -t LekaOS bootloader: @echo "" @echo "🏗️ Building Bootloader 🤖" - cmake --build $(TARGET_BUILD_DIR) -t bootloader + cmake --build $(TARGET_BOARD_BUILD_DIR) -t bootloader spikes: @echo "" @echo "🏗️ Building spikes 🍱" - cmake --build $(TARGET_BUILD_DIR) -t spikes + cmake --build $(TARGET_BOARD_BUILD_DIR) -t spikes tests_functional: @echo "" @echo "🏗️ Building functional tests ⚗️" - cmake --build $(TARGET_BUILD_DIR) -t tests_functional + cmake --build $(TARGET_BOARD_BUILD_DIR) -t tests_functional firmware: python3 tools/check_version.py ./config/os_version @@ -119,37 +122,30 @@ firmware_no_cleanup: # MARK: - Config targets # +# Global config config: @$(MAKE) config_cmake_target @$(MAKE) config_cmake_build -config_tools: - @$(MAKE) config_tools_target -# @$(MAKE) config_tools_build - -clean: - @$(MAKE) rm_build - -clean_config: - @$(MAKE) rm_build - @$(MAKE) rm_config - @$(MAKE) config - config_cmake_target: mkdir_cmake_config @echo "" @echo "🏃 Running configuration script for target $(TARGET_BOARD) 📝" - python3 $(CMAKE_DIR)/scripts/configure_cmake_for_target.py $(TARGET_BOARD) -p $(CMAKE_CONFIG_DIR) -a $(ROOT_DIR)/config/mbed_app.json + python3 $(CMAKE_DIR)/scripts/configure_cmake_for_target.py $(TARGET_BOARD) -p $(TARGET_BOARD_CMAKE_CONFIG_DIR) -a $(ROOT_DIR)/config/mbed_app.json + +config_cmake_build: mkdir_cmake_config + @echo "" + @echo "🏃 Running cmake configuration script for target $(TARGET_BOARD) 📝" + @cmake -S . -B $(TARGET_BOARD_BUILD_DIR) -GNinja -DCMAKE_CONFIG_DIR="$(TARGET_BOARD_CMAKE_CONFIG_DIR)" -DTARGET_BOARD="$(TARGET_BOARD)" -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_LOG_DEBUG=$(ENABLE_LOG_DEBUG) -DENABLE_SYSTEM_STATS=$(ENABLE_SYSTEM_STATS) -DBUILD_TARGETS_TO_USE_WITH_BOOTLOADER=$(BUILD_TARGETS_TO_USE_WITH_BOOTLOADER) + +# Tools +config_tools: + @$(MAKE) config_tools_target config_tools_target: mkdir_tools_config @echo "" @echo "🏃 Running configuration script for VSCode CMake Tools 📝" python3 $(CMAKE_DIR)/scripts/configure_cmake_for_target.py $(TARGET_BOARD) -p $(CMAKE_TOOLS_CONFIG_DIR) -a $(ROOT_DIR)/config/mbed_app.json -config_cmake_build: mkdir_cmake_config - @echo "" - @echo "🏃 Running cmake configuration script for target $(TARGET_BOARD) 📝" - @cmake -S . -B $(TARGET_BUILD_DIR) -GNinja -DCMAKE_CONFIG_DIR="$(CMAKE_CONFIG_DIR)" -DTARGET_BOARD="$(TARGET_BOARD)" -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_LOG_DEBUG=$(ENABLE_LOG_DEBUG) -DENABLE_SYSTEM_STATS=$(ENABLE_SYSTEM_STATS) -DBUILD_TARGETS_TO_USE_WITH_BOOTLOADER=$(BUILD_TARGETS_TO_USE_WITH_BOOTLOADER) - config_tools_build: mkdir_tools_config @echo "" @echo "🏃 Running cmake configuration script for target $(TARGET_BOARD) 📝" @@ -325,7 +321,7 @@ mcuboot_symlink_files: # mkdir_cmake_config: - @mkdir -p $(CMAKE_CONFIG_DIR) + @mkdir -p $(TARGET_BOARD_CMAKE_CONFIG_DIR) mkdir_tools_config: @mkdir -p $(CMAKE_TOOLS_CONFIG_DIR) @@ -337,20 +333,15 @@ mkdir_build_unit_tests: rm_build: @echo "" @echo "⚠️ Cleaning up $(TARGET_BOARD) build directory 🧹" - rm -rf $(TARGET_BUILD_DIR) + rm -rf $(TARGET_BOARD_BUILD_DIR) rm_build_all: @echo "" @echo "⚠️ Cleaning up all build directories 🧹" - rm -rf $(PROJECT_BUILD_DIR) + rm -rf $(GLOBAL_BUILD_DIR) rm -rf $(CMAKE_TOOLS_BUILD_DIR) rm -rf ./compile_commands.json -rm_config: - @echo "" - @echo "⚠️ Cleaning up $(TARGET_BOARD) cmake_config directory 🧹" - rm -rf $(CMAKE_CONFIG_DIR) - deep_clean: @$(MAKE) rm_build_all @$(MAKE) rm_unit_tests From 1daf3041853ee35f4e34275537e1dbf064e3d171 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 24 Jan 2023 10:23:50 +0100 Subject: [PATCH 089/143] :recycle: (ci): Refactor functional tests workflow --- .github/workflows/ci-functional_tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci-functional_tests.yml b/.github/workflows/ci-functional_tests.yml index 3b8e485440..9ece71b086 100644 --- a/.github/workflows/ci-functional_tests.yml +++ b/.github/workflows/ci-functional_tests.yml @@ -32,9 +32,8 @@ jobs: - name: Config & Build run: | - make clean + make deep_clean make pull_deps - make config_tools make config make From 3ce8658cb0fd2ba3f7b741c0813f44f643c7cc4c Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 24 Jan 2023 10:37:20 +0100 Subject: [PATCH 090/143] :sparkles: (makefile): Add firmware target + new script This allows us to build both normal execs and firmware/os images without the need to deep clean everything --- .github/workflows/ci-create_release.yml | 2 +- Makefile | 47 ++++++++++++++--- tools/generate_firmware.sh | 68 +++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 8 deletions(-) create mode 100755 tools/generate_firmware.sh diff --git a/.github/workflows/ci-create_release.yml b/.github/workflows/ci-create_release.yml index bd59787532..1b7a65c1a8 100644 --- a/.github/workflows/ci-create_release.yml +++ b/.github/workflows/ci-create_release.yml @@ -52,7 +52,7 @@ jobs: - name: Build firmware, os, bootloader run: | - make firmware + make config_firmware firmware - name: Ccache post build run: | diff --git a/Makefile b/Makefile index 4ad446837c..d81aed65c7 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,10 @@ GLOBAL_BUILD_DIR := $(ROOT_DIR)/_build TARGET_BOARD_BUILD_DIR := $(GLOBAL_BUILD_DIR)/${TARGET_BOARD} TARGET_BOARD_CMAKE_CONFIG_DIR := $(TARGET_BOARD_BUILD_DIR)/cmake_config +# Firmware = os + bootloader +FIRMWARE_BUILD_DIR := $(ROOT_DIR)/_build_firmware +FIRMWARE_CONFIG_DIR := $(FIRMWARE_BUILD_DIR)/cmake_config + # Unit tests UNIT_TESTS_BUILD_DIR := $(ROOT_DIR)/_build_unit_tests UNIT_TESTS_COVERAGE_DIR := $(UNIT_TESTS_BUILD_DIR)/_coverage @@ -110,18 +114,27 @@ tests_functional: @echo "🏗️ Building functional tests ⚗️" cmake --build $(TARGET_BOARD_BUILD_DIR) -t tests_functional -firmware: - python3 tools/check_version.py ./config/os_version - ./tools/firmware/build_firmware.sh -r -v $(OS_VERSION) - -firmware_no_cleanup: - python3 tools/check_version.py ./config/os_version - ./tools/firmware/build_firmware.sh -v $(OS_VERSION) +firmware: bootloader + @echo "" + @echo "🏗️ Building Firmware = LekaOS + Bootloader 🤖" + cmake --build $(FIRMWARE_BUILD_DIR) -t LekaOS + @echo "" + @echo "🛂 Check os version" + @echo "" + @python3 tools/check_version.py ./config/os_version + @echo "" + @echo "🧑‍🔬 Generate firmware + os images" + @bash ./tools/generate_firmware.sh # # MARK: - Config targets # +config_all: + @$(MAKE) config + @$(MAKE) config_firmware + @$(MAKE) config_unit_tests_lite COVERAGE=ON + # Global config config: @$(MAKE) config_cmake_target @@ -137,6 +150,22 @@ config_cmake_build: mkdir_cmake_config @echo "🏃 Running cmake configuration script for target $(TARGET_BOARD) 📝" @cmake -S . -B $(TARGET_BOARD_BUILD_DIR) -GNinja -DCMAKE_CONFIG_DIR="$(TARGET_BOARD_CMAKE_CONFIG_DIR)" -DTARGET_BOARD="$(TARGET_BOARD)" -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_LOG_DEBUG=$(ENABLE_LOG_DEBUG) -DENABLE_SYSTEM_STATS=$(ENABLE_SYSTEM_STATS) -DBUILD_TARGETS_TO_USE_WITH_BOOTLOADER=$(BUILD_TARGETS_TO_USE_WITH_BOOTLOADER) +# Firmware config +config_firmware: config + @$(MAKE) config_firmware_target + @$(MAKE) config_firmware_build + +config_firmware_target: mkdir_firmware_config + @echo "" + @echo "🏃 Running configuration script for firmware (os + bootloader) 📝" + python3 $(CMAKE_DIR)/scripts/configure_cmake_for_target.py $(TARGET_BOARD) -p $(FIRMWARE_CONFIG_DIR) -a $(ROOT_DIR)/config/mbed_app.json + +config_firmware_build: mkdir_firmware_config + @echo "" + @echo "🏃 Running cmake configuration script for firmware (os + bootloader) 📝" + @cmake -S . -B $(FIRMWARE_BUILD_DIR) -GNinja -DCMAKE_CONFIG_DIR="$(FIRMWARE_CONFIG_DIR)" -DTARGET_BOARD="$(TARGET_BOARD)" -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_LOG_DEBUG=$(ENABLE_LOG_DEBUG) -DENABLE_SYSTEM_STATS=$(ENABLE_SYSTEM_STATS) -DBUILD_TARGETS_TO_USE_WITH_BOOTLOADER=ON + + # Tools config_tools: @$(MAKE) config_tools_target @@ -323,6 +352,9 @@ mcuboot_symlink_files: mkdir_cmake_config: @mkdir -p $(TARGET_BOARD_CMAKE_CONFIG_DIR) +mkdir_firmware_config: + @mkdir -p $(FIRMWARE_CONFIG_DIR) + mkdir_tools_config: @mkdir -p $(CMAKE_TOOLS_CONFIG_DIR) @@ -339,6 +371,7 @@ rm_build_all: @echo "" @echo "⚠️ Cleaning up all build directories 🧹" rm -rf $(GLOBAL_BUILD_DIR) + rm -rf $(FIRMWARE_BUILD_DIR) rm -rf $(CMAKE_TOOLS_BUILD_DIR) rm -rf ./compile_commands.json diff --git a/tools/generate_firmware.sh b/tools/generate_firmware.sh new file mode 100755 index 0000000000..2144e469b4 --- /dev/null +++ b/tools/generate_firmware.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +# +# MARK: - Requirements +# + +ROOT_DIR=$(pwd) + +if [[ $ROOT_DIR != *LekaOS ]]; then + echo "Script *must* be run from the root of the project with:" + echo " ./tools/generate_firmware.sh" + exit 1 +fi + +# +# MARK: - Variables +# + +# paths +CONFIG_DIR="$ROOT_DIR/config" +FIRMWARE_BUILD_DIR="$ROOT_DIR/_build_firmware/app" +RELEASE_DIR="$FIRMWARE_BUILD_DIR/release" + +# version + build number +OS_VERSION="$(cat $CONFIG_DIR/os_version)+$(date +%s)" + +# bootloader +BOOTLOADER_HEX_PATH="_build/LEKA_V1_2_DEV/app/bootloader/bootloader.hex" + +# os +OS_HEX_PATH="$FIRMWARE_BUILD_DIR/os/LekaOS.hex" +OS_SIGNED_HEX_PATH="$RELEASE_DIR/LekaOS-$OS_VERSION.hex" +OS_SIGNED_BIN_PATH="$RELEASE_DIR/LekaOS-$OS_VERSION.bin" + +# firmware +FIRMWARE_HEX="$RELEASE_DIR/Firmware-$OS_VERSION.hex" +FIRMWARE_BIN="$RELEASE_DIR/Firmware-$OS_VERSION.bin" + +# +# MARK: - Generate MCUboot Application (= os) +# + +# ? Note: MCUboot uses the term "application" where Leka uses the term "os" +# ? Both refer to the same thing, application == os + +echo " -- Create directory $RELEASE_DIR" +rm -rf $RELEASE_DIR +mkdir -p $RELEASE_DIR + +echo " -- Create signed os hex file" +imgtool sign -k signing-keys.pem --align 4 -v $OS_VERSION --header-size 4096 --pad-header -S 0x180000 $OS_HEX_PATH $OS_SIGNED_HEX_PATH + +echo " -- Convert signed os hex file to binary format" +arm-none-eabi-objcopy -I ihex -O binary $OS_SIGNED_HEX_PATH $OS_SIGNED_BIN_PATH + +# +# MARK: - Generate firmware = bootloader + application (os) +# + +echo " -- Merge bootloader + os to create firmware hex file" +hexmerge.py -o $FIRMWARE_HEX --no-start-addr $BOOTLOADER_HEX_PATH $OS_SIGNED_HEX_PATH + +echo " -- Convert firmware hex file to binary format" +arm-none-eabi-objcopy -I ihex -O binary $FIRMWARE_HEX $FIRMWARE_BIN From 52ade6e5873a2ec1df91a4d49f94702740e0a657 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Mon, 30 Jan 2023 13:41:28 +0100 Subject: [PATCH 091/143] :green_heart: (release): Fix release workflow to handle changes --- .github/workflows/ci-create_release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-create_release.yml b/.github/workflows/ci-create_release.yml index 1b7a65c1a8..cfff96bdb5 100644 --- a/.github/workflows/ci-create_release.yml +++ b/.github/workflows/ci-create_release.yml @@ -70,8 +70,8 @@ jobs: name: bootloader_os_firmware retention-days: 7 path: | - _release/Firmware-*.hex - _release/LekaOS-*.bin + _build_firmware/app/release/Firmware-*.hex + _build_firmware/app/release/LekaOS-*.bin # # Mark: - Check versions from files and os_version are identical @@ -81,5 +81,5 @@ jobs: id: check_os_firmware_version uses: ./.github/actions/check_version with: - firmware_hex_path: _release/Firmware-*.hex - os_bin_path: _release/LekaOS-*.bin + firmware_hex_path: _build_firmware/app/release/Firmware-*.hex + os_bin_path: _build_firmware/app/release/LekaOS-*.bin From b61eca83053917821e036bf4f4f9c0c785f2ed44 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Mon, 30 Jan 2023 13:43:36 +0100 Subject: [PATCH 092/143] :fire: (tools): Remove old firmware related scripts --- tools/firmware/build_application_os.sh | 45 ------------------------ tools/firmware/build_bootloader.sh | 18 ---------- tools/firmware/build_firmware.sh | 48 -------------------------- 3 files changed, 111 deletions(-) delete mode 100755 tools/firmware/build_application_os.sh delete mode 100755 tools/firmware/build_bootloader.sh delete mode 100755 tools/firmware/build_firmware.sh diff --git a/tools/firmware/build_application_os.sh b/tools/firmware/build_application_os.sh deleted file mode 100755 index da43f0ae41..0000000000 --- a/tools/firmware/build_application_os.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -# ? application_os: "application" is the term used by MCUBoot, "os" is the term used by Leka - -mkdir -p _tmp -mkdir -p _release - -# Variables -RECOMPILE_APPLICATION_OS="$1" - -APPLICATION_OS_HEX_SOURCE="_build/LEKA_V1_2_DEV/app/os/LekaOS.hex" -APPLICATION_OS_HEX_DESTINATION="$2" - -APPLICATION_OS_VERSION="$3" -APPLICATION_OS_HEX="_tmp/LekaOS.hex" -APPLICATION_OS_SIGNED_HEX=$APPLICATION_OS_HEX_DESTINATION -APPLICATION_OS_SIGNED_BIN="_release/LekaOS-$APPLICATION_OS_VERSION.bin" - -if [ -z "$APPLICATION_OS_VERSION" ]; then - echo "APPLICATION_OS_VERSION is unset" - exit 1 -fi - -if [ -z "$APPLICATION_OS_HEX_DESTINATION" ]; then - echo "APPLICATION_OS_HEX_DESTINATION is unset" - exit 1 -fi - -# Compile application_os -if [ "$RECOMPILE_APPLICATION_OS" = "true" ]; -then - make deep_clean - make config BUILD_TARGETS_TO_USE_WITH_BOOTLOADER=ON -fi; - -make - -# Get application_os binary -cp $APPLICATION_OS_HEX_SOURCE $APPLICATION_OS_HEX - -# Sign application_os with private key -imgtool sign -k signing-keys.pem --align 4 -v $APPLICATION_OS_VERSION --header-size 4096 --pad-header -S 0x180000 $APPLICATION_OS_HEX $APPLICATION_OS_SIGNED_HEX - -# Convert in binary -arm-none-eabi-objcopy -I ihex -O binary $APPLICATION_OS_SIGNED_HEX $APPLICATION_OS_SIGNED_BIN diff --git a/tools/firmware/build_bootloader.sh b/tools/firmware/build_bootloader.sh deleted file mode 100755 index 00def318bd..0000000000 --- a/tools/firmware/build_bootloader.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Variables -BOOTLOADER_HEX_SOURCE="_build/LEKA_V1_2_DEV/app/bootloader/bootloader.hex" -BOOTLOADER_HEX_DESTINATION="$1" - -if [ -z "$BOOTLOADER_HEX_DESTINATION" ]; then - echo "APPLICATION_HEX_SOURCE is unset" - exit 1 -fi - -# Compile bootloader -make deep_clean -make config -make bootloader - -# Get bootloader binary -cp $BOOTLOADER_HEX_SOURCE $BOOTLOADER_HEX_DESTINATION diff --git a/tools/firmware/build_firmware.sh b/tools/firmware/build_firmware.sh deleted file mode 100755 index c47103f611..0000000000 --- a/tools/firmware/build_firmware.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -# ? application_os: "application" is the term used by MCUBoot, "os" is the term used by Leka - -mkdir -p _tmp -mkdir -p _release - -# Variables -RECOMPILE_BOOTLOADER="false" -while getopts rv: flag -do - case "${flag}" in - r) RECOMPILE_BOOTLOADER="true";; - v) APPLICATION_OS_VERSION=$OPTARG;; - esac -done - -if [ -z "$APPLICATION_OS_VERSION" ]; then - echo "APPLICATION_OS_VERSION is unset" - exit 1 -fi - -BUILD_NUMBER=$(date +%s) -APPLICATION_OS_VERSION="$APPLICATION_OS_VERSION+$BUILD_NUMBER" - -BOOTLOADER_HEX="_tmp/bootloader.hex" -APPLICATION_OS_HEX="_tmp/LekaOS-$APPLICATION_OS_VERSION.hex" - -FIRMWARE_HEX="_release/Firmware-$APPLICATION_OS_VERSION.hex" -FIRMWARE_BIN="_release/Firmware-$APPLICATION_OS_VERSION.bin" - -# Get bootloader -if [ "$RECOMPILE_BOOTLOADER" = "true" ]; -then - echo "Build bootloader" - ./tools/firmware/build_bootloader.sh $BOOTLOADER_HEX -fi; - -# Get application_os -echo "Build application_os" -./tools/firmware/build_application_os.sh $RECOMPILE_BOOTLOADER $APPLICATION_OS_HEX $APPLICATION_OS_VERSION - -# Merge bootloader and application_os -echo "Merge bootloader & application_os" -hexmerge.py -o $FIRMWARE_HEX --no-start-addr $BOOTLOADER_HEX $APPLICATION_OS_HEX - -# Convert in binary -arm-none-eabi-objcopy -I ihex -O binary $FIRMWARE_HEX $FIRMWARE_BIN From 8b6fe305e3939a2eebfdba551711fdc87c8c1a52 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Thu, 2 Feb 2023 16:01:12 +0100 Subject: [PATCH 093/143] :construction: (tests): functional - IMUKit - Comment out failing tests --- .../tests/imu_kit/suite_imu_kit.cpp | 118 +++++++++--------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/tests/functional/tests/imu_kit/suite_imu_kit.cpp b/tests/functional/tests/imu_kit/suite_imu_kit.cpp index 482671647a..56363c3744 100644 --- a/tests/functional/tests/imu_kit/suite_imu_kit.cpp +++ b/tests/functional/tests/imu_kit/suite_imu_kit.cpp @@ -39,62 +39,64 @@ suite suite_imu_kit = [] { imukit.start(); }; - scenario("imu - get angles") = [&] { - when("robot on horizontal position") = [&] { - then("I expect pitch to be close to 0") = [&] { - auto pitch = imukit.getAngles().at(0); - expect(ge(pitch, default_min_bound_pitch)) << " (" << pitch << " < " << default_min_bound_pitch << ")"; - expect(le(pitch, default_max_bound_pitch)) << " (" << pitch << " > " << default_max_bound_pitch << ")"; - }; - - then("I expect roll to be close to 0") = [&] { - auto roll = imukit.getAngles().at(1); - expect(ge(roll, default_min_bound_roll)) << " (" << roll << " < " << default_min_bound_roll << ")"; - expect(le(roll, default_max_bound_roll)) << " (" << roll << " > " << default_max_bound_roll << ")"; - }; - - then("I expect yaw to be close to 180") = [&] { - auto yaw = imukit.getAngles().at(2); - expect(ge(yaw, default_min_bound_yaw)) << " (" << yaw << " < " << default_min_bound_yaw << ")"; - expect(le(yaw, default_max_bound_yaw)) << " (" << yaw << " > " << default_max_bound_yaw << ")"; - }; - }; - }; - - scenario("imu - measurement stability") = [&] { - given("a new origin is set") = [&] { - imukit.setOrigin(); - - then("I expect yaw to be reset to 180 degrees") = [&] { - auto [pitch, roll, yaw] = imukit.getAngles(); - expect(yaw > default_min_bound_yaw) - << "Yaw (" << yaw << ") lesser than minimal bound (" << default_min_bound_yaw << ")"; - expect(yaw < default_max_bound_yaw) - << "Yaw (" << yaw << ") greater than maximal bound (" << default_max_bound_yaw << ")"; - }; - when("I wait for 10 seconds") = [&] { - auto [first_pitch, first_roll, first_yaw] = imukit.getAngles(); - - rtos::ThisThread::sleep_for(10s); - - auto [current_pitch, current_roll, current_yaw] = imukit.getAngles(); - - auto pitch_drift = first_pitch - current_pitch; - auto roll_drift = first_roll - current_roll; - auto yaw_drift = first_yaw - current_yaw; - - then("I expect pitch NOT to drift") = [&] { - expect(le(pitch_drift, maximal_pitch_noise_amplitude)) - << "(" << pitch_drift << " > " << maximal_pitch_noise_amplitude << ")"; - }; - then("I expect roll NOT to drift") = [&] { - expect(le(roll_drift, maximal_roll_noise_amplitude)) - << "(" << roll_drift << " > " << maximal_roll_noise_amplitude << ")"; - }; - then("I expect yaw to drift slightly") = [&] { - expect(le(yaw_drift, maximal_yaw_drift)) << "(" << yaw_drift << " > " << maximal_yaw_drift << ")"; - }; - }; - }; - }; + // TODO (@ladislas, @hugo) Update this following tests with new fusion algorithm + + // scenario("imu - get angles") = [&] { + // when("robot on horizontal position") = [&] { + // then("I expect pitch to be close to 0") = [&] { + // auto pitch = imukit.getAngles().at(0); + // expect(ge(pitch, default_min_bound_pitch)) << " (" << pitch << " < " << default_min_bound_pitch << ")"; + // expect(le(pitch, default_max_bound_pitch)) << " (" << pitch << " > " << default_max_bound_pitch << ")"; + // }; + + // then("I expect roll to be close to 0") = [&] { + // auto roll = imukit.getAngles().at(1); + // expect(ge(roll, default_min_bound_roll)) << " (" << roll << " < " << default_min_bound_roll << ")"; + // expect(le(roll, default_max_bound_roll)) << " (" << roll << " > " << default_max_bound_roll << ")"; + // }; + + // then("I expect yaw to be close to 180") = [&] { + // auto yaw = imukit.getAngles().at(2); + // expect(ge(yaw, default_min_bound_yaw)) << " (" << yaw << " < " << default_min_bound_yaw << ")"; + // expect(le(yaw, default_max_bound_yaw)) << " (" << yaw << " > " << default_max_bound_yaw << ")"; + // }; + // }; + // }; + + // scenario("imu - measurement stability") = [&] { + // given("a new origin is set") = [&] { + // imukit.setOrigin(); + + // then("I expect yaw to be reset to 180 degrees") = [&] { + // auto [pitch, roll, yaw] = imukit.getAngles(); + // expect(yaw > default_min_bound_yaw) + // << "Yaw (" << yaw << ") lesser than minimal bound (" << default_min_bound_yaw << ")"; + // expect(yaw < default_max_bound_yaw) + // << "Yaw (" << yaw << ") greater than maximal bound (" << default_max_bound_yaw << ")"; + // }; + // when("I wait for 10 seconds") = [&] { + // auto [first_pitch, first_roll, first_yaw] = imukit.getAngles(); + + // rtos::ThisThread::sleep_for(10s); + + // auto [current_pitch, current_roll, current_yaw] = imukit.getAngles(); + + // auto pitch_drift = first_pitch - current_pitch; + // auto roll_drift = first_roll - current_roll; + // auto yaw_drift = first_yaw - current_yaw; + + // then("I expect pitch NOT to drift") = [&] { + // expect(le(pitch_drift, maximal_pitch_noise_amplitude)) + // << "(" << pitch_drift << " > " << maximal_pitch_noise_amplitude << ")"; + // }; + // then("I expect roll NOT to drift") = [&] { + // expect(le(roll_drift, maximal_roll_noise_amplitude)) + // << "(" << roll_drift << " > " << maximal_roll_noise_amplitude << ")"; + // }; + // then("I expect yaw to drift slightly") = [&] { + // expect(le(yaw_drift, maximal_yaw_drift)) << "(" << yaw_drift << " > " << maximal_yaw_drift << ")"; + // }; + // }; + // }; + // }; }; From 38b52f1a68a23b8149ddbcd763a0844c569a1814 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 1 Feb 2023 12:12:08 +0100 Subject: [PATCH 094/143] :white_check_mark: (filemanager): Refactor FileManagerKit_test - Removed not used include - Rename methods of FileSystemTest to be more explicit - Remove complexity of multiple path and use only two --- .../tests/FileManagerKit_test.cpp | 81 +++++++++---------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/libs/FileManagerKit/tests/FileManagerKit_test.cpp b/libs/FileManagerKit/tests/FileManagerKit_test.cpp index 5cd7a56fd4..98ed410ec0 100644 --- a/libs/FileManagerKit/tests/FileManagerKit_test.cpp +++ b/libs/FileManagerKit/tests/FileManagerKit_test.cpp @@ -2,15 +2,9 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include -#include +#include #include "FileManagerKit.h" -#include "LogKit.h" -#include "filesystem" #include "gtest/gtest.h" using namespace leka; @@ -18,66 +12,63 @@ using namespace leka; class FileSystemTest : public ::testing::Test { protected: - void SetUp() override { spy_remove_all_directories(); } - // void TearDown() override {} + // void SetUp() override {} + void TearDown() override { spy_remove_all_directories(); } - void spy_mkdir(const std::filesystem::path &path) { std::filesystem::create_directories(path); } - - void spy_rmdir(const std::filesystem::path &path) { std::filesystem::remove_all(path); } - - void spy_create_all_directories() - { - std::filesystem::create_directories(path_A); - std::filesystem::create_directories(path_B); - std::filesystem::create_directories(path_A1); - std::filesystem::create_directories(path_B1); - } + void spy_create_directory(const std::filesystem::path &path) { std::filesystem::create_directories(path); } + void spy_remove_directory(const std::filesystem::path &path) { std::filesystem::remove_all(path); } void spy_remove_all_directories() { - std::filesystem::remove_all(path_A); - std::filesystem::remove_all(path_B); - std::filesystem::remove_all(path_A1); - std::filesystem::remove_all(path_B1); + spy_remove_directory(path_sub_directory); + spy_remove_directory(path_directory); } - const std::filesystem::path path_A = std::filesystem::temp_directory_path() / "A"; - const std::filesystem::path path_B = std::filesystem::temp_directory_path() / "B"; - const std::filesystem::path path_A1 = std::filesystem::temp_directory_path() / "A/A1"; - const std::filesystem::path path_B1 = std::filesystem::temp_directory_path() / "B/B1"; + const std::filesystem::path path_directory = std::filesystem::temp_directory_path() / "ABC"; + const std::filesystem::path path_sub_directory = std::filesystem::temp_directory_path() / "ABC/DEF"; }; TEST_F(FileSystemTest, createDirectory) { - auto created = FileManagerKit::create_directory(path_A); - ASSERT_TRUE(created); + spy_remove_directory(path_directory); + + auto is_created = FileManagerKit::create_directory(path_directory); + + EXPECT_TRUE(is_created); } TEST_F(FileSystemTest, createAlreadyExistingDirectory) { - spy_mkdir(path_A); - auto created = FileManagerKit::create_directory(path_A); - ASSERT_FALSE(created); + spy_create_directory(path_directory); + + auto is_created = FileManagerKit::create_directory(path_directory); + + EXPECT_FALSE(is_created); } -TEST_F(FileSystemTest, createSubDirectory) +TEST_F(FileSystemTest, createSubDirectoryInExistingDirectory) { - spy_mkdir(path_A); - auto created = FileManagerKit::create_directory(path_A1); - ASSERT_TRUE(created); + spy_create_directory(path_directory); + + auto is_created = FileManagerKit::create_directory(path_sub_directory); + + EXPECT_TRUE(is_created); } -TEST_F(FileSystemTest, removeDirectory) +TEST_F(FileSystemTest, removeExistingDirectory) { - spy_rmdir(path_B); - spy_mkdir(path_B); - auto removed = FileManagerKit::remove(path_B); - ASSERT_TRUE(removed); + spy_create_directory(path_directory); + + auto is_removed = FileManagerKit::remove(path_directory); + + EXPECT_TRUE(is_removed); } TEST_F(FileSystemTest, removeNotExistingDirectory) { - spy_rmdir(path_B); - auto removed = FileManagerKit::remove(path_B); - ASSERT_FALSE(removed); + spy_remove_directory(path_directory); + + auto is_removed = FileManagerKit::remove(path_directory); + + EXPECT_FALSE(is_removed); } From c1c19c5ed5e27a94db921e60ef5eb1d8bf7ae06b Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 1 Feb 2023 11:11:12 +0100 Subject: [PATCH 095/143] :wrench: (clang-tidy): Ignore specific floating point values --- .clang-tidy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.clang-tidy b/.clang-tidy index 081b05e780..1df1409d8d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -96,6 +96,8 @@ CheckOptions: value: true - key: readability-magic-numbers.IgnoredIntegerValues value: "1;2;3;4;10;100;1000;60;80;3600;9600;57600;115200;128;255;1000000" + - key: readability-magic-numbers.IgnoredFloatingPointValues + value: "1;2;3;4;10;100;1000;" - key: readability-identifier-length.IgnoredVariableNames value: "^(fs|c|ms|fh|t|bd|s|hz|id|[abcijkxyz])$" - key: readability-identifier-length.IgnoredParameterNames From a6d15df84fedc63e8cf9f2d8234750fabb092802 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Thu, 2 Feb 2023 12:09:44 +0100 Subject: [PATCH 096/143] :sparkles: (spikes): Add spike lk_sensors_imu_lsm6dsox_fusion_calibration --- spikes/CMakeLists.txt | 2 + .../CMakeLists.txt | 25 + .../fusion/CMakeLists.txt | 11 + .../fusion/Fusion.h | 31 ++ .../fusion/FusionAhrs.c | 354 ++++++++++++++ .../fusion/FusionAhrs.h | 104 ++++ .../fusion/FusionAxes.h | 187 +++++++ .../fusion/FusionCalibration.h | 44 ++ .../fusion/FusionCompass.c | 36 ++ .../fusion/FusionCompass.h | 24 + .../fusion/FusionMath.h | 459 ++++++++++++++++++ .../fusion/FusionOffset.c | 77 +++ .../fusion/FusionOffset.h | 40 ++ .../main.cpp | 148 ++++++ 14 files changed, 1542 insertions(+) create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/CMakeLists.txt create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/CMakeLists.txt create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/Fusion.h create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.c create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.h create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAxes.h create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCalibration.h create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.c create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.h create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionMath.h create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.c create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.h create mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/main.cpp diff --git a/spikes/CMakeLists.txt b/spikes/CMakeLists.txt index 38a5f895ba..5a0723d8fa 100644 --- a/spikes/CMakeLists.txt +++ b/spikes/CMakeLists.txt @@ -28,6 +28,7 @@ add_subdirectory(${SPIKES_DIR}/lk_qdac) add_subdirectory(${SPIKES_DIR}/lk_reinforcer) add_subdirectory(${SPIKES_DIR}/lk_rfid) add_subdirectory(${SPIKES_DIR}/lk_sensors_battery) +add_subdirectory(${SPIKES_DIR}/lk_sensors_imu_lsm6dsox_fusion_calibration) add_subdirectory(${SPIKES_DIR}/lk_sensors_light) add_subdirectory(${SPIKES_DIR}/lk_sensors_microphone) add_subdirectory(${SPIKES_DIR}/lk_sensors_temperature_humidity) @@ -69,6 +70,7 @@ add_dependencies(spikes_leka spike_lk_reinforcer spike_lk_rfid spike_lk_sensors_battery + spike_lk_sensors_imu_lsm6dsox_fusion_calibration spike_lk_sensors_light spike_lk_sensors_microphone spike_lk_sensors_temperature_humidity diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/CMakeLists.txt b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/CMakeLists.txt new file mode 100644 index 0000000000..ae5593814a --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/CMakeLists.txt @@ -0,0 +1,25 @@ +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +add_mbed_executable(spike_lk_sensors_imu_lsm6dsox_fusion_calibration) + +add_subdirectory(fusion) + +target_include_directories(spike_lk_sensors_imu_lsm6dsox_fusion_calibration + PRIVATE + . +) + +target_sources(spike_lk_sensors_imu_lsm6dsox_fusion_calibration + PRIVATE + main.cpp +) + +target_link_libraries(spike_lk_sensors_imu_lsm6dsox_fusion_calibration + CoreIMU + CoreI2C + Fusion +) + +target_link_custom_leka_targets(spike_lk_sensors_imu_lsm6dsox_fusion_calibration) diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/CMakeLists.txt b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/CMakeLists.txt new file mode 100644 index 0000000000..bc410776c5 --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/CMakeLists.txt @@ -0,0 +1,11 @@ +# ? Note the following code has been heavily inspired from: +# ? https://github.com/xioTechnologies/Fusion/blob/main/Examples/Advanced +# ? For more information, see https://github.com/xioTechnologies/Fusion + +file(GLOB_RECURSE files "*.c") + +add_library(Fusion ${files}) + +if(UNIX AND NOT APPLE) + target_link_libraries(Fusion m) # link math library for Linux +endif() diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/Fusion.h b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/Fusion.h new file mode 100644 index 0000000000..bcd248421b --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/Fusion.h @@ -0,0 +1,31 @@ +/** + * @file Fusion.h + * @author Seb Madgwick + * @brief Main header file for the Fusion library. This is the only file that + * needs to be included when using the library. + */ + +#ifndef FUSION_H +#define FUSION_H + +//------------------------------------------------------------------------------ +// Includes + +#ifdef __cplusplus +extern "C" { +#endif + +#include "FusionAhrs.h" +#include "FusionAxes.h" +#include "FusionCalibration.h" +#include "FusionCompass.h" +#include "FusionMath.h" +#include "FusionOffset.h" + +#ifdef __cplusplus +} +#endif + +#endif +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.c b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.c new file mode 100644 index 0000000000..579138e05c --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.c @@ -0,0 +1,354 @@ +/** + * @file FusionAhrs.c + * @author Seb Madgwick + * @brief AHRS algorithm to combine gyroscope, accelerometer, and magnetometer + * measurements into a single measurement of orientation relative to the Earth. + */ + +//------------------------------------------------------------------------------ +// Includes + +#include // FLT_MAX +#include "FusionAhrs.h" +#include "FusionCompass.h" +#include // atan2f, cosf, powf, sinf + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Initial gain used during the initialisation. + */ +#define INITIAL_GAIN (10.0f) + +/** + * @brief Initialisation period in seconds. + */ +#define INITIALISATION_PERIOD (3.0f) + +//------------------------------------------------------------------------------ +// Functions + +/** + * @brief Initialises the AHRS algorithm structure. + * @param ahrs AHRS algorithm structure. + */ +void FusionAhrsInitialise(FusionAhrs *const ahrs) { + const FusionAhrsSettings settings = { + .gain = 0.5f, + .accelerationRejection = 90.0f, + .magneticRejection = 90.0f, + .rejectionTimeout = 0, + }; + FusionAhrsSetSettings(ahrs, &settings); + FusionAhrsReset(ahrs); +} + +/** + * @brief Resets the AHRS algorithm. This is equivalent to reinitialising the + * algorithm while maintaining the current settings. + * @param ahrs AHRS algorithm structure. + */ +void FusionAhrsReset(FusionAhrs *const ahrs) { + ahrs->quaternion = FUSION_IDENTITY_QUATERNION; + ahrs->accelerometer = FUSION_VECTOR_ZERO; + ahrs->initialising = true; + ahrs->rampedGain = INITIAL_GAIN; + ahrs->halfAccelerometerFeedback = FUSION_VECTOR_ZERO; + ahrs->halfMagnetometerFeedback = FUSION_VECTOR_ZERO; + ahrs->accelerometerIgnored = false; + ahrs->accelerationRejectionTimer = 0; + ahrs->accelerationRejectionTimeout = false; + ahrs->magnetometerIgnored = false; + ahrs->magneticRejectionTimer = 0; + ahrs->magneticRejectionTimeout = false; +} + +/** + * @brief Sets the AHRS algorithm settings. + * @param ahrs AHRS algorithm structure. + * @param settings Settings. + */ +void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings) { + ahrs->settings.gain = settings->gain; + if ((settings->accelerationRejection == 0.0f) || (settings->rejectionTimeout == 0)) { + ahrs->settings.accelerationRejection = FLT_MAX; + } else { + ahrs->settings.accelerationRejection = powf(0.5f * sinf(FusionDegreesToRadians(settings->accelerationRejection)), 2); + } + if ((settings->magneticRejection == 0.0f) || (settings->rejectionTimeout == 0)) { + ahrs->settings.magneticRejection = FLT_MAX; + } else { + ahrs->settings.magneticRejection = powf(0.5f * sinf(FusionDegreesToRadians(settings->magneticRejection)), 2); + } + ahrs->settings.rejectionTimeout = settings->rejectionTimeout; + if (ahrs->initialising == false) { + ahrs->rampedGain = ahrs->settings.gain; + } + ahrs->rampedGainStep = (INITIAL_GAIN - ahrs->settings.gain) / INITIALISATION_PERIOD; +} + +/** + * @brief Updates the AHRS algorithm using the gyroscope, accelerometer, and + * magnetometer measurements. + * @param ahrs AHRS algorithm structure. + * @param gyroscope Gyroscope measurement in degrees per second. + * @param accelerometer Accelerometer measurement in g. + * @param magnetometer Magnetometer measurement in arbitrary units. + * @param deltaTime Delta time in seconds. + */ +void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const FusionVector magnetometer, const float deltaTime) { +#define Q ahrs->quaternion.element + + // Store accelerometer + ahrs->accelerometer = accelerometer; + + // Ramp down gain during initialisation + if (ahrs->initialising == true) { + ahrs->rampedGain -= ahrs->rampedGainStep * deltaTime; + if (ahrs->rampedGain < ahrs->settings.gain) { + ahrs->rampedGain = ahrs->settings.gain; + ahrs->initialising = false; + ahrs->accelerationRejectionTimeout = false; + } + } + + // Calculate direction of gravity indicated by algorithm + const FusionVector halfGravity = { + .axis.x = Q.x * Q.z - Q.w * Q.y, + .axis.y = Q.y * Q.z + Q.w * Q.x, + .axis.z = Q.w * Q.w - 0.5f + Q.z * Q.z, + }; // third column of transposed rotation matrix scaled by 0.5 + + // Calculate accelerometer feedback + FusionVector halfAccelerometerFeedback = FUSION_VECTOR_ZERO; + ahrs->accelerometerIgnored = true; + if (FusionVectorIsZero(accelerometer) == false) { + + // Enter acceleration recovery state if acceleration rejection times out + if (ahrs->accelerationRejectionTimer > ahrs->settings.rejectionTimeout) { + const FusionQuaternion quaternion = ahrs->quaternion; + FusionAhrsReset(ahrs); + ahrs->quaternion = quaternion; + ahrs->accelerationRejectionTimer = 0; + ahrs->accelerationRejectionTimeout = true; + } + + // Calculate accelerometer feedback scaled by 0.5 + ahrs->halfAccelerometerFeedback = FusionVectorCrossProduct(FusionVectorNormalise(accelerometer), halfGravity); + + // Ignore accelerometer if acceleration distortion detected + if ((ahrs->initialising == true) || (FusionVectorMagnitudeSquared(ahrs->halfAccelerometerFeedback) <= ahrs->settings.accelerationRejection)) { + halfAccelerometerFeedback = ahrs->halfAccelerometerFeedback; + ahrs->accelerometerIgnored = false; + ahrs->accelerationRejectionTimer -= ahrs->accelerationRejectionTimer >= 10 ? 10 : 0; + } else { + ahrs->accelerationRejectionTimer++; + } + } + + // Calculate magnetometer feedback + FusionVector halfMagnetometerFeedback = FUSION_VECTOR_ZERO; + ahrs->magnetometerIgnored = true; + if (FusionVectorIsZero(magnetometer) == false) { + + // Set to compass heading if magnetic rejection times out + ahrs->magneticRejectionTimeout = false; + if (ahrs->magneticRejectionTimer > ahrs->settings.rejectionTimeout) { + FusionAhrsSetHeading(ahrs, FusionCompassCalculateHeading(halfGravity, magnetometer)); + ahrs->magneticRejectionTimer = 0; + ahrs->magneticRejectionTimeout = true; + } + + // Compute direction of west indicated by algorithm + const FusionVector halfWest = { + .axis.x = Q.x * Q.y + Q.w * Q.z, + .axis.y = Q.w * Q.w - 0.5f + Q.y * Q.y, + .axis.z = Q.y * Q.z - Q.w * Q.x + }; // second column of transposed rotation matrix scaled by 0.5 + + // Calculate magnetometer feedback scaled by 0.5 + ahrs->halfMagnetometerFeedback = FusionVectorCrossProduct(FusionVectorNormalise(FusionVectorCrossProduct(halfGravity, magnetometer)), halfWest); + + // Ignore magnetometer if magnetic distortion detected + if ((ahrs->initialising == true) || (FusionVectorMagnitudeSquared(ahrs->halfMagnetometerFeedback) <= ahrs->settings.magneticRejection)) { + halfMagnetometerFeedback = ahrs->halfMagnetometerFeedback; + ahrs->magnetometerIgnored = false; + ahrs->magneticRejectionTimer -= ahrs->magneticRejectionTimer >= 10 ? 10 : 0; + } else { + ahrs->magneticRejectionTimer++; + } + } + + // Convert gyroscope to radians per second scaled by 0.5 + const FusionVector halfGyroscope = FusionVectorMultiplyScalar(gyroscope, FusionDegreesToRadians(0.5f)); + + // Apply feedback to gyroscope + const FusionVector adjustedHalfGyroscope = FusionVectorAdd(halfGyroscope, FusionVectorMultiplyScalar(FusionVectorAdd(halfAccelerometerFeedback, halfMagnetometerFeedback), ahrs->rampedGain)); + + // Integrate rate of change of quaternion + ahrs->quaternion = FusionQuaternionAdd(ahrs->quaternion, FusionQuaternionMultiplyVector(ahrs->quaternion, FusionVectorMultiplyScalar(adjustedHalfGyroscope, deltaTime))); + + // Normalise quaternion + ahrs->quaternion = FusionQuaternionNormalise(ahrs->quaternion); +#undef Q +} + +/** + * @brief Updates the AHRS algorithm using the gyroscope and accelerometer + * measurements only. + * @param ahrs AHRS algorithm structure. + * @param gyroscope Gyroscope measurement in degrees per second. + * @param accelerometer Accelerometer measurement in g. + * @param deltaTime Delta time in seconds. + */ +void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float deltaTime) { + + // Update AHRS algorithm + FusionAhrsUpdate(ahrs, gyroscope, accelerometer, FUSION_VECTOR_ZERO, deltaTime); + + // Zero heading during initialisation + if ((ahrs->initialising == true) && (ahrs->accelerationRejectionTimeout == false)) { + FusionAhrsSetHeading(ahrs, 0.0f); + } +} + +/** + * @brief Updates the AHRS algorithm using the gyroscope, accelerometer, and + * heading measurements. + * @param ahrs AHRS algorithm structure. + * @param gyroscope Gyroscope measurement in degrees per second. + * @param accelerometer Accelerometer measurement in g. + * @param heading Heading measurement in degrees. + * @param deltaTime Delta time in seconds. + */ +void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float heading, const float deltaTime) { +#define Q ahrs->quaternion.element + + // Calculate roll + const float roll = atan2f(Q.w * Q.x + Q.y * Q.z, 0.5f - Q.y * Q.y - Q.x * Q.x); + + // Calculate magnetometer + const float headingRadians = FusionDegreesToRadians(heading); + const float sinHeadingRadians = sinf(headingRadians); + const FusionVector magnetometer = { + .axis.x = cosf(headingRadians), + .axis.y = -1.0f * cosf(roll) * sinHeadingRadians, + .axis.z = sinHeadingRadians * sinf(roll), + }; + + // Update AHRS algorithm + FusionAhrsUpdate(ahrs, gyroscope, accelerometer, magnetometer, deltaTime); +#undef Q +} + +/** + * @brief Returns the quaternion describing the sensor relative to the Earth. + * @param ahrs AHRS algorithm structure. + * @return Quaternion describing the sensor relative to the Earth. + */ +FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs) { + return ahrs->quaternion; +} + +/** + * @brief Returns the linear acceleration measurement equal to the accelerometer + * measurement with the 1 g of gravity removed. + * @param ahrs AHRS algorithm structure. + * @return Linear acceleration measurement in g. + */ +FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) { +#define Q ahrs->quaternion.element + const FusionVector gravity = { + .axis.x = 2.0f * (Q.x * Q.z - Q.w * Q.y), + .axis.y = 2.0f * (Q.y * Q.z + Q.w * Q.x), + .axis.z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z), + }; // third column of transposed rotation matrix + const FusionVector linearAcceleration = FusionVectorSubtract(ahrs->accelerometer, gravity); + return linearAcceleration; +#undef Q +} + +/** + * @brief Returns the Earth acceleration measurement equal to accelerometer + * measurement in the Earth coordinate frame with the 1 g of gravity removed. + * @param ahrs AHRS algorithm structure. + * @return Earth acceleration measurement in g. + */ +FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) { +#define Q ahrs->quaternion.element +#define A ahrs->accelerometer.axis + const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations + const float qwqx = Q.w * Q.x; + const float qwqy = Q.w * Q.y; + const float qwqz = Q.w * Q.z; + const float qxqy = Q.x * Q.y; + const float qxqz = Q.x * Q.z; + const float qyqz = Q.y * Q.z; + const FusionVector earthAcceleration = { + .axis.x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z), + .axis.y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z), + .axis.z = (2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z)) - 1.0f, + }; // rotation matrix multiplied with the accelerometer, with 1 g subtracted + return earthAcceleration; +#undef Q +#undef A +} + +/** + * @brief Returns the AHRS algorithm internal states. + * @param ahrs AHRS algorithm structure. + * @return AHRS algorithm internal states. + */ +FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs) { + const FusionAhrsInternalStates internalStates = { + .accelerationError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfAccelerometerFeedback))), + .accelerometerIgnored = ahrs->accelerometerIgnored, + .accelerationRejectionTimer = ahrs->settings.rejectionTimeout == 0 ? 0.0f : (float) ahrs->accelerationRejectionTimer / (float) ahrs->settings.rejectionTimeout, + .magneticError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfMagnetometerFeedback))), + .magnetometerIgnored = ahrs->magnetometerIgnored, + .magneticRejectionTimer = ahrs->settings.rejectionTimeout == 0 ? 0.0f : (float) ahrs->magneticRejectionTimer / (float) ahrs->settings.rejectionTimeout, + }; + return internalStates; +} + +/** + * @brief Returns the AHRS algorithm flags. + * @param ahrs AHRS algorithm structure. + * @return AHRS algorithm flags. + */ +FusionAhrsFlags FusionAhrsGetFlags(FusionAhrs *const ahrs) { + const unsigned int warningTimeout = ahrs->settings.rejectionTimeout / 4; + const FusionAhrsFlags flags = { + .initialising = ahrs->initialising, + .accelerationRejectionWarning = ahrs->accelerationRejectionTimer > warningTimeout, + .accelerationRejectionTimeout = ahrs->accelerationRejectionTimeout, + .magneticRejectionWarning = ahrs->magneticRejectionTimer > warningTimeout, + .magneticRejectionTimeout = ahrs->magneticRejectionTimeout, + }; + return flags; +} + +/** + * @brief Sets the heading of the orientation measurement provided by the AHRS + * algorithm. This function can be used to reset drift in heading when the AHRS + * algorithm is being used without a magnetometer. + * @param ahrs AHRS algorithm structure. + * @param heading Heading angle in degrees. + */ +void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading) { +#define Q ahrs->quaternion.element + const float yaw = atan2f(Q.w * Q.z + Q.x * Q.y, 0.5f - Q.y * Q.y - Q.z * Q.z); + const float halfYawMinusHeading = 0.5f * (yaw - FusionDegreesToRadians(heading)); + const FusionQuaternion rotation = { + .element.w = cosf(halfYawMinusHeading), + .element.x = 0.0f, + .element.y = 0.0f, + .element.z = -1.0f * sinf(halfYawMinusHeading), + }; + ahrs->quaternion = FusionQuaternionMultiply(rotation, ahrs->quaternion); +#undef Q +} + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.h b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.h new file mode 100644 index 0000000000..cebae66c3b --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.h @@ -0,0 +1,104 @@ +/** + * @file FusionAhrs.h + * @author Seb Madgwick + * @brief AHRS algorithm to combine gyroscope, accelerometer, and magnetometer + * measurements into a single measurement of orientation relative to the Earth. + */ + +#ifndef FUSION_AHRS_H +#define FUSION_AHRS_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionMath.h" +#include + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief AHRS algorithm settings. + */ +typedef struct { + float gain; + float accelerationRejection; + float magneticRejection; + unsigned int rejectionTimeout; +} FusionAhrsSettings; + +/** + * @brief AHRS algorithm structure. Structure members are used internally and + * must not be accessed by the application. + */ +typedef struct { + FusionAhrsSettings settings; + FusionQuaternion quaternion; + FusionVector accelerometer; + bool initialising; + float rampedGain; + float rampedGainStep; + FusionVector halfAccelerometerFeedback; + FusionVector halfMagnetometerFeedback; + bool accelerometerIgnored; + unsigned int accelerationRejectionTimer; + bool accelerationRejectionTimeout; + bool magnetometerIgnored; + unsigned int magneticRejectionTimer; + bool magneticRejectionTimeout; +} FusionAhrs; + +/** + * @brief AHRS algorithm internal states. + */ +typedef struct { + float accelerationError; + bool accelerometerIgnored; + float accelerationRejectionTimer; + float magneticError; + bool magnetometerIgnored; + float magneticRejectionTimer; +} FusionAhrsInternalStates; + +/** + * @brief AHRS algorithm flags. + */ +typedef struct { + bool initialising; + bool accelerationRejectionWarning; + bool accelerationRejectionTimeout; + bool magneticRejectionWarning; + bool magneticRejectionTimeout; +} FusionAhrsFlags; + +//------------------------------------------------------------------------------ +// Function declarations + +void FusionAhrsInitialise(FusionAhrs *const ahrs); + +void FusionAhrsReset(FusionAhrs *const ahrs); + +void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings); + +void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const FusionVector magnetometer, const float deltaTime); + +void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float deltaTime); + +void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float heading, const float deltaTime); + +FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs); + +FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs); + +FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs); + +FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs); + +FusionAhrsFlags FusionAhrsGetFlags(FusionAhrs *const ahrs); + +void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading); + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAxes.h b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAxes.h new file mode 100644 index 0000000000..b5e8e2994b --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAxes.h @@ -0,0 +1,187 @@ +/** + * @file FusionAxes.h + * @author Seb Madgwick + * @brief Swaps sensor axes for alignment with the body axes. + */ + +#ifndef FUSION_AXES_H +#define FUSION_AXES_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionMath.h" + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Axes alignment describing the sensor axes relative to the body axes. + * For example, if the body X axis is aligned with the sensor Y axis and the + * body Y axis is aligned with sensor X axis but pointing the opposite direction + * then alignment is +Y-X+Z. + */ +typedef enum { + FusionAxesAlignmentPXPYPZ, /* +X+Y+Z */ + FusionAxesAlignmentPXNZPY, /* +X-Z+Y */ + FusionAxesAlignmentPXNYNZ, /* +X-Y-Z */ + FusionAxesAlignmentPXPZNY, /* +X+Z-Y */ + FusionAxesAlignmentNXPYNZ, /* -X+Y-Z */ + FusionAxesAlignmentNXPZPY, /* -X+Z+Y */ + FusionAxesAlignmentNXNYPZ, /* -X-Y+Z */ + FusionAxesAlignmentNXNZNY, /* -X-Z-Y */ + FusionAxesAlignmentPYNXPZ, /* +Y-X+Z */ + FusionAxesAlignmentPYNZNX, /* +Y-Z-X */ + FusionAxesAlignmentPYPXNZ, /* +Y+X-Z */ + FusionAxesAlignmentPYPZPX, /* +Y+Z+X */ + FusionAxesAlignmentNYPXPZ, /* -Y+X+Z */ + FusionAxesAlignmentNYNZPX, /* -Y-Z+X */ + FusionAxesAlignmentNYNXNZ, /* -Y-X-Z */ + FusionAxesAlignmentNYPZNX, /* -Y+Z-X */ + FusionAxesAlignmentPZPYNX, /* +Z+Y-X */ + FusionAxesAlignmentPZPXPY, /* +Z+X+Y */ + FusionAxesAlignmentPZNYPX, /* +Z-Y+X */ + FusionAxesAlignmentPZNXNY, /* +Z-X-Y */ + FusionAxesAlignmentNZPYPX, /* -Z+Y+X */ + FusionAxesAlignmentNZNXPY, /* -Z-X+Y */ + FusionAxesAlignmentNZNYNX, /* -Z-Y-X */ + FusionAxesAlignmentNZPXNY, /* -Z+X-Y */ +} FusionAxesAlignment; + +//------------------------------------------------------------------------------ +// Inline functions + +/** + * @brief Swaps sensor axes for alignment with the body axes. + * @param sensor Sensor axes. + * @param alignment Axes alignment. + * @return Sensor axes aligned with the body axes. + */ +static inline FusionVector FusionAxesSwap(const FusionVector sensor, const FusionAxesAlignment alignment) { + FusionVector result; + switch (alignment) { + case FusionAxesAlignmentPXPYPZ: + break; + case FusionAxesAlignmentPXNZPY: + result.axis.x = +sensor.axis.x; + result.axis.y = -sensor.axis.z; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentPXNYNZ: + result.axis.x = +sensor.axis.x; + result.axis.y = -sensor.axis.y; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentPXPZNY: + result.axis.x = +sensor.axis.x; + result.axis.y = +sensor.axis.z; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentNXPYNZ: + result.axis.x = -sensor.axis.x; + result.axis.y = +sensor.axis.y; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentNXPZPY: + result.axis.x = -sensor.axis.x; + result.axis.y = +sensor.axis.z; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentNXNYPZ: + result.axis.x = -sensor.axis.x; + result.axis.y = -sensor.axis.y; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentNXNZNY: + result.axis.x = -sensor.axis.x; + result.axis.y = -sensor.axis.z; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentPYNXPZ: + result.axis.x = +sensor.axis.y; + result.axis.y = -sensor.axis.x; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentPYNZNX: + result.axis.x = +sensor.axis.y; + result.axis.y = -sensor.axis.z; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPYPXNZ: + result.axis.x = +sensor.axis.y; + result.axis.y = +sensor.axis.x; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentPYPZPX: + result.axis.x = +sensor.axis.y; + result.axis.y = +sensor.axis.z; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNYPXPZ: + result.axis.x = -sensor.axis.y; + result.axis.y = +sensor.axis.x; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentNYNZPX: + result.axis.x = -sensor.axis.y; + result.axis.y = -sensor.axis.z; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNYNXNZ: + result.axis.x = -sensor.axis.y; + result.axis.y = -sensor.axis.x; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentNYPZNX: + result.axis.x = -sensor.axis.y; + result.axis.y = +sensor.axis.z; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPZPYNX: + result.axis.x = +sensor.axis.z; + result.axis.y = +sensor.axis.y; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPZPXPY: + result.axis.x = +sensor.axis.z; + result.axis.y = +sensor.axis.x; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentPZNYPX: + result.axis.x = +sensor.axis.z; + result.axis.y = -sensor.axis.y; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentPZNXNY: + result.axis.x = +sensor.axis.z; + result.axis.y = -sensor.axis.x; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentNZPYPX: + result.axis.x = -sensor.axis.z; + result.axis.y = +sensor.axis.y; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNZNXPY: + result.axis.x = -sensor.axis.z; + result.axis.y = -sensor.axis.x; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentNZNYNX: + result.axis.x = -sensor.axis.z; + result.axis.y = -sensor.axis.y; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentNZPXNY: + result.axis.x = -sensor.axis.z; + result.axis.y = +sensor.axis.x; + result.axis.z = -sensor.axis.y; + return result; + } + return sensor; +} + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCalibration.h b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCalibration.h new file mode 100644 index 0000000000..9193e04034 --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCalibration.h @@ -0,0 +1,44 @@ +/** + * @file FusionCalibration.h + * @author Seb Madgwick + * @brief Gyroscope, accelerometer, and magnetometer calibration models. + */ + +#ifndef FUSION_CALIBRATION_H +#define FUSION_CALIBRATION_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionMath.h" + +//------------------------------------------------------------------------------ +// Inline functions + +/** + * @brief Gyroscope and accelerometer calibration model. + * @param uncalibrated Uncalibrated measurement. + * @param misalignment Misalignment matrix. + * @param sensitivity Sensitivity. + * @param offset Offset. + * @return Calibrated measurement. + */ +static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibrated, const FusionMatrix misalignment, const FusionVector sensitivity, const FusionVector offset) { + return FusionMatrixMultiplyVector(misalignment, FusionVectorHadamardProduct(FusionVectorSubtract(uncalibrated, offset), sensitivity)); +} + +/** + * @brief Magnetometer calibration model. + * @param uncalibrated Uncalibrated measurement. + * @param softIronMatrix Soft-iron matrix. + * @param hardIronOffset Hard-iron offset. + * @return Calibrated measurement. + */ +static inline FusionVector FusionCalibrationMagnetic(const FusionVector uncalibrated, const FusionMatrix softIronMatrix, const FusionVector hardIronOffset) { + return FusionVectorSubtract(FusionMatrixMultiplyVector(softIronMatrix, uncalibrated), hardIronOffset); +} + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.c b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.c new file mode 100644 index 0000000000..ad6a7ac3f7 --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.c @@ -0,0 +1,36 @@ +/** + * @file FusionCompass.c + * @author Seb Madgwick + * @brief Tilt-compensated compass to calculate an heading relative to magnetic + * north using accelerometer and magnetometer measurements. + */ + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionCompass.h" +#include // atan2f + +//------------------------------------------------------------------------------ +// Functions + +/** + * @brief Calculates the heading relative to magnetic north. + * @param accelerometer Accelerometer measurement in any calibrated units. + * @param magnetometer Magnetometer measurement in any calibrated units. + * @return Heading angle in degrees. + */ +float FusionCompassCalculateHeading(const FusionVector accelerometer, const FusionVector magnetometer) { + + // Compute direction of magnetic west (Earth's y axis) + const FusionVector magneticWest = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); + + // Compute direction of magnetic north (Earth's x axis) + const FusionVector magneticNorth = FusionVectorNormalise(FusionVectorCrossProduct(magneticWest, accelerometer)); + + // Calculate angular heading relative to magnetic north + return FusionRadiansToDegrees(atan2f(magneticWest.axis.x, magneticNorth.axis.x)); +} + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.h b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.h new file mode 100644 index 0000000000..1ecfab61a0 --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.h @@ -0,0 +1,24 @@ +/** + * @file FusionCompass.h + * @author Seb Madgwick + * @brief Tilt-compensated compass to calculate an heading relative to magnetic + * north using accelerometer and magnetometer measurements. + */ + +#ifndef FUSION_COMPASS_H +#define FUSION_COMPASS_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionMath.h" + +//------------------------------------------------------------------------------ +// Function declarations + +float FusionCompassCalculateHeading(const FusionVector accelerometer, const FusionVector magnetometer); + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionMath.h b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionMath.h new file mode 100644 index 0000000000..bbcc279908 --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionMath.h @@ -0,0 +1,459 @@ +/** + * @file FusionMath.h + * @author Seb Madgwick + * @brief Math library. + */ + +#ifndef FUSION_MATH_H +#define FUSION_MATH_H + +//------------------------------------------------------------------------------ +// Includes + +#include // M_PI, sqrtf, atan2f, asinf +#include +#include + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief 3D vector. + */ +typedef union { + float array[3]; + + struct { + float x; + float y; + float z; + } axis; +} FusionVector; + +/** + * @brief Quaternion. + */ +typedef union { + float array[4]; + + struct { + float w; + float x; + float y; + float z; + } element; +} FusionQuaternion; + +/** + * @brief 3x3 matrix in row-major order. + * See http://en.wikipedia.org/wiki/Row-major_order + */ +typedef union { + float array[3][3]; + + struct { + float xx; + float xy; + float xz; + float yx; + float yy; + float yz; + float zx; + float zy; + float zz; + } element; +} FusionMatrix; + +/** + * @brief Euler angles. Roll, pitch, and yaw correspond to rotations around + * X, Y, and Z respectively. + */ +typedef union { + float array[3]; + + struct { + float roll; + float pitch; + float yaw; + } angle; +} FusionEuler; + +/** + * @brief Vector of zeros. + */ +#define FUSION_VECTOR_ZERO ((FusionVector){ .array = {0.0f, 0.0f, 0.0f} }) + +/** + * @brief Vector of ones. + */ +#define FUSION_VECTOR_ONES ((FusionVector){ .array = {1.0f, 1.0f, 1.0f} }) + +/** + * @brief Identity quaternion. + */ +#define FUSION_IDENTITY_QUATERNION ((FusionQuaternion){ .array = {1.0f, 0.0f, 0.0f, 0.0f} }) + +/** + * @brief Identity matrix. + */ +#define FUSION_IDENTITY_MATRIX ((FusionMatrix){ .array = {{1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}} }) + +/** + * @brief Euler angles of zero. + */ +#define FUSION_EULER_ZERO ((FusionEuler){ .array = {0.0f, 0.0f, 0.0f} }) + +/** + * @brief Pi. May not be defined in math.h. + */ +#ifndef M_PI +#define M_PI (3.14159265358979323846) +#endif + +/** + * @brief Include this definition or add as a preprocessor definition to use + * normal square root operations. + */ +//#define FUSION_USE_NORMAL_SQRT + +//------------------------------------------------------------------------------ +// Inline functions - Degrees and radians conversion + +/** + * @brief Converts degrees to radians. + * @param degrees Degrees. + * @return Radians. + */ +static inline float FusionDegreesToRadians(const float degrees) { + return degrees * ((float) M_PI / 180.0f); +} + +/** + * @brief Converts radians to degrees. + * @param radians Radians. + * @return Degrees. + */ +static inline float FusionRadiansToDegrees(const float radians) { + return radians * (180.0f / (float) M_PI); +} + +//------------------------------------------------------------------------------ +// Inline functions - Arc sine + +/** + * @brief Returns the arc sine of the value. + * @param value Value. + * @return Arc sine of the value. + */ +static inline float FusionAsin(const float value) { + if (value <= -1.0f) { + return (float) M_PI / -2.0f; + } + if (value >= 1.0f) { + return (float) M_PI / 2.0f; + } + return asinf(value); +} + +//------------------------------------------------------------------------------ +// Inline functions - Fast inverse square root + +#ifndef FUSION_USE_NORMAL_SQRT + +/** + * @brief Calculates the reciprocal of the square root. + * See https://pizer.wordpress.com/2008/10/12/fast-inverse-square-root/ + * @param x Operand. + * @return Reciprocal of the square root of x. + */ +static inline float FusionFastInverseSqrt(const float x) { + + typedef union { + float f; + int32_t i; + } Union32; + + Union32 union32 = {.f = x}; + union32.i = 0x5F1F1412 - (union32.i >> 1); + return union32.f * (1.69000231f - 0.714158168f * x * union32.f * union32.f); +} + +#endif + +//------------------------------------------------------------------------------ +// Inline functions - Vector operations + +/** + * @brief Returns true if the vector is zero. + * @param vector Vector. + * @return True if the vector is zero. + */ +static inline bool FusionVectorIsZero(const FusionVector vector) { + return (vector.axis.x == 0.0f) && (vector.axis.y == 0.0f) && (vector.axis.z == 0.0f); +} + +/** + * @brief Returns the sum of two vectors. + * @param vectorA Vector A. + * @param vectorB Vector B. + * @return Sum of two vectors. + */ +static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const FusionVector vectorB) { + FusionVector result; + result.axis.x = vectorA.axis.x + vectorB.axis.x; + result.axis.y = vectorA.axis.y + vectorB.axis.y; + result.axis.z = vectorA.axis.z + vectorB.axis.z; + return result; +} + +/** + * @brief Returns vector B subtracted from vector A. + * @param vectorA Vector A. + * @param vectorB Vector B. + * @return Vector B subtracted from vector A. + */ +static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, const FusionVector vectorB) { + FusionVector result; + result.axis.x = vectorA.axis.x - vectorB.axis.x; + result.axis.y = vectorA.axis.y - vectorB.axis.y; + result.axis.z = vectorA.axis.z - vectorB.axis.z; + return result; +} + +/** + * @brief Returns the sum of the elements. + * @param vector Vector. + * @return Sum of the elements. + */ +static inline float FusionVectorSum(const FusionVector vector) { + return vector.axis.x + vector.axis.y + vector.axis.z; +} + +/** + * @brief Returns the multiplication of a vector by a scalar. + * @param vector Vector. + * @param scalar Scalar. + * @return Multiplication of a vector by a scalar. + */ +static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, const float scalar) { + FusionVector result; + result.axis.x = vector.axis.x * scalar; + result.axis.y = vector.axis.y * scalar; + result.axis.z = vector.axis.z * scalar; + return result; +} + +/** + * @brief Calculates the Hadamard product (element-wise multiplication). + * @param vectorA Vector A. + * @param vectorB Vector B. + * @return Hadamard product. + */ +static inline FusionVector FusionVectorHadamardProduct(const FusionVector vectorA, const FusionVector vectorB) { + FusionVector result; + result.axis.x = vectorA.axis.x * vectorB.axis.x; + result.axis.y = vectorA.axis.y * vectorB.axis.y; + result.axis.z = vectorA.axis.z * vectorB.axis.z; + return result; +} + +/** + * @brief Returns the cross product. + * @param vectorA Vector A. + * @param vectorB Vector B. + * @return Cross product. + */ +static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, const FusionVector vectorB) { +#define A vectorA.axis +#define B vectorB.axis + FusionVector result; + result.axis.x = A.y * B.z - A.z * B.y; + result.axis.y = A.z * B.x - A.x * B.z; + result.axis.z = A.x * B.y - A.y * B.x; + return result; +#undef A +#undef B +} + +/** + * @brief Returns the vector magnitude squared. + * @param vector Vector. + * @return Vector magnitude squared. + */ +static inline float FusionVectorMagnitudeSquared(const FusionVector vector) { + return FusionVectorSum(FusionVectorHadamardProduct(vector, vector)); +} + +/** + * @brief Returns the vector magnitude. + * @param vector Vector. + * @return Vector magnitude. + */ +static inline float FusionVectorMagnitude(const FusionVector vector) { + return sqrtf(FusionVectorMagnitudeSquared(vector)); +} + +/** + * @brief Returns the normalised vector. + * @param vector Vector. + * @return Normalised vector. + */ +static inline FusionVector FusionVectorNormalise(const FusionVector vector) { +#ifdef FUSION_USE_NORMAL_SQRT + const float magnitudeReciprocal = 1.0f / sqrtf(FusionVectorMagnitudeSquared(vector)); +#else + const float magnitudeReciprocal = FusionFastInverseSqrt(FusionVectorMagnitudeSquared(vector)); +#endif + return FusionVectorMultiplyScalar(vector, magnitudeReciprocal); +} + +//------------------------------------------------------------------------------ +// Inline functions - Quaternion operations + +/** + * @brief Returns the sum of two quaternions. + * @param quaternionA Quaternion A. + * @param quaternionB Quaternion B. + * @return Sum of two quaternions. + */ +static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) { + FusionQuaternion result; + result.element.w = quaternionA.element.w + quaternionB.element.w; + result.element.x = quaternionA.element.x + quaternionB.element.x; + result.element.y = quaternionA.element.y + quaternionB.element.y; + result.element.z = quaternionA.element.z + quaternionB.element.z; + return result; +} + +/** + * @brief Returns the multiplication of two quaternions. + * @param quaternionA Quaternion A (to be post-multiplied). + * @param quaternionB Quaternion B (to be pre-multiplied). + * @return Multiplication of two quaternions. + */ +static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) { +#define A quaternionA.element +#define B quaternionB.element + FusionQuaternion result; + result.element.w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z; + result.element.x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y; + result.element.y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x; + result.element.z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w; + return result; +#undef A +#undef B +} + +/** + * @brief Returns the multiplication of a quaternion with a vector. This is a + * normal quaternion multiplication where the vector is treated a + * quaternion with a W element value of zero. The quaternion is post- + * multiplied by the vector. + * @param quaternion Quaternion. + * @param vector Vector. + * @return Multiplication of a quaternion with a vector. + */ +static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuaternion quaternion, const FusionVector vector) { +#define Q quaternion.element +#define V vector.axis + FusionQuaternion result; + result.element.w = -Q.x * V.x - Q.y * V.y - Q.z * V.z; + result.element.x = Q.w * V.x + Q.y * V.z - Q.z * V.y; + result.element.y = Q.w * V.y - Q.x * V.z + Q.z * V.x; + result.element.z = Q.w * V.z + Q.x * V.y - Q.y * V.x; + return result; +#undef Q +#undef V +} + +/** + * @brief Returns the normalised quaternion. + * @param quaternion Quaternion. + * @return Normalised quaternion. + */ +static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion quaternion) { +#define Q quaternion.element +#ifdef FUSION_USE_NORMAL_SQRT + const float magnitudeReciprocal = 1.0f / sqrtf(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); +#else + const float magnitudeReciprocal = FusionFastInverseSqrt(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); +#endif + FusionQuaternion normalisedQuaternion; + normalisedQuaternion.element.w = Q.w * magnitudeReciprocal; + normalisedQuaternion.element.x = Q.x * magnitudeReciprocal; + normalisedQuaternion.element.y = Q.y * magnitudeReciprocal; + normalisedQuaternion.element.z = Q.z * magnitudeReciprocal; + return normalisedQuaternion; +#undef Q +} + +//------------------------------------------------------------------------------ +// Inline functions - Matrix operations + +/** + * @brief Returns the multiplication of a matrix with a vector. + * @param matrix Matrix. + * @param vector Vector. + * @return Multiplication of a matrix with a vector. + */ +static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, const FusionVector vector) { +#define R matrix.element + FusionVector result; + result.axis.x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z; + result.axis.y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z; + result.axis.z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z; + return result; +#undef R +} + +//------------------------------------------------------------------------------ +// Inline functions - Conversion operations + +/** + * @brief Converts a quaternion to a rotation matrix. + * @param quaternion Quaternion. + * @return Rotation matrix. + */ +static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quaternion) { +#define Q quaternion.element + const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations + const float qwqx = Q.w * Q.x; + const float qwqy = Q.w * Q.y; + const float qwqz = Q.w * Q.z; + const float qxqy = Q.x * Q.y; + const float qxqz = Q.x * Q.z; + const float qyqz = Q.y * Q.z; + FusionMatrix matrix; + matrix.element.xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x); + matrix.element.xy = 2.0f * (qxqy - qwqz); + matrix.element.xz = 2.0f * (qxqz + qwqy); + matrix.element.yx = 2.0f * (qxqy + qwqz); + matrix.element.yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y); + matrix.element.yz = 2.0f * (qyqz - qwqx); + matrix.element.zx = 2.0f * (qxqz - qwqy); + matrix.element.zy = 2.0f * (qyqz + qwqx); + matrix.element.zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z); + return matrix; +#undef Q +} + +/** + * @brief Converts a quaternion to ZYX Euler angles in degrees. + * @param quaternion Quaternion. + * @return Euler angles in degrees. + */ +static inline FusionEuler FusionQuaternionToEuler(const FusionQuaternion quaternion) { +#define Q quaternion.element + const float halfMinusQySquared = 0.5f - Q.y * Q.y; // calculate common terms to avoid repeated operations + FusionEuler euler; + euler.angle.roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)); + euler.angle.pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))); + euler.angle.yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)); + return euler; +#undef Q +} + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.c b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.c new file mode 100644 index 0000000000..b21794d38c --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.c @@ -0,0 +1,77 @@ +/** + * @file FusionOffset.c + * @author Seb Madgwick + * @brief Gyroscope offset correction algorithm for run-time calibration of the + * gyroscope offset. + */ + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionOffset.h" +#include // fabs + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Cutoff frequency in Hz. + */ +#define CUTOFF_FREQUENCY (0.02f) + +/** + * @brief Timeout in seconds. + */ +#define TIMEOUT (5) + +/** + * @brief Threshold in degrees per second. + */ +#define THRESHOLD (3.0f) + +//------------------------------------------------------------------------------ +// Functions + +/** + * @brief Initialises the gyroscope offset algorithm. + * @param offset Gyroscope offset algorithm structure. + * @param sampleRate Sample rate in Hz. + */ +void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate) { + offset->filterCoefficient = 2.0f * (float) M_PI * CUTOFF_FREQUENCY * (1.0f / (float) sampleRate); + offset->timeout = TIMEOUT * sampleRate; + offset->timer = 0; + offset->gyroscopeOffset = FUSION_VECTOR_ZERO; +} + +/** + * @brief Updates the gyroscope offset algorithm and returns the corrected + * gyroscope measurement. + * @param offset Gyroscope offset algorithm structure. + * @param gyroscope Gyroscope measurement in degrees per second. + * @return Corrected gyroscope measurement in degrees per second. + */ +FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope) { + + // Subtract offset from gyroscope measurement + gyroscope = FusionVectorSubtract(gyroscope, offset->gyroscopeOffset); + + // Reset timer if gyroscope not stationary + if ((fabs(gyroscope.axis.x) > THRESHOLD) || (fabs(gyroscope.axis.y) > THRESHOLD) || (fabs(gyroscope.axis.z) > THRESHOLD)) { + offset->timer = 0; + return gyroscope; + } + + // Increment timer while gyroscope stationary + if (offset->timer < offset->timeout) { + offset->timer++; + return gyroscope; + } + + // Adjust offset if timer has elapsed + offset->gyroscopeOffset = FusionVectorAdd(offset->gyroscopeOffset, FusionVectorMultiplyScalar(gyroscope, offset->filterCoefficient)); + return gyroscope; +} + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.h b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.h new file mode 100644 index 0000000000..51ae4a8967 --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.h @@ -0,0 +1,40 @@ +/** + * @file FusionOffset.h + * @author Seb Madgwick + * @brief Gyroscope offset correction algorithm for run-time calibration of the + * gyroscope offset. + */ + +#ifndef FUSION_OFFSET_H +#define FUSION_OFFSET_H + +//------------------------------------------------------------------------------ +// Includes + +#include "FusionMath.h" + +//------------------------------------------------------------------------------ +// Definitions + +/** + * @brief Gyroscope offset algorithm structure. Structure members are used + * internally and must not be accessed by the application. + */ +typedef struct { + float filterCoefficient; + unsigned int timeout; + unsigned int timer; + FusionVector gyroscopeOffset; +} FusionOffset; + +//------------------------------------------------------------------------------ +// Function declarations + +void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate); + +FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope); + +#endif + +//------------------------------------------------------------------------------ +// End of file diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/main.cpp b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/main.cpp new file mode 100644 index 0000000000..70b70d8a78 --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/main.cpp @@ -0,0 +1,148 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "drivers/HighResClock.h" +#include "rtos/ThisThread.h" + +#include "CoreI2C.h" +#include "CoreLSM6DSOX.hpp" +#include "LogKit.h" + +// ? Note the following code has been heavily inspired from: +// ? https://github.com/xioTechnologies/Fusion/blob/main/Examples/Advanced/main.c +// ? For more information, see https://github.com/xioTechnologies/Fusion + +#include "fusion/Fusion.h" + +using namespace std::chrono; +using namespace leka; + +namespace { + +namespace imu { + + namespace internal { + + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); + + } // namespace internal + + CoreLSM6DSOX lsm6dsox(internal::i2c, internal::drdy_irq); + +} // namespace imu + +namespace fusion { + + constexpr auto kODR_HZ = int {52}; + + auto ahrs = FusionAhrs {}; + + const FusionAhrsSettings settings = { + .gain = 0.5F, + .accelerationRejection = 10.0F, + .magneticRejection = 0.0F, + .rejectionTimeout = static_cast(5 * kODR_HZ), // ? # of samples in 5 seconds + }; + + auto timestamp_now = rtos::Kernel::Clock::now(); + auto timestamp_previous = rtos::Kernel::Clock::now(); + + auto global_offset = FusionOffset {}; + + constexpr auto CALIBRATION = bool {true}; + // constexpr auto CALIBRATION = bool {false}; + + void callback(const interface::LSM6DSOX::SensorData &data) + { + timestamp_now = rtos::Kernel::Clock::now(); + auto timestamp_now_ms = mbed::HighResClock::now().time_since_epoch().count(); + + // ? Acquire latest sensor data + auto gyroscope = FusionVector {{data.gy.x, data.gy.y, data.gy.z}}; + auto accelerometer = FusionVector {{data.xl.x, data.xl.y, data.xl.z}}; + + if constexpr (CALIBRATION) { + // ? Define calibration offsets + // ? Data: https://www.dropbox.com/scl/fi/cue7qpb77892rozyjbmcq/2022_01_16-IMU-Calibration_Data.xlsx + // const auto gyroscope_offset = FusionVector {{}}; + // const auto accelerometer_offset = FusionVector {{}}; + constexpr auto gyroscope_offset = FusionVector {{0.02544522554F, -0.3286247803F, 0.3205770357F}}; + constexpr auto accelerometer_offset = FusionVector {{0.006480437024F, -0.01962820621F, 0.003259031049F}}; + + // ? Apply calibration offsets + gyroscope = + FusionCalibrationInertial(gyroscope, FUSION_IDENTITY_MATRIX, FUSION_VECTOR_ONES, gyroscope_offset); + accelerometer = FusionCalibrationInertial(accelerometer, FUSION_IDENTITY_MATRIX, FUSION_VECTOR_ONES, + accelerometer_offset); + + // ? Update gyroscope offset correction algorithm + gyroscope = FusionOffsetUpdate(&global_offset, gyroscope); + } + + // ? Calculate delta time (in seconds) to account for gyroscope sample clock error + auto delta_time = static_cast((timestamp_now - timestamp_previous).count()) / 1000.F; + timestamp_previous = timestamp_now; + + // ? Update gyroscope AHRS algorithm + FusionAhrsUpdateNoMagnetometer(&ahrs, gyroscope, accelerometer, delta_time); + + // ? Get quaternion + const auto quaternion = FusionAhrsGetQuaternion(&ahrs); + + const auto q_w = quaternion.element.w; + const auto q_x = quaternion.element.x; + const auto q_y = quaternion.element.y; + const auto q_z = quaternion.element.z; + + // ? Get Euler angles + const auto euler = FusionQuaternionToEuler(quaternion); + + const auto pitch = euler.angle.pitch; + const auto roll = euler.angle.roll; + const auto yaw = euler.angle.yaw; + + // ? Log values + // ? See https://x-io.co.uk/downloads/x-IMU3-User-Manual-v1.0.pdf#page=24 + log_free("I,%" PRId64 ",%f,%f,%f,%f,%f,%f\r\nQ,%" PRId64 ",%f,%f,%f,%f\r\n", timestamp_now_ms, gyroscope.axis.x, + gyroscope.axis.y, gyroscope.axis.z, accelerometer.axis.x, accelerometer.axis.y, accelerometer.axis.z, + timestamp_now_ms, q_w, q_x, q_y, q_z); + }; + +} // namespace fusion + +} // namespace + +auto main() -> int +{ + logger::init(); + + rtos::ThisThread::sleep_for(1s); + + imu::lsm6dsox.init(); + + imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Off); + + // ? Initialise algorithms + FusionAhrsInitialise(&fusion::ahrs); + + if constexpr (fusion::CALIBRATION) { + // ? Set AHRS algorithm settings + FusionAhrsSetSettings(&fusion::ahrs, &fusion::settings); + + // ? Set global fusion offset + FusionOffsetInitialise(&fusion::global_offset, fusion::kODR_HZ); + } + + rtos::ThisThread::sleep_for(1s); + + imu::lsm6dsox.registerOnGyDataReadyCallback(fusion::callback); + imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Normal); + + while (true) { + rtos::ThisThread::sleep_for(5s); + } +} From 574ee028ced00c0e210e7dd6c53b5dc7620a0f5e Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Sat, 4 Feb 2023 15:16:20 +0100 Subject: [PATCH 097/143] :truck: (spikes): Rename accel_gyro to sensors_imu_lsm6dsox --- spikes/CMakeLists.txt | 4 ++-- spikes/lk_accel_gyro/CMakeLists.txt | 22 ------------------- spikes/lk_sensors_imu_lsm6dsox/CMakeLists.txt | 22 +++++++++++++++++++ .../main.cpp | 0 4 files changed, 24 insertions(+), 24 deletions(-) delete mode 100644 spikes/lk_accel_gyro/CMakeLists.txt create mode 100644 spikes/lk_sensors_imu_lsm6dsox/CMakeLists.txt rename spikes/{lk_accel_gyro => lk_sensors_imu_lsm6dsox}/main.cpp (100%) diff --git a/spikes/CMakeLists.txt b/spikes/CMakeLists.txt index 5a0723d8fa..55e406009d 100644 --- a/spikes/CMakeLists.txt +++ b/spikes/CMakeLists.txt @@ -2,7 +2,6 @@ # Copyright 2020 APF France handicap # SPDX-License-Identifier: Apache-2.0 -add_subdirectory(${SPIKES_DIR}/lk_accel_gyro) add_subdirectory(${SPIKES_DIR}/lk_audio) add_subdirectory(${SPIKES_DIR}/lk_behavior_kit) add_subdirectory(${SPIKES_DIR}/lk_ble) @@ -28,6 +27,7 @@ add_subdirectory(${SPIKES_DIR}/lk_qdac) add_subdirectory(${SPIKES_DIR}/lk_reinforcer) add_subdirectory(${SPIKES_DIR}/lk_rfid) add_subdirectory(${SPIKES_DIR}/lk_sensors_battery) +add_subdirectory(${SPIKES_DIR}/lk_sensors_imu_lsm6dsox) add_subdirectory(${SPIKES_DIR}/lk_sensors_imu_lsm6dsox_fusion_calibration) add_subdirectory(${SPIKES_DIR}/lk_sensors_light) add_subdirectory(${SPIKES_DIR}/lk_sensors_microphone) @@ -48,7 +48,6 @@ add_subdirectory(${SPIKES_DIR}/stl_cxxsupport) add_custom_target(spikes_leka) add_dependencies(spikes_leka - spike_lk_accel_gyro spike_lk_ble spike_lk_bluetooth spike_lk_cg_animations @@ -70,6 +69,7 @@ add_dependencies(spikes_leka spike_lk_reinforcer spike_lk_rfid spike_lk_sensors_battery + spike_lk_sensors_imu_lsm6dsox spike_lk_sensors_imu_lsm6dsox_fusion_calibration spike_lk_sensors_light spike_lk_sensors_microphone diff --git a/spikes/lk_accel_gyro/CMakeLists.txt b/spikes/lk_accel_gyro/CMakeLists.txt deleted file mode 100644 index 5259e3d82f..0000000000 --- a/spikes/lk_accel_gyro/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -# Leka - LekaOS -# Copyright 2022 APF France handicap -# SPDX-License-Identifier: Apache-2.0 - -add_mbed_executable(spike_lk_accel_gyro) - -target_include_directories(spike_lk_accel_gyro - PRIVATE - . -) - -target_sources(spike_lk_accel_gyro - PRIVATE - main.cpp -) - -target_link_libraries(spike_lk_accel_gyro - CoreIMU - CoreI2C -) - -target_link_custom_leka_targets(spike_lk_accel_gyro) diff --git a/spikes/lk_sensors_imu_lsm6dsox/CMakeLists.txt b/spikes/lk_sensors_imu_lsm6dsox/CMakeLists.txt new file mode 100644 index 0000000000..6764b69af4 --- /dev/null +++ b/spikes/lk_sensors_imu_lsm6dsox/CMakeLists.txt @@ -0,0 +1,22 @@ +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +add_mbed_executable(spike_lk_sensors_imu_lsm6dsox) + +target_include_directories(spike_lk_sensors_imu_lsm6dsox + PRIVATE + . +) + +target_sources(spike_lk_sensors_imu_lsm6dsox + PRIVATE + main.cpp +) + +target_link_libraries(spike_lk_sensors_imu_lsm6dsox + CoreIMU + CoreI2C +) + +target_link_custom_leka_targets(spike_lk_sensors_imu_lsm6dsox) diff --git a/spikes/lk_accel_gyro/main.cpp b/spikes/lk_sensors_imu_lsm6dsox/main.cpp similarity index 100% rename from spikes/lk_accel_gyro/main.cpp rename to spikes/lk_sensors_imu_lsm6dsox/main.cpp From 06ec203343a2dd4f9731fcb32096cd3d072b0bfb Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Fri, 10 Feb 2023 15:26:50 +0100 Subject: [PATCH 098/143] :wrench: (clang-format): Update ignored files, directories --- .clang-format-ignore | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.clang-format-ignore b/.clang-format-ignore index a6bfdae312..5684191c15 100644 --- a/.clang-format-ignore +++ b/.clang-format-ignore @@ -1,6 +1,5 @@ ./extern ./cmake ./targets -./_build -./_build_unit_tests -./_build_cmake_tools +./_build* +*external* From c914267041503f1d4058b398a02b8a4fcb109062 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 15 Feb 2023 10:17:04 +0100 Subject: [PATCH 099/143] :test_tube: (LSM6DSOX): Add failing test on empty drdy callback --- drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp b/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp index 6e32c3af81..e1173e9f97 100644 --- a/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp +++ b/drivers/CoreIMU/tests/CoreLSM6DSOX_test.cpp @@ -78,3 +78,11 @@ TEST_F(CoreLSM6DSOXTest, onGyrDRDY) auto on_rise_callback = spy_InterruptIn_getRiseCallback(); on_rise_callback(); } + +TEST_F(CoreLSM6DSOXTest, emptyOnGyrDrdyCallback) +{ + lsm6dsox.registerOnGyDataReadyCallback({}); + + auto on_rise_callback = spy_InterruptIn_getRiseCallback(); + on_rise_callback(); +} From 186dc3d8e262e12230baacdace616f863df8ff73 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 15 Feb 2023 10:17:48 +0100 Subject: [PATCH 100/143] :bug: (LSM6DSOX): Fix crash on empty drdy callback Check if callback is set before calling --- drivers/CoreIMU/include/CoreLSM6DSOX.hpp | 2 +- drivers/CoreIMU/source/CoreLSM6DSOX.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/CoreIMU/include/CoreLSM6DSOX.hpp b/drivers/CoreIMU/include/CoreLSM6DSOX.hpp index 5081254e49..fc2a022fac 100644 --- a/drivers/CoreIMU/include/CoreLSM6DSOX.hpp +++ b/drivers/CoreIMU/include/CoreLSM6DSOX.hpp @@ -47,7 +47,7 @@ class CoreLSM6DSOX : public interface::LSM6DSOX std::array data_raw_xl {}; std::array data_raw_gy {}; - drdy_callback_t _on_gy_data_ready_callback; + drdy_callback_t _on_gy_data_ready_callback {}; static constexpr uint8_t kMaxBufferLength = 32; std::array _rx_buffer {}; diff --git a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp index 528168a546..4de839ecae 100644 --- a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp +++ b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp @@ -90,7 +90,9 @@ void CoreLSM6DSOX::onGyrDataReadyHandler() _sensor_data.xl.y = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(1)) / _1k; _sensor_data.xl.z = lsm6dsox_from_fs4_to_mg(data_raw_xl.at(2)) / _1k; - _on_gy_data_ready_callback(_sensor_data); + if (_on_gy_data_ready_callback) { + _on_gy_data_ready_callback(_sensor_data); + } } auto CoreLSM6DSOX::read(uint8_t register_address, uint16_t number_bytes_to_read, uint8_t *p_buffer) -> int32_t From 22b7de927aeac6e44dd801f5f09c8b53a7c283e0 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Mon, 6 Feb 2023 13:45:27 +0100 Subject: [PATCH 101/143] :fire: (IMUKit): Remove Mahony implementation --- libs/IMUKit/CMakeLists.txt | 2 - libs/IMUKit/include/IMUKit.hpp | 19 +-- libs/IMUKit/include/internal/Mahony.cpp | 157 ------------------------ libs/IMUKit/include/internal/Mahony.hpp | 52 -------- libs/IMUKit/source/IMUKit.cpp | 18 +-- libs/IMUKit/tests/IMUKit_test.cpp | 154 ----------------------- 6 files changed, 9 insertions(+), 393 deletions(-) delete mode 100644 libs/IMUKit/include/internal/Mahony.cpp delete mode 100644 libs/IMUKit/include/internal/Mahony.hpp diff --git a/libs/IMUKit/CMakeLists.txt b/libs/IMUKit/CMakeLists.txt index 8b1c84fb10..d70e967c70 100644 --- a/libs/IMUKit/CMakeLists.txt +++ b/libs/IMUKit/CMakeLists.txt @@ -12,12 +12,10 @@ target_include_directories(IMUKit target_sources(IMUKit PRIVATE source/IMUKit.cpp - include/internal/Mahony.cpp ) target_link_libraries(IMUKit CoreIMU - EventLoopKit ) if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") diff --git a/libs/IMUKit/include/IMUKit.hpp b/libs/IMUKit/include/IMUKit.hpp index 28b723d9bc..68b7f9be29 100644 --- a/libs/IMUKit/include/IMUKit.hpp +++ b/libs/IMUKit/include/IMUKit.hpp @@ -4,40 +4,27 @@ #pragma once +#include #include #include "interface/LSM6DSOX.hpp" -#include "internal/Mahony.hpp" namespace leka { class IMUKit { public: - explicit IMUKit(interface::LSM6DSOX &lsm6dsox) - : _lsm6dsox(lsm6dsox) { - // nothing to do - }; + explicit IMUKit(interface::LSM6DSOX &lsm6dsox) : _lsm6dsox(lsm6dsox) {} void init(); void start(); void stop(); - void setOrigin(); + void setOrigin(); auto getAngles() -> std::array; private: - void computeAngles(const interface::LSM6DSOX::SensorData &imu_data); interface::LSM6DSOX &_lsm6dsox; - - ahrs::Mahony _mahony {}; - struct SamplingConfig { - const std::chrono::milliseconds delay {}; - const float frequency {}; - }; - // ? Sampling config deprecated. - // TODO(@ladislas @hugo): Use dynamic sampling frequency. - const SamplingConfig kDefaultSamplingConfig {.delay = std::chrono::milliseconds {70}, .frequency = 13.F}; }; } // namespace leka diff --git a/libs/IMUKit/include/internal/Mahony.cpp b/libs/IMUKit/include/internal/Mahony.cpp deleted file mode 100644 index 389da6ddac..0000000000 --- a/libs/IMUKit/include/internal/Mahony.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 Adafruit Industries (MIT License) -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "Mahony.hpp" -#include -#include - -namespace leka::ahrs { - -void Mahony::update(std::tuple accel, std::tuple gyro, - std::tuple mag) -{ - auto [ax, ay, az] = accel; - auto [gx, gy, gz] = gyro; - auto [mx, my, mz] = mag; - - float dt = _invSampleFreq; - float recipNorm; - float q0q0; - float q0q1; - float q0q2; - float q0q3; - float q1q1; - float q1q2; - float q1q3; - float q2q2; - float q2q3; - float q3q3; - float hx; - float hy; - float bx; - float bz; - float halfvx; - float halfvy; - float halfvz; - float halfwx; - float halfwy; - float halfwz; - float halfex; - float halfey; - float halfez; - float qa; - float qb; - float qc; - - // LCOV_EXCL_START - Exclude while magnetometer is not used - if ((mx == !0.0F) && (my == !0.0F) && (mz == !0.0F)) { - recipNorm = utils::math::invSqrt(mx * mx + my * my + mz * mz); - mx *= recipNorm; - my *= recipNorm; - mz *= recipNorm; - } - // LCOV_EXCL_STOP - - gx *= std::numbers::pi_v / 180.F; - gy *= std::numbers::pi_v / 180.F; - gz *= std::numbers::pi_v / 180.F; - - if ((ax != 0.0F) || (ay != 0.0F) || (az != 0.0F)) { - recipNorm = utils::math::invSqrt(ax * ax + ay * ay + az * az); - ax *= recipNorm; - ay *= recipNorm; - az *= recipNorm; - - q0q0 = _q0 * _q0; - q0q1 = _q0 * _q1; - q0q2 = _q0 * _q2; - q0q3 = _q0 * _q3; - q1q1 = _q1 * _q1; - q1q2 = _q1 * _q2; - q1q3 = _q1 * _q3; - q2q2 = _q2 * _q2; - q2q3 = _q2 * _q3; - q3q3 = _q3 * _q3; - - hx = 2.0F * (mx * (0.5F - q2q2 - q3q3) + my * (q1q2 - q0q3) + mz * (q1q3 + q0q2)); - hy = 2.0F * (mx * (q1q2 + q0q3) + my * (0.5F - q1q1 - q3q3) + mz * (q2q3 - q0q1)); - bx = sqrtf(hx * hx + hy * hy); - bz = 2.0F * (mx * (q1q3 - q0q2) + my * (q2q3 + q0q1) + mz * (0.5F - q1q1 - q2q2)); - - halfvx = q1q3 - q0q2; - halfvy = q0q1 + q2q3; - halfvz = q0q0 - 0.5F + q3q3; - halfwx = bx * (0.5F - q2q2 - q3q3) + bz * (q1q3 - q0q2); - halfwy = bx * (q1q2 - q0q3) + bz * (q0q1 + q2q3); - halfwz = bx * (q0q2 + q1q3) + bz * (0.5F - q1q1 - q2q2); - - halfex = (ay * halfvz - az * halfvy) + (my * halfwz - mz * halfwy); - halfey = (az * halfvx - ax * halfvz) + (mz * halfwx - mx * halfwz); - halfez = (ax * halfvy - ay * halfvx) + (mx * halfwy - my * halfwx); - - gx += kTwoKp * halfex; - gy += kTwoKp * halfey; - gz += kTwoKp * halfez; - } - - gx *= (0.5F * dt); - gy *= (0.5F * dt); - gz *= (0.5F * dt); - qa = _q0; - qb = _q1; - qc = _q2; - _q0 += (-qb * gx - qc * gy - _q3 * gz); - _q1 += (qa * gx + qc * gz - _q3 * gy); - _q2 += (qa * gy - qb * gz + _q3 * gx); - _q3 += (qa * gz + qb * gy - qc * gx); - - recipNorm = utils::math::invSqrt(_q0 * _q0 + _q1 * _q1 + _q2 * _q2 + _q3 * _q3); - _q0 *= recipNorm; - _q1 *= recipNorm; - _q2 *= recipNorm; - _q3 *= recipNorm; - anglesComputed = false; -} - -auto Mahony::getRoll() -> float -{ - if (!anglesComputed) { - computeAngles(); - } - return _roll * 180.F * std::numbers::inv_pi_v; -} -auto Mahony::getPitch() -> float -{ - if (!anglesComputed) { - computeAngles(); - } - return _pitch * 180.F * std::numbers::inv_pi_v; -} -auto Mahony::getYaw() -> float -{ - if (!anglesComputed) { - computeAngles(); - } - return _yaw * 180.F * std::numbers::inv_pi_v + 180.0F; -} - -void Mahony::setOrigin() -{ - _q0 = 1.F; - _q1 = _q2 = _q3 = 0.0F; -} - -void Mahony::computeAngles() -{ - _roll = atan2f(_q0 * _q1 + _q2 * _q3, 0.5F - _q1 * _q1 - _q2 * _q2); - _pitch = asinf(-2.0F * (_q1 * _q3 - _q0 * _q2)); - _yaw = atan2f(_q1 * _q2 + _q0 * _q3, 0.5F - _q2 * _q2 - _q3 * _q3); - gravity_vector[0] = 2.0F * (_q1 * _q3 - _q0 * _q2); - gravity_vector[1] = 2.0F * (_q0 * _q1 + _q2 * _q3); - gravity_vector[2] = 2.0F * (_q1 * _q0 - 0.5F + _q3 * _q3); - anglesComputed = true; -} - -} // namespace leka::ahrs diff --git a/libs/IMUKit/include/internal/Mahony.hpp b/libs/IMUKit/include/internal/Mahony.hpp deleted file mode 100644 index 6a12cd868a..0000000000 --- a/libs/IMUKit/include/internal/Mahony.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// Leka - LekaOS -// Copyright 2020 Adafruit Industries (MIT License) -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -// ? Heavily inspired by (taken from) Adafruit' AHRS library -// ? https://github.com/adafruit/Adafruit_AHRS - -// ? Original paper by Robert Mahony -// ? https://ieeexplore.ieee.org/document/4608934 - -#pragma once - -#include -#include - -#include "MathUtils.h" - -namespace leka::ahrs { - -class Mahony -{ - public: - Mahony() = default; - - void begin(float sampleFrequency) { _invSampleFreq = 1.0F / sampleFrequency; } - void update(std::tuple, std::tuple, std::tuple); - - auto getRoll() -> float; - auto getPitch() -> float; - auto getYaw() -> float; - - void setOrigin(); - - private: - void computeAngles(); - - const float kTwoKp = float {2.0F * 2.F}; - - float _q0 {1.0F}; - float _q1 {0.0F}; - float _q2 {0.0F}; - float _q3 {0.0F}; - float _invSampleFreq {}; - float _roll {}; - float _pitch {}; - float _yaw {}; - std::array gravity_vector {}; - bool anglesComputed = false; -}; - -} // namespace leka::ahrs diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index aec8d633d5..d4bd854761 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -8,9 +8,10 @@ using namespace leka; void IMUKit::init() { - _mahony.begin(kDefaultSamplingConfig.frequency); + auto on_drdy_callback = [this](const interface::LSM6DSOX::SensorData &data) { + // TODO(@ladislas): to implement + }; - auto on_drdy_callback = [this](const interface::LSM6DSOX::SensorData &imu_data) { computeAngles(imu_data); }; _lsm6dsox.registerOnGyDataReadyCallback(on_drdy_callback); } @@ -26,18 +27,11 @@ void IMUKit::stop() void IMUKit::setOrigin() { - _mahony.setOrigin(); + // TODO(@ladislas): to implement } auto IMUKit::getAngles() -> std::array { - return {_mahony.getPitch(), _mahony.getRoll(), _mahony.getYaw()}; -} - -void IMUKit::computeAngles(const interface::LSM6DSOX::SensorData &imu_data) -{ - auto xl_data = std::make_tuple(imu_data.xl.x, imu_data.xl.y, imu_data.xl.z); - auto gy_data = std::make_tuple(imu_data.gy.x, imu_data.gy.y, imu_data.gy.z); - constexpr auto mag_data = std::make_tuple(0.0F, 0.0F, 0.0F); - _mahony.update(xl_data, gy_data, mag_data); + // TODO(@ladislas): to implement + return {0.F, 0.F, 0.F}; } diff --git a/libs/IMUKit/tests/IMUKit_test.cpp b/libs/IMUKit/tests/IMUKit_test.cpp index 98ae3ceba5..9937c236bb 100644 --- a/libs/IMUKit/tests/IMUKit_test.cpp +++ b/libs/IMUKit/tests/IMUKit_test.cpp @@ -18,8 +18,6 @@ using ::testing::_; using ::testing::AtLeast; using ::testing::SaveArg; -// TODO(@leka/dev-embedded): temporary fix, changes are needed when updating fusion algorithm - class IMUKitTest : public ::testing::Test { protected: @@ -51,155 +49,3 @@ TEST_F(IMUKitTest, stop) imukit.stop(); } - -TEST_F(IMUKitTest, computeAnglesNullAccelerations) -{ - std::function callback {}; - - EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - - imukit.init(); - - interface::LSM6DSOX::SensorData imu_data - { - {0, 0, 0}, { 0, 0, 0 } - }; - - callback(imu_data); - - auto [pitch, roll, yaw] = imukit.getAngles(); - - for (auto i = 0; i < 100; ++i) { - auto [pitch, roll, yaw] = imukit.getAngles(); - EXPECT_EQ(pitch, 0); - EXPECT_EQ(roll, 0); - EXPECT_EQ(yaw, 180); - } -} - -TEST_F(IMUKitTest, defaultPosition) -{ - std::function callback {}; - - EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - - imukit.init(); - - interface::LSM6DSOX::SensorData imu_data - { - {0, 0, 1000}, { 0, 0, 0 } - }; - - callback(imu_data); - - auto [pitch, roll, yaw] = imukit.getAngles(); - - for (auto i = 0; i < 100; ++i) { - auto [pitch, roll, yaw] = imukit.getAngles(); - EXPECT_EQ(pitch, 0); - EXPECT_EQ(roll, 0); - EXPECT_EQ(yaw, 180); - } -} - -// TODO (@ladislas, @hugo) Go further with numeric testing when new fusion is implemented - -// TEST_F(IMUKitTest, robotRolled90DegreesOnTheTop) -// { -// std::function callback {}; - -// EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - -// imukit.init(); - -// interface::LSM6DSOX::SensorData imu_data -// { -// {1000, 0, 0}, { 0, 0, 0 } -// }; - -// callback(imu_data); - -// auto [pitch, roll, yaw] = imukit.getAngles(); - -// for (auto i = 0; i < 100; ++i) { -// auto [pitch, roll, yaw] = imukit.getAngles(); -// EXPECT_TRUE(-100 < pitch && pitch < -80); -// EXPECT_EQ(roll, 0); -// EXPECT_EQ(yaw, 180); -// } -// } - -// TEST_F(IMUKitTest, robotRolled90DegreesOnTheBottom) -// { -// std::function callback {}; - -// EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - -// imukit.init(); - -// interface::LSM6DSOX::SensorData imu_data -// { -// {-1000, 0, 0}, { 0, 0, 0 } -// }; - -// callback(imu_data); - -// auto [pitch, roll, yaw] = imukit.getAngles(); - -// for (auto i = 0; i < 100; ++i) { -// auto [pitch, roll, yaw] = imukit.getAngles(); -// EXPECT_TRUE(80 < pitch && pitch < 100); -// EXPECT_EQ(roll, 0); -// EXPECT_EQ(yaw, 180); -// } -// } - -// TEST_F(IMUKitTest, robotRolled90DegreesOnItsLeft) -// { -// std::function callback {}; - -// EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - -// imukit.init(); - -// interface::LSM6DSOX::SensorData imu_data -// { -// {0, 1000, 0}, { 0, 0, 0 } -// }; - -// callback(imu_data); - -// auto [pitch, roll, yaw] = imukit.getAngles(); - -// for (auto i = 0; i < 100; ++i) { -// auto [pitch, roll, yaw] = imukit.getAngles(); -// EXPECT_EQ(pitch, 0); -// EXPECT_TRUE(80 < roll && roll < 100); -// EXPECT_EQ(yaw, 180); -// } -// } - -// TEST_F(IMUKitTest, robotRolled90DegreesOnItsRight) -// { -// std::function callback {}; - -// EXPECT_CALL(mock_lsm6dox, registerOnGyDataReadyCallback).WillOnce(SaveArg<0>(&callback)); - -// imukit.init(); - -// interface::LSM6DSOX::SensorData imu_data -// { -// {0, -1000, 0}, { 0, 0, 0 } -// }; - -// callback(imu_data); - -// auto [pitch, roll, yaw] = imukit.getAngles(); - -// for (auto i = 0; i < 100; ++i) { -// auto [pitch, roll, yaw] = imukit.getAngles(); -// EXPECT_EQ(pitch, 0); -// EXPECT_TRUE(-100 < roll && roll < -80); -// EXPECT_EQ(yaw, 180); -// } -// } From 2006879613c09b89afbe105f104dee22ebc845ac Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Mon, 6 Feb 2023 16:59:11 +0100 Subject: [PATCH 102/143] :truck: (IMUKit): Move Fusion lib from spike to lib external dir --- libs/IMUKit/CMakeLists.txt | 2 ++ libs/IMUKit/external/CMakeLists.txt | 21 +++++++++++++++++++ .../IMUKit/external}/fusion/Fusion.h | 0 .../IMUKit/external}/fusion/FusionAhrs.c | 0 .../IMUKit/external}/fusion/FusionAhrs.h | 0 .../IMUKit/external}/fusion/FusionAxes.h | 0 .../external}/fusion/FusionCalibration.h | 0 .../IMUKit/external}/fusion/FusionCompass.c | 0 .../IMUKit/external}/fusion/FusionCompass.h | 0 .../IMUKit/external}/fusion/FusionMath.h | 0 .../IMUKit/external}/fusion/FusionOffset.c | 0 .../IMUKit/external}/fusion/FusionOffset.h | 0 .../CMakeLists.txt | 2 -- .../fusion/CMakeLists.txt | 11 ---------- 14 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 libs/IMUKit/external/CMakeLists.txt rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/Fusion.h (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionAhrs.c (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionAhrs.h (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionAxes.h (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionCalibration.h (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionCompass.c (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionCompass.h (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionMath.h (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionOffset.c (100%) rename {spikes/lk_sensors_imu_lsm6dsox_fusion_calibration => libs/IMUKit/external}/fusion/FusionOffset.h (100%) delete mode 100644 spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/CMakeLists.txt diff --git a/libs/IMUKit/CMakeLists.txt b/libs/IMUKit/CMakeLists.txt index d70e967c70..313036b4bd 100644 --- a/libs/IMUKit/CMakeLists.txt +++ b/libs/IMUKit/CMakeLists.txt @@ -2,6 +2,8 @@ # Copyright 2022 APF France handicap # SPDX-License-Identifier: Apache-2.0 +add_subdirectory(external) + add_library(IMUKit STATIC) target_include_directories(IMUKit diff --git a/libs/IMUKit/external/CMakeLists.txt b/libs/IMUKit/external/CMakeLists.txt new file mode 100644 index 0000000000..5deb6a333a --- /dev/null +++ b/libs/IMUKit/external/CMakeLists.txt @@ -0,0 +1,21 @@ +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +add_library(Fusion STATIC) + +target_include_directories(Fusion + PUBLIC + . +) + +target_sources(Fusion + PRIVATE + fusion/FusionAhrs.c + fusion/FusionCompass.c + fusion/FusionOffset.c +) + +if(UNIX AND NOT APPLE) + target_link_libraries(Fusion m) # link math library for Linux +endif() diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/Fusion.h b/libs/IMUKit/external/fusion/Fusion.h similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/Fusion.h rename to libs/IMUKit/external/fusion/Fusion.h diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.c b/libs/IMUKit/external/fusion/FusionAhrs.c similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.c rename to libs/IMUKit/external/fusion/FusionAhrs.c diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.h b/libs/IMUKit/external/fusion/FusionAhrs.h similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAhrs.h rename to libs/IMUKit/external/fusion/FusionAhrs.h diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAxes.h b/libs/IMUKit/external/fusion/FusionAxes.h similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionAxes.h rename to libs/IMUKit/external/fusion/FusionAxes.h diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCalibration.h b/libs/IMUKit/external/fusion/FusionCalibration.h similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCalibration.h rename to libs/IMUKit/external/fusion/FusionCalibration.h diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.c b/libs/IMUKit/external/fusion/FusionCompass.c similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.c rename to libs/IMUKit/external/fusion/FusionCompass.c diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.h b/libs/IMUKit/external/fusion/FusionCompass.h similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionCompass.h rename to libs/IMUKit/external/fusion/FusionCompass.h diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionMath.h b/libs/IMUKit/external/fusion/FusionMath.h similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionMath.h rename to libs/IMUKit/external/fusion/FusionMath.h diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.c b/libs/IMUKit/external/fusion/FusionOffset.c similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.c rename to libs/IMUKit/external/fusion/FusionOffset.c diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.h b/libs/IMUKit/external/fusion/FusionOffset.h similarity index 100% rename from spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/FusionOffset.h rename to libs/IMUKit/external/fusion/FusionOffset.h diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/CMakeLists.txt b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/CMakeLists.txt index ae5593814a..d7ae9518e5 100644 --- a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/CMakeLists.txt +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/CMakeLists.txt @@ -4,8 +4,6 @@ add_mbed_executable(spike_lk_sensors_imu_lsm6dsox_fusion_calibration) -add_subdirectory(fusion) - target_include_directories(spike_lk_sensors_imu_lsm6dsox_fusion_calibration PRIVATE . diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/CMakeLists.txt b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/CMakeLists.txt deleted file mode 100644 index bc410776c5..0000000000 --- a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/fusion/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# ? Note the following code has been heavily inspired from: -# ? https://github.com/xioTechnologies/Fusion/blob/main/Examples/Advanced -# ? For more information, see https://github.com/xioTechnologies/Fusion - -file(GLOB_RECURSE files "*.c") - -add_library(Fusion ${files}) - -if(UNIX AND NOT APPLE) - target_link_libraries(Fusion m) # link math library for Linux -endif() From 7154b27cbfb37b8c7f0d8bfeeed22e56954393d0 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 7 Feb 2023 10:14:10 +0100 Subject: [PATCH 103/143] :recycle: (IMUKit): Rename getAngles to getEulerAngles + return struct --- libs/IMUKit/include/IMUKit.hpp | 12 ++++++++---- libs/IMUKit/source/IMUKit.cpp | 5 ++--- libs/MotionKit/source/MotionKit.cpp | 4 ++-- spikes/lk_imu_kit/main.cpp | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/libs/IMUKit/include/IMUKit.hpp b/libs/IMUKit/include/IMUKit.hpp index 68b7f9be29..41153db935 100644 --- a/libs/IMUKit/include/IMUKit.hpp +++ b/libs/IMUKit/include/IMUKit.hpp @@ -4,13 +4,16 @@ #pragma once -#include -#include - #include "interface/LSM6DSOX.hpp" namespace leka { +struct EulerAngles { + float pitch; + float roll; + float yaw; +}; + class IMUKit { public: @@ -21,10 +24,11 @@ class IMUKit void stop(); void setOrigin(); - auto getAngles() -> std::array; + auto getEulerAngles() -> const EulerAngles &; private: interface::LSM6DSOX &_lsm6dsox; + EulerAngles _euler_angles {}; }; } // namespace leka diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index d4bd854761..25b3ae85c4 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -30,8 +30,7 @@ void IMUKit::setOrigin() // TODO(@ladislas): to implement } -auto IMUKit::getAngles() -> std::array +auto IMUKit::getEulerAngles() -> const EulerAngles & { - // TODO(@ladislas): to implement - return {0.F, 0.F, 0.F}; + return _euler_angles; } diff --git a/libs/MotionKit/source/MotionKit.cpp b/libs/MotionKit/source/MotionKit.cpp index e40a63b0ab..a9d05384ac 100644 --- a/libs/MotionKit/source/MotionKit.cpp +++ b/libs/MotionKit/source/MotionKit.cpp @@ -83,7 +83,7 @@ void MotionKit::run() }; while (must_rotate()) { - auto [current_pitch, current_roll, current_yaw] = _imukit.getAngles(); + auto [current_pitch, current_roll, current_yaw] = _imukit.getEulerAngles(); check_complete_rotations_executed(current_yaw); @@ -95,7 +95,7 @@ void MotionKit::run() _rotations_to_execute = 0; while (_stabilisation_requested || _target_not_reached) { - auto [pitch, roll, yaw] = _imukit.getAngles(); + auto [pitch, roll, yaw] = _imukit.getEulerAngles(); auto [speed, rotation] = _pid.processPID(pitch, roll, yaw); executeSpeed(speed, rotation); diff --git a/spikes/lk_imu_kit/main.cpp b/spikes/lk_imu_kit/main.cpp index 6d38341829..7c4427b85a 100644 --- a/spikes/lk_imu_kit/main.cpp +++ b/spikes/lk_imu_kit/main.cpp @@ -45,7 +45,7 @@ auto main() -> int imukit.start(); while (true) { - auto [pitch, roll, yaw] = imukit.getAngles(); + auto [pitch, roll, yaw] = imukit.getEulerAngles(); log_info("Pitch : %7.2f, Roll : %7.2f Yaw : %7.2f", pitch, roll, yaw); rtos::ThisThread::sleep_for(140ms); From caa341c7d497d32753fd8db91b94b26d1defe32b Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 7 Feb 2023 14:21:26 +0100 Subject: [PATCH 104/143] :sparkles: (IMUKit): Implement xioTechnologies/Fusion to get Euler angles --- libs/IMUKit/CMakeLists.txt | 1 + libs/IMUKit/include/IMUKit.hpp | 2 + libs/IMUKit/source/IMUKit.cpp | 83 ++++++++++++++++++++++++++++++++-- spikes/lk_imu_kit/main.cpp | 2 + 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/libs/IMUKit/CMakeLists.txt b/libs/IMUKit/CMakeLists.txt index 313036b4bd..5cc9a3bdd0 100644 --- a/libs/IMUKit/CMakeLists.txt +++ b/libs/IMUKit/CMakeLists.txt @@ -18,6 +18,7 @@ target_sources(IMUKit target_link_libraries(IMUKit CoreIMU + Fusion ) if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") diff --git a/libs/IMUKit/include/IMUKit.hpp b/libs/IMUKit/include/IMUKit.hpp index 41153db935..65238e6c3a 100644 --- a/libs/IMUKit/include/IMUKit.hpp +++ b/libs/IMUKit/include/IMUKit.hpp @@ -27,6 +27,8 @@ class IMUKit auto getEulerAngles() -> const EulerAngles &; private: + void drdy_callback(const interface::LSM6DSOX::SensorData &data); + interface::LSM6DSOX &_lsm6dsox; EulerAngles _euler_angles {}; }; diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index 25b3ae85c4..4884316272 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -4,13 +4,58 @@ #include "IMUKit.hpp" +#include "rtos/Kernel.h" + +#include "fusion/Fusion.h" + +// ? Note the following code has been heavily inspired from: +// ? https://github.com/xioTechnologies/Fusion/blob/main/Examples/Advanced/main.c +// ? For more information, see https://github.com/xioTechnologies/Fusion + +using namespace std::chrono; using namespace leka; +namespace fusion { + +constexpr auto kODR_HZ = int {52}; + +static auto ahrs = FusionAhrs {}; + +const FusionAhrsSettings settings = { + .gain = 0.5F, + .accelerationRejection = 10.0F, + .magneticRejection = 0.0F, + .rejectionTimeout = static_cast(5 * kODR_HZ), // # of samples in 5 seconds +}; + +// ? Define calibration offsets +// ? Data: https://www.dropbox.com/scl/fi/cue7qpb77892rozyjbmcq/2022_01_16-IMU-Calibration_Data.xlsx +constexpr auto gyroscope_offset = FusionVector {{0.02544522554F, -0.3286247803F, 0.3205770357F}}; +constexpr auto accelerometer_offset = FusionVector {{0.006480437024F, -0.01962820621F, 0.003259031049F}}; + +auto timestamp_now = rtos::Kernel::Clock::now(); +auto timestamp_previous = rtos::Kernel::Clock::now(); + +auto gyroscope = FusionVector {}; +auto accelerometer = FusionVector {}; + +auto global_offset = FusionOffset {}; + +constexpr auto CALIBRATION = bool {true}; + +} // namespace fusion + void IMUKit::init() { - auto on_drdy_callback = [this](const interface::LSM6DSOX::SensorData &data) { - // TODO(@ladislas): to implement - }; + // ? Initialise algorithms + FusionAhrsInitialise(&fusion::ahrs); + + if constexpr (fusion::CALIBRATION) { + FusionAhrsSetSettings(&fusion::ahrs, &fusion::settings); + FusionOffsetInitialise(&fusion::global_offset, fusion::kODR_HZ); + } + + auto on_drdy_callback = [this](const interface::LSM6DSOX::SensorData &data) { drdy_callback(data); }; _lsm6dsox.registerOnGyDataReadyCallback(on_drdy_callback); } @@ -34,3 +79,35 @@ auto IMUKit::getEulerAngles() -> const EulerAngles & { return _euler_angles; } + +void IMUKit::drdy_callback(const interface::LSM6DSOX::SensorData &data) +{ + // ? Note: For a detailed explanation on the code below, checkout + // ? https://github.com/leka/LekaOS/tree/develop/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration + + fusion::timestamp_now = rtos::Kernel::Clock::now(); + + fusion::gyroscope = FusionVector {{data.gy.x, data.gy.y, data.gy.z}}; + fusion::accelerometer = FusionVector {{data.xl.x, data.xl.y, data.xl.z}}; + + if constexpr (fusion::CALIBRATION) { + fusion::gyroscope = FusionCalibrationInertial(fusion::gyroscope, FUSION_IDENTITY_MATRIX, FUSION_VECTOR_ONES, + fusion::gyroscope_offset); + fusion::accelerometer = FusionCalibrationInertial(fusion::accelerometer, FUSION_IDENTITY_MATRIX, + FUSION_VECTOR_ONES, fusion::accelerometer_offset); + + fusion::gyroscope = FusionOffsetUpdate(&fusion::global_offset, fusion::gyroscope); + } + + auto delta_time = static_cast((fusion::timestamp_now - fusion::timestamp_previous).count()) / 1000.F; + fusion::timestamp_previous = fusion::timestamp_now; + + FusionAhrsUpdateNoMagnetometer(&fusion::ahrs, fusion::gyroscope, fusion::accelerometer, delta_time); + + const auto euler = FusionQuaternionToEuler(FusionAhrsGetQuaternion(&fusion::ahrs)); + _euler_angles = { + .pitch = euler.angle.pitch, + .roll = euler.angle.roll, + .yaw = euler.angle.yaw, + }; +}; diff --git a/spikes/lk_imu_kit/main.cpp b/spikes/lk_imu_kit/main.cpp index 7c4427b85a..8707125b50 100644 --- a/spikes/lk_imu_kit/main.cpp +++ b/spikes/lk_imu_kit/main.cpp @@ -34,6 +34,7 @@ IMUKit imukit(imu::lsm6dsox); auto main() -> int { + rtos::ThisThread::sleep_for(140ms); logger::init(); HelloWorld hello; @@ -41,6 +42,7 @@ auto main() -> int imu::lsm6dsox.init(); + imukit.stop(); imukit.init(); imukit.start(); From 20cde9d038589955149784a85b1cb39615f83065 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 8 Feb 2023 10:26:45 +0100 Subject: [PATCH 105/143] :bulb: (IMUKit): Add comment about setOrigin implementation --- libs/IMUKit/source/IMUKit.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index 4884316272..c21d25653d 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -73,6 +73,9 @@ void IMUKit::stop() void IMUKit::setOrigin() { // TODO(@ladislas): to implement + // ? Reseting the algorithm might not be the best answer as it takes a few second to stabilize + // ? Because the readings are very stable, it would be easier to take the current yaw + // ? and start counting from there } auto IMUKit::getEulerAngles() -> const EulerAngles & From 2dd12c9afc941097773aaa59051e7aca7f10f271 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 8 Feb 2023 12:02:04 +0100 Subject: [PATCH 106/143] :clown_face: (LSM6DSOX): Update mock to be able to call on drdy callback --- libs/MotionKit/tests/MotionKit_test.cpp | 2 -- tests/unit/mocks/mocks/leka/LSM6DSOX.h | 11 ++++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/MotionKit/tests/MotionKit_test.cpp b/libs/MotionKit/tests/MotionKit_test.cpp index 33a2e787a4..878cb65299 100644 --- a/libs/MotionKit/tests/MotionKit_test.cpp +++ b/libs/MotionKit/tests/MotionKit_test.cpp @@ -29,8 +29,6 @@ class MotionKitTest : public ::testing::Test void SetUp() override { - EXPECT_CALL(lsm6dsox, registerOnGyDataReadyCallback).Times(1); - imukit.init(); motion.init(); } diff --git a/tests/unit/mocks/mocks/leka/LSM6DSOX.h b/tests/unit/mocks/mocks/leka/LSM6DSOX.h index a06ea5cea0..09d614eba6 100644 --- a/tests/unit/mocks/mocks/leka/LSM6DSOX.h +++ b/tests/unit/mocks/mocks/leka/LSM6DSOX.h @@ -13,8 +13,17 @@ class LSM6DSOX : public interface::LSM6DSOX { public: MOCK_METHOD(void, init, (), (override)); - MOCK_METHOD(void, registerOnGyDataReadyCallback, (std::function const &), (override)); MOCK_METHOD(void, setPowerMode, (PowerMode), (override)); + + void registerOnGyDataReadyCallback(std::function const &cb) override + { + drdy_callback = cb; + } + + void call_drdy_callback(const SensorData &data) { drdy_callback(data); } + + private: + std::function drdy_callback {}; }; } // namespace leka::mock From 638ee3221189fa82e7f553d329d3486068f53406 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 8 Feb 2023 12:03:05 +0100 Subject: [PATCH 107/143] :white_check_mark: (IMUKit): Add unit tests --- libs/IMUKit/tests/IMUKit_test.cpp | 45 ++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/libs/IMUKit/tests/IMUKit_test.cpp b/libs/IMUKit/tests/IMUKit_test.cpp index 9937c236bb..a5e399382f 100644 --- a/libs/IMUKit/tests/IMUKit_test.cpp +++ b/libs/IMUKit/tests/IMUKit_test.cpp @@ -12,11 +12,10 @@ #include "mocks/leka/LSM6DSOX.h" #include "stubs/leka/EventLoopKit.h" #include "stubs/mbed/InterruptIn.h" +#include "stubs/mbed/Kernel.h" using namespace leka; -using ::testing::_; -using ::testing::AtLeast; -using ::testing::SaveArg; +using namespace std::chrono; class IMUKitTest : public ::testing::Test { @@ -49,3 +48,43 @@ TEST_F(IMUKitTest, stop) imukit.stop(); } + +TEST_F(IMUKitTest, getEulerAngles) +{ + auto [pitch, roll, yaw] = imukit.getEulerAngles(); + + EXPECT_EQ(pitch, 0); + EXPECT_EQ(roll, 0); + EXPECT_EQ(yaw, 0); +} + +TEST_F(IMUKitTest, setOrigin) +{ + // TODO(@ladislas): add tests + imukit.setOrigin(); +} + +TEST_F(IMUKitTest, onDataReady) +{ + const auto data_initial = interface::LSM6DSOX::SensorData { + .xl = {.x = 0.F, .y = 0.F, .z = 0.F}, .gy = {.x = 0.F, .y = 0.F, .z = 0.F } + }; + + mock_lsm6dox.call_drdy_callback(data_initial); + + const auto angles_initial = imukit.getEulerAngles(); + + spy_kernel_addElapsedTimeToTickCount(80ms); + + const auto data_updated = interface::LSM6DSOX::SensorData { + .xl = {.x = 1.F, .y = 2.F, .z = 3.F}, .gy = {.x = 1.F, .y = 2.F, .z = 3.F } + }; + + mock_lsm6dox.call_drdy_callback(data_updated); + + auto angles_updated = imukit.getEulerAngles(); + + EXPECT_NE(angles_initial.pitch, angles_updated.pitch); + EXPECT_NE(angles_initial.roll, angles_updated.roll); + EXPECT_NE(angles_initial.yaw, angles_updated.yaw); +} From e8253ad23bcecadf84099fb998cd3421b1b66fe2 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 8 Feb 2023 12:13:41 +0100 Subject: [PATCH 108/143] :recycle: (IMUKit): Make getEulerAngles const --- libs/IMUKit/include/IMUKit.hpp | 2 +- libs/IMUKit/source/IMUKit.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/IMUKit/include/IMUKit.hpp b/libs/IMUKit/include/IMUKit.hpp index 65238e6c3a..72d113dba1 100644 --- a/libs/IMUKit/include/IMUKit.hpp +++ b/libs/IMUKit/include/IMUKit.hpp @@ -24,7 +24,7 @@ class IMUKit void stop(); void setOrigin(); - auto getEulerAngles() -> const EulerAngles &; + [[nodiscard]] auto getEulerAngles() const -> const EulerAngles &; private: void drdy_callback(const interface::LSM6DSOX::SensorData &data); diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index c21d25653d..d96f15fb36 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -78,7 +78,7 @@ void IMUKit::setOrigin() // ? and start counting from there } -auto IMUKit::getEulerAngles() -> const EulerAngles & +auto IMUKit::getEulerAngles() const -> const EulerAngles & { return _euler_angles; } From 5e35b6dcd9a4ed542e7566a7fe55d00f682638e8 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 8 Feb 2023 13:51:48 +0100 Subject: [PATCH 109/143] :alembic: (spikes): IMUKit - Get angles once and rely on values being references --- spikes/lk_imu_kit/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spikes/lk_imu_kit/main.cpp b/spikes/lk_imu_kit/main.cpp index 8707125b50..8b2cedc7e6 100644 --- a/spikes/lk_imu_kit/main.cpp +++ b/spikes/lk_imu_kit/main.cpp @@ -46,10 +46,10 @@ auto main() -> int imukit.init(); imukit.start(); + const auto &[pitch, roll, yaw] = imukit.getEulerAngles(); + while (true) { - auto [pitch, roll, yaw] = imukit.getEulerAngles(); log_info("Pitch : %7.2f, Roll : %7.2f Yaw : %7.2f", pitch, roll, yaw); - rtos::ThisThread::sleep_for(140ms); } } From c63bf24f52cfd01fb3de3bfb19432292e9bfed8e Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Thu, 9 Feb 2023 00:37:24 +0100 Subject: [PATCH 110/143] :recycle: (LSM6DSOX): Force drdy_callback_t to pass by value vs reference --- drivers/CoreIMU/include/interface/LSM6DSOX.hpp | 2 +- tests/unit/mocks/mocks/leka/LSM6DSOX.h | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/drivers/CoreIMU/include/interface/LSM6DSOX.hpp b/drivers/CoreIMU/include/interface/LSM6DSOX.hpp index c408568944..5440e15fd3 100644 --- a/drivers/CoreIMU/include/interface/LSM6DSOX.hpp +++ b/drivers/CoreIMU/include/interface/LSM6DSOX.hpp @@ -39,7 +39,7 @@ class LSM6DSOX Gyroscope gy = {0, 0, 0}; }; - using drdy_callback_t = std::function; + using drdy_callback_t = std::function; virtual void init() = 0; virtual void registerOnGyDataReadyCallback(drdy_callback_t const &callback) = 0; diff --git a/tests/unit/mocks/mocks/leka/LSM6DSOX.h b/tests/unit/mocks/mocks/leka/LSM6DSOX.h index 09d614eba6..5d0a21e80f 100644 --- a/tests/unit/mocks/mocks/leka/LSM6DSOX.h +++ b/tests/unit/mocks/mocks/leka/LSM6DSOX.h @@ -15,15 +15,12 @@ class LSM6DSOX : public interface::LSM6DSOX MOCK_METHOD(void, init, (), (override)); MOCK_METHOD(void, setPowerMode, (PowerMode), (override)); - void registerOnGyDataReadyCallback(std::function const &cb) override - { - drdy_callback = cb; - } + void registerOnGyDataReadyCallback(drdy_callback_t const &cb) override { drdy_callback = cb; } void call_drdy_callback(const SensorData &data) { drdy_callback(data); } private: - std::function drdy_callback {}; + drdy_callback_t drdy_callback {}; }; } // namespace leka::mock From a735c118954f94c8aa202e6eabb0f9dd256731fd Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Thu, 9 Feb 2023 00:43:55 +0100 Subject: [PATCH 111/143] :recycle: (IMUKit): Pass and return sensor data by value --- libs/IMUKit/include/IMUKit.hpp | 4 ++-- libs/IMUKit/source/IMUKit.cpp | 4 ++-- spikes/lk_imu_kit/main.cpp | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libs/IMUKit/include/IMUKit.hpp b/libs/IMUKit/include/IMUKit.hpp index 72d113dba1..44dc217529 100644 --- a/libs/IMUKit/include/IMUKit.hpp +++ b/libs/IMUKit/include/IMUKit.hpp @@ -24,10 +24,10 @@ class IMUKit void stop(); void setOrigin(); - [[nodiscard]] auto getEulerAngles() const -> const EulerAngles &; + [[nodiscard]] auto getEulerAngles() const -> EulerAngles; private: - void drdy_callback(const interface::LSM6DSOX::SensorData &data); + void drdy_callback(interface::LSM6DSOX::SensorData data); interface::LSM6DSOX &_lsm6dsox; EulerAngles _euler_angles {}; diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index d96f15fb36..c1e2f5a3e8 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -78,12 +78,12 @@ void IMUKit::setOrigin() // ? and start counting from there } -auto IMUKit::getEulerAngles() const -> const EulerAngles & +auto IMUKit::getEulerAngles() const -> EulerAngles { return _euler_angles; } -void IMUKit::drdy_callback(const interface::LSM6DSOX::SensorData &data) +void IMUKit::drdy_callback(const interface::LSM6DSOX::SensorData data) { // ? Note: For a detailed explanation on the code below, checkout // ? https://github.com/leka/LekaOS/tree/develop/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration diff --git a/spikes/lk_imu_kit/main.cpp b/spikes/lk_imu_kit/main.cpp index 8b2cedc7e6..c93230c6ba 100644 --- a/spikes/lk_imu_kit/main.cpp +++ b/spikes/lk_imu_kit/main.cpp @@ -46,9 +46,8 @@ auto main() -> int imukit.init(); imukit.start(); - const auto &[pitch, roll, yaw] = imukit.getEulerAngles(); - while (true) { + const auto [pitch, roll, yaw] = imukit.getEulerAngles(); log_info("Pitch : %7.2f, Roll : %7.2f Yaw : %7.2f", pitch, roll, yaw); rtos::ThisThread::sleep_for(140ms); } From 65b8aaa36e1ef9c3225a484fae5aa18c3c6bc5fe Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 1 Feb 2023 11:51:25 +0100 Subject: [PATCH 112/143] :sparkles: (filemanager): Add is_missing and exists methods for file --- libs/FileManagerKit/include/FileManagerKit.h | 2 + libs/FileManagerKit/source/FileManagerKit.cpp | 11 +++++ .../tests/FileManagerKit_test.cpp | 43 +++++++++++++++++++ spikes/lk_file_manager_kit/main.cpp | 8 ++-- .../tests/file_manager/suite_file_manager.cpp | 26 ++++++++--- 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/libs/FileManagerKit/include/FileManagerKit.h b/libs/FileManagerKit/include/FileManagerKit.h index 16027ca668..b519adeddc 100644 --- a/libs/FileManagerKit/include/FileManagerKit.h +++ b/libs/FileManagerKit/include/FileManagerKit.h @@ -65,4 +65,6 @@ struct File : public interface::File, public mbed::NonCopyable { auto create_directory(const std::filesystem::path &path) -> bool; auto remove(const std::filesystem::path &path) -> bool; +auto file_exists(const std::filesystem::path &path) -> bool; +auto file_is_missing(const std::filesystem::path &path) -> bool; } // namespace leka::FileManagerKit diff --git a/libs/FileManagerKit/source/FileManagerKit.cpp b/libs/FileManagerKit/source/FileManagerKit.cpp index cb3785b349..83819e3ef3 100644 --- a/libs/FileManagerKit/source/FileManagerKit.cpp +++ b/libs/FileManagerKit/source/FileManagerKit.cpp @@ -20,3 +20,14 @@ auto FileManagerKit::remove(const std::filesystem::path &path) -> bool { return std::filesystem::remove(path); } + +auto FileManagerKit::file_exists(const std::filesystem::path &path) -> bool +{ + auto file = FileManagerKit::File {path, "r"}; + return file.is_open(); +} + +auto FileManagerKit::file_is_missing(const std::filesystem::path &path) -> bool +{ + return !FileManagerKit::file_exists(path); +} diff --git a/libs/FileManagerKit/tests/FileManagerKit_test.cpp b/libs/FileManagerKit/tests/FileManagerKit_test.cpp index 98ed410ec0..936503c005 100644 --- a/libs/FileManagerKit/tests/FileManagerKit_test.cpp +++ b/libs/FileManagerKit/tests/FileManagerKit_test.cpp @@ -18,14 +18,19 @@ class FileSystemTest : public ::testing::Test void spy_create_directory(const std::filesystem::path &path) { std::filesystem::create_directories(path); } void spy_remove_directory(const std::filesystem::path &path) { std::filesystem::remove_all(path); } + void spy_create_file(const std::filesystem::path &path) { std::fopen(path.c_str(), "w"); } + void spy_remove_file(const std::filesystem::path &path) { std::filesystem::remove_all(path); } + void spy_remove_all_directories() { + spy_remove_file(path_file); spy_remove_directory(path_sub_directory); spy_remove_directory(path_directory); } const std::filesystem::path path_directory = std::filesystem::temp_directory_path() / "ABC"; const std::filesystem::path path_sub_directory = std::filesystem::temp_directory_path() / "ABC/DEF"; + const std::filesystem::path path_file = std::filesystem::temp_directory_path() / "ABC/any_file.txt"; }; TEST_F(FileSystemTest, createDirectory) @@ -72,3 +77,41 @@ TEST_F(FileSystemTest, removeNotExistingDirectory) EXPECT_FALSE(is_removed); } + +TEST_F(FileSystemTest, fileExistsTrue) +{ + spy_create_directory(path_directory); + spy_create_file(path_file); + + auto does_exist = FileManagerKit::file_exists(path_file); + + EXPECT_TRUE(does_exist); +} + +TEST_F(FileSystemTest, fileIsMissingFalse) +{ + spy_create_directory(path_directory); + spy_create_file(path_file); + + auto is_missing = FileManagerKit::file_is_missing(path_file); + + EXPECT_FALSE(is_missing); +} + +TEST_F(FileSystemTest, fileExistsFalse) +{ + spy_remove_file(path_file); + + auto does_exist = FileManagerKit::file_exists(path_file); + + EXPECT_FALSE(does_exist); +} + +TEST_F(FileSystemTest, fileIsMissingTrue) +{ + spy_remove_file(path_file); + + auto is_missing = FileManagerKit::file_is_missing(path_file); + + EXPECT_TRUE(is_missing); +} diff --git a/spikes/lk_file_manager_kit/main.cpp b/spikes/lk_file_manager_kit/main.cpp index 012ff436f4..83b8fe68f5 100644 --- a/spikes/lk_file_manager_kit/main.cpp +++ b/spikes/lk_file_manager_kit/main.cpp @@ -70,9 +70,11 @@ auto main() -> int rtos::ThisThread::sleep_for(1s); - file.open(file_for_sha256_path); - auto sha256 = file.getSHA256(); - printSHA256(sha256); + if (FileManagerKit::file_exists(file_for_sha256_path)) { + file.open(file_for_sha256_path); + auto sha256 = file.getSHA256(); + printSHA256(sha256); + } while (true) { auto t = rtos::Kernel::Clock::now() - start; diff --git a/tests/functional/tests/file_manager/suite_file_manager.cpp b/tests/functional/tests/file_manager/suite_file_manager.cpp index d41e71a4b2..980a2f34ea 100644 --- a/tests/functional/tests/file_manager/suite_file_manager.cpp +++ b/tests/functional/tests/file_manager/suite_file_manager.cpp @@ -3,10 +3,10 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "FileManagerKit.h" #include "boost/ut.hpp" -#include "filesystem" #include "tests/config.h" #include "tests/utils.h" @@ -27,13 +27,13 @@ struct path { log << ""; log << "Cleaning up files, directories"; - for (const auto &p: path::all) { - if (std::filesystem::exists(p)) { - log << "Removing:" << p; - std::filesystem::remove(p); - expect(not std::filesystem::exists(p)) << p << "still exists"; + for (const auto &path: path::all) { + if (std::filesystem::exists(path)) { + log << "Removing:" << path; + std::filesystem::remove(path); + expect(not std::filesystem::exists(path)) << path << "still exists"; } else { - log << "Doesn't exit:" << p; + log << "Doesn't exit:" << path; } } log << ""; @@ -89,6 +89,12 @@ suite suite_file_manager_kit = [] { expect(not file.is_open()) << "Failed to close file"; }; + "new file exists"_test = [&] { + auto exists = FileManagerKit::file_exists(path::dir_file); + + expect(exists >> fatal) << "Failed to check the existance of file"; + }; + "write to new file"_test = [&] { file.open(path::dir_file, "w"); auto bytes = file.write(input_data); @@ -117,4 +123,10 @@ suite suite_file_manager_kit = [] { }; "clean up created files"_test = [] { path::remove_all(); }; + + "file does not exist"_test = [&] { + auto dir_file_is_missing = FileManagerKit::file_is_missing(path::dir_file); + + expect(dir_file_is_missing >> fatal) << "Failed to check the non existance of file"; + }; }; From 1a6db9dd9b122bd5d1016eb1c9f645bb7ecad212 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Thu, 2 Feb 2023 15:42:49 +0100 Subject: [PATCH 113/143] :recycle: (filemanager): Use file_exists and file_is_missing --- app/bootloader/main.cpp | 6 +- libs/ConfigKit/include/ConfigKit.h | 18 +++--- libs/FirmwareKit/source/FirmwareKit.cpp | 41 +++++++------ libs/VideoKit/source/VideoKit.cpp | 82 ++++++++++++++----------- spikes/lk_audio/main.cpp | 11 ++-- 5 files changed, 88 insertions(+), 70 deletions(-) diff --git a/app/bootloader/main.cpp b/app/bootloader/main.cpp index 31a9d15630..2c901b2909 100644 --- a/app/bootloader/main.cpp +++ b/app/bootloader/main.cpp @@ -112,12 +112,12 @@ namespace factory_reset { auto getCounter() -> uint8_t { - FileManagerKit::File file {internal::factory_reset_counter_path, "r"}; - - if (!file.is_open()) { + if (FileManagerKit::file_is_missing(internal::factory_reset_counter_path)) { return default_limit + 1; } + auto file = FileManagerKit::File {internal::factory_reset_counter_path, "r"}; + auto data = std::array {}; file.read(data); diff --git a/libs/ConfigKit/include/ConfigKit.h b/libs/ConfigKit/include/ConfigKit.h index 0140632898..084bcccc23 100644 --- a/libs/ConfigKit/include/ConfigKit.h +++ b/libs/ConfigKit/include/ConfigKit.h @@ -18,21 +18,23 @@ class ConfigKit template [[nodiscard]] auto read(Config const &config) const { - if (FileManagerKit::File file {config.path(), "r"}; file.is_open()) { - auto data = std::array {}; - file.read(data); - + if (FileManagerKit::file_is_missing(config.path())) { if constexpr (SIZE == 1) { - return data[0]; + return config.default_value()[0]; } else { - return data; + return config.default_value(); } } + auto file = FileManagerKit::File {config.path(), "r"}; + + auto data = std::array {}; + file.read(data); + if constexpr (SIZE == 1) { - return config.default_value()[0]; + return data[0]; } else { - return config.default_value(); + return data; } } diff --git a/libs/FirmwareKit/source/FirmwareKit.cpp b/libs/FirmwareKit/source/FirmwareKit.cpp index a71c374736..52184904f0 100644 --- a/libs/FirmwareKit/source/FirmwareKit.cpp +++ b/libs/FirmwareKit/source/FirmwareKit.cpp @@ -28,18 +28,20 @@ auto FirmwareKit::getPathOfVersion(const Version &version) const -> std::filesys auto FirmwareKit::isVersionAvailable(const Version &version) -> bool { - auto path = getPathOfVersion(version); - auto file_exists = false; - - if (auto is_open = _file.open(path); is_open) { - constexpr auto kMinimalFileSizeInBytes = std::size_t {300'000}; + auto path = getPathOfVersion(version); - file_exists = _file.size() >= kMinimalFileSizeInBytes; + if (FileManagerKit::file_is_missing(path)) { + return false; } + _file.open(path); + + constexpr auto kMinimalFileSizeInBytes = std::size_t {300'000}; + auto file_size_is_correct = _file.size() >= kMinimalFileSizeInBytes; + _file.close(); - return file_exists; + return file_size_is_correct; } auto FirmwareKit::loadFirmware(const Version &version) -> bool @@ -67,20 +69,23 @@ auto FirmwareKit::loadFactoryFirmware() -> bool auto FirmwareKit::load(const std::filesystem::path &path) -> bool { - if (auto is_open = _file.open(path); is_open) { - auto address = uint32_t {0x0}; - auto buffer = std::array {}; + if (FileManagerKit::file_is_missing(path)) { + return false; + } + + _file.open(path); - _flash.erase(); + auto address = uint32_t {0x0}; + auto buffer = std::array {}; - while (auto bytes_read = _file.read(buffer.data(), std::size(buffer))) { - _flash.write(address, buffer, bytes_read); - address += bytes_read; - } + _flash.erase(); - _file.close(); - return true; + while (auto bytes_read = _file.read(buffer.data(), std::size(buffer))) { + _flash.write(address, buffer, bytes_read); + address += bytes_read; } - return false; + _file.close(); + + return true; } diff --git a/libs/VideoKit/source/VideoKit.cpp b/libs/VideoKit/source/VideoKit.cpp index 13f0add6a9..985176613a 100644 --- a/libs/VideoKit/source/VideoKit.cpp +++ b/libs/VideoKit/source/VideoKit.cpp @@ -37,18 +37,22 @@ void VideoKit::displayImage(const std::filesystem::path &path) return; } - if (auto file = FileManagerKit::File {path}; file.is_open()) { - _must_stop = true; - _event_loop.stop(); + if (FileManagerKit::file_is_missing(path)) { + return; + } + + auto file = FileManagerKit::File {path}; - _current_path = path; + _must_stop = true; + _event_loop.stop(); - rtos::ThisThread::sleep_for(100ms); + _current_path = path; - _video.displayImage(file); + rtos::ThisThread::sleep_for(100ms); - file.close(); - } + _video.displayImage(file); + + file.close(); } void VideoKit::fillWhiteBackgroundAndDisplayImage(const std::filesystem::path &path) @@ -59,39 +63,43 @@ void VideoKit::fillWhiteBackgroundAndDisplayImage(const std::filesystem::path &p return; } - if (auto file = FileManagerKit::File {path}; file.is_open()) { - _must_stop = true; - _event_loop.stop(); + if (FileManagerKit::file_is_missing(path)) { + return; + } + + auto file = FileManagerKit::File {path}; - _current_path = path; + _must_stop = true; + _event_loop.stop(); - rtos::ThisThread::sleep_for(100ms); + _current_path = path; - _video.clearScreen(); - _video.displayImage(file); + rtos::ThisThread::sleep_for(100ms); - file.close(); - } + _video.clearScreen(); + _video.displayImage(file); + + file.close(); } void VideoKit::playVideoOnce(const std::filesystem::path &path, const std::function &on_video_ended_callback) { const std::scoped_lock lock(mutex); - if (auto file = FileManagerKit::File {path}; file.is_open()) { - file.close(); + if (FileManagerKit::file_is_missing(path)) { + return; + } - _must_stop = true; - _event_loop.stop(); + _must_stop = true; + _event_loop.stop(); - _current_path = path; - _must_loop = false; + _current_path = path; + _must_loop = false; - rtos::ThisThread::sleep_for(100ms); + rtos::ThisThread::sleep_for(100ms); - _on_video_ended_callback = on_video_ended_callback; - _event_loop.start(); - } + _on_video_ended_callback = on_video_ended_callback; + _event_loop.start(); } void VideoKit::playVideoOnRepeat(const std::filesystem::path &path, @@ -99,20 +107,20 @@ void VideoKit::playVideoOnRepeat(const std::filesystem::path &path, { const std::scoped_lock lock(mutex); - if (auto file = FileManagerKit::File {path}; file.is_open()) { - file.close(); + if (FileManagerKit::file_is_missing(path)) { + return; + } - _must_stop = true; - _event_loop.stop(); + _must_stop = true; + _event_loop.stop(); - _current_path = path; - _must_loop = true; + _current_path = path; + _must_loop = true; - rtos::ThisThread::sleep_for(100ms); + rtos::ThisThread::sleep_for(100ms); - _on_video_ended_callback = on_video_ended_callback; - _event_loop.start(); - } + _on_video_ended_callback = on_video_ended_callback; + _event_loop.start(); } void VideoKit::stopVideo() diff --git a/spikes/lk_audio/main.cpp b/spikes/lk_audio/main.cpp index 77eb6ce8a9..3483f70263 100644 --- a/spikes/lk_audio/main.cpp +++ b/spikes/lk_audio/main.cpp @@ -73,11 +73,14 @@ auto main() -> int initializeSD(); + if (FileManagerKit::file_is_missing(sound_file_path)) { + return 1; + } + while (true) { - if (auto is_open = file.open(sound_file_path); is_open) { - playSound(); - file.close(); - } + file.open(sound_file_path); + playSound(); + file.close(); rtos::ThisThread::sleep_for(1s); } From 9cc3074ae958d0e24a72d8a7161db5ceb564a949 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Fri, 10 Feb 2023 00:28:06 +0100 Subject: [PATCH 114/143] :clown_face: (stubs): Add HighResClock + mbed (us) ticker api stubs --- tests/unit/stubs/CMakeLists.txt | 2 ++ tests/unit/stubs/stubs/mbed/HighResClock.h | 9 +++++++++ .../unit/stubs/stubs/mbed/source/mbed_ticker_api.cpp | 11 +++++++++++ .../stubs/stubs/mbed/source/mbed_us_ticker_api.cpp | 12 ++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 tests/unit/stubs/stubs/mbed/HighResClock.h create mode 100644 tests/unit/stubs/stubs/mbed/source/mbed_ticker_api.cpp create mode 100644 tests/unit/stubs/stubs/mbed/source/mbed_us_ticker_api.cpp diff --git a/tests/unit/stubs/CMakeLists.txt b/tests/unit/stubs/CMakeLists.txt index 01859ccd08..5a8f5ae903 100644 --- a/tests/unit/stubs/CMakeLists.txt +++ b/tests/unit/stubs/CMakeLists.txt @@ -30,6 +30,8 @@ target_sources(stubs ${UNIT_TESTS_STUBS_MBED_DIR}/source/Kernel.cpp ${UNIT_TESTS_STUBS_MBED_DIR}/source/mbed_critical.cpp ${UNIT_TESTS_STUBS_MBED_DIR}/source/mbed_power_mgmt.cpp + ${UNIT_TESTS_STUBS_MBED_DIR}/source/mbed_ticker_api.cpp + ${UNIT_TESTS_STUBS_MBED_DIR}/source/mbed_us_ticker_api.cpp ${UNIT_TESTS_STUBS_MBED_DIR}/source/EventQueue_extension.cpp diff --git a/tests/unit/stubs/stubs/mbed/HighResClock.h b/tests/unit/stubs/stubs/mbed/HighResClock.h new file mode 100644 index 0000000000..e23de24298 --- /dev/null +++ b/tests/unit/stubs/stubs/mbed/HighResClock.h @@ -0,0 +1,9 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "drivers/HighResClock.h" + +namespace leka { + +} // namespace leka diff --git a/tests/unit/stubs/stubs/mbed/source/mbed_ticker_api.cpp b/tests/unit/stubs/stubs/mbed/source/mbed_ticker_api.cpp new file mode 100644 index 0000000000..56b82b6e44 --- /dev/null +++ b/tests/unit/stubs/stubs/mbed/source/mbed_ticker_api.cpp @@ -0,0 +1,11 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "../HighResClock.h" + +auto ticker_read_us(const ticker_data_t *const ticker) -> us_timestamp_t +{ + us_timestamp_t ret {}; + return ret; +} diff --git a/tests/unit/stubs/stubs/mbed/source/mbed_us_ticker_api.cpp b/tests/unit/stubs/stubs/mbed/source/mbed_us_ticker_api.cpp new file mode 100644 index 0000000000..0c1707924b --- /dev/null +++ b/tests/unit/stubs/stubs/mbed/source/mbed_us_ticker_api.cpp @@ -0,0 +1,12 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "../HighResClock.h" + +ticker_data_t us_data = {}; + +auto get_us_ticker_data(void) -> const ticker_data_t * +{ + return &us_data; +} From e037c7e2cd03c1526f4ef1f5e45c934e5946a2e7 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 15 Feb 2023 15:39:06 +0100 Subject: [PATCH 115/143] :sparkles: (LSM6DSOX): SensorData - add timestamp (ms) --- drivers/CoreIMU/include/interface/LSM6DSOX.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/CoreIMU/include/interface/LSM6DSOX.hpp b/drivers/CoreIMU/include/interface/LSM6DSOX.hpp index 5440e15fd3..dc1bf18a39 100644 --- a/drivers/CoreIMU/include/interface/LSM6DSOX.hpp +++ b/drivers/CoreIMU/include/interface/LSM6DSOX.hpp @@ -6,6 +6,8 @@ #include +#include "rtos/Kernel.h" + namespace leka::interface { class LSM6DSOX @@ -23,6 +25,8 @@ class LSM6DSOX // TODO(@ladislas) - user types --> move to include/types struct SensorData { + using time_point_t = std::chrono::time_point; + struct Accelerometer { float x = {}; float y = {}; @@ -37,6 +41,8 @@ class LSM6DSOX Accelerometer xl = {0, 0, 0}; Gyroscope gy = {0, 0, 0}; + + time_point_t timestamp {}; }; using drdy_callback_t = std::function; From 6c6ece328ae8eedb67664e6464de75bafa88b662 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 15 Feb 2023 15:49:01 +0100 Subject: [PATCH 116/143] :recycle: (LSM6DSOX): Pass ISR timestamp to drdy callback --- drivers/CoreIMU/include/CoreLSM6DSOX.hpp | 2 +- drivers/CoreIMU/source/CoreLSM6DSOX.cpp | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/drivers/CoreIMU/include/CoreLSM6DSOX.hpp b/drivers/CoreIMU/include/CoreLSM6DSOX.hpp index fc2a022fac..69d38bca71 100644 --- a/drivers/CoreIMU/include/CoreLSM6DSOX.hpp +++ b/drivers/CoreIMU/include/CoreLSM6DSOX.hpp @@ -34,7 +34,7 @@ class CoreLSM6DSOX : public interface::LSM6DSOX static auto ptr_io_read(CoreLSM6DSOX *handle, uint8_t read_address, uint8_t *p_buffer, uint16_t number_bytes_to_read) -> int32_t; - void onGyrDataReadyHandler(); + void onGyrDataReadyHandler(auto timestamp); void setGyrDataReadyInterrupt(); interface::I2C &_i2c; diff --git a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp index 4de839ecae..0d33ce3d3b 100644 --- a/drivers/CoreIMU/source/CoreLSM6DSOX.cpp +++ b/drivers/CoreIMU/source/CoreLSM6DSOX.cpp @@ -76,10 +76,12 @@ void CoreLSM6DSOX::registerOnGyDataReadyCallback(drdy_callback_t const &callback _on_gy_data_ready_callback = callback; } -void CoreLSM6DSOX::onGyrDataReadyHandler() +void CoreLSM6DSOX::onGyrDataReadyHandler(auto timestamp) { static constexpr auto _1k = float {1000.F}; + _sensor_data = SensorData {.timestamp = timestamp}; + lsm6dsox_angular_rate_raw_get(&_register_io_function, data_raw_gy.data()); _sensor_data.gy.x = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(0)) / _1k; _sensor_data.gy.y = lsm6dsox_from_fs500_to_mdps(data_raw_gy.at(1)) / _1k; @@ -142,7 +144,10 @@ void CoreLSM6DSOX::setGyrDataReadyInterrupt() }; lsm6dsox_pin_int1_route_set(&_register_io_function, gyro_int1); - auto gyr_drdy_callback = [this] { _event_queue.call([this] { onGyrDataReadyHandler(); }); }; + auto gyr_drdy_callback = [this] { + auto timestamp = rtos::Kernel::Clock::now(); + _event_queue.call([this, timestamp] { onGyrDataReadyHandler(timestamp); }); + }; _drdy_irq.onRise(gyr_drdy_callback); } From de83e33092c53915239b49d8058a54e51f175b89 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 15 Feb 2023 16:05:09 +0100 Subject: [PATCH 117/143] :recycle: (spikes): IMUKit + LSM6DSOX - Use isr timestamp --- spikes/lk_sensors_imu_lsm6dsox/main.cpp | 13 ++++++++----- .../main.cpp | 17 +++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/spikes/lk_sensors_imu_lsm6dsox/main.cpp b/spikes/lk_sensors_imu_lsm6dsox/main.cpp index d0e5b3218c..554511ee4a 100644 --- a/spikes/lk_sensors_imu_lsm6dsox/main.cpp +++ b/spikes/lk_sensors_imu_lsm6dsox/main.cpp @@ -2,6 +2,8 @@ // Copyright 2022 APF France handicap // SPDX-License-Identifier: Apache-2.0 +#include + #include "rtos/ThisThread.h" #include "CoreI2C.h" @@ -40,12 +42,13 @@ auto main() -> int imu::lsm6dsox.setPowerMode(CoreLSM6DSOX::PowerMode::Off); - auto callback = [](const interface::LSM6DSOX::SensorData &imu_data) { - const auto &[xlx, xly, xlz] = imu_data.xl; - const auto &[gx, gy, gz] = imu_data.gy; + auto callback = [](const interface::LSM6DSOX::SensorData data) { + const auto &[xlx, xly, xlz] = data.xl; + const auto &[gx, gy, gz] = data.gy; + const auto timestamp = data.timestamp.time_since_epoch().count(); - log_debug("xl.x: %7.2f, xl.y: %7.2f, xl.z: %7.2f, gy.x: %7.2f, gy.y: %7.2f, gy.z: %7.2f", xlx, xly, xlz, gx, gy, - gz); + log_debug("ts: %" PRId64 "ms, xl.x: %7.2f, xl.y: %7.2f, xl.z: %7.2f, gy.x: %7.2f, gy.y: %7.2f, gy.z: %7.2f", + timestamp, xlx, xly, xlz, gx, gy, gz); }; imu::lsm6dsox.registerOnGyDataReadyCallback(callback); diff --git a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/main.cpp b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/main.cpp index 70b70d8a78..f9aee3d5db 100644 --- a/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/main.cpp +++ b/spikes/lk_sensors_imu_lsm6dsox_fusion_calibration/main.cpp @@ -48,18 +48,17 @@ namespace fusion { .rejectionTimeout = static_cast(5 * kODR_HZ), // ? # of samples in 5 seconds }; - auto timestamp_now = rtos::Kernel::Clock::now(); - auto timestamp_previous = rtos::Kernel::Clock::now(); + interface::LSM6DSOX::SensorData::time_point_t timestamp_previous = {}; auto global_offset = FusionOffset {}; constexpr auto CALIBRATION = bool {true}; // constexpr auto CALIBRATION = bool {false}; - void callback(const interface::LSM6DSOX::SensorData &data) + void callback(const interface::LSM6DSOX::SensorData data) { - timestamp_now = rtos::Kernel::Clock::now(); - auto timestamp_now_ms = mbed::HighResClock::now().time_since_epoch().count(); + auto timestamp_now = data.timestamp; + auto timestamp_now_us = std::chrono::microseconds {timestamp_now.time_since_epoch()}.count(); // ? Acquire latest sensor data auto gyroscope = FusionVector {{data.gy.x, data.gy.y, data.gy.z}}; @@ -68,8 +67,6 @@ namespace fusion { if constexpr (CALIBRATION) { // ? Define calibration offsets // ? Data: https://www.dropbox.com/scl/fi/cue7qpb77892rozyjbmcq/2022_01_16-IMU-Calibration_Data.xlsx - // const auto gyroscope_offset = FusionVector {{}}; - // const auto accelerometer_offset = FusionVector {{}}; constexpr auto gyroscope_offset = FusionVector {{0.02544522554F, -0.3286247803F, 0.3205770357F}}; constexpr auto accelerometer_offset = FusionVector {{0.006480437024F, -0.01962820621F, 0.003259031049F}}; @@ -84,7 +81,7 @@ namespace fusion { } // ? Calculate delta time (in seconds) to account for gyroscope sample clock error - auto delta_time = static_cast((timestamp_now - timestamp_previous).count()) / 1000.F; + auto delta_time = std::chrono::duration(timestamp_now - timestamp_previous).count(); timestamp_previous = timestamp_now; // ? Update gyroscope AHRS algorithm @@ -107,9 +104,9 @@ namespace fusion { // ? Log values // ? See https://x-io.co.uk/downloads/x-IMU3-User-Manual-v1.0.pdf#page=24 - log_free("I,%" PRId64 ",%f,%f,%f,%f,%f,%f\r\nQ,%" PRId64 ",%f,%f,%f,%f\r\n", timestamp_now_ms, gyroscope.axis.x, + log_free("I,%" PRId64 ",%f,%f,%f,%f,%f,%f\r\nQ,%" PRId64 ",%f,%f,%f,%f\r\n", timestamp_now_us, gyroscope.axis.x, gyroscope.axis.y, gyroscope.axis.z, accelerometer.axis.x, accelerometer.axis.y, accelerometer.axis.z, - timestamp_now_ms, q_w, q_x, q_y, q_z); + timestamp_now_us, q_w, q_x, q_y, q_z); }; } // namespace fusion From 85c830561848a84597eb83b9772a908be2660065 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Sun, 19 Feb 2023 08:34:56 +0100 Subject: [PATCH 118/143] :arrow_up: (IMUKit): Bump Fusion to v1.0.9 --- libs/IMUKit/external/fusion/FusionAhrs.c | 65 ++++++------ libs/IMUKit/external/fusion/FusionAhrs.h | 2 +- libs/IMUKit/external/fusion/FusionMath.h | 130 +++++++++++++---------- 3 files changed, 104 insertions(+), 93 deletions(-) diff --git a/libs/IMUKit/external/fusion/FusionAhrs.c b/libs/IMUKit/external/fusion/FusionAhrs.c index 579138e05c..de875fb2f5 100644 --- a/libs/IMUKit/external/fusion/FusionAhrs.c +++ b/libs/IMUKit/external/fusion/FusionAhrs.c @@ -114,11 +114,11 @@ void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, cons } // Calculate direction of gravity indicated by algorithm - const FusionVector halfGravity = { - .axis.x = Q.x * Q.z - Q.w * Q.y, - .axis.y = Q.y * Q.z + Q.w * Q.x, - .axis.z = Q.w * Q.w - 0.5f + Q.z * Q.z, - }; // third column of transposed rotation matrix scaled by 0.5 + const FusionVector halfGravity = {.axis = { + .x = Q.x * Q.z - Q.w * Q.y, + .y = Q.y * Q.z + Q.w * Q.x, + .z = Q.w * Q.w - 0.5f + Q.z * Q.z, + }}; // third column of transposed rotation matrix scaled by 0.5 // Calculate accelerometer feedback FusionVector halfAccelerometerFeedback = FUSION_VECTOR_ZERO; @@ -161,11 +161,11 @@ void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, cons } // Compute direction of west indicated by algorithm - const FusionVector halfWest = { - .axis.x = Q.x * Q.y + Q.w * Q.z, - .axis.y = Q.w * Q.w - 0.5f + Q.y * Q.y, - .axis.z = Q.y * Q.z - Q.w * Q.x - }; // second column of transposed rotation matrix scaled by 0.5 + const FusionVector halfWest = {.axis = { + .x = Q.x * Q.y + Q.w * Q.z, + .y = Q.w * Q.w - 0.5f + Q.y * Q.y, + .z = Q.y * Q.z - Q.w * Q.x + }}; // second column of transposed rotation matrix scaled by 0.5 // Calculate magnetometer feedback scaled by 0.5 ahrs->halfMagnetometerFeedback = FusionVectorCrossProduct(FusionVectorNormalise(FusionVectorCrossProduct(halfGravity, magnetometer)), halfWest); @@ -231,11 +231,11 @@ void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector // Calculate magnetometer const float headingRadians = FusionDegreesToRadians(heading); const float sinHeadingRadians = sinf(headingRadians); - const FusionVector magnetometer = { - .axis.x = cosf(headingRadians), - .axis.y = -1.0f * cosf(roll) * sinHeadingRadians, - .axis.z = sinHeadingRadians * sinf(roll), - }; + const FusionVector magnetometer = {.axis = { + .x = cosf(headingRadians), + .y = -1.0f * cosf(roll) * sinHeadingRadians, + .z = sinHeadingRadians * sinf(roll), + }}; // Update AHRS algorithm FusionAhrsUpdate(ahrs, gyroscope, accelerometer, magnetometer, deltaTime); @@ -259,11 +259,11 @@ FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs) { */ FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) { #define Q ahrs->quaternion.element - const FusionVector gravity = { - .axis.x = 2.0f * (Q.x * Q.z - Q.w * Q.y), - .axis.y = 2.0f * (Q.y * Q.z + Q.w * Q.x), - .axis.z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z), - }; // third column of transposed rotation matrix + const FusionVector gravity = {.axis = { + .x = 2.0f * (Q.x * Q.z - Q.w * Q.y), + .y = 2.0f * (Q.y * Q.z + Q.w * Q.x), + .z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z), + }}; // third column of transposed rotation matrix const FusionVector linearAcceleration = FusionVectorSubtract(ahrs->accelerometer, gravity); return linearAcceleration; #undef Q @@ -285,12 +285,11 @@ FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) { const float qxqy = Q.x * Q.y; const float qxqz = Q.x * Q.z; const float qyqz = Q.y * Q.z; - const FusionVector earthAcceleration = { - .axis.x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z), - .axis.y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z), - .axis.z = (2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z)) - 1.0f, - }; // rotation matrix multiplied with the accelerometer, with 1 g subtracted - return earthAcceleration; + return (FusionVector) {.axis = { + .x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z), + .y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z), + .z = (2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z)) - 1.0f, + }}; // rotation matrix multiplied with the accelerometer, with 1 g subtracted #undef Q #undef A } @@ -317,7 +316,7 @@ FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahr * @param ahrs AHRS algorithm structure. * @return AHRS algorithm flags. */ -FusionAhrsFlags FusionAhrsGetFlags(FusionAhrs *const ahrs) { +FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs) { const unsigned int warningTimeout = ahrs->settings.rejectionTimeout / 4; const FusionAhrsFlags flags = { .initialising = ahrs->initialising, @@ -340,12 +339,12 @@ void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading) { #define Q ahrs->quaternion.element const float yaw = atan2f(Q.w * Q.z + Q.x * Q.y, 0.5f - Q.y * Q.y - Q.z * Q.z); const float halfYawMinusHeading = 0.5f * (yaw - FusionDegreesToRadians(heading)); - const FusionQuaternion rotation = { - .element.w = cosf(halfYawMinusHeading), - .element.x = 0.0f, - .element.y = 0.0f, - .element.z = -1.0f * sinf(halfYawMinusHeading), - }; + const FusionQuaternion rotation = {.element = { + .w = cosf(halfYawMinusHeading), + .x = 0.0f, + .y = 0.0f, + .z = -1.0f * sinf(halfYawMinusHeading), + }}; ahrs->quaternion = FusionQuaternionMultiply(rotation, ahrs->quaternion); #undef Q } diff --git a/libs/IMUKit/external/fusion/FusionAhrs.h b/libs/IMUKit/external/fusion/FusionAhrs.h index cebae66c3b..968240b6c0 100644 --- a/libs/IMUKit/external/fusion/FusionAhrs.h +++ b/libs/IMUKit/external/fusion/FusionAhrs.h @@ -94,7 +94,7 @@ FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs); FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs); -FusionAhrsFlags FusionAhrsGetFlags(FusionAhrs *const ahrs); +FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs); void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading); diff --git a/libs/IMUKit/external/fusion/FusionMath.h b/libs/IMUKit/external/fusion/FusionMath.h index bbcc279908..567dca685b 100644 --- a/libs/IMUKit/external/fusion/FusionMath.h +++ b/libs/IMUKit/external/fusion/FusionMath.h @@ -199,10 +199,11 @@ static inline bool FusionVectorIsZero(const FusionVector vector) { * @return Sum of two vectors. */ static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const FusionVector vectorB) { - FusionVector result; - result.axis.x = vectorA.axis.x + vectorB.axis.x; - result.axis.y = vectorA.axis.y + vectorB.axis.y; - result.axis.z = vectorA.axis.z + vectorB.axis.z; + const FusionVector result = {.axis = { + .x = vectorA.axis.x + vectorB.axis.x, + .y = vectorA.axis.y + vectorB.axis.y, + .z = vectorA.axis.z + vectorB.axis.z, + }}; return result; } @@ -213,10 +214,11 @@ static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const Fus * @return Vector B subtracted from vector A. */ static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, const FusionVector vectorB) { - FusionVector result; - result.axis.x = vectorA.axis.x - vectorB.axis.x; - result.axis.y = vectorA.axis.y - vectorB.axis.y; - result.axis.z = vectorA.axis.z - vectorB.axis.z; + const FusionVector result = {.axis = { + .x = vectorA.axis.x - vectorB.axis.x, + .y = vectorA.axis.y - vectorB.axis.y, + .z = vectorA.axis.z - vectorB.axis.z, + }}; return result; } @@ -236,10 +238,11 @@ static inline float FusionVectorSum(const FusionVector vector) { * @return Multiplication of a vector by a scalar. */ static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, const float scalar) { - FusionVector result; - result.axis.x = vector.axis.x * scalar; - result.axis.y = vector.axis.y * scalar; - result.axis.z = vector.axis.z * scalar; + const FusionVector result = {.axis = { + .x = vector.axis.x * scalar, + .y = vector.axis.y * scalar, + .z = vector.axis.z * scalar, + }}; return result; } @@ -250,10 +253,11 @@ static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, * @return Hadamard product. */ static inline FusionVector FusionVectorHadamardProduct(const FusionVector vectorA, const FusionVector vectorB) { - FusionVector result; - result.axis.x = vectorA.axis.x * vectorB.axis.x; - result.axis.y = vectorA.axis.y * vectorB.axis.y; - result.axis.z = vectorA.axis.z * vectorB.axis.z; + const FusionVector result = {.axis = { + .x = vectorA.axis.x * vectorB.axis.x, + .y = vectorA.axis.y * vectorB.axis.y, + .z = vectorA.axis.z * vectorB.axis.z, + }}; return result; } @@ -266,10 +270,11 @@ static inline FusionVector FusionVectorHadamardProduct(const FusionVector vector static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, const FusionVector vectorB) { #define A vectorA.axis #define B vectorB.axis - FusionVector result; - result.axis.x = A.y * B.z - A.z * B.y; - result.axis.y = A.z * B.x - A.x * B.z; - result.axis.z = A.x * B.y - A.y * B.x; + const FusionVector result = {.axis = { + .x = A.y * B.z - A.z * B.y, + .y = A.z * B.x - A.x * B.z, + .z = A.x * B.y - A.y * B.x, + }}; return result; #undef A #undef B @@ -317,11 +322,12 @@ static inline FusionVector FusionVectorNormalise(const FusionVector vector) { * @return Sum of two quaternions. */ static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) { - FusionQuaternion result; - result.element.w = quaternionA.element.w + quaternionB.element.w; - result.element.x = quaternionA.element.x + quaternionB.element.x; - result.element.y = quaternionA.element.y + quaternionB.element.y; - result.element.z = quaternionA.element.z + quaternionB.element.z; + const FusionQuaternion result = {.element = { + .w = quaternionA.element.w + quaternionB.element.w, + .x = quaternionA.element.x + quaternionB.element.x, + .y = quaternionA.element.y + quaternionB.element.y, + .z = quaternionA.element.z + quaternionB.element.z, + }}; return result; } @@ -334,11 +340,12 @@ static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quater static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) { #define A quaternionA.element #define B quaternionB.element - FusionQuaternion result; - result.element.w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z; - result.element.x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y; - result.element.y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x; - result.element.z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w; + const FusionQuaternion result = {.element = { + .w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z, + .x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y, + .y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x, + .z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w, + }}; return result; #undef A #undef B @@ -356,11 +363,12 @@ static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion q static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuaternion quaternion, const FusionVector vector) { #define Q quaternion.element #define V vector.axis - FusionQuaternion result; - result.element.w = -Q.x * V.x - Q.y * V.y - Q.z * V.z; - result.element.x = Q.w * V.x + Q.y * V.z - Q.z * V.y; - result.element.y = Q.w * V.y - Q.x * V.z + Q.z * V.x; - result.element.z = Q.w * V.z + Q.x * V.y - Q.y * V.x; + const FusionQuaternion result = {.element = { + .w = -Q.x * V.x - Q.y * V.y - Q.z * V.z, + .x = Q.w * V.x + Q.y * V.z - Q.z * V.y, + .y = Q.w * V.y - Q.x * V.z + Q.z * V.x, + .z = Q.w * V.z + Q.x * V.y - Q.y * V.x, + }}; return result; #undef Q #undef V @@ -378,12 +386,13 @@ static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion #else const float magnitudeReciprocal = FusionFastInverseSqrt(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); #endif - FusionQuaternion normalisedQuaternion; - normalisedQuaternion.element.w = Q.w * magnitudeReciprocal; - normalisedQuaternion.element.x = Q.x * magnitudeReciprocal; - normalisedQuaternion.element.y = Q.y * magnitudeReciprocal; - normalisedQuaternion.element.z = Q.z * magnitudeReciprocal; - return normalisedQuaternion; + const FusionQuaternion result = {.element = { + .w = Q.w * magnitudeReciprocal, + .x = Q.x * magnitudeReciprocal, + .y = Q.y * magnitudeReciprocal, + .z = Q.z * magnitudeReciprocal, + }}; + return result; #undef Q } @@ -398,10 +407,11 @@ static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion */ static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, const FusionVector vector) { #define R matrix.element - FusionVector result; - result.axis.x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z; - result.axis.y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z; - result.axis.z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z; + const FusionVector result = {.axis = { + .x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z, + .y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z, + .z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z, + }}; return result; #undef R } @@ -423,16 +433,17 @@ static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quate const float qxqy = Q.x * Q.y; const float qxqz = Q.x * Q.z; const float qyqz = Q.y * Q.z; - FusionMatrix matrix; - matrix.element.xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x); - matrix.element.xy = 2.0f * (qxqy - qwqz); - matrix.element.xz = 2.0f * (qxqz + qwqy); - matrix.element.yx = 2.0f * (qxqy + qwqz); - matrix.element.yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y); - matrix.element.yz = 2.0f * (qyqz - qwqx); - matrix.element.zx = 2.0f * (qxqz - qwqy); - matrix.element.zy = 2.0f * (qyqz + qwqx); - matrix.element.zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z); + const FusionMatrix matrix = {.element = { + .xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x), + .xy = 2.0f * (qxqy - qwqz), + .xz = 2.0f * (qxqz + qwqy), + .yx = 2.0f * (qxqy + qwqz), + .yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y), + .yz = 2.0f * (qyqz - qwqx), + .zx = 2.0f * (qxqz - qwqy), + .zy = 2.0f * (qyqz + qwqx), + .zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z), + }}; return matrix; #undef Q } @@ -445,10 +456,11 @@ static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quate static inline FusionEuler FusionQuaternionToEuler(const FusionQuaternion quaternion) { #define Q quaternion.element const float halfMinusQySquared = 0.5f - Q.y * Q.y; // calculate common terms to avoid repeated operations - FusionEuler euler; - euler.angle.roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)); - euler.angle.pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))); - euler.angle.yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)); + const FusionEuler euler = {.angle = { + .roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)), + .pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))), + .yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)), + }}; return euler; #undef Q } From 09d61b33a46536f2c425e25e47eafe3662ebbbdb Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Mon, 20 Feb 2023 11:15:29 +0100 Subject: [PATCH 119/143] :fire: (libs): Remove PrettyPrinter, as not used anymore --- libs/CMakeLists.txt | 2 - libs/PrettyPrinter/CMakeLists.txt | 21 ----- libs/PrettyPrinter/include/PrettyPrinter.h | 16 ---- libs/PrettyPrinter/source/PrettyPrinter.cpp | 96 --------------------- 4 files changed, 135 deletions(-) delete mode 100644 libs/PrettyPrinter/CMakeLists.txt delete mode 100644 libs/PrettyPrinter/include/PrettyPrinter.h delete mode 100644 libs/PrettyPrinter/source/PrettyPrinter.cpp diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index e91e96c36e..f498e76cf5 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -25,5 +25,3 @@ add_subdirectory(${LIBS_DIR}/TouchSensorKit) add_subdirectory(${LIBS_DIR}/UIAnimationKit) add_subdirectory(${LIBS_DIR}/VideoKit) add_subdirectory(${LIBS_DIR}/WebKit) - -add_subdirectory(${LIBS_DIR}/PrettyPrinter) diff --git a/libs/PrettyPrinter/CMakeLists.txt b/libs/PrettyPrinter/CMakeLists.txt deleted file mode 100644 index e032554e45..0000000000 --- a/libs/PrettyPrinter/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Leka - LekaOS -# Copyright 2020 APF France handicap -# SPDX-License-Identifier: Apache-2.0 - -add_library(lib_PrettyPrinter STATIC) - -target_include_directories(lib_PrettyPrinter - PUBLIC - ./include -) - -target_sources(lib_PrettyPrinter - PRIVATE - ./source/PrettyPrinter.cpp -) - -target_link_libraries(lib_PrettyPrinter - PRIVATE - mbed-os - LogKit -) diff --git a/libs/PrettyPrinter/include/PrettyPrinter.h b/libs/PrettyPrinter/include/PrettyPrinter.h deleted file mode 100644 index 88fbb5e5da..0000000000 --- a/libs/PrettyPrinter/include/PrettyPrinter.h +++ /dev/null @@ -1,16 +0,0 @@ -// Mbed OS -// Copyright 2018 ARM Limited -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "ble/BLE.h" - -namespace leka::ble { - -void printError(ble_error_t error, const char *msg); -void printAddress(const ::ble::address_t &addr); -void printMacAddress(); -auto phy_to_string(::ble::phy_t phy) -> const char *; - -} // namespace leka::ble diff --git a/libs/PrettyPrinter/source/PrettyPrinter.cpp b/libs/PrettyPrinter/source/PrettyPrinter.cpp deleted file mode 100644 index 70482ffb10..0000000000 --- a/libs/PrettyPrinter/source/PrettyPrinter.cpp +++ /dev/null @@ -1,96 +0,0 @@ -// Mbed OS -// Copyright 2018 ARM Limited -// SPDX-License-Identifier: Apache-2.0 - -// LCOV_EXCL_START - -#include "PrettyPrinter.h" - -#include "LogKit.h" - -void leka::ble::printError(ble_error_t error, const char *msg) -{ - log_debug("%s: ", msg); - switch (error) { - case BLE_ERROR_NONE: - log_error("BLE_ERROR_NONE: No error"); - break; - case BLE_ERROR_BUFFER_OVERFLOW: - log_error( - "BLE_ERROR_BUFFER_OVERFLOW: The requested action would cause a buffer overflow and has been " - "aborted"); - break; - case BLE_ERROR_NOT_IMPLEMENTED: - log_error( - "BLE_ERROR_NOT_IMPLEMENTED: Requested a feature that isn't yet implement or isn't supported by the " - "target HW"); - break; - case BLE_ERROR_PARAM_OUT_OF_RANGE: - log_error("BLE_ERROR_PARAM_OUT_OF_RANGE: One of the supplied parameters is outside the valid range"); - break; - case BLE_ERROR_INVALID_PARAM: - log_error("BLE_ERROR_INVALID_PARAM: One of the supplied parameters is invalid"); - break; - case BLE_STACK_BUSY: - log_error("BLE_STACK_BUSY: The stack is busy"); - break; - case BLE_ERROR_INVALID_STATE: - log_error("BLE_ERROR_INVALID_STATE: Invalid state"); - break; - case BLE_ERROR_NO_MEM: - log_error("BLE_ERROR_NO_MEM: Out of Memory"); - break; - case BLE_ERROR_OPERATION_NOT_PERMITTED: - log_error("BLE_ERROR_OPERATION_NOT_PERMITTED"); - break; - case BLE_ERROR_INITIALIZATION_INCOMPLETE: - log_error("BLE_ERROR_INITIALIZATION_INCOMPLETE"); - break; - case BLE_ERROR_ALREADY_INITIALIZED: - log_error("BLE_ERROR_ALREADY_INITIALIZED"); - break; - case BLE_ERROR_UNSPECIFIED: - log_error("BLE_ERROR_UNSPECIFIED: Unknown error"); - break; - case BLE_ERROR_INTERNAL_STACK_FAILURE: - log_error("BLE_ERROR_INTERNAL_STACK_FAILURE: internal stack failure"); - break; - case BLE_ERROR_NOT_FOUND: - log_error("BLE_ERROR_NOT_FOUND"); - break; - default: - log_error("Unknown error"); - } -} - -/** print device address to the terminal */ -void leka::ble::printAddress(const ::ble::address_t &addr) -{ - log_info("%02x:%02x:%02x:%02x:%02x:%02x\r\n", addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); -} - -void leka::ble::printMacAddress() -{ - /* Print out device MAC address to the console*/ - ::ble::own_address_type_t addr_type; - ::ble::address_t address; - BLE::Instance().gap().getAddress(addr_type, address); - log_info("DEVICE MAC ADDRESS: "); - leka::ble::printAddress(address); -} - -auto leka::ble::phy_to_string(::ble::phy_t phy) -> const char * -{ - switch (phy.value()) { - case ::ble::phy_t::LE_1M: - return "LE 1M"; - case ::ble::phy_t::LE_2M: - return "LE 2M"; - case ::ble::phy_t::LE_CODED: - return "LE coded"; - default: - return "invalid PHY"; - } -} - -// LCOV_EXCL_STOP From bae81bafef6415a46d003f541f5dcacfe71f0ced Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 22 Feb 2023 08:58:35 +0100 Subject: [PATCH 120/143] :truck: (actions): Rename action compare_files to compare_base_head_changes - improve output - use table for env info - move map diff to detailed summary --- .../action.yml | 32 ++++++++++--------- .../compare_base_head_files.sh} | 4 +-- .../generate_statistics.sh | 2 +- .../get_all_targets.sh | 0 .../get_diffs.sh | 4 +-- .../utils.sh | 0 .../ci-code_analysis-compare_base_head.yml | 4 +-- 7 files changed, 24 insertions(+), 22 deletions(-) rename .github/actions/{compare_files => compare_base_head_changes}/action.yml (57%) rename .github/actions/{compare_files/compare_files.sh => compare_base_head_changes/compare_base_head_files.sh} (95%) rename .github/actions/{compare_files => compare_base_head_changes}/generate_statistics.sh (99%) rename .github/actions/{compare_files => compare_base_head_changes}/get_all_targets.sh (100%) rename .github/actions/{compare_files => compare_base_head_changes}/get_diffs.sh (93%) rename .github/actions/{compare_files => compare_base_head_changes}/utils.sh (100%) diff --git a/.github/actions/compare_files/action.yml b/.github/actions/compare_base_head_changes/action.yml similarity index 57% rename from .github/actions/compare_files/action.yml rename to .github/actions/compare_base_head_changes/action.yml index f893d14f5e..910351c6c8 100644 --- a/.github/actions/compare_files/action.yml +++ b/.github/actions/compare_base_head_changes/action.yml @@ -2,7 +2,7 @@ # Copyright 2021 APF France handicap # SPDX-License-Identifier: Apache-2.0 -name: "Compare files" +name: "Compare base/head changes" description: "" inputs: @@ -25,10 +25,10 @@ inputs: runs: using: "composite" steps: - - name: Compare files - id: compare_files + - name: Compare changes + id: compare_base_head_changes shell: bash - run: ${{ github.action_path }}/compare_files.sh ${{ inputs.base_dir }} ${{ inputs.head_dir }} + run: ${{ github.action_path }}/compare_base_head_files.sh ${{ inputs.base_dir }} ${{ inputs.head_dir }} - name: Generate statistics id: generate_statistics @@ -43,17 +43,24 @@ runs: - name: Publish differences uses: marocchino/sticky-pull-request-comment@v2 with: - header: compare_files-${{ inputs.comment_header }} + header: compare_base_head_changes-${{ inputs.comment_header }} message: | # File comparision analysis report - ## :bookmark: Info + ## :pushpin: Info - - base: [`${{ env.BASE_REF}}`](https://github.com/leka/LekaOS/tree/${{ env.BASE_REF}}) / ${{ env.BASE_SHA }} + `${{ env.BASE_MBED_VERSION }}` + `${{ env.BASE_CXX_STANDARD }}` - - head: [`${{ env.HEAD_REF }}`](https://github.com/leka/LekaOS/tree/${{ env.HEAD_REF }}) / ${{ env.HEAD_SHA }} + `${{ env.HEAD_MBED_VERSION }}` + `${{ env.HEAD_CXX_STANDARD }}` - toolchain: `${{ env.TOOLCHAIN_VERSION }}` - enable_log_debug: `${{ inputs.enable_log_debug }}` + | | `base` | `head` | + |--------|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------| + | branch | [`${{ env.BASE_REF}}`](https://github.com/leka/LekaOS/tree/${{ env.BASE_REF}}) | [`${{ env.HEAD_REF }}`](https://github.com/leka/LekaOS/tree/${{ env.HEAD_REF }}) | + | sha | ${{ env.BASE_SHA }} | ${{ env.HEAD_SHA }} | + | mbed | `${{ env.BASE_MBED_VERSION }}` | `${{ env.HEAD_MBED_VERSION }}` | + | `-std` | `${{ env.BASE_CXX_STANDARD }}` | `${{ env.HEAD_CXX_STANDARD }}` | + + ## :memo: Firmware impact analysis + ${{ env.FIRMWARE_STATISTICS_OUTPUT }}

@@ -73,10 +80,10 @@ runs:
- ## :memo: Summary + ## :microscope: Detailed impact analysis
- Click to show summary + Click to show detailed analysis for all targets - :heavy_check_mark: - existing target - :sparkles: - new target @@ -86,13 +93,8 @@ runs: ${{ env.STATUS_DIFF_OUTPUT }} -
- ## :world_map: Map files diff output -
- Click to show diff list - ${{ env.MAP_DIFF_OUTPUT }}
diff --git a/.github/actions/compare_files/compare_files.sh b/.github/actions/compare_base_head_changes/compare_base_head_files.sh similarity index 95% rename from .github/actions/compare_files/compare_files.sh rename to .github/actions/compare_base_head_changes/compare_base_head_files.sh index da231f37a1..ec5aa3b7b0 100755 --- a/.github/actions/compare_files/compare_files.sh +++ b/.github/actions/compare_base_head_changes/compare_base_head_files.sh @@ -7,8 +7,8 @@ shopt -s xpg_echo BASE_DIR=$1 HEAD_DIR=$2 -source ./.github/actions/compare_files/utils.sh -source ./.github/actions/compare_files/get_all_targets.sh $BASE_DIR $HEAD_DIR +source ./.github/actions/compare_base_head_changes/utils.sh +source ./.github/actions/compare_base_head_changes/get_all_targets.sh $BASE_DIR $HEAD_DIR echo 'STATUS_DIFF_OUTPUT<> $GITHUB_ENV diff --git a/.github/actions/compare_files/generate_statistics.sh b/.github/actions/compare_base_head_changes/generate_statistics.sh similarity index 99% rename from .github/actions/compare_files/generate_statistics.sh rename to .github/actions/compare_base_head_changes/generate_statistics.sh index 743158f92d..a238a88768 100755 --- a/.github/actions/compare_files/generate_statistics.sh +++ b/.github/actions/compare_base_head_changes/generate_statistics.sh @@ -7,7 +7,7 @@ shopt -s xpg_echo BASE_DIR=$1 HEAD_DIR=$2 -source ./.github/actions/compare_files/utils.sh +source ./.github/actions/compare_base_head_changes/utils.sh # # MARK: - bootloader statistics diff --git a/.github/actions/compare_files/get_all_targets.sh b/.github/actions/compare_base_head_changes/get_all_targets.sh similarity index 100% rename from .github/actions/compare_files/get_all_targets.sh rename to .github/actions/compare_base_head_changes/get_all_targets.sh diff --git a/.github/actions/compare_files/get_diffs.sh b/.github/actions/compare_base_head_changes/get_diffs.sh similarity index 93% rename from .github/actions/compare_files/get_diffs.sh rename to .github/actions/compare_base_head_changes/get_diffs.sh index 9bbca58c49..616566b488 100755 --- a/.github/actions/compare_files/get_diffs.sh +++ b/.github/actions/compare_base_head_changes/get_diffs.sh @@ -3,8 +3,8 @@ shopt -s xpg_echo BASE_DIR=$1 HEAD_DIR=$2 -source ./.github/actions/compare_files/utils.sh -source ./.github/actions/compare_files/get_all_targets.sh +source ./.github/actions/compare_base_head_changes/utils.sh +source ./.github/actions/compare_base_head_changes/get_all_targets.sh no_map_diff=true diff --git a/.github/actions/compare_files/utils.sh b/.github/actions/compare_base_head_changes/utils.sh similarity index 100% rename from .github/actions/compare_files/utils.sh rename to .github/actions/compare_base_head_changes/utils.sh diff --git a/.github/workflows/ci-code_analysis-compare_base_head.yml b/.github/workflows/ci-code_analysis-compare_base_head.yml index 0fe475e6d2..63495bc067 100644 --- a/.github/workflows/ci-code_analysis-compare_base_head.yml +++ b/.github/workflows/ci-code_analysis-compare_base_head.yml @@ -150,7 +150,7 @@ jobs: # Mark: - Job - compare_bin_map_files # - compare_base_head_files: + compare_base_head_changes: name: Compare base/head files runs-on: ubuntu-22.04 needs: [build_base_ref, build_head_ref] @@ -181,7 +181,7 @@ jobs: run: ls -R - name: Compare files - uses: ./.github/actions/compare_files + uses: ./.github/actions/compare_base_head_changes with: comment_header: enable_log_debug-${{ matrix.enable_log_debug }} enable_log_debug: ${{ matrix.enable_log_debug }} From 6967e5c4d82e100c19fd1f11b9cfa2df1f6f2f89 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 22 Feb 2023 10:52:43 +0100 Subject: [PATCH 121/143] :truck: (workflows): Rename workflow compare_base_head to impact_of_changes --- .github/actions/compare_base_head_changes/action.yml | 2 +- ...base_head.yml => ci-code_analysis-impact_of_changes.yml} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename .github/workflows/{ci-code_analysis-compare_base_head.yml => ci-code_analysis-impact_of_changes.yml} (98%) diff --git a/.github/actions/compare_base_head_changes/action.yml b/.github/actions/compare_base_head_changes/action.yml index 910351c6c8..fdb62f9c64 100644 --- a/.github/actions/compare_base_head_changes/action.yml +++ b/.github/actions/compare_base_head_changes/action.yml @@ -45,7 +45,7 @@ runs: with: header: compare_base_head_changes-${{ inputs.comment_header }} message: | - # File comparision analysis report + # PR changes analysis report ## :pushpin: Info diff --git a/.github/workflows/ci-code_analysis-compare_base_head.yml b/.github/workflows/ci-code_analysis-impact_of_changes.yml similarity index 98% rename from .github/workflows/ci-code_analysis-compare_base_head.yml rename to .github/workflows/ci-code_analysis-impact_of_changes.yml index 63495bc067..0177df3735 100644 --- a/.github/workflows/ci-code_analysis-compare_base_head.yml +++ b/.github/workflows/ci-code_analysis-impact_of_changes.yml @@ -2,7 +2,7 @@ # Copyright 2022 APF France handicap # SPDX-License-Identifier: Apache-2.0 -name: Code Analysis - Compare base/head +name: Code Analysis - Impact of changes on: pull_request: @@ -151,7 +151,7 @@ jobs: # compare_base_head_changes: - name: Compare base/head files + name: Compare base/head changes runs-on: ubuntu-22.04 needs: [build_base_ref, build_head_ref] @@ -180,7 +180,7 @@ jobs: working-directory: build_artifacts run: ls -R - - name: Compare files + - name: Compare base/head files uses: ./.github/actions/compare_base_head_changes with: comment_header: enable_log_debug-${{ matrix.enable_log_debug }} From efc98bd059363bf51dc3a0e6b3cc96ce8f3f21cc Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 22 Feb 2023 08:38:23 +0100 Subject: [PATCH 122/143] :construction_worker: (setup): Export BASE/HEAD toolchain versions to env --- .github/actions/setup/setup_env.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup/setup_env.sh b/.github/actions/setup/setup_env.sh index 817fd88363..b9b61d928f 100755 --- a/.github/actions/setup/setup_env.sh +++ b/.github/actions/setup/setup_env.sh @@ -34,6 +34,14 @@ echo "BASE_MBED_VERSION=$BASE_MBED_VERSION" >> $GITHUB_ENV echo "BASE_MCUBOOT_VERSION=$BASE_MCUBOOT_VERSION" >> $GITHUB_ENV echo "BASE_CXX_STANDARD=$BASE_CXX_STANDARD" >> $GITHUB_ENV +BASE_ARM_TOOLCHAIN_URL=$(cat config/toolchain_gcc_arm_none_eabi_url) +BASE_ARM_TOOLCHAIN_ARCHIVE="base_arm_toolchain_archive" +BASE_ARM_TOOLCHAIN_EXTRACT_DIRECTORY="base_arm_toolchain" + +echo "BASE_ARM_TOOLCHAIN_ARCHIVE=$BASE_ARM_TOOLCHAIN_ARCHIVE" >> $GITHUB_ENV +echo "BASE_ARM_TOOLCHAIN_EXTRACT_DIRECTORY=$BASE_ARM_TOOLCHAIN_EXTRACT_DIRECTORY" >> $GITHUB_ENV +echo "BASE_ARM_TOOLCHAIN_URL=$BASE_ARM_TOOLCHAIN_URL" >> $GITHUB_ENV + git checkout $HEAD_REF HEAD_SHA=$(git rev-parse --short HEAD) @@ -46,6 +54,14 @@ echo "HEAD_MBED_VERSION=$HEAD_MBED_VERSION" >> $GITHUB_ENV echo "HEAD_MCUBOOT_VERSION=$HEAD_MCUBOOT_VERSION" >> $GITHUB_ENV echo "HEAD_CXX_STANDARD=$HEAD_CXX_STANDARD" >> $GITHUB_ENV +HEAD_ARM_TOOLCHAIN_URL=$(cat config/toolchain_gcc_arm_none_eabi_url) +HEAD_ARM_TOOLCHAIN_ARCHIVE="head_arm_toolchain_archive" +HEAD_ARM_TOOLCHAIN_EXTRACT_DIRECTORY="head_arm_toolchain" + +echo "HEAD_ARM_TOOLCHAIN_ARCHIVE=$HEAD_ARM_TOOLCHAIN_ARCHIVE" >> $GITHUB_ENV +echo "HEAD_ARM_TOOLCHAIN_EXTRACT_DIRECTORY=$HEAD_ARM_TOOLCHAIN_EXTRACT_DIRECTORY" >> $GITHUB_ENV +echo "HEAD_ARM_TOOLCHAIN_URL=$HEAD_ARM_TOOLCHAIN_URL" >> $GITHUB_ENV + # # MARK: - ccache # @@ -63,7 +79,7 @@ echo "CCACHE_COMPILERCHECK=$CCACHE_COMPILERCHECK" >> $GITHUB_ENV echo "CCACHE_LOGFILE=$CCACHE_LOGFILE" >> $GITHUB_ENV # -# MARK: - ARM GCC Toolchain +# MARK: - ARM GCC Toolchain (generic) # ARM_TOOLCHAIN_FILENAME="gcc-arm-none-eabi-*-x86_64-linux.tar.bz2" From eb0ec5c1cdc8686c0c5a8f48c50c3838ed354b75 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 22 Feb 2023 12:20:56 +0100 Subject: [PATCH 123/143] :construction_worker: (workflows): Add new workflow code_analysis-toolchain_upgrade --- .../compare_base_head_changes/action.yml | 2 +- .../compare_toolchain_upgrade/action.yml | 71 +++++ .../compare_base_head_files.sh | 109 ++++++++ .../generate_statistics.sh | 176 +++++++++++++ .../generate_sticky_note.rb | 77 ++++++ .../get_all_targets.sh | 45 ++++ .../compare_toolchain_upgrade/get_diffs.sh | 80 ++++++ .../compare_toolchain_upgrade/utils.sh | 86 ++++++ .../ci-code_analysis-toolchain_upgrade.yml | 246 ++++++++++++++++++ 9 files changed, 891 insertions(+), 1 deletion(-) create mode 100644 .github/actions/compare_toolchain_upgrade/action.yml create mode 100755 .github/actions/compare_toolchain_upgrade/compare_base_head_files.sh create mode 100755 .github/actions/compare_toolchain_upgrade/generate_statistics.sh create mode 100755 .github/actions/compare_toolchain_upgrade/generate_sticky_note.rb create mode 100755 .github/actions/compare_toolchain_upgrade/get_all_targets.sh create mode 100755 .github/actions/compare_toolchain_upgrade/get_diffs.sh create mode 100644 .github/actions/compare_toolchain_upgrade/utils.sh create mode 100644 .github/workflows/ci-code_analysis-toolchain_upgrade.yml diff --git a/.github/actions/compare_base_head_changes/action.yml b/.github/actions/compare_base_head_changes/action.yml index fdb62f9c64..b5e44f1d79 100644 --- a/.github/actions/compare_base_head_changes/action.yml +++ b/.github/actions/compare_base_head_changes/action.yml @@ -59,7 +59,7 @@ runs: | mbed | `${{ env.BASE_MBED_VERSION }}` | `${{ env.HEAD_MBED_VERSION }}` | | `-std` | `${{ env.BASE_CXX_STANDARD }}` | `${{ env.HEAD_CXX_STANDARD }}` | - ## :memo: Firmware impact analysis + ## :robot: Firmware impact analysis ${{ env.FIRMWARE_STATISTICS_OUTPUT }} diff --git a/.github/actions/compare_toolchain_upgrade/action.yml b/.github/actions/compare_toolchain_upgrade/action.yml new file mode 100644 index 0000000000..bbd4711669 --- /dev/null +++ b/.github/actions/compare_toolchain_upgrade/action.yml @@ -0,0 +1,71 @@ +# Leka - LekaOS +# Copyright 2021 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: "Compare toolchain upgrade" +description: "" + +inputs: + comment_header: + description: "Specify comment header if needed" + required: true + + enable_log_debug: + description: "ENABLE_LOG_DEBUG=[ON, OFF]" + required: true + + base_dir: + description: "Path to base files" + required: true + + head_dir: + description: "Path to head files" + required: true + +runs: + using: "composite" + steps: + - name: Compare changes + id: compare_base_head_changes + shell: bash + run: ${{ github.action_path }}/compare_base_head_files.sh ${{ inputs.base_dir }} ${{ inputs.head_dir }} + env: + RUNNER_HOME: ${{ env.RUNNER_HOME }} + + - name: Generate statistics + id: generate_statistics + shell: bash + run: ${{ github.action_path }}/generate_statistics.sh ${{ inputs.base_dir }} ${{ inputs.head_dir }} + env: + RUNNER_HOME: ${{ env.RUNNER_HOME }} + + - name: Get diffs + id: get_diffs + shell: bash + run: ${{ github.action_path }}/get_diffs.sh ${{ inputs.base_dir }} ${{ inputs.head_dir }} + env: + RUNNER_HOME: ${{ env.RUNNER_HOME }} + + - name: Create stick message + id: create_sticky_message + shell: bash + run: ruby ${{ github.action_path }}/generate_sticky_note.rb + env: + RUNNER_HOME: ${{ env.RUNNER_HOME }} + ENABLE_LOG_DEBUG: ${{ inputs.enable_log_debug }} + BASE_ARM_TOOLCHAIN_VERSION: ${{ env.BASE_ARM_TOOLCHAIN_VERSION }} + HEAD_ARM_TOOLCHAIN_VERSION: ${{ env.HEAD_ARM_TOOLCHAIN_VERSION }} + BASE_REF: ${{ env.BASE_REF }} + HEAD_REF: ${{ env.HEAD_REF }} + BASE_SHA: ${{ env.BASE_SHA }} + HEAD_SHA: ${{ env.HEAD_SHA }} + BASE_MBED_VERSION: ${{ env.BASE_MBED_VERSION }} + HEAD_MBED_VERSION: ${{ env.HEAD_MBED_VERSION }} + BASE_CXX_STANDARD: ${{ env.BASE_CXX_STANDARD }} + HEAD_CXX_STANDARD: ${{ env.HEAD_CXX_STANDARD }} + + - name: Publish differences + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: compare_toolchain_upgrade-${{ inputs.comment_header }} + path: ${{ env.RUNNER_HOME }}/STICKY_MESSAGE.md diff --git a/.github/actions/compare_toolchain_upgrade/compare_base_head_files.sh b/.github/actions/compare_toolchain_upgrade/compare_base_head_files.sh new file mode 100755 index 0000000000..5dba6581ae --- /dev/null +++ b/.github/actions/compare_toolchain_upgrade/compare_base_head_files.sh @@ -0,0 +1,109 @@ +# Leka - LekaOS +# Copyright 2021 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +shopt -s xpg_echo + +BASE_DIR=$1 +HEAD_DIR=$2 + +source ./.github/actions/compare_base_head_changes/utils.sh +source ./.github/actions/compare_base_head_changes/get_all_targets.sh $BASE_DIR $HEAD_DIR + +STATUS_DIFF_OUTPUT="$RUNNER_HOME/STATUS_DIFF_OUTPUT.md" +touch $STATUS_DIFF_OUTPUT + +echo "| Target | Status | .bin | .map | Total Flash (base/head) | Total Flash Δ | Static RAM (base/head) | Static RAM Δ |" >> $STATUS_DIFF_OUTPUT +echo "|-------|:------:|:------:|:------:|:------:|:------:|:------:|:------:|" >> $STATUS_DIFF_OUTPUT + +for target in "${all_targets[@]}"; do + target_name=$target + + echo -n "| $target_name " >> $STATUS_DIFF_OUTPUT + + if [[ " ${added_targets[*]} " =~ " $target " ]]; then + + echo -n "| :sparkles: | - | - " >> $STATUS_DIFF_OUTPUT + + createSizeTextFile $HEAD_DIR $target_name + + head_flash_with_percentage="$(getUsedFlashSizeWithPercentage $HEAD_DIR $target_name)" + head_ram_with_percentage="$(getUsedRamSizeWithPercentage $HEAD_DIR $target_name)" + + echo -n "| $head_flash_with_percentage | - | $head_ram_with_percentage | - |\n" >> $STATUS_DIFF_OUTPUT + + elif [[ " ${deleted_targets[*]} " =~ " $target " ]]; then + + echo -n "| :coffin: | - | - | - | - | - | - |\n" >> $STATUS_DIFF_OUTPUT + + else + + echo -n "| :heavy_check_mark: " >> $STATUS_DIFF_OUTPUT + + if ! output=$(diff $BASE_DIR/$target_name.bin $HEAD_DIR/$target_name.bin 2>/dev/null); then + echo -n "| :x: " >> $STATUS_DIFF_OUTPUT + else + echo -n "| :white_check_mark: " >> $STATUS_DIFF_OUTPUT + fi + + createMapTextFile $BASE_DIR $target_name + createMapTextFile $HEAD_DIR $target_name + + createSizeTextFile $BASE_DIR $target_name + createSizeTextFile $HEAD_DIR $target_name + + + if ! output=$(diff $BASE_DIR/$target_name-map.txt $HEAD_DIR/$target_name-map.txt 2>/dev/null); then + echo -n "| :x: " >> $STATUS_DIFF_OUTPUT + else + echo -n "| :white_check_mark: " >> $STATUS_DIFF_OUTPUT + fi + + base_flash_with_percentage="$(getUsedFlashSizeWithPercentage $BASE_DIR $target_name)" + head_flash_with_percentage="$(getUsedFlashSizeWithPercentage $HEAD_DIR $target_name)" + + base_flash="$(getUsedFlashSize $BASE_DIR $target_name)" + head_flash="$(getUsedFlashSize $HEAD_DIR $target_name)" + + diff_flash=$(($head_flash - $base_flash)) + diff_flash_percentage="$((100 * ($head_flash - $base_flash) / $base_flash))%" + + output_flash="$base_flash_with_percentage
$head_flash_with_percentage" + output_flash_delta="" + + if [ $diff_flash -lt 0 ]; then + output_flash_delta=":chart_with_downwards_trend:
$diff_flash ($diff_flash_percentage)" + elif [ $diff_flash -gt 0 ]; then + output_flash_delta=":chart_with_upwards_trend:
+$diff_flash (+$diff_flash_percentage)" + else + output_flash="$base_flash_with_percentage" + output_flash_delta="ø" + fi + + base_ram_with_percentage="$(getUsedRamSizeWithPercentage $BASE_DIR $target_name)" + head_ram_with_percentage="$(getUsedRamSizeWithPercentage $HEAD_DIR $target_name)" + + base_ram="$(getUsedRamSize $BASE_DIR $target_name)" + head_ram="$(getUsedRamSize $HEAD_DIR $target_name)" + + diff_ram=$(($head_ram - $base_ram)) + diff_ram_percentage="$((100 * ($head_ram - $base_ram) / $base_ram))%" + + output_ram="$base_ram_with_percentage
$head_ram_with_percentage" + output_ram_delta="" + + if [ $diff_ram -lt 0 ]; then + output_ram_delta=":chart_with_downwards_trend:
$diff_ram ($diff_ram_percentage)" + elif [ $diff_ram -gt 0 ]; then + output_ram_delta=":chart_with_upwards_trend:
+$diff_ram (+$diff_ram_percentage)" + else + output_ram="$base_ram_with_percentage" + output_ram_delta="ø" + fi + + echo -n "| $output_flash | $output_flash_delta | $output_ram | $output_ram_delta " >> $STATUS_DIFF_OUTPUT + + echo -n "|\n" >> $STATUS_DIFF_OUTPUT + fi + +done diff --git a/.github/actions/compare_toolchain_upgrade/generate_statistics.sh b/.github/actions/compare_toolchain_upgrade/generate_statistics.sh new file mode 100755 index 0000000000..f053007e96 --- /dev/null +++ b/.github/actions/compare_toolchain_upgrade/generate_statistics.sh @@ -0,0 +1,176 @@ +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +shopt -s xpg_echo + +BASE_DIR=$1 +HEAD_DIR=$2 + +source ./.github/actions/compare_base_head_changes/utils.sh + +FIRMWARE_STATISTICS_OUTPUT="$RUNNER_HOME/FIRMWARE_STATISTICS_OUTPUT.md" +touch $FIRMWARE_STATISTICS_OUTPUT + +# +# MARK: - bootloader statistics +# + +echo "Creating statistics for bootloader" + +bootloader_target_name="bootloader" +bootloader_memory_section_size="0x40000" + +createSizeTextFile $BASE_DIR $bootloader_target_name +createSizeTextFile $HEAD_DIR $bootloader_target_name + +# MARK: Flash Used +bootloader_base_flash_used="$(getUsedFlashSize $BASE_DIR $bootloader_target_name)" +bootloader_base_flash_used_percentage="$((100 * $bootloader_base_flash_used / $bootloader_memory_section_size))%" + +bootloader_head_flash_used="$(getUsedFlashSize $HEAD_DIR $bootloader_target_name)" +bootloader_head_flash_used_percentage="$((100 * $bootloader_head_flash_used / $bootloader_memory_section_size))%" + +output_bootloader_base_flash_used="$bootloader_base_flash_used ($bootloader_base_flash_used_percentage)" +output_bootloader_head_flash_used="$bootloader_head_flash_used ($bootloader_head_flash_used_percentage)" + +OUTPUT_BOOTLOADER_FLASH_USED="$output_bootloader_base_flash_used
$output_bootloader_head_flash_used" + +# MARK: Flash Used Delta +bootloader_diff_flash=$(($bootloader_head_flash_used - $bootloader_base_flash_used)) +bootloader_diff_flash_percentage="$((100 * ($bootloader_head_flash_used - $bootloader_base_flash_used) / $bootloader_base_flash_used))%" + +OUTPUT_BOOTLOADER_FLASH_USED_DELTA="" + +if [ $bootloader_diff_flash -lt 0 ]; then + OUTPUT_BOOTLOADER_FLASH_USED_DELTA=":chart_with_downwards_trend:
$bootloader_diff_flash ($bootloader_diff_flash_percentage)" +elif [ $bootloader_diff_flash -gt 0 ]; then + OUTPUT_BOOTLOADER_FLASH_USED_DELTA=":chart_with_upwards_trend:
+$bootloader_diff_flash (+$bootloader_diff_flash_percentage)" +else + OUTPUT_BOOTLOADER_FLASH_USED="$output_bootloader_base_flash_used" + OUTPUT_BOOTLOADER_FLASH_USED_DELTA="ø" +fi + +# MARK: Flash Available +bootloader_base_flash_available="$(($bootloader_memory_section_size - $bootloader_base_flash_used))" +bootloader_base_flash_available_percentage="$((100 * $bootloader_base_flash_available / $bootloader_memory_section_size))%" + +bootloader_head_flash_available="$(($bootloader_memory_section_size - $bootloader_head_flash_used))" +bootloader_head_flash_available_percentage="$((100 * $bootloader_head_flash_available / $bootloader_memory_section_size))%" + +output_bootloader_base_flash_available="$bootloader_base_flash_available ($bootloader_base_flash_available_percentage)" +output_bootloader_head_flash_available="$bootloader_head_flash_available ($bootloader_head_flash_available_percentage)" + +OUTPUT_BOOTLOADER_FLASH_AVAILABLE="$output_bootloader_base_flash_available
$output_bootloader_head_flash_available" + +# MARK: Static RAM +output_bootloader_base_ram_with_percentage="$(getUsedRamSizeWithPercentage $BASE_DIR $bootloader_target_name)" +output_bootloader_head_ram_with_percentage="$(getUsedRamSizeWithPercentage $HEAD_DIR $bootloader_target_name)" + +OUTPUT_BOOTLOADER_RAM="$output_bootloader_base_ram_with_percentage
$output_bootloader_head_ram_with_percentage" + +# MARK: Static RAM Delta + +bootloader_base_ram="$(getUsedRamSize $BASE_DIR $bootloader_target_name)" +bootloader_head_ram="$(getUsedRamSize $HEAD_DIR $bootloader_target_name)" + +bootloader_diff_ram=$(($bootloader_head_ram - $bootloader_base_ram)) +bootloader_diff_ram_percentage="$((100 * ($bootloader_head_ram - $bootloader_base_ram) / $bootloader_base_ram))%" + +OUTPUT_BOOTLOADER_RAM_DELTA="" + +if [ $bootloader_diff_ram -lt 0 ]; then + OUTPUT_BOOTLOADER_RAM_DELTA=":chart_with_downwards_trend:
$bootloader_diff_ram ($bootloader_diff_ram_percentage)" +elif [ $bootloader_diff_ram -gt 0 ]; then + OUTPUT_BOOTLOADER_RAM_DELTA=":chart_with_upwards_trend:
+$bootloader_diff_ram (+$bootloader_diff_ram_percentage)" +else + OUTPUT_BOOTLOADER_RAM="$output_bootloader_base_ram_with_percentage" + OUTPUT_BOOTLOADER_RAM_DELTA="ø" +fi + +# +# MARK: - os statistics +# + +echo "Creating statistics for os" + +leka_os_target_name="LekaOS" +leka_os_memory_section_size="0x17E000" + +createSizeTextFile $BASE_DIR $leka_os_target_name +createSizeTextFile $HEAD_DIR $leka_os_target_name + +# MARK: Flash Used +leka_os_base_flash_used="$(getUsedFlashSize $BASE_DIR $leka_os_target_name)" +leka_os_base_flash_used_percentage="$((100 * $leka_os_base_flash_used / $leka_os_memory_section_size))%" + +leka_os_head_flash_used="$(getUsedFlashSize $HEAD_DIR $leka_os_target_name)" +leka_os_head_flash_used_percentage="$((100 * $leka_os_head_flash_used / $leka_os_memory_section_size))%" + +output_leka_os_base_flash_used="$leka_os_base_flash_used ($leka_os_base_flash_used_percentage)" +output_leka_os_head_flash_used="$leka_os_head_flash_used ($leka_os_head_flash_used_percentage)" + +OUTPUT_LEKA_OS_FLASH_USED="$output_leka_os_base_flash_used
$output_leka_os_head_flash_used" + +# MARK: Flash Used Delta +leka_os_diff_flash=$(($leka_os_head_flash_used - $leka_os_base_flash_used)) +leka_os_diff_flash_percentage="$((100 * ($leka_os_head_flash_used - $leka_os_base_flash_used) / $leka_os_base_flash_used))%" + +OUTPUT_LEKA_OS_FLASH_USED_DELTA="" + +if [ $leka_os_diff_flash -lt 0 ]; then + OUTPUT_LEKA_OS_FLASH_USED_DELTA=":chart_with_downwards_trend:
$leka_os_diff_flash ($leka_os_diff_flash_percentage)" +elif [ $leka_os_diff_flash -gt 0 ]; then + OUTPUT_LEKA_OS_FLASH_USED_DELTA=":chart_with_upwards_trend:
+$leka_os_diff_flash (+$leka_os_diff_flash_percentage)" +else + OUTPUT_LEKA_OS_FLASH_USED="$output_leka_os_base_flash_used" + OUTPUT_LEKA_OS_FLASH_USED_DELTA="ø" +fi + +# MARK: Flash Available +leka_os_base_flash_available="$(($leka_os_memory_section_size - $leka_os_base_flash_used))" +leka_os_base_flash_available_percentage="$((100 * $leka_os_base_flash_available / $leka_os_memory_section_size))%" + +leka_os_head_flash_available="$(($leka_os_memory_section_size - $leka_os_head_flash_used))" +leka_os_head_flash_available_percentage="$((100 * $leka_os_head_flash_available / $leka_os_memory_section_size))%" + +output_leka_os_base_flash_available="$leka_os_base_flash_available ($leka_os_base_flash_available_percentage)" +output_leka_os_head_flash_available="$leka_os_head_flash_available ($leka_os_head_flash_available_percentage)" + +OUTPUT_LEKA_OS_FLASH_AVAILABLE="$output_leka_os_base_flash_available
$output_leka_os_head_flash_available" + +# MARK: Static RAM +output_leka_os_base_ram_with_percentage="$(getUsedRamSizeWithPercentage $BASE_DIR $leka_os_target_name)" +output_leka_os_head_ram_with_percentage="$(getUsedRamSizeWithPercentage $HEAD_DIR $leka_os_target_name)" + +OUTPUT_LEKA_OS_RAM="$output_leka_os_base_ram_with_percentage
$output_leka_os_head_ram_with_percentage" + +# MARK: Static RAM Delta +leka_os_base_ram="$(getUsedRamSize $BASE_DIR $leka_os_target_name)" +leka_os_head_ram="$(getUsedRamSize $HEAD_DIR $leka_os_target_name)" + +leka_os_diff_ram=$(($leka_os_head_ram - $leka_os_base_ram)) +leka_os_diff_ram_percentage="$((100 * ($leka_os_head_ram - $leka_os_base_ram) / $leka_os_base_ram))%" + +OUTPUT_LEKA_OS_RAM_DELTA="" + +if [ $leka_os_diff_ram -lt 0 ]; then + OUTPUT_LEKA_OS_RAM_DELTA=":chart_with_downwards_trend:
$leka_os_diff_ram ($leka_os_diff_ram_percentage)" +elif [ $leka_os_diff_ram -gt 0 ]; then + OUTPUT_LEKA_OS_RAM_DELTA=":chart_with_upwards_trend:
+$leka_os_diff_ram (+$leka_os_diff_ram_percentage)" +else + OUTPUT_LEKA_OS_RAM="$output_leka_os_base_ram_with_percentage" + OUTPUT_LEKA_OS_RAM_DELTA="ø" +fi + +# +# MARK: - markdown output +# + +echo "Creating markdown output" + +echo -n "| Target | Flash Used (base/head) | Fash Used Δ | Flash Available (base/head) | Static RAM (base/head) | Static RAM Δ |\n" >> $FIRMWARE_STATISTICS_OUTPUT +echo -n "|--------|:----------------------:|:-----------:|:---------------------------:|:----------------------:|:------------:|\n" >> $FIRMWARE_STATISTICS_OUTPUT + +echo -n "| bootloader | $OUTPUT_BOOTLOADER_FLASH_USED | $OUTPUT_BOOTLOADER_FLASH_USED_DELTA | $OUTPUT_BOOTLOADER_FLASH_AVAILABLE | $OUTPUT_BOOTLOADER_RAM | $OUTPUT_BOOTLOADER_RAM_DELTA |\n" >> $FIRMWARE_STATISTICS_OUTPUT +echo -n "| os | $OUTPUT_LEKA_OS_FLASH_USED | $OUTPUT_LEKA_OS_FLASH_USED_DELTA | $OUTPUT_LEKA_OS_FLASH_AVAILABLE | $OUTPUT_LEKA_OS_RAM | $OUTPUT_LEKA_OS_RAM_DELTA |\n" >> $FIRMWARE_STATISTICS_OUTPUT diff --git a/.github/actions/compare_toolchain_upgrade/generate_sticky_note.rb b/.github/actions/compare_toolchain_upgrade/generate_sticky_note.rb new file mode 100755 index 0000000000..aa4716b1d5 --- /dev/null +++ b/.github/actions/compare_toolchain_upgrade/generate_sticky_note.rb @@ -0,0 +1,77 @@ +#!/usr/bin/env ruby + +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +FIRMWARE_STATISTICS_OUTPUT_PATH="#{ENV["RUNNER_HOME"]}/FIRMWARE_STATISTICS_OUTPUT.md" +STATUS_DIFF_OUTPUT_PATH="#{ENV["RUNNER_HOME"]}/STATUS_DIFF_OUTPUT.md" +MAP_DIFF_OUTPUT_PATH="#{ENV["RUNNER_HOME"]}/MAP_DIFF_OUTPUT.md" + +FIRMWARE_STATISTICS_OUTPUT=File.read(FIRMWARE_STATISTICS_OUTPUT_PATH) +STATUS_DIFF_OUTPUT=File.read(STATUS_DIFF_OUTPUT_PATH) +MAP_DIFF_OUTPUT=File.read(MAP_DIFF_OUTPUT_PATH) + +sticky_message = <<-EOF + +# Toolchain upgrade analysis report + +## :pushpin: Info + +- enable_log_debug: `#{ENV["ENABLE_LOG_DEBUG"]}` + +| | `base` | `head` | +|-----------|----------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| toolchain | `#{ENV["BASE_ARM_TOOLCHAIN_VERSION"]}` | `#{ENV["HEAD_ARM_TOOLCHAIN_VERSION"]}` | +| branch | [`#{ENV["BASE_REF"]}`](https://github.com/leka/LekaOS/tree/#{ENV["BASE_REF"]}) | [`#{ENV["HEAD_REF"]}`](https://github.com/leka/LekaOS/tree/#{ENV["HEAD_REF"]}) | +| sha | #{ENV["BASE_SHA"]} | #{ENV["HEAD_SHA"]} | +| mbed | `#{ENV["BASE_MBED_VERSION"]}` | `#{ENV["HEAD_MBED_VERSION"]}` | +| `-std` | `#{ENV["BASE_CXX_STANDARD"]}` | `#{ENV["HEAD_CXX_STANDARD"]}` | + +## :robot: Firmware impact analysis + +#{FIRMWARE_STATISTICS_OUTPUT} + +
+Click to show memory sections + +``` +| - | Hex | Bytes | KiB | +|------------|---------:|----------:|-----:| +| Flash | 0x200000 | 2 097 152 | 2048 | +| SRAM | 0x80000 | 524 288 | 512 | +| Bootloader | 0x40000 | 262 144 | 256 | +| Header | 0x1000 | 4 096 | 4 | +| OS | 0x17E000 | 1 564 672 | 1528 | +| Tail | 0x1000 | 4 096 | 4 | +| Scratch | 0x40000 | 262 144 | 256 | +``` + +
+ +## :microscope: Detailed impact analysis + +
+Click to show detailed analysis for all targets + +- :heavy_check_mark: - existing target +- :sparkles: - new target +- :coffin: - deleted target +- :white_check_mark: - files are the same +- :x: - files are different + +#{STATUS_DIFF_OUTPUT} + +## :world_map: Map files diff output + +#{MAP_DIFF_OUTPUT} + +
+ +EOF + +puts sticky_message + +STICKY_MESSAGE_PATH = "#{ENV["RUNNER_HOME"]}/STICKY_MESSAGE.md" + +File.write(STICKY_MESSAGE_PATH, "#{sticky_message}", mode: 'a') diff --git a/.github/actions/compare_toolchain_upgrade/get_all_targets.sh b/.github/actions/compare_toolchain_upgrade/get_all_targets.sh new file mode 100755 index 0000000000..189311a31a --- /dev/null +++ b/.github/actions/compare_toolchain_upgrade/get_all_targets.sh @@ -0,0 +1,45 @@ +# Leka - LekaOS +# Copyright 2021 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +shopt -s xpg_echo + +BASE_DIR=$1 +HEAD_DIR=$2 + +# +# MARK: - Find all targets +# + +echo "Set all targets" + +base_targets=($(echo $(find $BASE_DIR -name '*.bin' -execdir basename -s '.bin' {} +) | tr ' ' '\n' | sort -du | tr '\n' ' ')) +head_targets=($(echo $(find $HEAD_DIR -name '*.bin' -execdir basename -s '.bin' {} +) | tr ' ' '\n' | sort -du | tr '\n' ' ')) +all_targets=($(echo "${base_targets[@]} ${head_targets[@]}" | tr ' ' '\n' | sort -du | tr '\n' ' ')) + +# +# MARK: - Find added/deleted targets +# + +echo "Set added/deleted targets" + +added_targets=() +deleted_targets=() + +for target in "${all_targets[@]}"; do + if [[ ${base_targets[*]} =~ "$target" ]] && ! [[ ${head_targets[*]} =~ "$target" ]]; then + deleted_targets+=($target) + elif ! [[ ${base_targets[*]} =~ "$target" ]] && [[ ${head_targets[*]} =~ "$target" ]]; then + added_targets+=($target) + fi +done + +# +# MARK: - Echo results +# + +echo "all: ${all_targets[*]}" +echo "base: ${base_targets[*]}" +echo "head: ${head_targets[*]}" +echo "added: ${added_targets[*]}" +echo "deleted: ${deleted_targets[*]}" diff --git a/.github/actions/compare_toolchain_upgrade/get_diffs.sh b/.github/actions/compare_toolchain_upgrade/get_diffs.sh new file mode 100755 index 0000000000..04b947bafc --- /dev/null +++ b/.github/actions/compare_toolchain_upgrade/get_diffs.sh @@ -0,0 +1,80 @@ +shopt -s xpg_echo + +BASE_DIR=$1 +HEAD_DIR=$2 + +source ./.github/actions/compare_base_head_changes/utils.sh +source ./.github/actions/compare_base_head_changes/get_all_targets.sh + +MAP_DIFF_OUTPUT="$RUNNER_HOME/MAP_DIFF_OUTPUT.md" +touch $MAP_DIFF_OUTPUT + +no_map_diff=true + +for target in "${all_targets[@]}"; do +target_name=$target + + if ! [[ " ${added_targets[*]} " =~ " $target " ]] && ! [[ " ${deleted_targets[*]} " =~ " $target " ]]; then + + echo "$target not deleted nor new, running diff" + + if ! diff_map_output=$(diff --unified=150 $BASE_DIR/$target_name-map.txt $HEAD_DIR/$target_name-map.txt); then + + if ! diff_size_output=$(diff --unified=150 $BASE_DIR/$target_name-code_size.txt $HEAD_DIR/$target_name-code_size.txt); then + diff_size_output=$(cat $HEAD_DIR/$target_name-code_size.txt) + fi + + echo $diff_map_output + echo $diff_size_output + + echo "
" >> $MAP_DIFF_OUTPUT + echo "$target_name (click to expand)" >> $MAP_DIFF_OUTPUT + echo "" >> $MAP_DIFF_OUTPUT + + echo "\`\`\`diff" >> $MAP_DIFF_OUTPUT + echo "$diff_map_output" >> $MAP_DIFF_OUTPUT + echo "\`\`\`" >> $MAP_DIFF_OUTPUT + + echo "\`\`\`diff" >> $MAP_DIFF_OUTPUT + echo "$diff_size_output" >> $MAP_DIFF_OUTPUT + echo "\`\`\`" >> $MAP_DIFF_OUTPUT + + echo "" >> $MAP_DIFF_OUTPUT + echo "
" >> $MAP_DIFF_OUTPUT + echo "" >> $MAP_DIFF_OUTPUT + + no_map_diff=false + fi + + elif [[ " ${added_targets[*]} " =~ " $target " ]]; then + + echo "$target added, showing map information" + + map_output=$(cat $HEAD_DIR/$target_name-map.txt) + size_output=$(cat $HEAD_DIR/$target_name-code_size.txt) + + echo "
" >> $MAP_DIFF_OUTPUT + echo "$target_name (click to expand)" >> $MAP_DIFF_OUTPUT + echo "" >> $MAP_DIFF_OUTPUT + + echo "\`\`\`" >> $MAP_DIFF_OUTPUT + echo "$map_output" >> $MAP_DIFF_OUTPUT + echo "\`\`\`" >> $MAP_DIFF_OUTPUT + + echo "\`\`\`" >> $MAP_DIFF_OUTPUT + echo "$size_output" >> $MAP_DIFF_OUTPUT + echo "\`\`\`" >> $MAP_DIFF_OUTPUT + + echo "" >> $MAP_DIFF_OUTPUT + echo "
" >> $MAP_DIFF_OUTPUT + echo "" >> $MAP_DIFF_OUTPUT + + no_map_diff=false + + fi + +done + +if $no_map_diff; then + echo "No differenes where found in map files." >> $MAP_DIFF_OUTPUT +fi diff --git a/.github/actions/compare_toolchain_upgrade/utils.sh b/.github/actions/compare_toolchain_upgrade/utils.sh new file mode 100644 index 0000000000..08c51415ba --- /dev/null +++ b/.github/actions/compare_toolchain_upgrade/utils.sh @@ -0,0 +1,86 @@ +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +# +# MARK: - create files +# + +function createMapTextFile() { + local DIR=$1 + local TARGET_NAME=$2 + + python3 ./extern/mbed-os/tools/memap.py -t GCC_ARM $DIR/$TARGET_NAME.map > $DIR/$TARGET_NAME-map.txt +} + +function createSizeTextFile() { + local DIR=$1 + local TARGET_NAME=$2 + + bash ./tools/get_code_size.sh $DIR/$TARGET_NAME.elf --markdown > $DIR/$TARGET_NAME-code_size.txt +} + +# +# MARK: - get flash/ram +# + +function getUsedFlashSize() { + local DIR=$1 + local TARGET_NAME=$2 + + local SIZE=$(grep -Po '(?<=Flash used:\s)[[:digit:]]*' $DIR/$TARGET_NAME-code_size.txt) + echo $SIZE +} + + +function getUsedFlashSizeWithPercentage() { + local DIR=$1 + local TARGET_NAME=$2 + + local SIZE=$(grep -Po '(?<=Flash used:\s)[[:digit:]]* \([[:digit:]]*%\)' $DIR/$TARGET_NAME-code_size.txt) + echo $SIZE +} + +function getUsedRamSize() { + local DIR=$1 + local TARGET_NAME=$2 + + local SIZE=$(grep -Po '(?<=SRAM used:\s)[[:digit:]]*' $DIR/$TARGET_NAME-code_size.txt) + echo $SIZE +} + +function getUsedRamSizeWithPercentage() { + local DIR=$1 + local TARGET_NAME=$2 + + local SIZE=$(grep -Po '(?<=SRAM used:\s)[[:digit:]]* \([[:digit:]]*%\)' $DIR/$TARGET_NAME-code_size.txt) + echo $SIZE +} + +# +# MARK: - calculate diff/percentage +# + +function diff_flash_base_head() { + local BASE_FLASH=$1 + local HEAD_FLASH=$2 + + local DIFF=$(($HEAD_FLASH - $BASE_FLASH)) + echo $DIFF +} + +function diff_flash_base_head_percentage() { + local BASE_FLASH=$1 + local HEAD_FLASH=$2 + + local PERCENTAGE="$((100 * ($HEAD_FLASH - $BASE_FLASH) / $BASE_FLASH))%" + echo $PERCENTAGE +} + +function diff_flash_percentage() { + local BASE_FLASH=$1 + local HEAD_FLASH=$2 + + local PERCENTAGE="$((100 * ($HEAD_FLASH - $BASE_FLASH) / $BASE_FLASH))%" + echo $PERCENTAGE +} diff --git a/.github/workflows/ci-code_analysis-toolchain_upgrade.yml b/.github/workflows/ci-code_analysis-toolchain_upgrade.yml new file mode 100644 index 0000000000..78f58e21e6 --- /dev/null +++ b/.github/workflows/ci-code_analysis-toolchain_upgrade.yml @@ -0,0 +1,246 @@ +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Code Analysis - Toolchain upgrade + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "config/toolchain_gcc_arm_none_eabi_url" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # + # Mark: - Job - build_base_toolchain + # + + build_base_toolchain: + name: build w/ base toolchain + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + enable_log_debug: ["ON", "OFF"] + + steps: + # + # Mark: - Setup + # + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Setup CI + id: setup_ci + uses: ./.github/actions/setup + with: + ccache_key: ${{ runner.os }}-ccache-toolchain_upgrade-build_base_toolchain-enable_log_debug-(${{ matrix.enable_log_debug }}) + ccache_restore_keys: | + ${{ runner.os }}-ccache-toolchain_upgrade-build_base_toolchain-enable_log_debug-(${{ matrix.enable_log_debug }})- + ${{ runner.os }}-ccache-toolchain_upgrade-build_base_toolchain-enable_log_debug- + ${{ runner.os }}-ccache-toolchain_upgrade-build_base_toolchain- + ${{ runner.os }}-ccache-toolchain_upgrade- + ${{ runner.os }}-ccache- + + - name: Install BASE Arm GCC Toolchain + run: | + rm -rf ${{ env.RUNNER_HOME }}/arm_gnu_toolchain + wget -c ${{ env.BASE_ARM_TOOLCHAIN_URL }} -O ${{ env.BASE_ARM_TOOLCHAIN_ARCHIVE }} + mkdir -p ${{ env.BASE_ARM_TOOLCHAIN_EXTRACT_DIRECTORY }} + tar -xvf ${{ env.BASE_ARM_TOOLCHAIN_ARCHIVE }} -C ${{ env.BASE_ARM_TOOLCHAIN_EXTRACT_DIRECTORY }} --strip-components=1 + rm -rf ${{ env.BASE_ARM_TOOLCHAIN_ARCHIVE }} + mv ${{ env.BASE_ARM_TOOLCHAIN_EXTRACT_DIRECTORY }} ${{ env.RUNNER_HOME }}/arm_gnu_toolchain + + - name: Add BASE Arm GCC Toolchain to path + run: | + echo "${{ env.RUNNER_HOME }}/arm_gnu_toolchain/bin" >> $GITHUB_PATH + + - name: Test BASE Arm GCC Toolchain + run: | + ls -al ${{ env.RUNNER_HOME }}/arm_gnu_toolchain/bin + arm-none-eabi-gcc -v + + - name: Add BASE Arm GCC Toolchain version to env + run: | + BASE_ARM_TOOLCHAIN_VERSION=$(arm-none-eabi-gcc --version | grep -Po '(?<=gcc ).*') + echo "BASE_ARM_TOOLCHAIN_VERSION=$BASE_ARM_TOOLCHAIN_VERSION" >> $GITHUB_ENV + + # + # Mark: - Config & build + # + + - name: Config & build + id: config_build + uses: ./.github/actions/config_build + with: + enable_log_debug: ${{ matrix.enable_log_debug }} + checkout_base_ref: true + + # + # Mark: - Move files + # + + - name: Move ${{ env.BASE_REF }}:${{ env.BASE_SHA }} bin & map files to temporary directory + run: | + mkdir -p build_artifacts + find _build ! -path '*CMakeFiles*' -name "*.bin" -print0 | xargs -0 -I {} cp {} build_artifacts + find _build ! -path '*CMakeFiles*' -name "*.map" -print0 | xargs -0 -I {} cp {} build_artifacts + find _build ! -path '*CMakeFiles*' -name "*.hex" -print0 | xargs -0 -I {} cp {} build_artifacts + find _build ! -path '*CMakeFiles*' -name "*.elf" -print0 | xargs -0 -I {} cp {} build_artifacts + + # + # Mark: - Upload artifacts + # + + - uses: actions/upload-artifact@v3 + with: + name: base_ref-build-enable_log_debug-${{ matrix.enable_log_debug }} + path: build_artifacts + retention-days: 1 + + outputs: + BASE_ARM_TOOLCHAIN_VERSION: ${{ env.BASE_ARM_TOOLCHAIN_VERSION }} + + # + # Mark: - Job - build_head_toolchain + # + + build_head_toolchain: + name: build w/ new toolchain + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + enable_log_debug: ["ON", "OFF"] + + steps: + # + # Mark: - Setup + # + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Setup CI + id: setup_ci + uses: ./.github/actions/setup + with: + ccache_key: ${{ runner.os }}-ccache-toolchain_upgrade-build_head_toolchain-enable_log_debug-(${{ matrix.enable_log_debug }}) + ccache_restore_keys: | + ${{ runner.os }}-ccache-toolchain_upgrade-build_head_toolchain-enable_log_debug-(${{ matrix.enable_log_debug }})- + ${{ runner.os }}-ccache-toolchain_upgrade-build_head_toolchain-enable_log_debug- + ${{ runner.os }}-ccache-toolchain_upgrade-build_head_toolchain- + ${{ runner.os }}-ccache-toolchain_upgrade- + ${{ runner.os }}-ccache- + + - name: Install HEAD Arm GCC Toolchain + run: | + rm -rf ${{ env.RUNNER_HOME }}/arm_gnu_toolchain + wget -c ${{ env.HEAD_ARM_TOOLCHAIN_URL }} -O ${{ env.HEAD_ARM_TOOLCHAIN_ARCHIVE }} + mkdir -p ${{ env.HEAD_ARM_TOOLCHAIN_EXTRACT_DIRECTORY }} + tar -xvf ${{ env.HEAD_ARM_TOOLCHAIN_ARCHIVE }} -C ${{ env.HEAD_ARM_TOOLCHAIN_EXTRACT_DIRECTORY }} --strip-components=1 + rm -rf ${{ env.HEAD_ARM_TOOLCHAIN_ARCHIVE }} + mv ${{ env.HEAD_ARM_TOOLCHAIN_EXTRACT_DIRECTORY }} ${{ env.RUNNER_HOME }}/arm_gnu_toolchain + + - name: Add HEAD Arm GCC Toolchain to path + run: | + echo "${{ env.RUNNER_HOME }}/arm_gnu_toolchain/bin" >> $GITHUB_PATH + + - name: Test HEAD Arm GCC Toolchain + run: | + ls -al ${{ env.RUNNER_HOME }}/arm_gnu_toolchain/bin + arm-none-eabi-gcc -v + + - name: Add HEAD Arm GCC Toolchain version to env + run: | + HEAD_ARM_TOOLCHAIN_VERSION=$(arm-none-eabi-gcc --version | grep -Po '(?<=gcc ).*') + echo "HEAD_ARM_TOOLCHAIN_VERSION=$HEAD_ARM_TOOLCHAIN_VERSION" >> $GITHUB_ENV + + # + # Mark: - Config & build + # + + - name: Config & build + id: config_build + uses: ./.github/actions/config_build + with: + enable_log_debug: ${{ matrix.enable_log_debug }} + checkout_base_ref: false + + # + # Mark: - Move files + # + + - name: Move ${{ env.HEAD_REF }}:${{ env.HEAD_SHA }} bin & map files to temporary directory + run: | + mkdir -p build_artifacts + find _build ! -path '*CMakeFiles*' -name "*.bin" -print0 | xargs -0 -I {} cp {} build_artifacts + find _build ! -path '*CMakeFiles*' -name "*.map" -print0 | xargs -0 -I {} cp {} build_artifacts + find _build ! -path '*CMakeFiles*' -name "*.hex" -print0 | xargs -0 -I {} cp {} build_artifacts + find _build ! -path '*CMakeFiles*' -name "*.elf" -print0 | xargs -0 -I {} cp {} build_artifacts + + # + # Mark: - Upload artifacts + # + + - uses: actions/upload-artifact@v3 + with: + name: head_ref-build-enable_log_debug-${{ matrix.enable_log_debug }} + path: build_artifacts + retention-days: 1 + + outputs: + HEAD_ARM_TOOLCHAIN_VERSION: ${{ env.HEAD_ARM_TOOLCHAIN_VERSION }} + + # + # Mark: - Job - compare_bin_map_files + # + + compare_base_head_files: + name: Compare base/head files + runs-on: ubuntu-22.04 + needs: [build_base_toolchain, build_head_toolchain] + + strategy: + fail-fast: true + matrix: + enable_log_debug: ["ON", "OFF"] + + steps: + # + # Mark: - Setup + # + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Setup CI + id: setup_ci + uses: ./.github/actions/setup + + - uses: actions/download-artifact@v3 + with: + path: build_artifacts + + - name: Display structure of downloaded files + working-directory: build_artifacts + run: ls -R + + - name: Compare files + uses: ./.github/actions/compare_toolchain_upgrade + with: + comment_header: enable_log_debug-${{ matrix.enable_log_debug }} + enable_log_debug: ${{ matrix.enable_log_debug }} + base_dir: build_artifacts/base_ref-build-enable_log_debug-${{ matrix.enable_log_debug }} + head_dir: build_artifacts/head_ref-build-enable_log_debug-${{ matrix.enable_log_debug }} + env: + BASE_ARM_TOOLCHAIN_VERSION: ${{ needs.build_base_toolchain.outputs.BASE_ARM_TOOLCHAIN_VERSION }} + HEAD_ARM_TOOLCHAIN_VERSION: ${{ needs.build_head_toolchain.outputs.HEAD_ARM_TOOLCHAIN_VERSION }} From 517f504929800eaf3b81e9234a8ae04a72ba253e Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Tue, 21 Feb 2023 17:38:32 +0100 Subject: [PATCH 124/143] :construction_worker: (workflow): Do not run base/head comparison on toolchain upgrade --- .github/workflows/ci-code_analysis-impact_of_changes.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-code_analysis-impact_of_changes.yml b/.github/workflows/ci-code_analysis-impact_of_changes.yml index 0177df3735..bc02f34562 100644 --- a/.github/workflows/ci-code_analysis-impact_of_changes.yml +++ b/.github/workflows/ci-code_analysis-impact_of_changes.yml @@ -7,6 +7,8 @@ name: Code Analysis - Impact of changes on: pull_request: types: [opened, synchronize, reopened] + paths-ignore: + - "config/toolchain_gcc_arm_none_eabi_url" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 331e6ee8df4e0c57bc5b224719636e2a0e20d8ec Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Fri, 24 Feb 2023 23:55:43 +0100 Subject: [PATCH 125/143] :page_facing_up: (workflows): Add or update licenses, copyright date --- .github/actions/compare_base_head_changes/get_diffs.sh | 4 ++++ .github/actions/compare_toolchain_upgrade/action.yml | 2 +- .github/actions/compare_toolchain_upgrade/get_diffs.sh | 4 ++++ .github/workflows/ci-code_analysis-toolchain_upgrade.yml | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/actions/compare_base_head_changes/get_diffs.sh b/.github/actions/compare_base_head_changes/get_diffs.sh index 616566b488..909b870413 100755 --- a/.github/actions/compare_base_head_changes/get_diffs.sh +++ b/.github/actions/compare_base_head_changes/get_diffs.sh @@ -1,3 +1,7 @@ +# Leka - LekaOS +# Copyright 2022 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + shopt -s xpg_echo BASE_DIR=$1 diff --git a/.github/actions/compare_toolchain_upgrade/action.yml b/.github/actions/compare_toolchain_upgrade/action.yml index bbd4711669..b37f096da1 100644 --- a/.github/actions/compare_toolchain_upgrade/action.yml +++ b/.github/actions/compare_toolchain_upgrade/action.yml @@ -1,5 +1,5 @@ # Leka - LekaOS -# Copyright 2021 APF France handicap +# Copyright 2023 APF France handicap # SPDX-License-Identifier: Apache-2.0 name: "Compare toolchain upgrade" diff --git a/.github/actions/compare_toolchain_upgrade/get_diffs.sh b/.github/actions/compare_toolchain_upgrade/get_diffs.sh index 04b947bafc..3600f6e45e 100755 --- a/.github/actions/compare_toolchain_upgrade/get_diffs.sh +++ b/.github/actions/compare_toolchain_upgrade/get_diffs.sh @@ -1,3 +1,7 @@ +# Leka - LekaOS +# Copyright 2021 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + shopt -s xpg_echo BASE_DIR=$1 diff --git a/.github/workflows/ci-code_analysis-toolchain_upgrade.yml b/.github/workflows/ci-code_analysis-toolchain_upgrade.yml index 78f58e21e6..0fac4dbe12 100644 --- a/.github/workflows/ci-code_analysis-toolchain_upgrade.yml +++ b/.github/workflows/ci-code_analysis-toolchain_upgrade.yml @@ -1,5 +1,5 @@ # Leka - LekaOS -# Copyright 2022 APF France handicap +# Copyright 2023 APF France handicap # SPDX-License-Identifier: Apache-2.0 name: Code Analysis - Toolchain upgrade From bb59bd51b076d8de723c74a800856a0a5a6a67da Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Sat, 25 Feb 2023 00:07:30 +0100 Subject: [PATCH 126/143] :children_crossing: (workflows): Improve analysis reports - add nice emojis - fix typos --- .github/actions/check_version/action.yml | 2 +- .github/actions/check_version/generate_report.sh | 5 ++--- .github/actions/compare_base_head_changes/action.yml | 2 +- .../actions/compare_base_head_changes/generate_statistics.sh | 2 +- .../actions/compare_toolchain_upgrade/generate_statistics.sh | 2 +- .../compare_toolchain_upgrade/generate_sticky_note.rb | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/actions/check_version/action.yml b/.github/actions/check_version/action.yml index 588de253df..422ee98220 100644 --- a/.github/actions/check_version/action.yml +++ b/.github/actions/check_version/action.yml @@ -58,7 +58,7 @@ runs: with: header: publish_version message: | - # Version comparison + # :bookmark: Version comparison ${{ env.VERSION_COMPARISON_OUTPUT }} diff --git a/.github/actions/check_version/generate_report.sh b/.github/actions/check_version/generate_report.sh index 3ef258494a..7170f09d4f 100755 --- a/.github/actions/check_version/generate_report.sh +++ b/.github/actions/check_version/generate_report.sh @@ -39,11 +39,10 @@ echo "Creating markdown output" echo 'VERSION_COMPARISON_OUTPUT<> $GITHUB_ENV -echo -n "| - | Version | Same as filename | Same as os_version |\n" >> $GITHUB_ENV +echo -n "| | Version | Same as filename | Same as os_version |\n" >> $GITHUB_ENV echo -n "|:---------------------------------:|:--------------------------:|:----------------:|:--------------------:|\n" >> $GITHUB_ENV - echo -n "| **os** |\`$OUTPUT_OS_VERSION\` |$OUTPUT_OS_VERSION_SAME_AS_FILE |$OUTPUT_OS_VERSION_SAME_AS_OS_VERSION_CONFIG |\n" >> $GITHUB_ENV -echo -n "| **firmware**
(os + bootloader) |\`$OUTPUT_FIRMWARE_VERSION\`|$OUTPUT_FIRMWARE_VERSION_SAME_AS_FILE|$OUTPUT_FIRMWARE_VERSION_SAME_AS_OS_VERSION_CONFIG|\n" >> $GITHUB_ENV +echo -n "| **firmware**
(bootloader + os) |\`$OUTPUT_FIRMWARE_VERSION\`|$OUTPUT_FIRMWARE_VERSION_SAME_AS_FILE|$OUTPUT_FIRMWARE_VERSION_SAME_AS_OS_VERSION_CONFIG|\n" >> $GITHUB_ENV echo 'EOF_VERSION_COMPARISON_OUTPUT' >> $GITHUB_ENV diff --git a/.github/actions/compare_base_head_changes/action.yml b/.github/actions/compare_base_head_changes/action.yml index b5e44f1d79..09dd4ad25d 100644 --- a/.github/actions/compare_base_head_changes/action.yml +++ b/.github/actions/compare_base_head_changes/action.yml @@ -45,7 +45,7 @@ runs: with: header: compare_base_head_changes-${{ inputs.comment_header }} message: | - # PR changes analysis report + # :chart_with_upwards_trend: Changes Impact Analysis Report ## :pushpin: Info diff --git a/.github/actions/compare_base_head_changes/generate_statistics.sh b/.github/actions/compare_base_head_changes/generate_statistics.sh index a238a88768..d04968b1ec 100755 --- a/.github/actions/compare_base_head_changes/generate_statistics.sh +++ b/.github/actions/compare_base_head_changes/generate_statistics.sh @@ -168,7 +168,7 @@ echo "Creating markdown output" echo 'FIRMWARE_STATISTICS_OUTPUT<> $GITHUB_ENV -echo -n "| Target | Flash Used (base/head) | Fash Used Δ | Flash Available (base/head) | Static RAM (base/head) | Static RAM Δ |\n" >> $GITHUB_ENV +echo -n "| Target | Flash Used (base/head) | Flash Used Δ | Flash Available (base/head) | Static RAM (base/head) | Static RAM Δ |\n" >> $GITHUB_ENV echo -n "|--------|:----------------------:|:-----------:|:---------------------------:|:----------------------:|:------------:|\n" >> $GITHUB_ENV echo -n "| bootloader | $OUTPUT_BOOTLOADER_FLASH_USED | $OUTPUT_BOOTLOADER_FLASH_USED_DELTA | $OUTPUT_BOOTLOADER_FLASH_AVAILABLE | $OUTPUT_BOOTLOADER_RAM | $OUTPUT_BOOTLOADER_RAM_DELTA |\n" >> $GITHUB_ENV diff --git a/.github/actions/compare_toolchain_upgrade/generate_statistics.sh b/.github/actions/compare_toolchain_upgrade/generate_statistics.sh index f053007e96..ade886bdc0 100755 --- a/.github/actions/compare_toolchain_upgrade/generate_statistics.sh +++ b/.github/actions/compare_toolchain_upgrade/generate_statistics.sh @@ -169,7 +169,7 @@ fi echo "Creating markdown output" -echo -n "| Target | Flash Used (base/head) | Fash Used Δ | Flash Available (base/head) | Static RAM (base/head) | Static RAM Δ |\n" >> $FIRMWARE_STATISTICS_OUTPUT +echo -n "| Target | Flash Used (base/head) | Flash Used Δ | Flash Available (base/head) | Static RAM (base/head) | Static RAM Δ |\n" >> $FIRMWARE_STATISTICS_OUTPUT echo -n "|--------|:----------------------:|:-----------:|:---------------------------:|:----------------------:|:------------:|\n" >> $FIRMWARE_STATISTICS_OUTPUT echo -n "| bootloader | $OUTPUT_BOOTLOADER_FLASH_USED | $OUTPUT_BOOTLOADER_FLASH_USED_DELTA | $OUTPUT_BOOTLOADER_FLASH_AVAILABLE | $OUTPUT_BOOTLOADER_RAM | $OUTPUT_BOOTLOADER_RAM_DELTA |\n" >> $FIRMWARE_STATISTICS_OUTPUT diff --git a/.github/actions/compare_toolchain_upgrade/generate_sticky_note.rb b/.github/actions/compare_toolchain_upgrade/generate_sticky_note.rb index aa4716b1d5..20a63c7dc4 100755 --- a/.github/actions/compare_toolchain_upgrade/generate_sticky_note.rb +++ b/.github/actions/compare_toolchain_upgrade/generate_sticky_note.rb @@ -14,7 +14,7 @@ sticky_message = <<-EOF -# Toolchain upgrade analysis report +# :toolbox: Toolchain Upgrade Analysis Report ## :pushpin: Info From 5a79f8d060fe09d48e5b9c0357d6ac8c6ef175c1 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Sat, 25 Feb 2023 00:20:55 +0100 Subject: [PATCH 127/143] :construction_worker: (workflows): impact_of_changes - path-ignore toolchain upgrade files --- .github/workflows/ci-code_analysis-impact_of_changes.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-code_analysis-impact_of_changes.yml b/.github/workflows/ci-code_analysis-impact_of_changes.yml index bc02f34562..d3f636d31b 100644 --- a/.github/workflows/ci-code_analysis-impact_of_changes.yml +++ b/.github/workflows/ci-code_analysis-impact_of_changes.yml @@ -9,6 +9,8 @@ on: types: [opened, synchronize, reopened] paths-ignore: - "config/toolchain_gcc_arm_none_eabi_url" + - ".github/workflows/ci-code_analysis-toolchain_upgrade.yml" + - ".github/actions/compare_toolchain_upgrade/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From a1395a995434ac8577de8746b1d94d9e33a310f9 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 8 Mar 2023 10:26:10 +0100 Subject: [PATCH 128/143] :wrench: (tools): Python - add mbed_requirements.txt file This allow us to control what goes inside and to not depend on external repositories to install those (i.e. no need to clone mbed-os to access the requirements) also: - add pyelftool dep - update setup action - update documentation --- .github/actions/setup/action.yml | 2 +- docs/INSTALL.md | 2 +- tools/config/mbed_requirements.txt | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 tools/config/mbed_requirements.txt diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 7b47ae8add..4707f75101 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -127,7 +127,7 @@ runs: shell: bash run: | pip install --upgrade --upgrade-strategy eager mbed-cli mbed-tools imgtool - pip install --upgrade --upgrade-strategy eager -r ./extern/mbed-os/requirements.txt + pip install --upgrade --upgrade-strategy eager -r ./tools/config/mbed_requirements.txt pip install --upgrade --upgrade-strategy eager -r ./extern/mcuboot/scripts/requirements.txt - name: Test pip packages diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 4095732b65..87c8c62895 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -71,7 +71,7 @@ python3 -m pip install -U --user mbed-cli mbed-tools python3 -m pip install -U --user pyserial intelhex prettytable # and finally mbed-cli requirements -python3 -m pip install -U --user -r https://raw.githubusercontent.com/ARMmbed/mbed-os/master/requirements.txt +python3 -m pip install -U --user -r ./tools/config/mbed_requirements.txt ``` ### 3. Install arm-none-eabi-gcc diff --git a/tools/config/mbed_requirements.txt b/tools/config/mbed_requirements.txt new file mode 100644 index 0000000000..9e80f591fc --- /dev/null +++ b/tools/config/mbed_requirements.txt @@ -0,0 +1,12 @@ +PrettyTable<=1.0.1; python_version < '3.6' +prettytable>=2.0,<3.0; python_version >= '3.6' +future>=0.18.0,<1.0 +jinja2>=2.11.3 +intelhex>=2.3.0,<3.0.0 +mbed-tools +mbed-os-tools +pyelftools + +# needed for signing secure images +cryptography +cbor From 57f1060497a9f1efb4ff423047e7cff2d29d4a94 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Mon, 6 Mar 2023 09:47:13 +0100 Subject: [PATCH 129/143] :building_construction: ((interface)): Add IMUKit interface --- include/interface/libs/IMUKit.hpp | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 include/interface/libs/IMUKit.hpp diff --git a/include/interface/libs/IMUKit.hpp b/include/interface/libs/IMUKit.hpp new file mode 100644 index 0000000000..4d2f80459b --- /dev/null +++ b/include/interface/libs/IMUKit.hpp @@ -0,0 +1,36 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +namespace leka { + +struct EulerAngles { + float pitch; + float roll; + float yaw; +}; + +namespace interface { + + class IMUKit + { + public: + using angles_ready_callback_t = std::function; + + virtual ~IMUKit() = default; + + virtual void start() = 0; + virtual void stop() = 0; + + virtual void setOrigin() = 0; + virtual void onEulerAnglesReady(angles_ready_callback_t const &callback) = 0; + [[nodiscard]] virtual auto getEulerAngles() const -> EulerAngles = 0; + }; + +} // namespace interface + +} // namespace leka From 3c8bfd9854d925b4ccf6c9631b1ae979adcfd33c Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Mon, 6 Mar 2023 09:59:37 +0100 Subject: [PATCH 130/143] :arrow_up: (IMUKit): IMUKit derive from interface --- app/os/main.cpp | 1 + libs/IMUKit/include/IMUKit.hpp | 17 ++++++---------- spikes/lk_command_kit/main.cpp | 1 + tests/unit/mocks/mocks/leka/IMUKit.hpp | 28 ++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 tests/unit/mocks/mocks/leka/IMUKit.hpp diff --git a/app/os/main.cpp b/app/os/main.cpp index 3ac5081c44..aa65b7270b 100644 --- a/app/os/main.cpp +++ b/app/os/main.cpp @@ -47,6 +47,7 @@ #include "FlashNumberCounting.h" #include "FoodRecognition.h" #include "HelloWorld.h" +#include "IMUKit.hpp" #include "LedColorRecognition.h" #include "LedKit.h" #include "LedNumberCounting.h" diff --git a/libs/IMUKit/include/IMUKit.hpp b/libs/IMUKit/include/IMUKit.hpp index 44dc217529..0158757418 100644 --- a/libs/IMUKit/include/IMUKit.hpp +++ b/libs/IMUKit/include/IMUKit.hpp @@ -5,26 +5,21 @@ #pragma once #include "interface/LSM6DSOX.hpp" +#include "interface/libs/IMUKit.hpp" namespace leka { -struct EulerAngles { - float pitch; - float roll; - float yaw; -}; - -class IMUKit +class IMUKit : public interface::IMUKit { public: explicit IMUKit(interface::LSM6DSOX &lsm6dsox) : _lsm6dsox(lsm6dsox) {} void init(); - void start(); - void stop(); + void start() final; + void stop() final; - void setOrigin(); - [[nodiscard]] auto getEulerAngles() const -> EulerAngles; + void setOrigin() final; + [[nodiscard]] auto getEulerAngles() const -> EulerAngles final; private: void drdy_callback(interface::LSM6DSOX::SensorData data); diff --git a/spikes/lk_command_kit/main.cpp b/spikes/lk_command_kit/main.cpp index 8282dfe00c..248a1acc0b 100644 --- a/spikes/lk_command_kit/main.cpp +++ b/spikes/lk_command_kit/main.cpp @@ -32,6 +32,7 @@ #include "EventLoopKit.h" #include "FATFileSystem.h" #include "HelloWorld.h" +#include "IMUKit.hpp" #include "LedKit.h" #include "LogKit.h" #include "ReinforcerKit.h" diff --git a/tests/unit/mocks/mocks/leka/IMUKit.hpp b/tests/unit/mocks/mocks/leka/IMUKit.hpp new file mode 100644 index 0000000000..cb93b1b90f --- /dev/null +++ b/tests/unit/mocks/mocks/leka/IMUKit.hpp @@ -0,0 +1,28 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "gmock/gmock.h" +#include "interface/libs/IMUKit.hpp" + +namespace leka::mock { + +class IMUKit : public interface::IMUKit +{ + public: + MOCK_METHOD(void, start, (), (override)); + MOCK_METHOD(void, stop, (), (override)); + MOCK_METHOD(void, setOrigin, (), (override)); + MOCK_METHOD(EulerAngles, getEulerAngles, (), (const, override)); + + void onEulerAnglesReady(angles_ready_callback_t const &cb) override { angles_ready_callback = cb; } + + void call_angles_ready_callback(const EulerAngles &data) { angles_ready_callback(data); } + + private: + angles_ready_callback_t angles_ready_callback {}; +}; + +} // namespace leka::mock From 75de22d1a4ea1bc9f62e9a848c66d47a15ef86da Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Mon, 6 Mar 2023 10:10:39 +0100 Subject: [PATCH 131/143] :sparkles: (IMUKit): Add onEulerAnglesReady function --- libs/IMUKit/include/IMUKit.hpp | 2 ++ libs/IMUKit/source/IMUKit.cpp | 9 ++++++++ libs/IMUKit/tests/IMUKit_test.cpp | 35 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/libs/IMUKit/include/IMUKit.hpp b/libs/IMUKit/include/IMUKit.hpp index 0158757418..ababcfb531 100644 --- a/libs/IMUKit/include/IMUKit.hpp +++ b/libs/IMUKit/include/IMUKit.hpp @@ -19,6 +19,7 @@ class IMUKit : public interface::IMUKit void stop() final; void setOrigin() final; + void onEulerAnglesReady(angles_ready_callback_t const &callback) final; [[nodiscard]] auto getEulerAngles() const -> EulerAngles final; private: @@ -26,6 +27,7 @@ class IMUKit : public interface::IMUKit interface::LSM6DSOX &_lsm6dsox; EulerAngles _euler_angles {}; + angles_ready_callback_t _on_euler_angles_rdy_callback {}; }; } // namespace leka diff --git a/libs/IMUKit/source/IMUKit.cpp b/libs/IMUKit/source/IMUKit.cpp index c1e2f5a3e8..d1681c2691 100644 --- a/libs/IMUKit/source/IMUKit.cpp +++ b/libs/IMUKit/source/IMUKit.cpp @@ -83,6 +83,11 @@ auto IMUKit::getEulerAngles() const -> EulerAngles return _euler_angles; } +void IMUKit::onEulerAnglesReady(angles_ready_callback_t const &callback) +{ + _on_euler_angles_rdy_callback = callback; +} + void IMUKit::drdy_callback(const interface::LSM6DSOX::SensorData data) { // ? Note: For a detailed explanation on the code below, checkout @@ -113,4 +118,8 @@ void IMUKit::drdy_callback(const interface::LSM6DSOX::SensorData data) .roll = euler.angle.roll, .yaw = euler.angle.yaw, }; + + if (_on_euler_angles_rdy_callback) { + _on_euler_angles_rdy_callback(_euler_angles); + } }; diff --git a/libs/IMUKit/tests/IMUKit_test.cpp b/libs/IMUKit/tests/IMUKit_test.cpp index a5e399382f..242bc8a86c 100644 --- a/libs/IMUKit/tests/IMUKit_test.cpp +++ b/libs/IMUKit/tests/IMUKit_test.cpp @@ -66,6 +66,41 @@ TEST_F(IMUKitTest, setOrigin) TEST_F(IMUKitTest, onDataReady) { + testing::MockFunction mock_callback {}; + + imukit.onEulerAnglesReady(mock_callback.AsStdFunction()); + + const auto data_initial = interface::LSM6DSOX::SensorData { + .xl = {.x = 0.F, .y = 0.F, .z = 0.F}, .gy = {.x = 0.F, .y = 0.F, .z = 0.F } + }; + + EXPECT_CALL(mock_callback, Call); + + mock_lsm6dox.call_drdy_callback(data_initial); + + const auto angles_initial = imukit.getEulerAngles(); + + spy_kernel_addElapsedTimeToTickCount(80ms); + + const auto data_updated = interface::LSM6DSOX::SensorData { + .xl = {.x = 1.F, .y = 2.F, .z = 3.F}, .gy = {.x = 1.F, .y = 2.F, .z = 3.F } + }; + + EXPECT_CALL(mock_callback, Call); + + mock_lsm6dox.call_drdy_callback(data_updated); + + auto angles_updated = imukit.getEulerAngles(); + + EXPECT_NE(angles_initial.pitch, angles_updated.pitch); + EXPECT_NE(angles_initial.roll, angles_updated.roll); + EXPECT_NE(angles_initial.yaw, angles_updated.yaw); +} + +TEST_F(IMUKitTest, onDataReadyEmptyEulerAngleCallback) +{ + imukit.onEulerAnglesReady({}); + const auto data_initial = interface::LSM6DSOX::SensorData { .xl = {.x = 0.F, .y = 0.F, .z = 0.F}, .gy = {.x = 0.F, .y = 0.F, .z = 0.F } }; From f6c6d61dc2c169db034bac640d121363d8bb2d9b Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Mon, 6 Mar 2023 10:21:59 +0100 Subject: [PATCH 132/143] :sparkles: (RotationControl): Replace PID by ControlRotation --- libs/MotionKit/CMakeLists.txt | 4 +- libs/MotionKit/include/PID.hpp | 44 -------- libs/MotionKit/include/RotationControl.hpp | 57 ++++++++++ libs/MotionKit/source/PID.cpp | 42 ------- libs/MotionKit/source/RotationControl.cpp | 63 +++++++++++ libs/MotionKit/tests/PID_test.cpp | 87 --------------- libs/MotionKit/tests/RotationControl_test.cpp | 105 ++++++++++++++++++ 7 files changed, 227 insertions(+), 175 deletions(-) delete mode 100644 libs/MotionKit/include/PID.hpp create mode 100644 libs/MotionKit/include/RotationControl.hpp delete mode 100644 libs/MotionKit/source/PID.cpp create mode 100644 libs/MotionKit/source/RotationControl.cpp delete mode 100644 libs/MotionKit/tests/PID_test.cpp create mode 100644 libs/MotionKit/tests/RotationControl_test.cpp diff --git a/libs/MotionKit/CMakeLists.txt b/libs/MotionKit/CMakeLists.txt index 50a3eaad3c..3ced4c564f 100644 --- a/libs/MotionKit/CMakeLists.txt +++ b/libs/MotionKit/CMakeLists.txt @@ -12,7 +12,7 @@ target_include_directories(MotionKit target_sources(MotionKit PRIVATE source/MotionKit.cpp - source/PID.cpp + source/RotationControl.cpp ) target_link_libraries(MotionKit @@ -23,6 +23,6 @@ target_link_libraries(MotionKit if(${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests") leka_unit_tests_sources( tests/MotionKit_test.cpp - tests/PID_test.cpp + tests/RotationControl_test.cpp ) endif() diff --git a/libs/MotionKit/include/PID.hpp b/libs/MotionKit/include/PID.hpp deleted file mode 100644 index 3a6319223e..0000000000 --- a/libs/MotionKit/include/PID.hpp +++ /dev/null @@ -1,44 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "interface/drivers/Motor.h" - -namespace leka { - -class PID -{ - public: - PID() = default; - - auto processPID([[maybe_unused]] float pitch, [[maybe_unused]] float roll, float yaw) - -> std::tuple; - - private: - // ? Kp, Ki, Kd were found empirically by increasing Kp until the rotation angle exceeds the target angle - // ? Then increase Kd to fix this excess angle - // ? Repeat this protocol until there is no Kd high enough to compensate Kp - // ? Then take the last set of Kp, Kd value with no excess angle - // ? Finally choose a low Ki that smooth out the movement - struct Parameters { - static constexpr auto Kp = float {0.3F}; - static constexpr auto Ki = float {0.0001F}; - static constexpr auto Kd = float {0.4F}; - }; - const float kStaticBound = 5.F; - const float kDeltaT = 70.F; - const float kTargetAngle = 180.F; - - float _error_position_total = 0.F; - float _error_position_current = 0.F; - float _error_position_last = 0.F; - float _proportional = 0.F; - float _integral = 0.F; - float _derivative = 0.F; -}; - -} // namespace leka diff --git a/libs/MotionKit/include/RotationControl.hpp b/libs/MotionKit/include/RotationControl.hpp new file mode 100644 index 0000000000..c06b246cc9 --- /dev/null +++ b/libs/MotionKit/include/RotationControl.hpp @@ -0,0 +1,57 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "interface/libs/IMUKit.hpp" +namespace leka { + +class RotationControl +{ + public: + RotationControl() = default; + + void setTarget(EulerAngles starting_angles, float number_of_rotations); + auto processRotationAngle(EulerAngles current_angles) -> float; + + private: + void calculateTotalYawRotation(EulerAngles angle); + [[nodiscard]] auto mapSpeed(float speed) const -> float; + + // ? Kp, Ki, Kd were found empirically by increasing Kp until the rotation angle exceeds the target angle + // ? Then increase Kd to fix this excess angle + // ? Repeat this protocol until there is no Kd high enough to compensate Kp + // ? Then take the last set of Kp, Kd value with no excess angle + // ? Finally choose a low Ki that smooth out the movement + struct Parameters { + static constexpr auto Kp = float {0.3F}; + static constexpr auto Ki = float {0.0001F}; + static constexpr auto Kd = float {0.4F}; + }; + + static constexpr float kStaticBound = 5.F; + static constexpr float kDeltaT = 20.F; + + // ? When the motor is stopped, PWM values under kMinimalViableRobotPwm are too low to generate enough torque for + // ? the motor to start spinning ? At the same time, kMinimalViableRobotPwm needs to be the lowest possible to avoid + // ? overshooting when the target is reached + static constexpr float kMinimalViableRobotPwm = 0.15F; + static constexpr float kPwmMaxValue = 1.F; + static constexpr float kEpsilon = 0.005F; + + static constexpr float kInputSpeedLimit = 90 * (Parameters::Kp + Parameters::Kd) / kDeltaT; + + float _angle_rotation_target = 0.F; + float _angle_rotation_sum = 0.F; + + EulerAngles _euler_angles_previous {}; + + float _error_position_total = 0.F; + float _error_position_last = 0.F; + float _proportional = 0.F; + float _integral = 0.F; + float _derivative = 0.F; +}; + +} // namespace leka diff --git a/libs/MotionKit/source/PID.cpp b/libs/MotionKit/source/PID.cpp deleted file mode 100644 index 2317d2a10b..0000000000 --- a/libs/MotionKit/source/PID.cpp +++ /dev/null @@ -1,42 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "PID.hpp" -#include - -using namespace leka; - -auto PID::processPID([[maybe_unused]] float pitch, [[maybe_unused]] float roll, float yaw) - -> std::tuple -{ - auto direction = Rotation {}; - _error_position_current = kTargetAngle - yaw; - - if (std::abs(_error_position_current) < kStaticBound) { - _error_position_total += _error_position_current; - _error_position_total = std::min(_error_position_total, 50.F / Parameters::Ki); - } else { - _error_position_total = 0.F; - } - if (std::abs(_error_position_current) < kStaticBound) { - _derivative = 0.F; - } - - _proportional = _error_position_current * Parameters::Kp; - _integral = _error_position_total * Parameters::Ki; - _derivative = (_error_position_current - _error_position_last) * Parameters::Kd; - - _error_position_last = _error_position_current; - - auto speed = (_proportional + _integral + _derivative) / kDeltaT; - - if (speed < 0) { - speed = -speed; - direction = Rotation::counterClockwise; - } else { - direction = Rotation::clockwise; - } - - return {speed, direction}; -} diff --git a/libs/MotionKit/source/RotationControl.cpp b/libs/MotionKit/source/RotationControl.cpp new file mode 100644 index 0000000000..aa54808053 --- /dev/null +++ b/libs/MotionKit/source/RotationControl.cpp @@ -0,0 +1,63 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "RotationControl.hpp" +#include + +#include "MathUtils.h" + +using namespace leka; + +void RotationControl::setTarget(EulerAngles starting_angle, float number_of_rotations) +{ + _euler_angles_previous = starting_angle; + _angle_rotation_target = number_of_rotations * 360.F; + _angle_rotation_sum = 0; +} + +auto RotationControl::processRotationAngle(EulerAngles current_angles) -> float +{ + calculateTotalYawRotation(current_angles); + + auto error_position_current = _angle_rotation_target - _angle_rotation_sum; + + if (std::abs(error_position_current) < kStaticBound) { + _error_position_total += error_position_current; + _error_position_total = std::min(_error_position_total, 50.F / Parameters::Ki); + } else { + _error_position_total = 0.F; + } + if (std::abs(error_position_current) < kStaticBound) { + _derivative = 0.F; + } + + _proportional = error_position_current * Parameters::Kp; + _integral = _error_position_total * Parameters::Ki; + _derivative = (error_position_current - _error_position_last) * Parameters::Kd; + + _error_position_last = error_position_current; + + auto speed = (_proportional + _integral + _derivative) / kDeltaT; + + return mapSpeed(speed); +} + +void RotationControl::calculateTotalYawRotation(EulerAngles angle) +{ + if (auto abs_yaw_delta = std::abs(_euler_angles_previous.yaw - angle.yaw); abs_yaw_delta >= 300.F) { + _angle_rotation_sum += 360.F - abs_yaw_delta; + } else { + _angle_rotation_sum += abs_yaw_delta; + } + _euler_angles_previous = angle; +} + +auto RotationControl::mapSpeed(float speed) const -> float +{ + auto bounded_speed = utils::math::map(speed, 0.F, kInputSpeedLimit, kMinimalViableRobotPwm, kPwmMaxValue); + if (bounded_speed < kMinimalViableRobotPwm + kEpsilon) { + return 0.0; + } + return bounded_speed; +} diff --git a/libs/MotionKit/tests/PID_test.cpp b/libs/MotionKit/tests/PID_test.cpp deleted file mode 100644 index 69e33374fb..0000000000 --- a/libs/MotionKit/tests/PID_test.cpp +++ /dev/null @@ -1,87 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -#include "PID.hpp" - -#include "gtest/gtest.h" - -using namespace leka; - -class PIDTest : public ::testing::Test -{ - protected: - PIDTest() = default; - - // void SetUp() override { } - // void TearDown() override {} - - PID pid {}; - - float max_speed_value = 1.8F; //? ((360-180)*Kp + (360-180)*Kd)/KDeltaT -}; - -TEST_F(PIDTest, initialization) -{ - ASSERT_NE(&pid, nullptr); -} - -TEST_F(PIDTest, processPIDDefaultPosition) -{ - auto pitch = 0.F; - auto roll = 0.F; - auto yaw = 180.F; - - auto [speed, direction] = pid.processPID(pitch, roll, yaw); - - EXPECT_EQ(speed, 0.F); - EXPECT_EQ(direction, Rotation::clockwise); -} - -TEST_F(PIDTest, processPIDRolledOverAHalfRight) -{ - auto pitch = 0.F; - auto roll = 0.F; - auto yaw = 0.F; - - auto [speed, direction] = pid.processPID(pitch, roll, yaw); - - EXPECT_EQ(speed, max_speed_value); - EXPECT_EQ(direction, Rotation::clockwise); -} - -TEST_F(PIDTest, processPIDRolledOverAQuarterRight) -{ - auto pitch = 0.F; - auto roll = 0.F; - auto yaw = 90.F; - - auto [speed, direction] = pid.processPID(pitch, roll, yaw); - - EXPECT_EQ(speed, 0.9F); - EXPECT_EQ(direction, Rotation::clockwise); -} - -TEST_F(PIDTest, processPIDRolledOverAQuarterLeft) -{ - auto pitch = 0.F; - auto roll = 0.F; - auto yaw = 270.F; - - auto [speed, direction] = pid.processPID(pitch, roll, yaw); - - EXPECT_EQ(speed, 0.9F); - EXPECT_EQ(direction, Rotation::counterClockwise); -} - -TEST_F(PIDTest, processPIDRolledOverAHalfLeft) -{ - auto pitch = 0.F; - auto roll = 0.F; - auto yaw = 360.F; - - auto [speed, direction] = pid.processPID(pitch, roll, yaw); - - EXPECT_EQ(speed, max_speed_value); - EXPECT_EQ(direction, Rotation::counterClockwise); -} diff --git a/libs/MotionKit/tests/RotationControl_test.cpp b/libs/MotionKit/tests/RotationControl_test.cpp new file mode 100644 index 0000000000..fbd2139e01 --- /dev/null +++ b/libs/MotionKit/tests/RotationControl_test.cpp @@ -0,0 +1,105 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include "RotationControl.hpp" + +#include "LogKit.h" +#include "gtest/gtest.h" + +using namespace leka; + +class RotationControlTest : public ::testing::Test +{ + protected: + RotationControlTest() = default; + + // void SetUp() override {} + // void TearDown() override {} + + RotationControl rotation_control {}; + + EulerAngles angle {.yaw = 0.F}; + + const uint8_t kDegreeBoundNullSpeedArea = 3; + + float current_speed; + float previous_speed = 1000; +}; + +TEST_F(RotationControlTest, initialization) +{ + ASSERT_NE(&rotation_control, nullptr); +} + +TEST_F(RotationControlTest, processRotationAngleCounterClockwise) +{ + rotation_control.setTarget(angle, 1.0); + + for (auto i = 0; i < 180; i += 5) { + auto current_angle = EulerAngles {.yaw = static_cast(i)}; + + current_speed = rotation_control.processRotationAngle(current_angle); + + EXPECT_LE(current_speed, previous_speed); + + previous_speed = current_speed; + } + + for (auto i = -180; i < -kDegreeBoundNullSpeedArea; i += 5) { + auto current_angle = EulerAngles {.yaw = static_cast(i)}; + + current_speed = rotation_control.processRotationAngle(current_angle); + + EXPECT_LE(current_speed, previous_speed); + + previous_speed = current_speed; + } + + for (auto i = -kDegreeBoundNullSpeedArea; i < 0; i += 1) { + auto current_angle = EulerAngles {.yaw = static_cast(i)}; + + current_speed = rotation_control.processRotationAngle(current_angle); + + EXPECT_EQ(current_speed, 0.F); + } +} + +TEST_F(RotationControlTest, processRotationAngleClockwise) +{ + rotation_control.setTarget(angle, 1.0); + + for (auto i = 0.F; i > -180; i -= 5) { + auto current_angle = EulerAngles {.yaw = static_cast(i)}; + + current_speed = rotation_control.processRotationAngle(current_angle); + + EXPECT_LE(current_speed, previous_speed); + + log_debug("1."); + + previous_speed = current_speed; + } + + for (auto i = 180; i > kDegreeBoundNullSpeedArea; i -= 5) { + auto current_angle = EulerAngles {.yaw = static_cast(i)}; + + current_speed = rotation_control.processRotationAngle(current_angle); + + EXPECT_LE(current_speed, previous_speed); + + log_debug("2"); + + previous_speed = current_speed; + } + + for (auto i = kDegreeBoundNullSpeedArea; i > 0; i -= 1) { + auto current_angle = EulerAngles {.yaw = static_cast(i)}; + + current_speed = rotation_control.processRotationAngle(current_angle); + + log_debug("3."); + + EXPECT_EQ(current_speed, 0.F); + } +} From 89b4b5f988af64c2143fb0bdeabb95be01a27167 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Mon, 6 Mar 2023 10:50:44 +0100 Subject: [PATCH 133/143] :recycle: (MotionKit): Refactor with ControlRotation and IMUKit changes --- app/os/main.cpp | 5 +- libs/MotionKit/include/MotionKit.hpp | 45 ++---- libs/MotionKit/source/MotionKit.cpp | 101 ++++---------- libs/MotionKit/tests/MotionKit_test.cpp | 129 +++++++----------- libs/ReinforcerKit/source/ReinforcerKit.cpp | 4 +- .../tests/ReinforcerKit_test.cpp | 30 ++-- spikes/lk_command_kit/main.cpp | 4 +- spikes/lk_motion_kit/main.cpp | 27 ++-- spikes/lk_reinforcer/main.cpp | 4 +- 9 files changed, 116 insertions(+), 233 deletions(-) diff --git a/app/os/main.cpp b/app/os/main.cpp index aa65b7270b..4c64b55ce7 100644 --- a/app/os/main.cpp +++ b/app/os/main.cpp @@ -271,13 +271,11 @@ auto imukit = IMUKit {imu::lsm6dsox}; namespace motion::internal { - EventLoopKit event_loop {}; CoreTimeout timeout {}; } // namespace motion::internal -auto motionkit = MotionKit {motors::left::motor, motors::right::motor, imukit, motion::internal::event_loop, - motion::internal::timeout}; +auto motionkit = MotionKit {motors::left::motor, motors::right::motor, imukit, motion::internal::timeout}; auto behaviorkit = BehaviorKit {videokit, ledkit, motors::left::motor, motors::right::motor}; auto reinforcerkit = ReinforcerKit {videokit, ledkit, motionkit}; @@ -563,7 +561,6 @@ auto main() -> int imu::lsm6dsox.init(); imukit.init(); - motionkit.init(); robot::controller.initializeComponents(); robot::controller.registerOnUpdateLoadedCallback(firmware::setPendingUpdate); diff --git a/libs/MotionKit/include/MotionKit.hpp b/libs/MotionKit/include/MotionKit.hpp index ffb7fcc985..87a7a3aad3 100644 --- a/libs/MotionKit/include/MotionKit.hpp +++ b/libs/MotionKit/include/MotionKit.hpp @@ -5,65 +5,42 @@ #pragma once #include -#include -#include "IMUKit.hpp" -#include "PID.hpp" +#include "RotationControl.hpp" #include "interface/drivers/Timeout.h" +#include "interface/libs/IMUKit.hpp" namespace leka { class MotionKit { public: - MotionKit(interface::Motor &motor_left, interface::Motor &motor_right, IMUKit &imu_kit, - interface::EventLoop &event_loop, interface::Timeout &timeout) - : _motor_left(motor_left), - _motor_right(motor_right), - _imukit(imu_kit), - _event_loop(event_loop), - _timeout(timeout) + MotionKit(interface::Motor &motor_left, interface::Motor &motor_right, interface::IMUKit &imu_kit, + interface::Timeout &timeout) + : _motor_left(motor_left), _motor_right(motor_right), _imukit(imu_kit), _timeout(timeout) { } - void init(); - - void rotate(uint8_t number_of_rotations, Rotation direction, - const std::function &on_rotation_ended_callback = {}); - void startStabilisation(); + void startYawRotation(float degrees, Rotation direction, + const std::function &on_rotation_ended_callback = {}); void stop(); private: - void run(); + void processAngleForRotation(const EulerAngles &angles, Rotation direction); - [[nodiscard]] auto mapSpeed(float speed) const -> float; - void executeSpeed(float speed, Rotation direction); + void setMotorsSpeedAndDirection(float speed, Rotation direction); interface::Motor &_motor_left; interface::Motor &_motor_right; - IMUKit &_imukit; - interface::EventLoop &_event_loop; + interface::IMUKit &_imukit; interface::Timeout &_timeout; - PID _pid; - uint8_t _rotations_to_execute = 0; + RotationControl _rotation_control; std::function _on_rotation_ended_callback {}; bool _target_not_reached = false; - bool _stabilisation_requested = false; bool _rotate_x_turns_requested = false; - - const float kReferenceAngle = 180.F; - const float kPIDMaxValue = 1.8F; - - // ? When the motor is stopped, PWM values under kMinimalViableRobotPwm are too low to generate enough torque for - // ? the motor to start spinning ? At the same time, kMinimalViableRobotPwm needs to be the lowest possible to avoid - // ? overshooting when the target is reached - - const float kMinimalViableRobotPwm = 0.15F; - const float kPwmMaxValue = 1.F; - const float kEpsilon = 0.005F; }; } // namespace leka diff --git a/libs/MotionKit/source/MotionKit.cpp b/libs/MotionKit/source/MotionKit.cpp index a9d05384ac..9c105e3b02 100644 --- a/libs/MotionKit/source/MotionKit.cpp +++ b/libs/MotionKit/source/MotionKit.cpp @@ -4,127 +4,80 @@ #include "MotionKit.hpp" -#include "MathUtils.h" #include "ThisThread.h" using namespace leka; using namespace std::chrono_literals; -void MotionKit::init() -{ - _event_loop.registerCallback([this] { run(); }); -} - void MotionKit::stop() { _timeout.stop(); _motor_left.stop(); _motor_right.stop(); - _event_loop.stop(); - _stabilisation_requested = false; + _imukit.onEulerAnglesReady({}); + _target_not_reached = false; _rotate_x_turns_requested = false; } -void MotionKit::rotate(uint8_t number_of_rotations, Rotation direction, - const std::function &on_rotation_ended_callback) +void MotionKit::startYawRotation(float degrees, Rotation direction, + const std::function &on_rotation_ended_callback) { stop(); - _imukit.start(); - _imukit.setOrigin(); + auto starting_angle = _imukit.getEulerAngles(); + _rotation_control.setTarget(starting_angle, degrees); _target_not_reached = true; - _stabilisation_requested = false; _rotate_x_turns_requested = true; - _rotations_to_execute = number_of_rotations; - - _motor_left.spin(direction, kPwmMaxValue); - _motor_right.spin(direction, kPwmMaxValue); - auto on_timeout = [this] { stop(); }; _timeout.onTimeout(on_timeout); _timeout.start(10s); - _event_loop.start(); - - _on_rotation_ended_callback = on_rotation_ended_callback; -} - -void MotionKit::startStabilisation() -{ - stop(); + auto on_euler_angles_rdy_callback = [this, direction](const EulerAngles &euler_angles) { + processAngleForRotation(euler_angles, direction); + }; - _imukit.start(); - _imukit.setOrigin(); + _imukit.onEulerAnglesReady(on_euler_angles_rdy_callback); - _target_not_reached = false; - _stabilisation_requested = true; - _rotate_x_turns_requested = false; - - _event_loop.start(); + _on_rotation_ended_callback = on_rotation_ended_callback; } // LCOV_EXCL_START - Dynamic behavior, involving motors and time. -void MotionKit::run() +void MotionKit::processAngleForRotation(const EulerAngles &angles, Rotation direction) { - auto last_yaw = kReferenceAngle; - auto rotations_executed = 0; + auto must_stop = [&] { return !_rotate_x_turns_requested && !_target_not_reached; }; - auto must_rotate = [&] { return _rotate_x_turns_requested && rotations_executed != _rotations_to_execute; }; + if (must_stop()) { + stop(); - auto check_complete_rotations_executed = [&](auto current_yaw) { - if (std::abs(last_yaw - current_yaw) >= 300.F) { - ++rotations_executed; + if (_on_rotation_ended_callback) { + _on_rotation_ended_callback(); } - }; - - while (must_rotate()) { - auto [current_pitch, current_roll, current_yaw] = _imukit.getEulerAngles(); - check_complete_rotations_executed(current_yaw); - - rtos::ThisThread::sleep_for(70ms); - last_yaw = current_yaw; + return; } - _rotate_x_turns_requested = false; - _rotations_to_execute = 0; - - while (_stabilisation_requested || _target_not_reached) { - auto [pitch, roll, yaw] = _imukit.getEulerAngles(); - auto [speed, rotation] = _pid.processPID(pitch, roll, yaw); + if (_rotate_x_turns_requested && _target_not_reached) { + auto speed = _rotation_control.processRotationAngle(angles); - executeSpeed(speed, rotation); - - rtos::ThisThread::sleep_for(70ms); - } - - if (_on_rotation_ended_callback != nullptr) { - _on_rotation_ended_callback(); + setMotorsSpeedAndDirection(speed, direction); } - - _imukit.stop(); -} - -auto MotionKit::mapSpeed(float speed) const -> float -{ - return utils::math::map(speed, 0.F, kPIDMaxValue, kMinimalViableRobotPwm, kPwmMaxValue); } -void MotionKit::executeSpeed(float speed, Rotation direction) +void MotionKit::setMotorsSpeedAndDirection(float speed, Rotation direction) { - auto speed_bounded = mapSpeed(speed); - if (speed_bounded <= kMinimalViableRobotPwm + kEpsilon) { + if (speed == 0.F) { _motor_left.stop(); _motor_right.stop(); - _target_not_reached = false; + _target_not_reached = false; + _rotate_x_turns_requested = false; } else { - _motor_left.spin(direction, speed_bounded); - _motor_right.spin(direction, speed_bounded); + _motor_left.spin(direction, speed); + _motor_right.spin(direction, speed); _target_not_reached = true; } } diff --git a/libs/MotionKit/tests/MotionKit_test.cpp b/libs/MotionKit/tests/MotionKit_test.cpp index 878cb65299..94a0476f6d 100644 --- a/libs/MotionKit/tests/MotionKit_test.cpp +++ b/libs/MotionKit/tests/MotionKit_test.cpp @@ -7,13 +7,13 @@ #include "IMUKit.hpp" #include "gtest/gtest.h" #include "mocks/leka/CoreMotor.h" -#include "mocks/leka/LSM6DSOX.h" +#include "mocks/leka/IMUKit.hpp" #include "mocks/leka/Timeout.h" -#include "stubs/leka/EventLoopKit.h" using namespace leka; -using ::testing::MockFunction; +using ::testing::_; +using ::testing::AtLeast; // TODO(@leka/dev-embedded): temporary fix, changes are needed when updating fusion algorithm @@ -27,28 +27,19 @@ class MotionKitTest : public ::testing::Test protected: MotionKitTest() = default; - void SetUp() override - { - imukit.init(); - motion.init(); - } + // void SetUp() override {} // void TearDown() override {} - stub::EventLoopKit stub_event_loop_motion {}; - mock::CoreMotor mock_motor_left {}; mock::CoreMotor mock_motor_right {}; - mock::LSM6DSOX lsm6dsox {}; - mock::Timeout mock_timeout {}; - std::tuple accel_data = {0.F, 0.F, 0.F}; - std::tuple gyro_data = {0.F, 0.F, 0.F}; + const EulerAngles angles {0.F, 0.F, 0.F}; - IMUKit imukit {lsm6dsox}; + mock::IMUKit mock_imukit {}; - MotionKit motion {mock_motor_left, mock_motor_right, imukit, stub_event_loop_motion, mock_timeout}; + MotionKit motion {mock_motor_left, mock_motor_right, mock_imukit, mock_timeout}; }; TEST_F(MotionKitTest, initialization) @@ -56,91 +47,86 @@ TEST_F(MotionKitTest, initialization) ASSERT_NE(&motion, nullptr); } -TEST_F(MotionKitTest, registerMockCallbackAndRotate) +TEST_F(MotionKitTest, rotateClockwise) { - auto mock_function_motion = MockFunction {}; - auto loop_motion = [&] { mock_function_motion.Call(); }; - - EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); - EXPECT_CALL(mock_function_motion, Call()).Times(1); - EXPECT_CALL(mock_timeout, stop).Times(1); EXPECT_CALL(mock_motor_left, stop).Times(1); EXPECT_CALL(mock_motor_right, stop).Times(1); + EXPECT_CALL(mock_imukit, getEulerAngles).Times(1); + EXPECT_CALL(mock_timeout, onTimeout).Times(1); EXPECT_CALL(mock_timeout, start).Times(1); - EXPECT_CALL(mock_motor_left, spin(Rotation::clockwise, 1)).Times(1); - EXPECT_CALL(mock_motor_right, spin(Rotation::clockwise, 1)).Times(1); + EXPECT_CALL(mock_motor_left, spin(Rotation::clockwise, _)).Times(AtLeast(1)); + EXPECT_CALL(mock_motor_right, spin(Rotation::clockwise, _)).Times(AtLeast(1)); - stub_event_loop_motion.registerCallback(loop_motion); - motion.rotate(1, Rotation::clockwise); + motion.startYawRotation(1, Rotation::clockwise); + + mock_imukit.call_angles_ready_callback(angles); } -TEST_F(MotionKitTest, registerMockCallbackAndStartStabilisation) +TEST_F(MotionKitTest, rotateCounterClockwise) { - auto mock_function_motion = MockFunction {}; - auto loop_motion = [&] { mock_function_motion.Call(); }; - - EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); - EXPECT_CALL(mock_function_motion, Call()).Times(1); - EXPECT_CALL(mock_timeout, stop).Times(1); EXPECT_CALL(mock_motor_left, stop).Times(1); EXPECT_CALL(mock_motor_right, stop).Times(1); - stub_event_loop_motion.registerCallback(loop_motion); - motion.startStabilisation(); + EXPECT_CALL(mock_imukit, getEulerAngles).Times(1); + + EXPECT_CALL(mock_timeout, onTimeout).Times(1); + EXPECT_CALL(mock_timeout, start).Times(1); + + EXPECT_CALL(mock_motor_left, spin(Rotation::counterClockwise, _)).Times(AtLeast(1)); + EXPECT_CALL(mock_motor_right, spin(Rotation::counterClockwise, _)).Times(AtLeast(1)); + + motion.startYawRotation(1, Rotation::counterClockwise); + + mock_imukit.call_angles_ready_callback(angles); } TEST_F(MotionKitTest, rotateAndStop) { - auto mock_function_motion = MockFunction {}; - auto loop_motion = [&] { - mock_function_motion.Call(); - motion.stop(); - }; - - EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); - EXPECT_CALL(mock_function_motion, Call()).Times(1); - - EXPECT_CALL(mock_timeout, stop).Times(2); - EXPECT_CALL(mock_motor_left, stop).Times(2); - EXPECT_CALL(mock_motor_right, stop).Times(2); + EXPECT_CALL(mock_timeout, stop).Times(1); + EXPECT_CALL(mock_motor_left, stop).Times(1); + EXPECT_CALL(mock_motor_right, stop).Times(1); - EXPECT_CALL(mock_motor_left, spin).Times(1); - EXPECT_CALL(mock_motor_right, spin).Times(1); + EXPECT_CALL(mock_imukit, getEulerAngles).Times(1); EXPECT_CALL(mock_timeout, onTimeout).Times(1); EXPECT_CALL(mock_timeout, start).Times(1); - stub_event_loop_motion.registerCallback(loop_motion); - motion.rotate(1, Rotation::clockwise); + EXPECT_CALL(mock_motor_left, spin(Rotation::clockwise, _)).Times(AtLeast(1)); + EXPECT_CALL(mock_motor_right, spin(Rotation::clockwise, _)).Times(AtLeast(1)); + + motion.startYawRotation(1, Rotation::clockwise); + mock_imukit.call_angles_ready_callback(angles); + + EXPECT_CALL(mock_timeout, stop).Times(1); + EXPECT_CALL(mock_motor_left, stop).Times(1); + EXPECT_CALL(mock_motor_right, stop).Times(1); + + motion.stop(); } TEST_F(MotionKitTest, rotateAndTimeOutOver) { - auto mock_function_motion = MockFunction {}; - auto loop_motion = [&] { mock_function_motion.Call(); }; - interface::Timeout::callback_t on_timeout_callback = {}; - EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); - EXPECT_CALL(mock_function_motion, Call()).Times(1); - EXPECT_CALL(mock_timeout, stop).Times(1); EXPECT_CALL(mock_motor_left, stop).Times(1); EXPECT_CALL(mock_motor_right, stop).Times(1); - EXPECT_CALL(mock_motor_left, spin).Times(1); - EXPECT_CALL(mock_motor_right, spin).Times(1); + EXPECT_CALL(mock_imukit, getEulerAngles).Times(1); EXPECT_CALL(mock_timeout, onTimeout).WillOnce(GetCallback(&on_timeout_callback)); EXPECT_CALL(mock_timeout, start).Times(1); - stub_event_loop_motion.registerCallback(loop_motion); - motion.rotate(1, Rotation::clockwise); + EXPECT_CALL(mock_motor_left, spin(Rotation::clockwise, _)).Times(AtLeast(1)); + EXPECT_CALL(mock_motor_right, spin(Rotation::clockwise, _)).Times(AtLeast(1)); + + motion.startYawRotation(1, Rotation::clockwise); + mock_imukit.call_angles_ready_callback(angles); EXPECT_CALL(mock_timeout, stop).Times(1); EXPECT_CALL(mock_motor_left, stop).Times(1); @@ -148,22 +134,3 @@ TEST_F(MotionKitTest, rotateAndTimeOutOver) on_timeout_callback(); } - -TEST_F(MotionKitTest, startStabilisationAndStop) -{ - auto mock_function_motion = MockFunction {}; - auto loop_motion = [&] { - mock_function_motion.Call(); - motion.stop(); - }; - - EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); - EXPECT_CALL(mock_function_motion, Call()).Times(1); - - EXPECT_CALL(mock_timeout, stop).Times(2); - EXPECT_CALL(mock_motor_left, stop).Times(2); - EXPECT_CALL(mock_motor_right, stop).Times(2); - - stub_event_loop_motion.registerCallback(loop_motion); - motion.startStabilisation(); -} diff --git a/libs/ReinforcerKit/source/ReinforcerKit.cpp b/libs/ReinforcerKit/source/ReinforcerKit.cpp index 9d1c2bd93d..de5143a324 100644 --- a/libs/ReinforcerKit/source/ReinforcerKit.cpp +++ b/libs/ReinforcerKit/source/ReinforcerKit.cpp @@ -52,14 +52,14 @@ void ReinforcerKit::playBlinkGreen() { _videokit.playVideoOnce("/fs/home/vid/system/robot-system-reinforcer-happy-no_eyebrows.avi"); _ledkit.start(&led::animation::blink_green); - _motionkit.rotate(3, Rotation::clockwise, [this] { _ledkit.stop(); }); + _motionkit.startYawRotation(3, Rotation::clockwise, [this] { _ledkit.stop(); }); } void ReinforcerKit::playSpinBlink() { _videokit.playVideoOnce("/fs/home/vid/system/robot-system-reinforcer-happy-no_eyebrows.avi"); _ledkit.start(&led::animation::spin_blink); - _motionkit.rotate(3, Rotation::counterClockwise, [this] { _ledkit.stop(); }); + _motionkit.startYawRotation(3, Rotation::counterClockwise, [this] { _ledkit.stop(); }); } void ReinforcerKit::playFire() diff --git a/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp b/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp index 6559cba720..db34bcd9d3 100644 --- a/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp +++ b/libs/ReinforcerKit/tests/ReinforcerKit_test.cpp @@ -9,17 +9,15 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "mocks/leka/CoreMotor.h" -#include "mocks/leka/LSM6DSOX.h" +#include "mocks/leka/IMUKit.hpp" #include "mocks/leka/LedKit.h" #include "mocks/leka/Timeout.h" #include "mocks/leka/VideoKit.h" -#include "stubs/leka/EventLoopKit.h" using namespace leka; -using ::testing::AnyNumber; +using ::testing::AtLeast; using ::testing::AtMost; -using ::testing::Sequence; MATCHER_P(isSameAnimation, expected_animation, "") { @@ -39,30 +37,28 @@ class ReinforcerkitTest : public ::testing::Test mock::LedKit mock_ledkit; - stub::EventLoopKit stub_event_loop_motion {}; - mock::CoreMotor mock_motor_left {}; mock::CoreMotor mock_motor_right {}; - mock::LSM6DSOX lsm6dsox {}; - mock::Timeout mock_timeout {}; - IMUKit imukit {lsm6dsox}; + mock::IMUKit mock_imukit {}; - MotionKit motion {mock_motor_left, mock_motor_right, imukit, stub_event_loop_motion, mock_timeout}; + MotionKit motion {mock_motor_left, mock_motor_right, mock_imukit, mock_timeout}; ReinforcerKit reinforcerkit; + const EulerAngles angles {0.0F, 0.0F, 0.F}; + void expectedCallsMovingReinforcer(interface::LEDAnimation *animation) { EXPECT_CALL(mock_videokit, playVideoOnce); EXPECT_CALL(mock_motor_left, stop).Times(1); EXPECT_CALL(mock_motor_right, stop).Times(1); - EXPECT_CALL(mock_motor_left, spin).Times(1); - EXPECT_CALL(mock_motor_right, spin).Times(1); - EXPECT_CALL(lsm6dsox, setPowerMode(interface::LSM6DSOX::PowerMode::Normal)).Times(1); + EXPECT_CALL(mock_motor_left, spin).Times(AtLeast(1)); + EXPECT_CALL(mock_motor_right, spin).Times(AtLeast(1)); EXPECT_CALL(mock_timeout, stop).Times(AtMost(1)); + EXPECT_CALL(mock_imukit, getEulerAngles).Times(1); EXPECT_CALL(mock_timeout, onTimeout).Times(AtMost(1)); EXPECT_CALL(mock_timeout, start).Times(AtMost(1)); EXPECT_CALL(mock_ledkit, start(isSameAnimation(animation))); @@ -85,6 +81,8 @@ TEST_F(ReinforcerkitTest, playBlinkGreen) expectedCallsMovingReinforcer(&led::animation::blink_green); reinforcerkit.play(ReinforcerKit::Reinforcer::BlinkGreen); + + mock_imukit.call_angles_ready_callback(angles); } TEST_F(ReinforcerkitTest, playSpinBlink) @@ -92,6 +90,8 @@ TEST_F(ReinforcerkitTest, playSpinBlink) expectedCallsMovingReinforcer(&led::animation::spin_blink); reinforcerkit.play(ReinforcerKit::Reinforcer::SpinBlink); + + mock_imukit.call_angles_ready_callback(angles); } TEST_F(ReinforcerkitTest, playFire) @@ -125,6 +125,8 @@ TEST_F(ReinforcerkitTest, SetBlinkGreenAndPlayDefaultReinforcer) reinforcerkit.setDefaultReinforcer(ReinforcerKit::Reinforcer::BlinkGreen); reinforcerkit.playDefault(); + + mock_imukit.call_angles_ready_callback(angles); } TEST_F(ReinforcerkitTest, SetSpinBlinkAndPlayDefaultReinforcer) @@ -133,6 +135,8 @@ TEST_F(ReinforcerkitTest, SetSpinBlinkAndPlayDefaultReinforcer) reinforcerkit.setDefaultReinforcer(ReinforcerKit::Reinforcer::SpinBlink); reinforcerkit.playDefault(); + + mock_imukit.call_angles_ready_callback(angles); } TEST_F(ReinforcerkitTest, SetFireAndPlayDefaultReinforcer) diff --git a/spikes/lk_command_kit/main.cpp b/spikes/lk_command_kit/main.cpp index 248a1acc0b..0b0061d0bb 100644 --- a/spikes/lk_command_kit/main.cpp +++ b/spikes/lk_command_kit/main.cpp @@ -111,12 +111,11 @@ auto imukit = IMUKit {imu::lsm6dsox}; namespace motion::internal { -EventLoopKit event_loop {}; CoreTimeout timeout {}; } // namespace motion::internal -auto motionkit = MotionKit {motor::left, motor::right, imukit, motion::internal::event_loop, motion::internal::timeout}; +auto motionkit = MotionKit {motor::left, motor::right, imukit, motion::internal::timeout}; namespace display { @@ -254,7 +253,6 @@ auto main() -> int imu::lsm6dsox.init(); imukit.init(); - motionkit.init(); turnOff(); diff --git a/spikes/lk_motion_kit/main.cpp b/spikes/lk_motion_kit/main.cpp index f3d92971da..193c20b751 100644 --- a/spikes/lk_motion_kit/main.cpp +++ b/spikes/lk_motion_kit/main.cpp @@ -73,13 +73,11 @@ auto imukit = IMUKit {imu::lsm6dsox}; namespace motion::internal { - EventLoopKit event_loop {}; CoreTimeout timeout {}; } // namespace motion::internal -auto motionkit = MotionKit {motors::left::motor, motors::right::motor, imukit, motion::internal::event_loop, - motion::internal::timeout}; +auto motionkit = MotionKit {motors::left::motor, motors::right::motor, imukit, motion::internal::timeout}; namespace rfid { @@ -96,33 +94,25 @@ void onMagicCardAvailable(const MagicCard &card) { switch (card.getId()) { case (MagicCard::number_1.getId()): - motionkit.rotate(1, Rotation::counterClockwise, [] { log_debug("Callback end of rotation"); }); + motionkit.startYawRotation(1, Rotation::counterClockwise, [] { log_debug("Callback end of rotation"); }); break; case (MagicCard::number_2.getId()): - motionkit.rotate(2, Rotation::clockwise); + motionkit.startYawRotation(2, Rotation::clockwise); break; case (MagicCard::number_3.getId()): - motionkit.rotate(3, Rotation::counterClockwise); + motionkit.startYawRotation(3, Rotation::counterClockwise); break; case (MagicCard::number_4.getId()): - motionkit.rotate(4, Rotation::clockwise); + motionkit.startYawRotation(4, Rotation::clockwise); break; case (MagicCard::number_5.getId()): - motionkit.rotate(5, Rotation::counterClockwise); + motionkit.startYawRotation(5, Rotation::counterClockwise); break; case (MagicCard::number_6.getId()): - motionkit.rotate(6, Rotation::clockwise); + motionkit.startYawRotation(6, Rotation::clockwise); break; case (MagicCard::number_7.getId()): - motionkit.rotate(7, Rotation::counterClockwise); - break; - case (MagicCard::number_8.getId()): - motionkit.startStabilisation(); - rtos::ThisThread::sleep_for(10s); - motionkit.stop(); - break; - case (MagicCard::number_9.getId()): - motionkit.startStabilisation(); + motionkit.startYawRotation(7, Rotation::counterClockwise); break; case (MagicCard::number_10.getId()): motionkit.stop(); @@ -143,7 +133,6 @@ auto main() -> int imu::lsm6dsox.init(); imukit.init(); - motionkit.init(); rfidkit.init(); rfidkit.onTagActivated(onMagicCardAvailable); diff --git a/spikes/lk_reinforcer/main.cpp b/spikes/lk_reinforcer/main.cpp index c2a0262ff1..4e6c5947df 100644 --- a/spikes/lk_reinforcer/main.cpp +++ b/spikes/lk_reinforcer/main.cpp @@ -150,12 +150,11 @@ auto imukit = IMUKit {imu::lsm6dsox}; namespace motion::internal { - EventLoopKit event_loop {}; CoreTimeout timeout {}; } // namespace motion::internal -auto motionkit = MotionKit {motor::left, motor::right, imukit, motion::internal::event_loop, motion::internal::timeout}; +auto motionkit = MotionKit {motor::left, motor::right, imukit, motion::internal::timeout}; namespace display::internal { @@ -204,7 +203,6 @@ auto main() -> int videokit.initializeScreen(); imu::lsm6dsox.init(); imukit.init(); - motionkit.init(); rtos::ThisThread::sleep_for(3s); From eee68130f0b7052b4560c4335a8a747fddf92045 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Thu, 9 Mar 2023 10:38:59 +0100 Subject: [PATCH 134/143] :fire: (MotionKit): Remove useless boolean --- libs/MotionKit/include/MotionKit.hpp | 3 +-- libs/MotionKit/source/MotionKit.cpp | 23 +++++++---------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/libs/MotionKit/include/MotionKit.hpp b/libs/MotionKit/include/MotionKit.hpp index 87a7a3aad3..71c61ba32b 100644 --- a/libs/MotionKit/include/MotionKit.hpp +++ b/libs/MotionKit/include/MotionKit.hpp @@ -39,8 +39,7 @@ class MotionKit RotationControl _rotation_control; std::function _on_rotation_ended_callback {}; - bool _target_not_reached = false; - bool _rotate_x_turns_requested = false; + bool _target_not_reached = false; }; } // namespace leka diff --git a/libs/MotionKit/source/MotionKit.cpp b/libs/MotionKit/source/MotionKit.cpp index 9c105e3b02..50a032ece8 100644 --- a/libs/MotionKit/source/MotionKit.cpp +++ b/libs/MotionKit/source/MotionKit.cpp @@ -4,8 +4,6 @@ #include "MotionKit.hpp" -#include "ThisThread.h" - using namespace leka; using namespace std::chrono_literals; @@ -17,8 +15,7 @@ void MotionKit::stop() _imukit.onEulerAnglesReady({}); - _target_not_reached = false; - _rotate_x_turns_requested = false; + _target_not_reached = false; } void MotionKit::startYawRotation(float degrees, Rotation direction, @@ -29,8 +26,7 @@ void MotionKit::startYawRotation(float degrees, Rotation direction, auto starting_angle = _imukit.getEulerAngles(); _rotation_control.setTarget(starting_angle, degrees); - _target_not_reached = true; - _rotate_x_turns_requested = true; + _target_not_reached = true; auto on_timeout = [this] { stop(); }; @@ -49,9 +45,11 @@ void MotionKit::startYawRotation(float degrees, Rotation direction, // LCOV_EXCL_START - Dynamic behavior, involving motors and time. void MotionKit::processAngleForRotation(const EulerAngles &angles, Rotation direction) { - auto must_stop = [&] { return !_rotate_x_turns_requested && !_target_not_reached; }; + if (_target_not_reached) { + auto speed = _rotation_control.processRotationAngle(angles); - if (must_stop()) { + setMotorsSpeedAndDirection(speed, direction); + } else { stop(); if (_on_rotation_ended_callback) { @@ -60,12 +58,6 @@ void MotionKit::processAngleForRotation(const EulerAngles &angles, Rotation dire return; } - - if (_rotate_x_turns_requested && _target_not_reached) { - auto speed = _rotation_control.processRotationAngle(angles); - - setMotorsSpeedAndDirection(speed, direction); - } } void MotionKit::setMotorsSpeedAndDirection(float speed, Rotation direction) @@ -73,8 +65,7 @@ void MotionKit::setMotorsSpeedAndDirection(float speed, Rotation direction) if (speed == 0.F) { _motor_left.stop(); _motor_right.stop(); - _target_not_reached = false; - _rotate_x_turns_requested = false; + _target_not_reached = false; } else { _motor_left.spin(direction, speed); _motor_right.spin(direction, speed); From 4cf6ce9365ffb3f3e6c23698377e99a5d106a4e6 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Thu, 9 Mar 2023 10:42:05 +0100 Subject: [PATCH 135/143] :recycle: (MotionKit): Change all number_of_rotations in degrees --- libs/MotionKit/include/RotationControl.hpp | 2 +- libs/MotionKit/source/RotationControl.cpp | 4 ++-- libs/MotionKit/tests/MotionKit_test.cpp | 9 +++++---- libs/ReinforcerKit/include/ReinforcerKit.h | 1 + libs/ReinforcerKit/source/ReinforcerKit.cpp | 4 ++-- spikes/lk_motion_kit/main.cpp | 14 +++++++------- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/libs/MotionKit/include/RotationControl.hpp b/libs/MotionKit/include/RotationControl.hpp index c06b246cc9..f9c47749f4 100644 --- a/libs/MotionKit/include/RotationControl.hpp +++ b/libs/MotionKit/include/RotationControl.hpp @@ -12,7 +12,7 @@ class RotationControl public: RotationControl() = default; - void setTarget(EulerAngles starting_angles, float number_of_rotations); + void setTarget(EulerAngles starting_angles, float degrees); auto processRotationAngle(EulerAngles current_angles) -> float; private: diff --git a/libs/MotionKit/source/RotationControl.cpp b/libs/MotionKit/source/RotationControl.cpp index aa54808053..1081cad029 100644 --- a/libs/MotionKit/source/RotationControl.cpp +++ b/libs/MotionKit/source/RotationControl.cpp @@ -9,10 +9,10 @@ using namespace leka; -void RotationControl::setTarget(EulerAngles starting_angle, float number_of_rotations) +void RotationControl::setTarget(EulerAngles starting_angle, float degrees) { _euler_angles_previous = starting_angle; - _angle_rotation_target = number_of_rotations * 360.F; + _angle_rotation_target = degrees; _angle_rotation_sum = 0; } diff --git a/libs/MotionKit/tests/MotionKit_test.cpp b/libs/MotionKit/tests/MotionKit_test.cpp index 94a0476f6d..cc43c175e0 100644 --- a/libs/MotionKit/tests/MotionKit_test.cpp +++ b/libs/MotionKit/tests/MotionKit_test.cpp @@ -36,6 +36,7 @@ class MotionKitTest : public ::testing::Test mock::Timeout mock_timeout {}; const EulerAngles angles {0.F, 0.F, 0.F}; + const float kOneTurnDegrees = 360.0; mock::IMUKit mock_imukit {}; @@ -61,7 +62,7 @@ TEST_F(MotionKitTest, rotateClockwise) EXPECT_CALL(mock_motor_left, spin(Rotation::clockwise, _)).Times(AtLeast(1)); EXPECT_CALL(mock_motor_right, spin(Rotation::clockwise, _)).Times(AtLeast(1)); - motion.startYawRotation(1, Rotation::clockwise); + motion.startYawRotation(kOneTurnDegrees, Rotation::clockwise); mock_imukit.call_angles_ready_callback(angles); } @@ -80,7 +81,7 @@ TEST_F(MotionKitTest, rotateCounterClockwise) EXPECT_CALL(mock_motor_left, spin(Rotation::counterClockwise, _)).Times(AtLeast(1)); EXPECT_CALL(mock_motor_right, spin(Rotation::counterClockwise, _)).Times(AtLeast(1)); - motion.startYawRotation(1, Rotation::counterClockwise); + motion.startYawRotation(kOneTurnDegrees, Rotation::counterClockwise); mock_imukit.call_angles_ready_callback(angles); } @@ -99,7 +100,7 @@ TEST_F(MotionKitTest, rotateAndStop) EXPECT_CALL(mock_motor_left, spin(Rotation::clockwise, _)).Times(AtLeast(1)); EXPECT_CALL(mock_motor_right, spin(Rotation::clockwise, _)).Times(AtLeast(1)); - motion.startYawRotation(1, Rotation::clockwise); + motion.startYawRotation(kOneTurnDegrees, Rotation::clockwise); mock_imukit.call_angles_ready_callback(angles); EXPECT_CALL(mock_timeout, stop).Times(1); @@ -125,7 +126,7 @@ TEST_F(MotionKitTest, rotateAndTimeOutOver) EXPECT_CALL(mock_motor_left, spin(Rotation::clockwise, _)).Times(AtLeast(1)); EXPECT_CALL(mock_motor_right, spin(Rotation::clockwise, _)).Times(AtLeast(1)); - motion.startYawRotation(1, Rotation::clockwise); + motion.startYawRotation(kOneTurnDegrees, Rotation::clockwise); mock_imukit.call_angles_ready_callback(angles); EXPECT_CALL(mock_timeout, stop).Times(1); diff --git a/libs/ReinforcerKit/include/ReinforcerKit.h b/libs/ReinforcerKit/include/ReinforcerKit.h index 38e7ef8335..cb77530133 100644 --- a/libs/ReinforcerKit/include/ReinforcerKit.h +++ b/libs/ReinforcerKit/include/ReinforcerKit.h @@ -40,6 +40,7 @@ class ReinforcerKit interface::LedKit &_ledkit; MotionKit &_motionkit; Reinforcer _default_reinforcer = Reinforcer::Rainbow; + const float kThreeTurnDegrees = 1080.F; void playBlinkGreen(); void playSpinBlink(); diff --git a/libs/ReinforcerKit/source/ReinforcerKit.cpp b/libs/ReinforcerKit/source/ReinforcerKit.cpp index de5143a324..59dde69348 100644 --- a/libs/ReinforcerKit/source/ReinforcerKit.cpp +++ b/libs/ReinforcerKit/source/ReinforcerKit.cpp @@ -52,14 +52,14 @@ void ReinforcerKit::playBlinkGreen() { _videokit.playVideoOnce("/fs/home/vid/system/robot-system-reinforcer-happy-no_eyebrows.avi"); _ledkit.start(&led::animation::blink_green); - _motionkit.startYawRotation(3, Rotation::clockwise, [this] { _ledkit.stop(); }); + _motionkit.startYawRotation(kThreeTurnDegrees, Rotation::clockwise, [this] { _ledkit.stop(); }); } void ReinforcerKit::playSpinBlink() { _videokit.playVideoOnce("/fs/home/vid/system/robot-system-reinforcer-happy-no_eyebrows.avi"); _ledkit.start(&led::animation::spin_blink); - _motionkit.startYawRotation(3, Rotation::counterClockwise, [this] { _ledkit.stop(); }); + _motionkit.startYawRotation(kThreeTurnDegrees, Rotation::counterClockwise, [this] { _ledkit.stop(); }); } void ReinforcerKit::playFire() diff --git a/spikes/lk_motion_kit/main.cpp b/spikes/lk_motion_kit/main.cpp index 193c20b751..26ec4622ca 100644 --- a/spikes/lk_motion_kit/main.cpp +++ b/spikes/lk_motion_kit/main.cpp @@ -94,25 +94,25 @@ void onMagicCardAvailable(const MagicCard &card) { switch (card.getId()) { case (MagicCard::number_1.getId()): - motionkit.startYawRotation(1, Rotation::counterClockwise, [] { log_debug("Callback end of rotation"); }); + motionkit.startYawRotation(90, Rotation::counterClockwise, [] { log_debug("Callback end of rotation"); }); break; case (MagicCard::number_2.getId()): - motionkit.startYawRotation(2, Rotation::clockwise); + motionkit.startYawRotation(180, Rotation::clockwise); break; case (MagicCard::number_3.getId()): - motionkit.startYawRotation(3, Rotation::counterClockwise); + motionkit.startYawRotation(360, Rotation::counterClockwise); break; case (MagicCard::number_4.getId()): - motionkit.startYawRotation(4, Rotation::clockwise); + motionkit.startYawRotation(540, Rotation::clockwise); break; case (MagicCard::number_5.getId()): - motionkit.startYawRotation(5, Rotation::counterClockwise); + motionkit.startYawRotation(720, Rotation::counterClockwise); break; case (MagicCard::number_6.getId()): - motionkit.startYawRotation(6, Rotation::clockwise); + motionkit.startYawRotation(1080, Rotation::clockwise); break; case (MagicCard::number_7.getId()): - motionkit.startYawRotation(7, Rotation::counterClockwise); + motionkit.startYawRotation(1080, Rotation::counterClockwise); break; case (MagicCard::number_10.getId()): motionkit.stop(); From cea0b4cc72050e2d36e644938edf2de63d228aef Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 3 Mar 2023 13:16:02 +0100 Subject: [PATCH 136/143] :bug: (RC): Move onMagicCardAvailable at end of onTagActivated The RC-callback of onTagActivated might be called by _current_activity of ActivityKit. If it leads to call stopAutonomousActivityMode it will set the _current_activity to nullptr and not let RC-callback to finish resulting in HardFault. Co-Authored-By: Hugo Pezziardi <84374761+HPezz@users.noreply.github.com> --- libs/RobotKit/include/RobotController.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index f4f83b8783..9f343e9e1e 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -436,8 +436,12 @@ class RobotController : public interface::RobotController // Setup callbacks for monitoring _rfidkit.onTagActivated([this](const MagicCard &card) { - onMagicCardAvailable(card); + // ! IMPORTANT NOTE + // ! The order of the following functions MUST NOT + // ! be changed. It is a temporary fix for #1311 + // TODO(@leka/dev-embedded): remove when fixed _service_magic_card.setMagicCard(card); + onMagicCardAvailable(card); }); _battery_kit.onDataUpdated([this](uint8_t level) { From 491bbc9dbd3abe6202b788aad8fce0d3cf9cff2b Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Fri, 3 Mar 2023 16:51:59 +0100 Subject: [PATCH 137/143] :recycle: (ColorKit): conversion - add const to local variables --- libs/ColorKit/source/conversion.cpp | 43 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/libs/ColorKit/source/conversion.cpp b/libs/ColorKit/source/conversion.cpp index f8c950dc8e..57430b8a91 100644 --- a/libs/ColorKit/source/conversion.cpp +++ b/libs/ColorKit/source/conversion.cpp @@ -18,20 +18,23 @@ auto ColorKit::internal::clamp(const float &value) -> uint8_t if (value < 0.F) { return 0; } - return static_cast(std::round(value * 255.F)); + + const auto rounded = static_cast(std::round(value * 255.F)); + + return rounded; } auto ColorKit::internal::oklab2lrgb(const oklab_t &oklab) -> lrgb_t { - auto l_ = oklab.x + 0.3963377774F * oklab.y + 0.2158037573F * oklab.z; - auto m_ = oklab.x - 0.1055613458F * oklab.y - 0.0638541728F * oklab.z; - auto s_ = oklab.x - 0.0894841775F * oklab.y - 1.2914855480F * oklab.z; + const auto l_ = oklab.x + 0.3963377774F * oklab.y + 0.2158037573F * oklab.z; + const auto m_ = oklab.x - 0.1055613458F * oklab.y - 0.0638541728F * oklab.z; + const auto s_ = oklab.x - 0.0894841775F * oklab.y - 1.2914855480F * oklab.z; - auto l = l_ * l_ * l_; - auto m = m_ * m_ * m_; - auto s = s_ * s_ * s_; + const auto l = l_ * l_ * l_; + const auto m = m_ * m_ * m_; + const auto s = s_ * s_ * s_; - auto lrgb = lrgb_t { + const auto lrgb = lrgb_t { +4.0767416621F * l - 3.3077115913F * m + 0.2309699292F * s, -1.2684380046F * l + 2.6097574011F * m - 0.3413193965F * s, -0.0041960863F * l - 0.7034186147F * m + 1.7076147010F * s, @@ -42,15 +45,15 @@ auto ColorKit::internal::oklab2lrgb(const oklab_t &oklab) -> lrgb_t auto ColorKit::internal::lrgb2oklab(const lrgb_t &lrgb) -> oklab_t { - auto l = 0.4122214708F * lrgb.x + 0.5363325363F * lrgb.y + 0.0514459929F * lrgb.z; - auto m = 0.2119034982F * lrgb.x + 0.6806995451F * lrgb.y + 0.1073969566F * lrgb.z; - auto s = 0.0883024619F * lrgb.x + 0.2817188376F * lrgb.y + 0.6299787005F * lrgb.z; + const auto l = 0.4122214708F * lrgb.x + 0.5363325363F * lrgb.y + 0.0514459929F * lrgb.z; + const auto m = 0.2119034982F * lrgb.x + 0.6806995451F * lrgb.y + 0.1073969566F * lrgb.z; + const auto s = 0.0883024619F * lrgb.x + 0.2817188376F * lrgb.y + 0.6299787005F * lrgb.z; - auto l_ = std::cbrtf(l); - auto m_ = std::cbrtf(m); - auto s_ = std::cbrtf(s); + const auto l_ = std::cbrtf(l); + const auto m_ = std::cbrtf(m); + const auto s_ = std::cbrtf(s); - auto oklab = oklab_t { + const auto oklab = oklab_t { 0.2104542553F * l_ + 0.7936177850F * m_ - 0.0040720468F * s_, 1.9779984951F * l_ - 2.4285922050F * m_ + 0.4505937099F * s_, 0.0259040371F * l_ + 0.7827717662F * m_ - 0.8086757660F * s_, @@ -61,17 +64,17 @@ auto ColorKit::internal::lrgb2oklab(const lrgb_t &lrgb) -> oklab_t auto ColorKit::internal::rgb2oklab(const RGB &rgb) -> oklab_t { - auto lrgb = lrgb_t {static_cast(rgb.red) / 255.F, static_cast(rgb.green) / 255.F, - static_cast(rgb.blue) / 255.F}; - auto oklab = lrgb2oklab(lrgb); + const auto lrgb = lrgb_t {static_cast(rgb.red) / 255.F, static_cast(rgb.green) / 255.F, + static_cast(rgb.blue) / 255.F}; + const auto oklab = lrgb2oklab(lrgb); return oklab; } auto ColorKit::internal::oklab2rgb(const oklab_t &oklab) -> RGB { - auto lrgb = oklab2lrgb(oklab); - auto rgb = RGB {clamp(lrgb.r), clamp(lrgb.g), clamp(lrgb.b)}; + const auto lrgb = oklab2lrgb(oklab); + const auto rgb = RGB {clamp(lrgb.r), clamp(lrgb.g), clamp(lrgb.b)}; return rgb; } From db2f1c9522df42cfe3ec7e66822c014f7731bf84 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 3 Mar 2023 10:59:02 +0100 Subject: [PATCH 138/143] :sparkles: (spikes): Add ActivityKit spike --- spikes/CMakeLists.txt | 2 + spikes/lk_activity_kit/CMakeLists.txt | 37 ++++ spikes/lk_activity_kit/main.cpp | 297 ++++++++++++++++++++++++++ 3 files changed, 336 insertions(+) create mode 100644 spikes/lk_activity_kit/CMakeLists.txt create mode 100644 spikes/lk_activity_kit/main.cpp diff --git a/spikes/CMakeLists.txt b/spikes/CMakeLists.txt index 55e406009d..cc3bd94479 100644 --- a/spikes/CMakeLists.txt +++ b/spikes/CMakeLists.txt @@ -2,6 +2,7 @@ # Copyright 2020 APF France handicap # SPDX-License-Identifier: Apache-2.0 +add_subdirectory(${SPIKES_DIR}/lk_activity_kit) add_subdirectory(${SPIKES_DIR}/lk_audio) add_subdirectory(${SPIKES_DIR}/lk_behavior_kit) add_subdirectory(${SPIKES_DIR}/lk_ble) @@ -48,6 +49,7 @@ add_subdirectory(${SPIKES_DIR}/stl_cxxsupport) add_custom_target(spikes_leka) add_dependencies(spikes_leka + spike_lk_activity_kit spike_lk_ble spike_lk_bluetooth spike_lk_cg_animations diff --git a/spikes/lk_activity_kit/CMakeLists.txt b/spikes/lk_activity_kit/CMakeLists.txt new file mode 100644 index 0000000000..d551bd3a0f --- /dev/null +++ b/spikes/lk_activity_kit/CMakeLists.txt @@ -0,0 +1,37 @@ +# Leka - LekaOS +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +add_mbed_executable(spike_lk_activity_kit) + +target_include_directories(spike_lk_activity_kit + PRIVATE + . +) + +target_sources(spike_lk_activity_kit + PRIVATE + main.cpp +) + +target_link_libraries(spike_lk_activity_kit + CoreTimeout + FileManagerKit + BehaviorKit + CorePwm + CoreMotor + VideoKit + LedKit + CoreLED + CoreBufferedSerial + CoreRFIDReader + RFIDKit + ActivityKit + CoreI2C + CoreIMU + IMUKit + MotionKit + EventLoopKit +) + +target_link_custom_leka_targets(spike_lk_activity_kit) diff --git a/spikes/lk_activity_kit/main.cpp b/spikes/lk_activity_kit/main.cpp new file mode 100644 index 0000000000..3b651ba635 --- /dev/null +++ b/spikes/lk_activity_kit/main.cpp @@ -0,0 +1,297 @@ +// Leka - LekaOS +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "rtos/Kernel.h" +#include "rtos/ThisThread.h" +#include "rtos/Thread.h" + +#include "ActivityKit.h" +#include "BehaviorKit.h" +#include "ChooseReinforcer.h" +#include "CoreBufferedSerial.h" +#include "CoreDMA2D.hpp" +#include "CoreDSI.hpp" +#include "CoreFont.hpp" +#include "CoreGraphics.hpp" +#include "CoreI2C.h" +#include "CoreInterruptIn.h" +#include "CoreJPEG.hpp" +#include "CoreJPEGModeDMA.hpp" +#include "CoreJPEGModePolling.hpp" +#include "CoreLCD.hpp" +#include "CoreLCDDriverOTM8009A.hpp" +#include "CoreLL.h" +#include "CoreLSM6DSOX.hpp" +#include "CoreLTDC.hpp" +#include "CoreMotor.h" +#include "CorePwm.h" +#include "CoreRFIDReaderCR95HF.h" +#include "CoreSDRAM.hpp" +#include "CoreSPI.h" +#include "CoreSTM32Hal.h" +#include "CoreTimeout.h" +#include "CoreVideo.hpp" +#include "DisplayTags.h" +#include "EmotionRecognition.h" +#include "EventLoopKit.h" +#include "FATFileSystem.h" +#include "FlashNumberCounting.h" +#include "FoodRecognition.h" +#include "HelloWorld.h" +#include "IMUKit.hpp" +#include "LedColorRecognition.h" +#include "LedKit.h" +#include "LedNumberCounting.h" +#include "LogKit.h" +#include "NumberRecognition.h" +#include "PictoColorRecognition.h" +#include "RFIDKit.h" +#include "ReinforcerKit.h" +#include "SDBlockDevice.h" +#include "ShapeRecognition.h" +#include "SuperSimon.h" +#include "VideoKit.h" + +using namespace leka; +using namespace std::chrono; + +namespace { + +namespace sd { + + namespace internal { + + auto bd = SDBlockDevice {SD_SPI_MOSI, SD_SPI_MISO, SD_SPI_SCK}; + auto fs = FATFileSystem {"fs"}; + + constexpr auto default_frequency = uint64_t {25'000'000}; + + } // namespace internal + + void init() + { + internal::bd.init(); + internal::bd.frequency(internal::default_frequency); + internal::fs.mount(&internal::bd); + } + +} // namespace sd + +namespace leds { + + namespace internal { + + namespace ears { + + auto spi = CoreSPI {LED_EARS_SPI_MOSI, NC, LED_EARS_SPI_SCK}; + constexpr auto size = 2; + + } // namespace ears + + namespace belt { + + auto spi = CoreSPI {LED_BELT_SPI_MOSI, NC, LED_BELT_SPI_SCK}; + constexpr auto size = 20; + + } // namespace belt + + namespace animations { + + auto event_loop = EventLoopKit {}; + + } // namespace animations + + } // namespace internal + + auto ears = CoreLED {internal::ears::spi}; + auto belt = CoreLED {internal::belt::spi}; + + void turnOff() + { + ears.setColor(RGB::black); + belt.setColor(RGB::black); + ears.show(); + belt.show(); + } + +} // namespace leds + +auto ledkit = LedKit {leds::internal::animations::event_loop, leds::ears, leds::belt}; + +namespace motors { + + namespace left { + + namespace internal { + + auto dir_1 = mbed::DigitalOut {MOTOR_LEFT_DIRECTION_1}; + auto dir_2 = mbed::DigitalOut {MOTOR_LEFT_DIRECTION_2}; + auto speed = CorePwm {MOTOR_LEFT_PWM}; + + } // namespace internal + + auto motor = CoreMotor {internal::dir_1, internal::dir_2, internal::speed}; + + } // namespace left + + namespace right { + + namespace internal { + + auto dir_1 = mbed::DigitalOut {MOTOR_RIGHT_DIRECTION_1}; + auto dir_2 = mbed::DigitalOut {MOTOR_RIGHT_DIRECTION_2}; + auto speed = CorePwm {MOTOR_RIGHT_PWM}; + + } // namespace internal + + auto motor = CoreMotor {internal::dir_1, internal::dir_2, internal::speed}; + + } // namespace right + + void turnOff() + { + left::motor.stop(); + right::motor.stop(); + } + +} // namespace motors + +namespace display::internal { + + auto event_loop = EventLoopKit {}; + + auto corell = CoreLL {}; + auto pixel = CGPixel {corell}; + auto hal = CoreSTM32Hal {}; + auto coresdram = CoreSDRAM {hal}; + auto coredma2d = CoreDMA2D {hal}; + auto coredsi = CoreDSI {hal}; + auto coreltdc = CoreLTDC {hal}; + auto coregraphics = CoreGraphics {coredma2d}; + auto corefont = CoreFont {pixel}; + auto coreotm = CoreLCDDriverOTM8009A {coredsi, PinName::SCREEN_BACKLIGHT_PWM}; + auto corelcd = CoreLCD {coreotm}; + auto _corejpegmode = CoreJPEGModeDMA {hal}; + auto corejpeg = CoreJPEG {hal, _corejpegmode}; + + extern "C" auto corevideo = + CoreVideo {hal, coresdram, coredma2d, coredsi, coreltdc, corelcd, coregraphics, corefont, corejpeg}; + +} // namespace display::internal + +auto videokit = VideoKit {display::internal::event_loop, display::internal::corevideo}; + +namespace imu { + + namespace internal { + + auto drdy_irq = CoreInterruptIn {PinName::SENSOR_IMU_IRQ}; + auto i2c = CoreI2C(PinName::SENSOR_IMU_TH_I2C_SDA, PinName::SENSOR_IMU_TH_I2C_SCL); + + } // namespace internal + + auto lsm6dsox = CoreLSM6DSOX(internal::i2c, internal::drdy_irq); + +} // namespace imu + +auto imukit = IMUKit {imu::lsm6dsox}; + +namespace motion::internal { + + CoreTimeout timeout {}; + +} // namespace motion::internal + +auto motionkit = MotionKit {motors::left::motor, motors::right::motor, imukit, motion::internal::timeout}; + +auto behaviorkit = BehaviorKit {videokit, ledkit, motors::left::motor, motors::right::motor}; +auto reinforcerkit = ReinforcerKit {videokit, ledkit, motionkit}; + +namespace rfid { + + auto serial = CoreBufferedSerial(RFID_UART_TX, RFID_UART_RX, 57600); + auto reader = CoreRFIDReaderCR95HF(serial); + +} // namespace rfid + +auto rfidkit = RFIDKit(rfid::reader); + +namespace activities { + + namespace internal { + + auto display_tag = leka::activity::DisplayTags(rfidkit, videokit); + auto choose_reinforcer = leka::activity::ChooseReinforcer(rfidkit, videokit, reinforcerkit); + auto number_recognition = leka::activity::NumberRecognition(rfidkit, videokit, reinforcerkit); + auto picto_color_recognition = leka::activity::PictoColorRecognition(rfidkit, videokit, reinforcerkit); + auto led_color_recognition = leka::activity::LedColorRecognition(rfidkit, videokit, reinforcerkit, leds::belt); + auto emotion_recognition = leka::activity::EmotionRecognition(rfidkit, videokit, reinforcerkit); + auto food_recognition = leka::activity::FoodRecognition(rfidkit, videokit, reinforcerkit); + auto led_number_counting = leka::activity::LedNumberCounting(rfidkit, videokit, reinforcerkit, leds::belt); + auto flash_number_counting = leka::activity::FlashNumberCounting(rfidkit, videokit, reinforcerkit, leds::belt); + auto super_simon = leka::activity::SuperSimon(rfidkit, videokit, reinforcerkit, leds::belt); + auto shape_recognition = leka::activity::ShapeRecognition(rfidkit, videokit, reinforcerkit); + + } // namespace internal + + inline const std::unordered_map activities { + {MagicCard::number_10, &internal::display_tag}, + {MagicCard::number_0, &internal::choose_reinforcer}, + {MagicCard::number_1, &internal::number_recognition}, + {MagicCard::number_2, &internal::picto_color_recognition}, + {MagicCard::number_3, &internal::led_color_recognition}, + {MagicCard::number_4, &internal::emotion_recognition}, + {MagicCard::number_5, &internal::food_recognition}, + {MagicCard::number_6, &internal::led_number_counting}, + {MagicCard::number_7, &internal::flash_number_counting}, + {MagicCard::number_8, &internal::super_simon}, + {MagicCard::number_9, &internal::shape_recognition}, + }; + +} // namespace activities + +auto activitykit = ActivityKit {videokit}; + +} // namespace + +auto main() -> int +{ + logger::init(); + + log_info("Hello, World!\n\n"); + + rtos::ThisThread::sleep_for(1s); + + HelloWorld hello; + hello.start(); + + activitykit.registerActivities(activities::activities); + + rfidkit.init(); + + sd::init(); + + imu::lsm6dsox.init(); + imukit.init(); + + ledkit.init(); + + videokit.initializeScreen(); + videokit.displayImage("/fs/home/img/system/robot-misc-splash_screen-large-400.jpg"); + + rtos::ThisThread::sleep_for(1s); + + rfidkit.onTagActivated([](const MagicCard &card) { + log_info("card: %ld", card.getId()); + activitykit.start(card); + }); + + while (true) { + log_info("Still alive"); + rtos::ThisThread::sleep_for(10s); + } +} From 38a4d3652184a5073f4ee73fdd024b501b947e7f Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 10 Mar 2023 18:18:28 +0100 Subject: [PATCH 139/143] :white_check_mark: (MotionKit): Remove LogKit from RotationControl ut's --- libs/MotionKit/tests/RotationControl_test.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libs/MotionKit/tests/RotationControl_test.cpp b/libs/MotionKit/tests/RotationControl_test.cpp index fbd2139e01..8229cf00ef 100644 --- a/libs/MotionKit/tests/RotationControl_test.cpp +++ b/libs/MotionKit/tests/RotationControl_test.cpp @@ -4,7 +4,6 @@ #include "RotationControl.hpp" -#include "LogKit.h" #include "gtest/gtest.h" using namespace leka; @@ -76,8 +75,6 @@ TEST_F(RotationControlTest, processRotationAngleClockwise) EXPECT_LE(current_speed, previous_speed); - log_debug("1."); - previous_speed = current_speed; } @@ -88,8 +85,6 @@ TEST_F(RotationControlTest, processRotationAngleClockwise) EXPECT_LE(current_speed, previous_speed); - log_debug("2"); - previous_speed = current_speed; } @@ -98,8 +93,6 @@ TEST_F(RotationControlTest, processRotationAngleClockwise) current_speed = rotation_control.processRotationAngle(current_angle); - log_debug("3."); - EXPECT_EQ(current_speed, 0.F); } } From 48dd46678a4c7e0b0262088235d4d3f6ef14ae60 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 3 Mar 2023 10:59:02 +0100 Subject: [PATCH 140/143] :children_crossing: (ActivityKit): Add callback to run before process Update ActivityKit spike --- libs/ActivityKit/include/ActivityKit.h | 3 +++ .../include/activities/ChooseReinforcer.h | 2 +- libs/ActivityKit/include/activities/DisplayTags.h | 2 +- .../include/activities/EmotionRecognition.h | 2 +- .../include/activities/FlashNumberCounting.h | 2 +- .../include/activities/FoodRecognition.h | 2 +- .../include/activities/LedColorRecognition.h | 2 +- .../include/activities/LedNumberCounting.h | 2 +- .../include/activities/NumberRecognition.h | 2 +- .../include/activities/PictoColorRecognition.h | 2 +- .../include/activities/ShapeRecognition.h | 2 +- libs/ActivityKit/include/activities/SuperSimon.h | 2 +- libs/ActivityKit/include/interface/Activity.h | 6 ++++-- libs/ActivityKit/source/ActivityKit.cpp | 7 ++++++- .../source/activities/ChooseReinforcer.cpp | 9 +++++++-- .../ActivityKit/source/activities/DisplayTags.cpp | 9 +++++++-- .../source/activities/EmotionRecognition.cpp | 9 +++++++-- .../source/activities/FlashNumberCounting.cpp | 9 +++++++-- .../source/activities/FoodRecognition.cpp | 9 +++++++-- .../source/activities/LedColorRecognition.cpp | 9 +++++++-- .../source/activities/LedNumberCounting.cpp | 9 +++++++-- .../source/activities/NumberRecognition.cpp | 9 +++++++-- .../source/activities/PictoColorRecognition.cpp | 9 +++++++-- .../source/activities/ShapeRecognition.cpp | 9 +++++++-- libs/ActivityKit/source/activities/SuperSimon.cpp | 9 +++++++-- libs/ActivityKit/tests/ActivityKit_test.cpp | 15 +++++++++++++++ libs/ActivityKit/tests/mocks/Activity.h | 2 +- spikes/lk_activity_kit/main.cpp | 2 ++ 28 files changed, 119 insertions(+), 37 deletions(-) diff --git a/libs/ActivityKit/include/ActivityKit.h b/libs/ActivityKit/include/ActivityKit.h index 2a66f6cd62..d1765fe477 100644 --- a/libs/ActivityKit/include/ActivityKit.h +++ b/libs/ActivityKit/include/ActivityKit.h @@ -17,6 +17,7 @@ class ActivityKit explicit ActivityKit(interface::VideoKit &videokit) : _videokit(videokit) {}; void registerActivities(std::unordered_map const &activities); + void registerBeforeProcessCallback(const std::function &callback); void start(const MagicCard &card); void stop(); @@ -26,6 +27,8 @@ class ActivityKit [[nodiscard]] auto isPlaying() const -> bool; private: + std::function _before_process_callback {}; + interface::VideoKit &_videokit; interface::Activity *_current_activity = nullptr; std::unordered_map _activities {}; diff --git a/libs/ActivityKit/include/activities/ChooseReinforcer.h b/libs/ActivityKit/include/activities/ChooseReinforcer.h index 585dbea22d..fb7bbd5582 100644 --- a/libs/ActivityKit/include/activities/ChooseReinforcer.h +++ b/libs/ActivityKit/include/activities/ChooseReinforcer.h @@ -19,7 +19,7 @@ class ChooseReinforcer : public interface::Activity explicit ChooseReinforcer(RFIDKit &rfidkit, interface::VideoKit &videokit, ReinforcerKit &reinforcerkit) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/DisplayTags.h b/libs/ActivityKit/include/activities/DisplayTags.h index 2461478476..dcc8e8be8b 100644 --- a/libs/ActivityKit/include/activities/DisplayTags.h +++ b/libs/ActivityKit/include/activities/DisplayTags.h @@ -17,7 +17,7 @@ class DisplayTags : public interface::Activity public: explicit DisplayTags(RFIDKit &rfidkit, interface::VideoKit &videokit) : _rfidkit(rfidkit), _videokit(videokit) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/EmotionRecognition.h b/libs/ActivityKit/include/activities/EmotionRecognition.h index 2c4d32319c..39eb4409a1 100644 --- a/libs/ActivityKit/include/activities/EmotionRecognition.h +++ b/libs/ActivityKit/include/activities/EmotionRecognition.h @@ -22,7 +22,7 @@ class EmotionRecognition : public interface::Activity explicit EmotionRecognition(RFIDKit &rfidkit, interface::VideoKit &videokit, ReinforcerKit &reinforcerkit) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/FlashNumberCounting.h b/libs/ActivityKit/include/activities/FlashNumberCounting.h index 01c4de129f..f825a6e5d3 100644 --- a/libs/ActivityKit/include/activities/FlashNumberCounting.h +++ b/libs/ActivityKit/include/activities/FlashNumberCounting.h @@ -20,7 +20,7 @@ class FlashNumberCounting : public interface::Activity interface::LED &led) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit), _led(led) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/FoodRecognition.h b/libs/ActivityKit/include/activities/FoodRecognition.h index 42c87f117e..d9034456e0 100644 --- a/libs/ActivityKit/include/activities/FoodRecognition.h +++ b/libs/ActivityKit/include/activities/FoodRecognition.h @@ -22,7 +22,7 @@ class FoodRecognition : public interface::Activity explicit FoodRecognition(RFIDKit &rfidkit, interface::VideoKit &videokit, ReinforcerKit &reinforcerkit) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/LedColorRecognition.h b/libs/ActivityKit/include/activities/LedColorRecognition.h index 354255c09c..d2591c0469 100644 --- a/libs/ActivityKit/include/activities/LedColorRecognition.h +++ b/libs/ActivityKit/include/activities/LedColorRecognition.h @@ -24,7 +24,7 @@ class LedColorRecognition : public interface::Activity interface::LED &led) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit), _led(led) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/LedNumberCounting.h b/libs/ActivityKit/include/activities/LedNumberCounting.h index 8078e54dbb..ad4f76316d 100644 --- a/libs/ActivityKit/include/activities/LedNumberCounting.h +++ b/libs/ActivityKit/include/activities/LedNumberCounting.h @@ -21,7 +21,7 @@ class LedNumberCounting : public interface::Activity interface::LED &led) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit), _led(led) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/NumberRecognition.h b/libs/ActivityKit/include/activities/NumberRecognition.h index ac40f57128..c893e8f397 100644 --- a/libs/ActivityKit/include/activities/NumberRecognition.h +++ b/libs/ActivityKit/include/activities/NumberRecognition.h @@ -22,7 +22,7 @@ class NumberRecognition : public interface::Activity explicit NumberRecognition(RFIDKit &rfidkit, interface::VideoKit &videokit, ReinforcerKit &reinforcerkit) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/PictoColorRecognition.h b/libs/ActivityKit/include/activities/PictoColorRecognition.h index 57ee42e382..363ccd5f64 100644 --- a/libs/ActivityKit/include/activities/PictoColorRecognition.h +++ b/libs/ActivityKit/include/activities/PictoColorRecognition.h @@ -22,7 +22,7 @@ class PictoColorRecognition : public interface::Activity explicit PictoColorRecognition(RFIDKit &rfidkit, interface::VideoKit &videokit, ReinforcerKit &reinforcerkit) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/ShapeRecognition.h b/libs/ActivityKit/include/activities/ShapeRecognition.h index 15362036be..0c9cd44661 100644 --- a/libs/ActivityKit/include/activities/ShapeRecognition.h +++ b/libs/ActivityKit/include/activities/ShapeRecognition.h @@ -22,7 +22,7 @@ class ShapeRecognition : public interface::Activity explicit ShapeRecognition(RFIDKit &rfidkit, interface::VideoKit &videokit, ReinforcerKit &reinforcerkit) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/activities/SuperSimon.h b/libs/ActivityKit/include/activities/SuperSimon.h index ce4a3ff117..2e1e949488 100644 --- a/libs/ActivityKit/include/activities/SuperSimon.h +++ b/libs/ActivityKit/include/activities/SuperSimon.h @@ -23,7 +23,7 @@ class SuperSimon : public interface::Activity interface::LED &led) : _rfidkit(rfidkit), _videokit(videokit), _reinforcerkit(reinforcerkit), _led(led) {}; - void start() final; + void start(const std::function &before_process_callback) final; void stop() final; private: diff --git a/libs/ActivityKit/include/interface/Activity.h b/libs/ActivityKit/include/interface/Activity.h index 1b4e42810d..6c6d2ac17e 100644 --- a/libs/ActivityKit/include/interface/Activity.h +++ b/libs/ActivityKit/include/interface/Activity.h @@ -4,6 +4,8 @@ #pragma once +#include + namespace leka::interface { class Activity @@ -11,8 +13,8 @@ class Activity public: virtual ~Activity() = default; - virtual void start() = 0; - virtual void stop() = 0; + virtual void start(const std::function &before_process_callback) = 0; + virtual void stop() = 0; }; } // namespace leka::interface diff --git a/libs/ActivityKit/source/ActivityKit.cpp b/libs/ActivityKit/source/ActivityKit.cpp index 66cb6b6d12..173c91f661 100644 --- a/libs/ActivityKit/source/ActivityKit.cpp +++ b/libs/ActivityKit/source/ActivityKit.cpp @@ -11,6 +11,11 @@ void ActivityKit::registerActivities(std::unordered_map &callback) +{ + _before_process_callback = callback; +} + void ActivityKit::start(const MagicCard &card) { stop(); @@ -21,7 +26,7 @@ void ActivityKit::start(const MagicCard &card) } _current_activity = _activities.at(card); - _current_activity->start(); + _current_activity->start(_before_process_callback); } void ActivityKit::stop() diff --git a/libs/ActivityKit/source/activities/ChooseReinforcer.cpp b/libs/ActivityKit/source/activities/ChooseReinforcer.cpp index f1fe30ceaf..cb65a40527 100644 --- a/libs/ActivityKit/source/activities/ChooseReinforcer.cpp +++ b/libs/ActivityKit/source/activities/ChooseReinforcer.cpp @@ -9,13 +9,18 @@ #include "rtos/ThisThread.h" namespace leka::activity { -void ChooseReinforcer::start() +void ChooseReinforcer::start(const std::function &before_process_callback) { _videokit.displayImage("fs/home/img/system/robot-face-smiling-slightly.jpg"); _backup_callback = _rfidkit.getCallback(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void ChooseReinforcer::processCard(const MagicCard &card) diff --git a/libs/ActivityKit/source/activities/DisplayTags.cpp b/libs/ActivityKit/source/activities/DisplayTags.cpp index 15e45fe13e..b7e97914db 100644 --- a/libs/ActivityKit/source/activities/DisplayTags.cpp +++ b/libs/ActivityKit/source/activities/DisplayTags.cpp @@ -8,13 +8,18 @@ namespace leka::activity { -void DisplayTags::start() +void DisplayTags::start(const std::function &before_process_callback) { _videokit.displayImage("fs/home/img/system/robot-misc-robot-misc-screen_empty_white.jpg"); _backup_callback = _rfidkit.getCallback(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void DisplayTags::stop() diff --git a/libs/ActivityKit/source/activities/EmotionRecognition.cpp b/libs/ActivityKit/source/activities/EmotionRecognition.cpp index bcf9587456..b34bb31846 100644 --- a/libs/ActivityKit/source/activities/EmotionRecognition.cpp +++ b/libs/ActivityKit/source/activities/EmotionRecognition.cpp @@ -10,7 +10,7 @@ #include "rtos/ThisThread.h" namespace leka::activity { -void EmotionRecognition::start() +void EmotionRecognition::start(const std::function &before_process_callback) { _current_round = 0; _current_emotion = {}; @@ -19,7 +19,12 @@ void EmotionRecognition::start() std::shuffle(_emotions.begin(), _emotions.end(), std::mt19937(static_cast(time(nullptr)))); launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void EmotionRecognition::stop() diff --git a/libs/ActivityKit/source/activities/FlashNumberCounting.cpp b/libs/ActivityKit/source/activities/FlashNumberCounting.cpp index a535c99114..686a8e8a77 100644 --- a/libs/ActivityKit/source/activities/FlashNumberCounting.cpp +++ b/libs/ActivityKit/source/activities/FlashNumberCounting.cpp @@ -11,7 +11,7 @@ namespace leka::activity { -void FlashNumberCounting::start() +void FlashNumberCounting::start(const std::function &before_process_callback) { using namespace std::chrono; @@ -27,7 +27,12 @@ void FlashNumberCounting::start() launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void FlashNumberCounting::stop() diff --git a/libs/ActivityKit/source/activities/FoodRecognition.cpp b/libs/ActivityKit/source/activities/FoodRecognition.cpp index 6ca1ee6522..1f93180236 100644 --- a/libs/ActivityKit/source/activities/FoodRecognition.cpp +++ b/libs/ActivityKit/source/activities/FoodRecognition.cpp @@ -11,7 +11,7 @@ namespace leka::activity { -void FoodRecognition::start() +void FoodRecognition::start(const std::function &before_process_callback) { _current_round = 0; _current_food = {}; @@ -20,7 +20,12 @@ void FoodRecognition::start() std::shuffle(_foods.begin(), _foods.end(), std::mt19937(static_cast(time(nullptr)))); launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void FoodRecognition::stop() diff --git a/libs/ActivityKit/source/activities/LedColorRecognition.cpp b/libs/ActivityKit/source/activities/LedColorRecognition.cpp index 01e1319a9e..dd12bd1d81 100644 --- a/libs/ActivityKit/source/activities/LedColorRecognition.cpp +++ b/libs/ActivityKit/source/activities/LedColorRecognition.cpp @@ -10,7 +10,7 @@ namespace leka::activity { -void LedColorRecognition::start() +void LedColorRecognition::start(const std::function &before_process_callback) { _current_round = 0; _current_color = {}; @@ -21,7 +21,12 @@ void LedColorRecognition::start() std::shuffle(_colors.begin(), _colors.end(), std::mt19937(static_cast(time(nullptr)))); launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void LedColorRecognition::stop() diff --git a/libs/ActivityKit/source/activities/LedNumberCounting.cpp b/libs/ActivityKit/source/activities/LedNumberCounting.cpp index e45f262a05..0b77b689b7 100644 --- a/libs/ActivityKit/source/activities/LedNumberCounting.cpp +++ b/libs/ActivityKit/source/activities/LedNumberCounting.cpp @@ -11,7 +11,7 @@ namespace leka::activity { -void LedNumberCounting::start() +void LedNumberCounting::start(const std::function &before_process_callback) { _current_round = 0; _current_leds_number = 0; @@ -24,7 +24,12 @@ void LedNumberCounting::start() std::shuffle(_led_numbers.begin(), _led_numbers.end(), std::mt19937(static_cast(time(nullptr)))); launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void LedNumberCounting::stop() diff --git a/libs/ActivityKit/source/activities/NumberRecognition.cpp b/libs/ActivityKit/source/activities/NumberRecognition.cpp index f999cf17c1..d898089262 100644 --- a/libs/ActivityKit/source/activities/NumberRecognition.cpp +++ b/libs/ActivityKit/source/activities/NumberRecognition.cpp @@ -11,7 +11,7 @@ namespace leka::activity { -void NumberRecognition::start() +void NumberRecognition::start(const std::function &before_process_callback) { _current_round = 0; _current_number = {}; @@ -20,7 +20,12 @@ void NumberRecognition::start() std::shuffle(_numbers.begin(), _numbers.end(), std::mt19937(static_cast(time(nullptr)))); launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void NumberRecognition::stop() diff --git a/libs/ActivityKit/source/activities/PictoColorRecognition.cpp b/libs/ActivityKit/source/activities/PictoColorRecognition.cpp index ccbc353862..69f97bfa36 100644 --- a/libs/ActivityKit/source/activities/PictoColorRecognition.cpp +++ b/libs/ActivityKit/source/activities/PictoColorRecognition.cpp @@ -11,7 +11,7 @@ namespace leka::activity { -void PictoColorRecognition::start() +void PictoColorRecognition::start(const std::function &before_process_callback) { _current_round = 0; _current_color = {}; @@ -20,7 +20,12 @@ void PictoColorRecognition::start() std::shuffle(_colors.begin(), _colors.end(), std::mt19937(static_cast(time(nullptr)))); launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void PictoColorRecognition::stop() diff --git a/libs/ActivityKit/source/activities/ShapeRecognition.cpp b/libs/ActivityKit/source/activities/ShapeRecognition.cpp index 62e8607770..a5ce9556e6 100644 --- a/libs/ActivityKit/source/activities/ShapeRecognition.cpp +++ b/libs/ActivityKit/source/activities/ShapeRecognition.cpp @@ -11,7 +11,7 @@ namespace leka::activity { -void ShapeRecognition::start() +void ShapeRecognition::start(const std::function &before_process_callback) { _score = 0; _current_shape = {}; @@ -20,7 +20,12 @@ void ShapeRecognition::start() std::shuffle(_shapes.begin(), _shapes.end(), std::mt19937(static_cast(time(nullptr)))); launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void ShapeRecognition::stop() diff --git a/libs/ActivityKit/source/activities/SuperSimon.cpp b/libs/ActivityKit/source/activities/SuperSimon.cpp index e65400923c..8803eb4cb8 100644 --- a/libs/ActivityKit/source/activities/SuperSimon.cpp +++ b/libs/ActivityKit/source/activities/SuperSimon.cpp @@ -12,7 +12,7 @@ namespace leka::activity { -void SuperSimon::start() +void SuperSimon::start(const std::function &before_process_callback) { using namespace std::chrono; @@ -27,7 +27,12 @@ void SuperSimon::start() launchNextRound(); - _rfidkit.onTagActivated([this](const MagicCard &card) { processCard(card); }); + _rfidkit.onTagActivated([this, &before_process_callback](const MagicCard &card) { + if (before_process_callback != nullptr) { + before_process_callback(); + } + processCard(card); + }); } void SuperSimon::stop() diff --git a/libs/ActivityKit/tests/ActivityKit_test.cpp b/libs/ActivityKit/tests/ActivityKit_test.cpp index 14abc9905e..9209d1b03f 100644 --- a/libs/ActivityKit/tests/ActivityKit_test.cpp +++ b/libs/ActivityKit/tests/ActivityKit_test.cpp @@ -12,6 +12,8 @@ using namespace leka; using ::testing::AnyNumber; using ::testing::InSequence; +using ::testing::MockFunction; +using ::testing::SaveArg; class ActivityKitTest : public ::testing::Test { @@ -134,3 +136,16 @@ TEST_F(ActivityKitTest, displayFRMainMenu) activitykit.displayMainMenu(dice_roll_FR); } + +TEST_F(ActivityKitTest, registerBeforeProcessCallback) +{ + MockFunction callback; + activitykit.registerBeforeProcessCallback(callback.AsStdFunction()); + + std::function before_process_callback_registered {}; + EXPECT_CALL(mock_activity_0, start).WillOnce(SaveArg<0>(&before_process_callback_registered)); + activitykit.start(MagicCard::number_0); + + EXPECT_CALL(callback, Call); + before_process_callback_registered(); +} diff --git a/libs/ActivityKit/tests/mocks/Activity.h b/libs/ActivityKit/tests/mocks/Activity.h index 0b32c2e29e..7c131e63c1 100644 --- a/libs/ActivityKit/tests/mocks/Activity.h +++ b/libs/ActivityKit/tests/mocks/Activity.h @@ -12,7 +12,7 @@ namespace leka::mock { class Activity : public interface::Activity { public: - MOCK_METHOD(void, start, (), ()); + MOCK_METHOD(void, start, (const std::function &), ()); MOCK_METHOD(void, stop, (), ()); }; diff --git a/spikes/lk_activity_kit/main.cpp b/spikes/lk_activity_kit/main.cpp index 3b651ba635..a8742d2cdf 100644 --- a/spikes/lk_activity_kit/main.cpp +++ b/spikes/lk_activity_kit/main.cpp @@ -285,6 +285,8 @@ auto main() -> int rtos::ThisThread::sleep_for(1s); + activitykit.registerBeforeProcessCallback([] { log_info("Callback call"); }); + rfidkit.onTagActivated([](const MagicCard &card) { log_info("card: %ld", card.getId()); activitykit.start(card); From e409b791ea1cedef10657d4df77b195def9591f6 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Fri, 10 Mar 2023 19:17:10 +0100 Subject: [PATCH 141/143] :children_crossing: (rc): Add autonomous activities timeout Timeout reset on any magic card --- app/os/main.cpp | 6 ++- libs/RobotKit/include/RobotController.h | 31 ++++++++++++--- libs/RobotKit/tests/RobotController_test.h | 26 ++++++++++--- .../RobotController_test_registerEvents.cpp | 2 + ...troller_test_stateAutonomousActivities.cpp | 39 +++++++++++++++++++ .../RobotController_test_stateCharging.cpp | 8 ++++ ...tController_test_stateEmergencyStopped.cpp | 4 ++ ...RobotController_test_stateFileExchange.cpp | 4 ++ .../tests/RobotController_test_stateIdle.cpp | 7 ++++ .../RobotController_test_stateSleeping.cpp | 7 ++++ .../RobotController_test_stateWorking.cpp | 11 ++++++ 11 files changed, 133 insertions(+), 12 deletions(-) diff --git a/app/os/main.cpp b/app/os/main.cpp index 4c64b55ce7..6cf597ac1b 100644 --- a/app/os/main.cpp +++ b/app/os/main.cpp @@ -403,8 +403,9 @@ namespace robot { namespace internal { - auto timeout_state_internal = CoreTimeout {}; - auto timeout_state_transition = CoreTimeout {}; + auto timeout_state_internal = CoreTimeout {}; + auto timeout_state_transition = CoreTimeout {}; + auto timeout_autonomous_activities = CoreTimeout {}; auto mcu = CoreMCU {}; auto serialnumberkit = SerialNumberKit {mcu, SerialNumberKit::DEFAULT_CONFIG}; @@ -414,6 +415,7 @@ namespace robot { auto controller = RobotController { internal::timeout_state_internal, internal::timeout_state_transition, + internal::timeout_autonomous_activities, battery::cells, internal::serialnumberkit, firmware::kit, diff --git a/libs/RobotKit/include/RobotController.h b/libs/RobotKit/include/RobotController.h index 9f343e9e1e..d6e4a98e0b 100644 --- a/libs/RobotKit/include/RobotController.h +++ b/libs/RobotKit/include/RobotController.h @@ -48,13 +48,15 @@ class RobotController : public interface::RobotController sm_t state_machine {static_cast(*this), logger}; explicit RobotController(interface::Timeout &timeout_state_internal, interface::Timeout &timeout_state_transition, - interface::Battery &battery, SerialNumberKit &serialnumberkit, - interface::FirmwareUpdate &firmware_update, interface::Motor &motor_left, - interface::Motor &motor_right, interface::LED &ears, interface::LED &belt, - interface::LedKit &ledkit, interface::LCD &lcd, interface::VideoKit &videokit, - BehaviorKit &behaviorkit, CommandKit &cmdkit, RFIDKit &rfidkit, ActivityKit &activitykit) + interface::Timeout &timeout_autonomous_activities, interface::Battery &battery, + SerialNumberKit &serialnumberkit, interface::FirmwareUpdate &firmware_update, + interface::Motor &motor_left, interface::Motor &motor_right, interface::LED &ears, + interface::LED &belt, interface::LedKit &ledkit, interface::LCD &lcd, + interface::VideoKit &videokit, BehaviorKit &behaviorkit, CommandKit &cmdkit, + RFIDKit &rfidkit, ActivityKit &activitykit) : _timeout_state_internal(timeout_state_internal), _timeout_state_transition(timeout_state_transition), + _timeout_autonomous_activities(timeout_autonomous_activities), _battery(battery), _serialnumberkit(serialnumberkit), _firmware_update(firmware_update), @@ -230,6 +232,7 @@ class RobotController : public interface::RobotController void stopAutonomousActivityMode() final { + _timeout_autonomous_activities.stop(); _behaviorkit.stop(); _activitykit.stop(); } @@ -385,11 +388,24 @@ class RobotController : public interface::RobotController void raiseAutonomousActivityModeExited() { raise(system::robot::sm::event::autonomous_activities_mode_exited {}); } + void resetAutonomousActivitiesTimeout() + { + _timeout_autonomous_activities.stop(); + + auto on_autonomous_activities_timeout = [this] { + raise(system::robot::sm::event::autonomous_activities_mode_exited {}); + }; + _timeout_autonomous_activities.onTimeout(on_autonomous_activities_timeout); + + _timeout_autonomous_activities.start(_timeout_autonomous_activities_duration); + } + void onMagicCardAvailable(const MagicCard &card) { using namespace std::chrono; // ! TODO: Refactor with composite SM & CoreTimer instead of start/stop + resetAutonomousActivitiesTimeout(); auto is_playing = _activitykit.isPlaying(); auto NOT_is_playing = !is_playing; @@ -444,6 +460,8 @@ class RobotController : public interface::RobotController onMagicCardAvailable(card); }); + _activitykit.registerBeforeProcessCallback([this] { resetAutonomousActivitiesTimeout(); }); + _battery_kit.onDataUpdated([this](uint8_t level) { auto is_charging = _battery.isCharging(); @@ -542,6 +560,9 @@ class RobotController : public interface::RobotController std::chrono::seconds _deep_sleep_timeout_duration {600}; interface::Timeout &_timeout_state_transition; + interface::Timeout &_timeout_autonomous_activities; + std::chrono::seconds _timeout_autonomous_activities_duration {600}; + const rtos::Kernel::Clock::time_point kSystemStartupTimestamp = rtos::Kernel::Clock::now(); rtos::Kernel::Clock::time_point start = rtos::Kernel::Clock::now(); diff --git a/libs/RobotKit/tests/RobotController_test.h b/libs/RobotKit/tests/RobotController_test.h index 0b87015de0..f57b95a72b 100644 --- a/libs/RobotKit/tests/RobotController_test.h +++ b/libs/RobotKit/tests/RobotController_test.h @@ -78,6 +78,7 @@ class RobotControllerTest : public testing::Test mock::Timeout timeout_state_internal {}; mock::Timeout timeout_state_transition {}; + mock::Timeout timeout_autonomous_activities {}; mock::Battery battery {}; mock::MCU mock_mcu {}; @@ -112,6 +113,7 @@ class RobotControllerTest : public testing::Test RobotController> rc {timeout_state_internal, timeout_state_transition, + timeout_autonomous_activities, battery, serialnumberkit, firmware_update, @@ -130,11 +132,12 @@ class RobotControllerTest : public testing::Test ble::GapMock &mbed_mock_gap = ble::gap_mock(); ble::GattServerMock &mbed_mock_gatt = ble::gatt_server_mock(); - interface::Timeout::callback_t on_sleep_timeout = {}; - interface::Timeout::callback_t on_deep_sleep_timeout = {}; - interface::Timeout::callback_t on_idle_timeout = {}; - interface::Timeout::callback_t on_sleeping_start_timeout = {}; - interface::Timeout::callback_t on_charging_start_timeout = {}; + interface::Timeout::callback_t on_sleep_timeout = {}; + interface::Timeout::callback_t on_deep_sleep_timeout = {}; + interface::Timeout::callback_t on_idle_timeout = {}; + interface::Timeout::callback_t on_sleeping_start_timeout = {}; + interface::Timeout::callback_t on_charging_start_timeout = {}; + interface::Timeout::callback_t on_autonomous_activities_timeout = {}; std::function on_charge_did_start {}; std::function on_charge_did_stop {}; @@ -289,4 +292,17 @@ class RobotControllerTest : public testing::Test .Times(1); EXPECT_CALL(mock_lcd, turnOn); } + + void expectedCallsResetAutonomousActivitiesTimeout() + { + auto expected_duration = std::chrono::seconds {600}; + { + InSequence seq; + + EXPECT_CALL(timeout_autonomous_activities, stop); + EXPECT_CALL(timeout_autonomous_activities, onTimeout) + .WillOnce(GetCallback(&on_autonomous_activities_timeout)); + EXPECT_CALL(timeout_autonomous_activities, start(std::chrono::microseconds {expected_duration})); + } + } }; diff --git a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp index e6dd286b78..ecf7320b8c 100644 --- a/libs/RobotKit/tests/RobotController_test_registerEvents.cpp +++ b/libs/RobotKit/tests/RobotController_test_registerEvents.cpp @@ -189,5 +189,7 @@ TEST_F(RobotControllerTest, onTagActivated) // TODO: Specify which BLE service and what is expected if necessary EXPECT_CALL(mbed_mock_gatt, write(_, _, _, _)).Times(2); + expectedCallsResetAutonomousActivitiesTimeout(); + onTagActivated(MagicCard::none); } diff --git a/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp b/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp index 0cabf84c82..8911b0a5b3 100644 --- a/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateAutonomousActivities.cpp @@ -9,6 +9,7 @@ TEST_F(RobotControllerTest, stateAutonomousActivityConnectedEventCommandReceived rc.state_machine.set_current_states(lksm::state::autonomous_activities, lksm::state::connected); Sequence on_autonomous_activity_exit_sequence; + EXPECT_CALL(timeout_autonomous_activities, stop).InSequence(on_autonomous_activity_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_autonomous_activity_exit_sequence); EXPECT_CALL(mock_motor_left, stop).InSequence(on_autonomous_activity_exit_sequence); EXPECT_CALL(mock_motor_right, stop).InSequence(on_autonomous_activity_exit_sequence); @@ -39,6 +40,7 @@ TEST_F(RobotControllerTest, stateAutonomousActivityEventBleConnection) EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(false)); + EXPECT_CALL(timeout_autonomous_activities, stop); expectedCallsStopActuators(); Sequence on_ble_connection_sequence; EXPECT_CALL(mock_ledkit, start(isSameAnimation(&led::animation::ble_connection))) @@ -64,6 +66,7 @@ TEST_F(RobotControllerTest, stateAutonomousActivityEventChargeDidStartGuardIsCha EXPECT_CALL(battery, isCharging).WillRepeatedly(Return(true)); Sequence on_autonomous_activity_exit_sequence; + EXPECT_CALL(timeout_autonomous_activities, stop).InSequence(on_autonomous_activity_exit_sequence); EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_autonomous_activity_exit_sequence); EXPECT_CALL(mock_motor_left, stop).InSequence(on_autonomous_activity_exit_sequence); EXPECT_CALL(mock_motor_right, stop).InSequence(on_autonomous_activity_exit_sequence); @@ -129,6 +132,7 @@ TEST_F(RobotControllerTest, stateAutonomousActivityStartActivityActivityAlreadyS }; set_activitykit_is_playing(); + expectedCallsResetAutonomousActivitiesTimeout(); EXPECT_CALL(mock_videokit, displayImage).Times(0); rc.onMagicCardAvailable(MagicCard::number_10); @@ -150,6 +154,7 @@ TEST_F(RobotControllerTest, stateAutonomousActivityActivityStartedBackToMenuDela }; set_activitykit_is_playing(); + expectedCallsResetAutonomousActivitiesTimeout(); EXPECT_CALL(mock_videokit, displayImage).Times(0); spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); @@ -173,9 +178,11 @@ TEST_F(RobotControllerTest, stateAutonomousActivityActivityStartedBackToMenu) }; set_activitykit_is_playing(); + EXPECT_CALL(timeout_autonomous_activities, stop); EXPECT_CALL(mock_videokit, stopVideo); expectedCallsStopMotors(); + expectedCallsResetAutonomousActivitiesTimeout(); EXPECT_CALL(mock_videokit, displayImage).Times(1); spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); @@ -196,6 +203,8 @@ TEST_F(RobotControllerTest, stateAutonomousActivityDiceRollDetectedDelayNotOver) EXPECT_CALL(mock_motor_right, stop).Times(0); EXPECT_CALL(mock_videokit, displayImage).Times(0); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::dice_roll); @@ -209,6 +218,7 @@ TEST_F(RobotControllerTest, stateAutonomousActivityDiceRollDetectedDelayOverEven auto minimal_delay_over = 2001ms; + EXPECT_CALL(timeout_autonomous_activities, stop).Times(AtLeast(1)); EXPECT_CALL(mock_videokit, stopVideo).Times(AtLeast(1)); EXPECT_CALL(mock_motor_left, stop).Times(AtLeast(1)); EXPECT_CALL(mock_motor_right, stop).Times(AtLeast(1)); @@ -219,6 +229,8 @@ TEST_F(RobotControllerTest, stateAutonomousActivityDiceRollDetectedDelayOverEven EXPECT_CALL(mock_videokit, playVideoOnRepeat).InSequence(on_idle_entry_sequence); EXPECT_CALL(mock_lcd, turnOn).InSequence(on_idle_entry_sequence); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); rc.onMagicCardAvailable(MagicCard::dice_roll); @@ -232,6 +244,7 @@ TEST_F(RobotControllerTest, stateAutonomousActivityDiceRollDetectedDelayOverEven auto minimal_delay_over = 2001ms; + EXPECT_CALL(timeout_autonomous_activities, stop).Times(AtLeast(1)); EXPECT_CALL(mock_videokit, stopVideo).Times(AtLeast(1)); EXPECT_CALL(mock_motor_left, stop).Times(AtLeast(1)); EXPECT_CALL(mock_motor_right, stop).Times(AtLeast(1)); @@ -241,8 +254,34 @@ TEST_F(RobotControllerTest, stateAutonomousActivityDiceRollDetectedDelayOverEven EXPECT_CALL(timeout_state_transition, start).InSequence(on_working_entry_sequence); EXPECT_CALL(mock_videokit, displayImage).InSequence(on_working_entry_sequence); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); rc.onMagicCardAvailable(MagicCard::dice_roll); EXPECT_TRUE(rc.state_machine.is(lksm::state::working)); } + +// ! TODO: Refactor with composite SM & CoreTimer mock +TEST_F(RobotControllerTest, stateAutonomousActivityDisconnectedEventAutonomousActivityExitedFromTimeout) +{ + expectedCallsResetAutonomousActivitiesTimeout(); + rc.resetAutonomousActivitiesTimeout(); // To register callback in on_autonomous_activities_timeout + + rc.state_machine.set_current_states(lksm::state::autonomous_activities, lksm::state::disconnected); + + EXPECT_CALL(timeout_autonomous_activities, stop).Times(AtLeast(1)); + EXPECT_CALL(mock_videokit, stopVideo).Times(AtLeast(1)); + EXPECT_CALL(mock_motor_left, stop).Times(AtLeast(1)); + EXPECT_CALL(mock_motor_right, stop).Times(AtLeast(1)); + + Sequence on_idle_entry_sequence; + EXPECT_CALL(timeout_state_transition, onTimeout).InSequence(on_idle_entry_sequence); + EXPECT_CALL(timeout_state_transition, start).InSequence(on_idle_entry_sequence); + EXPECT_CALL(mock_videokit, playVideoOnRepeat).InSequence(on_idle_entry_sequence); + EXPECT_CALL(mock_lcd, turnOn).InSequence(on_idle_entry_sequence); + + on_autonomous_activities_timeout(); + + EXPECT_TRUE(rc.state_machine.is(lksm::state::idle)); +} diff --git a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp index 9e88f511e4..08b71a56b0 100644 --- a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp @@ -372,6 +372,8 @@ TEST_F(RobotControllerTest, stateChargingEventEmergencyStopDelayNotOver) auto maximal_delay_before_over = 9s; + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -393,6 +395,8 @@ TEST_F(RobotControllerTest, stateChargingEventEmergencyStopDelayOver) expectedCallsStopActuators(); EXPECT_CALL(mock_lcd, turnOff); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(delay_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -412,6 +416,8 @@ TEST_F(RobotControllerTest, stateChargingDiceRollDetectedDelayNotOver) EXPECT_CALL(mock_lcd, turnOn).Times(0); EXPECT_CALL(timeout_state_internal, start).Times(0); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::dice_roll); @@ -444,6 +450,8 @@ TEST_F(RobotControllerTest, stateChargingDiceRollDetectedDelayOverEventAutonomou .WillOnce(GetCallback(&on_charging_start_timeout)); EXPECT_CALL(timeout_state_internal, start).Times(AnyNumber()); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); rc.onMagicCardAvailable(MagicCard::dice_roll); diff --git a/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp b/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp index 628a0c672e..5fbb893f3f 100644 --- a/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateEmergencyStopped.cpp @@ -189,6 +189,7 @@ TEST_F(RobotControllerTest, stateEmergencyStoppedDiceRollDetectedDelayNotOver) auto maximal_delay_before_over = 1s; + expectedCallsResetAutonomousActivitiesTimeout(); EXPECT_CALL(mock_videokit, displayImage).Times(0); spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); @@ -209,6 +210,7 @@ TEST_F(RobotControllerTest, auto minimal_delay_over = 1001ms; + expectedCallsResetAutonomousActivitiesTimeout(); EXPECT_CALL(mock_videokit, displayImage).Times(1); spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); @@ -241,6 +243,8 @@ TEST_F(RobotControllerTest, EXPECT_CALL(timeout_state_internal, onTimeout).InSequence(start_charging_behavior_sequence); EXPECT_CALL(timeout_state_internal, start).InSequence(start_charging_behavior_sequence); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); rc.onMagicCardAvailable(MagicCard::dice_roll); diff --git a/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp b/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp index 095251a318..5c1aa79ada 100644 --- a/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateFileExchange.cpp @@ -129,6 +129,8 @@ TEST_F(RobotControllerTest, stateFileExchangeEventEmergencyStopDelayNotOver) auto maximal_delay_before_over = 9s; + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -152,6 +154,8 @@ TEST_F(RobotControllerTest, stateFileExchangeEventEmergencyStopDelayOver) EXPECT_CALL(mock_lcd, turnOff).Times(1); EXPECT_CALL(mock_videokit, stopVideo).Times(2); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(delay_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); diff --git a/libs/RobotKit/tests/RobotController_test_stateIdle.cpp b/libs/RobotKit/tests/RobotController_test_stateIdle.cpp index 15ac9dcb50..666e67ce3e 100644 --- a/libs/RobotKit/tests/RobotController_test_stateIdle.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateIdle.cpp @@ -131,6 +131,8 @@ TEST_F(RobotControllerTest, stateIdleEventEmergencyStopDelayNotOver) auto maximal_delay_before_over = 9s; + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -153,6 +155,8 @@ TEST_F(RobotControllerTest, stateIdleEventEmergencyStopDelayOver) EXPECT_CALL(mock_lcd, turnOff).Times(1); EXPECT_CALL(mock_videokit, stopVideo).Times(AtLeast(1)); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(delay_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -168,6 +172,8 @@ TEST_F(RobotControllerTest, stateIdleDiceRollDetectedDelayNotOver) EXPECT_CALL(mock_videokit, displayImage).Times(0); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::dice_roll); @@ -186,6 +192,7 @@ TEST_F(RobotControllerTest, stateIdleDiceRollDetectedDelayOverEventAutonomousAct EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_idle_sequence); expectedCallsStopMotors(); + expectedCallsResetAutonomousActivitiesTimeout(); EXPECT_CALL(mock_videokit, displayImage).Times(1); spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); diff --git a/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp b/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp index 861a84df31..49a029271d 100644 --- a/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp @@ -104,6 +104,8 @@ TEST_F(RobotControllerTest, stateSleepingEventEmergencyStopDelayNotOver) auto maximal_delay_before_over = 9s; + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -127,6 +129,8 @@ TEST_F(RobotControllerTest, stateSleepingEventEmergencyStopDelayOver) EXPECT_CALL(mock_lcd, turnOff).Times(1); EXPECT_CALL(mock_videokit, stopVideo).Times(AtLeast(1)); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(delay_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -142,6 +146,8 @@ TEST_F(RobotControllerTest, stateSleepingDiceRollDetectedDelayNotOver) EXPECT_CALL(mock_videokit, displayImage).Times(0); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::dice_roll); @@ -161,6 +167,7 @@ TEST_F(RobotControllerTest, stateSleepingDiceRollDetectedDelayOverEventAutonomou EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); expectedCallsStopMotors(); + expectedCallsResetAutonomousActivitiesTimeout(); EXPECT_CALL(mock_videokit, displayImage).Times(1); spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); diff --git a/libs/RobotKit/tests/RobotController_test_stateWorking.cpp b/libs/RobotKit/tests/RobotController_test_stateWorking.cpp index 23c7e31312..50dc44b0ad 100644 --- a/libs/RobotKit/tests/RobotController_test_stateWorking.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateWorking.cpp @@ -78,6 +78,8 @@ TEST_F(RobotControllerTest, stateWorkingEventEmergencyStopDelayNotOver) auto maximal_delay_before_over = 10s; + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -100,6 +102,8 @@ TEST_F(RobotControllerTest, stateWorkingEventEmergencyStopDelayOver) EXPECT_CALL(mock_lcd, turnOff).Times(1); EXPECT_CALL(mock_videokit, stopVideo).Times(2); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(delay_over); rc.onMagicCardAvailable(MagicCard::emergency_stop); @@ -114,6 +118,8 @@ TEST_F(RobotControllerTest, stateWorkingDiceRollDetectedDelayNotOver) EXPECT_CALL(mock_videokit, displayImage).Times(0); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::dice_roll); @@ -130,6 +136,7 @@ TEST_F(RobotControllerTest, stateWorkingDiceRollDetectedDelayOverEventAutonomous Sequence on_exit_working_sequence; EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_working_sequence); + expectedCallsResetAutonomousActivitiesTimeout(); EXPECT_CALL(mock_videokit, displayImage).Times(1); spy_kernel_addElapsedTimeToTickCount(minimal_delay_over); @@ -153,6 +160,8 @@ TEST_F(RobotControllerTest, stateWorkingImpossibleSituationActivityStarted) }; set_activitykit_is_playing(); + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::number_0); @@ -166,6 +175,8 @@ TEST_F(RobotControllerTest, stateWorkingActivityStartedNotPlaying) auto maximal_delay_before_over = 1s; + expectedCallsResetAutonomousActivitiesTimeout(); + spy_kernel_addElapsedTimeToTickCount(maximal_delay_before_over); rc.onMagicCardAvailable(MagicCard::number_0); From b9b43ac98e128663e7b849beaf15a2261a03e694 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Tue, 14 Mar 2023 14:52:58 +0100 Subject: [PATCH 142/143] :fire: (State Machine): Remove DeepSleeping SM state --- libs/RobotKit/include/StateMachine.h | 4 +- .../RobotController_test_stateCharging.cpp | 44 +++++++++---------- .../RobotController_test_stateSleeping.cpp | 42 +++++++++--------- libs/RobotKit/tests/StateMachine_test.cpp | 36 +++++++-------- 4 files changed, 63 insertions(+), 63 deletions(-) diff --git a/libs/RobotKit/include/StateMachine.h b/libs/RobotKit/include/StateMachine.h index 10c03c75bc..b6eda9921f 100644 --- a/libs/RobotKit/include/StateMachine.h +++ b/libs/RobotKit/include/StateMachine.h @@ -232,7 +232,7 @@ struct StateMachine { , sm::state::sleeping + event [sm::guard::is_charging {}] = sm::state::charging , sm::state::sleeping + event = sm::state::emergency_stopped , sm::state::sleeping + event = sm::state::autonomous_activities - , sm::state::sleeping + event = sm::state::deep_sleeping + //, sm::state::sleeping + event = sm::state::deep_sleeping , sm::state::deep_sleeping + boost::sml::on_entry<_> / sm::action::suspend_hardware_for_deep_sleep {} @@ -248,7 +248,7 @@ struct StateMachine { , sm::state::charging + event = sm::state::charging , sm::state::charging + event = sm::state::emergency_stopped , sm::state::charging + event = sm::state::charging - , sm::state::charging + event = sm::state::deep_sleeping + //, sm::state::charging + event = sm::state::deep_sleeping , sm::state::file_exchange + boost::sml::on_entry<_> / sm::action::start_file_exchange {} , sm::state::file_exchange + boost::sml::on_exit<_> / sm::action::stop_file_exchange {} diff --git a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp index 08b71a56b0..7683debc89 100644 --- a/libs/RobotKit/tests/RobotController_test_stateCharging.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateCharging.cpp @@ -458,25 +458,25 @@ TEST_F(RobotControllerTest, stateChargingDiceRollDetectedDelayOverEventAutonomou EXPECT_TRUE(rc.state_machine.is(lksm::state::charging)); } -TEST_F(RobotControllerTest, stateChargingEventTimeout) -{ - Sequence get_on_deep_sleep_timeout_callback; - EXPECT_CALL(timeout_state_transition, onTimeout) - .InSequence(get_on_deep_sleep_timeout_callback) - .WillOnce(GetCallback(&on_deep_sleep_timeout)); - EXPECT_CALL(timeout_state_transition, start).InSequence(get_on_deep_sleep_timeout_callback); - rc.startDeepSleepTimeout(); - - rc.state_machine.set_current_states(lksm::state::charging); - - Sequence on_charging_exit_sequence; - EXPECT_CALL(timeout_state_transition, stop).InSequence(on_charging_exit_sequence); - EXPECT_CALL(timeout_state_internal, stop).InSequence(on_charging_exit_sequence); - EXPECT_CALL(mock_ledkit, stop).InSequence(on_charging_exit_sequence); - EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_charging_exit_sequence); - expectedCallsStopMotors(); - - on_deep_sleep_timeout(); - - EXPECT_TRUE(rc.state_machine.is(lksm::state::deep_sleeping)); -} +// TEST_F(RobotControllerTest, stateChargingEventTimeout) +// { +// Sequence get_on_deep_sleep_timeout_callback; +// EXPECT_CALL(timeout_state_transition, onTimeout) +// .InSequence(get_on_deep_sleep_timeout_callback) +// .WillOnce(GetCallback(&on_deep_sleep_timeout)); +// EXPECT_CALL(timeout_state_transition, start).InSequence(get_on_deep_sleep_timeout_callback); +// rc.startDeepSleepTimeout(); + +// rc.state_machine.set_current_states(lksm::state::charging); + +// Sequence on_charging_exit_sequence; +// EXPECT_CALL(timeout_state_transition, stop).InSequence(on_charging_exit_sequence); +// EXPECT_CALL(timeout_state_internal, stop).InSequence(on_charging_exit_sequence); +// EXPECT_CALL(mock_ledkit, stop).InSequence(on_charging_exit_sequence); +// EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_charging_exit_sequence); +// expectedCallsStopMotors(); + +// on_deep_sleep_timeout(); + +// EXPECT_TRUE(rc.state_machine.is(lksm::state::deep_sleeping)); +// } diff --git a/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp b/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp index 49a029271d..d0942b04a9 100644 --- a/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp +++ b/libs/RobotKit/tests/RobotController_test_stateSleeping.cpp @@ -176,24 +176,24 @@ TEST_F(RobotControllerTest, stateSleepingDiceRollDetectedDelayOverEventAutonomou EXPECT_TRUE(rc.state_machine.is(lksm::state::autonomous_activities)); } -TEST_F(RobotControllerTest, stateSleepingEventTimeout) -{ - Sequence get_on_deep_sleep_timeout_callback; - EXPECT_CALL(timeout_state_transition, onTimeout) - .InSequence(get_on_deep_sleep_timeout_callback) - .WillOnce(GetCallback(&on_deep_sleep_timeout)); - EXPECT_CALL(timeout_state_transition, start).InSequence(get_on_deep_sleep_timeout_callback); - rc.startDeepSleepTimeout(); - - rc.state_machine.set_current_states(lksm::state::sleeping); - - Sequence on_exit_sleeping_sequence; - EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_sleeping_sequence); - EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); - EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); - expectedCallsStopMotors(); - - on_deep_sleep_timeout(); - - EXPECT_TRUE(rc.state_machine.is(lksm::state::deep_sleeping)); -} +// TEST_F(RobotControllerTest, stateSleepingEventTimeout) +// { +// Sequence get_on_deep_sleep_timeout_callback; +// EXPECT_CALL(timeout_state_transition, onTimeout) +// .InSequence(get_on_deep_sleep_timeout_callback) +// .WillOnce(GetCallback(&on_deep_sleep_timeout)); +// EXPECT_CALL(timeout_state_transition, start).InSequence(get_on_deep_sleep_timeout_callback); +// rc.startDeepSleepTimeout(); + +// rc.state_machine.set_current_states(lksm::state::sleeping); + +// Sequence on_exit_sleeping_sequence; +// EXPECT_CALL(timeout_state_transition, stop).InSequence(on_exit_sleeping_sequence); +// EXPECT_CALL(timeout_state_internal, stop).InSequence(on_exit_sleeping_sequence); +// EXPECT_CALL(mock_videokit, stopVideo).InSequence(on_exit_sleeping_sequence); +// expectedCallsStopMotors(); + +// on_deep_sleep_timeout(); + +// EXPECT_TRUE(rc.state_machine.is(lksm::state::deep_sleeping)); +// } diff --git a/libs/RobotKit/tests/StateMachine_test.cpp b/libs/RobotKit/tests/StateMachine_test.cpp index b682d67334..c0f483df27 100644 --- a/libs/RobotKit/tests/StateMachine_test.cpp +++ b/libs/RobotKit/tests/StateMachine_test.cpp @@ -232,18 +232,18 @@ TEST_F(StateMachineTest, stateSleepEventAutonomousActivityRequested) EXPECT_TRUE(sm.is(lksm::state::autonomous_activities)); } -TEST_F(StateMachineTest, stateSleepEventTimeout) -{ - sm.set_current_states(lksm::state::sleeping); +// TEST_F(StateMachineTest, stateSleepEventTimeout) +// { +// sm.set_current_states(lksm::state::sleeping); - EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); - EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); - EXPECT_CALL(mock_rc, suspendHardwareForDeepSleep).Times(1); +// EXPECT_CALL(mock_rc, stopSleepingBehavior).Times(1); +// EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); +// EXPECT_CALL(mock_rc, suspendHardwareForDeepSleep).Times(1); - sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); +// sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); - EXPECT_TRUE(sm.is(lksm::state::deep_sleeping)); -} +// EXPECT_TRUE(sm.is(lksm::state::deep_sleeping)); +// } TEST_F(StateMachineTest, stateIdleEventChargeDidStart) { @@ -359,18 +359,18 @@ TEST_F(StateMachineTest, stateChargingEventAutonomousActivityRequested) EXPECT_TRUE(sm.is(lksm::state::charging)); } -TEST_F(StateMachineTest, stateChargingEventTimeout) -{ - sm.set_current_states(lksm::state::charging); +// TEST_F(StateMachineTest, stateChargingEventTimeout) +// { +// sm.set_current_states(lksm::state::charging); - EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); - EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); - EXPECT_CALL(mock_rc, suspendHardwareForDeepSleep).Times(1); +// EXPECT_CALL(mock_rc, stopChargingBehavior).Times(1); +// EXPECT_CALL(mock_rc, stopDeepSleepTimeout).Times(1); +// EXPECT_CALL(mock_rc, suspendHardwareForDeepSleep).Times(1); - sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); +// sm.process_event(lksm::event::deep_sleep_timeout_did_end {}); - EXPECT_TRUE(sm.is(lksm::state::deep_sleeping)); -} +// EXPECT_TRUE(sm.is(lksm::state::deep_sleeping)); +// } TEST_F(StateMachineTest, stateSleepingEventBleConnection) { From d96020a87dbc5340190c7e1f975805892400f353 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Fri, 13 Jan 2023 10:57:05 +0100 Subject: [PATCH 143/143] :bookmark: (bump): Bump version v1.4.0 --- config/os_version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/os_version b/config/os_version index f0bb29e763..88c5fb891d 100644 --- a/config/os_version +++ b/config/os_version @@ -1 +1 @@ -1.3.0 +1.4.0