<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel Romero</title>
    <description>The latest articles on DEV Community by Daniel Romero (@infoslack).</description>
    <link>https://dev.to/infoslack</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F20198%2F3009f092-4e8b-453e-8bd6-ab0b70f8c74a.png</url>
      <title>DEV Community: Daniel Romero</title>
      <link>https://dev.to/infoslack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/infoslack"/>
    <language>en</language>
    <item>
      <title>Second article on my journey with the robot, now on building a physical API. CAN bus telemetry with a Rust daemon, dashboards and alerts, and a live 3D copy of the arm mirroring the real one.</title>
      <dc:creator>Daniel Romero</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:54:20 +0000</pubDate>
      <link>https://dev.to/infoslack/second-article-on-my-journey-with-the-robot-now-on-building-a-physical-api-can-bus-telemetry-with-1dee</link>
      <guid>https://dev.to/infoslack/second-article-on-my-journey-with-the-robot-now-on-building-a-physical-api-can-bus-telemetry-with-1dee</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/infoslack/building-a-physical-api-3488" class="crayons-story__hidden-navigation-link"&gt;Building a physical API&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/infoslack" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F20198%2F3009f092-4e8b-453e-8bd6-ab0b70f8c74a.png" alt="infoslack profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/infoslack" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Daniel Romero
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Daniel Romero
                
              
              &lt;div id="story-author-preview-content-4090866" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/infoslack" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F20198%2F3009f092-4e8b-453e-8bd6-ab0b70f8c74a.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Daniel Romero&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/infoslack/building-a-physical-api-3488" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 7&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/infoslack/building-a-physical-api-3488" id="article-link-4090866"&gt;
          Building a physical API
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/robotics"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;robotics&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/fastapi"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;fastapi&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/infoslack/building-a-physical-api-3488" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/infoslack/building-a-physical-api-3488#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>api</category>
      <category>iot</category>
      <category>rust</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a physical API</title>
      <dc:creator>Daniel Romero</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:51:55 +0000</pubDate>
      <link>https://dev.to/infoslack/building-a-physical-api-3488</link>
      <guid>https://dev.to/infoslack/building-a-physical-api-3488</guid>
      <description>&lt;p&gt;In the first article I told the story of building the robot, from printing the parts and making the motors move to teleoperating with a PS4 controller, collecting demonstrations, and training a model that picks bottles off a table. At the end I mentioned an idea I've been calling a physical API, a layer that exposes what the robot does in the physical world as data, meaning telemetry, history, and the feedback that guides future training. This article is about the first big piece of that layer, observability.&lt;/p&gt;

&lt;p&gt;While a model or the teleop runs, my view of the hardware is whatever the terminal prints. For a quick test that's enough, but the failures I want to avoid don't announce themselves there, like a motor slowly overheating across twenty minutes of episodes, the power supply voltage dropping when all six joints pull at once, or the control loop quietly running below the rate the whole design assumes. Observability is how I get to see these things coming, instead of discovering them after a burned motor or a session of bad data, and it's also how I learn what normal looks like, so the abnormal stands out. What made it possible to build without much effort is that the raw material was already available. Every one of those answers travels on the CAN bus the whole time, and I just had to listen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listening to the bus
&lt;/h2&gt;

&lt;p&gt;In MIT mode, every command a motor receives generates a reply. Each reply frame carries the motor's position, velocity, torque, and temperature, packed in 8 bytes. With six motors commanded 100 times per second, that's around 600 frames per second of live hardware state on the wire, feeding the memory boxes the driver keeps per motor and then being discarded.&lt;/p&gt;

&lt;p&gt;CAN is a broadcast medium. Every node on the bus sees every frame, and reading costs the other participants nothing. So the telemetry problem reduces to a program that opens the same bus, decodes the frames that pass by, and ships them somewhere useful, without ever transmitting. The control loop doesn't know it exists.&lt;/p&gt;

