diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8d793d0..aa9c5153 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,28 @@ jobs: run: | ./.github/scripts/get_firmware_name.sh ${{ matrix.target }} ${{ matrix.serial_plotter_flag }} ${{ matrix.battery_flag }} ${{ matrix.nimble_flag }} + - name: Enable coverage (non-macOS) + if: runner.os != 'macOS' && matrix.coverage + run: | + sed -i '/__OH_FIRMWARE__/p; s/-D __OH_FIRMWARE__/-lgcov --coverage/' platformio.ini + - name: Enable coverage (macOS) + if: runner.os == 'macOS' && matrix.coverage + run: | + sed -i '' '/__OH_FIRMWARE__/p; s/-D __OH_FIRMWARE__/-lgcov --coverage/' platformio.ini + + - name: Update build flags (non-macOS) + if: runner.os != 'macOS' + run: | + sed -i '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.battery_flag }}/' platformio.ini + sed -i '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.serial_plotter_flag }}/' platformio.ini + sed -i '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.nimble_flag }}/' platformio.ini + - name: Update build flags (macOS) + if: runner.os == 'macOS' + run: | + sed -i '' '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.battery_flag }}/' platformio.ini + sed -i '' '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.serial_plotter_flag }}/' platformio.ini + sed -i '' '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.nimble_flag }}/' platformio.ini + - name: Speedup package installation if: matrix.coverage uses: abbbi/github-actions-tune@v1 @@ -88,7 +110,7 @@ jobs: path: | ~/.platformio/.cache ./.pio - key: ${{ runner.os }}-pio-${{ matrix.target }}-${{ hashFiles('platformio.ini') }} + key: ${{ runner.os }}-pio-${{ matrix.target }}-${{ hashFiles('**/*.ini') }} restore-keys: | ${{ runner.os }}-pio-${{ matrix.target }}- ${{ runner.os }}-pio- @@ -98,28 +120,6 @@ jobs: with: python-version: '3.9' - - name: Enable coverage (non-macOS) - if: runner.os != 'macOS' && matrix.coverage - run: | - sed -i '/__OH_FIRMWARE__/p; s/-D __OH_FIRMWARE__/-lgcov --coverage/' platformio.ini - - name: Enable coverage (macOS) - if: runner.os == 'macOS' && matrix.coverage - run: | - sed -i '' '/__OH_FIRMWARE__/p; s/-D __OH_FIRMWARE__/-lgcov --coverage/' platformio.ini - - - name: Update build flags (non-macOS) - if: runner.os != 'macOS' - run: | - sed -i '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.battery_flag }}/' platformio.ini - sed -i '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.serial_plotter_flag }}/' platformio.ini - sed -i '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.nimble_flag }}/' platformio.ini - - name: Update build flags (macOS) - if: runner.os == 'macOS' - run: | - sed -i '' '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.battery_flag }}/' platformio.ini - sed -i '' '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.serial_plotter_flag }}/' platformio.ini - sed -i '' '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.nimble_flag }}/' platformio.ini - - name: Install PlatformIO run: | python -m pip install --upgrade pip @@ -165,31 +165,31 @@ jobs: target: - lucidgloves-prototype3 - lucidgloves-prototype4 + - indexer-c + - indexer-cf + - indexer-cs + - indexer-csf comm_flag: - - COMMUNICATION_PROTOCOL=COMMUNICATION_PROTOCOL_SERIAL - - COMMUNICATION_PROTOCOL=COMMUNICATION_PROTOCOL_BLE + - COMMUNICATION_PROTOCOL=OPENGLOVES_COMM_SERIAL + - COMMUNICATION_PROTOCOL=OPENGLOVES_COMM_BTSERIAL coverage: [ false ] include: - os: ubuntu-latest - target: lucidgloves-prototype4 + target: lucidgloves-prototype4-ffb curl_calibration_flag: CALIBRATION_CURL="OH::MinMaxCalibrator" coverage: true - os: ubuntu-latest - target: lucidgloves-prototype4 + target: lucidgloves-prototype4-ffb curl_calibration_flag: CALIBRATION_CURL="OH::CenterPointDeviationCalibrator" coverage: true - os: ubuntu-latest - target: lucidgloves-prototype4 + target: lucidgloves-prototype4-ffb curl_calibration_flag: CALIBRATION_CURL="OH::FixedCenterPointDeviationCalibrator" coverage: true - os: ubuntu-latest - target: indexer-c - comm_flag: COMMUNICATION_PROTOCOL=COMMUNICATION_PROTOCOL_SERIAL - coverage: true - - os: ubuntu-latest - target: indexer-cs - comm_flag: COMMUNICATION_PROTOCOL=COMMUNICATION_PROTOCOL_SERIAL + target: indexer-csf + comm_flag: COMMUNICATION_PROTOCOL=OPENGLOVES_COMM_BTSERIAL coverage: true steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aab94612..65ceb54b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,8 +124,11 @@ jobs: target: - lucidgloves-prototype3 - lucidgloves-prototype4 + - lucidgloves-prototype4-ffb - indexer-c + - indexer-cf - indexer-cs + - indexer-csf comm_flag: - OPENGLOVES_COMMUNCATION=OPENGLOVES_COMM_SERIAL - OPENGLOVES_COMMUNCATION=OPENGLOVES_COMM_BTSERIAL @@ -137,6 +140,15 @@ jobs: run: | ./.github/scripts/get_firmware_name.sh ${{ matrix.target }} ${{ matrix.comm_flag }} + - name: Update build command (non-Windows) + run: | + sed -i '/\[env\]/p; s/\[env\]/upload_protocol = custom/' platformio.ini + sed -i '/\[env\]/p; s/\[env\]/upload_command = \$PYTHONEXE .\/scripts\/ci\/create-archive.py \$FLASH_EXTRA_IMAGES \$ESP32_APP_OFFSET \$SOURCE/' platformio.ini + + - name: Update build flags (non-macOS) + run: | + sed -i '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.comm_flag }}/' platformio.ini + - name: Cache pip uses: actions/cache@v3 with: @@ -168,15 +180,6 @@ jobs: pio upgrade --dev pio pkg update --global - - name: Update build command (non-Windows) - run: | - sed -i '/\[env\]/p; s/\[env\]/upload_protocol = custom/' platformio.ini - sed -i '/\[env\]/p; s/\[env\]/upload_command = \$PYTHONEXE .\/scripts\/ci\/create-archive.py \$FLASH_EXTRA_IMAGES \$ESP32_APP_OFFSET \$SOURCE/' platformio.ini - - - name: Update build flags (non-macOS) - run: | - sed -i '/__OH_FIRMWARE__/p; s/__OH_FIRMWARE__/${{ matrix.comm_flag }}/' platformio.ini - - name: Build run: | mkdir build diff --git a/.vscode/settings.json b/.vscode/settings.json index 85901741..56a04ab3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,7 +58,8 @@ "xmemory": "cpp", "xtree": "cpp", "xhash": "cpp", - "xlocnum": "cpp" + "xlocnum": "cpp", + "ledc.h": "c" }, "files.eol": "\n", "files.insertFinalNewline": true, diff --git a/firmware/mode_configs/opengloves/opengloves.cpp b/firmware/mode_configs/opengloves/opengloves.cpp index 3c83fbd1..d575a4cb 100644 --- a/firmware/mode_configs/opengloves/opengloves.cpp +++ b/firmware/mode_configs/opengloves/opengloves.cpp @@ -1,7 +1,8 @@ #include #include -#include +#include #include +#include #include #include #include @@ -122,6 +123,15 @@ #define UPDATE_RATE 90 #endif +#define FFB_THUMB_ENABLED (defined(PIN_FFB_THUMB) && (PIN_FFB_THUMB != -1)) +#define FFB_INDEX_ENABLED (defined(PIN_FFB_INDEX) && (PIN_FFB_INDEX != -1)) +#define FFB_MIDDLE_ENABLED (defined(PIN_FFB_MIDDLE) && (PIN_FFB_MIDDLE != -1)) +#define FFB_RING_ENABLED (defined(PIN_FFB_RING) && (PIN_FFB_RING != -1)) +#define FFB_PINKY_ENABLED (defined(PIN_FFB_PINKY) && (PIN_FFB_PINKY != -1)) + +#define FFB_ENABLED \ + (FFB_THUMB_ENABLED || FFB_INDEX_ENABLED || FFB_MIDDLE_ENABLED || FFB_RING_ENABLED || FFB_PINKY_ENABLED) + using namespace OpenGloves; HandSensors handSensors = { @@ -268,9 +278,35 @@ std::vector*> joysticks = { std::vector otherSensors = std::vector(); -OpenGlovesConfig config = OpenGlovesConfig(UPDATE_RATE, CALIBRATION_DURATION, CALIBRATION_ALWAYS_CALIBRATE); +OpenGlovesTrackingTaskConfig config = + OpenGlovesTrackingTaskConfig(UPDATE_RATE, CALIBRATION_DURATION, CALIBRATION_ALWAYS_CALIBRATE); OpenGlovesTrackingTask* trackingTask; +#if FFB_ENABLED +HandActuators handActuators = { +#if FFB_THUMB_ENABLED + .thumb = new OH::ServoActuator(PIN_FFB_THUMB), +#endif + +#if FFB_INDEX_ENABLED + .index = new OH::ServoActuator(PIN_FFB_INDEX), +#endif + +#if FFB_MIDDLE_ENABLED + .middle = new OH::ServoActuator(PIN_FFB_MIDDLE), +#endif + +#if FFB_RING_ENABLED + .ring = new OH::ServoActuator(PIN_FFB_RING), +#endif + +#if FFB_PINKY_ENABLED + .pinky = new OH::ServoActuator(PIN_FFB_PINKY), +#endif +}; +OpenGlovesForceFeedbackTask* ffbTask; +#endif + void setupMode() { #if OPENGLOVES_COMMUNCATION == OPENGLOVES_COMM_SERIAL @@ -302,6 +338,21 @@ void setupMode() .priority = OPENGLOVES_FINGERS_TASK_PRIORITY, } ); + +#if FFB_ENABLED + ffbTask = new OpenGlovesForceFeedbackTask( + *communication, + handActuators, + UPDATE_RATE, + { + .name = "OpenGlovesForceFeedbackTask", + .stackDepth = 8192, + .priority = OPENGLOVES_FINGERS_TASK_PRIORITY, + } + ); + ffbTask->begin(); +#endif + trackingTask->begin(); } diff --git a/ini/opengloves.ini b/ini/opengloves.ini index 2a5e761b..15732211 100644 --- a/ini/opengloves.ini +++ b/ini/opengloves.ini @@ -132,6 +132,50 @@ build_src_filter = ${opengloves.build_src_filter} + lib_deps = ${opengloves.lib_deps} +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; LucidGloves Prototype 4 + Force Feedback +; Wiring Diagram: https://github.com/LucidVR/lucidgloves/wiki/Prototype-4-Wiring-Diagram +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +[env:lucidgloves-prototype4-ffb] +platform = ${opengloves.platform} +platform_packages = ${opengloves.platform_packages} +framework = ${opengloves.framework} +board = ${opengloves.board} +upload_speed = ${opengloves.upload_speed} +monitor_speed = ${opengloves.monitor_speed} + +build_flags = ${opengloves.build_flags} + ; Pins configuration + ; Comment out to disable + -D PIN_FINGER_THUMB=32 + -D PIN_FINGER_INDEX=35 + -D PIN_FINGER_MIDDLE=34 + -D PIN_FINGER_RING=39 + -D PIN_FINGER_PINKY=36 + + -D PIN_JOYSTICK_X=33 + -D PIN_JOYSTICK_Y=25 + -D PIN_BUTTON_JOYSTICK=26 + + -D PIN_BUTTON_A=27 + -D PIN_BUTTON_B=14 + ; -D PIN_BUTTON_MENU=27 + -D PIN_BUTTON_CALIBRATE=12 + ; -D PIN_BUTTON_TRIGGER=12 ; unused if GESTURE_TRIGGER is true + ; -D PIN_BUTTON_GRAB=13 ; unused if GESTURE_GRAB is true + ; -D PIN_BUTTON_PINCH=23 ; unused if GESTURE_PINCH is true + + -D PIN_FFB_THUMB=17 + -D PIN_FFB_INDEX=21 + -D PIN_FFB_MIDDLE=19 + -D PIN_FFB_RING=18 + -D PIN_FFB_PINKY=5 + +build_unflags = ${opengloves.build_unflags} +build_src_filter = ${opengloves.build_src_filter} + + +lib_deps = ${opengloves.lib_deps} + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Indexer C ; Wiring Diagram: https://github.com/Valsvirtuals/Indexer/wiki/wiring-and-pinout @@ -170,6 +214,50 @@ build_src_filter = ${opengloves.build_src_filter} + lib_deps = ${opengloves.lib_deps} +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Indexer CF +; Wiring Diagram: https://github.com/Valsvirtuals/Indexer/wiki/wiring-and-pinout +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +[env:indexer-cf] +platform = ${opengloves.platform} +platform_packages = ${opengloves.platform_packages} +framework = ${opengloves.framework} +board = wemos_d1_mini32 +upload_speed = ${opengloves.upload_speed} +monitor_speed = ${opengloves.monitor_speed} + +build_flags = ${opengloves.build_flags} + ; Pins configuration + ; Comment out to disable + -D PIN_FINGER_THUMB=25 + -D PIN_FINGER_INDEX=14 + -D PIN_FINGER_MIDDLE=33 + -D PIN_FINGER_RING=39 + -D PIN_FINGER_PINKY=36 + + -D PIN_JOYSTICK_X=12 + -D PIN_JOYSTICK_Y=4 + -D PIN_BUTTON_JOYSTICK=0 + + -D PIN_BUTTON_A=2 + -D PIN_BUTTON_B=11 + ; -D PIN_BUTTON_MENU=5 + -D PIN_BUTTON_CALIBRATE=27 + ; -D PIN_BUTTON_TRIGGER=19 ; unused if GESTURE_TRIGGER is true + ; -D PIN_BUTTON_GRAB=18 ; unused if GESTURE_GRAB is true + ; -D PIN_BUTTON_PINCH=23 ; unused if GESTURE_PINCH is true + + -D PIN_FFB_THUMB=16 + -D PIN_FFB_INDEX=17 + -D PIN_FFB_MIDDLE=21 + -D PIN_FFB_RING=22 + -D PIN_FFB_PINKY=1 + +build_unflags = ${opengloves.build_unflags} +build_src_filter = ${opengloves.build_src_filter} + + +lib_deps = ${opengloves.lib_deps} + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Indexer CS ; Wiring Diagram: https://github.com/Valsvirtuals/Indexer/wiki/wiring-and-pinout @@ -213,3 +301,53 @@ build_unflags = ${opengloves.build_unflags} build_src_filter = ${opengloves.build_src_filter} + lib_deps = ${opengloves.lib_deps} + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Indexer CSF +; Wiring Diagram: https://github.com/Valsvirtuals/Indexer/wiki/wiring-and-pinout +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +[env:indexer-csf] +platform = ${opengloves.platform} +platform_packages = ${opengloves.platform_packages} +framework = ${opengloves.framework} +board = wemos_d1_mini32 +upload_speed = ${opengloves.upload_speed} +monitor_speed = ${opengloves.monitor_speed} + +build_flags = ${opengloves.build_flags} + ; Pins configuration + ; Comment out to disable + -D PIN_FINGER_THUMB=25 + -D PIN_FINGER_INDEX=14 + -D PIN_FINGER_MIDDLE=33 + -D PIN_FINGER_RING=39 + -D PIN_FINGER_PINKY=36 + + -D PIN_FINGER_THUMB_SPLAY=32 + -D PIN_FINGER_INDEX_SPLAY=13 + -D PIN_FINGER_MIDDLE_SPLAY=34 + -D PIN_FINGER_RING_SPLAY=35 + -D PIN_FINGER_PINKY_SPLAY=26 + + -D PIN_JOYSTICK_X=12 + -D PIN_JOYSTICK_Y=4 + -D PIN_BUTTON_JOYSTICK=0 + + -D PIN_BUTTON_A=2 + -D PIN_BUTTON_B=11 + ; -D PIN_BUTTON_MENU=5 + -D PIN_BUTTON_CALIBRATE=27 + ; -D PIN_BUTTON_TRIGGER=19 ; unused if GESTURE_TRIGGER is true + ; -D PIN_BUTTON_GRAB=18 ; unused if GESTURE_GRAB is true + ; -D PIN_BUTTON_PINCH=23 ; unused if GESTURE_PINCH is true + + -D PIN_FFB_THUMB=16 + -D PIN_FFB_INDEX=17 + -D PIN_FFB_MIDDLE=21 + -D PIN_FFB_RING=22 + -D PIN_FFB_PINKY=1 + +build_unflags = ${opengloves.build_unflags} +build_src_filter = ${opengloves.build_src_filter} + + +lib_deps = ${opengloves.lib_deps} diff --git a/lib/arduino/output_writers/pwm.cpp b/lib/arduino/output_writers/pwm.cpp index ad478f8a..344cce1f 100644 --- a/lib/arduino/output_writers/pwm.cpp +++ b/lib/arduino/output_writers/pwm.cpp @@ -21,7 +21,7 @@ namespace OH { void PWMOutputWriter::writeOutput(oh_output_intensity_t intensity) { #if defined(ESP32) - ledcWrite(chan, simpleMap(intensity, OH_OUTPUT_INTENSITY_MAX, 4096)); + ledcWrite(chan, simpleMap(intensity, OH_OUTPUT_INTENSITY_MAX, (1 << this->resolution) - 1)); #else analogWrite(this->pin, simpleMap(intensity, OH_OUTPUT_INTENSITY_MAX, 255)); #endif diff --git a/lib/arduino/output_writers/servo.hpp b/lib/arduino/output_writers/servo.hpp new file mode 100644 index 00000000..6223d265 --- /dev/null +++ b/lib/arduino/output_writers/servo.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include + +namespace OH { + class ServoActuator : public AbstractActuator { + public: + ServoActuator(const uint8_t pin, const uint16_t min = 500, const uint16_t max = 2400) : + pin(pin), min(min), max(max){}; + + void setup() override + { + servo.attach(this->pin, this->min, this->max); + this->writeOutput(0); + }; + + void writeOutput(oh_output_intensity_t intensity) override + { + servo.writeMicroseconds(accurateMap(intensity, 0, OH_OUTPUT_INTENSITY_MAX, this->min, this->max)); + }; + + protected: + Servo servo = Servo(); + uint8_t pin; + uint16_t min, max; + }; +} // namespace OH diff --git a/lib/core/utility.hpp b/lib/core/utility.hpp index 3d40497e..7a60c5b1 100644 --- a/lib/core/utility.hpp +++ b/lib/core/utility.hpp @@ -30,7 +30,7 @@ namespace OH { }; template - constexpr _Tp accurateMap(_Tp x, _Tp in_min, _Tp in_max, _Tp out_min, _Tp out_max) + inline _Tp accurateMap(_Tp x, _Tp in_min, _Tp in_max, _Tp out_min, _Tp out_max) { const _Tp run = in_max - in_min; if (run == 0) { diff --git a/lib/opengloves/og_alpha_encoding.cpp b/lib/opengloves/og_alpha_encoding.cpp new file mode 100644 index 00000000..ed30cbf4 --- /dev/null +++ b/lib/opengloves/og_alpha_encoding.cpp @@ -0,0 +1,57 @@ +#include "og_alpha_encoding.hpp" + +namespace OpenGloves { + std::map AlphaEncodingService::splitCommands(std::string input_string) + { + std::map commands; + + size_t start = 0; // Start of the current command + for (size_t i = 0; i < input_string.size(); i++) { + char ch = input_string[i]; + + // Start a new command if the character is non-numeric or an opening parenthesis + // and previous character is a numeric character + if ((!(isdigit(ch)) || ch == '(') && i > 0 && isdigit(input_string[i - 1])) { + AlphaEncodingService::splitCommand(input_string, start, i, commands); + start = i; + } + } + + AlphaEncodingService::splitCommand(input_string, start, input_string.size(), commands); + + return commands; + } + + void AlphaEncodingService::splitCommand( + const std::string& input_string, size_t start, size_t end, std::map& commands + ) + { + std::string current_command = input_string.substr(start, end - start); + + if (current_command.empty()) { + return; + } + + // Split the command into prefix and number + size_t split_index = current_command.find_last_not_of(valueSymbols) + 1; + + if (split_index >= current_command.size()) { + log_w("Invalid command: %s", current_command.c_str()); + return; + } + + std::string prefix = current_command.substr(0, split_index); + int number = std::stoi(current_command.substr(split_index)); + + // Check if the command prefix is in commandMap + auto it = commandMap.find(prefix); + if (it == commandMap.end()) { + log_w("Unknown command prefix: %s", prefix.c_str()); + return; + } + + // Look up the Command enum value for the prefix in the commandMap + Command command = it->second; + commands[command] = number; + } +} // namespace OpenGloves diff --git a/lib/opengloves/og_alpha_encoding.hpp b/lib/opengloves/og_alpha_encoding.hpp new file mode 100644 index 00000000..acc3744f --- /dev/null +++ b/lib/opengloves/og_alpha_encoding.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace OpenGloves { + class AlphaEncodingService { + public: + inline static const std::string valueSymbols = "0123456789"; + inline static const std::map commandMap = { + // clang-format off + { "A", Command::ThumbCurl }, + { "(AB)", Command::ThumbSplay }, + { "B", Command::IndexCurl }, + { "(BB)", Command::IndexSplay }, + { "C", Command::MiddleCurl }, + { "(CB)", Command::MiddleSplay }, + { "D", Command::RingCurl }, + { "(DB)", Command::RingSplay }, + { "E", Command::PinkyCurl }, + { "(EB)", Command::PinkySplay }, + // clang-format on + }; + + AlphaEncodingService() = default; + + static std::map splitCommands(std::string input_string); + + private: + CommandCallback callback = nullptr; + + static void splitCommand( + const std::string& input_string, size_t start, size_t end, std::map& commands + ); + }; +} // namespace OpenGloves diff --git a/lib/opengloves/og_ffb.hpp b/lib/opengloves/og_ffb.hpp new file mode 100644 index 00000000..c1a8b42a --- /dev/null +++ b/lib/opengloves/og_ffb.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include + +namespace OpenGloves { + struct HandActuators { + std::optional thumb = std::nullopt; + std::optional index = std::nullopt; + std::optional middle = std::nullopt; + std::optional ring = std::nullopt; + std::optional pinky = std::nullopt; + }; +} // namespace OpenGloves diff --git a/lib/opengloves/og_protocol.hpp b/lib/opengloves/og_protocol.hpp index 1f11fae2..3d601656 100644 --- a/lib/opengloves/og_protocol.hpp +++ b/lib/opengloves/og_protocol.hpp @@ -52,5 +52,26 @@ namespace OpenGloves { public: virtual void setup() = 0; virtual void send(std::vector& sensors) = 0; + virtual bool hasData() = 0; + virtual size_t readCommand(char* buffer, size_t length) = 0; }; + + typedef enum Command { + ThumbCurl, + ThumbSplay, + + IndexCurl, + IndexSplay, + + MiddleCurl, + MiddleSplay, + + RingCurl, + RingSplay, + + PinkyCurl, + PinkySplay, + } Command; + + typedef std::function CommandCallback; } // namespace OpenGloves diff --git a/lib/opengloves_serial/og_serial_commmunications.hpp b/lib/opengloves_serial/og_serial_communication.hpp similarity index 63% rename from lib/opengloves_serial/og_serial_commmunications.hpp rename to lib/opengloves_serial/og_serial_communication.hpp index 42543a53..45d89a52 100644 --- a/lib/opengloves_serial/og_serial_commmunications.hpp +++ b/lib/opengloves_serial/og_serial_communication.hpp @@ -46,23 +46,50 @@ namespace OpenGloves { return offset; } + + virtual bool hasData() = 0; + + virtual size_t readCommand(char* buffer, size_t length) = 0; }; class SerialCommunication : public ISerialCommunication { private: unsigned long baud; + bool ready = false; public: SerialCommunication(HardwareSerial& channel, unsigned long baud) : baud(baud), ISerialCommunication(channel){}; + void setup() override + { + if (this->ready) { + return; + } + + static_cast(this->channel).begin(this->baud); + this->ready = true; + } + bool isReady() override { - return true; + return this->ready; } - void setup() override + bool hasData() override { - static_cast(this->channel).begin(this->baud); + return this->isReady() && static_cast(this->channel).available() > 0; + } + + size_t readCommand(char* buffer, size_t length) override + { + if (!this->hasData()) { + return false; + } + + size_t bytesRead = static_cast(this->channel).readBytesUntil('\n', buffer, length); + buffer[bytesRead] = '\0'; + + return bytesRead; } }; @@ -73,19 +100,41 @@ namespace OpenGloves { public: BTSerialCommunication(BluetoothSerial& channel, std::string name) : ISerialCommunication(channel), name(name){}; - bool isReady() override - { - return static_cast(this->channel).hasClient(); - } - void setup() override { auto& serial = static_cast(this->channel); + if (serial.isReady()) { + return; + } + // serial.register_callback([](esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { // log_i("Bluetooth event: %d", event); // }); // serial.setTimeout(4); serial.begin(name.c_str()); } + + bool isReady() override + { + auto& serial = static_cast(this->channel); + return serial.isReady() && serial.hasClient(); + } + + bool hasData() override + { + return this->isReady() && static_cast(this->channel).available() > 0; + } + + size_t readCommand(char* buffer, size_t length) override + { + if (!this->hasData()) { + return false; + } + + size_t bytesRead = static_cast(this->channel).readBytesUntil('\n', buffer, length); + buffer[bytesRead] = '\0'; + + return bytesRead; + } }; } // namespace OpenGloves diff --git a/lib/opengloves_task/opengloves_task.hpp b/lib/opengloves_task/opengloves_task.hpp index 22282ecc..d52f5cd4 100644 --- a/lib/opengloves_task/opengloves_task.hpp +++ b/lib/opengloves_task/opengloves_task.hpp @@ -5,7 +5,9 @@ #include #include -#include +#include +#include +#include #include #include #include @@ -16,8 +18,8 @@ #include namespace OpenGloves { - struct OpenGlovesConfig { - size_t updateInterval; + struct OpenGlovesTrackingTaskConfig { + size_t updateIntervalMs; size_t calibrationDuration; bool alwaysCalibrate; @@ -26,11 +28,11 @@ namespace OpenGloves { * @param calibrationDuration The duration in milliseconds that the * calibration button should be held for. */ - OpenGlovesConfig(size_t updateRate, size_t calibrationDuration, bool alwaysCalibrate) : + OpenGlovesTrackingTaskConfig(size_t updateRate, size_t calibrationDuration, bool alwaysCalibrate) : calibrationDuration(calibrationDuration), alwaysCalibrate(alwaysCalibrate) { // Convert the update rate to an interval in milliseconds. - this->updateInterval = 1000 / updateRate; + this->updateIntervalMs = 1000 / updateRate; } }; @@ -49,7 +51,7 @@ namespace OpenGloves { * @param taskConfig The task configuration. */ OpenGlovesTrackingTask( - OpenGlovesConfig& config, + OpenGlovesTrackingTaskConfig& config, ICommunication& communication, HandSensors& fingers, std::vector*>& buttons, @@ -128,7 +130,7 @@ namespace OpenGloves { }; private: - OpenGlovesConfig& config; + OpenGlovesTrackingTaskConfig& config; HandSensors& fingers; ICommunication& communication; @@ -205,10 +207,125 @@ namespace OpenGloves { // Delay until the next update. auto elapsed = millis() - now; - if (elapsed < this->config.updateInterval) { - delay(this->config.updateInterval - elapsed); + if (elapsed < this->config.updateIntervalMs) { + delay(this->config.updateIntervalMs - elapsed); } } }; }; + + class OpenGlovesForceFeedbackTask : public OH::Task { + friend class OH::Task; + + public: + OpenGlovesForceFeedbackTask( + ICommunication& communication, HandActuators& actuators, size_t updateRate, OH::TaskConfig taskConfig + ) : + communication(communication), actuators(actuators), OH::Task(taskConfig) + { + this->updateIntervalMs = 1000 / updateRate; + }; + + void begin() override + { + log_d("Starting OpenGloves force feedback task: %p", this); + this->setup(); + this->OH::Task::begin(); + }; + + private: + ICommunication& communication; + HandActuators& actuators; + size_t updateIntervalMs; + + void setup() + { + log_d("Setting up OpenGloves force feedback task: %p", this); + this->communication.setup(); + + if (this->actuators.thumb.has_value()) { + this->actuators.thumb.value()->setup(); + } + + if (this->actuators.index.has_value()) { + this->actuators.index.value()->setup(); + } + + if (this->actuators.middle.has_value()) { + this->actuators.middle.value()->setup(); + } + + if (this->actuators.ring.has_value()) { + this->actuators.ring.value()->setup(); + } + + if (this->actuators.pinky.has_value()) { + this->actuators.pinky.value()->setup(); + } + } + + void run() + { + char commandBuffer[256]; + std::string command; + while (true) { + auto now = millis(); + + if (this->communication.hasData()) { + auto bytesRead = this->communication.readCommand(commandBuffer, sizeof(commandBuffer)); + if (bytesRead == 0) { + continue; + } + + command.assign(commandBuffer, bytesRead); + + auto commands = AlphaEncodingService::splitCommands(command); + + for (auto& [command, value] : commands) { + this->handleCommand(command, value); + } + } + + // Delay until the next update. + auto elapsed = millis() - now; + if (elapsed < this->updateIntervalMs) { + delay(this->updateIntervalMs - elapsed); + } + } + } + + void handleCommand(Command command, uint16_t value) + { + switch (command) { + case Command::ThumbCurl: + if (this->actuators.thumb.has_value()) { + this->actuators.thumb.value()->writeOutput(value); + } + break; + case Command::IndexCurl: + if (this->actuators.index.has_value()) { + this->actuators.index.value()->writeOutput(value); + } + break; + case Command::MiddleCurl: + if (this->actuators.middle.has_value()) { + this->actuators.middle.value()->writeOutput(value); + } + break; + case Command::RingCurl: + if (this->actuators.ring.has_value()) { + this->actuators.ring.value()->writeOutput(value); + } + break; + case Command::PinkyCurl: + if (this->actuators.pinky.has_value()) { + this->actuators.pinky.value()->writeOutput(value); + } + break; + default: + log_w("Unhandled command: %d", command); + break; + } + } + }; } // namespace OpenGloves diff --git a/lib/output/abstract_actuator.hpp b/lib/output/abstract_actuator.hpp index b7621503..50df1752 100644 --- a/lib/output/abstract_actuator.hpp +++ b/lib/output/abstract_actuator.hpp @@ -2,8 +2,8 @@ #include -#define OH_OUTPUT_INTENSITY_T uint8_t -#define OH_OUTPUT_INTENSITY_MAX UINT8_MAX +#define OH_OUTPUT_INTENSITY_T uint16_t +#define OH_OUTPUT_INTENSITY_MAX 4095 typedef OH_OUTPUT_INTENSITY_T oh_output_intensity_t; diff --git a/platformio.ini b/platformio.ini index f1306061..e6e18eec 100644 --- a/platformio.ini +++ b/platformio.ini @@ -41,6 +41,7 @@ lib_deps = adafruit/Adafruit INA219@^1.2.1 sparkfun/SparkFun MAX1704x Fuel Gauge Arduino Library@^1.0.4 h2zero/NimBLE-Arduino@^1.4.0 + madhephaestus/ESP32Servo @ ^0.13.0 [env] build_flags = ${common.build_flags} diff --git a/test/test_bhaptics/main.cpp b/test/test_bhaptics/main.cpp index cbe8669e..1725cca1 100644 --- a/test/test_bhaptics/main.cpp +++ b/test/test_bhaptics/main.cpp @@ -73,22 +73,22 @@ void test_layout_tactsuitx16(void) vestX16OutputTransformer(body, value, bhLayout, bhLayoutSize, layoutGroups, layoutGroupsSize); TEST_ASSERT_EQUAL_INT(0, actuator0->intensity); - TEST_ASSERT_EQUAL_INT(17, actuator1->intensity); - TEST_ASSERT_EQUAL_INT(204, actuator2->intensity); - TEST_ASSERT_EQUAL_INT(221, actuator3->intensity); - TEST_ASSERT_EQUAL_INT(34, actuator4->intensity); - TEST_ASSERT_EQUAL_INT(51, actuator5->intensity); - TEST_ASSERT_EQUAL_INT(238, actuator6->intensity); - TEST_ASSERT_EQUAL_INT(255, actuator7->intensity); - - TEST_ASSERT_EQUAL_INT(68, actuator8->intensity); - TEST_ASSERT_EQUAL_INT(85, actuator9->intensity); - TEST_ASSERT_EQUAL_INT(136, actuator10->intensity); - TEST_ASSERT_EQUAL_INT(153, actuator11->intensity); - TEST_ASSERT_EQUAL_INT(102, actuator12->intensity); - TEST_ASSERT_EQUAL_INT(119, actuator13->intensity); - TEST_ASSERT_EQUAL_INT(170, actuator14->intensity); - TEST_ASSERT_EQUAL_INT(187, actuator15->intensity); + TEST_ASSERT_EQUAL_INT(273, actuator1->intensity); + TEST_ASSERT_EQUAL_INT(3276, actuator2->intensity); + TEST_ASSERT_EQUAL_INT(3549, actuator3->intensity); + TEST_ASSERT_EQUAL_INT(546, actuator4->intensity); + TEST_ASSERT_EQUAL_INT(819, actuator5->intensity); + TEST_ASSERT_EQUAL_INT(3822, actuator6->intensity); + TEST_ASSERT_EQUAL_INT(4095, actuator7->intensity); + + TEST_ASSERT_EQUAL_INT(1092, actuator8->intensity); + TEST_ASSERT_EQUAL_INT(1365, actuator9->intensity); + TEST_ASSERT_EQUAL_INT(2184, actuator10->intensity); + TEST_ASSERT_EQUAL_INT(2457, actuator11->intensity); + TEST_ASSERT_EQUAL_INT(1638, actuator12->intensity); + TEST_ASSERT_EQUAL_INT(1911, actuator13->intensity); + TEST_ASSERT_EQUAL_INT(2730, actuator14->intensity); + TEST_ASSERT_EQUAL_INT(3003, actuator15->intensity); } void test_layout_tactsuitx40(void) @@ -131,36 +131,36 @@ void test_layout_tactsuitx40(void) vestOutputTransformer(body, value, bhLayout, bhLayoutSize); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[0][0])->intensity); - TEST_ASSERT_EQUAL_INT(17, static_cast(frontMatrix[0][1])->intensity); + TEST_ASSERT_EQUAL_INT(273, static_cast(frontMatrix[0][1])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[0][2])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[0][3])->intensity); - TEST_ASSERT_EQUAL_INT(34, static_cast(frontMatrix[1][0])->intensity); - TEST_ASSERT_EQUAL_INT(51, static_cast(frontMatrix[1][1])->intensity); + TEST_ASSERT_EQUAL_INT(546, static_cast(frontMatrix[1][0])->intensity); + TEST_ASSERT_EQUAL_INT(819, static_cast(frontMatrix[1][1])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[1][2])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[1][3])->intensity); - TEST_ASSERT_EQUAL_INT(68, static_cast(frontMatrix[2][0])->intensity); - TEST_ASSERT_EQUAL_INT(85, static_cast(frontMatrix[2][1])->intensity); + TEST_ASSERT_EQUAL_INT(1092, static_cast(frontMatrix[2][0])->intensity); + TEST_ASSERT_EQUAL_INT(1365, static_cast(frontMatrix[2][1])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[2][2])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[2][3])->intensity); - TEST_ASSERT_EQUAL_INT(102, static_cast(frontMatrix[3][0])->intensity); - TEST_ASSERT_EQUAL_INT(119, static_cast(frontMatrix[3][1])->intensity); + TEST_ASSERT_EQUAL_INT(1638, static_cast(frontMatrix[3][0])->intensity); + TEST_ASSERT_EQUAL_INT(1911, static_cast(frontMatrix[3][1])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[3][2])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[3][3])->intensity); - TEST_ASSERT_EQUAL_INT(136, static_cast(frontMatrix[4][0])->intensity); - TEST_ASSERT_EQUAL_INT(153, static_cast(frontMatrix[4][1])->intensity); + TEST_ASSERT_EQUAL_INT(2184, static_cast(frontMatrix[4][0])->intensity); + TEST_ASSERT_EQUAL_INT(2457, static_cast(frontMatrix[4][1])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[4][2])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(frontMatrix[4][3])->intensity); - TEST_ASSERT_EQUAL_INT(170, static_cast(backMatrix[0][0])->intensity); - TEST_ASSERT_EQUAL_INT(187, static_cast(backMatrix[0][1])->intensity); + TEST_ASSERT_EQUAL_INT(2730, static_cast(backMatrix[0][0])->intensity); + TEST_ASSERT_EQUAL_INT(3003, static_cast(backMatrix[0][1])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(backMatrix[0][2])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(backMatrix[0][3])->intensity); - TEST_ASSERT_EQUAL_INT(204, static_cast(backMatrix[1][0])->intensity); - TEST_ASSERT_EQUAL_INT(221, static_cast(backMatrix[1][1])->intensity); + TEST_ASSERT_EQUAL_INT(3276, static_cast(backMatrix[1][0])->intensity); + TEST_ASSERT_EQUAL_INT(3549, static_cast(backMatrix[1][1])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(backMatrix[1][2])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(backMatrix[1][3])->intensity); - TEST_ASSERT_EQUAL_INT(238, static_cast(backMatrix[2][0])->intensity); - TEST_ASSERT_EQUAL_INT(255, static_cast(backMatrix[2][1])->intensity); + TEST_ASSERT_EQUAL_INT(3822, static_cast(backMatrix[2][0])->intensity); + TEST_ASSERT_EQUAL_INT(4095, static_cast(backMatrix[2][1])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(backMatrix[2][2])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(backMatrix[2][3])->intensity); TEST_ASSERT_EQUAL_INT(0, static_cast(backMatrix[3][0])->intensity); @@ -199,7 +199,7 @@ void test_layout_tactal(void) std::string value = std::string((char*) values, sizeof(values)); plainOutputTransformer(body, value, bhLayout, bhLayoutSize, OUTPUT_PATH_ACCESSORY); - TEST_ASSERT_EQUAL_INT(255, actuator0->intensity); + TEST_ASSERT_EQUAL_INT(4095, actuator0->intensity); TEST_ASSERT_EQUAL_INT(0, actuator1->intensity); TEST_ASSERT_EQUAL_INT(0, actuator2->intensity); TEST_ASSERT_EQUAL_INT(0, actuator3->intensity); @@ -209,12 +209,12 @@ void test_layout_tactal(void) value = "\x10\x20\x30\x40\x50\x60"; plainOutputTransformer(body, value, bhLayout, bhLayoutSize, OUTPUT_PATH_ACCESSORY); - TEST_ASSERT_EQUAL_INT(40, actuator0->intensity); - TEST_ASSERT_EQUAL_INT(81, actuator1->intensity); - TEST_ASSERT_EQUAL_INT(122, actuator2->intensity); - TEST_ASSERT_EQUAL_INT(163, actuator3->intensity); - TEST_ASSERT_EQUAL_INT(204, actuator4->intensity); - TEST_ASSERT_EQUAL_INT(244, actuator5->intensity); + TEST_ASSERT_EQUAL_INT(655, actuator0->intensity); + TEST_ASSERT_EQUAL_INT(1310, actuator1->intensity); + TEST_ASSERT_EQUAL_INT(1965, actuator2->intensity); + TEST_ASSERT_EQUAL_INT(2620, actuator3->intensity); + TEST_ASSERT_EQUAL_INT(3276, actuator4->intensity); + TEST_ASSERT_EQUAL_INT(3931, actuator5->intensity); } void test_layout_tactosy2(void) @@ -244,7 +244,7 @@ void test_layout_tactosy2(void) std::string value = std::string((char*) values, sizeof(values)); plainOutputTransformer(body, value, bhLayout, bhLayoutSize, OUTPUT_PATH_ACCESSORY); - TEST_ASSERT_EQUAL_INT(255, actuator0->intensity); + TEST_ASSERT_EQUAL_INT(4095, actuator0->intensity); TEST_ASSERT_EQUAL_INT(0, actuator1->intensity); TEST_ASSERT_EQUAL_INT(0, actuator2->intensity); TEST_ASSERT_EQUAL_INT(0, actuator3->intensity); @@ -253,12 +253,12 @@ void test_layout_tactosy2(void) value = "\x10\x20\x30\x40\x50\x60"; plainOutputTransformer(body, value, bhLayout, bhLayoutSize, OUTPUT_PATH_ACCESSORY); - TEST_ASSERT_EQUAL_INT(40, actuator0->intensity); - TEST_ASSERT_EQUAL_INT(81, actuator1->intensity); - TEST_ASSERT_EQUAL_INT(122, actuator2->intensity); - TEST_ASSERT_EQUAL_INT(163, actuator3->intensity); - TEST_ASSERT_EQUAL_INT(204, actuator4->intensity); - TEST_ASSERT_EQUAL_INT(244, actuator5->intensity); + TEST_ASSERT_EQUAL_INT(655, actuator0->intensity); + TEST_ASSERT_EQUAL_INT(1310, actuator1->intensity); + TEST_ASSERT_EQUAL_INT(1965, actuator2->intensity); + TEST_ASSERT_EQUAL_INT(2620, actuator3->intensity); + TEST_ASSERT_EQUAL_INT(3276, actuator4->intensity); + TEST_ASSERT_EQUAL_INT(3931, actuator5->intensity); } void test_layout_tactosyh(void) @@ -286,15 +286,15 @@ void test_layout_tactosyh(void) std::string value = std::string((char*) values, sizeof(values)); plainOutputTransformer(body, value, bhLayout, bhLayoutSize, OUTPUT_PATH_ACCESSORY); - TEST_ASSERT_EQUAL_INT(255, actuator0->intensity); + TEST_ASSERT_EQUAL_INT(4095, actuator0->intensity); TEST_ASSERT_EQUAL_INT(0, actuator1->intensity); TEST_ASSERT_EQUAL_INT(0, actuator2->intensity); value = "\x10\x20\x30"; plainOutputTransformer(body, value, bhLayout, bhLayoutSize, OUTPUT_PATH_ACCESSORY); - TEST_ASSERT_EQUAL_INT(40, actuator0->intensity); - TEST_ASSERT_EQUAL_INT(81, actuator1->intensity); - TEST_ASSERT_EQUAL_INT(122, actuator2->intensity); + TEST_ASSERT_EQUAL_INT(655, actuator0->intensity); + TEST_ASSERT_EQUAL_INT(1310, actuator1->intensity); + TEST_ASSERT_EQUAL_INT(1965, actuator2->intensity); } void test_layout_tactosyf(void) @@ -322,15 +322,15 @@ void test_layout_tactosyf(void) std::string value = std::string((char*) values, sizeof(values)); plainOutputTransformer(body, value, bhLayout, bhLayoutSize, OUTPUT_PATH_ACCESSORY); - TEST_ASSERT_EQUAL_INT(255, actuator0->intensity); + TEST_ASSERT_EQUAL_INT(4095, actuator0->intensity); TEST_ASSERT_EQUAL_INT(0, actuator1->intensity); TEST_ASSERT_EQUAL_INT(0, actuator2->intensity); value = "\x10\x20\x30"; plainOutputTransformer(body, value, bhLayout, bhLayoutSize, OUTPUT_PATH_ACCESSORY); - TEST_ASSERT_EQUAL_INT(40, actuator0->intensity); - TEST_ASSERT_EQUAL_INT(81, actuator1->intensity); - TEST_ASSERT_EQUAL_INT(122, actuator2->intensity); + TEST_ASSERT_EQUAL_INT(655, actuator0->intensity); + TEST_ASSERT_EQUAL_INT(1310, actuator1->intensity); + TEST_ASSERT_EQUAL_INT(1965, actuator2->intensity); } int process(void) diff --git a/test/test_opengloves_alpha_encoding/main.cpp b/test/test_opengloves_alpha_encoding/main.cpp new file mode 100644 index 00000000..7f5f7200 --- /dev/null +++ b/test/test_opengloves_alpha_encoding/main.cpp @@ -0,0 +1,135 @@ +#include +#include + +using namespace OpenGloves; + +void testSplitCommands(void) +{ + std::map> input_strings = { + // curl only + { + "A2048\n\0", + { + { Command::ThumbCurl, 2048 }, + }, + }, + // curl only + { + "A100B200C300D400E500", + { + { Command::ThumbCurl, 100 }, + { Command::IndexCurl, 200 }, + { Command::MiddleCurl, 300 }, + { Command::RingCurl, 400 }, + { Command::PinkyCurl, 500 }, + }, + }, + // curl (unordered) + { + "E500A100B200D400C300", + { + { Command::ThumbCurl, 100 }, + { Command::IndexCurl, 200 }, + { Command::MiddleCurl, 300 }, + { Command::RingCurl, 400 }, + { Command::PinkyCurl, 500 }, + }, + }, + // curl (with invalid data) + { + "A100B200C300D400E", + { + { Command::ThumbCurl, 100 }, + { Command::IndexCurl, 200 }, + { Command::MiddleCurl, 300 }, + { Command::RingCurl, 400 }, + }, + }, + // curl (with unknown prefix) + { + "X100A100B200C300D400E500", + { + { Command::ThumbCurl, 100 }, + { Command::IndexCurl, 200 }, + { Command::MiddleCurl, 300 }, + { Command::RingCurl, 400 }, + { Command::PinkyCurl, 500 }, + }, + }, + // curl + splay + { + "A1(AB)2B3(BB)4C5(CB)6D7(DB)8E9(EB)10", + { + { Command::ThumbCurl, 1 }, + { Command::ThumbSplay, 2 }, + { Command::IndexCurl, 3 }, + { Command::IndexSplay, 4 }, + { Command::MiddleCurl, 5 }, + { Command::MiddleSplay, 6 }, + { Command::RingCurl, 7 }, + { Command::RingSplay, 8 }, + { Command::PinkyCurl, 9 }, + { Command::PinkySplay, 10 }, + }, + }, + // curl + splay (unordered) + { + "E9A1B3D7C5(BB)4(AB)2(EB)10(CB)6(DB)8", + { + { Command::ThumbCurl, 1 }, + { Command::ThumbSplay, 2 }, + { Command::IndexCurl, 3 }, + { Command::IndexSplay, 4 }, + { Command::MiddleCurl, 5 }, + { Command::MiddleSplay, 6 }, + { Command::RingCurl, 7 }, + { Command::RingSplay, 8 }, + { Command::PinkyCurl, 9 }, + { Command::PinkySplay, 10 }, + }, + }, + }; + + for (auto& [input_string, expected_commands] : input_strings) { + std::map commands = AlphaEncodingService::splitCommands(input_string); + + TEST_ASSERT_EQUAL_size_t_MESSAGE( + expected_commands.size(), + commands.size(), + "Expected commands size does not match actual commands size" + ); + + for (auto& [command, value] : expected_commands) { + TEST_ASSERT_EQUAL_INT(value, commands[command]); + } + } +} + +int process(void) +{ + UNITY_BEGIN(); + + RUN_TEST(testSplitCommands); + + return UNITY_END(); +} + +#ifdef ARDUINO + +#include + +void setup(void) +{ + process(); +} + +void loop(void) {} + +#else + +int main(int argc, char** argv) +{ + return process(); +} + +#endif