<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged robotics</title>
<link>https://coderlegion.com/tag/robotics</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building aegis-filter: Why Standard Kalman Filters Fail in Adversarial Environments</title>
<link>https://coderlegion.com/30401/building-aegis-filter-why-standard-kalman-filters-fail-in-adversarial-environments</link>
<description>&lt;p&gt;When building telemetry filters for autonomous drones or robotics, the textbook recommendation is always the same: &lt;em&gt;“Just slap a Kalman Filter on it.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In theory, the linear Kalman filter is mathematically optimal under Gaussian white noise. In the real world, however, physical sensors do not live in textbook conditions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Adversarial GPS/Baro Spoofing:&lt;/strong&gt; A rogue transmitter or RF jammer injects sudden, massive delta jumps (e.g., +150 meters in a single millisecond).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Numerical Covariance Degeneracy:&lt;/strong&gt; Floating-point round-off errors over millions of iterations cause the error covariance matrix $P$ to lose its positive-definiteness ($P \not\succ 0$), producing negative variances and exploding state estimates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Severe Non-Linearities:&lt;/strong&gt; Radar range calculations ($r = \sqrt{x^2 + y^2}$) and orientation quaternions break linear assumptions; Extended Kalman Filters (EKF) require calculating brittle analytical Jacobian matrices that drift rapidly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To solve these mission-critical vulnerabilities, we built and open-sourced &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://crates.io/crates/aegis-filter&quot;&gt;&lt;code&gt;aegis-filter&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; under &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Exploidus&quot;&gt;Exploidus&lt;/a&gt;&lt;/strong&gt;—a cyber-resilient, &lt;code&gt;#![no_std]&lt;/code&gt;, hard real-time estimation engine written from scratch in pure Rust.&lt;/p&gt;
&lt;p&gt;Here is the engineering breakdown of how it works and what the simulation numbers prove.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-core-architecture&quot;&gt;1. The Core Architecture&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;aegis-filter&lt;/code&gt; is engineered with three core defenses:&lt;/p&gt;
&lt;h4 id=&quot;a-joseph-stabilized-covariance-update&quot;&gt;A. Joseph-Stabilized Covariance Update&lt;/h4&gt;
&lt;p&gt;Standard Kalman filters update covariance using $P_k = (I - K_k H&lt;em&gt;k) P&lt;/em&gt;{k|k-1}$. In finite-precision IEEE 754 floating-point arithmetic, subtraction can yield asymmetric, non-positive-definite matrices. &lt;/p&gt;
&lt;p&gt;&lt;code&gt;aegis-filter&lt;/code&gt; enforces the &lt;strong&gt;Joseph Stabilized Form&lt;/strong&gt;:&lt;br&gt;
\[P_k = (I - K_k H_k) P_{k|k-1} (I - K_k H_k)^T + K_k R_k K_k^T\]&lt;/p&gt;
&lt;p&gt;Because both terms are quadratic forms ($A P A^T + B R B^T$), positive-definiteness is mathematically guaranteed across infinite operational lifetimes.&lt;/p&gt;
&lt;h4 id=&quot;b-chi-square-3-sigma-outlier-rejection-gating&quot;&gt;B. Chi-Square / 3-Sigma Outlier Rejection Gating&lt;/h4&gt;
&lt;p&gt;Before any raw measurement is allowed to alter the state vector, the innovation $\tilde{y}_k = z_k - H&lt;em&gt;k \hat{x}&lt;/em&gt;{k|k-1}$ is evaluated against the innovation covariance $S_k = H&lt;em&gt;k P&lt;/em&gt;{k|k-1} H_k^T + R_k$:&lt;br&gt;
\[\text{Reject if } |\tilde{y}_k| &amp;gt; \gamma \sqrt{S_k}\]&lt;/p&gt;
&lt;p&gt;If an adversary transmits a spoofed altitude or an IMU glitches, the reading is gated at line-rate. The filter falls back safely to its physical state propagation without corrupting the state.&lt;/p&gt;
&lt;h4 id=&quot;c-unscented-kalman-filtering-ukf-amp-h-infty-robust-minimax&quot;&gt;C. Unscented Kalman Filtering (UKF) &amp;amp; $H_\infty$ Robust Minimax&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UKF (Merwe Scaled Sigma Points):&lt;/strong&gt; Replaces brittle Jacobian derivations by propagating $2n + 1$ deterministic sigma points through non-linear observation closures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$H_\infty$ Minimax:&lt;/strong&gt; Optimizes state estimation against the &lt;em&gt;worst-case&lt;/em&gt; disturbance rather than assuming well-behaved Gaussian noise.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-flight-telemetry-amp-spoofing-resilience-simulation&quot;&gt;2. Flight Telemetry &amp;amp; Spoofing Resilience Simulation&lt;/h3&gt;
&lt;p&gt;To prove real-world resilience, we simulated a quadcopter ascending at $1.5\text{ m/s}$ subjected to atmospheric turbulence, normal sensor noise, and deliberate adversarial spoofing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Step 12:&lt;/strong&gt; Instantaneous $+150\text{m}$ GPS spoofing attack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step 22:&lt;/strong&gt; Severe $-80\text{m}$ sensor hardware dropout glitch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is the telemetry log produced by running &lt;code&gt;cargo run --example flight_telemetry&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;===============================================================================
  AEGIS-FILTER :: FLIGHT TELEMETRY &amp;amp; ADVERSARIAL RESILIENCE SIMULATION
  Developed under Exploidus (https://github.com/Exploidus)
===============================================================================

Step   | True Alt (m) | Sensor Raw (m) | Aegis Est (m)  | Abs Error    | Status    
-------+--------------+----------------+----------------+--------------+-----------
1      | 1.50         | 1.79           | 1.75           | 0.254        | VALID     
2      | 3.00         | 3.22           | 3.24           | 0.238        | VALID     
3      | 4.50         | 3.46           | 4.31           | 0.187        | VALID     
4      | 6.00         | 6.74           | 6.06           | 0.057        | VALID     
5      | 7.50         | 7.67           | 7.58           | 0.081        | VALID     
6      | 9.00         | 8.90           | 9.04           | 0.045        | VALID     
7      | 10.50        | 9.68           | 10.39          | 0.113        | VALID     
8      | 12.00        | 12.98          | 12.08          | 0.075        | VALID     
9      | 13.50        | 13.41          | 13.55          | 0.047        | VALID     
10     | 15.00        | 14.69          | 14.99          | 0.009        | VALID     
11     | 16.50        | 16.04          | 16.42          | 0.080        | VALID     
12     | 18.00        | 168.00         | 17.92          | 0.080        | SPOOF REJECT
13     | 19.50        | 19.08          | 19.36          | 0.138        | VALID     
14     | 21.00        | 20.65          | 20.83          | 0.172        | VALID     
15     | 22.50        | 22.43          | 22.34          | 0.156        | VALID     
16     | 24.00        | 24.93          | 24.01          | 0.012        | VALID     
17     | 25.50        | 24.79          | 25.40          | 0.099        | VALID     
18     | 27.00        | 26.78          | 26.88          | 0.116        | VALID     
19     | 28.50        | 28.76          | 28.44          | 0.060        | VALID     
20     | 30.00        | 30.64          | 30.04          | 0.044        | VALID     
21     | 31.50        | 30.61          | 31.41          | 0.095        | VALID     
22     | 33.00        | -47.00         | 32.91          | 0.095        | SPOOF REJECT
23     | 34.50        | 34.96          | 34.50          | 0.003        | VALID     
24     | 36.00        | 36.25          | 36.04          | 0.036        | VALID     
25     | 37.50        | 36.60          | 37.39          | 0.110        | VALID     
26     | 39.00        | 39.34          | 38.96          | 0.041        | VALID     
27     | 40.50        | 40.99          | 40.54          | 0.039        | VALID     
28     | 42.00        | 41.84          | 42.01          | 0.009        | VALID     
29     | 43.50        | 42.78          | 43.40          | 0.100        | VALID     
30     | 45.00        | 45.60          | 45.00          | 0.003        | VALID     

-------------------------------------------------------------------------------
  SIMULATION SUMMARY:
  - Total Steps Processed      : 30
  - Valid Readings Fused       : 28
  - Adversarial Spikes Blocked : 2 (100% Gated)
  - Final Variance Confidence  : 0.1185
===============================================================================
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaways-from-the-data-&quot;&gt;Key Takeaways from the Data:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Millimeter Tracking Precision:&lt;/strong&gt; Under normal noisy conditions (Step 10, Step 30), estimation error dropped to &lt;strong&gt;0.003–0.009 meters (3–9 mm)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flawless Spoof Rejection:&lt;/strong&gt; When the raw altitude leaped to $168\text{m}$ at Step 12, the filter isolated the anomaly, kept the aircraft altitude at $17.92\text{m}$ (error: $0.080\text{m}$), and prevented a fatal motor cut.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-quick-start-rust-&quot;&gt;3. Quick Start (Rust)&lt;/h3&gt;
&lt;p&gt;Add &lt;code&gt;aegis-filter&lt;/code&gt; to your &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[dependencies]
aegis-filter = &quot;0.1.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For bare-metal Cortex-M/RISC-V embedded targets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[dependencies]
aegis-filter = { version = &quot;0.1.0&quot;, default-features = false }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;1d-outlier-gated-fusion-example-&quot;&gt;1D Outlier-Gated Fusion Example:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use aegis_filter::{ScalarKalmanFilter, FilterError};

fn main() -&amp;gt; Result&amp;lt;(), FilterError&amp;gt; {
    // Initial state: 0.0m, Initial variance: 5.0, Process noise: 0.02, Measurement noise: 0.8
    let mut filter = ScalarKalmanFilter::new(0.0, 5.0, 0.02, 0.8)?;

    let sensor_stream = [1.5, 3.1, 4.4, 168.0 /* Spoofed spike */, 6.0];

    for &amp;amp;raw_reading in &amp;amp;sensor_stream {
        filter.predict(1.5)?; // Predict forward by 1.5m climb

        match filter.update(raw_reading, Some(3.0)) { // 3-sigma gate
            Ok(estimated_alt) =&amp;gt; println!(&quot;Estimated Altitude: {:.2}m&quot;, estimated_alt),
            Err(FilterError::MeasurementGated) =&amp;gt; {
                println!(&quot;Spoofed sensor reading ({:.1}m) rejected! Holding state.&quot;, raw_reading);
            }
            Err(e) =&amp;gt; return Err(e),
        }
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-open-source-links&quot;&gt;4. Open Source Links&lt;/h3&gt;
&lt;p&gt;The engine is open-source under dual MIT/Apache-2.0 licenses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Crates.io:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://crates.io/crates/aegis-filter&quot;&gt;crates.io/crates/aegis-filter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Exploidus/aegis-filter&quot;&gt;Exploidus/aegis-filter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.rs/aegis-filter&quot;&gt;docs.rs/aegis-filter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Feedback, issues, and PRs from control engineers and systems developers are warmly welcome!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30401/building-aegis-filter-why-standard-kalman-filters-fail-in-adversarial-environments</guid>
<pubDate>Fri, 09 Oct 2026 05:45:50 +0000</pubDate>
</item>
<item>
<title># MyZubster Overnight Update: Metaverse Safety and a Live Space Station</title>
<link>https://coderlegion.com/26181/myzubster-overnight-update-metaverse-safety-and-a-live-space-station</link>
<description>&lt;h2 id=&quot;myzubster-overnight-update-metaverse-safety-and-a-live-space-station&quot;&gt;MyZubster Overnight Update: Metaverse Safety and a Live Space Station&lt;/h2&gt;
&lt;p&gt;Last night we shipped two important improvements to the MyZubster open-source ecosystem: a formal safety baseline for our Metaverse and a public connection to the MyZubster Space Station.&lt;/p&gt;
&lt;h3 id=&quot;neon-plaza-is-becoming-a-real-shared-environment&quot;&gt;Neon Plaza is becoming a real shared environment&lt;/h3&gt;
&lt;p&gt;The MyZubster Metaverse is available here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Neon Plaza already supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shared MongoDB-backed presence&lt;/li&gt;
&lt;li&gt;Keyboard and touch movement&lt;/li&gt;
&lt;li&gt;Public chat and emotes&lt;/li&gt;
&lt;li&gt;Proximity awareness&lt;/li&gt;
&lt;li&gt;Guest and account-linked characters&lt;/li&gt;
&lt;li&gt;Portals to Marketplace, LIFE, Identity, Visual and Zorgax&lt;/li&gt;
&lt;li&gt;Session and browser capability dashboards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The current synchronization model uses serverless-compatible shared polling. WebSockets, 3D/WebXR rendering and spatial voice remain part of the future roadmap.&lt;/p&gt;
&lt;h3 id=&quot;privacy-and-abuse-control-baseline&quot;&gt;Privacy and abuse-control baseline&lt;/h3&gt;
&lt;p&gt;We merged &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/920&quot;&gt;PR #920&lt;/a&gt;, which documents the verified privacy and retention boundaries of Neon Plaza.&lt;/p&gt;
&lt;p&gt;The baseline establishes that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Presence expires after 90 seconds of inactivity&lt;/li&gt;
&lt;li&gt;Public chat expires after one hour&lt;/li&gt;
&lt;li&gt;User-controlled inputs are sanitized and length-limited&lt;/li&gt;
&lt;li&gt;Logs exclude chat content, tokens and session identifiers&lt;/li&gt;
&lt;li&gt;Authenticated characters are resolved through MyZubster accounts&lt;/li&gt;
&lt;li&gt;Client-provided identity claims are never considered verified&lt;/li&gt;
&lt;li&gt;Permanent movement history is not collected&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The change passed CI, frontend and backend tests, lint, Security Audit, Continuous Evidence Gate and Vercel checks.&lt;/p&gt;
&lt;p&gt;We also documented the remaining gaps honestly: distributed rate limiting, spam detection, mute, block, reporting, moderator auditing and formal deletion procedures are not complete yet.&lt;/p&gt;
&lt;h3 id=&quot;myzubster-space-station-is-online&quot;&gt;MyZubster Space Station is online&lt;/h3&gt;
&lt;p&gt;We also merged &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/921&quot;&gt;PR #921&lt;/a&gt;, introducing the public Space Station dashboard:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/space-station&quot;&gt;https://www.myzubster.com/space-station&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The website now accesses station health, robot and mission information through same-origin API proxy routes. Backend secrets are never exposed to the browser.&lt;/p&gt;
&lt;p&gt;The live health service currently reports:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;station&quot;: &quot;MYZUBSTER-SPACE-STATION&quot;,
  &quot;status&quot;: &quot;online&quot;,
  &quot;version&quot;: &quot;0.2.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browser access remains read-only. Inventory mutations, economic operations and robot commands will not be exposed until a production authentication and authorization layer is available.&lt;/p&gt;
&lt;p&gt;A broader read-only integration for spaces, inventory, MYZ balances and missions is under development in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/919&quot;&gt;PR #919&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;where-this-is-going&quot;&gt;Where this is going&lt;/h3&gt;
&lt;p&gt;The goal is to connect several real components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neon Plaza as the social and immersive layer&lt;/li&gt;
&lt;li&gt;Space Station as the robotics and mission layer&lt;/li&gt;
&lt;li&gt;MyZubster Identity for persistent participants&lt;/li&gt;
&lt;li&gt;Marketplace for creator services and digital resources&lt;/li&gt;
&lt;li&gt;LIFE projects for environmental data and verifiable missions&lt;/li&gt;
&lt;li&gt;Evidence gates for reproducible technical claims&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The priority is not to call everything “complete.” The priority is to build openly, test each layer and distinguish production functionality from experiments and roadmap ideas.&lt;/p&gt;
&lt;p&gt;MyZubster is gradually becoming an open ecosystem where people, software agents, robots and real-world data can interact through verifiable identities and transparent infrastructure.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;br&gt;
Metaverse: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
Space Station: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/space-station&quot;&gt;https://www.myzubster.com/space-station&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;opensource-metaverse-robotics-nodejs-react-mongodb-webdevelopment&quot;&gt;OpenSource #Metaverse #Robotics #NodeJS #React #MongoDB #WebDevelopment&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26181/myzubster-overnight-update-metaverse-safety-and-a-live-space-station</guid>
<pubDate>Fri, 04 Sep 2026 00:38:10 +0000</pubDate>
</item>
<item>
<title>I Built a Real-Time Perception Stack on CARLA. Here Is What Actually Happened.</title>
<link>https://coderlegion.com/25804/i-built-a-real-time-perception-stack-on-carla-here-is-what-actually-happened</link>
<description>&lt;p&gt;I want to tell you about the three weeks I spent making YOLO and ROS 2 talk to each other inside a simulator, and why it was nothing like the tutorials made it look.&lt;/p&gt;
&lt;p&gt;This is not a clean architecture post. This is a &quot;here is what broke, here is what I changed, here is what I actually learned&quot; post.&lt;/p&gt;
&lt;p&gt;If you are building robotics perception systems, or you are just curious what it looks like to wire YOLOv11, ByteTrack, StrongSORT, CARLA, and ROS 2 into something that runs in real time, this one is for you.&lt;/p&gt;
&lt;h3 id=&quot;what-i-was-building-and-why&quot;&gt;What I was building and why&lt;/h3&gt;
&lt;p&gt;The goal was a real-time robotic perception system that could:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;detect objects in a moving vehicle simulation&lt;/li&gt;
&lt;li&gt;track those objects across frames with stable identities&lt;/li&gt;
&lt;li&gt;feed structured perception data into a ROS 2 navigation stack&lt;/li&gt;
&lt;li&gt;run fast enough to be genuinely useful for autonomous decision making&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CARLA was the simulator. YOLOv11 was the detector. ByteTrack and StrongSORT handled multi-object tracking. ROS 2 was the middleware. ONNX and TensorRT handled edge optimisation.&lt;/p&gt;
&lt;p&gt;On paper this is a well-defined problem. In practice, every one of those components has opinions about data formats, timing, and threading that you only discover by running them together.&lt;/p&gt;
&lt;h3 id=&quot;problem-one-getting-camera-data-out-of-carla-cleanly&quot;&gt;Problem one: getting camera data out of CARLA cleanly&lt;/h3&gt;
&lt;p&gt;CARLA has its own Python API and its own event-driven callback system. ROS 2 has its own message types, publisher model, and executor. Getting them to cooperate without dropping frames or blocking the main thread is not automatic.&lt;/p&gt;
&lt;p&gt;Here is a simplified version of how I handled the camera subscription and published frames into ROS 2:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import carla
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import numpy as np

class CARLACameraNode(Node):
    def __init__(self):
        super().__init__('carla_camera_node')
        self.publisher = self.create_publisher(Image, '/carla/camera/rgb', 10)
        self.bridge = CvBridge()

    def camera_callback(self, image):
        array = np.frombuffer(image.raw_data, dtype=np.uint8)
        array = array.reshape((image.height, image.width, 4))
        bgr = array[:, :, :3]
        msg = self.bridge.cv2_to_imgmsg(bgr, encoding='bgr8')
        msg.header.stamp = self.get_clock().now().to_msg()
        self.publisher.publish(msg)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key thing I got wrong initially: I was not stamping the header with the ROS 2 clock.&lt;/p&gt;
&lt;p&gt;That one missing line caused timestamp mismatches downstream that made the tracker behave erratically. ByteTrack and StrongSORT both calculate displacement and velocity across frames using time deltas. If those deltas are wrong or inconsistent, the association algorithm produces noisy, unstable tracks that look like a model problem but are actually a data problem.&lt;/p&gt;
&lt;p&gt;That bug cost me two days.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Add the timestamp. Always. It is not optional in robotics.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;problem-two-yolo-runs-fast-ros-2-callbacks-do-not-wait&quot;&gt;Problem two: YOLO runs fast, ROS 2 callbacks do not wait&lt;/h3&gt;
&lt;p&gt;YOLOv11 at a reasonable resolution on a GPU is quick. But &quot;quick&quot; in model benchmarking terms and &quot;quick&quot; in robotics middleware terms are genuinely different things.&lt;/p&gt;
&lt;p&gt;ROS 2 uses a callback-based execution model. If your inference call is blocking the subscription callback thread, you are missing messages while you compute. The subscriber queue fills. You start processing frames that are already stale by the time inference finishes. Your tracker is now seeing the world as it was, not as it is.&lt;/p&gt;
&lt;p&gt;The fix was decoupling ingestion from inference using a bounded queue and a dedicated inference thread:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import threading
import queue
from rclpy.node import Node
from sensor_msgs.msg import Image

class PerceptionNode(Node):
    def __init__(self):
        super().__init__('perception_node')
        self.frame_queue = queue.Queue(maxsize=5)
        self.subscription = self.create_subscription(
            Image,
            '/carla/camera/rgb',
            self.image_callback,
            10
        )
        self.inference_thread = threading.Thread(
            target=self.run_inference,
            daemon=True
        )
        self.inference_thread.start()

    def image_callback(self, msg):
        if not self.frame_queue.full():
            self.frame_queue.put(msg)

    def run_inference(self):
        while rclpy.ok():
            msg = self.frame_queue.get()
            # run YOLOv11 inference here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;maxsize=5&lt;/code&gt; on the queue is deliberate. When inference cannot keep up, I drop old frames rather than accumulate a growing backlog of stale images.&lt;/p&gt;
&lt;p&gt;This is a real-time systems principle worth internalising: &lt;strong&gt;a stale frame processed late is usually worse than a dropped frame.&lt;/strong&gt; Timeliness matters as much as completeness in a live perception pipeline.&lt;/p&gt;
&lt;h3 id=&quot;problem-three-tracking-across-frames-is-not-free&quot;&gt;Problem three: tracking across frames is not free&lt;/h3&gt;
&lt;p&gt;ByteTrack and StrongSORT both work by associating new detections to existing tracks across frames using motion models and, in StrongSORT's case, appearance embeddings. This sounds clean until you hit occlusion, rapidly moving objects, or a detector that briefly misses something due to a lighting change or partial obstruction.&lt;/p&gt;
&lt;p&gt;What I observed specifically in CARLA: when the ego vehicle turned sharply, vehicles and pedestrians would leave the camera frustum and re-enter within a few frames. The tracker sometimes assigned fresh IDs to those returning objects, breaking the continuity that downstream navigation reasoning depended on.&lt;/p&gt;
&lt;p&gt;The parameters that mattered most here were:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tracker_config = {
    &quot;max_age&quot;: 30,        # frames a track survives without a detection match
    &quot;min_hits&quot;: 3,        # detections needed before a track is confirmed
    &quot;iou_threshold&quot;: 0.3  # minimum overlap for detection-to-track association
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Increase &lt;code&gt;max_age&lt;/code&gt; too much and you get ghost tracks for objects that have genuinely left the scene. Decrease it too much and you lose real tracks during brief occlusions. Tune &lt;code&gt;iou_threshold&lt;/code&gt; too high and the tracker drops tracks during fast motion. Too low and it starts merging distinct objects.&lt;/p&gt;
&lt;p&gt;There is no universally correct set of values. They depend on your scene density, your detector confidence, your frame rate, and what your downstream system needs from the tracker. The only way to find the right values is to run scenarios, observe failures, and adjust deliberately.&lt;/p&gt;
&lt;h3 id=&quot;problem-four-edge-hardware-changes-the-problem-completely&quot;&gt;Problem four: edge hardware changes the problem completely&lt;/h3&gt;
&lt;p&gt;Everything above ran well on my development GPU. Then I exported the model and tested closer to constrained hardware conditions.&lt;/p&gt;
&lt;p&gt;The frame rate dropped. The tracker fell behind. The pipeline that felt smooth in development started introducing the kind of latency that makes real-time perception meaningless.&lt;/p&gt;
&lt;p&gt;The fix was ONNX export followed by TensorRT optimisation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import torch

model = load_yolov11(&quot;yolov11n.pt&quot;)
model.eval()

dummy_input = torch.randn(1, 3, 640, 640).cuda()

torch.onnx.export(
    model,
    dummy_input,
    &quot;perception.onnx&quot;,
    opset_version=17,
    input_names=[&quot;images&quot;],
    output_names=[&quot;output0&quot;],
    dynamic_axes={&quot;images&quot;: {0: &quot;batch&quot;}}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After TensorRT conversion and INT8 quantisation, inference time dropped enough to bring the full pipeline back into a usable range. But the deeper lesson is not about TensorRT specifically. It is that &lt;strong&gt;edge deployment is a different problem to development, and discovering that at the end of the project means redesigning under pressure.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Test on target hardware early. Not day one necessarily, but not month three either.&lt;/p&gt;
&lt;h3 id=&quot;problem-five-your-confidence-threshold-is-a-system-design-decision&quot;&gt;Problem five: your confidence threshold is a system design decision&lt;/h3&gt;
&lt;p&gt;This one is subtle and I did not fully appreciate it until I had the full pipeline running.&lt;/p&gt;
&lt;p&gt;YOLO outputs detections with confidence scores. It is tempting to pick a threshold like 0.5 and move on. But in a live robotics system, that threshold is not a model hyperparameter. It is an architectural decision about when your robot is allowed to act on what it sees.&lt;/p&gt;
&lt;p&gt;A threshold that is too low sends noisy, unreliable detections to the tracker. The tracker generates unstable tracks. The navigation stack gets bad inputs. The robot behaves erratically.&lt;/p&gt;
&lt;p&gt;A threshold that is too high causes the detector to miss real objects in challenging conditions. Tracks are lost. The robot navigates as if obstacles do not exist.&lt;/p&gt;
&lt;p&gt;What helped was treating the threshold not as a single number but as part of a validation layer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def is_valid_detection(detection, track_age, platform_stable):
    if detection.confidence &amp;lt; CONFIDENCE_THRESHOLD:
        return False
    if track_age &amp;lt; MIN_TRACK_AGE:
        return False
    if not platform_stable:
        return False
    return True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Multiple signals need to agree before a detection influences navigation. This pattern, requiring consensus across confidence, temporal stability, and platform state, is something I now consider non-negotiable in any perception pipeline with real-world consequences.&lt;/p&gt;
&lt;h3 id=&quot;five-things-i-know-now-that-i-did-not-know-at-the-start&quot;&gt;Five things I know now that I did not know at the start&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Timestamps are not optional.&lt;/strong&gt; Misaligned timestamps produce bugs that look like model bugs and take days to diagnose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decouple your threads early.&lt;/strong&gt; Blocking the callback thread is an easy mistake with expensive downstream consequences.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The tracker is part of your perception system.&lt;/strong&gt; Not an add-on you configure at the end. Design around it from day one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test on target hardware before you think you are done.&lt;/strong&gt; Edge deployment is a different engineering problem to development.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Log everything during development.&lt;/strong&gt; You cannot replay failures you did not record, and in robotics you will need to replay failures.&lt;/p&gt;
&lt;h3 id=&quot;closing-thought&quot;&gt;Closing thought&lt;/h3&gt;
&lt;p&gt;Building this system taught me that robotics perception is not a model problem. It is a systems problem where the model is one moving part among many.&lt;/p&gt;
&lt;p&gt;The interesting engineering is in how you connect those parts, how you handle failures at the boundaries between them, and how you make the full pipeline behave reliably when nothing goes quite according to plan.&lt;/p&gt;
&lt;p&gt;Which is always.&lt;/p&gt;
&lt;p&gt;If you are building something similar or want to dig into any part of this, drop a comment. I am always happy to talk robotics and perception systems.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25804/i-built-a-real-time-perception-stack-on-carla-here-is-what-actually-happened</guid>
<pubDate>Sun, 30 Aug 2026 08:57:09 +0000</pubDate>
</item>
<item>
<title>I built a programming language because C++ almost killed my drone</title>
<link>https://coderlegion.com/22891/i-built-a-programming-language-because-c-almost-killed-my-drone</link>
<description>&lt;p&gt;I'm 17. Last year I was writing drone firmware in C++ and Python. One afternoon my ultrasonic sensor returned &lt;code&gt;null&lt;/code&gt; during a landing sequence. The code didn't check for it. The drone tried to divide by zero, the motor controller panicked, and my DJI Tello-sized quadcopter dropped from 3 meters onto concrete.&lt;/p&gt;
&lt;p&gt;That was a $400 lesson in why runtime errors in robotics aren't just bugs. They're physics problems.&lt;/p&gt;
&lt;p&gt;So I built a programming language.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-nobody-talks-about&quot;&gt;The problem nobody talks about&lt;/h3&gt;
&lt;p&gt;When you're writing a web app and something crashes, you reload the page. When you're writing drone code and something crashes, the drone crashes. Into whatever's below it. Usually something expensive or fragile or alive.&lt;/p&gt;
&lt;p&gt;C++ and Python dominate robotics. Both assume you'll handle edge cases yourself. Sensor returns &lt;code&gt;null&lt;/code&gt;? That's your problem. Timing deadline missed? Good luck. No fallback path for when the lidar fails over water? Figure it out.&lt;/p&gt;
&lt;p&gt;ROS (Robot Operating System) helps with some of this, but it's an entire infrastructure layer. What I wanted was something simpler: a language where the compiler catches the stuff that kills robots before you ever compile it.&lt;/p&gt;
&lt;p&gt;Here's what I mean. In C++, you write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float distance = sensor.read();
float speed = target_distance / distance;
motor.set(speed);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;distance&lt;/code&gt; is 0, you get a division by zero. If &lt;code&gt;sensor.read()&lt;/code&gt; returns &lt;code&gt;NaN&lt;/code&gt;, you get &lt;code&gt;NaN&lt;/code&gt; propagation through your motor controller. The compiler doesn't care. It'll happily compile this and let your drone fall out of the sky.&lt;/p&gt;
&lt;p&gt;In Fabric (the language I built), you'd write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;sensor distance: Sensor&amp;lt;f32, ±0.02&amp;gt;
actuator motor: Motor

loop {
    let speed = target_distance / distance
    motor.write(speed)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you try to use &lt;code&gt;distance&lt;/code&gt; without checking that the sensor returned a valid reading, the compiler throws an error. If &lt;code&gt;distance&lt;/code&gt; could be zero and you didn't add a fallback path, the compiler throws an error. If your sensor has an uncertainty bound of ±0.02 and you're doing math that could amplify that uncertainty past a safety threshold, the compiler throws an error.&lt;/p&gt;
&lt;p&gt;The compiler knows about physics. Not perfectly, but enough to catch the stuff that actually kills robots.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-it-works-under-the-hood&quot;&gt;How it works under the hood&lt;/h3&gt;
&lt;p&gt;I wrote the whole thing in Rust. About 4,500 lines across 8 crates. Here's the pipeline:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;.fab source → Lexer → Parser → AST → Type Checker → Fallback Graph → IPET Timing → CodeGen → .py / .c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each step catches a different category of mistakes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The type system&lt;/strong&gt; tracks sensor uncertainty as a first-class concept. When you declare &lt;code&gt;Sensor&amp;lt;f32, ±0.02&amp;gt;&lt;/code&gt;, that &lt;code&gt;±0.02&lt;/code&gt; isn't decoration. It propagates through every math operation. Add two sensors with ±0.02 uncertainty and the result has ±0.04. Multiply by a constant and the uncertainty scales accordingly. If the final result's uncertainty exceeds what's safe for your actuator, the compiler catches it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fallback graph&lt;/strong&gt; checks that every sensor has a plan B. Not just declared, but actually reachable. If your fallback function A calls fallback function B, and B calls A, that's a cycle. The compiler detects it. If sensor X has no fallback at all, the compiler refuses to build.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IPET timing analysis&lt;/strong&gt; uses an Integer Linear Program to prove worst-case execution time at compile time. It builds a control flow graph, assigns cycle costs to each instruction (ARM Cortex-M4 model: ALU = 1 cycle, float division = 14 cycles, sensor read = 2 cycles), and solves for the maximum possible execution time across all code paths including loops.&lt;/p&gt;
&lt;p&gt;I didn't come up with this technique. It's from Li &amp;amp; Malik's 1994 paper on implicit path enumeration. But I implemented it in Rust using a pure-Rust ILP solver called &lt;code&gt;good_lp&lt;/code&gt; with the &lt;code&gt;microlp&lt;/code&gt; backend. No external solver dependency. No C FFI. Just Rust solving linear programs.&lt;/p&gt;
&lt;p&gt;The solver maximizes &lt;code&gt;sum(cost_i * x_i)&lt;/code&gt; subject to flow conservation constraints. If the solver fails (which happens on really complex control flow), the system falls back to a conservative estimate instead of crashing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-two-target-code-generation&quot;&gt;The two-target code generation&lt;/h3&gt;
&lt;p&gt;Fabric compiles to two backends: Python for Webots simulation, and C for ARM Cortex-M hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python output&lt;/strong&gt; generates a Webots controller class with sensor initialization, motor setup, fallback state tracking, and all the logic. You can test your drone code in simulation before touching hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C output&lt;/strong&gt; generates code that includes a &lt;code&gt;hal.h&lt;/code&gt; hardware abstraction layer. Static sensor handles, &lt;code&gt;#define TIMEOUT_MS&lt;/code&gt; for fallback deadlines, proper C types (&lt;code&gt;float&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;), and deadline enforcement using &lt;code&gt;hal_get_time_ms()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Same source file, two completely different targets. No &lt;code&gt;#ifdef&lt;/code&gt; in your code. No conditional compilation. The compiler handles the target-specific stuff.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-d-do-differently&quot;&gt;What I'd do differently&lt;/h3&gt;
&lt;p&gt;The parser is hand-rolled. About 700 lines of Pratt parser with recursive descent for statements. I started with chumsky (a Rust parser combinator library) but their API changed between versions and the documentation was sparse when I needed it most. So I wrote my own.&lt;/p&gt;
&lt;p&gt;It's fine. But it means every time I want to add a new syntax construct, I'm writing parser code by hand. A proper parser generator would've saved time in the long run, even with the initial investment.&lt;/p&gt;
&lt;p&gt;The loop bound estimation is a heuristic. If your loop body has 1-3 statements, the compiler assumes it runs 10 times. 4-6 statements? 5 times. 7+? 3 times. This is obviously wrong for real programs. Proper static bound analysis would make the IPET results much more accurate. It's on my list.&lt;/p&gt;
&lt;p&gt;The C output references a &lt;code&gt;clamp()&lt;/code&gt; function that it never defines. If you actually try to compile the generated C code on hardware, you'll get a linker error. I know about it. I just haven't fixed it yet.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-i-m-publishing-this&quot;&gt;Why I'm publishing this&lt;/h3&gt;
&lt;p&gt;I put all 8 crates on crates.io. The lexer, parser, type checker, everything. The repo is at github.com/subhansh-dev/fabric.&lt;/p&gt;
&lt;p&gt;Partly because I want other people to try it. If you're working on robotics or drones and you've been bitten by runtime errors, Fabric might save you some pain.&lt;/p&gt;
&lt;p&gt;Mostly because I think the idea matters more than my implementation. The safety-critical systems world has had formal methods and static analysis for decades. But it's all locked behind expensive tools and enterprise licenses. I wanted to build something that a 17-year-old with a Raspberry Pi could use.&lt;/p&gt;
&lt;p&gt;Is my implementation perfect? No. The IPET loop bounds are guesses. The C backend doesn't handle drone swarms. The match expression codegen references some variables it doesn't always generate. I know about all of these. They're bugs, not features, and I'll fix them.&lt;/p&gt;
&lt;p&gt;But the core idea is sound: compile-time safety for robotics shouldn't require a $50,000 license for Simulink. Sometimes the right answer to &quot;how do we prevent this bug&quot; isn't &quot;write better tests&quot; or &quot;add more runtime checks.&quot; Sometimes it's &quot;make the compiler say no.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's next&lt;/h3&gt;
&lt;p&gt;I'm working on adding hardware-in-the-loop testing support. The idea is that Fabric could instrument your actual hardware and compare runtime sensor readings against the compile-time uncertainty bounds. If reality diverges from what the compiler predicted, you get a warning before your next flight.&lt;/p&gt;
&lt;p&gt;Also working on proper static loop bound analysis. The current heuristic is fine for demos but useless for real deployments. I'm looking at abstract interpretation techniques to actually prove bounds instead of guessing.&lt;/p&gt;
&lt;p&gt;If any of this sounds interesting, check out the repo. Open an issue. Try it on your own robot. Break it and tell me how.&lt;/p&gt;
&lt;p&gt;That's how this gets better.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Fabric is MIT licensed. The crates are published on crates.io. The docs are in the README because I'm 17 and haven't written proper docs yet.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;gh repo link :- &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/subhansh-dev/fabric&quot;&gt;https://github.com/subhansh-dev/fabric&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22891/i-built-a-programming-language-because-c-almost-killed-my-drone</guid>
<pubDate>Fri, 17 Jul 2026 14:10:22 +0000</pubDate>
</item>
<item>
<title>Open-Sourcing a 6-Tool Bio-Robotics &amp; Bionics MCP Server (v1.0.0)</title>
<link>https://coderlegion.com/22789/open-sourcing-a-6-tool-bio-robotics-bionics-mcp-server-v1-0-0</link>
<description>&lt;p&gt;We just officially open-sourced &lt;strong&gt;&lt;code&gt;seosiri-biorobotics&lt;/code&gt; v1.0.0&lt;/strong&gt;, a stateless, local-first Model Context Protocol (MCP) server designed to bridge the integration gap between biological datasets and physical robotic actuators.&lt;/p&gt;
&lt;h3 id=&quot;-the-architecture-decoupling-intent-from-actuation&quot;&gt;🛠️ The Architecture: Decoupling Intent from Actuation&lt;/h3&gt;
&lt;p&gt;Standard laboratory automation is heavily fragmented. Biologists use genomic databases, while mechanical engineers program robotics via proprietary vendor-locked scripts. &lt;/p&gt;
&lt;p&gt;Our system decouples these layers completely:&lt;br&gt;
&lt;code&gt;[Raw Bio-Data / EMG Input] ──► [MCP Server] ──► [Deterministic Math Core] ──► [G-code Serial Stream]&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Context Broker (MCP):&lt;/strong&gt; Connects to the live UniProt REST API, with a local-first &lt;strong&gt;SQLite database cache (&lt;code&gt;local_db.py&lt;/code&gt;)&lt;/strong&gt; to ensure offline autonomy and bypass network latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Math Core (&lt;code&gt;core_math.py&lt;/code&gt;):&lt;/strong&gt; Converts biological parameters to metric displacement vectors in SI units (meters).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Hardware Gateway (&lt;code&gt;hardware_gateway.py&lt;/code&gt;):&lt;/strong&gt; Translates coordinates into standard Cartesian G-code (&lt;code&gt;G1 X[Value] F1500&lt;/code&gt;) used globally by CNC gantries and motor controllers.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;-the-6-tool-suite-live-tested-&quot;&gt;📦 The 6-Tool Suite (Live-Tested)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fetch_genomic_data&lt;/code&gt;: Queries sequence parameters (with automated SQLite database caching).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolve_biotech_spatial_intent&lt;/code&gt;: Translates concentration inputs into metric space coordinates.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map_plate_coordinate&lt;/code&gt;: Maps alphanumeric well IDs to standard 96-well plate offsets in millimeters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;calculate_pipetting_speed&lt;/code&gt;: Calibrates feedrates and delays based on fluid viscosity (cP).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;calculate_dna_melting_temp&lt;/code&gt;: Evaluates GC-content and melting temperature (Tm) to recommend deck-bed heating offsets.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;translate_emg_to_actuation&lt;/code&gt;: Translates human surface EMG microvolt biosignals into safe joint angles with active spasm protection.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-try-it-zero-install-required-&quot;&gt;🚀 Try It (Zero Install Required)&lt;/h3&gt;
&lt;p&gt;The project is officially listed on the Glama.ai directory, with verified A-grade license and maintenance scores. You can run the entire 6-tool suite inside their web-based sandbox inspector directly, with no local setup required.&lt;/p&gt;
&lt;p&gt;Read our deep-dive architectural analysis and access the open-source codebase:&lt;br&gt;
👉 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.seosiri.com/2026/07/seosiri-bio-robotics-core-engine.html&quot;&gt;https://www.seosiri.com/2026/07/seosiri-bio-robotics-core-engine.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We would highly appreciate your technical feedback, PRs, and system reviews!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22789/open-sourcing-a-6-tool-bio-robotics-bionics-mcp-server-v1-0-0</guid>
<pubDate>Thu, 16 Jul 2026 05:53:56 +0000</pubDate>
</item>
<item>
<title>Building a real-time drone ground control station in React</title>
<link>https://coderlegion.com/22778/building-a-real-time-drone-ground-control-station-in-react</link>
<description>&lt;p&gt;I had a quadcopter publishing telemetry over MAVLink and a stubborn preference for not leaving the browser. QGroundControl and Mission Planner are both excellent, but they are native apps, and the thing I actually wanted was a status page the rest of the team could open in a tab. Attitude, position, battery, and a running log of whatever the flight controller was complaining about.&lt;/p&gt;
&lt;p&gt;The catch is that a drone publishes attitude at 30 to 50 Hz, and the first React version of this that anyone writes tends to fall over the moment real data arrives. You wire each reading to a piece of state, the state updates a few dozen times a second across five or six components, and React spends its whole frame budget reconciling instead of drawing. The display either lags behind the aircraft or, worse, different instruments end up showing different instants of the same moment.&lt;/p&gt;
&lt;p&gt;So I built it with &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JayaSaiKishanChapparam/altara&quot;&gt;Altara&lt;/a&gt;, a set of telemetry components I maintain that keep the high-frequency path off React's render cycle. This post builds the ground control station in two passes. First the entire dashboard in mock mode, with no drone and no rosbridge involved, so you can get the layout and styling right against animated data. Then we point the exact same components at a real MAVROS stack.&lt;/p&gt;
&lt;h3 id=&quot;what-we-re-building&quot;&gt;What we're building&lt;/h3&gt;
&lt;p&gt;Four panels, the same four every ground station has:&lt;/p&gt;
&lt;p&gt;A primary flight display (PFD), the artificial horizon with airspeed and altitude tapes, a heading scale, and a vertical speed indicator. A moving map showing the aircraft's position and the track it has flown. A battery gauge. And an event log for the messages coming off the flight controller.&lt;/p&gt;
&lt;p&gt;We will lay them out in a grid, get all four animating, and only then think about where the numbers come from.&lt;/p&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;The PFD lives in the aerospace package and the map, gauge, and log live in core, so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @altara/core @altara/aerospace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The map renders with Leaflet, which Altara treats as an optional peer dependency and loads on mount. If you skip it the map will throw a setup hint at you, so install it now even though we are still in mock mode:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install leaflet@^1.9.4 react-leaflet@^4.2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-smallest-thing-that-moves&quot;&gt;The smallest thing that moves&lt;/h3&gt;
&lt;p&gt;Before the full grid, here is the PFD and the map side by side, animating themselves with no data plumbing at all:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { PrimaryFlightDisplay } from '@altara/aerospace';
import { LiveMap } from '@altara/core';

export function FlightDeck() {
  return (
    &amp;lt;div style={{ display: 'flex', gap: 16 }}&amp;gt;
      &amp;lt;PrimaryFlightDisplay mockMode size=&quot;md&quot; showFlightDirector /&amp;gt;
      &amp;lt;LiveMap mockMode /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole thing. The &lt;code&gt;mockMode&lt;/code&gt; flag on each component switches on a built-in generator, so there is no provider to set up and nothing to feed them.&lt;/p&gt;
&lt;p&gt;What you see is worth describing, because it is more deliberate than random jitter. The PFD runs a gentle cruise profile. The horizon banks left and right through about 18 degrees on a slow cycle, the nose pitches up and down a few degrees out of phase with the bank so it reads as a coordinated turn rather than a metronome, the heading scale sweeps slowly around east, and the airspeed and altitude tapes drift around 120 knots and 4500 feet. The vertical speed needle leads the altitude changes correctly, because it is driven by the rate of the same wave. With &lt;code&gt;showFlightDirector&lt;/code&gt; on, the magenta guidance bars lag the aircraft slightly, so they look like something the pilot is chasing instead of sitting dead center. It reads as an aircraft loafing along in cruise, which is exactly the point. You want to develop against motion that looks plausible.&lt;/p&gt;
&lt;p&gt;The map, meanwhile, flies a slow circle around a fixed point and turns its nose to follow the track, leaving a trail behind it.&lt;/p&gt;
&lt;h3 id=&quot;the-full-dashboard-still-faked&quot;&gt;The full dashboard, still faked&lt;/h3&gt;
&lt;p&gt;Now the real layout. Four panels in a grid, every one of them in mock mode except the log, which works a little differently:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { LiveMap, Gauge, EventLog, type EventLogEntry } from '@altara/core';
import { PrimaryFlightDisplay } from '@altara/aerospace';

const demoEvents: EventLogEntry[] = [
  { timestamp: Date.now() - 9000, severity: 'info', message: 'EKF using GPS' },
  { timestamp: Date.now() - 6000, severity: 'info', message: 'Armed' },
  { timestamp: Date.now() - 3000, severity: 'warn', message: 'Wind 11 m/s, approaching limit' },
  { timestamp: Date.now() - 1000, severity: 'info', message: 'Waypoint 3 reached' },
];

export function GroundControlStation() {
  return (
    &amp;lt;div
      style={{
        display: 'grid',
        gridTemplateColumns: '1fr 1fr',
        gap: 16,
        padding: 16,
        alignItems: 'start',
      }}
    &amp;gt;
      &amp;lt;PrimaryFlightDisplay mockMode size=&quot;md&quot; showFlightDirector /&amp;gt;
      &amp;lt;LiveMap mockMode /&amp;gt;
      &amp;lt;Gauge
        mockMode
        mockProfile=&quot;ramp&quot;
        min={0}
        max={100}
        label=&quot;Battery&quot;
        unit=&quot;%&quot;
        thresholds={[
          { value: 0, color: 'var(--vt-color-danger)' },
          { value: 20, color: 'var(--vt-color-warn)' },
          { value: 40, color: 'var(--vt-color-active)' },
        ]}
      /&amp;gt;
      &amp;lt;EventLog entries={demoEvents} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things in there are worth pulling out.&lt;/p&gt;
&lt;p&gt;The gauge has a &lt;code&gt;mockProfile&lt;/code&gt;. The default mock animation sweeps the needle up and down with a sine wave, which is fine for a generic gauge but looks absurd on a battery, because the charge appears to refill itself every few seconds. The &lt;code&gt;ramp&lt;/code&gt; profile drains it from full to empty and resets, so a screenshot of your dashboard does not show a battery doing something physically impossible. Small detail, but it is the difference between a demo that looks considered and one that looks like a component sampler.&lt;/p&gt;
&lt;p&gt;The event log is the odd one out. It has no mock mode, and that is on purpose. Every other component here carries a single number per channel, and the log carries text and a severity, which is a different kind of thing entirely. So instead of a &lt;code&gt;mockMode&lt;/code&gt; flag, you hand it an array of entries and own that array yourself. Here it is a static list. Later, when we have real data, we will append to it from the flight controller's log stream. The shape is just a timestamp, a message, and one of &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt;, or &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One layout thing that will save you a confused minute. The PFD has a fixed pixel footprint rather than a fluid one, around 680px wide at &lt;code&gt;size=&quot;lg&quot;&lt;/code&gt; and narrower at &lt;code&gt;md&lt;/code&gt;. In a two-up grid that means the grid has to be wide enough to hold it, or the instrument overflows its column. I dropped to &lt;code&gt;size=&quot;md&quot;&lt;/code&gt; above so the dashboard sits comfortably at normal widths. If you want the larger PFD, give the grid the room for it rather than expecting the instrument to shrink.&lt;/p&gt;
&lt;p&gt;Everything above runs without a drone, without rosbridge, and without a simulator. You can build the entire dashboard, get the grid proportions right, tune the thresholds, and hand it to a designer, all before any real telemetry exists. That is the reason to start in mock mode rather than treating it as an afterthought. The hard part of a ground station is rarely the data. It is the layout and the rendering, and you can finish both of those against fake motion.&lt;/p&gt;
&lt;p&gt;Next we throw the mock flag away and point these same four components at a live MAVROS stack over rosbridge, which is where the interesting wiring lives: turning an IMU quaternion into something the horizon can draw, getting five channels onto one display, and feeding text logs into a component that only speaks numbers.&lt;/p&gt;
&lt;h3 id=&quot;wiring-it-to-a-real-drone&quot;&gt;Wiring it to a real drone&lt;/h3&gt;
&lt;p&gt;The plan from here is the same four components, none of them changed, fed by a live MAVROS stack instead of the mock generator. The bridge between the two worlds is a small idea. Every Altara component that takes live data takes a &lt;code&gt;dataSource&lt;/code&gt;, an object you subscribe to for a stream of timestamped numbers. rosbridge, meanwhile, hands you ROS topics over a WebSocket. So the wiring is mostly about turning topics into data sources, with a couple of places where the translation is more than a rename.&lt;/p&gt;
&lt;p&gt;You will need a rosbridge server in front of your ROS graph (the standard &lt;code&gt;rosbridge_suite&lt;/code&gt;), and for a MAVLink vehicle, MAVROS bridging the flight controller into ROS. Once that is up, everything below talks to it at &lt;code&gt;ws://localhost:9090&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These four are the imports for the whole section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useEffect, useMemo, useState } from 'react';
import { EventLog, Gauge, LiveMap, type EventLogEntry } from '@altara/core';
import { PrimaryFlightDisplay } from '@altara/aerospace';
import {
  createBatteryStateAdapter,
  createImuAdapter,
  createRosbridgeAdapter,
  mergeChannels,
} from '@altara/ros';
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-pfd-five-numbers-two-topics-one-instrument&quot;&gt;The PFD: five numbers, two topics, one instrument&lt;/h4&gt;
&lt;p&gt;This is the one with the actual problem in it. A primary flight display shows five live values: roll, pitch, heading, airspeed, and altitude. They do not come from one place. Roll and pitch come from the IMU, and heading, airspeed, and altitude come from the flight controller's HUD topic. The display, though, wants a single data source.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function LivePrimaryFlightDisplay({ url }: { url: string }) {
  const source = useMemo(() =&amp;gt; {
    const imu = createImuAdapter({ url, topic: '/mavros/imu/data' });
    const hud = createRosbridgeAdapter({
      url,
      topic: '/mavros/vfr_hud',
      messageType: 'mavros_msgs/VFR_HUD',
      channels: {
        heading: (m) =&amp;gt; m.heading,
        airspeed: (m) =&amp;gt; m.airspeed * 1.94384, // m/s -&amp;gt; kt
        altitude: (m) =&amp;gt; m.altitude * 3.28084, // m -&amp;gt; ft
      },
    });
    return mergeChannels({
      roll: imu.roll,
      pitch: imu.pitch,
      heading: hud.heading,
      airspeed: hud.airspeed,
      altitude: hud.altitude,
    });
  }, [url]);

  useEffect(() =&amp;gt; () =&amp;gt; source.destroy(), [source]);

  return &amp;lt;PrimaryFlightDisplay dataSource={source} showFlightDirector /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three things are happening there.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;createImuAdapter&lt;/code&gt; opens one socket to the IMU topic and gives you back roll, pitch, and yaw as separate channels. That matters more than it looks, because &lt;code&gt;sensor_msgs/Imu&lt;/code&gt; reports orientation as a quaternion and the horizon needs degrees. The adapter does that conversion for you, and if you ever want it on its own, &lt;code&gt;quaternionToEuler&lt;/code&gt; is exported too. There is one detail in that conversion worth knowing about, which I will come back to.&lt;/p&gt;
&lt;p&gt;The HUD adapter uses the generic &lt;code&gt;createRosbridgeAdapter&lt;/code&gt; with a &lt;code&gt;channels&lt;/code&gt; map, which pulls several numbers off a single message. VFR_HUD publishes heading already in degrees, but airspeed in meters per second and altitude in meters, so the conversions to knots and feet are mine to do, right there in the extractor. The tapes display whatever number you give them. Hand them meters and they will read 4500 meters without complaint.&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;mergeChannels&lt;/code&gt; unions those named sources into one channel-tagged stream, keyed by the names I passed. That key is what the PFD routes on internally, so the labels are load bearing. &lt;code&gt;roll&lt;/code&gt; has to be &lt;code&gt;roll&lt;/code&gt;. The whole display ends up running on two sockets, one to the IMU and one to the HUD, which is about as lean as five live values across two topics gets. One more thing the merge does quietly: calling &lt;code&gt;destroy()&lt;/code&gt; on the merged source tears down both child adapters, which is why the cleanup above only has to destroy the one thing it kept a handle to.&lt;/p&gt;
&lt;p&gt;A note on the IMU topic, since it is an easy half hour to lose. Use &lt;code&gt;/mavros/imu/data&lt;/code&gt;, not &lt;code&gt;/mavros/imu/data_raw&lt;/code&gt;. The raw topic ships without orientation populated, so the quaternion is all zeros and your horizon sits there perfectly level while the aircraft rolls.&lt;/p&gt;
&lt;h4 id=&quot;the-conversion-and-the-value-that-goes-nan&quot;&gt;The conversion, and the value that goes NaN&lt;/h4&gt;
&lt;p&gt;The quaternion to roll/pitch/yaw conversion is standard, but the pitch term is an &lt;code&gt;asin&lt;/code&gt;, and &lt;code&gt;asin&lt;/code&gt; is only defined on the range minus one to one. Near vertical, where pitch approaches plus or minus 90 degrees, floating point error can push the argument a hair past one, and &lt;code&gt;asin&lt;/code&gt; of 1.0000001 is NaN. One NaN in the pitch channel and the horizon blanks. So the conversion clamps the argument before the &lt;code&gt;asin&lt;/code&gt;. You inherit that clamp for free through &lt;code&gt;createImuAdapter&lt;/code&gt;, but if you pull &lt;code&gt;quaternionToEuler&lt;/code&gt; out to use directly, that is the line that keeps a drone pointed straight up from blanking your display.&lt;/p&gt;
&lt;h4 id=&quot;the-map-gps-is-two-numbers-that-arrive-separately&quot;&gt;The map: GPS is two numbers that arrive separately&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;LiveMap&lt;/code&gt; is a controlled component. It does not take a &lt;code&gt;dataSource&lt;/code&gt;, it takes a &lt;code&gt;position&lt;/code&gt;. And GPS arrives as a &lt;code&gt;sensor_msgs/NavSatFix&lt;/code&gt; message where latitude and longitude are two fields, which the adapter surfaces as two channels. So you merge them like the PFD, then accumulate the stream into a position object yourself.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function LiveDroneMap({ url }: { url: string }) {
  const source = useMemo(() =&amp;gt; {
    const gps = createRosbridgeAdapter({
      url,
      topic: '/mavros/global_position/global',
      messageType: 'sensor_msgs/NavSatFix',
      channels: {
        lat: (m) =&amp;gt; m.latitude,
        lng: (m) =&amp;gt; m.longitude,
      },
    });
    return mergeChannels({ lat: gps.lat, lng: gps.lng });
  }, [url]);

  const [position, setPosition] = useState&amp;lt;{ lat: number; lng: number }&amp;gt;();
  useEffect(() =&amp;gt; {
    const latest: { lat?: number; lng?: number } = {};
    const off = source.subscribe((v) =&amp;gt; {
      if (v.channel === 'lat') latest.lat = v.value;
      if (v.channel === 'lng') latest.lng = v.value;
      if (latest.lat !== undefined &amp;amp;&amp;amp; latest.lng !== undefined) {
        setPosition({ lat: latest.lat, lng: latest.lng });
      }
    });
    return () =&amp;gt; {
      off();
      source.destroy();
    };
  }, [source]);

  return position ? &amp;lt;LiveMap position={position} trackLength={800} /&amp;gt; : &amp;lt;LiveMap /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The thing to notice is the accumulation. Latitude and longitude come in as separate samples, so &lt;code&gt;position&lt;/code&gt; stays undefined until both have landed at least once. That is the &lt;code&gt;position ? ... : &amp;lt;LiveMap /&amp;gt;&lt;/code&gt; at the end. For the first fraction of a second after connecting, before both axes have arrived, you render an empty map rather than a marker at the equator. &lt;code&gt;trackLength&lt;/code&gt; is how many points of history the trail keeps.&lt;/p&gt;
&lt;h4 id=&quot;the-battery-when-the-firmware-will-not-tell-you-the-charge&quot;&gt;The battery: when the firmware will not tell you the charge&lt;/h4&gt;
&lt;p&gt;The battery looks like the easy one, and on good hardware it is. &lt;code&gt;createBatteryStateAdapter&lt;/code&gt; reads the state of charge straight off &lt;code&gt;sensor_msgs/BatteryState&lt;/code&gt; and you are done.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function LiveBatteryGauge({ url }: { url: string }) {
  const source = useMemo(
    () =&amp;gt;
      createBatteryStateAdapter({
        url,
        topic: '/mavros/battery',
        voltageRange: { min: 14.0, max: 16.8 }, // 4S LiPo (3.5–4.2 V/cell)
      }),
    [url],
  );
  useEffect(() =&amp;gt; () =&amp;gt; source.destroy(), [source]);

  return (
    &amp;lt;Gauge
      dataSource={source}
      min={0}
      max={100}
      label=&quot;Battery&quot;
      unit=&quot;%&quot;
      thresholds={[
        { value: 0, color: 'var(--vt-color-danger)' },
        { value: 20, color: 'var(--vt-color-warn)' },
        { value: 40, color: 'var(--vt-color-active)' },
      ]}
    /&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;voltageRange&lt;/code&gt; is there for the common case where the firmware cannot give you a charge percentage at all. Plenty of PX4 and ArduPilot setups publish percentage as minus one when there is no battery estimator configured, and a gauge reading a steady minus one is useless. When that happens, the adapter falls back to estimating charge from pack voltage, with &lt;code&gt;voltageRange&lt;/code&gt; telling it the empty and full voltages of your pack. The 14.0 to 16.8 above is a 4S LiPo, 3.5 to 4.2 volts a cell. Be honest with yourself about what that estimate is, though. A LiPo's voltage sags under load and the discharge curve is famously flat through the middle, so a voltage based percentage is a presence of charge indicator, not a fuel gauge. It will tell you the battery is there and roughly how worried to be. It will not tell you how many minutes you have left.&lt;/p&gt;
&lt;h4 id=&quot;the-event-log-text-in-a-numeric-world&quot;&gt;The event log: text in a numeric world&lt;/h4&gt;
&lt;p&gt;The other three components all carry numbers. The log carries text and a severity, which the rest of the library has no concept of. Altara's data model is numeric all the way down, deliberately, because that is what keeps the high-frequency path fast. So there is no adapter for log messages, and rather than bolt strings onto a numeric pipeline, the log just talks to rosbridge directly. You open the socket, subscribe to &lt;code&gt;/rosout&lt;/code&gt;, and map each message into the entry shape the log wants.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const ROSOUT_LEVEL: Record&amp;lt;number, EventLogEntry['severity']&amp;gt; = {
  20: 'info',
  30: 'warn',
  40: 'error',
  50: 'error', // rcl_interfaces/Log fatal -&amp;gt; error
};

export function RosoutEventLog({ url }: { url: string }) {
  const [entries, setEntries] = useState&amp;lt;EventLogEntry[]&amp;gt;([]);

  useEffect(() =&amp;gt; {
    const ws = new WebSocket(url);
    ws.onopen = () =&amp;gt;
      ws.send(JSON.stringify({ op: 'subscribe', topic: '/rosout', type: 'rcl_interfaces/Log' }));
    ws.onmessage = (e) =&amp;gt; {
      const data = typeof e.data === 'string' ? e.data : '';
      if (!data) return;
      const env = JSON.parse(data) as {
        op: string;
        topic?: string;
        msg?: { level: number; name: string; msg: string; stamp?: { sec: number; nanosec: number } };
      };
      if (env.op !== 'publish' || env.topic !== '/rosout' || !env.msg) return;
      const m = env.msg;
      if (m.level &amp;lt; 20) return; // skip debug
      setEntries((prev) =&amp;gt;
        [
          ...prev,
          {
            timestamp: m.stamp ? m.stamp.sec * 1000 + m.stamp.nanosec / 1e6 : Date.now(),
            severity: ROSOUT_LEVEL[m.level] ?? 'info',
            message: `[${m.name}] ${m.msg}`,
          },
        ].slice(-500),
      );
    };
    return () =&amp;gt; ws.close();
  }, [url]);

  return &amp;lt;EventLog entries={entries} maxEntries={500} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things in there earn their keep. The &lt;code&gt;slice(-500)&lt;/code&gt; caps the array you hold in state, and it is yours to remember. &lt;code&gt;maxEntries&lt;/code&gt; on the component limits what gets rendered, not what you retain, so a chatty &lt;code&gt;/rosout&lt;/code&gt; will grow your state without bound if you forget the slice. And map the fatal level (50) explicitly. If you leave it out it falls through to the &lt;code&gt;?? 'info'&lt;/code&gt; default, and a crash shows up colored like a routine status line, which is the one time you really do not want that.&lt;/p&gt;
&lt;h3 id=&quot;what-it-took-to-make-the-horizon-not-lie&quot;&gt;What it took to make the horizon not lie&lt;/h3&gt;
&lt;p&gt;The wiring above is the part you write. The part you do not see is why the PFD holds together at all, and it comes down to two decisions that were not obvious to me until the naive version failed.&lt;/p&gt;
&lt;p&gt;The first is that the entire instrument is one canvas driven by one &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop, and incoming data never touches React state. A PFD is really six instruments sharing a frame: the attitude sphere, the airspeed tape, the altitude tape, the vertical speed needle, the heading scale, and the flight director. The obvious React build gives each of them its own state and lets props flow down. The problem shows up in a banking turn. The airspeed tape repaints on one frame, the attitude on the next, and for a moment they are showing different instants of the same maneuver. The instrument visibly disagrees with itself. The fix was to stop using React for the moving parts entirely. Data, whether from mock, props, or rosbridge, mutates a single ref, and one animation loop reads that ref every frame and paints the whole instrument at once. React mounts the canvas and then gets out of the way. There are zero re-renders during flight, and because everything is painted from one ref in one pass, the six instruments can never disagree.&lt;/p&gt;
&lt;p&gt;The second decision is a single minus sign, and it is the kind of bug that passes every screenshot and fails in the air. The horizon moves and the aircraft symbol stays fixed, which is how real attitude indicators work. You do that with a canvas transform, not by repositioning shapes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;ctx.translate(cx, cy);
ctx.rotate((-s.roll * Math.PI) / 180); // note the minus
ctx.translate(0, s.pitch * pitchPxPerDeg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You rotate the coordinate system and draw the sky, ground, and pitch ladder into it, which is far simpler than computing rotated coordinates for every line and clips cleanly at the edges. The minus on the roll is the part that matters. The sphere has to rotate opposite to the aircraft, so that when the drone banks right, the horizon tilts left behind a fixed wing symbol. Drop the minus and a static screenshot looks completely correct, every line where you expect it. Then the aircraft banks and the horizon banks with it, in the same direction, and the instrument is now confidently telling the pilot the opposite of the truth. The flight director bars layer on top of all this from the error between commanded and actual attitude, which is why they drift toward center as the aircraft captures the command rather than sitting pinned.&lt;/p&gt;
&lt;h3 id=&quot;where-that-leaves-you&quot;&gt;Where that leaves you&lt;/h3&gt;
&lt;p&gt;The same four panels you built against fake motion are now running on a real aircraft, and almost nothing about the components changed between the two. That is the whole argument for starting in mock mode. The dashboard, the layout, the thresholds, the thing you actually design and iterate on, none of it cares whether the numbers are real. The real work was never the rendering, which the library handles, but the translation at the edges: a quaternion into degrees, five channels onto one instrument, two GPS axes into a position, a flat discharge curve into a believable percentage, and text logs into a pipeline that otherwise only counts.&lt;/p&gt;
&lt;p&gt;The components are MIT licensed and on npm as &lt;code&gt;@altara/core&lt;/code&gt;, &lt;code&gt;@altara/aerospace&lt;/code&gt;, and &lt;code&gt;@altara/ros&lt;/code&gt;. The full source for every snippet here lives in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JayaSaiKishanChapparam/altara&quot;&gt;repo&lt;/a&gt;, and there is a &lt;a rel=&quot;nofollow&quot; href=&quot;https://jayasaikishanchapparam.github.io/altara/demo/&quot;&gt;live demo&lt;/a&gt; of the dashboard if you want to see it move before installing anything. The live demo goes a step past the snippet in this post, with a streaming event log and the real wiring block included, both of which are exactly what we built above.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22778/building-a-real-time-drone-ground-control-station-in-react</guid>
<pubDate>Wed, 15 Jul 2026 21:38:10 +0000</pubDate>
</item>
<item>
<title>RobotsGov by ThinkNEO</title>
<link>https://coderlegion.com/21424/robotsgov-by-thinkneo</link>
<description>&lt;p&gt;RobotsGov — runtime governance for robots that think&lt;br&gt;
Most &quot;robot safety&quot; stops at the body: collision avoidance, e-stops, functional safety. That's necessary, but it doesn't answer a different question — will the AI inside the robot do something it was never authorized to do?&lt;br&gt;
RobotsGov is a control plane that decides what a robot is allowed to do and enforces it before the action executes — not a dashboard of what already happened. Safety, compliance, and intent gated at runtime, across industrial fleets and domestic robots.&lt;br&gt;
How it works&lt;br&gt;
A governed control topic sits in front of the robot's native command interface. Every verb the AI emits is evaluated against tenant policy before it reaches the hardware:&lt;/p&gt;
&lt;p&gt;Allow — in-scope commands pass through unchanged&lt;br&gt;
Block — out-of-policy verbs are denied at the gate; they never become commands on the wire&lt;br&gt;
Mask / HITL — sensitive actions are minimized or held for human approval&lt;/p&gt;
&lt;p&gt;Each decision is written to a tamper-evident signed ledger (Ed25519, hash-chained). p99 enforcement latency is 0.825 ms — below the noise floor of the robot's own control loop.&lt;br&gt;
Proven end-to-end on real humanoid SDKs&lt;br&gt;
This isn't a mock. We took a publicly available humanoid SDK and put our governance bridge in front of it. The governed topic translates each verb to the robot's native protocol (DDS → LCM), and we verified on the wire that forbidden verbs (e.g. high-risk locomotion, fall-to-ground) never became commands — while authorized ones passed through governed and logged. Same model that governs cloud agents, now governing a physical robot.&lt;br&gt;
Open at the base&lt;br&gt;
RobotsGov runs on two open protocols, published with permanent DOIs under Apache-2.0:&lt;/p&gt;
&lt;p&gt;AIRGP — Artificial Intelligence Runtime Governance Protocol (DOI 10.5281/zenodo.20001903)&lt;br&gt;
A2ASTC — Agent to Agent Software Team Compliance (DOI 10.5281/zenodo.20002917)&lt;/p&gt;
&lt;p&gt;The standard is open and auditable — governance shouldn't be locked to a single vendor.&lt;br&gt;
One governance model, from cloud to robotics.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://robotsgov.com&quot;&gt;https://robotsgov.com&lt;/a&gt;&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/21424/robotsgov-by-thinkneo</guid>
<pubDate>Fri, 26 Jun 2026 17:48:44 +0000</pubDate>
</item>
<item>
<title>Governing a Robot Is the Same Problem Twice: Real-Time Commands and the Training Pipeline</title>
<link>https://coderlegion.com/19315/governing-a-robot-is-the-same-problem-twice-real-time-commands-and-the-training-pipeline</link>
<description>&lt;p&gt;A robot that has learned to do something dangerous is more dangerous than one that never learned at all, because you trusted it.&lt;br&gt;
That sentence is the whole reason this post exists. Most conversations about &quot;AI safety in robotics&quot; stay at the level of philosophy — alignment, intent, the long-term questions. Those matter. But there is a much more immediate, much more boring problem sitting in front of anyone actually putting an autonomous robot into the world right now, and it has nothing to do with philosophy. It's this: an autonomous robot makes decisions, and those decisions become physical motion, and physical motion has consequences you cannot take back. Between the decision and the motion, something has to be allowed to say no.&lt;br&gt;
I've spent the last while building exactly that &quot;something&quot; — a governance layer — and wiring it into two of the standard open-source robotics frameworks rather than any single manufacturer's proprietary SDK, precisely because the problem is universal and the solution should be too. I'm not going to show you how the governance core works; that's ours, and a blog post is the wrong place for it anyway. What I can do is show you where it plugs in, and why governing a robot turns out to be the same problem in two completely different places. If you work with ROS 2 or with HuggingFace's LeRobot, the integration points will be immediately recognizable, and that recognizability is the point: the gate goes exactly where you'd expect it to, if you were being honest about where danger enters.&lt;br&gt;
Problem one: the command, in real time (ROS 2)&lt;br&gt;
If you've touched ROS 2, you know /cmd_vel. It's the topic that carries velocity commands — a Twist message with linear and angular components — from whatever is doing the thinking to whatever is doing the moving. A planner, a teleop node, or a learned policy publishes to /cmd_vel, and the robot's base controller turns that into wheels and motors actually moving.&lt;br&gt;
Here is the thing nobody emphasizes enough: /cmd_vel is a firehose pointed at the physical world, and in a default setup, nothing stands between the thing producing commands and the wheels. Whatever publishes, moves. If the policy has a bad moment — and learned policies have bad moments — the bad command goes straight to the motors. The architecture trusts the producer completely.&lt;br&gt;
The governance approach is almost embarrassingly simple to describe, which is part of why it works: don't let the planner publish to /cmd_vel directly. Insert a relay. The planner publishes its intended command to a different topic — call it /cmd_vel_proposed — and a governor node is the only thing that publishes to the real /cmd_vel. The governor subscribes to the proposals, evaluates each one, and republishes to the live topic only the commands that pass. A command that violates policy — exceeds a linear or angular velocity limit, arrives while an emergency stop is latched — does not get forwarded. It gets replaced with a zero Twist, and the decision gets logged.&lt;br&gt;
The shape of that node is exactly what a ROS 2 developer would sketch on a whiteboard:&lt;br&gt;
pythonclass CommandRelayGate(Node):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def __init__(self):
    super().__init__('thinkneo_governor')
    self.sub = self.create_subscription(
        Twist, '/cmd_vel_proposed', self.on_proposed, 10)
    self.pub = self.create_publisher(Twist, '/cmd_vel', 10)
    self.governor = ActionGovernor(...)   # the part I'm not detailing

def on_proposed(self, cmd: Twist):
    decision = self.governor.evaluate(cmd)   # PASS / BLOCK + reason
    if decision.allowed:
        self.pub.publish(cmd)
    else:
        self.pub.publish(Twist())            # zero — full stop
        # decision is recorded out-of-band, with its reason
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole topology. Subscribe to proposals, evaluate, forward or zero-out. What I'm deliberately leaving as ActionGovernor(...) — how it evaluates, what the policy model is, how limits and e-stop state are represented and checked — is the part that's ours. But notice what's not hidden, because it can't be and shouldn't be: the relay pattern itself is just good engineering. The governor sits at the one chokepoint where intent has been expressed but motion has not yet happened. That's the only place a real-time guarantee can exist. Govern upstream of the planner and you're trusting the planner. Govern downstream of the motors and you're cleaning up an accident.&lt;br&gt;
One design decision worth calling out, because it's a tell for whether someone has actually run this: the evaluation core is kept completely free of ROS itself. It has no rclpy dependency. That sounds like a small thing and it is not. It means the governance logic can be unit-tested without a running robot, without a simulator, without ROS installed at all — you feed it commands and assert on decisions. We have a mock test suite that does exactly this. If your safety-critical logic can only be tested by standing up the whole robot stack, you will not test it enough, and &quot;not enough&quot; is not an acceptable amount of testing for the code that decides whether a machine moves.&lt;br&gt;
Problem two: the command, before it ever exists (LeRobot)&lt;br&gt;
Now the same problem, somewhere completely different.&lt;br&gt;
HuggingFace's LeRobot is one of the cleanest open frameworks for training robot policies — imitation learning, reinforcement learning, the whole loop of teaching a robot to do a task from data. And here's the uncomfortable insight that connects it to the /cmd_vel gate above: every dangerous real-time command was, at some point, a checkpoint that got promoted. The bad moment a policy has in production was trained into it. The governor at /cmd_vel catches the symptom. But the disease entered the system earlier, when a model that learned the wrong thing was allowed to graduate from &quot;a checkpoint&quot; to &quot;the policy we deploy.&quot;&lt;br&gt;
So governance can't only live at runtime. It has to live in the training pipeline too, at the moment a checkpoint is about to be trusted.&lt;br&gt;
LeRobot, like most modern training frameworks, gives you a callback interface — hooks that fire during training and evaluation. That's the integration point, and again it's exactly where you'd put it if you were thinking clearly. A governance callback does two things during a training run. First, it streams the train and eval metrics out to a trace — a durable record of how this model actually behaved as it learned, not a number someone wrote down once and lost. Second, and this is the one that matters, it applies a promotion gate: when a checkpoint is a candidate to be promoted, the gate evaluates it and returns PASS or BLOCK. A checkpoint that doesn't clear the bar does not get promoted, regardless of how good its headline metric looks.&lt;br&gt;
The shape, again, is the framework's own callback surface, nothing exotic:&lt;br&gt;
pythonclass GovernancePromotionCallback:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def on_eval_end(self, metrics: dict):
    trace.record(metrics)                  # streamed to the trace
    verdict = promotion_gate.check(metrics) # PASS / BLOCK
    if verdict.blocked:
        raise PromotionBlocked(verdict.reason)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What promotion_gate.check actually weighs — that's the part I'm not handing over. But the principle is the same one as the ROS 2 gate, rotated ninety degrees. There is a moment of transition — proposed command becomes real command; candidate checkpoint becomes deployed policy — and governance is the thing that sits at that transition and can refuse it. I've watched this gate do its job on a live run: a real trace, a checkpoint that looked acceptable on its primary metric, and the gate correctly blocking the promotion. That &quot;correctly&quot; is doing a lot of work, and it's the part that took the real engineering — but the architecture of catching it at promotion time is the transferable idea.&lt;br&gt;
Why these are the same problem&lt;br&gt;
Step back and the two integrations rhyme exactly.&lt;br&gt;
In ROS 2, the dangerous thing is a command that's about to reach the motors, and the gate sits between /cmd_vel_proposed and /cmd_vel. In LeRobot, the dangerous thing is a policy that's about to be deployed, and the gate sits between &quot;candidate checkpoint&quot; and &quot;promoted.&quot; Different timescale — milliseconds versus a training run — but identical structure: a transition from intent to consequence, with an enforceable checkpoint placed precisely at the transition.&lt;br&gt;
That structural sameness is not a coincidence, and noticing it is the entire thesis of how I think about governing autonomous systems. It doesn't matter whether the agent is a velocity command, a model checkpoint, a tool call in a software agent, or a humanoid deciding to take a step. Wherever a decision is about to become an effect you can't undo, that's where the gate goes. Everything else — the policies, the limits, the scoring — is implementation. The architecture is just: find the transition, put an enforceable, logged checkpoint on it, and make sure that checkpoint can say no.&lt;br&gt;
The two frameworks share a common governance core underneath, with a control-plane client that also works offline — because a robot that loses its network connection should not lose its ability to be governed; if anything it needs it more. Both integrations ship with mock test suites that run without the framework even installed, for the reason I gave above: safety logic you can't easily test is safety logic you won't test.&lt;br&gt;
I'm not publishing the core, and I won't pretend the hard part is the plumbing I've shown you — the hard part is what's inside the boxes I left as .... But I wanted to show that it's real, running, and wired into the actual tools the robotics community uses, rather than gesturing at a future where someone, someday, governs their robots. The integration points are the honest part. The gate goes where danger enters. In ROS 2 that's one topic upstream of the motors. In LeRobot that's one callback before a checkpoint is trusted. Same problem, twice.&lt;/p&gt;
&lt;p&gt;Thinkneo AI builds an AI governance layer for autonomous systems, with integrations for ROS 2 and HuggingFace LeRobot among others. We write about what it actually takes to make AI safe in production — physical or otherwise.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19315/governing-a-robot-is-the-same-problem-twice-real-time-commands-and-the-training-pipeline</guid>
<pubDate>Sat, 30 May 2026 18:21:31 +0000</pubDate>
</item>
<item>
<title>A drone that works without GPS!!! No Kidding</title>
<link>https://coderlegion.com/16361/a-drone-that-works-without-gps-no-kidding</link>
<description>&lt;h2 id=&quot;ghostpilot-teaching-drones-to-navigate-without-gps-using-visual-slam-and-agentic-ai&quot;&gt;GhostPilot: Teaching Drones to Navigate Without GPS Using Visual SLAM and Agentic AI&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;By Aman Sachan, 18-year-old robotics enthusiast&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Hello friends and fellow builders!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Imagine you're flying a drone through a dark warehouse, a dense forest, or a GPS-jammed zone. Satellites? No signal. But your drone still knows exactly where it is, understands your natural language instructions, plans its path, avoids obstacles, and completes the mission. That's not sci-fi anymore — that's &lt;strong&gt;GhostPilot&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A few days ago, Vanessa Paul from CoderLegion reached out after reading my dev.to post, and I'm thrilled to expand on it here for you all. As an 18-year-old who's been tinkering with robots and code, I built GhostPilot to solve a real pain point in a fun, educational way. Let's dive in together, step by step, like we're sitting in an old workshop with tools scattered around.&lt;/p&gt;
&lt;h4 id=&quot;the-honest-problem-with-today-s-drones&quot;&gt;The Honest Problem with Today's Drones&lt;/h4&gt;
&lt;p&gt;Drones are incredible, but most become nearly useless without GPS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Jamming attacks&lt;/strong&gt; (real-world examples from conflicts show 85%+ loss rates)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Indoor environments&lt;/strong&gt;, urban canyons, or heavy tree cover&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signal multipath&lt;/strong&gt; errors that destroy accuracy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Expensive military systems solve this, but they're out of reach for most of us. Academic SLAM projects often have broken code after the paper is published.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GhostPilot&lt;/strong&gt; is my open-source answer: a practical, educational navigation stack that runs on affordable hardware and uses modern AI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/AmSach/GhostPilot&quot;&gt;https://github.com/AmSach/GhostPilot&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;what-ghostpilot-actually-does&quot;&gt;What GhostPilot Actually Does&lt;/h4&gt;
&lt;p&gt;GhostPilot combines three powerful ideas into one cohesive system:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Visual-Inertial SLAM&lt;/strong&gt; — The drone's &quot;eyes + inner ear&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic AI Mission Planner&lt;/strong&gt; — Natural language → smart actions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nav2 Navigation&lt;/strong&gt; — Reliable path planning and obstacle avoidance&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can literally tell the drone:&lt;br&gt;
&lt;em&gt;&quot;Fly to the third floor, check each room for occupants, then land at the helipad.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And it works (in simulation today, on real hardware soon).&lt;/p&gt;
&lt;h4 id=&quot;breaking-it-down-like-a-patient-teacher&quot;&gt;Breaking It Down Like a Patient Teacher&lt;/h4&gt;
&lt;h5 id=&quot;part-1-visual-inertial-slam-how-the-drone-knows-where-am-i-&quot;&gt;Part 1: Visual-Inertial SLAM (How the Drone Knows &quot;Where Am I?&quot;)&lt;/h5&gt;
&lt;p&gt;SLAM stands for &lt;strong&gt;Simultaneous Localization and Mapping&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your camera (e.g. Intel RealSense D435i) sees visual features in the environment.&lt;/li&gt;
&lt;li&gt;The IMU (Inertial Measurement Unit) measures acceleration and rotation hundreds of times per second.&lt;/li&gt;
&lt;li&gt;Algorithms like &lt;strong&gt;VINS-Mono&lt;/strong&gt; fuse both to create accurate 6 degrees-of-freedom pose (position + orientation) while building a map.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why this fusion is magical&lt;/strong&gt;: Cameras are great for long-term accuracy but can suffer from blur or lighting issues. IMU is fast and smooth but drifts quickly. Together? Robust magic.&lt;/p&gt;
&lt;p&gt;In GhostPilot, I've prepared a clean ROS2 wrapper (&lt;code&gt;slam_node.py&lt;/code&gt;). Full VINS-Mono integration is the next exciting milestone.&lt;/p&gt;
&lt;h5 id=&quot;part-2-agentic-ai-giving-drones-real-intelligence&quot;&gt;Part 2: Agentic AI – Giving Drones Real Intelligence&lt;/h5&gt;
&lt;p&gt;This is my favorite part.&lt;/p&gt;
&lt;p&gt;Traditional systems need you to program exact waypoints. GhostPilot uses an LLM-powered parser that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understands natural English commands&lt;/li&gt;
&lt;li&gt;Breaks them into structured navigation goals&lt;/li&gt;
&lt;li&gt;Adds safety constraints&lt;/li&gt;
&lt;li&gt;Feeds everything to the navigation stack&lt;/li&gt;
&lt;li&gt;Monitors and can replan if needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's like having a thoughtful co-pilot who speaks your language.&lt;/p&gt;
&lt;h5 id=&quot;part-3-nav2-the-trusted-foundation&quot;&gt;Part 3: Nav2 – The Trusted Foundation&lt;/h5&gt;
&lt;p&gt;Nav2 (from the ROS2 ecosystem) is industry-grade software for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Global path planning&lt;/li&gt;
&lt;li&gt;Local obstacle avoidance&lt;/li&gt;
&lt;li&gt;Recovery behaviors when things go wrong&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GhostPilot bridges the SLAM pose directly into Nav2 so everything works together seamlessly.&lt;/p&gt;
&lt;h4 id=&quot;system-architecture-simple-diagram-in-text-&quot;&gt;System Architecture (Simple Diagram in Text)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Natural Language Command
          ↓
[Agentic Mission Planner - LLM]
          ↓
Structured Goals + Safety Rules
          ↓
[Nav2 Stack - Planning &amp;amp; Control]
     ↑               ↑
SLAM Pose     Sensor Data (Camera + IMU)
          ↑
   Edge Hardware (Jetson Orin / Pi 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything runs locally. No cloud dependency.&lt;/p&gt;
&lt;h4 id=&quot;hardware-amp-getting-started-super-practical-&quot;&gt;Hardware &amp;amp; Getting Started (Super Practical)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Recommended (but start cheap):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compute: NVIDIA Jetson Orin (powerful) or Raspberry Pi 5&lt;/li&gt;
&lt;li&gt;Camera: Intel RealSense D435i (stereo + IMU)&lt;/li&gt;
&lt;li&gt;Flight Controller: PX4 with MAVLink&lt;/li&gt;
&lt;li&gt;Frame: Any compatible quadcopter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Quick Start (Simulation First!)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/AmSach/GhostPilot.git
cd GhostPilot

./scripts/setup_jetson.sh   # or manual ROS2 Humble setup

# Terminal 1 - Simulation
ros2 launch ghostpilot_gazebo indoor_warehouse.launch.py

# Terminal 2 - Agent
ros2 run ghostpilot_agent mission_parser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Try giving it commands and watch it move! There's also a lightweight &lt;code&gt;simulate.py&lt;/code&gt; for quick testing.&lt;/p&gt;
&lt;h4 id=&quot;current-status-full-transparency-&quot;&gt;Current Status (Full Transparency)&lt;/h4&gt;
&lt;p&gt;This is an early but solid foundation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mission parser + executor ✅&lt;/li&gt;
&lt;li&gt;Core bridges and tests ✅&lt;/li&gt;
&lt;li&gt;Full SLAM + real hardware flights → In progress (help wanted!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Roadmap&lt;/strong&gt; includes better SLAM options (ORB-SLAM3 too), multi-drone support, richer simulations, and real flight testing.&lt;/p&gt;
&lt;h4 id=&quot;why-i-built-this-at-18&quot;&gt;Why I Built This at 18&lt;/h4&gt;
&lt;p&gt;I love robotics because it combines so many fields, computer vision, control theory, AI, hardware, and real-world problem solving. GPS fragility bothered me, so I started building. GhostPilot isn't perfect yet, but it's open, documented, and designed for others to learn from and improve.&lt;/p&gt;
&lt;h4 id=&quot;come-build-with-me&quot;&gt;Come Build With Me&lt;/h4&gt;
&lt;p&gt;Whether you're a student, hobbyist, or experienced roboticist, there's a place for you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Improve documentation&lt;/li&gt;
&lt;li&gt;Help with SLAM integration&lt;/li&gt;
&lt;li&gt;Enhance the AI agent&lt;/li&gt;
&lt;li&gt;Test on real hardware&lt;/li&gt;
&lt;li&gt;Create new simulation worlds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pull requests are warmly welcome!&lt;/p&gt;
&lt;h4 id=&quot;me-y-all-really-wanna-know-about-me-&quot;&gt;Me? (Y'all really wanna know about me? )&lt;/h4&gt;
&lt;p&gt;Well, im mostly a bitchy 18yo but ure open to connect with me here, also, don't hate on me for taking help of AI in building this (and also writing this post but I won't admit)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/theamansachan&quot;&gt;linkedin.com/in/theamansachan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/AmSach&quot;&gt;github.com/AmSach&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instagram&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://instagram.com/i.amsach&quot;&gt;instagram.com/i.amsach&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Happy flying (and coding)!&lt;/em&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16361/a-drone-that-works-without-gps-no-kidding</guid>
<pubDate>Sat, 02 May 2026 03:17:51 +0000</pubDate>
</item>
<item>
<title>Understanding and Coding a Swerve Module in FRC</title>
<link>https://coderlegion.com/6643/understanding-and-coding-a-swerve-module-in-frc</link>
<description>&lt;p&gt;Swerve drive is one of the most flexible and rewarding drivetrain systems in FRC. It allows your robot to move in any direction — forward, sideways, diagonally — while keeping its orientation fixed or rotating independently. That control comes at the cost of complexity: four independently steered modules, each with its own motor pair, encoder, and PID logic.&lt;/p&gt;
&lt;p&gt;In this post, we’ll walk through the foundation of a &lt;strong&gt;swerve module implementation in WPILib using CTRE’s Phoenix 6 API&lt;/strong&gt;. By the end, you should understand not only &lt;em&gt;how&lt;/em&gt; to code a swerve module, but &lt;em&gt;why&lt;/em&gt; each piece matters. This article is written both for rookie programmers and experienced teams who want a clean, modular reference.&lt;/p&gt;
&lt;h4 id=&quot;the-idea-behind-swerve&quot;&gt;The Idea Behind Swerve&lt;/h4&gt;
&lt;p&gt;A traditional tank drive uses two sides of motors: one for the left, one for the right. To turn, one side spins faster than the other. This works well, but it means your robot can’t move sideways without turning first.&lt;/p&gt;
&lt;p&gt;Swerve drive changes that completely. Instead of linking wheels in pairs, &lt;strong&gt;each wheel (or module)&lt;/strong&gt; can rotate independently to face any direction and drive at any speed.&lt;/p&gt;
&lt;p&gt;So what does that enable?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The robot can move &lt;strong&gt;in any direction&lt;/strong&gt; instantly — forwards, sideways, diagonally.&lt;/li&gt;
&lt;li&gt;It can &lt;strong&gt;rotate and translate at the same time&lt;/strong&gt;, something impossible in tank drive.&lt;/li&gt;
&lt;li&gt;It makes autonomous paths smoother and more efficient, since motion is continuous rather than segmented.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mathematically, this is made possible by &lt;em&gt;vector addition.&lt;/em&gt; Each wheel’s motion contributes a velocity vector. By combining all four vectors, the robot can produce any movement it wants — translation, rotation, or both simultaneously.&lt;/p&gt;
&lt;p&gt;Each swerve module contains the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Turning motor&lt;/li&gt;
&lt;li&gt;Driving motor&lt;/li&gt;
&lt;li&gt;External encoder&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It makes a movement like this possible:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miro.medium.com/v2/resize:fit:1129/1*4Js_HlsmrjROPEUe3StXzw.gif&quot; alt=&quot;Swerve module movement&quot;&gt;&lt;br&gt;
&lt;em&gt;Screen recorded from &lt;a rel=&quot;nofollow&quot; href=&quot;https://pathplanner.dev/home.html&quot;&gt;PathPlanner&lt;/a&gt; GUI&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;defining-constants&quot;&gt;Defining Constants&lt;/h3&gt;
&lt;p&gt;Before writing any logic, we define our constants. These govern tuning parameters, motor calibration, and physical conversions. Keeping them centralized makes it easier to iterate later.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Constants {
    public static final double kPModuleTurningController = 0.5;
    public static final double kPModuleDriveController = 0;
    
    public static final double kMaxSpeedMetersPerSecond = 4.0;
    // TODO: 'EncoderDistancePerPulse' should be calculated based on the gearing and wheel diameter
    public static final double kDriveEncoderDistancePerPulse = 0.0011265396; 
    
    public static final String CAN_BUS_NAME = &quot;CANivore&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kPModuleTurningController&lt;/code&gt;: Controls how aggressively the module turns toward its target angle.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kPModuleDriveController&lt;/code&gt;: Starts at zero; increase later when tuning drive responsiveness.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;kDriveEncoderDistancePerPulse&lt;/code&gt; constant translates motor encoder readings into meters or inches traveled, you can change the units you use by changing this constant too — just try to keep everything consistent. You’ll need to calculate this using your wheel diameter and gear reduction.&lt;/p&gt;
&lt;p&gt;Finally, specifying the CAN bus name (“CANivore” in this example) helps WPILib locate your devices if you use multiple buses. If you are not using any additional CAN bus and directly connect it to roborio you don’t need this constant.&lt;/p&gt;
&lt;h3 id=&quot;constructing-the-swerve-module&quot;&gt;Constructing the Swerve Module&lt;/h3&gt;
&lt;p&gt;Now we move into the &lt;code&gt;SwerveModule&lt;/code&gt; class, which is what we will use for the rest of the tutorial. There are 4 swerve modules in each robot, so we wanted to have a unified way of controlling these motors. &lt;code&gt;SwerveModule&lt;/code&gt; class allows just that.&lt;/p&gt;
&lt;p&gt;Here is the field of the class where we initialize our core motors and controllers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SwerveModule {
  private final TalonFX m_driveMotor;
  private final TalonFX m_turningMotor;
  private final AnalogInput m_turningEncoderInput;

  private double turningMotorOffsetRadians;
  private double m_driveMotorGain;

  private final PIDController m_drivePIDController =
      new PIDController(Constants.kPModuleDriveController, 0, 0);
  private final PIDController m_turningPIDController =
      new PIDController(Constants.kPModuleTurningController, 0, 0.0001);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We first set our two motors — one for forward/backward and one for turning. In our case, these are TalonFX, but it is also possible to use SparkMax or other motors by changing some of the code. &lt;code&gt;AnalogInput&lt;/code&gt; is used as the encoder; it is an external encoder, not the built-in encoder — more on that later in the post.&lt;/p&gt;
&lt;p&gt;Then the &lt;code&gt;turningMotorOffsetRadians&lt;/code&gt; and &lt;code&gt;m_driveMotorGain&lt;/code&gt; are both values that need to be tuned to actually make the front of the robot front — eg, the 0 degrees of the swerve module might be facing some diagonal direction, so we want to add an offset to zero it to the direction that we desire.&lt;/p&gt;
&lt;p&gt;PIDController for driving is more about improving accuracy and having a smoother drive; however, it is not required. On the other hand, the &lt;code&gt;PIDController&lt;/code&gt; for turning is something you need to use and fine-tune to get better results in rotation, which would significantly impact the speed of the robot.&lt;/p&gt;
&lt;h3 id=&quot;initialization-and-setup&quot;&gt;Initialization and Setup&lt;/h3&gt;
&lt;p&gt;Inside the constructor, we initialize hardware and configure basic parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public SwerveModule(
      int driveMotorChannel,
      int turningMotorChannel,
      int analogEncoderPort,
      double turningMotorOffsetRadians,
      double driveMotorGain
      ) {

    m_driveMotor = new TalonFX(driveMotorChannel, new CANBus(Constants.CAN_BUS_NAME));
    m_turningMotor = new TalonFX(turningMotorChannel,  new CANBus(Constants.CAN_BUS_NAME));

    m_driveMotor.setNeutralMode(NeutralModeValue.Brake);
    m_turningMotor.setNeutralMode(NeutralModeValue.Brake);

    this.turningMotorOffsetRadians = turningMotorOffsetRadians;

    m_driveMotorGain = driveMotorGain;

    m_turningEncoderInput = new AnalogInput(analogEncoderPort);
    
    m_turningPIDController.enableContinuousInput(-Math.PI, Math.PI);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We start off by initializing our two motors — again, you may not need the second parameter if you are directly connecting the motors to RoboRio.&lt;/p&gt;
&lt;p&gt;We set the mode of both motors to &lt;code&gt;Brake&lt;/code&gt; as we want to be able to quickly change direction and move.&lt;/p&gt;
&lt;p&gt;We set the tuning variables to the parameters. And initialize the encoder input with the given port.&lt;/p&gt;
&lt;h3 id=&quot;why-enablecontinuousinput-matters&quot;&gt;Why enableContinuousInput() Matters&lt;/h3&gt;
&lt;p&gt;A PID controller normally drives the error between the current and target values. Without continuous input, going from 0° to 360° would make the motor rotate a full revolution — even though those angles are equivalent.&lt;/p&gt;
&lt;p&gt;By calling:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;m_turningPIDController.enableContinuousInput(-Math.PI, Math.PI);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We tell the controller that &lt;code&gt;-pi&lt;/code&gt; and &lt;code&gt;pi&lt;/code&gt; represent the same point, allowing it to take the &lt;em&gt;shortest&lt;/em&gt; path between two angles.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miro.medium.com/v2/resize:fit:947/1*lfN3TbC2f8_GUXioAEkuFg.png&quot; alt=&quot;PID continuous input&quot;&gt;&lt;br&gt;
&lt;em&gt;Left is how the PID views the value before continuous input, and right is how it views the values after continuous input.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; WPILib’s &lt;code&gt;SwerveModuleState&lt;/code&gt; also uses radians, so keeping everything in radians avoids unnecessary conversions and rounding errors.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;getting-the-module-s-state&quot;&gt;Getting the Module’s State&lt;/h3&gt;
&lt;p&gt;Before commanding motion, you need a way to know what each module is doing right now — its &lt;strong&gt;current wheel angle&lt;/strong&gt; and &lt;strong&gt;drive velocity&lt;/strong&gt;. That’s where the following helper methods come in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public SwerveModuleState getState() {
    return new SwerveModuleState(
        m_driveMotor.getVelocity().getValueAsDouble(),
        new Rotation2d(getTurningEncoderRadians())
    );
}

public SwerveModulePosition getPosition() {
    return new SwerveModulePosition(
        m_driveMotor.getPosition().getValueAsDouble() * Constants.kDriveEncoderDistancePerPulse,
        new Rotation2d(getTurningEncoderRadians())
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getState()&lt;/code&gt; returns the &lt;strong&gt;instantaneous velocity and orientation&lt;/strong&gt; of the wheel — this is what the kinematics class uses to estimate robot motion in real time.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getPosition()&lt;/code&gt; reports the &lt;strong&gt;total distance traveled&lt;/strong&gt; and current orientation, which odometry uses for tracking the robot’s pose across the field.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See how both of them use &lt;code&gt;getTurningEncoderRadians()&lt;/code&gt; which converts the analog voltage from the encoder into a meaningful angle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public double getTurningEncoderRadians(){
    double angle = (1.0 - (m_turningEncoderInput.getVoltage()/RobotController.getVoltage5V())) * 2.0 * Math.PI + turningMotorOffsetRadians;
    angle %= 2.0 * Math.PI;
    if (angle &amp;lt; 0.0) {
        angle += 2.0 * Math.PI;
    }

    return angle;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To summarize, the encoder returns a value — in volts — between 0V and 5V. We can use this to map out a full rotation, as there is a linear relationship between encoder value and the angle. We can divide this encoder value by 5V to get a range 0–1, and that would correspond to 0–2pi rad. So we just multiply it by 2pi and add &lt;code&gt;turningMotorOffsetRadians&lt;/code&gt;, depending on the front of the robot and the zero position of the encoder.&lt;/p&gt;
&lt;p&gt;An important note here is that subtracting from 1 in the equation depends on the placement of the encoder, so you may want to see if the change in angle makes sense and if it is reversed, just remove “1 -”.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miro.medium.com/v2/resize:fit:1169/1*o1J6iOcnhvnNxpsVEHlnzA.png&quot; alt=&quot;Encoder angle calculation&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;commanding-the-module-setdesiredstate-&quot;&gt;Commanding the Module — &lt;code&gt;setDesiredState()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is the core function of your &lt;code&gt;SwerveModule&lt;/code&gt; class. It takes in a target velocity and angle (from WPILib’s &lt;code&gt;SwerveModuleState&lt;/code&gt;) and drives the motors to achieve that.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void setDesiredState(SwerveModuleState desiredState) {
    SwerveModuleState state = desiredState;

    // Prevent jittering at very low speeds
    if (Math.abs(state.speedMetersPerSecond) &amp;lt; 0.001) {
        stop();
        return;
    }

    state.optimize(new Rotation2d(getTurningEncoderRadians()));

    final double driveOutput = m_drivePIDController.calculate(
        m_driveMotor.getVelocity().getValueAsDouble(),
        state.speedMetersPerSecond
    );

    final double driveFeedForward = state.speedMetersPerSecond / Constants.kMaxSpeedMetersPerSecond;

    final double turnOutput = m_turningPIDController.calculate(
        getTurningEncoderRadians(),
        state.angle.getRadians()
    );

    m_driveMotor.set(
        MathUtil.clamp((driveOutput + driveFeedForward) * m_driveMotorGain, -1.0, 1.0)
    );
    m_turningMotor.set(turnOutput);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s unpack this:&lt;/p&gt;
&lt;h4 id=&quot;preventing-jitter&quot;&gt;Preventing Jitter&lt;/h4&gt;
&lt;p&gt;When the desired speed is extremely small (less than 0.1% of full power), the module shouldn’t bother rotating to match a new angle. That would only cause the wheel to twitch in place.&lt;br&gt;
So we check:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (Math.abs(state.speedMetersPerSecond) &amp;lt; 0.001) {
    stop();
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This line saves wear on your steering motor and reduces noise when the robot is idle.&lt;/p&gt;
&lt;h4 id=&quot;optimizing-the-wheel-angle&quot;&gt;Optimizing the Wheel Angle&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;state.optimize(new Rotation2d(getTurningEncoderRadians()));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SwerveModuleState.optimize()&lt;/code&gt; ensures the module takes the &lt;em&gt;shortest&lt;/em&gt; way to reach the target direction. For example, instead of rotating 270°, it might flip the wheel 90° and reverse drive direction — same motion, faster response.&lt;/p&gt;
&lt;p&gt;See the illustration below:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miro.medium.com/v2/resize:fit:1225/1*YLN_I5huo0Jiv_mCInOu4A.png&quot; alt=&quot;Wheel angle optimization&quot;&gt;&lt;br&gt;
&lt;em&gt;Illustration on the left rotates more compared to the right illustration to be in the same position.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;drive-output-calculation&quot;&gt;Drive Output Calculation&lt;/h4&gt;
&lt;p&gt;The drive motor’s control has two parts:&lt;/p&gt;
&lt;p&gt;First, PID drives the motor to the target speed by using the previously defined proportional, integral, and derivative values. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://frcdocs.wpi.edu/en/latest/docs/software/advanced-controls/introduction/pid-video.html&quot;&gt;See more on how PID works&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;m_drivePIDController.calculate(currentVelocity, targetVelocity)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we use feedforward to keep the wheel moving at the same velocity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;state.speedMetersPerSecond / Constants.kMaxSpeedMetersPerSecond
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are combined, scaled by &lt;code&gt;m_driveMotorGain&lt;/code&gt;, and clamped between -1 and 1 (full reverse to full forward).&lt;/p&gt;
&lt;h4 id=&quot;turning-output-calculation&quot;&gt;Turning Output Calculation&lt;/h4&gt;
&lt;p&gt;Turning is entirely handled by the PID controller:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;final double turnOutput =
    m_turningPIDController.calculate(getTurningEncoderRadians(), state.angle.getRadians());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This minimizes angular error between the current and target direction. Because we previously enabled continuous input, it always takes the shortest path across the ±pi boundary.&lt;/p&gt;
&lt;h4 id=&quot;setting-motor-outputs&quot;&gt;Setting Motor Outputs&lt;/h4&gt;
&lt;p&gt;Finally, we command the motors:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;m_driveMotor.set(
        MathUtil.clamp(
            (driveOutput + driveFeedForward) * m_driveMotorGain,
             -1.0, // min -100%
             1.0) // max +100%
    );
    m_turningMotor.set(turnOutput);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, each module runs semi-independently: it continuously reads its encoder, calculates new outputs, and steers to the desired state.&lt;/p&gt;
&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting It All Together&lt;/h3&gt;
&lt;p&gt;With &lt;code&gt;SwerveModule&lt;/code&gt; complete, you can now instantiate four of them in a drivetrain subsystem:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SwerveModule frontLeft = new SwerveModule(...);
SwerveModule frontRight = new SwerveModule(...);
SwerveModule backLeft = new SwerveModule(...);
SwerveModule backRight = new SwerveModule(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, using &lt;code&gt;SwerveDriveKinematics&lt;/code&gt;, combine their states into a robot-level motion command. WPILib’s kinematics and odometry classes handle the math — your &lt;code&gt;SwerveModule&lt;/code&gt; executes per-wheel control.&lt;/p&gt;
&lt;h3 id=&quot;next-steps-&quot;&gt;Next Steps:&lt;/h3&gt;
&lt;p&gt;After this point, you need to perform the calculations to enable these separate modules to work together in moving the robot. This could be done in a wide variety of ways, depending on how you want to organize everything, but just make sure to keep your units consistent.&lt;/p&gt;
&lt;p&gt;You can watch &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/0Xi9yb1IMyA&quot;&gt;this video&lt;/a&gt; to go deeper into swerve drive and how to code it.&lt;/p&gt;
&lt;p&gt;Here is the complete code of the &lt;code&gt;SwerveModule&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/SuleymanSade/SwerveDriveBase?source=post_page-----6dc48ee97300---------------------------------------&quot;&gt;GitHub - SuleymanSade/SwerveDriveBase&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I write about learning AI by &lt;strong&gt;building real tools&lt;/strong&gt;, not just tutorials.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I write more tutorials, personal thoughts and development stories on my blogs you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read more of my posts on &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@suleymansade09&quot;&gt;Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Let’s connect on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/suleymansade/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks for reading — let me know if you’d like to collaborate on projects similar to these or have any questions.&lt;/p&gt;
</description>
<category>Miscellaneous</category>
<guid isPermaLink="true">https://coderlegion.com/6643/understanding-and-coding-a-swerve-module-in-frc</guid>
<pubDate>Sun, 19 Oct 2025 04:09:58 +0000</pubDate>
</item>
</channel>
</rss>