&lt;p&gt;I wrote that program in Rust and called it sentinel. It runs on the Raspberry Pi in the robot's head, next to the teleop but in a separate process, and its structure is small. One thread reads the CAN socket and decodes status frames, a second thread wakes up once per second, drains what accumulated, and POSTs the batch to my API over HTTP. Between the two sits a bounded channel. The reader also doesn't keep everything it hears. Each motor reports its state 100 times per second, far more than monitoring needs, so only 10 of those samples per second go through and the rest are discarded, which makes storage ten times cheaper.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v1nh1n9usx001rrnimn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v1nh1n9usx001rrnimn.jpg" alt="Sentinel diagram" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The design decision I care most about is that telemetry is best effort. If the API is slow or offline, the channel fills, and the reader starts throwing samples away instead of queueing them, keeping only a count of what was lost. That count travels in the next batch that succeeds, so the hole in the data explains itself. I tested this by taking the API down for forty minutes during a session. Sentinel kept running on stable memory, and when the API came back, the first batch reported around 140 thousand discarded samples, which is what six motors at ten samples per second accumulate over those forty minutes, so the loss accounting checks out. A telemetry agent that grows a queue until it takes down the machine it's supposed to watch would be worse than no agent.&lt;/p&gt;

&lt;p&gt;Listening to the replies also means listening to the commands. The command frames carry the target position for each joint, so sentinel remembers the last target it saw and attaches it to every sample. The difference between where a joint was told to be and where it actually is becomes a tracking error signal, and that turned out to be one of the most informative charts. A small constant offset is just gravity working against a finite gain, a transient spike is a fast motion, and a sustained error is a joint fighting an obstacle.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the robot is quiet
&lt;/h2&gt;

&lt;p&gt;A powered but disengaged robot sends no status frames, and that's precisely when I want to watch the power supply. For that case sentinel has an active mode. It notices the bus has gone silent and starts asking the motors, one question per second, for the bus voltage and each motor's current, rotating through them so every motor gets its turn. The reply comes back on the same bus and the same decoding path picks it up.&lt;/p&gt;

&lt;p&gt;Writing to the bus sounds like it contradicts the passive design, but CAN itself resolves this. When two nodes transmit at the same time, the frame with the numerically lower identifier wins arbitration. The parameter read frame has a higher identifier than every control and status frame, so my questions always lose to control traffic. They can be delayed by the robot working, but they can never delay the robot. That property is what let me later keep the voltage readings running even during teleoperation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API and the database
&lt;/h2&gt;

&lt;p&gt;The receiving side is deliberately boring. It's a FastAPI service backed by TimescaleDB, a Postgres extension for time series that splits large tables into time-ordered chunks, so queries over a recent window stay fast no matter how much history sits behind them. Sentinel POSTs batches, the API writes rows, and a retention policy deletes anything older than thirty days.&lt;/p&gt;

&lt;p&gt;The surface of the API is small. Telemetry comes in through two endpoints, one for the joint samples and one for the voltage readings, each taking a batch per request with a key in the header. A robot doesn't need to be registered anywhere. The first batch that arrives with a new robot id creates it, and every batch after that refreshes its last seen timestamp, which is also how everything downstream decides whether a robot is online.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8cc9zyqh3dz4sos95je9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8cc9zyqh3dz4sos95je9.png" alt="The API" width="800" height="396"&gt;&lt;/a&gt;&lt;br&gt;
The reading side has three endpoints. One lists the robots, one returns a robot's current snapshot, and one returns the history of a metric for charts. The snapshot does more work than it sounds, because the pieces arrive at different rhythms. The freshest status sample of each joint, the last voltage reading, the last current measurement, and the collector's own health report all live in different rows with different timestamps, so the endpoint gathers the latest of each and merges everything into one answer. The history endpoint aggregates on the database side, grouping the raw samples into time buckets, so a window of fifteen minutes or a whole day comes back as roughly two hundred points either way, instead of a dump of raw rows.&lt;/p&gt;

&lt;p&gt;Going through an HTTP API instead of letting the collector write straight to the database is the part that makes this a layer instead of a script. The same door, with the same key, works for the next robot I add, and it's the same door the human feedback from the first article comes through, with each verdict landing next to the telemetry of the episode it judges and helping decide what goes into the next dataset. That flow deserves its own article, so I'll leave the details for a next post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grafana as the starting point
&lt;/h2&gt;

