Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Embedded Ros Bridge

ASecurity

Use when writing firmware that bridges a microcontroller (STM32/micro-ROS) to a ROS 2 graph — bare-metal C/C++ driver patterns, UART/CAN transport layers, micro-ROS executor setup, and LVGL GUI initialization for embedded displays.

2 stars
0 votes
0 copies
0 views
Added 9/19/2026
ai-agentspythongoc++angularnodedocker

Works with

cli

Security Analysis

A100/100

Scanned 9/19/2026

Install to Claude Code

$npx -y skills add enesbirlik/claude-code-robotics --skill embedded-ros-bridge --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Embedded Ros Bridge?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Embedded Ros Bridge
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/enesbirlik-embedded-ros-bridge/badge)](https://www.skillsdirectory.com/skills/enesbirlik-embedded-ros-bridge)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: embedded-ros-bridge
description: Use when writing firmware that bridges a microcontroller (STM32/micro-ROS) to a ROS 2 graph — bare-metal C/C++ driver patterns, UART/CAN transport layers, micro-ROS executor setup, and LVGL GUI initialization for embedded displays.
---

# Embedded / micro-ROS Bridge Firmware

Rules and templates for STM32 (or similar Cortex-M) firmware that participates in a ROS 2
graph via micro-ROS, plus patterns for raw CAN bus drivers and embedded LVGL displays that sit
alongside it.

## 1. Architecture overview

```
┌─────────────────────┐   serial / UDP   ┌───────────────────────────────┐
│ micro-ROS Agent      │◄────────────────►│ STM32 firmware                │
│ (ros2 run micro_ros_ │   XRCE-DDS       │  - rclc executor              │
│  agent ...)          │                  │  - publishers/subscribers     │
│ runs on the Linux    │                  │  - HAL-based transport layer  │
│ host / SBC           │                  │  - sensor/motor drivers       │
└─────────────────────┘                  └───────────────────────────────┘
```

- The **agent** runs on the Linux side (the robot's SBC or a dev machine) and bridges XRCE-DDS
  micro-ROS traffic into the full ROS 2 graph.
- The **client** (this firmware) links against `micro_ros_stm32cubemx_utils` /
  `micro_ros_platformio` and only ever depends on `rcl`, `rclc`, and generated message headers
  — never the full `rclcpp`, which doesn't fit on a microcontroller.
- **Version pinning matters**: the micro-ROS client library major.minor must match the agent's
  major.minor (both track a ROS 2 distro, e.g. both built against Humble). A mismatch fails
  the XRCE-DDS session handshake with an opaque timeout, not a clear version error.

## 2. Firmware project structure (STM32CubeIDE / CubeMX + micro-ROS)

```
firmware/
├── Core/
│   ├── Inc/                 # CubeMX-generated HAL headers
│   └── Src/
│       ├── main.c           # rclc_support_init, node/pub/sub creation, executor loop
│       ├── freertos.c        # (optional) if using a micro-ROS FreeRTOS task
│       └── uart_transport.c  # custom rmw transport bound to a HAL UART
├── microros_static_library/  # generated by the micro_ros_setup / Docker builder
└── Drivers/                  # CubeMX HAL + BSP drivers
```

## 3. `main.c` pattern (bare-metal, no RTOS)

```c
#include "rcl/rcl.h"
#include "rclc/rclc.h"
#include "rclc/executor.h"
#include <geometry_msgs/msg/twist.h>
#include <std_msgs/msg/int32.h>

#define RCCHECK(fn) do { \
  rcl_ret_t rc = fn; \
  if (rc != RCL_RET_OK) { on_rcl_error(rc, __LINE__); } \
} while (0)

static rclc_support_t support;
static rcl_allocator_t allocator;
static rcl_node_t node;
static rcl_subscription_t cmd_vel_sub;
static geometry_msgs__msg__Twist cmd_vel_msg;
static rclc_executor_t executor;

static void on_rcl_error(rcl_ret_t rc, int line)
{
  (void)rc; (void)line;
  /* Latch a fault LED / watchdog-safe halt instead of silently continuing with
   * an uninitialized node -- never ignore an RCL init failure on an embedded target. */
  while (1) { HAL_GPIO_TogglePin(FAULT_LED_GPIO_Port, FAULT_LED_Pin); HAL_Delay(100); }
}

static void cmd_vel_callback(const void * msgin)
{
  const geometry_msgs__msg__Twist * msg = (const geometry_msgs__msg__Twist *)msgin;
  apply_wheel_setpoints(msg->linear.x, msg->angular.z);
}

void micro_ros_main(void)
{
  allocator = rcl_get_default_allocator();

  rmw_uros_set_custom_transport(
    true, NULL,
    stm32_uart_open, stm32_uart_close,
    stm32_uart_write, stm32_uart_read);

  RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));
  RCCHECK(rclc_node_init_default(&node, "stm32_motor_bridge", "", &support));

  RCCHECK(rclc_subscription_init_default(
    &cmd_vel_sub, &node,
    ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist),
    "cmd_vel"));

  RCCHECK(rclc_executor_init(&executor, &support.context, 1, &allocator));
  RCCHECK(rclc_executor_add_subscription(
    &executor, &cmd_vel_sub, &cmd_vel_msg, &cmd_vel_callback, ON_NEW_DATA));

  while (1) {
    rclc_executor_spin_some(&executor, RCL_MS_TO_NS(10));
    run_control_loop_step();   /* bounded, non-blocking, never a HAL_Delay() here */
    HAL_Delay(10);
  }
}
```

Rules:
- Wrap **every** `rcl_ret_t`-returning call in an `RCCHECK`-style macro. A silently ignored
  `RCL_RET_ERROR` on an embedded target leaves the node in an undefined half-initialized state
  with no stack trace to debug later — fail loud and fail fast (a latched fault indicator),
  never fail silent.
- Use `rclc_executor_spin_some()` in the main loop with a small bounded timeout, never a
  blocking `rclc_executor_spin()` that never returns — the loop needs to return control so
  `run_control_loop_step()` (motor PID, watchdog feed, sensor polling) still executes even if
  no message arrived.
- Size the executor's handle count (`rclc_executor_init(&executor, &support.context, N, ...)`)
  to exactly the number of subscriptions/timers/services you register — `micro_ros_utilities`
  allocates fixed-size static memory for these, so this must be known at compile time; there is
  no dynamic growth.
- Never call `malloc`/`free` from an interrupt context or from the UART RX ISR that feeds the
  transport layer — use the static memory profile (`colcon.meta` / `rmw_microxrcedds` config)
  micro-ROS ships for constrained targets.
- Feed an independent hardware watchdog (`IWDG`) from the main loop, not from inside the
  executor callback — if XRCE-DDS communication stalls, the robot must still be able to detect
  a hung control loop and reset.

## 4. UART transport layer

```c
#include "usart.h"   /* CubeMX-generated huart2 etc. */

bool stm32_uart_open(struct uxrCustomTransport * transport)
{
  (void)transport;
  return true;   /* UART already initialized by MX_USART2_UART_Init() in main() */
}

bool stm32_uart_close(struct uxrCustomTransport * transport)
{
  (void)transport;
  return true;
}

size_t stm32_uart_write(struct uxrCustomTransport * transport,
                         const uint8_t * buf, size_t len, uint8_t * err)
{
  (void)transport;
  HAL_StatusTypeDef status = HAL_UART_Transmit(&huart2, (uint8_t *)buf, len, 100);
  *err = (status == HAL_OK) ? 0 : 1;
  return (status == HAL_OK) ? len : 0;
}

size_t stm32_uart_read(struct uxrCustomTransport * transport,
                        uint8_t * buf, size_t len, int timeout_ms, uint8_t * err)
{
  (void)transport;
  HAL_StatusTypeDef status = HAL_UART_Receive(&huart2, buf, len, timeout_ms);
  *err = (status == HAL_OK) ? 0 : 1;
  return (status == HAL_OK) ? len : 0;
}
```

Rules:
- The XRCE-DDS session layer already frames and checksums messages — the transport only needs
  reliable byte-in/byte-out semantics, never add extra framing here.
- Use bounded `HAL_UART_Transmit`/`Receive` timeouts, never `HAL_MAX_DELAY` — a stalled UART
  peripheral must not hang the whole executor loop indefinitely.
- Pick a baud rate that comfortably covers your message rate and size with margin (115200 is a
  reasonable default; raise it for high-rate sensor topics like IMU at >100 Hz).

## 5. CAN bus: bare-metal driver + Linux-side bridge

micro-ROS itself communicates over serial or UDP, not CAN — CAN is best used as the
low-latency bus between the MCU and motor controllers/sensors, bridged into ROS 2 on the Linux
side via SocketCAN, keeping the DDS session off the real-time bus entirely.

```c
/* Firmware side: fixed-format CAN frame for periodic telemetry (bxCAN/FDCAN HAL). */
typedef struct __attribute__((packed)) {
  int16_t wheel_velocity_mrad_s;   /* milli-rad/s, avoids float-in-CAN-payload ambiguity */
  uint16_t battery_mv;
  uint8_t fault_flags;
} MotorTelemetryFrame;

void send_motor_telemetry(int16_t velocity_mrad_s, uint16_t battery_mv, uint8_t faults)
{
  MotorTelemetryFrame frame = {velocity_mrad_s, battery_mv, faults};
  CAN_TxHeaderTypeDef header = {
    .StdId = CAN_ID_MOTOR_TELEMETRY,
    .IDE = CAN_ID_STD,
    .RTR = CAN_RTR_DATA,
    .DLC = sizeof(frame),
  };
  uint32_t mailbox;
  HAL_CAN_AddTxMessage(&hcan1, &header, (uint8_t *)&frame, &mailbox);
}
```

```python
# Linux side: a plain rclpy node bridging SocketCAN into a ROS 2 topic (python-can + rclpy).
# Keep this bridge node minimal and single-purpose -- it should only translate frames, not
# implement control logic, so the safety-critical loop stays entirely on the MCU.
import can
import rclpy
from rclpy.node import Node
from std_msgs.msg import Int32MultiArray

CAN_ID_MOTOR_TELEMETRY = 0x100


class CanBridgeNode(Node):
    def __init__(self) -> None:
        super().__init__('can_bridge_node')
        self._bus = can.interface.Bus(channel='can0', bustype='socketcan')
        self._pub = self.create_publisher(Int32MultiArray, 'motor_telemetry', 10)
        self._timer = self.create_timer(0.01, self._poll_can)

    def _poll_can(self) -> None:
        msg = self._bus.recv(timeout=0.0)
        if msg is not None and msg.arbitration_id == CAN_ID_MOTOR_TELEMETRY:
            out = Int32MultiArray()
            out.data = list(msg.data)
            self._pub.publish(out)
```

Rules:
- Use fixed-width packed structs for CAN payloads (max 8 bytes classic CAN, 64 bytes FDCAN) —
  never send floats without an explicit fixed-point convention documented next to the struct.
- Assign CAN IDs from a single header shared by firmware and any Linux-side decoder — never
  duplicate magic ID numbers in two places that can drift out of sync.
- Keep safety-critical control decisions (e-stop, current limiting) on the MCU side of the CAN
  bus; the Linux-side bridge should be a thin, replaceable translation layer.

## 6. LVGL GUI on an embedded display

```c
#include "lvgl.h"

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[MY_DISP_HOR_RES * 10];

static void disp_flush_cb(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
  /* Use DMA for the actual transfer; never block here with a busy-wait longer than one
   * refresh period, or LVGL's internal timing budget is violated. */
  st7789_draw_bitmap_dma(area->x1, area->y1, area->x2, area->y2, (uint16_t *)color_p);
}

static void dma_transfer_complete_callback(void)
{
  lv_disp_flush_ready(&disp_drv);   /* called from DMA ISR when the transfer finishes */
}

void lvgl_display_init(void)
{
  lv_init();
  lv_disp_draw_buf_init(&draw_buf, buf1, NULL, MY_DISP_HOR_RES * 10);

  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
  disp_drv.hor_res = MY_DISP_HOR_RES;
  disp_drv.ver_res = MY_DISP_VER_RES;
  disp_drv.flush_cb = disp_flush_cb;
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register(&disp_drv);
}

/* Called from the periodic loop (a 5-10 ms tick, e.g. a low-priority FreeRTOS task or a
 * SysTick-driven counter in the bare-metal main loop). */
void lvgl_tick_and_handle(void)
{
  lv_tick_inc(5);
  lv_timer_handler();
}
```

Bridging live robot state into the GUI safely:

```c
/* Shared state written by the micro-ROS subscription callback, read by the GUI loop.
 * Use a critical section or lightweight spinlock, not a full RTOS mutex, if the writer is an
 * ISR/executor callback and the reader is a bounded polling loop -- keep the critical
 * section as short as a struct copy, never hold it across an LVGL draw call. */
typedef struct { float battery_voltage; uint8_t fault_flags; } RobotStateSnapshot;
static volatile RobotStateSnapshot g_robot_state;

static void battery_topic_callback(const void * msgin)
{
  const std_msgs__msg__Float32 * msg = (const std_msgs__msg__Float32 *)msgin;
  __disable_irq();
  g_robot_state.battery_voltage = msg->data;
  __enable_irq();
}

void lvgl_update_battery_label(lv_obj_t * label)
{
  RobotStateSnapshot snapshot;
  __disable_irq();
  snapshot = g_robot_state;
  __enable_irq();
  lv_label_set_text_fmt(label, "%.2f V", (double)snapshot.battery_voltage);
}
```

Rules:
- Drive the physical panel refresh with DMA and signal `lv_disp_flush_ready()` from the DMA
  completion ISR — a blocking SPI/parallel write in `flush_cb` will visibly stall the whole UI
  and starve the micro-ROS executor if they share a core.
- Call `lv_timer_handler()` on a steady period (5-30 ms depending on `LV_DISP_DEF_REFR_PERIOD`
  in `lv_conf.h`) from a single consistent context — never from both the main loop and an ISR.
- Cross-thread/ISR state shared between a micro-ROS callback and the LVGL loop must go through
  a minimal critical section (disable IRQ / RTOS mutex) around a small struct copy, never a
  raw unguarded global read/write — a torn read of a multi-byte value is a real, if rare,
  correctness bug on Cortex-M.

Attribution

enesbirlikenesbirlik
View sourceMore from enesbirlik →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, wenyan-lite, wenyan-full, wenyan-ultra. Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.

1023331 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

686011 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3331 votes

catchup

Recovers prior coding-agent session context by running `catchup <agent> --since-compact`, which extracts a clean summary of a previous Codex, Claude Code, Antigravity, OpenCode, or Pi Agent session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", or asks to recover/summarize a previous session before continuing. Do NOT use for the current conversation, git history, or any non-agent log.

611 votes

math-skill

A comprehensive mathematical reasoning skill for AI assistants — handles arithmetic to research-level problems with rigorous step-by-step reasoning, systematic verification, and transparent uncertainty handling

381 votes
View all in ai-agents →