Install
$ agentstack add skill-arpitg1304-robotics-agent-skills-robotics-software-principles ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Robotics Software Design Principles
Why Robotics Software Is Different
Robotics code operates under constraints that most software never faces:
- Physical consequences — A bug doesn't just crash a process, it crashes a robot into a wall
- Real-time deadlines — Missing a 1ms control loop deadline can cause oscillation or damage
- Sensor uncertainty — All inputs are noisy, delayed, and occasionally wrong
- Hardware diversity — Same algorithm must work on 10 different grippers from 5 vendors
- Sim-to-real gap — Code must run identically in simulation and on real hardware
- Long-running operation — Robots run for hours/days; memory leaks and drift matter
- Safety criticality — Some failures must NEVER happen, regardless of software state
These constraints demand disciplined design. Below are principles that account for them.
Principle 1: Single Responsibility — One Module, One Job
Every module (node, class, function) should have exactly ONE reason to change.
Why it matters in robotics: A perception module that also does control means a camera driver update can break your arm controller. In safety-critical systems, this coupling is unacceptable.
# ❌ BAD: God module — perception + planning + control + logging
class RobotController:
def __init__(self):
self.camera = RealSenseCamera()
self.detector = YOLODetector()
self.planner = RRTPlanner()
self.arm = UR5Driver()
self.logger = DataLogger()
def run(self):
image = self.camera.capture()
objects = self.detector.detect(image)
path = self.planner.plan(objects[0].pose)
self.arm.execute(path)
self.logger.log(image, objects, path)
# If ANY of these changes, you touch this class
# ✅ GOOD: Separated responsibilities with clear interfaces
class PerceptionModule:
"""ONLY responsibility: raw sensor data → detected objects"""
def __init__(self, camera: CameraInterface, detector: DetectorInterface):
self.camera = camera
self.detector = detector
def get_detections(self) -> List[Detection]:
image = self.camera.capture()
return self.detector.detect(image)
class PlanningModule:
"""ONLY responsibility: goal + world state → trajectory"""
def __init__(self, planner: PlannerInterface):
self.planner = planner
def plan_to(self, target: Pose, obstacles: List[Obstacle]) -> Trajectory:
return self.planner.plan(target, obstacles)
class ExecutionModule:
"""ONLY responsibility: trajectory → hardware commands"""
def __init__(self, arm: ArmInterface):
self.arm = arm
def execute(self, trajectory: Trajectory) -> ExecutionResult:
return self.arm.follow_trajectory(trajectory)
Test: Can you describe what a module does WITHOUT using "and"? If not, split it.
Principle 2: Dependency Inversion — Depend on Abstractions, Not Hardware
High-level modules (planning, behavior) should never depend on low-level modules (drivers, hardware). Both should depend on abstractions.
Why it matters in robotics: This is the foundation of sim-to-real. If your planner imports UR5Driver directly, it can't run in simulation. If it depends on ArmInterface, you swap implementations freely.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional
import numpy as np
# ─── ABSTRACTIONS (the contracts) ────────────────────────────
class ArmInterface(ABC):
"""Abstract arm — every arm implementation must honor this contract"""
@abstractmethod
def get_joint_positions(self) -> np.ndarray:
"""Returns current joint positions in radians"""
...
@abstractmethod
def get_ee_pose(self) -> Pose:
"""Returns current end-effector pose"""
...
@abstractmethod
def move_to_joints(self, positions: np.ndarray,
velocity: float = 0.5) -> bool:
"""Move to joint positions. Returns True on success."""
...
@abstractmethod
def stop(self) -> None:
"""Immediately stop all motion"""
...
@property
@abstractmethod
def joint_limits(self) -> List[tuple]:
"""Returns [(min, max)] for each joint"""
...
class CameraInterface(ABC):
"""Abstract camera — any RGB camera must honor this"""
@abstractmethod
def capture(self) -> np.ndarray:
"""Returns (H, W, 3) uint8 RGB image"""
...
@abstractmethod
def get_intrinsics(self) -> CameraIntrinsics:
"""Returns camera intrinsic parameters"""
...
@property
@abstractmethod
def resolution(self) -> tuple:
"""Returns (width, height)"""
...
class GripperInterface(ABC):
@abstractmethod
def open(self, width: float = 1.0) -> bool: ...
@abstractmethod
def close(self, force: float = 0.5) -> bool: ...
@abstractmethod
def get_width(self) -> float: ...
@abstractmethod
def is_grasping(self) -> bool: ...
# ─── CONCRETE IMPLEMENTATIONS ────────────────────────────────
class UR5Arm(ArmInterface):
"""Real UR5 via RTDE protocol"""
def __init__(self, ip: str):
self.rtde = RTDEControl(ip)
self.rtde_receive = RTDEReceive(ip)
def get_joint_positions(self) -> np.ndarray:
return np.array(self.rtde_receive.getActualQ())
def move_to_joints(self, positions, velocity=0.5):
self.rtde.moveJ(positions.tolist(), velocity)
return True
def stop(self):
self.rtde.stopScript()
@property
def joint_limits(self):
return [(-2*np.pi, 2*np.pi)] * 6
class MuJoCoArm(ArmInterface):
"""Simulated arm in MuJoCo — SAME interface"""
def __init__(self, model_path: str, joint_names: List[str]):
self.model = mujoco.MjModel.from_xml_path(model_path)
self.data = mujoco.MjData(self.model)
self.joint_ids = [mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, n)
for n in joint_names]
def get_joint_positions(self) -> np.ndarray:
return np.array([self.data.qpos[jid] for jid in self.joint_ids])
def move_to_joints(self, positions, velocity=0.5):
# Simulate motion with position control
self.data.ctrl[:len(positions)] = positions
for _ in range(100):
mujoco.mj_step(self.model, self.data)
return True
def stop(self):
self.data.ctrl[:] = 0
# ─── HIGH-LEVEL CODE DEPENDS ONLY ON ABSTRACTIONS ────────────
class PickPlaceTask:
"""This class works with ANY arm + gripper + camera.
It never knows or cares if it's sim or real."""
def __init__(self, arm: ArmInterface, gripper: GripperInterface,
camera: CameraInterface, detector: DetectorInterface):
self.arm = arm
self.gripper = gripper
self.camera = camera
self.detector = detector
def execute(self, target_class: str) -> bool:
image = self.camera.capture()
detections = self.detector.detect(image)
target = next((d for d in detections if d.label == target_class), None)
if target is None:
return False
self.arm.move_to_joints(self.ik(target.pose))
self.gripper.close()
self.arm.move_to_joints(self.place_joints)
self.gripper.open()
return True
The Dependency Rule in Robotics:
Application / Tasks
↓ depends on
Interfaces (ABC)
↑ implements
Hardware Drivers / Simulators
Arrows point inward. High-level policy never imports low-level drivers.
Principle 3: Open-Closed — Extend Without Modifying
Modules should be open for extension but closed for modification. Add new capabilities by adding new code, not changing existing code.
Why it matters in robotics: You constantly add new sensors, new robots, new tasks. If adding a new camera requires modifying your perception pipeline, you'll break existing deployments.
# ❌ BAD: Adding a new sensor requires modifying existing code
class PerceptionPipeline:
def process(self, sensor_type: str, data):
if sensor_type == 'realsense':
return self._process_realsense(data)
elif sensor_type == 'zed':
return self._process_zed(data)
elif sensor_type == 'oakd': # New sensor = modify this class
return self._process_oakd(data)
# ✅ GOOD: Plugin architecture — add sensors without touching core
class SensorPlugin(ABC):
"""Base class for all sensor plugins"""
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def process(self, raw_data) -> ProcessedData: ...
@abstractmethod
def get_intrinsics(self) -> dict: ...
class RealSensePlugin(SensorPlugin):
def name(self): return 'realsense'
def process(self, raw_data):
# RealSense-specific processing
return ProcessedData(...)
class ZEDPlugin(SensorPlugin):
def name(self): return 'zed'
def process(self, raw_data):
# ZED-specific processing
return ProcessedData(...)
# Core pipeline never changes when you add sensors
class PerceptionPipeline:
def __init__(self):
self._plugins: dict[str, SensorPlugin] = {}
def register_sensor(self, plugin: SensorPlugin):
"""Extend the pipeline without modifying it"""
self._plugins[plugin.name()] = plugin
def process(self, sensor_name: str, data):
if sensor_name not in self._plugins:
raise ValueError(f"Unknown sensor: {sensor_name}")
return self._plugins[sensor_name].process(data)
# Adding OAK-D = add a file, register at startup. Zero changes to core.
class OAKDPlugin(SensorPlugin):
def name(self): return 'oakd'
def process(self, raw_data):
return ProcessedData(...)
pipeline = PerceptionPipeline()
pipeline.register_sensor(RealSensePlugin())
pipeline.register_sensor(OAKDPlugin()) # No core code changed
Principle 4: Interface Segregation — Small, Focused Interfaces
Don't force modules to depend on interfaces they don't use. Many small interfaces beat one large one.
Why it matters in robotics: A simple 1-DOF gripper shouldn't implement a 6-DOF dexterous hand interface. A fixed camera shouldn't implement pan-tilt methods.
# ❌ BAD: Fat interface — every camera must implement ALL of these
class CameraInterface(ABC):
@abstractmethod
def capture_rgb(self) -> np.ndarray: ...
@abstractmethod
def capture_depth(self) -> np.ndarray: ...
@abstractmethod
def capture_pointcloud(self) -> np.ndarray: ...
@abstractmethod
def set_exposure(self, value: float): ...
@abstractmethod
def set_pan_tilt(self, pan: float, tilt: float): ...
@abstractmethod
def stream_video(self) -> Iterator[np.ndarray]: ...
# A simple USB webcam can't do half of these!
# ✅ GOOD: Segregated interfaces — implement only what you support
class RGBCamera(ABC):
"""Any camera that produces RGB images"""
@abstractmethod
def capture_rgb(self) -> np.ndarray: ...
@property
@abstractmethod
def resolution(self) -> tuple: ...
class DepthCamera(ABC):
"""Cameras that also produce depth"""
@abstractmethod
def capture_depth(self) -> np.ndarray: ...
@abstractmethod
def get_depth_intrinsics(self) -> DepthIntrinsics: ...
class ControllableCamera(ABC):
"""Cameras with adjustable settings"""
@abstractmethod
def set_exposure(self, value: float): ...
@abstractmethod
def set_white_balance(self, value: float): ...
class PTZCamera(ABC):
"""Pan-tilt-zoom cameras"""
@abstractmethod
def set_pan_tilt(self, pan: float, tilt: float): ...
@abstractmethod
def set_zoom(self, level: float): ...
# A RealSense implements RGB + Depth, but not PTZ
class RealSenseD435(RGBCamera, DepthCamera, ControllableCamera):
def capture_rgb(self): ...
def capture_depth(self): ...
def set_exposure(self, value): ...
# No PTZ methods — it's not a PTZ camera!
# A webcam implements only RGB
class USBWebcam(RGBCamera):
def capture_rgb(self): ...
# Nothing else required
# Perception code that only needs RGB doesn't pull in depth dependencies
class ObjectDetector:
def __init__(self, camera: RGBCamera): # Only needs RGB
self.camera = camera
def detect(self) -> List[Detection]:
image = self.camera.capture_rgb()
return self.model.predict(image)
Principle 5: Liskov Substitution — Replaceable Implementations
Any implementation of an interface must be substitutable without the caller knowing. If your code works with ArmInterface, it must work with ANY arm that implements it.
Why it matters in robotics: Sim-to-real transfer, hardware swaps, and multi-robot support all depend on this.
# ❌ BAD: Violates substitution — caller must know the implementation
class FrankaArm(ArmInterface):
def move_to_joints(self, positions, velocity=0.5):
if len(positions) != 7:
raise ValueError("Franka has 7 joints!") # Franka-specific
# ...
class UR5Arm(ArmInterface):
def move_to_joints(self, positions, velocity=0.5):
if len(positions) != 6:
raise ValueError("UR5 has 6 joints!") # UR5-specific
# ...
# Caller must know which arm it's using to pass correct joint count!
# This breaks substitutability.
# ✅ GOOD: Self-describing implementations
class ArmInterface(ABC):
@property
@abstractmethod
def num_joints(self) -> int: ...
@property
@abstractmethod
def joint_limits(self) -> List[tuple]: ...
@abstractmethod
def move_to_joints(self, positions: np.ndarray, velocity: float = 0.5) -> bool:
"""Positions must have length == self.num_joints"""
...
class FrankaArm(ArmInterface):
@property
def num_joints(self): return 7
def move_to_joints(self, positions, velocity=0.5):
assert len(positions) == self.num_joints
# ...
# Caller code is generic — works with any arm
def move_to_home(arm: ArmInterface):
home = np.zeros(arm.num_joints) # Queries the arm, doesn't assume
arm.move_to_joints(home)
Substitution test: Take every line of caller code. Replace UR5 with Franka with SimArm. Does it still work? If not, your abstraction leaks.
Principle 6: Separation of Rates — Respect Timing Boundaries
Different subsystems run at different rates. Never couple them.
Component Typical Rate Criticality
─────────────────────────────────────────────────
Safety monitor 1000 Hz HARD real-time
Joint controller 500-1000 Hz HARD real-time
Trajectory exec 100-200 Hz Firm real-time
State estimation 50-200 Hz Firm real-time
Perception 10-30 Hz Soft real-time
Planning 1-10 Hz Best effort
Task management 0.1-1 Hz Best effort
Logging 1-30 Hz Best effort
UI/Dashboard 1-10 Hz Best effort
# ❌ BAD: Perception blocks the control loop
class Robot:
def control_loop(self): # Must run at 100Hz = 10ms budget
image = self.camera.capture() # 5ms
objects = self.detector.detect(image) # 200ms ← BLOCKS!
pose = self.estimate_pose(objects) # 2ms
cmd = self.controller.compute(pose) # 0.1ms
self.arm.send_command(cmd) # 0.5ms
# Total: 207ms. Control runs at 5Hz instead of 100Hz!
# ✅ GOOD: Decoupled rates with async boundaries
class Robot:
def __init__(self):
self.latest_detections = []
self.detection_lock = threading.Lock()
# Perception runs in its own thread at its own rate
self.perception_thread = threading.Thread(
target=self._perception_loop, daemon=True)
self.perception_thread.start()
def _perception_loop(self):
"""Runs at ~10
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [arpitg1304](https://github.com/arpitg1304)
- **Source:** [arpitg1304/robotics-agent-skills](https://github.com/arpitg1304/robotics-agent-skills)
- **License:** Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.