&lt;p&gt;Before writing any interface of my own, I pointed Grafana at the database. The dashboards are provisioned as code in the repo, so bringing the stack up recreates them, and within a day I had the bus voltage as a big number, circular gauges for each motor's temperature and current, the tracking error over time, and alert rules for the situations I actually fear, a motor past 50 degrees, the supply under 45 volts, or the robot silent for too long.&lt;/p&gt;

&lt;p&gt;The panels answer different kinds of questions. Temperature and current per motor tell me whether the hardware is suffering. Heat builds slowly over a long session, so the gauges creep instead of jumping, and each joint's current shows how hard it works, with the shoulder carrying most of the load and spikes marking the moments the arm meets resistance. The voltage panel watches the power supply react to that same effort. Tracking error covers the motion itself, and the position, velocity, and torque charts per joint let me scroll back through a session and see what the arm was doing at the moment something looks off. The control loop rate watches the software. And two small panels watch the watcher, the count of samples sentinel had to discard and the age of the freshest data, so I know when to distrust the rest of the dashboard. A joint filter narrows all of it to a single motor when I'm chasing something specific.&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/GRuMJ1eVoiY"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
With the real robot on, the numbers gained texture. The supply idles at 47.8 volts and dips visibly under load. The shoulder pitch motor holds around 5 newton meters doing nothing but resisting gravity. The control loop reports between 99 and 101 hertz, measured by counting status frames, which would catch the class of bug where some blocking call quietly drags the loop down.&lt;/p&gt;

&lt;p&gt;Grafana answered the question of whether I could see the robot at all, cheaply, and it taught me which visualizations matter before I invested in anything custom. It remains my engineering console.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control plane
&lt;/h2&gt;

&lt;p&gt;An engineering console works for the person who built it. For anyone else, a wall of charts asks too much, you need to know which panel matters, which threshold is normal, and which number is safe to ignore. The control plane is my answer to that. It's a web frontend in the visual language of the project, reading from the same API, where the first glance already answers the questions that matter. Is the robot online, how much power is it drawing, is anything running hot.&lt;/p&gt;

&lt;p&gt;The main page shows the fleet, one card per robot. Each card carries the robot's status, the bus voltage, the total current draw, and a table of its motors with the temperature and current of each one. These are the same signals from the Grafana dashboard, reduced to what someone standing next to the robot actually wants to know, and presented with some personality, small hand-drawn icons that come alive while the robot is online. A page that people enjoy looking at gets looked at more often, and for monitoring that's half the job.&lt;/p&gt;

&lt;p&gt;Clicking a robot opens its page. The centerpiece is a live 3D model of the arm that moves with the joint positions coming off the bus, so teleoperating the real arm makes its copy move on the page a moment later, fed by nothing but the passive listening described earlier. Around it, the page shows the health of the collector itself, loop rate, staleness and discarded samples, along with charts of voltage and tracking error over the recent window.&lt;/p&gt;

&lt;p&gt;The motors table on this page also answers a question I had no way of answering before, which exact firmware each motor is running. Every joint shows its motor model and firmware version next to the live temperature and current, so the page works as an inventory of the arm and not just a monitor. How those version numbers get there is a story of its own, and it's the next section.&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/Phjp8MZj4EQ"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
There's more planned for this page. The robot already carries three cameras for the model, and streaming them into the control plane would let a remote session show what the robot sees next to what its joints report, in the same place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inventory
&lt;/h2&gt;

&lt;p&gt;Everything up to here watches what the robot is doing. There's a second kind of question observability has to answer, which is what the robot is. A robot is an assembly of parts that each carry an identity of their own, motors of different models, boards, and the software running on all of them, and that composition changes over time. A motor gets swapped after a failure, a firmware gets updated, a package gets upgraded on the Pi. When two robots are supposed to be identical, or when one robot is supposed to be the same as it was last month, the inventory is what turns supposed into verified. In this project it started with the hardest column of the motors table, the firmware version.&lt;/p&gt;

&lt;p&gt;That column took an afternoon of digging through the motor's manual. It lists version strings among the readable parameters, but on my motors those reads come back flagged as an error. The manual also describes a dedicated version query, a frame of the same communication type as the stop command, distinguished only by a marker byte in the payload. Sending a malformed one could stop a motor, so I tested it with a throwaway probe on an idle bus, one motor at a time. All six answered.&lt;/p&gt;

