{"id":28324851,"url":"https://github.com/linkoucommander/manipulator-control-framework","last_synced_at":"2026-02-28T21:32:15.005Z","repository":{"id":257999949,"uuid":"872806937","full_name":"LinkouCommander/Manipulator-Control-Framework","owner":"LinkouCommander","description":null,"archived":false,"fork":false,"pushed_at":"2025-06-16T20:18:37.000Z","size":7982,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-24T03:35:52.011Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LinkouCommander.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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,"zenodo":null}},"created_at":"2024-10-15T05:35:42.000Z","updated_at":"2025-06-16T20:18:40.000Z","dependencies_parsed_at":"2024-10-26T01:13:50.869Z","dependency_job_id":"3f8302ac-c802-4465-914c-412d649f5629","html_url":"https://github.com/LinkouCommander/Manipulator-Control-Framework","commit_stats":null,"previous_names":["linkoucommander/manipulator","linkoucommander/manipulator-control-framework"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/LinkouCommander/Manipulator-Control-Framework","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinkouCommander%2FManipulator-Control-Framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinkouCommander%2FManipulator-Control-Framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinkouCommander%2FManipulator-Control-Framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinkouCommander%2FManipulator-Control-Framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LinkouCommander","download_url":"https://codeload.github.com/LinkouCommander/Manipulator-Control-Framework/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LinkouCommander%2FManipulator-Control-Framework/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29952265,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-28T18:42:55.706Z","status":"ssl_error","status_checked_at":"2026-02-28T18:42:48.811Z","response_time":90,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2025-05-25T19:13:21.196Z","updated_at":"2026-02-28T21:32:14.966Z","avatar_url":"https://github.com/LinkouCommander.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Manipulator Control Framework\n\nThis is a Python-based control framework for a 10-DOF robotic manipulator equipped with Dynamixel servos, a slider, a webcam, force-sensitive resistors (FSRs), and an IMU sensor. The original purpose of this project is to serve as a real-world environment for training a robotic arm to play ball using reinforcement learning (RL).\n\n![DXL](tmp/dxlgif.gif)\n\n## Table of Contents\n\n- [Features](#features)\n- [Code Structure](#code-structure)\n- [Requirements](#requirements)\n- [Usage](#usage)\n- [Control and Learning (`main.py`)](#control-and-learning-mainpy)\n- [Dynamixel Servo (`dxl_module.py`)](#dynamixel-servo-dxl_modulepy)\n- [IMU (`imu_module.py`)](#imu-imu_modulepy)\n- [Slider \u0026 FSRs (`fsr_slider_module.py`)](#slider--fsrs-fsr_slider_modulepy)\n- [Webcam (`cam_module.py`)](#webcam-cam_modulepy)\n- [Possible Improvements](#possible-improvements)\n\n## Features\n\n- **10 DOF Robotic System**: Supports 9 Dynamixel servos and 1 slider motor with position control.\n\n- **FSR Sensor**: Reads analog force data and converts it into binary contact signals based on a configurable threshold.\n\n- **IMU Sensor**: Retrieves angular velocity data from a BLE-connected IMU device, which is used to compute the ball's angular velocity for rotation reward calculation.\n\n- **Webcam**: Detects and tracks a red ball in video frame, computing a lifting reward based on vertical movement.\n\n- **Threaded Data Collection**: Utilizes multithreading for efficient sensor data acquisition without blocking motor control.\n\n- **Data Visualization**: Provides plotting of sensor and reward data using Matplotlib.\n\n- **Modular Design**: Organized into separate modules for each sensor and actuator.\n\n## Code Structure\n```graphql\nmanipulator/\n│\n├── main.py                # PPO reinforcement learning training script\n├── module/                # Modules for interfacing with sensors and actuators\n│   ├── cam_module.py\n│   ├── fsr_slider_module.py\n│   ├── imu_module.py\n│   └── dxl_module.py\n├── tests/                 # Unit tests and validation scripts\n├── arduino/               # Arduino sketches for FSR and slider control\n├── README.md              # Project documentation\n└── ...\n```\n\n## Requirements\n### Hardware\n- Dynamixel servo\n- Force-sensitive resistors\n- Slider motor\n- WITMOTION WT9011DCL IMU sensor\n\n### Dependencies\n- Python 3\n- Dynamixel SDK\n- pyserial (for Arduino communication)\n- bleak (for BLE communication)\n- stable-baselines3 (RL algorithm library)\n- gymnasium (build RL environments)\n- numpy\n- opencv-python\n- matplotlib\n- imutils\n\n## Usage\n1. Install the required Python packages:\n    ```bash\n    pip install -r requirements.txt\n    ```\n2. In `main.py`, allocate the **port numbers** for the DXL handler and FSR/slider's Arduino:\n    ```python\n    fsr = FSRSerialReader(port='COM4', baudrate=115200, threshold=50)\n    dxl = DXLHandler(device_name='COM3', baudrate=1000000)\n    ```\n3. Turn on the hardware devices:\n   - Connect DXL, FSR/Slider and ensure they are supplied with sufficient voltage.\n   - Connect the webcam.\n   - Turn on the IMU.\n4. Run `main.py`\n    ```bash\n    python main.py\n    ```\n\n## Control and Learning (`main.py`)\n`HandEnv` class in `main.py` is a custom reinforcement learning environment based on the OpenAI Gymnasium interface, designed for controlling a robotic hand with multiple sensors.\n### Main Methods\n- `step(action)`: Executes one step, returns observation, reward, done, truncated, info.\n- `reset()`: Resets the environment, returns initial observation.\n- `render()`: Displays the camera feed.\n- `close()`: Closes all sensors and hardware connections.\n### Observation and Action Spaces\n| Space        | Dim | Description                         | Range    |\n|--------------|-----|-------------------------------------|----------|\n| Action       | 7   | 6 motor controls + 1 slider         | [-1, 1]  |\n| Observation  | 10  | 6 joint positions + 1 slider + 3 FSR| [-4, 4]  |\n### Workflow of `step()` Function\n```css\n[Action Input]\n       │\n       ▼\n[Motor \u0026 Slider Control]\n       │\n       ▼\n[Safety Monitoring]\n       │\n       ▼\n[Build Observation]\n       │\n       ▼\n[Reward Calculation]\n       │\n       ▼\n[Termination?]\n┌───────┴────────┐\n▼                ▼\n[Next Step]      [End Episode]\n\n```\n1. **Action Execution**:\n    - Receives a 7-element action array:\n        - **First 6 values**: Motor controls mapped to Dynamixel servo positions using linear interpolation between [-1,1] →[1800][2200]\n        - **7th value**: Slider control (currently commented out in code)\n    - Interact with hardware:\n        - Sends position commands to Dynamixel motors via `dxl.move_to_position()`\n        - Sends position commands to slider using `self.move_slider(action[6])`\n2. **Hardware Safety Monitor**:\n    - Monitors motor temperatures (servo overheated)\n    - Monitors movement timeout (claw is stucked)\n3. **Observation Construction**: Formatted as 10-dimensional numpy array\n    - 6 Dynamixel servos positions (mapped to [-1,1])\n    - Slider position (mapped to [-1,1])\n    - 3 FSR readings from `fsr.get_fsr()`\n4. **Reward Calculation**:\n    - Lifting reward: From camera tracking `cam.get_rewards()`\n    - Rotation reward: Calculated from IMU angular velocities\n        ```\n        rotation_reward = np.sqrt(x_velocity**2 + y_velocity**2 + z_velocity**2)\n        ```\n    - Weighted sum with curriculum learning:\n        ```\n        reward = lifting_reward * lift_weight + rotation_reward * rot_weight\n        ```\n5. **Episode Management**:\n    - Termination: `check_done()` based on accumulated rewards\n    - Truncation: \n        - Step limit (`self._ij` \u003e 20)\n        - DXL overheating\n        - Hardware failures (DXL stucked)\n\n## Dynamixel Servo (`dxl_module.py` )\nThis project uses nine Dynamixel servos (IDs: 10–12, 20–22, 30–32) to form a multi-joint robotic system. Each servo is controlled by specifying a goal position in the range 0–4095, where:\n- `0` corresponds to minimum angle (0°)\n- `4095` corresponds to maximum angle (approximate 360°)\n\nBelow shows how the Dynamixel servo connects to PC:\n```css\n+-------------+         USB         +------------+       TTL       +------------+\n|      PC     |  \u003c--------------\u003e   |    U2D2    | \u003c-------------\u003e | Dynamixel  |\n|  (USB Host) |                     | (Adapter)  |                 | (Servo)    |\n+-------------+                     +------------+                 +------------+\n```\n\nTo ensure system safety and hardware longevity, the system integrates:\n- **Collision Safety**: The command is considered a timeout if a motor fails to reach its destination within a given time.\n- **Overheat Check**: The system monitors motor temperatures using `read_temperature()` to prevent thermal overload.\n\n### Core Functions\nThe control system of Dynamixel Servo is built using the Dynamixel SDK, following are the important functions:\n- **PortHandler**: Handles low-level communication with the servo via a specified port (e.g., `COM4`).\n- **PacketHandler**: Manages the communication protocol (here, Protocol 2.0) and handles encoding/decoding of instructions and status packets.\n- `enable_torque()` / `disable_torque()`: Turn on/off motor actuation by writing to the torque control register.\n- `move_to_position()`: Commands specificed servos to move to target positions.\n- `read_positions()` / `read_temperature()`: Read latest motor position and temperature data.\n\n### Usage\n```python\nfrom dxl_module import DXLHandler\n\n# Create a DXLHandler instance \ndxl = DXLHandler(device_name='DXL COM port', baudrate=1000000) \n# Initialize the servos, including enabling torque and setting velocity\ndxl.start_dxl() \n\n# Move Dynamixel servos with IDs 10, 11, and 12 to the specified positions.\ndxl.move_to_position([10, 11, 12], [1024, 1536, 2048])\n# Read the current position and temperature from servos with IDs 10, 11, and 12.\npositions = dxl.read_positions([10, 11, 12])\ntemperatures = dxl.read_temperature([10, 11, 12])\n\n# Close the Dynamixel connection and disable torque\ndxl.stop_dxl()\n```\n\n## IMU (`imu_module.py`)\nThe `BLEIMUHandler` class is a major modification made to the original Python script from the manufacturer. It serves as an interface to connect to, disconnect from, and retrieve data from IMU.\n\n![imu](tmp/imu.jpg)\n![imu](tmp/imu_inball.jpg)\n\n### Core Functions\n- `scan()`: Performs a continuous search for BLE devices in the vicinity based on the specified target MAC address.\n- `start_imu()`: Initiates the scanning process, connects to the target device, and starts a separate thread to handle data acquisition. It ensures that the device is open and ready for communication.\n- `stop_imu`: Gracefully closes the connection to the IMU device and joins the data acquisition thread to ensure proper cleanup.\n- `updateIMUData()`: Retrieves and processes data from the IMU device. It returns the angular velocity values (AsX, AsY, AsZ) in radians, converting them from degrees to radians.\n\n### Usage\n```python\nfrom imu_module import BLEIMUHandler\n\nimu = BLEIMUHandler(target_device=\"Mac Addr. of IMU\")\nimu.start_imu()\nx_velocity, y_velocity, z_velocity = imu.updateIMUData()\nimu.stop_imu()\n```\n\n## Slider \u0026 FSRs (`fsr_slider_module.py`)\nThe robot arm uses one slider motor and three Force-Sensitive Resistors (FSRs), all controlled by a single Arduino. \n### Core Functions\nKey functions include:\n- `start_collection()`: Launches a background thread to continuously collect and threshold digital FSR data from the serial port.\n- `get_fsr()`: Retrieves the latest binary readings (0 or 1) from the three FSRs.\n- `send_slider_position()`: Sends a position command to the Arduino to control the slider motor. The valid position range is 75 (top position) to 145 (bottom position).\n### Usage\n```python\nfrom fsr_slider_module import FSRSerialReader\n\n# Create a FSRSerialReader instance\nfsr = FSRSerialReader(port='Arduino COM port', threshold=50)\n# Start collecting FSR data in background\nfsr.start_collection()\n\n# Get the latest FSR data\nd0, d1, d2 = get_fsr()\n# Sends a slider motor position command.\nrespond = send_slider_position(100)\n\n# Safe shut down\nfsr.stop_collection()\n```\n\n## Webcam (`cam_module.py`)\nThe webcam plays a crucial role in this system by detecting and tracking a red ball in real-time video frames, and computing a lifting reward based on the ball’s vertical movement.\n\n### Workflow for Calculating Lifting Reward\n1. **Capture Frame**: Obtain a frame from the camera and apply Gaussian Blur to reduce noise.\n2. **Color Filtering**: Use a red color filter to create a mask that highlights red objects in the frame.\n3. **Morphological Operations**: Apply Erosion and Dilation to smooth the edges of the colored region in the mask.\n4. **Contour Detection**: Use OpenCV functions to detect the largest contours in the red mask, treating them as the red ball. Obtain the position and radius of the detected red ball.\n5. **Reward Calculation**: Calculate the distance between the y-coordinate of the ball and the height threshold, and compute the lifting reward based on this distance.\n\n### Color Filtering\nSince the hue values of red span both ends of the HSV color space, two separate ranges is defined to filter out orange-tinted red and purple-tinted red markers, improving the accuracy of red object detection.\n- **Lower Hue Range** (`lower_red1 = [0, 50, 50]`, `upper_red1 = [15, 255, 255]`)  \n  → This range covers orange-tinted red to pure red.\n\n- **Upper Hue Range** (`lower_red2 = [170, 50, 50]`, `upper_red2 = [180, 255, 255]`)  \n  → This range covers deep red to purple-tinted red.\n\nBy combining these two ranges using `cv2.bitwise_or`, we ensure comprehensive red color detection, capturing variations across different lighting conditions and object surfaces.\n\n### Image Preprocessing\n\n#### Gaussian Blur (`cv2.GaussianBlur`)\nUsed to reduce high-frequency noise in the original image and eliminate excessive details, making edges smoother.\n\n#### Morphological Operations (`cv2.erode + cv2.dilate`)\nApplied after the image has been filtered into a binary mask using the red color filter.  \n- **Erosion (`cv2.erode`)** removes small noise along the edges, making object boundaries sharper.  \n- **Dilation (`cv2.dilate`)** restores objects that were shrunk due to erosion and fills small holes within color regions.  \n\n### Object Detection\n- `cv2.findContours`: Finds the contours in a binary image.\n\n- `cv2.minEnclosingCircle`: Finds the smallest enclosing circle around the largest contour to determine the ball's position `(x, y)` and radius.\n\n- `cv2.circle`: Draws the detected ball's contour and marks its center point.\n\n### Reward Calculating\nA horizontal line is drawn in the frame using the `height_threshold` to represent the desired height in the real world that we aim to reach.\n\nThe closer the y-coordinate of the ball's center point is to the set `height_threshold`, the higher the lifting reward the robot can achieve.\n\nCurrently, the reward range is from -3 to 0:\n- The reward is -3 when the ball is **not present in the image**.\n- The reward is 0 when the ball is **exactly at the `height_threshold`**.\n\n### Usage\n```python\n# Create a BallTracker instance and initializes the video stream\ncam = BallTracker(buffer_size=64, height_threshold=300, alpha=0.2)\nvs = cv2.VideoCapture(1)\n\n_, frame = vs.read()\n# Process the frame with the tracker\ncam.track_ball(frame)\n# Returns the most recent computed lifting reward.\nlifting_reward = cam.get_rewards()\n# Returns the latest processed frame with rendering\nrendered_frame = cam.get_frame()\n\nvs.release()\n```\n\n## Possible Improvements\n- `model.learn()` needs a callback capable of recognizing episode boundaries (termination occured).\n- FSR should  monitor data over a short time window to avoid missing brief fingertip contacts due to timing.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinkoucommander%2Fmanipulator-control-framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flinkoucommander%2Fmanipulator-control-framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinkoucommander%2Fmanipulator-control-framework/lists"}