# SO-101: Kicking off a new series

Back in my ["6 months in"](https://kamathrobotics.com/kamath-robotics-6-months-in) post, I mentioned an [SO-101](https://github.com/TheRobotStudio/SO-ARM100/blob/main/README.md) arm was next on my list - it's finally here, and a lot's happened since: I got a wrist camera into the URDF, turned a second arm into a lamp, and convinced myself to buy a new bipedal robot along the way. It also kicks off a new series, much like [LeKiwi](https://kamathrobotics.com/series/lekiwi), where once again, I'll be using [ROS 2](https://docs.ros.org/en/rolling/) and migrating the SO-101's software stack entirely off [LeRobot](https://huggingface.co/lerobot), eventually working toward some Physical AI experiments. This post, however, is just about the hardware - the software side starts next.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/6bd78a12-d49a-4da2-a5cb-5c19435d30da.jpg align="center")

## Building the SO-101

I had only built [SO-100](https://github.com/TheRobotStudio/SO-ARM100/blob/main/SO100.md)s before, so I went into this one expecting a familiar afternoon of fighting 3D-printed parts to get them to fit. Instead, it was noticeably easier - about 15 minutes, start to finish.

This wasn't just because I was familiar with the assembly - the design itself is more refined. There are fewer printed parts, the tolerances are better, and the fit and finish feel more polished out of the box. The cabling is simpler too: no more squeezing wires between printed parts, since everything now routes through dedicated channels built into the structure.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/db007db4-e757-40b2-953c-d6a353677791.jpg align="center")

I have one complaint though: on the SO-100, the shoulder pan joint sits on a spare servo horn acting as a bearing of sorts; on the SO-101, it's just bolted to the pan motor and cantilevers freely - a bit of stability traded for easier assembly.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/cdf039ad-680c-47be-990b-a7ebd56693a2.png align="center")

Not that I'm complaining; the SO-101 is genuinely a breeze to build. Kudos to [The Robot Studio](https://github.com/TheRobotStudio) for an elegant redesign.

### Updating the URDF

The SO-101's gripper has spaces for two hex nuts on the fixed jaw to mount accessories such as a wrist camera, with CAD files included in the [SO-ARM100 repository](https://github.com/TheRobotStudio/SO-ARM100). A wrist camera is super essential for most real applications of this arm, so it's a shame it's missing from the default URDF. I decided to fix that.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/46d8ec2f-9570-41f6-ac2a-f01ff114fab4.png align="center")

Getting the camera and mount into the URDF early mattered because I'm already planning self-collision checking down the line - whatever checker I build later depends on the robot's geometry from the URDF, so it needs to know that the wrist mount and camera exist from day one.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/79ee1099-078d-4bc3-9b0e-90bc1725266b.jpg align="center")

The [mount mesh](https://github.com/TheRobotStudio/SO-ARM100/tree/main/Optional/SO101_Wrist_Cam_Hex-Nut_Mount_32x32_UVC_Module) itself is straight from the SO-ARM100 repository - I didn't design it, just wired it into [`so101.urdf.xacro`](https://github.com/adityakamath/so_arm_ros2/blob/main/so_arm_description/urdf/so101/so101.urdf.xacro) as a chain of fixed-joint links (`wrist_camera_mount_link` → `wrist_camera_body_link` → `wrist_camera_frame` → `wrist_camera_optical_frame`), ending in a proper [REP-103](https://www.ros.org/reps/rep-0103.html) optical frame. SO-ARM100 doesn't ship a camera mesh, so I found one matching the camera I'm actually using - an [Innomaker U20CAM-1080P](https://www.amazon.nl/-/en/InnoMaker-Raspberry-Supports-Windows-Android/dp/B0CNCSFQC1/), a plain UVC webcam - and added it as its own link next to the mount.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/6e097dc3-909d-43c2-8849-0e9e752b10ee.png align="center")

Now, I understand that not everyone wants a wrist camera, so the whole chain sits behind a `wrist_camera_urdf` xacro argument, on by default. Turn it off and the mount and camera links just aren't there - the self-collision checker only ever sees geometry that's actually part of the arm.

If you want to use this URDF, I've generated updated URDF and MuJoCo XML files for the SO-101 with the wrist camera integrated, and opened a PR [here](https://github.com/TheRobotStudio/SO-ARM100/pull/176) on the SO-ARM100 repository. The configurable Xacro files with the `wrist_camera_urdf` gating live in the [`so_arm_description`](https://github.com/adityakamath/so_arm_ros2/tree/main/so_arm_description) package in the [`so_arm_ros2`](https://github.com/adityakamath/so_arm_ros2) repository.

### Custom Wrist Camera Node

The obvious choice for a UVC camera on ROS 2 is [`usb_cam`](https://github.com/ros-drivers/usb_cam) or [`v4l2_camera`](https://gitlab.com/boldhearts/ros2_v4l2_camera), both common ROS 2 packages, and either would have worked on Linux. But I want to ensure that `so_arm_ros2`'s ROS 2 stack runs equally on my Raspberry Pi and my MacBook via [Pixi](https://pixi.sh) and [RoboStack](https://robostack.github.io/), and neither of those Linux-only packages builds for Pixi's `osx-arm64` target. I didn't want the wrist camera to be the one feature that only worked on the Pi, so I wrote a small generic driver instead: [`opencv_camera_node`](https://github.com/adityakamath/so_arm_ros2/blob/main/so_arm_bringup/so_arm_bringup/opencv_camera_node.py), built on `cv2.VideoCapture` rather than V4L2 directly. It picks the right backend per platform - V4L2 on Linux, AVFoundation on macOS - and otherwise doesn't care which one it's running on.

This custom node's design follows LeRobot's own [`OpenCVCamera`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/cameras/opencv/camera_opencv.py): a daemon thread owns the blocking `cv2.VideoCapture.read()` call, and the non-blocking ROS publish timer just reads whatever frame is ready. That way a slow camera or a dropped frame never holds up the rest of the ROS graph. Another change compared to the standard ROS camera stack: neither `image_transport` nor `camera_info_manager` has Python bindings in this ROS 2 install, so rather than linking the C++ libraries from Python, the node publishes raw `Image` messages on `image_raw` directly and parses `CameraInfo` straight out of a standard `camera_calibration`\-format YAML file itself.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/7373b61f-27d5-4b2d-9b3c-0eb86387d706.png align="center")

I tested it on my MacBook via Pixi, and it worked as expected. Still need to calibrate the camera and populate the real intrinsics YAML file, but the basic pipeline runs fine with a dummy calibration file for now.

## SO-Lamp

I've been working with [STS3215](https://www.feetechrc.com/2020-05-13_56655.html) motors long enough to have amassed a fair amount of them, so after finishing the SO-101, I built a second one - this time with a lamp where the gripper would go. I had a spare [IKEA Strala](https://www.ikea.com/nl/nl/p/strala-snoerset-wit-20366404/) lamp cord set lying around, and it seemed like a perfect fit.

I kept it simple: 4-dof, with the SO-101's wrist redesigned into a lamp mount. An afternoon of CAD and printing later, the mount was ready. I paired it with a spare E14 bulb and a shade from a [3D-printable lamp design](https://makerworld.com/en/models/168157-industrial-strala?from=search#profileId-184590) I found online.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/d7aacb5f-0a68-441c-91a9-44951cc50969.jpg align="center")

For V2, I wanted it hooked into my home automation, so I ordered a [Tapo E14 smart bulb](https://nl.store.tapo.com/collections/slimme-verlichting/products/tapo-l430c-led-lamp2-pack), which is a bit bigger than the original, so I needed a lamp shade. I used [MakerLab](https://makerworld.com/en/makerlab)'s [customizable vase generator](https://makerworld.com/en/makerlab/makeMyVase?from=makerlab) to design one, cut the bottom out, and added threads to screw it straight onto the Strala cord set. I really liked how it turned out.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/3733dc40-01ee-4f9b-8378-32d06dea6769.jpg align="center")

My original goal was simple: I wanted a functional, actuated night lamp that I could control through my home automation, without unnecessary complexity. I briefly considered making it 5-dof, decided against it (despite people on Twitter telling me otherwise), and deliberately kept cameras and microphones off it, keeping the design focused solely on lighting and leaving out any AI features.

However, around this time I started seeing a surge of [DIY and commercial robot-arm lamps](https://x.com/irvinxyz/status/2088945113502257552) online (see [this survey](https://x.com/irvinxyz/status/2089198471290945584) by [@irvinxyz](https://x.com/irvinxyz) on Twitter), which got me rethinking my own build. I settled for the head design from the open-source [lamp](https://github.com/autonomous-ai/autonomous-os/tree/main/robots/lamp/hardware) (image below) from [Autonomous.ai](https://www.autonomous.ai/lamp) (which includes a 5th motor) and decided to adopt it, keeping the rest of the SO-101's 4-dof structure intact.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/d9bc255b-c5d9-4dcf-a169-e03c9b185f9e.webp align="center")

After some adjustments, the new lamp head was ready: I added M3 heat-set inserts and scaled down the original design. Mechanically, that's done, but the electronics are still catching up. The plan is to drop the Raspberry Pi for a [XIAO ESP32-S3 Sense](https://www.seeedstudio.com/XIAO-ESP32S3-Sense-p-5639.html) - [Seeed Studio](https://www.seeedstudio.com/)'s Sense module packs a mic, camera, and microSD slot, which covers most of what I wanted to add - but it hasn't arrived yet. I'll pair it with a [XIAO-compatible Bus Servo Driver](https://www.seeedstudio.com/Bus-Servo-Driver-Board-for-XIAO-p-6413.html) to power and control the STS3215 motors directly, which I'm also waiting on. For lighting, I'm swapping the E14 bulb for an off-the-shelf [LED ring](https://www.seeedstudio.com/Grove-RGB-LED-Ring-24-WS2813-Mini-p-4202.html) wired through the servo driver. Once it's all in, everything will live in the lamp head with just a single power cable running up to it. I love how the new head and 5-dof design look - the electronics and the 3D-printed cover are just going to take a bit longer.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/def84255-fb4c-4283-a90b-670acf8ae213.jpg align="center")

I still needed an actual night lamp good enough for reading in bed, so I put the now-freed-up E14 cord set and smart bulb into a [non-robotic 3D-printable lamp](https://makerworld.com/en/models/1751877-octopus-table-lamp-led-lamps-e14) I found online. Definitely the most beautiful lamp I've ever 3D-printed.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/743fd1c1-651c-4e66-aa86-da22226efc42.jpg align="center")

Back to the SO-Lamp: my favorite decision in this whole build was keeping it based on the SO-101 design. No new URDF to generate, no extra controller config beyond the lamp head changes. This makes life easier whenever I get around to simulating or controlling it in ROS 2.

## Microduck

While I was building the new lamp head, [Pollen Robotics](https://pollen-robotics.com) announced the [Microduck](https://pollen-robotics.com/microduck/) - a tiny bipedal robot that's compact, versatile, and genuinely cute, for just [$399](https://pollen-robotics.com/microduck/press-kit/). Between LeKiwi, the SO-101, and the SO-Lamp, I was pretty sure I didn't have time for another robot platform, so I almost skipped it.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/e6c3c906-ad36-4685-a33d-dd2602919bd9.jpg align="center")

A few things changed my mind. The Microduck runs on 15 [Dynamixel](https://www.robotis.us/dynamixel/) [XL330-M288-T](https://docs.robotis.com/docs/dxl/model_reference/x_series/xl_series/xl330-m288/) motors, which alone cost 30+ EUR each - around 450 EUR just in motors - making $399 a steal before anything else is even counted. It also packs a camera, a ToF sensor (likely the [VL53L5CX](https://www.st.com/en/imaging-and-photonics-solutions/vl53l5cx.html)), a [Rockchip RK3566](https://radxa.com/products/zeros/zero3w/) with an AI accelerator, two IMUs, a mic, and speakers - worth salvaging for parts alone if I ever get bored of the robot itself.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/fa8fe20e-b371-4cef-bab8-15d2c4af5d54.png align="center")

It's not just the hardware, either - it ships with a [simulation platform](https://huggingface.co/spaces/pollen-robotics/microduck-simulator), a [sim-to-real pipeline](https://github.com/pollen-robotics/microduck_rl), and [Rust-based firmware](https://github.com/pollen-robotics/microduck), which makes it a solid excuse to tinker with both reinforcement learning and Rust. While waiting for mine to arrive, I couldn't resist getting a head start: I took the meshes and MuJoCo files Pollen Robotics provided and put together a URDF and Xacro file for it, in case I ever want to bring the Microduck into ROS 2 down the line (even though I promised myself I wouldn't). It's up on [microduck\_description](https://github.com/adityakamath/microduck_description/) if you want to poke around.

![](https://cdn.hashnode.com/uploads/covers/63a0f156a59cad9bdbba046e/fb9f6b10-2734-4446-a82e-b49d3884108a.png align="center")

I'm glad I was sold on the Microduck and pre-ordered it quite fast - Pollen Robotics sold 10,000+ Microducks in the first few days, after which they dropped the pre-Christmas delivery promise and pushed the estimate out to 4-6 months. If all goes well, I should have my Microduck by the winter break. Fingers crossed.

## What's next

So around the holidays I'll be stretched thin between my current robots and the new Microduck. There's another new robot arriving around the same time (still WIP; more details to follow), so time management will matter. My goal is to finish the SO-Lamp before the MicroDuck shows up, then hand the hardware off to someone else (and the open-source community) to take it further.

For the SO-101, the wrist camera's optical frame math is done, but not calibrated - I want to calibrate it just like in my [earlier blog post](https://kamathrobotics.com/calibrating-a-monocular-camera-for-the-lekiwi-robot-using-ros-2). Beyond that, IK teleop and self-collision checking are the two things I keep talking about in this post, but that's its own story, coming soon.

Meanwhile, my UK visa has been approved, and I'm gearing up for my trip to ROSCon UK and the deep-dive session that I will be presenting there. I will be talking about the [`sts_hardware_interface`](https://github.com/adityakamath/sts_hardware_interface) and will use the LeKiwi and the SO-101 as examples.

Everything that I've talked about in this post lives in a new repository [`so_arm_ros2`](https://github.com/adityakamath/so_arm_ros2), which also includes a few things that I am yet to write about. As always, these projects are a work in progress and might change significantly over time. Still, feel free to poke around, provide feedback, or contribute if something catches your interest.