&lt;p&gt;The answer came with a surprise. The reply arrives as a frame of the same type as the status frames, so sentinel, which was running while I probed, decoded those replies as telemetry and recorded a motor running at 1050 degrees. The version reply has to be recognized by its own signature and intercepted before status decoding, and that check is now the first thing the decoder does.&lt;/p&gt;

&lt;p&gt;With that settled, sentinel asks each motor for its version once at startup, when the bus is quiet, and the control plane fills in the model and firmware of every joint. The versions revealed that firmware follows the motor model, with same-model joints running identical builds, and having that confirmed in a table instead of assumed is what an inventory is for. It's also the groundwork for updating firmware over the same channel later.&lt;/p&gt;

&lt;p&gt;The motors are only the first entry. The robot is also a computer, a Raspberry Pi with an operating system, drivers, and the packages my whole stack depends on, and all of that changes over time just like firmware does. The plan is for the inventory to cover that side too, the OS version, the kernel, the versions of the packages that matter, reported through the same API. When the robot behaves differently from one week to the next, the first question is what changed, and an inventory that covers the machine end to end is what lets me answer it without guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;What exists now is a robot that reports its own state while it works. Every joint's state arrives ten times a second, voltage and current keep coming even when it sits idle, the collector reports its own health, the database holds thirty days of history, alerts cover the failures I fear, and two interfaces sit on top, Grafana for me and the control plane for everyone else. None of it interferes with the control of the robot, and almost all of it comes from frames that were already on the wire.&lt;/p&gt;

&lt;p&gt;The next pieces connect this back to learning. Dataset events will put collection sessions on the same timeline as motor temperatures. Firmware updates will go through the door the inventory opened. And the feedback flow that helps decide what a dataset keeps is the subject of the next post. The physical API I described in the first article is becoming infrastructure, one piece at a time.&lt;/p&gt;

&lt;p&gt;Thanks for following along, and see you in the next one.&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>python</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>I've been building and training my own humanoid robot from an open-source project. Wrote up the whole journey, with videos of each stage.</title>
      <dc:creator>Daniel Romero</dc:creator>
      <pubDate>Thu, 02 Jul 2026 17:23:10 +0000</pubDate>
      <link>https://dev.to/infoslack/ive-been-building-and-training-my-own-humanoid-robot-from-an-open-source-project-wrote-up-the-2n3o</link>
      <guid>https://dev.to/infoslack/ive-been-building-and-training-my-own-humanoid-robot-from-an-open-source-project-wrote-up-the-2n3o</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/infoslack/building-my-humanoid-robot-pdg" class="crayons-story__hidden-navigation-link"&gt;Building my humanoid robot&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/infoslack" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F20198%2F3009f092-4e8b-453e-8bd6-ab0b70f8c74a.png" alt="infoslack profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/infoslack" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Daniel Romero
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Daniel Romero
                
              
              &lt;div id="story-author-preview-content-4052685" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/infoslack" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F20198%2F3009f092-4e8b-453e-8bd6-ab0b70f8c74a.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Daniel Romero&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/infoslack/building-my-humanoid-robot-pdg" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 2&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/infoslack/building-my-humanoid-robot-pdg" id="article-link-4052685"&gt;
          Building my humanoid robot
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/robotics"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;robotics&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/machinelearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;machinelearning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/infoslack/building-my-humanoid-robot-pdg" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;7&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/infoslack/building-my-humanoid-robot-pdg#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            13 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>hardware</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building my humanoid robot</title>
      <dc:creator>Daniel Romero</dc:creator>
      <pubDate>Thu, 02 Jul 2026 16:08:56 +0000</pubDate>
      <link>https://dev.to/infoslack/building-my-humanoid-robot-pdg</link>
      <guid>https://dev.to/infoslack/building-my-humanoid-robot-pdg</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Y2WNm-Lk7WA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Building my humanoid robot
&lt;/h2&gt;

