{"id":15132790,"url":"https://github.com/ggldnl/hexapod-operator","last_synced_at":"2025-04-12T14:22:10.358Z","repository":{"id":255665923,"uuid":"852831847","full_name":"ggldnl/Hexapod-Operator","owner":"ggldnl","description":"Servo2040 firmware for my hexapod robot","archived":false,"fork":false,"pushed_at":"2025-02-11T14:51:14.000Z","size":54,"stargazers_count":2,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-26T08:51:36.984Z","etag":null,"topics":["hexapod","hexapod-robot","raspberry-pi","rp2040","servo-controller"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ggldnl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-05T14:00:29.000Z","updated_at":"2025-03-05T17:23:09.000Z","dependencies_parsed_at":"2025-01-28T22:34:49.798Z","dependency_job_id":null,"html_url":"https://github.com/ggldnl/Hexapod-Operator","commit_stats":{"total_commits":28,"total_committers":2,"mean_commits":14.0,"dds":0.1071428571428571,"last_synced_commit":"b8051017e57aceca1e2fc0c2edc6d223ff8ea3fb"},"previous_names":["ggldnl/hexapod-operator"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ggldnl%2FHexapod-Operator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ggldnl%2FHexapod-Operator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ggldnl%2FHexapod-Operator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ggldnl%2FHexapod-Operator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ggldnl","download_url":"https://codeload.github.com/ggldnl/Hexapod-Operator/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248578883,"owners_count":21127720,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hexapod","hexapod-robot","raspberry-pi","rp2040","servo-controller"],"created_at":"2024-09-26T04:40:25.518Z","updated_at":"2025-04-12T14:22:10.351Z","avatar_url":"https://github.com/ggldnl.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hexapod Operator\n\nMy hexapod robot consists of two parts: a Controller and an Operator. The Controller is responsible for generating commands, which are then sent to the Operator for execution. I used a Raspberry Pi as Controller  and a Servo2040 board as Operator, for handling low-level control of the hexapod's servos.\nThis repository contains the firmware for the Servo2040 board.\n\nFor a complete overview of the project refer to the [main Hexapod repository](https://github.com/ggldnl/Hexapod.git). Take also a look to the [repository containing the Controller's code](https://github.com/ggldnl/Hexapod-Controller.git). \n\nBelow, you will find instructions on how to build and deploy the code and info on how the communication protocol I designed works.\n\n## 🛠️ Build and deployment\n\nBefore you start, take a look at this [template](https://github.com/pimoroni/pico-boilerplate?tab=readme-ov-file#before-you-start). This served as starting point to develop the firmware.\n\nIt's easier if you make a `pico` directory or similar in which you keep the SDK, Pimoroni Libraries and this project. This makes it easier to include libraries. At the end you will have this directory structure:\n\n```\npico\n├── Hexapod-Operator\n├── pico-sdk\n└── pimoroni-pico\n```\n\nFeel free to use another name for the `pico` directory. I will use this out of simplicity. \n\n### Prepare the build environment\n\nInstall build requirements:\n\n```bash\nsudo apt update\nsudo apt install cmake gcc-arm-none-eabi build-essential\n```\n\n### Download the pico SDK\n\nDownload the pico SDK in the `pico` directory:\n\n```bash\ncd pico\ngit clone https://github.com/raspberrypi/pico-sdk\ncd pico-sdk\ngit submodule update --init\nexport PICO_SDK_PATH=`pwd`\ncd ../\n```\n\nThe `PICO_SDK_PATH` set above will only last the duration of your session. To make it persistant you can add it to your `.bashrc`.\n\n```bash\necho 'export PICO_SDK_PATH=\"/path/to/pico-sdk\"' \u003e\u003e ~/.bashrc\n```\n\n### Download the Pimoroni libraries\n\nDownload the Pimoroni libraries in the `pico` directory:\n\n```bash\ngit clone https://github.com/pimoroni/pimoroni-pico\n```\n\n### Clone the project\n\n```bash\ngit clone https://github.com/ggldnl/Hexapod-Operator\n```\n\nIf you have not or don't want to set `PICO_SDK_PATH` and you are using vscode, you can edit `.vscode/settings.json` to pass the path directly to CMake.\n\n### Build\n\nCreate a build directory in the root folder of the project and compile.\n\n```bash\nmkdir build\ncd build\ncmake ..\nmake\n```\n\nOnce you compile the project you will end up with a `Hexapod.uf2` file inside the `build` directory.\n\n### Delpoy\n\n- Connect the servo2040 board to the computer;\n- Hold down the `boot/user` button, press the `reset` button at the same time, and let go of both buttons. The Servo2040 should now appear as drive to the computer;\n- Drag and drop the `Hexapod.uf2` image file to the Servo2040 drive, the device will automatically reboot and start the loaded program.\n\nIf you built the firmware on the raspberry pi that you will use for the Hexapod and you happen to be connected to it with ssh, you can:\n\n- Connect the servo2040 board to the raspberry through usb;\n- Hold down the `boot/user` button, press the `reset` button at the same time, and let go of both buttons. The Servo2040 should now appear as a block device when issuing `lsblk`;\n- Look for the new drive (e.g. `/dev/sda1` mounted at `/media/\u003cusername\u003e/RPI-RP2`);\n- From the `build` directory, `mv Hexapod.uf2 /media/\u003cusername\u003e/RPI-RP2`, the device will automatically reboot and start the loaded program.\n\n## 🔌 Connection\n\nConnect the Servo2040 board to the raspberry pi as follows:\n\n| Raspberry    | Servo2040 |\n|--------------|-----------|\n| 5V           | 5V        |\n| GND          | GND       |\n| GPIO14 (TXD) | SDA (RX)  |\n| GPIO15 (RXD) | SCL (TX)  |\n\nRemember to enable hardware uart: \n- `sudo raspi-config` \u003e `Interface Options` \u003e `Serial Port`\n- Would you like a login shell to be accessible over serial? \u003e `No`\n- Would you like the serial port hardware to be enabled? \u003e `Yes`\n- Save and reboot.\n\n## 📡 Communication protocol\n\nThis paragraph outlines the specifications for the communication protocol. Commands are sent from the controlling machine (Raspberry Pi) to the operator (Servo2040) over a serial connection. The two must agreen on the instruction table beforehand. \n\n### Instruction set\n\nThe following table describes the supported operations, their corresponding opcodes, the expected arguments, and the response format:\n\n| Operation              | OpCode (Hex) | Arguments                                                     | Response          |\n|------------------------|--------------|---------------------------------------------------------------|-------------------|\n| Get Voltage            | `0x01`       | None                                                          | `\u003cval\u003e` (4 bytes) |\n| Get Current            | `0x02`       | None                                                          | `\u003cval\u003e` (4 bytes) |\n| Read Sensor            | `0x03`       | `\u003cpin\u003e` (1 byte)                                              | `\u003cval\u003e` (4 bytes) |\n| Set LED                | `0x04`       | `\u003cpin\u003e` (1 byte) `\u003cr\u003e` (1 byte) `\u003cg\u003e` (1 byte) `\u003cb\u003e` (1 byte) | `0x00`  (1 byte)  |\n| Set LEDs               | `0x05`       | `\u003cnum\u003e` (1 byte) `\u003cpin\u003e` `\u003cr\u003e` `\u003cg\u003e` `\u003cb\u003e` (4 bytes) x num    | `0x00`  (1 byte)  |\n| Attach Servos          | `0x06`       | None                                                          | `0x00`  (1 byte)  |\n| Detach Servos          | `0x07`       | None                                                          | `0x00`  (1 byte)  |\n| Set Servo Pulse Width  | `0x08`       | `\u003cpin\u003e` (1 byte) `\u003cpulse_width\u003e` (4 bytes)                    | `0x00`  (1 byte)  |\n| Set Servos Pulse Width | `0x09`       | `\u003cnum\u003e` (1 byte) `\u003cpin\u003e` `\u003cpulse_width\u003e` (5 bytes) x num      | `0x00`  (1 byte)  |\n| Set Servo Angle        | `0x0A`       | `\u003cpin\u003e` (1 byte) `\u003cangle\u003e` (4 bytes)                          | `0x00`  (1 byte)  |\n| Set Servo Angles       | `0x0B`       | `\u003cnum\u003e` (1 byte) `\u003cpin\u003e` `\u003cangle\u003e` (5 bytes) x num            | `0x00`  (1 byte)  |\n| Connect Relay          | `0x0C`       | None                                                          | `0x00`  (1 byte)  |\n| Disconnect Relay       | `0x0D`       | None                                                          | `0x00`  (1 byte)  |\n\nDescription table:\n\n| Operation              | Description                                         |\n|------------------------|-----------------------------------------------------|\n| Get Voltage            | Reads the voltage on the external trace             |\n| Get Current            | Reads the current on the external trace             |\n| Read Sensor            | Reads the voltage value of an analog pin            |\n| Set LED                | Sets the rgb value for the given LED                |\n| Set LEDs               | For each LED pin, sets the respective rgb value     |\n| Attach Servos          | Attaches all the servos                             |\n| Detach Servos          | Detaches all the servos                             |\n| Set Servo Pulse Width  | Set the pulse width for the specified servo         |\n| Set Servos Pulse Width | For each Servo pin, sets the respective pulse width |\n| Set Servo Angle        | Set the angle for the specified servo               |\n| Set Servo Angles       | For each Servo pin, sets the respective angle       |\n| Connect Relay          | Turns the relay on, giving power to the servos      |\n| Disconnect Relay       | Turns the relay off, disconnecting the servos       |\n\nLeading `0xAA` and trailing `0xFF` bytes are added and serve as packet delimiters. \n\nThe protocol is designed using the Command Design Pattern, which simplifies the addition of new commands. \n\n### Implementation details\n\nWe start defining a shared object pool. \n\n```cpp\n// Shared object pool\nServoCluster servos = ServoCluster(pio0, 0, servo2040::SERVO_1, servo2040::NUM_SERVOS);\nWS2812 leds(servo2040::NUM_LEDS, pio1, 0, servo2040::LED_DATA);\nRelay relay(RELAY_PIN);\nAnalogReader reader;\n\nservos.init();\nleds.start();\n```\n\nEach command will take a reference to the object(s) it needs to work with. Commands that need, for example, to read from a sensor (internal or external), will have a reference to the `AnalogReader`, a utility class that encapsulates the logic for multiplexing and reading; the same way, commands that need to work with servos will take a reference to a unique `ServoCluster` object. This limits potential interference between commands and redundancy.\n\n```cpp\n// Initialize the dispatcher\nCommandDispatcher dispatcher;\n\n// Register the commands\ndispatcher.registerCommand(0x01, std::make_unique\u003cGetVoltageCommand\u003e(reader));\n```\n\nUpon receipt of a message, the `dispatcher` handles it. The first byte, the opcode, is used to lookup and dispatch the corresponding command. If the opcode matches a registered command, the `dispatcher` executes the command with the remainig bytes in the message as arguments. The response from the command is then sent back to the controlling machine over the serial connection.\n\n### Adding a new command\n\nAs an example we can add a command that toggles the status of a variable. It will need no arguments and return a single byte each time, `0x00`. \n\nCreate a new header file named `toggle_status_command.hpp` in the commands directory. Implement the class as follows:\n\n```cpp\n#ifndef TOGGLE_STATUS_COMMAND_HPP\n#define TOGGLE_STATUS_COMMAND_HPP\n\n#include \"command.hpp\"\n\nclass ToggleStatusCommand : public Command {\n\nprivate:\n\n    bool status;\n\npublic:\n\n    ToggleStatusCommand() : status(false) {}\n\n    void execute(const std::vector\u003cuint8_t\u003e\u0026 args) override {\n        status = !status;\n    }\n\n    std::vector\u003cuint8_t\u003e getResponse() override {\n        return {0x00};\n    }\n};\n\n#endif // TOGGLE_STATUS_COMMAND_HPP\n```\n\nTo be a valid command, the new class must extend the `Command` base class and implement the `execute()` and `getResponse()` methods. The `execute()` method contains the logic for toggling the status variable, and the `getResponse()` method returns a success response.\n\nNext, include the new command in your main program and register it with the dispatcher using an available opcode. Here’s how to do it:\n\n```cpp\n// Other includes\n#include \"commands/toggle_status_command.hpp\"\n\n// ...\n\nint main() {\n\n    // ...\n\n    // Initialize the dispatcher\n    CommandDispatcher dispatcher;\n\n    // Register the commands\n    dispatcher.registerCommand(0x01, std::make_unique\u003cGetVoltageCommand\u003e(reader));\n    dispatcher.registerCommand(0x02, std::make_unique\u003cGetCurrentCommand\u003e(reader));\n    // ...\n\n    // Register the new Toggle Status command\n    dispatcher.registerCommand(0x08, std::make_unique\u003cToggleStatusCommand\u003e());\n\n    // ... (rest of the main function)\n}\n```\n\nI used opcode `0x0E` as it's the first available. Once registered, the dispatcher will automatically invoke the new `ToggleStatusCommand` when the opcode `0x0E` is received as first byte over the serial connection. The following bytes are treated as arguments and interpreted.\n\n## 🤝 Contribution\n\nFeel free to contribute by opening issues or submitting pull requests. For further information, check out the [main Hexapod repository](https://github.com/ggldnl/Hexapod). Give a ⭐️ to this project if you liked the content.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fggldnl%2Fhexapod-operator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fggldnl%2Fhexapod-operator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fggldnl%2Fhexapod-operator/lists"}