← Home

BUILD LOG

THINGS I'VE SHIPPED

THINGS
I'VE BUILT

Rover systems · Video pipelines · Computer vision

[ PROJECT 001 ]

Planetary Rover

2025

C++ROS 2GStreamerH.265SRT / RTP

ROS 2 Video Streaming Node

Live video is the operator’s window into the rover. The problem was that every off-the-shelf streaming tool runs as its own process. You can’t start it, watch it, or change its settings through ROS 2, so the rest of our software had no control over the one feed the driver depends on.

So I wrote a C++ ROS 2 node that owns the GStreamer pipeline itself. The chain is simple: a capture source feeding x265enc, then a muxer or RTP payloader, then srtsink for SRT or udpsink for RTP. I went with H.265 because our wireless link is tight and shared with telemetry. At the same visual quality it needs roughly 40 to 50 percent less bitrate than H.264, and that margin is the difference between a usable feed and a frozen one. SRT handles long distance and flaky links. RTP is for low latency when we’re close.

The node exposes services to start, stop, and reconfigure the stream, and publishes status topics. Video became a normal part of the stack instead of a fragile side process.

FIG. 01 · Camera → GStreamer pipeline inside the ROS 2 node → SRT/RTP → ground station. Pulse: the H.265 stream leaving the pipeline

[ PROJECT 002 ]

Planetary Rover

2025

I-frame recoveryExponential backoffDynamic bitrate

Adaptive Network Resilience

When the rover moves, the wireless link drops packets. Each lost packet breaks the prediction chain between frames, and the driver’s screen either freezes or turns into blocks until the next keyframe shows up. I wanted that gap closed automatically.

I built a small control loop around the link. A monitor watches the SRT statistics and raises a loss event when loss crosses a threshold. Three things respond. The receiver asks for an I-frame so the decoder can resync right away. A backoff timer spaces those requests out, doubling the wait each time loss keeps coming. And a bitrate controller turns the encoder down. The backoff matters more than it sounds. I-frames are several times bigger than normal frames, and if every loss event fired a fresh request, the recovery traffic itself would choke a link that was already struggling.

Bitrate drops fast when loss rises and climbs back slowly once the monitor sees a clean stretch. The stream settles at whatever the link can actually carry at that moment.

FIG. 02 · Closed feedback ring. Pulse: one full circuit per beat, quickening as it closes back on the monitor

[ PROJECT 003 ]

Planetary Rover

2025

C++ThreadingSerial I/ORTCM

GPS RTCM Serial Refactor

Our GPS correction data (RTCM) came in through a busy-wait loop. The main loop kept polling the serial port whether or not any bytes were waiting, and it stalled whenever a read blocked. On the rover’s small onboard computer that spinning ate CPU the other processes needed, and their timing got jittery.

I moved the serial I/O into its own thread. That thread does a blocking read, so it sleeps in the kernel until data arrives and costs nothing while idle. It parses the RTCM frames and pushes finished messages into a thread-safe queue. The main thread pops from that queue when it wants to and never touches the port.

CPU load went down once the polling was gone, and the subsystems sharing the processor ran on steadier timing. Small change, big difference in how the whole rover behaved.

FIG. 03 · Before: one thread spinning on the port. After: blocking I/O thread + queue hand-off. Pulse: the queue only

[ PROJECT 004 ]

Planetary Rover

2025

PythonSRT stats APIROS 2

Real-Time SRT Telemetry Pipeline

SRT keeps good statistics about the link: round-trip time, estimated bandwidth, and packet-loss counters. They just sit inside the socket unless something pulls them out, and during a mission nobody has time to go looking.

I wrote a Python service that polls the socket’s stats API on a fixed interval. Each tick reads the counters, diffs them against the last sample to get rates, works out the loss percentage, and packages RTT, bandwidth, and loss into one timestamped message. That message goes out on a ROS 2 topic, so any node on the rover or any dashboard at the ground station can subscribe.

The value is in the timing. Loss usually starts climbing a few seconds before the picture visibly degrades. Seeing it move gives the operator time to slow down, drop bitrate, or reposition before the feed is gone.

FIG. 04 · Live diagnostics as three engraved dials. Only Packet Loss lights on the beat

[ PROJECT 005 ]

Planetary Rover

2025

WebRTCVideo compositingMulti-camera