&lt;p&gt;In December 2025 I decided to finally work on an idea I'd had for a while: to build, set up, and train a humanoid robot. My starting point was the &lt;a href="https://github.com/kscalelabs/kbot/" rel="noopener noreferrer"&gt;K-Bot&lt;/a&gt;, an open source project from K-Scale, with open CAD and detailed documentation.&lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://docs.kscale.dev/intro" rel="noopener noreferrer"&gt;K-Scale documentation page&lt;/a&gt; I grabbed the project's &lt;a href="https://cad.onshape.com/publications/e15cf8edefacbba3009917c0/w/32b153b5c765c603865bdfc0/e/51994a5d5abaa101c938a9f1" rel="noopener noreferrer"&gt;Onshape links&lt;/a&gt; and started printing the parts. I used PLA for most of the structure, and for the parts that take more stress, the sides of the torso, I ordered them in nylon from &lt;a href="https://jlc3dp.com/" rel="noopener noreferrer"&gt;JLC3DP&lt;/a&gt;, where I wanted more strength and a bit of flex. That same CAD also holds the description of the robot's joints and links, a file I used later for simulation and to know the limits of each joint, which saved me a lot of guessing when it came to programming the motion.&lt;/p&gt;

&lt;p&gt;With the parts printed I started assembling piece by piece, checking fits, screws, cable routing, and the warping from 3D printing, reading the notes in the documentation and going through the history of their Discord conversations. This was the stage that took the most patience, because I wanted to avoid slack or bigger problems in the assembly.&lt;/p&gt;

&lt;p&gt;I bought the motors from the &lt;a href="https://www.aliexpress.com/store/1103507056" rel="noopener noreferrer"&gt;Robstride store on AliExpress&lt;/a&gt; and they took a while to arrive, but in the end it worked out. With the parts assembled and the motors in hand, the next step was to make all of it move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The motors and CAN communication
&lt;/h2&gt;

&lt;p&gt;I decided to start with the K-Bot's right arm, with six Robstride motors: five in the arm joints (pitch, roll, yaw, elbow, and wrist) and one in the gripper, of different models and sizes depending on the joint, bigger near the shoulder, where the required torque is higher, and smaller toward the tips. Each one has its own ID, and they all talk over the same CAN bus.&lt;/p&gt;

&lt;p&gt;CAN, short for &lt;a href="https://en.wikipedia.org/wiki/CAN_bus" rel="noopener noreferrer"&gt;Controller Area Network&lt;/a&gt;, is a bus that came from the automotive industry. It's two twisted wires carrying a differential signal, with several devices hanging off that same pair, usually running at 1 Mbps. Each message has an identifier, and it's that ID that also settles priority when two nodes try to talk at the same time: whoever has the lower ID wins the bus. In my case, the host sends the command frames and reads the return frames through a USB to CAN converter, a &lt;a href="https://www.pibiger-tech.com/product/savvycan-fd-x2/" rel="noopener noreferrer"&gt;SavvyCAN-FD-X2&lt;/a&gt;, which supports CAN-FD and reaches 12 Mbps, even though the motor bus runs at 1 Mbps. I picked this converter based on another open source project, &lt;a href="https://openarm.dev/" rel="noopener noreferrer"&gt;OpenArm&lt;/a&gt;. Each motor is a node with its own ID. To give an idea of the headroom, at peak use, with six motors at 100 times per second, I take up around 15% of the bus bandwidth.&lt;/p&gt;

&lt;p&gt;I tested each motor separately before integrating everything: power it up, bring up the bus, check the communication, watch the motion response and the limits of each joint. This isolated test helped me understand the behavior of each actuator and catch problems early, before sending commands to the whole robot.&lt;/p&gt;

&lt;p&gt;Each Robstride is controlled in what they call MIT mode, a scheme that became known through &lt;a href="https://news.mit.edu/2019/mit-mini-cheetah-first-four-legged-robot-to-backflip-0304" rel="noopener noreferrer"&gt;MIT's mini-cheetah quadruped&lt;/a&gt;. In a single CAN frame I send the target position, the target velocity, the stiffness and damping gains (kp and kd), and a reference torque, all packed into the 8 bytes of data in the frame. The motor itself closes the loop and computes the final torque: kp times the position error, plus kd times the velocity error, plus the torque. That lets me choose how firm or soft each joint feels just by changing the gains. A higher kd is what gave me smooth motion, right at the motor, without needing any filter in software. And the soft-stop, when I want to release the arm, is just zeroing the stiffness and leaving a light damping, so it stops without locking up abruptly.&lt;/p&gt;

