From 9535f45c72e715b3c29425ddf1b2d3add0d193bb Mon Sep 17 00:00:00 2001 From: Clemens Elflein Date: Fri, 13 Jun 2025 22:18:26 +0000 Subject: [PATCH 01/10] Added HighlevelService Co-authored-by: Robert Vollmer --- CMakeLists.txt | 2 + services | 2 +- src/services.cpp | 2 + src/services.hpp | 2 + .../high_level_service/high_level_service.cpp | 46 +++++++++++ .../high_level_service/high_level_service.hpp | 82 +++++++++++++++++++ 6 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 src/services/high_level_service/high_level_service.cpp create mode 100644 src/services/high_level_service/high_level_service.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ca2987e4..a4f1c5c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/services/mower_service/mower_service.cpp src/services/gps_service/gps_service.cpp src/services/input_service/input_service.cpp + src/services/high_level_service/high_level_service.cpp # BQ2567 driver src/drivers/charger/bq_2576/bq_2576.cpp # BQ25679 driver @@ -129,6 +130,7 @@ target_add_service(${CMAKE_PROJECT_NAME} DiffDriveService ${CMAKE_CURRENT_SOURCE target_add_service(${CMAKE_PROJECT_NAME} MowerService ${CMAKE_CURRENT_SOURCE_DIR}/services/mower_service.json) target_add_service(${CMAKE_PROJECT_NAME} GpsService ${CMAKE_CURRENT_SOURCE_DIR}/services/gps_service.json) target_add_service(${CMAKE_PROJECT_NAME} InputService ${CMAKE_CURRENT_SOURCE_DIR}/services/input_service.json) +target_add_service(${CMAKE_PROJECT_NAME} HighLevelService ${CMAKE_CURRENT_SOURCE_DIR}/services/high_level_service.json) set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES SUFFIX ".elf") diff --git a/services b/services index e2d8b794..1bebed2d 160000 --- a/services +++ b/services @@ -1 +1 @@ -Subproject commit e2d8b7942f1d7f42f92d9c53caffec1634d6dd34 +Subproject commit 1bebed2d6c4f2ab080691bc263962dac77694ffb diff --git a/src/services.cpp b/src/services.cpp index 78f0f3ff..034bce0a 100644 --- a/src/services.cpp +++ b/src/services.cpp @@ -15,6 +15,7 @@ ImuService imu_service{xbot::service_ids::IMU}; PowerService power_service{xbot::service_ids::POWER}; GpsService gps_service{xbot::service_ids::GPS}; InputService input_service{xbot::service_ids::INPUT}; +HighLevelService high_level_service{xbot::service_ids::HIGH_LEVEL}; void StartServices() { #define START_IF_NEEDED(service, id) \ @@ -36,4 +37,5 @@ void StartServices() { START_IF_NEEDED(mower_service, MOWER) START_IF_NEEDED(gps_service, GPS) START_IF_NEEDED(input_service, INPUT) + START_IF_NEEDED(high_level_service, HIGH_LEVEL) } diff --git a/src/services.hpp b/src/services.hpp index 07d316c5..7ce2cdd8 100644 --- a/src/services.hpp +++ b/src/services.hpp @@ -4,6 +4,7 @@ #include "services/diff_drive_service/diff_drive_service.hpp" #include "services/emergency_service/emergency_service.hpp" #include "services/gps_service/gps_service.hpp" +#include "services/high_level_service/high_level_service.hpp" #include "services/imu_service/imu_service.hpp" #include "services/input_service/input_service.hpp" #include "services/mower_service/mower_service.hpp" @@ -16,6 +17,7 @@ extern ImuService imu_service; extern PowerService power_service; extern GpsService gps_service; extern InputService input_service; +extern HighLevelService high_level_service; void StartServices(); diff --git a/src/services/high_level_service/high_level_service.cpp b/src/services/high_level_service/high_level_service.cpp new file mode 100644 index 00000000..f25bfbb6 --- /dev/null +++ b/src/services/high_level_service/high_level_service.cpp @@ -0,0 +1,46 @@ +#include "high_level_service.hpp" + +void HighLevelService::OnStateIDChanged(const HighLevelStatus &new_value) { + xbot::service::Lock lk{&mtx_}; + state_id_ = new_value; +} + +void HighLevelService::OnStateNameChanged(const char *new_value, uint32_t length) { + xbot::service::Lock lk{&mtx_}; + if (new_value && length > 0 && length < state_name_.max_size()) { + state_name_ = new_value; + } +} + +void HighLevelService::OnSubStateNameChanged(const char *new_value, uint32_t length) { + xbot::service::Lock lk{&mtx_}; + if (new_value && length > 0 && length < sub_state_name_.max_size()) { + sub_state_name_ = new_value; + } +} + +void HighLevelService::OnGpsQualityChanged(const float &new_value) { + xbot::service::Lock lk{&mtx_}; + gps_quality_ = new_value; +} + +void HighLevelService::OnCurrentAreaChanged(const int16_t &new_value) { + xbot::service::Lock lk{&mtx_}; + current_area_ = new_value; +} + +void HighLevelService::OnCurrentPathChanged(const int16_t &new_value) { + xbot::service::Lock lk{&mtx_}; + current_path_ = new_value; +} + +void HighLevelService::OnCurrentPathIndexChanged(const int16_t &new_value) { + xbot::service::Lock lk{&mtx_}; + current_path_index_ = new_value; +} + +void HighLevelService::OnTransactionEnd() { + if (state_changed_callback_) { + state_changed_callback_(); + } +} diff --git a/src/services/high_level_service/high_level_service.hpp b/src/services/high_level_service/high_level_service.hpp new file mode 100644 index 00000000..9668f471 --- /dev/null +++ b/src/services/high_level_service/high_level_service.hpp @@ -0,0 +1,82 @@ +#ifndef HIGH_LEVEL_SERVICE_HPP +#define HIGH_LEVEL_SERVICE_HPP + +#include + +#include +#include + +using namespace xbot::service; + +class HighLevelService : public HighLevelServiceBase { + private: + THD_WORKING_AREA(wa, 1024){}; + + public: + explicit HighLevelService(uint16_t service_id) : HighLevelServiceBase(service_id, wa, sizeof(wa)) { + } + + HighLevelStatus GetStateId() { + xbot::service::Lock lk{&mtx_}; + return state_id_; + } + + etl::string<100> GetStateName() { + xbot::service::Lock lk{&mtx_}; + return state_name_; + } + + etl::string<100> GetSubStateName() { + xbot::service::Lock lk{&mtx_}; + return sub_state_name_; + } + + float GetGpsQuality() { + xbot::service::Lock lk{&mtx_}; + return gps_quality_; + } + + int16_t GetCurrentArea() { + xbot::service::Lock lk{&mtx_}; + return current_area_; + } + + int16_t GetCurrentPath() { + xbot::service::Lock lk{&mtx_}; + return current_path_; + } + + int16_t GetCurrentPathIndex() { + xbot::service::Lock lk{&mtx_}; + return current_path_index_; + } + + void SetCallback(const etl::delegate& callback) { + xbot::service::Lock lk{&mtx_}; + state_changed_callback_ = callback; + } + + private: + MUTEX_DECL(mtx_); + + HighLevelStatus state_id_ = HighLevelStatus::UNKNOWN; + etl::string<100> state_name_{}; + etl::string<100> sub_state_name_{}; + float gps_quality_ = 0; + int16_t current_area_ = 0; + int16_t current_path_ = 0; + int16_t current_path_index_ = 0; + + etl::delegate state_changed_callback_{}; + + void OnStateIDChanged(const HighLevelStatus& new_value) override; + void OnStateNameChanged(const char* new_value, uint32_t length) override; + void OnSubStateNameChanged(const char* new_value, uint32_t length) override; + void OnGpsQualityChanged(const float& new_value) override; + void OnCurrentAreaChanged(const int16_t& new_value) override; + void OnCurrentPathChanged(const int16_t& new_value) override; + void OnCurrentPathIndexChanged(const int16_t& new_value) override; + void OnTransactionEnd() override; +}; + +#endif // HIGH_LEVEL_SERVICE_HPP From ed41bd1b87105cad47ca6d4c8b82a0d9522c52af Mon Sep 17 00:00:00 2001 From: Clemens Elflein Date: Fri, 13 Jun 2025 22:38:04 +0000 Subject: [PATCH 02/10] wip cover UI --- CMakeLists.txt | 2 + robots/include/yardforce_robot.hpp | 2 + robots/src/yardforce_robot.cpp | 1 + src/drivers/ui/YardForceCoverUI/COBS.h | 107 ++++++++ src/drivers/ui/YardForceCoverUI/ui_board.h | 165 ++++++++++++ .../yard_force_cover_ui_driver.cpp | 248 ++++++++++++++++++ .../yard_force_cover_ui_driver.hpp | 48 ++++ .../high_level_service/high_level_service.hpp | 25 +- 8 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 src/drivers/ui/YardForceCoverUI/COBS.h create mode 100644 src/drivers/ui/YardForceCoverUI/ui_board.h create mode 100644 src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp create mode 100644 src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a4f1c5c1..7703064a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/drivers/input/gpio_input_driver.cpp src/drivers/input/worx_input_driver.cpp $<$:src/drivers/input/simulated_input_driver.cpp> + # YardForce Cover UI Driver + src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp # Raw driver debug interface src/debug/debug_tcp_interface.cpp src/debug/debug_udp_interface.cpp diff --git a/robots/include/yardforce_robot.hpp b/robots/include/yardforce_robot.hpp index b179da4c..23aadf4b 100644 --- a/robots/include/yardforce_robot.hpp +++ b/robots/include/yardforce_robot.hpp @@ -2,6 +2,7 @@ #define YARDFORCE_ROBOT_HPP #include +#include #include "robot.hpp" @@ -36,6 +37,7 @@ class YardForceRobot : public MowerRobot { private: BQ2576 charger_{}; + YardForceCoverUIDriver cover_ui_driver_{}; }; #endif // YARDFORCE_ROBOT_HPP diff --git a/robots/src/yardforce_robot.cpp b/robots/src/yardforce_robot.cpp index b2c9684a..fd9b66de 100644 --- a/robots/src/yardforce_robot.cpp +++ b/robots/src/yardforce_robot.cpp @@ -6,6 +6,7 @@ void YardForceRobot::InitPlatform() { InitMotors(); charger_.setI2C(&I2CD1); power_service.SetDriver(&charger_); + cover_ui_driver_.Start(&UARTD7); } bool YardForceRobot::IsHardwareSupported() { diff --git a/src/drivers/ui/YardForceCoverUI/COBS.h b/src/drivers/ui/YardForceCoverUI/COBS.h new file mode 100644 index 00000000..6e245a67 --- /dev/null +++ b/src/drivers/ui/YardForceCoverUI/COBS.h @@ -0,0 +1,107 @@ +// +// Copyright (c) 2011 Christopher Baker +// Copyright (c) 2011 Jacques Fortier +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRC_COBS_H +#define SRC_COBS_H + +/// \brief A Consistent Overhead Byte Stuffing (COBS) Encoder. +/// +/// Consistent Overhead Byte Stuffing (COBS) is an encoding that removes all 0 +/// bytes from arbitrary binary data. The encoded data consists only of bytes +/// with values from 0x01 to 0xFF. This is useful for preparing data for +/// transmission over a serial link (RS-232 or RS-485 for example), as the 0 +/// byte can be used to unambiguously indicate packet boundaries. COBS also has +/// the advantage of adding very little overhead (at least 1 byte, plus up to an +/// additional byte per 254 bytes of data). For messages smaller than 254 bytes, +/// the overhead is constant. +/// +/// \sa http://conferences.sigcomm.org/sigcomm/1997/papers/p062.pdf +/// \sa http://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing +/// \sa https://github.com/jacquesf/COBS-Consistent-Overhead-Byte-Stuffing +/// \sa http://www.jacquesf.com/2011/03/consistent-overhead-byte-stuffing +class COBS { + public: + /// \brief Encode a byte buffer with the COBS encoder. + /// \param buffer A pointer to the unencoded buffer to encode. + /// \param size The number of bytes in the \p buffer. + /// \param encodedBuffer The buffer for the encoded bytes. + /// \returns The number of bytes written to the \p encodedBuffer. + /// \warning The encodedBuffer must have at least getEncodedBufferSize() + /// allocated. + static size_t encode(const uint8_t* buffer, size_t size, uint8_t* encodedBuffer) { + size_t read_index = 0; + size_t write_index = 1; + size_t code_index = 0; + uint8_t code = 1; + + while (read_index < size) { + if (buffer[read_index] == 0) { + encodedBuffer[code_index] = code; + code = 1; + code_index = write_index++; + read_index++; + } else { + encodedBuffer[write_index++] = buffer[read_index++]; + code++; + + if (code == 0xFF) { + encodedBuffer[code_index] = code; + code = 1; + code_index = write_index++; + } + } + } + + encodedBuffer[code_index] = code; + + return write_index; + } + + /// \brief Decode a COBS-encoded buffer. + /// \param encodedBuffer A pointer to the \p encodedBuffer to decode. + /// \param size The number of bytes in the \p encodedBuffer. + /// \param decodedBuffer The target buffer for the decoded bytes. + /// \returns The number of bytes written to the \p decodedBuffer. + /// \warning decodedBuffer must have a minimum capacity of size. + static size_t decode(const uint8_t* encodedBuffer, size_t size, uint8_t* decodedBuffer) { + if (size == 0) return 0; + + size_t read_index = 0; + size_t write_index = 0; + uint8_t code = 0; + uint8_t i = 0; + + while (read_index < size) { + code = encodedBuffer[read_index]; + + if (read_index + code > size && code != 1) { + return 0; + } + + read_index++; + + for (i = 1; i < code; i++) { + decodedBuffer[write_index++] = encodedBuffer[read_index++]; + } + + if (code != 0xFF && read_index != size) { + decodedBuffer[write_index++] = '\0'; + } + } + + return write_index; + } + + /// \brief Get the maximum encoded buffer size for an unencoded buffer size. + /// \param unencodedBufferSize The size of the buffer to be encoded. + /// \returns the maximum size of the required encoded buffer. + static size_t getEncodedBufferSize(size_t unencodedBufferSize) { + return unencodedBufferSize + unencodedBufferSize / 254 + 1; + } +}; + +#endif diff --git a/src/drivers/ui/YardForceCoverUI/ui_board.h b/src/drivers/ui/YardForceCoverUI/ui_board.h new file mode 100644 index 00000000..b69c1f82 --- /dev/null +++ b/src/drivers/ui/YardForceCoverUI/ui_board.h @@ -0,0 +1,165 @@ +#ifndef _UI_DATATYPES_H_ +#define _UI_DATATYPES_H_ + +#include + +#include +// Protocol Header Info +enum TYPE { + Get_Version = 0xB0, + Set_Buzzer = 0xB1, + Set_LEDs = 0xB2, + Get_Button = 0xB3, + Get_Emergency = 0xB4, // Stock-CoverUI + Get_Rain = 0xB5, // Stock-CoverUI + Get_Subscribe = 0xB6 +}; + +// Function definitions for the 18 LEDS +enum LED_id { + LED_CHARGING = 0, + LED_BATTERY_LOW = 1, + LED_POOR_GPS = 2, + LED_MOWER_LIFTED = 3, + LED5 = 4, + LED6 = 5, + LED7 = 6, + LED8 = 7, + LED9 = 8, + LED10 = 9, + LED11 = 10, + LED_LOCK = 11, + LED_S2 = 12, + LED_S1 = 13, + LED15 = 14, + LED16 = 15, + LED17 = 16, + LED18 = 17 +}; + +enum LED_state { LED_off = 0b000, LED_blink_slow = 0b101, LED_blink_fast = 0b110, LED_on = 0b111 }; + +// Stock-CoverUI (same bitmask as in ll_status.emergency_bitmask of datatypes.h) +enum Emergency_state { + Emergency_latch = 0b00001, + Emergency_stop1 = 0b00010, + Emergency_stop2 = 0b00100, + Emergency_lift1 = 0b01000, + Emergency_lift2 = 0b10000 +}; + +enum Topic_state { + Topic_set_leds = 1 << 0, + Topic_set_ll_status = 1 << 1, + Topic_set_hl_state = 1 << 2, +}; + +#pragma pack(push, 1) +struct msg_get_version { + uint8_t type; // command type + uint8_t reserved; // padding + uint16_t version; + uint16_t crc; // CRC 16 +} __attribute__((packed)); +#pragma pack(pop) + +#pragma pack(push, 1) +struct msg_set_buzzer { + uint8_t type; // command type + uint8_t repeat; // Repeat X times + uint8_t on_time; // Signal on time + uint8_t off_time; // Signal off time + uint16_t crc; // CRC 16 +} __attribute__((packed)); +#pragma pack(pop) + +/** + * @brief Use this to update the LED matrix + * Each LED gets three bits with the following meaning: + * 0b000 = Off + * 0b001 = reserved for future use + * 0b010 = reserved for future use + * 0b011 = reserved for future use + * 0b100 = reserved for future use + * 0b101 = On slow blink + * 0b110 = On fast blink + * 0b111 = On + */ +#pragma pack(push, 1) +struct msg_set_leds { + uint8_t type; // command type + uint8_t reserved; // padding + uint64_t leds; + uint16_t crc; // CRC 16 +} __attribute__((packed)); +#pragma pack(pop) + +#pragma pack(push, 1) +struct msg_event_button { + uint8_t type; // command type + uint16_t button_id; + uint8_t press_duration; + uint16_t crc; // CRC 16 +} __attribute__((packed)); +#pragma pack(pop) + +// Stock-CoverUI +#pragma pack(push, 1) +struct msg_event_rain { + uint8_t type; // Command type + uint8_t reserved; // Padding + uint32_t value; + uint32_t threshold; // If value < threshold then it rains. Why a threshold? Cause there might be a future option to + // make it configurable on Stock-CoverUI + uint16_t crc; // CRC 16 +} __attribute__((packed)); +#pragma pack(pop) + +// Stock-CoverUI +#pragma pack(push, 1) +struct msg_event_emergency { + uint8_t type; // Command type + uint8_t state; // Same as in ll_status.emergency_bitmask of datatypes.h + uint16_t crc; // CRC 16 +} __attribute__((packed)); +#pragma pack(pop) + +// Cover UI might subscribe in what data it's interested to receive + +#pragma pack(push, 1) +struct msg_event_subscribe { + uint8_t type; // Command type + uint8_t topic_bitmask; // Bitmask of data subscription(s), see Topic_state + uint16_t interval; // Interval (ms) how often to send topic(s) + uint16_t crc; // CRC 16 +} __attribute__((packed)); +#pragma pack(pop) + +inline void setLed(struct msg_set_leds &msg, int led, uint8_t state) { + // mask the current led state + uint64_t mask = ~(((uint64_t)0b111) << (led * 3)); + msg.leds &= mask; + msg.leds |= ((uint64_t)(state & 0b111)) << (led * 3); +} + +inline void setBars7(struct msg_set_leds &msg, double value) { + int on_leds = round(value * 7.0); + for (int i = 0; i < 7; i++) { + setLed(msg, LED11 - i, i < on_leds ? LED_on : LED_off); + } +} + +inline void setBars4(struct msg_set_leds &msg, double value) { + if (value < 0) { + for (int i = 0; i < 4; i++) { + setLed(msg, i + LED15, LED_blink_fast); + } + } else { + int on_leds = round(value * 4.0); + for (int i = 0; i < 4; i++) { + setLed(msg, LED18 - i, i < on_leds ? LED_on : LED_off); + } + } +} + +#endif // _UI_DATATYPES_H_ diff --git a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp new file mode 100644 index 00000000..3bf9c270 --- /dev/null +++ b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp @@ -0,0 +1,248 @@ +#include "yard_force_cover_ui_driver.hpp" + +#include +#include + +#include + +#include "COBS.h" +#include "ui_board.h" + +static constexpr uint8_t EVT_PACKET_RECEIVED = 1; + +void YardForceCoverUIDriver::Start(UARTDriver *uart) { + if (uart == nullptr) { + chDbgAssert(uart != nullptr, "UART cannot be null"); + return; + } + if (thread_ != nullptr) { + ULOG_ERROR("Started YardForce CoverUI Driver twice!"); + return; + } + + // Init the UART + this->uart_ = uart; + uart_config_.speed = 115200; + uart_config_.context = this; + uart_config_.rxchar_cb = YardForceCoverUIDriver::UartRxChar; + if (uartStart(uart, &uart_config_) != MSG_OK) { + ULOG_ERROR("Error starting UART"); + return; + } + + thread_ = chThdCreateStatic(&wa_, sizeof(wa_), NORMALPRIO, ThreadHelper, this); +#ifdef USE_SEGGER_SYSTEMVIEW + processing_thread_->name = "YardForceUIDriver"; +#endif +} + +void YardForceCoverUIDriver::ThreadHelper(void *instance) { + auto *i = static_cast(instance); + i->ThreadFunc(); +} +void YardForceCoverUIDriver::UartRxChar(UARTDriver *driver, uint16_t data) { + YardForceCoverUIDriver *instance = reinterpret_cast(driver->config)->context; + chDbgAssert(instance != nullptr, "instance cannot be null!"); + chSysLockFromISR(); + if (instance->processing_) { + chSysUnlockFromISR(); + return; + } + instance->buffer_[instance->buffer_fill_++] = data; + if (data == 0) { + chEvtSignalI(instance->thread_, EVT_PACKET_RECEIVED); + } + chSysUnlockFromISR(); +} +void YardForceCoverUIDriver::ThreadFunc() { + while (true) { + bool timeout = chEvtWaitAnyTimeout(EVT_PACKET_RECEIVED, TIME_S2I(1)) == 0; + if (timeout) { + if (!board_found_) { + // Try to find the board + RequestFWVersion(); + } else { + UpdateUILeds(); + } + + continue; + } + chSysLock(); + // Forbid packet reception + processing_ = true; + chSysUnlock(); + if (buffer_fill_ > 0) { + ProcessPacket(); + } + buffer_fill_ = 0; + chSysLock(); + // Allow packet reception + processing_ = false; + chSysUnlock(); + } +} + +void YardForceCoverUIDriver::UpdateUILeds() { + // Show Info Docking LED + msg_set_leds leds_message{}; + leds_message.type = Set_LEDs; + + float charging_current = power_service.GetChargeCurrent(); + float adapter_volts = power_service.GetAdapterVolts(); + float battery_percent = power_service.GetBatteryPercent(); + float gps_quality_percent = high_level_service.GetGpsQuality(); + const auto high_level_state = high_level_service.GetHighLevelState(); + bool emergency = emergency_service.GetEmergency(); + + if ((charging_current > 0.80f) && (adapter_volts > 10.0f)) + setLed(leds_message, LED_CHARGING, LED_blink_fast); + else if ((charging_current <= 0.80f) && (charging_current >= 0.15f) && (adapter_volts > 20.0f)) + setLed(leds_message, LED_CHARGING, LED_blink_slow); + else if ((charging_current < 0.15f) && (adapter_volts > 20.0f)) + setLed(leds_message, LED_CHARGING, LED_on); + else + setLed(leds_message, LED_CHARGING, LED_off); + + // Show Info Battery state + if (battery_percent >= 0.1) + setLed(leds_message, LED_BATTERY_LOW, LED_off); + else + setLed(leds_message, LED_BATTERY_LOW, LED_on); + + if (adapter_volts < 10.0f) // activate only when undocked + { + // use the first LED row as bargraph + setBars7(leds_message, battery_percent); + if (gps_quality_percent == 0) { + // if quality is 0, flash all LEDs to notify the user to calibrate. + setBars4(leds_message, -1.0); + } else { + setBars4(leds_message, gps_quality_percent); + } + } else { + setBars7(leds_message, 0); + setBars4(leds_message, 0); + } + + if (gps_quality_percent < 0.25) { + setLed(leds_message, LED_POOR_GPS, LED_on); + } else if (gps_quality_percent < 0.50) { + setLed(leds_message, LED_POOR_GPS, LED_blink_fast); + } else if (gps_quality_percent < 0.75) { + setLed(leds_message, LED_POOR_GPS, LED_blink_slow); + } else { + setLed(leds_message, LED_POOR_GPS, LED_off); + } + + // Let S1 show if ros is connected and which state it's in + switch (high_level_state) { + case HighLevelService::HighLevelState::MODE_UNKNOWN: + setLed(leds_message, LED_S1, LED_off); + setLed(leds_message, LED_S2, LED_off); + break; + case HighLevelService::HighLevelState::MODE_IDLE: + setLed(leds_message, LED_S1, LED_on); + setLed(leds_message, LED_S2, LED_off); + break; + case HighLevelService::HighLevelState::MODE_AUTONOMOUS_MOWING: + setLed(leds_message, LED_S1, LED_blink_slow); + setLed(leds_message, LED_S2, LED_off); + break; + case HighLevelService::HighLevelState::MODE_AUTONOMOUS_DOCKING: + setLed(leds_message, LED_S1, LED_blink_slow); + setLed(leds_message, LED_S2, LED_blink_slow); + break; + case HighLevelService::HighLevelState::MODE_AUTONOMOUS_UNDOCKING: + setLed(leds_message, LED_S1, LED_blink_slow); + setLed(leds_message, LED_S2, LED_blink_fast); + break; + case HighLevelService::HighLevelState::MODE_RECORDING_OUTLINE: + setLed(leds_message, LED_S1, LED_blink_fast); + setLed(leds_message, LED_S2, LED_blink_slow); + break; + case HighLevelService::HighLevelState::MODE_RECORDING_OBSTACLE: + setLed(leds_message, LED_S1, LED_blink_fast); + setLed(leds_message, LED_S2, LED_blink_fast); + break; + default: + setLed(leds_message, LED_S1, LED_blink_fast); + setLed(leds_message, LED_S2, LED_on); + break; + } + + // Show Info mower lifted or stop button pressed + if (emergency) { + setLed(leds_message, LED_MOWER_LIFTED, LED_blink_fast); + } else { + setLed(leds_message, LED_MOWER_LIFTED, LED_off); + } + + sendUIMessage(&leds_message, sizeof(leds_message)); +} + +void YardForceCoverUIDriver::ProcessPacket() { + if (buffer_fill_ == 0) return; + uint16_t size = COBS::decode(const_cast(buffer_), buffer_fill_ - 1, encode_decode_buf_); + + auto *crc_pointer = reinterpret_cast(encode_decode_buf_ + (size - 2)); + u_int16_t readcrc = *crc_pointer; + + // check structure size + if (size < 4) { + return; + } + + // check the CRC + CRC16.reset(); + etl::begin(encode_decode_buf_); + CRC16.add(encode_decode_buf_, encode_decode_buf_ + size - 2); + uint16_t crc = CRC16.value(); + + if (crc != readcrc) return; + + if (encode_decode_buf_[0] == Get_Version && size == sizeof(struct msg_get_version)) { + board_found_ = true; + } else if (encode_decode_buf_[0] == Get_Button && size == sizeof(struct msg_event_button)) { + // TODO: Send an event to the InputService. + // msg_event_button *msg = (struct msg_event_button *)encode_decode_buf_; + } /* else if (encode_decode_buf_[0] == Get_Emergency && size == sizeof(struct msg_event_emergency)) { + struct msg_event_emergency *msg = (struct msg_event_emergency *)encode_decode_buf_; + stock_ui_emergency_state = msg->state; + } else if (encode_decode_buf_[0] == Get_Rain && size == sizeof(struct msg_event_rain)) { + struct msg_event_rain *msg = (struct msg_event_rain *)encode_decode_buf_; + stock_ui_rain = (msg->value < llhl_config.rain_threshold); + } else if (encode_decode_buf_[0] == Get_Subscribe && size == sizeof(struct msg_event_subscribe)) { + struct msg_event_subscribe *msg = (struct msg_event_subscribe *)encode_decode_buf_; + ui_topic_bitmask = msg->topic_bitmask; + ui_interval = msg->interval; + }*/ +} +void YardForceCoverUIDriver::sendUIMessage(void *msg, size_t size) { + // packages need to be at least 1 byte of type, 1 byte of data and 2 bytes of CRC + if (size < 4) { + return; + } + auto *data_pointer = static_cast(msg); + + // calculate the CRC + CRC16.reset(); + CRC16.add(data_pointer, data_pointer + size - 2); + uint16_t crc = CRC16.value(); + data_pointer[size - 1] = (crc >> 8) & 0xFF; + data_pointer[size - 2] = crc & 0xFF; + + if (COBS::getEncodedBufferSize(size) >= sizeof(encode_decode_buf_)) { + ULOG_ERROR("out_but_ size too small!"); + return; + } + + size_t encoded_size = COBS::encode(data_pointer, size, encode_decode_buf_); + encode_decode_buf_[encoded_size] = 0; + encoded_size++; + uartSendFullTimeout(uart_, &encoded_size, encode_decode_buf_, TIME_INFINITE); +} +void YardForceCoverUIDriver::RequestFWVersion() { + msg_get_version msg{}; + msg.type = Get_Version; + sendUIMessage(&msg, sizeof(msg)); +} diff --git a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp new file mode 100644 index 00000000..d53be3ae --- /dev/null +++ b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp @@ -0,0 +1,48 @@ +#ifndef YARD_FORCE_COVER_UI_DRIVER_HPP +#define YARD_FORCE_COVER_UI_DRIVER_HPP + +#include + +#include + +#include "ch.h" +#include "hal.h" +#include "ui_board.h" + +class YardForceCoverUIDriver { + private: + // Extend the config struct by a pointer to this instance, so that we can access it in callbacks. + struct UARTConfigEx : UARTConfig { + YardForceCoverUIDriver *context; + }; + + THD_WORKING_AREA(wa_, 1024); + thread_t *thread_ = nullptr; + UARTDriver *uart_ = nullptr; + UARTConfigEx uart_config_{}; + etl::crc16_ccitt CRC16{}; + + volatile uint8_t buffer_[255]; + volatile uint8_t buffer_fill_ = 0; + volatile bool processing_ = false; + + bool board_found_ = false; + uint8_t encode_decode_buf_[100]{}; + + static void ThreadHelper(void *instance); + static void UartRxChar(UARTDriver *driver, uint16_t data); + + void ThreadFunc(); + + void UpdateUILeds(); + + void ProcessPacket(); + + void sendUIMessage(void *msg, size_t size); + void RequestFWVersion(); + + public: + void Start(UARTDriver *uart); +}; + +#endif // YARD_FORCE_COVER_UI_DRIVER_HPP diff --git a/src/services/high_level_service/high_level_service.hpp b/src/services/high_level_service/high_level_service.hpp index 9668f471..fb198d4e 100644 --- a/src/services/high_level_service/high_level_service.hpp +++ b/src/services/high_level_service/high_level_service.hpp @@ -8,17 +8,38 @@ using namespace xbot::service; +#define HL_SUBMODE_SHIFT 6 + class HighLevelService : public HighLevelServiceBase { private: THD_WORKING_AREA(wa, 1024){}; public: + enum class HighLevelState : uint8_t { + // UNKNOWN (0) + MODE_UNKNOWN = 0, + + // IDLE mode (1) and submodes + MODE_IDLE = 1, + + // AUTONOMOUS mode (2) and submodes + MODE_AUTONOMOUS = 2, + MODE_AUTONOMOUS_MOWING = (0 << HL_SUBMODE_SHIFT) | MODE_AUTONOMOUS, + MODE_AUTONOMOUS_DOCKING = (1 << HL_SUBMODE_SHIFT) | MODE_AUTONOMOUS, + MODE_AUTONOMOUS_UNDOCKING = (2 << HL_SUBMODE_SHIFT) | MODE_AUTONOMOUS, + + // RECORDING mode (3) and submodes + MODE_RECORDING = 3, + MODE_RECORDING_OUTLINE = (1 << HL_SUBMODE_SHIFT) | MODE_RECORDING, + MODE_RECORDING_OBSTACLE = (2 << HL_SUBMODE_SHIFT) | MODE_RECORDING, + }; + explicit HighLevelService(uint16_t service_id) : HighLevelServiceBase(service_id, wa, sizeof(wa)) { } - HighLevelStatus GetStateId() { + HighLevelState GetHighLevelState() { xbot::service::Lock lk{&mtx_}; - return state_id_; + return static_cast(state_id_); } etl::string<100> GetStateName() { From df6230d508e8587b24eebb62c34959becbee1740 Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Sat, 14 Jun 2025 20:37:13 +0000 Subject: [PATCH 03/10] InputService delays config --- ext/xbot_framework | 2 +- services | 2 +- src/services/input_service/input_service.cpp | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ext/xbot_framework b/ext/xbot_framework index bc2e3b45..fe1ff211 160000 --- a/ext/xbot_framework +++ b/ext/xbot_framework @@ -1 +1 @@ -Subproject commit bc2e3b4570612318a1e21b21de9842a9ad9f8eec +Subproject commit fe1ff2112cfb57cf4af0e5d41d1c0acdddbe02e6 diff --git a/services b/services index e2d8b794..6a0ecc43 160000 --- a/services +++ b/services @@ -1 +1 @@ -Subproject commit e2d8b7942f1d7f42f92d9c53caffec1634d6dd34 +Subproject commit 6a0ecc4301c552edf74ae908df972c4e8da84920 diff --git a/src/services/input_service/input_service.cpp b/src/services/input_service/input_service.cpp index 1779ed4c..44769c5c 100644 --- a/src/services/input_service/input_service.cpp +++ b/src/services/input_service/input_service.cpp @@ -32,7 +32,7 @@ bool InputService::OnRegisterInputConfigsChanged(const void* data, size_t length lift_multiple_input_ = &all_inputs_.emplace_back(); lift_multiple_input_->idx = Input::VIRTUAL; lift_multiple_input_->emergency_reason = EmergencyReason::LIFT_MULTIPLE | EmergencyReason::LATCH; - lift_multiple_input_->emergency_delay_ms = 10; + lift_multiple_input_->emergency_delay_ms = LiftMultipleDelay.value; input_config_json_data_t json_data; json_data.callback = etl::make_delegate(*this); @@ -200,8 +200,13 @@ void InputService::OnInputChanged(Input& input, const bool active, const uint32_ SendInputEventHelper(input, InputEventType::ACTIVE); } else { SendInputEventHelper(input, InputEventType::INACTIVE); - // TODO: This obviously needs debouncing, more variants and configuration. - SendInputEventHelper(input, duration >= 500'000 ? InputEventType::LONG : InputEventType::SHORT); + if (duration >= LongPressTime.value) { + SendInputEventHelper(input, InputEventType::LONG); + } else if (duration >= DebounceTime.value) { + // Note that debouncing works only one-way here. + // If the input becomes inactive for just a nanosecond, it will interrupt the long press. + SendInputEventHelper(input, InputEventType::SHORT); + } } CommitTransaction(); } From aecd253d9d6041244a7aa6fa1933c7f99868b4ea Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Sat, 14 Jun 2025 20:37:13 +0000 Subject: [PATCH 04/10] InputService delays config --- ext/xbot_framework | 2 +- services | 2 +- src/services/input_service/input_service.cpp | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ext/xbot_framework b/ext/xbot_framework index bc2e3b45..fe1ff211 160000 --- a/ext/xbot_framework +++ b/ext/xbot_framework @@ -1 +1 @@ -Subproject commit bc2e3b4570612318a1e21b21de9842a9ad9f8eec +Subproject commit fe1ff2112cfb57cf4af0e5d41d1c0acdddbe02e6 diff --git a/services b/services index 1bebed2d..6a0ecc43 160000 --- a/services +++ b/services @@ -1 +1 @@ -Subproject commit 1bebed2d6c4f2ab080691bc263962dac77694ffb +Subproject commit 6a0ecc4301c552edf74ae908df972c4e8da84920 diff --git a/src/services/input_service/input_service.cpp b/src/services/input_service/input_service.cpp index 1779ed4c..44769c5c 100644 --- a/src/services/input_service/input_service.cpp +++ b/src/services/input_service/input_service.cpp @@ -32,7 +32,7 @@ bool InputService::OnRegisterInputConfigsChanged(const void* data, size_t length lift_multiple_input_ = &all_inputs_.emplace_back(); lift_multiple_input_->idx = Input::VIRTUAL; lift_multiple_input_->emergency_reason = EmergencyReason::LIFT_MULTIPLE | EmergencyReason::LATCH; - lift_multiple_input_->emergency_delay_ms = 10; + lift_multiple_input_->emergency_delay_ms = LiftMultipleDelay.value; input_config_json_data_t json_data; json_data.callback = etl::make_delegate(*this); @@ -200,8 +200,13 @@ void InputService::OnInputChanged(Input& input, const bool active, const uint32_ SendInputEventHelper(input, InputEventType::ACTIVE); } else { SendInputEventHelper(input, InputEventType::INACTIVE); - // TODO: This obviously needs debouncing, more variants and configuration. - SendInputEventHelper(input, duration >= 500'000 ? InputEventType::LONG : InputEventType::SHORT); + if (duration >= LongPressTime.value) { + SendInputEventHelper(input, InputEventType::LONG); + } else if (duration >= DebounceTime.value) { + // Note that debouncing works only one-way here. + // If the input becomes inactive for just a nanosecond, it will interrupt the long press. + SendInputEventHelper(input, InputEventType::SHORT); + } } CommitTransaction(); } From e75294d83ea2dd6a5879277547d5216e3bf2d8a9 Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Sat, 14 Jun 2025 21:59:44 +0000 Subject: [PATCH 05/10] Add a way to inject key presses --- src/drivers/input/input_driver.cpp | 13 +++++++++++-- src/drivers/input/input_driver.hpp | 5 ++++- src/services/input_service/input_service.hpp | 4 ++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/drivers/input/input_driver.cpp b/src/drivers/input/input_driver.cpp index 4ec17ab9..f8f43786 100644 --- a/src/drivers/input/input_driver.cpp +++ b/src/drivers/input/input_driver.cpp @@ -5,13 +5,13 @@ namespace xbot::driver::input { -bool Input::Update(bool new_active) { +bool Input::Update(bool new_active, uint32_t predate) { if (invert) { new_active = !new_active; } bool expected = !new_active; if (active.compare_exchange_strong(expected, new_active)) { - const uint32_t now = xbot::service::system::getTimeMicros(); + const uint32_t now = xbot::service::system::getTimeMicros() - predate; if (new_active) { active_since = now; } @@ -22,6 +22,15 @@ bool Input::Update(bool new_active) { return false; } +void Input::InjectPress(bool long_press) { + InjectPress(input_service.GetPressDelay(long_press)); +} + +void Input::InjectPress(uint32_t duration) { + Update(true, duration); + Update(false); +} + void InputDriver::AddInput(Input* input) { if (inputs_head_ == nullptr) { inputs_head_ = input; diff --git a/src/drivers/input/input_driver.hpp b/src/drivers/input/input_driver.hpp index 458e0c3e..40ac07f9 100644 --- a/src/drivers/input/input_driver.hpp +++ b/src/drivers/input/input_driver.hpp @@ -33,7 +33,10 @@ struct Input { return active; } - bool Update(bool new_active); + bool Update(bool new_active, uint32_t predate = 0); + + void InjectPress(bool long_press = false); + void InjectPress(uint32_t duration); uint32_t ActiveDuration(const uint32_t now) const { return now - active_since; diff --git a/src/services/input_service/input_service.hpp b/src/services/input_service/input_service.hpp index 6abe4cc8..52e93f01 100644 --- a/src/services/input_service/input_service.hpp +++ b/src/services/input_service/input_service.hpp @@ -32,6 +32,10 @@ class InputService : public InputServiceBase { etl::pair GetEmergencyReasons(uint32_t now); + uint32_t GetPressDelay(bool long_press = false) { + return long_press ? LongPressTime.value : DebounceTime.value; + } + private: MUTEX_DECL(mutex_); From 0e1ed149a18cd9a8411721a57b25139a3b6b252f Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Sun, 15 Jun 2025 22:41:42 +0000 Subject: [PATCH 06/10] Trigger emergency even for briefly active inputs This is especially relevant for the injected key presses, which predate the activation and immediately deactivate the input. The emergency check won't run between these two, so we need a flag for a pending emergency. --- src/drivers/input/input_driver.cpp | 13 ++++++++++++- src/drivers/input/input_driver.hpp | 8 +++++--- src/services/input_service/input_service.cpp | 11 +++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/drivers/input/input_driver.cpp b/src/drivers/input/input_driver.cpp index f8f43786..4a52c8ad 100644 --- a/src/drivers/input/input_driver.cpp +++ b/src/drivers/input/input_driver.cpp @@ -14,14 +14,25 @@ bool Input::Update(bool new_active, uint32_t predate) { const uint32_t now = xbot::service::system::getTimeMicros() - predate; if (new_active) { active_since = now; + input_service.OnInputChanged(*this, true, 0); + } else { + const uint32_t duration = ActiveDuration(now); + if (emergency_reason != 0 && duration >= emergency_delay_ms * 1'000) { + emergency_pending = true; + } + input_service.OnInputChanged(*this, false, duration); } - input_service.OnInputChanged(*this, new_active, ActiveDuration(now)); chEvtBroadcastFlags(&mower_events, MowerEvents::INPUTS_CHANGED); return true; } return false; } +bool Input::GetAndClearPendingEmergency() { + bool only_if_pending = true; + return emergency_pending.compare_exchange_strong(only_if_pending, false); +} + void Input::InjectPress(bool long_press) { InjectPress(input_service.GetPressDelay(long_press)); } diff --git a/src/drivers/input/input_driver.hpp b/src/drivers/input/input_driver.hpp index 40ac07f9..b8e2c39e 100644 --- a/src/drivers/input/input_driver.hpp +++ b/src/drivers/input/input_driver.hpp @@ -34,6 +34,7 @@ struct Input { } bool Update(bool new_active, uint32_t predate = 0); + bool GetAndClearPendingEmergency(); void InjectPress(bool long_press = false); void InjectPress(uint32_t duration); @@ -43,9 +44,10 @@ struct Input { } private: - etl::atomic active = false; - uint32_t active_since = 0; - Input* next_for_driver_ = nullptr; + etl::atomic active{false}; + etl::atomic emergency_pending{false}; + uint32_t active_since{0}; + Input* next_for_driver_{nullptr}; friend class InputDriver; friend struct InputIterable; diff --git a/src/services/input_service/input_service.cpp b/src/services/input_service/input_service.cpp index 44769c5c..bf6651df 100644 --- a/src/services/input_service/input_service.cpp +++ b/src/services/input_service/input_service.cpp @@ -223,10 +223,13 @@ etl::pair InputService::GetEmergencyReasons(uint32_t now) { Lock lk(&mutex_); uint16_t reasons = 0; uint32_t block_time = UINT32_MAX; - for (const auto& input : all_inputs_) { - // TODO: What if the input was triggered so briefly that we couldn't observe it? - if (input.emergency_reason == 0 || !input.IsActive()) continue; - if (TimeoutReached(input.ActiveDuration(now), input.emergency_delay_ms * 1'000, block_time)) { + for (auto& input : all_inputs_) { + if (input.emergency_reason == 0) continue; + if (input.GetAndClearPendingEmergency()) { + reasons |= input.emergency_reason; + block_time = 0; + } else if (input.IsActive() && + TimeoutReached(input.ActiveDuration(now), input.emergency_delay_ms * 1'000, block_time)) { reasons |= input.emergency_reason; } } From edcdf3bd373288afbcdad072e088b09b28539c39 Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Sun, 15 Jun 2025 23:48:36 +0000 Subject: [PATCH 07/10] Connect YardForce CoverUI to InputService --- CMakeLists.txt | 1 + robots/src/yardforce_robot.cpp | 1 + services | 2 +- src/drivers/input/gpio_input_driver.hpp | 1 - src/drivers/input/input_driver.hpp | 24 +++++++++++++++--- src/drivers/input/simulated_input_driver.hpp | 2 -- src/drivers/input/worx_input_driver.hpp | 1 - src/drivers/input/yard_force_input_driver.cpp | 23 +++++++++++++++++ src/drivers/input/yard_force_input_driver.hpp | 17 +++++++++++++ .../yard_force_cover_ui_driver.cpp | 25 ++++++++++++++----- .../yard_force_cover_ui_driver.hpp | 7 +++++- 11 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 src/drivers/input/yard_force_input_driver.cpp create mode 100644 src/drivers/input/yard_force_input_driver.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7703064a..855c5af4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/drivers/input/input_driver.cpp src/drivers/input/gpio_input_driver.cpp src/drivers/input/worx_input_driver.cpp + src/drivers/input/yard_force_input_driver.cpp $<$:src/drivers/input/simulated_input_driver.cpp> # YardForce Cover UI Driver src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp diff --git a/robots/src/yardforce_robot.cpp b/robots/src/yardforce_robot.cpp index fd9b66de..09eb6739 100644 --- a/robots/src/yardforce_robot.cpp +++ b/robots/src/yardforce_robot.cpp @@ -6,6 +6,7 @@ void YardForceRobot::InitPlatform() { InitMotors(); charger_.setI2C(&I2CD1); power_service.SetDriver(&charger_); + input_service.RegisterInputDriver("yardforce", &cover_ui_driver_.GetInputDriver()); cover_ui_driver_.Start(&UARTD7); } diff --git a/services b/services index 6a0ecc43..725db2c9 160000 --- a/services +++ b/services @@ -1 +1 @@ -Subproject commit 6a0ecc4301c552edf74ae908df972c4e8da84920 +Subproject commit 725db2c9cb07fc769ff75da987a22bfacb8a2dad diff --git a/src/drivers/input/gpio_input_driver.hpp b/src/drivers/input/gpio_input_driver.hpp index 61441fdd..d564b97a 100644 --- a/src/drivers/input/gpio_input_driver.hpp +++ b/src/drivers/input/gpio_input_driver.hpp @@ -1,7 +1,6 @@ #ifndef GPIO_INPUT_DRIVER_HPP #define GPIO_INPUT_DRIVER_HPP -#include #include #include "input_driver.hpp" diff --git a/src/drivers/input/input_driver.hpp b/src/drivers/input/input_driver.hpp index b8e2c39e..2051b96f 100644 --- a/src/drivers/input/input_driver.hpp +++ b/src/drivers/input/input_driver.hpp @@ -11,6 +11,7 @@ namespace xbot::driver::input { struct Input { enum { VIRTUAL = 255 }; + enum class Type : uint8_t { BUTTON, HALL }; // Configuration uint8_t idx; @@ -26,6 +27,18 @@ struct Input { struct { uint8_t bit; } worx; + + struct { + Input::Type type; + union { + struct { + uint8_t id; + } button; + struct { + uint8_t bit; + } hall; + }; + } yardforce; }; // State @@ -86,20 +99,23 @@ class InputDriver { public: virtual ~InputDriver() = default; explicit InputDriver() = default; + void AddInput(Input* input); void ClearInputs(); + InputIterable Inputs() { + return InputIterable{inputs_head_}; + } + virtual bool OnInputConfigValue(lwjson_stream_parser_t* jsp, const char* key, lwjson_stream_type_t type, Input& input) = 0; + virtual bool OnStart() { return true; }; virtual void OnStop(){}; - protected: + private: Input* inputs_head_ = nullptr; - InputIterable Inputs() { - return InputIterable{inputs_head_}; - } }; } // namespace xbot::driver::input diff --git a/src/drivers/input/simulated_input_driver.hpp b/src/drivers/input/simulated_input_driver.hpp index 71a66d49..5bdc6bff 100644 --- a/src/drivers/input/simulated_input_driver.hpp +++ b/src/drivers/input/simulated_input_driver.hpp @@ -1,8 +1,6 @@ #ifndef SIMULATED_INPUT_DRIVER_HPP #define SIMULATED_INPUT_DRIVER_HPP -#include - #include "input_driver.hpp" namespace xbot::driver::input { diff --git a/src/drivers/input/worx_input_driver.hpp b/src/drivers/input/worx_input_driver.hpp index 63fa0c05..9487dc2f 100644 --- a/src/drivers/input/worx_input_driver.hpp +++ b/src/drivers/input/worx_input_driver.hpp @@ -1,7 +1,6 @@ #ifndef WORX_INPUT_DRIVER_HPP #define WORX_INPUT_DRIVER_HPP -#include #include #include diff --git a/src/drivers/input/yard_force_input_driver.cpp b/src/drivers/input/yard_force_input_driver.cpp new file mode 100644 index 00000000..c59136b5 --- /dev/null +++ b/src/drivers/input/yard_force_input_driver.cpp @@ -0,0 +1,23 @@ +#include "yard_force_input_driver.hpp" + +#include + +#include + +namespace xbot::driver::input { + +bool YardForceInputDriver::OnInputConfigValue(lwjson_stream_parser_t *jsp, const char *key, lwjson_stream_type_t type, + Input &input) { + // TODO: We probably want to take strings here and map them to the ID. + if (strcmp(key, "button") == 0) { + input.yardforce.type = Input::Type::BUTTON; + return JsonGetNumber(jsp, type, input.yardforce.button.id); + } else if (strcmp(key, "hall") == 0) { + input.yardforce.type = Input::Type::HALL; + return JsonGetNumber(jsp, type, input.yardforce.hall.bit); + } + ULOG_ERROR("Unknown attribute \"%s\"", key); + return false; +} + +} // namespace xbot::driver::input diff --git a/src/drivers/input/yard_force_input_driver.hpp b/src/drivers/input/yard_force_input_driver.hpp new file mode 100644 index 00000000..72d661d5 --- /dev/null +++ b/src/drivers/input/yard_force_input_driver.hpp @@ -0,0 +1,17 @@ +#ifndef YARDFORCE_INPUT_DRIVER_HPP +#define YARDFORCE_INPUT_DRIVER_HPP + +#include + +#include "input_driver.hpp" + +namespace xbot::driver::input { +class YardForceInputDriver : public InputDriver { + public: + explicit YardForceInputDriver() = default; + bool OnInputConfigValue(lwjson_stream_parser_t *jsp, const char *key, lwjson_stream_type_t type, + Input &input) override; +}; +} // namespace xbot::driver::input + +#endif // YARDFORCE_INPUT_DRIVER_HPP diff --git a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp index 3bf9c270..bc738d59 100644 --- a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp +++ b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp @@ -3,11 +3,14 @@ #include #include +#include #include #include "COBS.h" #include "ui_board.h" +#define IS_BIT_SET(x, bit) ((x & (1 << bit)) != 0) + static constexpr uint8_t EVT_PACKET_RECEIVED = 1; void YardForceCoverUIDriver::Start(UARTDriver *uart) { @@ -203,12 +206,22 @@ void YardForceCoverUIDriver::ProcessPacket() { if (encode_decode_buf_[0] == Get_Version && size == sizeof(struct msg_get_version)) { board_found_ = true; } else if (encode_decode_buf_[0] == Get_Button && size == sizeof(struct msg_event_button)) { - // TODO: Send an event to the InputService. - // msg_event_button *msg = (struct msg_event_button *)encode_decode_buf_; - } /* else if (encode_decode_buf_[0] == Get_Emergency && size == sizeof(struct msg_event_emergency)) { - struct msg_event_emergency *msg = (struct msg_event_emergency *)encode_decode_buf_; - stock_ui_emergency_state = msg->state; - } else if (encode_decode_buf_[0] == Get_Rain && size == sizeof(struct msg_event_rain)) { + msg_event_button *msg = (struct msg_event_button *)encode_decode_buf_; + for (auto &input : input_driver_.Inputs()) { + if (input.yardforce.type == Input::Type::BUTTON && input.yardforce.button.id == msg->button_id) { + const bool long_press = msg->press_duration >= 1; + input.InjectPress(long_press); + break; + } + } + } else if (encode_decode_buf_[0] == Get_Emergency && size == sizeof(struct msg_event_emergency)) { + msg_event_emergency *msg = (struct msg_event_emergency *)encode_decode_buf_; + for (auto &input : input_driver_.Inputs()) { + if (input.yardforce.type == Input::Type::HALL) { + input.Update(IS_BIT_SET(msg->state, input.yardforce.hall.bit)); + } + } + } /* else if (encode_decode_buf_[0] == Get_Rain && size == sizeof(struct msg_event_rain)) { struct msg_event_rain *msg = (struct msg_event_rain *)encode_decode_buf_; stock_ui_rain = (msg->value < llhl_config.rain_threshold); } else if (encode_decode_buf_[0] == Get_Subscribe && size == sizeof(struct msg_event_subscribe)) { diff --git a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp index d53be3ae..626d7304 100644 --- a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp +++ b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include "ch.h" #include "hal.h" @@ -11,6 +11,8 @@ class YardForceCoverUIDriver { private: + YardForceInputDriver input_driver_{}; + // Extend the config struct by a pointer to this instance, so that we can access it in callbacks. struct UARTConfigEx : UARTConfig { YardForceCoverUIDriver *context; @@ -43,6 +45,9 @@ class YardForceCoverUIDriver { public: void Start(UARTDriver *uart); + YardForceInputDriver &GetInputDriver() { + return input_driver_; + } }; #endif // YARD_FORCE_COVER_UI_DRIVER_HPP From 7463cbd8fe879978bd7130e4aab879901354a18e Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Mon, 16 Jun 2025 20:13:42 +0000 Subject: [PATCH 08/10] Abstract the CRC lookup map --- boards/XCORE/board_utils.cpp | 29 +++------------- src/crc_lookup_map.hpp | 36 ++++++++++++++++++++ src/drivers/input/worx_input_driver.cpp | 45 +++++++++++++++---------- 3 files changed, 67 insertions(+), 43 deletions(-) create mode 100644 src/crc_lookup_map.hpp diff --git a/boards/XCORE/board_utils.cpp b/boards/XCORE/board_utils.cpp index b802a6e8..3a6fdcdf 100644 --- a/boards/XCORE/board_utils.cpp +++ b/boards/XCORE/board_utils.cpp @@ -1,6 +1,6 @@ #include "board_utils.hpp" -#include +#include #pragma pack(push, 1) struct LineParams { @@ -10,10 +10,6 @@ struct LineParams { }; #pragma pack(pop) -constexpr uint16_t crc16(const char* str) { - return etl::crc16_genibus(str, str + strlen(str)).value(); -} - constexpr uint32_t ports[] = {GPIOA_BASE, GPIOB_BASE, GPIOC_BASE, GPIOD_BASE, GPIOE_BASE, GPIOF_BASE, GPIOG_BASE, GPIOH_BASE}; @@ -123,28 +119,11 @@ constexpr LineParams lines[] = { {crc16("OSC_OUT"), port_idx('H'), 1}, }; -static_assert( - [] { - constexpr size_t count = sizeof(lines) / sizeof(LineParams); - for (size_t i = 0; i < count; i++) { - for (size_t j = i + 1; j < count; j++) { - if (lines[i].crc == lines[j].crc) { - return false; - } - } - } - return true; - }(), - "CRC16 values are not unique"); +static_assert(HasUniqueCrcs(lines), "CRC16 values are not unique"); ioline_t GetIoLineByName(const char* name) { - uint16_t crc = crc16(name); - for (const auto& line : lines) { - if (line.crc == crc) { - return PAL_LINE(ports[line.port], line.pad); - } - } - return PAL_NOLINE; + auto* line = LookupByName(name, lines); + return line != nullptr ? PAL_LINE(ports[line->port], line->pad) : PAL_NOLINE; } UARTDriver* GetUARTDriverByIndex(uint8_t index) { diff --git a/src/crc_lookup_map.hpp b/src/crc_lookup_map.hpp new file mode 100644 index 00000000..95051004 --- /dev/null +++ b/src/crc_lookup_map.hpp @@ -0,0 +1,36 @@ +#ifndef LOOKUP_HPP +#define LOOKUP_HPP + +#include + +#include +#include + +constexpr uint16_t crc16(const char* str) { + return etl::crc16_genibus(str, str + strlen(str)).value(); +} + +template +constexpr bool HasUniqueCrcs(const T (&arr)[N]) { + for (size_t i = 0; i < N; i++) { + for (size_t j = i + 1; j < N; j++) { + if (arr[i].crc == arr[j].crc) { + return false; + } + } + } + return true; +} + +template +const T* LookupByName(const char* name, const T (&arr)[N]) { + uint16_t crc = crc16(name); + for (const auto& item : arr) { + if (item.crc == crc) { + return &item; + } + } + return nullptr; +} + +#endif // LOOKUP_HPP diff --git a/src/drivers/input/worx_input_driver.cpp b/src/drivers/input/worx_input_driver.cpp index 6333b711..223ef986 100644 --- a/src/drivers/input/worx_input_driver.cpp +++ b/src/drivers/input/worx_input_driver.cpp @@ -1,42 +1,51 @@ #include "worx_input_driver.hpp" #include -#include -#include #include +#include #include #define IS_BIT_SET(x, bit) ((x & (1 << bit)) != 0) namespace xbot::driver::input { -static const etl::flat_map, uint8_t, 8> INPUT_BITS = { +#pragma pack(push, 1) +namespace { +struct InputParams { + uint16_t crc; + uint8_t bit; +}; +} // namespace +#pragma pack(pop) + +static constexpr InputParams available_inputs[] = { // Keys - {"start", 1}, - {"home", 2}, - {"back", 3}, + {crc16("start"), 1}, + {crc16("home"), 2}, + {crc16("back"), 3}, // Halls - {"battery_cover", 9}, - {"stop1", 10}, - {"trapped1", 12}, - {"trapped2", 13}, - {"stop2", 15}, + {crc16("battery_cover"), 9}, + {crc16("stop1"), 10}, + {crc16("trapped1"), 12}, + {crc16("trapped2"), 13}, + {crc16("stop2"), 15}, }; +static_assert(HasUniqueCrcs(available_inputs), "CRC16 values are not unique"); + bool WorxInputDriver::OnInputConfigValue(lwjson_stream_parser_t* jsp, const char* key, lwjson_stream_type_t type, Input& input) { if (strcmp(key, "id") == 0) { JsonExpectType(STRING); - decltype(INPUT_BITS)::key_type input_id{jsp->data.str.buff}; - auto bit_it = INPUT_BITS.find(input_id); - if (bit_it == INPUT_BITS.end()) { - ULOG_ERROR("Unknown Worx input ID \"%s\"", input_id.c_str()); - return false; + const auto* id = jsp->data.str.buff; + if (auto* params = LookupByName(id, available_inputs)) { + input.worx.bit = params->bit; + return true; } - input.worx.bit = bit_it->second; - return true; + ULOG_ERROR("Unknown ID \"%s\"", id); + return false; } ULOG_ERROR("Unknown attribute \"%s\"", key); return false; From 4f17cea688cd57dbae0ef7385f2a1a25a713dba3 Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Mon, 16 Jun 2025 20:53:40 +0000 Subject: [PATCH 09/10] Map YardForce button/hall id from string --- src/drivers/input/input_driver.hpp | 12 +---- src/drivers/input/yard_force_input_driver.cpp | 47 ++++++++++++++++--- .../yard_force_cover_ui_driver.cpp | 6 +-- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/drivers/input/input_driver.hpp b/src/drivers/input/input_driver.hpp index 2051b96f..af2120d4 100644 --- a/src/drivers/input/input_driver.hpp +++ b/src/drivers/input/input_driver.hpp @@ -11,7 +11,6 @@ namespace xbot::driver::input { struct Input { enum { VIRTUAL = 255 }; - enum class Type : uint8_t { BUTTON, HALL }; // Configuration uint8_t idx; @@ -29,15 +28,8 @@ struct Input { } worx; struct { - Input::Type type; - union { - struct { - uint8_t id; - } button; - struct { - uint8_t bit; - } hall; - }; + bool button : 1; + uint8_t id_or_bit : 7; } yardforce; }; diff --git a/src/drivers/input/yard_force_input_driver.cpp b/src/drivers/input/yard_force_input_driver.cpp index c59136b5..73b95677 100644 --- a/src/drivers/input/yard_force_input_driver.cpp +++ b/src/drivers/input/yard_force_input_driver.cpp @@ -2,19 +2,52 @@ #include +#include #include namespace xbot::driver::input { +#pragma pack(push, 1) +namespace { +struct InputParams { + uint16_t crc; + bool button : 1; + uint8_t id_or_bit : 7; +}; +} // namespace +#pragma pack(pop) + +static constexpr InputParams available_inputs[] = { + // Buttons + {crc16("home"), true, 2}, + {crc16("play"), true, 3}, + {crc16("s1"), true, 4}, + {crc16("s2"), true, 5}, + {crc16("lock"), true, 6}, + + // Halls + {crc16("stop1"), false, 1}, + {crc16("stop2"), false, 2}, + {crc16("lift"), false, 3}, + {crc16("bump"), false, 4}, + {crc16("liftx"), false, 5}, + {crc16("rbump"), false, 6}, +}; + +static_assert(HasUniqueCrcs(available_inputs), "CRC16 values are not unique"); + bool YardForceInputDriver::OnInputConfigValue(lwjson_stream_parser_t *jsp, const char *key, lwjson_stream_type_t type, Input &input) { - // TODO: We probably want to take strings here and map them to the ID. - if (strcmp(key, "button") == 0) { - input.yardforce.type = Input::Type::BUTTON; - return JsonGetNumber(jsp, type, input.yardforce.button.id); - } else if (strcmp(key, "hall") == 0) { - input.yardforce.type = Input::Type::HALL; - return JsonGetNumber(jsp, type, input.yardforce.hall.bit); + if (strcmp(key, "id") == 0) { + JsonExpectType(STRING); + const auto *id = jsp->data.str.buff; + if (auto *params = LookupByName(id, available_inputs)) { + input.yardforce.button = params->button; + input.yardforce.id_or_bit = params->id_or_bit; + return true; + } + ULOG_ERROR("Unknown ID \"%s\"", id); + return false; } ULOG_ERROR("Unknown attribute \"%s\"", key); return false; diff --git a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp index bc738d59..56c5811e 100644 --- a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp +++ b/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp @@ -208,7 +208,7 @@ void YardForceCoverUIDriver::ProcessPacket() { } else if (encode_decode_buf_[0] == Get_Button && size == sizeof(struct msg_event_button)) { msg_event_button *msg = (struct msg_event_button *)encode_decode_buf_; for (auto &input : input_driver_.Inputs()) { - if (input.yardforce.type == Input::Type::BUTTON && input.yardforce.button.id == msg->button_id) { + if (input.yardforce.button && input.yardforce.id_or_bit == msg->button_id) { const bool long_press = msg->press_duration >= 1; input.InjectPress(long_press); break; @@ -217,8 +217,8 @@ void YardForceCoverUIDriver::ProcessPacket() { } else if (encode_decode_buf_[0] == Get_Emergency && size == sizeof(struct msg_event_emergency)) { msg_event_emergency *msg = (struct msg_event_emergency *)encode_decode_buf_; for (auto &input : input_driver_.Inputs()) { - if (input.yardforce.type == Input::Type::HALL) { - input.Update(IS_BIT_SET(msg->state, input.yardforce.hall.bit)); + if (!input.yardforce.button) { + input.Update(IS_BIT_SET(msg->state, input.yardforce.id_or_bit)); } } } /* else if (encode_decode_buf_[0] == Get_Rain && size == sizeof(struct msg_event_rain)) { From b64bea7463eb4bfd5732a7c0e29b825df74cfb3a Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Mon, 16 Jun 2025 21:02:58 +0000 Subject: [PATCH 10/10] yard_force -> yardforce --- CMakeLists.txt | 4 ++-- robots/include/yardforce_robot.hpp | 2 +- ...yard_force_input_driver.cpp => yardforce_input_driver.cpp} | 2 +- ...yard_force_input_driver.hpp => yardforce_input_driver.hpp} | 0 ...orce_cover_ui_driver.cpp => yardforce_cover_ui_driver.cpp} | 2 +- ...orce_cover_ui_driver.hpp => yardforce_cover_ui_driver.hpp} | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename src/drivers/input/{yard_force_input_driver.cpp => yardforce_input_driver.cpp} (97%) rename src/drivers/input/{yard_force_input_driver.hpp => yardforce_input_driver.hpp} (100%) rename src/drivers/ui/YardForceCoverUI/{yard_force_cover_ui_driver.cpp => yardforce_cover_ui_driver.cpp} (99%) rename src/drivers/ui/YardForceCoverUI/{yard_force_cover_ui_driver.hpp => yardforce_cover_ui_driver.hpp} (95%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 855c5af4..244da901 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,10 +88,10 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/drivers/input/input_driver.cpp src/drivers/input/gpio_input_driver.cpp src/drivers/input/worx_input_driver.cpp - src/drivers/input/yard_force_input_driver.cpp + src/drivers/input/yardforce_input_driver.cpp $<$:src/drivers/input/simulated_input_driver.cpp> # YardForce Cover UI Driver - src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp + src/drivers/ui/YardForceCoverUI/yardforce_cover_ui_driver.cpp # Raw driver debug interface src/debug/debug_tcp_interface.cpp src/debug/debug_udp_interface.cpp diff --git a/robots/include/yardforce_robot.hpp b/robots/include/yardforce_robot.hpp index 23aadf4b..f26963cd 100644 --- a/robots/include/yardforce_robot.hpp +++ b/robots/include/yardforce_robot.hpp @@ -2,7 +2,7 @@ #define YARDFORCE_ROBOT_HPP #include -#include +#include #include "robot.hpp" diff --git a/src/drivers/input/yard_force_input_driver.cpp b/src/drivers/input/yardforce_input_driver.cpp similarity index 97% rename from src/drivers/input/yard_force_input_driver.cpp rename to src/drivers/input/yardforce_input_driver.cpp index 73b95677..62e195f2 100644 --- a/src/drivers/input/yard_force_input_driver.cpp +++ b/src/drivers/input/yardforce_input_driver.cpp @@ -1,4 +1,4 @@ -#include "yard_force_input_driver.hpp" +#include "yardforce_input_driver.hpp" #include diff --git a/src/drivers/input/yard_force_input_driver.hpp b/src/drivers/input/yardforce_input_driver.hpp similarity index 100% rename from src/drivers/input/yard_force_input_driver.hpp rename to src/drivers/input/yardforce_input_driver.hpp diff --git a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp b/src/drivers/ui/YardForceCoverUI/yardforce_cover_ui_driver.cpp similarity index 99% rename from src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp rename to src/drivers/ui/YardForceCoverUI/yardforce_cover_ui_driver.cpp index 56c5811e..450a8957 100644 --- a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.cpp +++ b/src/drivers/ui/YardForceCoverUI/yardforce_cover_ui_driver.cpp @@ -1,4 +1,4 @@ -#include "yard_force_cover_ui_driver.hpp" +#include "yardforce_cover_ui_driver.hpp" #include #include diff --git a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp b/src/drivers/ui/YardForceCoverUI/yardforce_cover_ui_driver.hpp similarity index 95% rename from src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp rename to src/drivers/ui/YardForceCoverUI/yardforce_cover_ui_driver.hpp index 626d7304..7e7b1924 100644 --- a/src/drivers/ui/YardForceCoverUI/yard_force_cover_ui_driver.hpp +++ b/src/drivers/ui/YardForceCoverUI/yardforce_cover_ui_driver.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include "ch.h" #include "hal.h"