Dual-Output Camera + WebRTC Mosaic

One video feed can’t serve everyone. The driver needs a single low-latency view with nothing competing for bandwidth, because every extra frame of delay shows up as overshoot in the controls. Everyone else wants to see all the cameras at once and doesn’t mind a bit of lag.

I designed the pipeline to capture and encode each camera once, then split after that. Path A is the operator stream, straight to a low-latency transport. Path B goes into a mosaic compositor that tiles however many feeds are live into a grid. The grid size comes from the feed count: columns are the ceiling of the square root of N, rows are N divided by columns rounded up. Two feeds give 2 by 1, four give 2 by 2, five give 3 by 2 with one empty tile. It re-tiles on the fly as cameras join or drop.

The mosaic goes out over WebRTC for two reasons. ICE handles NAT traversal, so remote viewers connect without any network setup, and browsers play it natively. Anyone with the link can watch every camera at once. No plugin, no client install.

FIG. 05 · One capture stage, two outputs. Pulse: born at the fork, runs both arms at once

[ PROJECT 006 ]

Planetary Rover

2025

Phoenix APIMotor controlManipulator

Phoenix Current Control: Grip Path

A motor controller can regulate position, velocity, or current. Current is the interesting one for a gripper, because torque is proportional to current, so commanding current is really commanding force. A gripper under position or velocity control keeps driving after it touches something. It either crushes the sample or stalls the motor and cooks the windings.

Our Phoenix controller logic on the grip path wasn’t handling current mode properly. The mode was selected but the closed-loop configuration and limits never got applied, so the gripper wasn’t actually force-limited. I fixed the setup order so the current-mode gains and peak limit are configured before the path is enabled, and made sure setpoints go out in the right units.

I validated it on the bench with current-draw measurements: close the gripper on an instrumented block, log commanded versus measured current, and check that the measured current rises to the limit and flattens there instead of spiking past it.

FIG. 06 · Control modes, and the current curve. Pulse: climbs the ribbon, is clamped flat at the limit plane

[ PROJECT 007 ]

Rover Software Team

2025

GitHub ActionsDockerCI/CD

Automated Test & Build Pipeline

Environment drift is the quiet killer on a student robotics team. A dozen laptops and the rover’s onboard computer each end up with slightly different ROS 2 patch versions, compilers, and system packages, and eventually code that builds on one machine fails on another for no obvious reason.

I containerized the build with Docker and wired it into GitHub Actions. The image pins everything that used to drift: the ROS 2 distribution, the C++ toolchain, and every system and Python dependency. The same image runs on contributors’ machines, in CI, and on the rover. Every push and pull request spins it up and runs the build, then the unit tests, then lint. Any failure blocks the merge and pings the author.

For a small team the payoff is very practical. The “works on my machine” arguments mostly went away, regressions get caught before they reach the rover, and onboarding a new member is now install Docker and pull one image.

FIG. 07 · Push → containerized build → tests → lint → pass gate. Pulse: the successful run, every time

[ PROJECT 008 ]

AI / ML · Computer Vision

2025

PythonFlaskYOLOv8MediaPipeSQLite3Three.js

PoseSync 3D

PoseSync 3D turns a raw video stream into a live 3D scene in the browser. Frames come in through a Flask ingest service and go to inference, where two models run together. YOLOv8 finds and boxes the people and objects in the frame. MediaPipe runs on each person crop and returns the fine-grained pose landmarks that YOLOv8 doesn’t give you. One answers where the subjects are, the other answers how each body is articulated, and I merge them into a single detection record per frame.

Inference is heavy, so it can’t sit on the ingest path. I put an asynchronous priority task queue between them. Frames keep arriving at full rate, live work goes to the front, batch jobs go to the back, and the ingest loop never blocks. Results land in SQLite3 under a normalized schema: sessions, frames, detections, and per-joint coordinates in separate related tables instead of one wide blob, indexed so range and per-entity queries stay fast as the dataset grows.

The Three.js frontend subscribes to new results and updates the scene in place. It moves existing joints instead of rebuilding anything, which is what keeps the 3D view running at interactive framerates with low latency.

FIG. 08 · Ingest → YOLOv8 + MediaPipe → priority queue → SQLite → live viewer. Pulse: the hand-off that keeps compute off the live path

[ END OF LOG · MORE UNDER CONSTRUCTION ]