&lt;p&gt;One detail that makes this scale well: I don't sit waiting for each motor's reply in the middle of the loop. The library I chose keeps a little memory box per motor with the last state it reported, position, velocity, torque, and temperature, and keeps updating that box in the background as the return frames come in over the bus. When my code asks where the motor is, it reads that memory right away, without going out to the wire. That's what lets me command the six motors at 100 Hz without choking.&lt;/p&gt;

&lt;p&gt;Getting &lt;a href="https://huggingface.co/lerobot" rel="noopener noreferrer"&gt;LeRobot&lt;/a&gt; to talk to these motors, though, took me a good while. This framework, which I use for teleoperation and training, already comes with support for Robstride motors. Except that support talks to them using the standard CAN frame, with an 11-bit identifier, in MIT mode. The motors on my K-Bot are set to Robstride's default protocol, the private mode, which uses the 29-bit extended identifier. They are two legitimate modes of the motor itself, but they don't talk to each other: with the bus on 29 bits and &lt;a href="https://github.com/huggingface/lerobot" rel="noopener noreferrer"&gt;LeRobot&lt;/a&gt; sending on 11, the motor simply wouldn't move, and without throwing any error, which threw me off quite a bit at the start. To get to 11-bit mode I'd have to send a protocol switch command to each motor and power cycle it.&lt;/p&gt;

&lt;p&gt;I had two paths, reconfigure the motors to 11-bit mode or talk to them in the mode they were already in. I went with the second, because I didn't want to have to do that protocol switch on each motor, one by one. For that I used &lt;a href="https://github.com/motorbridge/motorbridge" rel="noopener noreferrer"&gt;motorbridge&lt;/a&gt;, a driver written in Rust that speaks Robstride's private protocol on the 29-bit bus, with the same MIT command underneath. It has a wheel for aarch64, so it runs on the Raspberry Pi without any hassle. I wrapped that driver in a layer of my own application and started sending all the commands through it. That layer also solves a unit difference for free: LeRobot does the motion math in degrees and the motor uses radians, and the conversion happens on every read and write, so I don't have to remember that in the rest of the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting LeRobot
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/docs/lerobot/en/index" rel="noopener noreferrer"&gt;LeRobot is an open source library maintained by Hugging Face&lt;/a&gt; that standardizes the whole flow of teaching a robot by demonstration: you define the robot and a way to teleoperate it, record the demonstrations in a common dataset format, train a policy on top of that data, and run inference on the real robot. The base class contracts hold for any robot, so if mine follows those contracts, it drops into that pipeline and reuses the recording, training, and visualization tools that are already there.&lt;/p&gt;

&lt;p&gt;It all revolves around two ideas: a Robot, which knows how to read an observation and execute an action, and a Teleoperator, which produces an action from some input. I wrote my arm as a subclass of Robot that, underneath, sends the MIT commands through motorbridge, and I wrote the PS4 controller as a Teleoperator.&lt;/p&gt;

&lt;p&gt;The official teleoperation CLI didn't fit my case. It had the feedback sending tied to a specific robot, it didn't call the part that reads the controller buttons, and it didn't turn on the motor torque, so the arm would stay loose the whole time. So I wrote my own teleoperation command. It runs a loop around 100 times per second: reads the observation, reads the controller, computes the action, and sends it to the arm. The PS4 buttons become commands to engage the control, stop, and go back to the starting position, and there's a ramp on the gains when I engage, so it doesn't jump, plus per-joint limits so it doesn't go past what the mechanical structure can take.&lt;/p&gt;

&lt;p&gt;On the joystick, each analog stick controls the velocity of a joint: the more I tilt it, the faster it turns. On each pass of the loop I take that tilt, multiply it by the max velocity of that joint and by the time of the step, and add the result to a position target that keeps growing. Holding the stick pushes that target little by little, which gives a natural feel of steering the joint. The teleop works only with that target, and what brings the arm's real position up to it is the control at the motor.&lt;/p&gt;

&lt;p&gt;That changes how I turn on the torque. If I simply powered the motors, each one would try to go to the target stored at that moment, which is usually zero. Since the arm is almost never sitting exactly at zero, the motor would pull hard to close that gap all at once, and the arm would jerk. To avoid that, the instant I engage, before anything else I copy the current position of each joint into its target. That way the motor turns on already wanting to stay where the arm is, without moving, and only from there do the sticks start pushing the targets, with no jolt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collecting data
&lt;/h2&gt;

&lt;p&gt;With teleoperation working, I started recording demonstrations. Each demonstration is a whole episode of picking up the bottles and putting them in the basket, recorded while I teleoperate the arm myself. On each frame LeRobot stores the observation of that instant, the images from the three cameras and the state of all the joints, along with the action I ran through the controller. It's that observation-action pair, repeated frame by frame across hundreds of episodes, that becomes the &lt;a href="https://huggingface.co/spaces/lerobot/visualize_dataset?path=%2Finfoslack%2Fkbot_cappuccino_count%2Fepisode_0" rel="noopener noreferrer"&gt;training material&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While the control runs at 100 times per second, the recording happens at 30 frames per second. Storing three images and writing everything to disk on every control step would be too heavy, and 30 fps is already enough for the model to learn the motion, on top of being the rate the model I chose is trained at. LeRobot separates the data of each episode: the numeric part, state and action, goes into a table of columns, and the images from each camera are grouped into a compressed video, one per camera. Since there are thousands of frames per episode, that saves a lot of space. The image writing runs on separate threads so it doesn't stall the control loop, and the video compression happens at the end, when I close the episode.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/hvAVN2P9aW0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;An important choice was how to describe the task. In the text that goes with each episode I include the object and the quantity, something like pick up a number X of bottles and put them in the basket. That way the model has to read the instruction to know how many times to repeat the motion. The most valuable scenes are the ones where the table has more bottles than what was asked, for example three on the table and the request to pick only one. Those are what teach the model to stop at the right amount, instead of just grabbing everything in front of it.&lt;/p&gt;

&lt;p&gt;Collecting is more hands-on than it looks. I vary the position and rotation of the bottles on each episode to cover the whole workspace, and when I mess something up, grab a bottle the wrong way, drop one, or fumble in the middle, I re-record that episode instead of letting it slide, because a bad example teaches worse than a missing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing and training the model
&lt;/h2&gt;

&lt;p&gt;With the data in hand, I still had to choose the model, and to move fast I went with a VLA. VLA stands for &lt;a href="https://arxiv.org/abs/2505.04769" rel="noopener noreferrer"&gt;Vision-Language-Action&lt;/a&gt;. It's a kind of model that takes image, text, and the robot's state at the same time and produces movement as output. It starts from the models that already understand image and language, the same ones behind the assistants that can see a photo, and gains the ability to generate action, translating all of that into commands for the joints. When I show the cameras and say in text what the task is, it responds with the arm's movement.&lt;/p&gt;

&lt;p&gt;Among the open VLAs, I picked &lt;a href="https://huggingface.co/blog/smolvla" rel="noopener noreferrer"&gt;SmolVLA&lt;/a&gt;, a compact version of this kind of model, made inside the LeRobot ecosystem, from Hugging Face. Inside it has a vision and language model as a base and a part dedicated to producing action, and it comes pretrained with lots of examples from robots of many kinds. It's small enough to train and run on my GPU without much trouble. I did set up the path for a bigger model, &lt;a href="https://www.pi.website/download/pi05.pdf" rel="noopener noreferrer"&gt;pi0.5&lt;/a&gt;, but SmolVLA stayed as the main one because it's lighter and faster to iterate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fuser-attachments%2Fassets%2Fe8b3ca60-cfd0-4e42-b77a-8ed112186a80" class="article-body-image-wrapper"&gt;&lt;img width="2638" height="1542" alt="train-loss" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fuser-attachments%2Fassets%2Fe8b3ca60-cfd0-4e42-b77a-8ed112186a80"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the model chosen, I moved on to the fine-tuning: taking this model that already knows the basics of manipulating things and adjusting it with my own examples, from my robot and my task. In this fine-tuning the model still learns from my images and instructions, but only the action part gets updated, something like 100 million of the 450 million parameters, while the vision and language base stays as it was pretrained. That's what makes it fit comfortably on a single GPU (in my case an RTX 3090). It comes out much cheaper than training from scratch, and that's what let me get to a result with a few hundred demonstrations instead of thousands. The training itself is watching the loss curve drop and settle, saving several checkpoints along the way, and then testing some of them on the real arm to find the best one, which isn't always the last.&lt;/p&gt;

&lt;p&gt;At the end of this process I have a checkpoint that handles the task. What was left then was the practical part: putting this trained model in command of the arm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inference: when the model takes over the robot
&lt;/h2&gt;

&lt;p&gt;During data collection, the one generating the actions was the PS4 controller: on each pass of the loop, the teleop read the joystick, computed the joint targets, and the follower sent that to the motors. At inference, the model steps in exactly at that point. The only thing that changes in the loop is where the action comes from: where I used to read the controller, now I call SmolVLA. It gets the same observation, the camera images and the joint state, returns an action in the same format, and it goes down through the same layer to the motors. In practice, the model drives the arm through the same door I used with the controller in my hand. The difference is the pace: inference runs at 30 times per second, against the 100 of manual control, so I interpolate between one action of the model and the next to smooth the target that reaches the motors.&lt;/p&gt;

&lt;p&gt;With the action path identical between training and inference, what the model learns to produce is exactly what the robot knows how to execute, without any translation in the middle. And since the source of the action is interchangeable, if inference starts drifting off I take over the arm with the controller right away, through the same layer, without having to stop anything.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/5WbJZwKG17o"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;What the model sees comes from three USB cameras, each from a different point of view. One on the robot's head, looking forward, one on the wrist, close to the gripper, for finer manipulation, and a third fixed on a tripod above the table, giving a top view where no object gets hidden behind another. On each pass SmolVLA gets the three images along with the task text and the arm state, and from that it decides the next action. The multiple views give a better sense of depth and object position, which a single camera wouldn't, and that counts a lot when it comes to closing the gripper at the right spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Physical API
&lt;/h2&gt;

&lt;p&gt;Up to here I've talked about the whole software layer that controls the robot. The idea I've been chasing the most lately is called a &lt;a href="https://sidecar.ai/blog/the-physical-api-when-robots-become-as-easy-to-program-as-software" rel="noopener noreferrer"&gt;physical API&lt;/a&gt;. We use APIs all the time to send commands to a system and get responses, and what I'm building is a version of that for the physical world, a layer that connects what the robot does in the real world to the data, the training, and the interaction with the people around it.&lt;/p&gt;

&lt;p&gt;This starts with the hardware that stays with the robot: in the head sits a Raspberry Pi 5. It's what runs the teleoperation and the recording, sends the datasets to the training machine, and also drives a 7-inch touchscreen that became the robot's face. When idle, the screen shows an animation of blinking eyes.&lt;/p&gt;

&lt;p&gt;The first part of the physical API lives on that screen: collecting human feedback during inference. While the robot runs a task, anyone can judge right there whether that run went well or not and, when it didn't, point out what went wrong. Underneath, my teleoperation command brings up a local HTTP server. When I hit the stop, the state changes, the screen notices and swaps the eyes for the feedback window, then the answer goes back to the server and gets recorded, with the plan of using that to decide what goes into the next training.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Kxyozb4YiPU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;And the feedback is just the start. The same layer that talks to the motors gives telemetry and observability: since each motor already reports its own state on every cycle, I can track the temperature of each one and catch overheating before it turns into a problem, or check the battery health from the bus voltage. And the same channel works for maintenance, like doing a firmware update on the motors without taking anything apart.&lt;/p&gt;

&lt;p&gt;The plan is for this layer to grow beyond feedback and become the physical API I have in mind: a way to help the robot get better, in a continuous loop of use and correction, without relying only on isolated data collection sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It took me about 90 days to do everything I've described here. It was a deep dive into areas I didn't know well, and what's standing today became the base to keep going.&lt;/p&gt;

&lt;p&gt;The list of next steps is already big. I want to build the second arm, swap the joystick for a miniature replica of the robot that I teleoperate by moving a small copy instead of mapping everything on the controller, and have some parts made in aluminum, because the printed plastic structure won't take the weight of the motors of both arms.&lt;/p&gt;

&lt;p&gt;There's a lot ahead, and I plan to document every step. Thanks for following along, and see you in the next one.&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>machinelearning</category>
      <category>ai</category>
      <category>python</category>
    </item>
  </channel>
</rss>
