<?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: Le Tuan Nguyen</title>
    <description>The latest articles on DEV Community by Le Tuan Nguyen (@nguyenletuan).</description>
    <link>https://dev.to/nguyenletuan</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%2F4239556%2F5a0c257d-e4f8-4d8b-a87b-7a25ca8d5f38.png</url>
      <title>DEV Community: Le Tuan Nguyen</title>
      <link>https://dev.to/nguyenletuan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nguyenletuan"/>
    <language>en</language>
    <item>
      <title>Penumbra: an AI agent for satellites</title>
      <dc:creator>Le Tuan Nguyen</dc:creator>
      <pubDate>Wed, 11 Nov 2026 17:39:40 +0000</pubDate>
      <link>https://dev.to/nguyenletuan/penumbra-an-ai-agent-for-satellites-29ad</link>
      <guid>https://dev.to/nguyenletuan/penumbra-an-ai-agent-for-satellites-29ad</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/mlh-hackathon"&gt;MLH x DEV Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Penumbra is mission control for a fleet of 24 satellites. A satellite in low Earth orbit spends about a third of each orbit in Earth's shadow, running on its battery. If the power system fails there, operators may not see it until the next ground pass, and then they have a few minutes to send the right command.&lt;/p&gt;

&lt;p&gt;In Penumbra, a Gemini agent diagnoses the fault and proposes recovery plans. A physics simulator tests every plan against every cause that still fits the data, and a human approves the exact plan that passed. Nobody can approve a plan that failed the check, the AI included.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxvfmd0swup9ym8nenluz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxvfmd0swup9ym8nenluz.gif" alt="SAT-07: the plan-by-hypothesis matrix shows plan P-1 passing every cause; the operator approves and the plan executes" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We wanted an agent that is allowed to be wrong, in a system where its mistakes cannot reach the satellites. Most of our weekend went into the code around the model: the list of allowed commands, the verifier and the approval step. The orbits, the Sun's position and the ground stations are real. The power data and the faults are simulated, and the UI always labels which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/QapdEbnx69w" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://bullsinorbit.study/" rel="noopener noreferrer"&gt;bullsinorbit.study&lt;/a&gt;. Press START INCIDENT and three satellites start failing. One is fixed automatically under a flight rule, one waits for your approval, and one has no safe plan, so Penumbra recommends safe mode. At the end the app reveals the fault it injected, and you can check the agent's diagnosis against it. You can also hold Ctrl+Alt and describe an incident out loud.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbii7ykzvxyrrlmhqp042.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbii7ykzvxyrrlmhqp042.gif" alt="Reveal: the agent's diagnosis next to the injected fault, with the true battery curve over the verified band" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Anh-08" rel="noopener noreferrer"&gt;
        Anh-08
      &lt;/a&gt; / &lt;a href="https://github.com/Anh-08/penumbra" rel="noopener noreferrer"&gt;
        penumbra
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Anh-08/penumbra/docs/media/banner.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbanner.png" alt="Penumbra: verified AI mission ops for a 24-satellite LEO fleet. AI proposes, physics verifies, humans approve." width="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
  &lt;a href="https://www.youtube.com/watch?v=QapdEbnx69w" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbadges%2Fyoutube.svg" alt="Watch the demo on YouTube, 2:38"&gt;&lt;/a&gt;
  &lt;a href="https://devpost.com/software/penumbra-sp4gtd" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbadges%2Fdevpost.svg" alt="Read the project on Devpost"&gt;&lt;/a&gt;
  &lt;a href="https://bullsinorbit.study/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbadges%2Flive-demo.svg" alt="Live demo at bullsinorbit.study"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/Anh-08/penumbra/docs/media/badges/gemini.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbadges%2Fgemini.svg" alt="Gemini: agent and voice commands"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/Anh-08/penumbra/docs/media/badges/elevenlabs.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbadges%2Felevenlabs.svg" alt="ElevenLabs: voice callouts"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/Anh-08/penumbra/docs/media/badges/vultr.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbadges%2Fvultr.svg" alt="Vultr: public demo"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/Anh-08/penumbra/docs/media/badges/python.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbadges%2Fpython.svg" alt="Python 3.11"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/Anh-08/penumbra/docs/media/badges/ub-hacking.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fbadges%2Fub-hacking.svg" alt="UB Hacking 2026"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;b&gt;
  &lt;a href="https://www.youtube.com/watch?v=QapdEbnx69w" rel="nofollow noopener noreferrer"&gt;Demo video&lt;/a&gt; ·
  &lt;a href="https://bullsinorbit.study/" rel="nofollow noopener noreferrer"&gt;Live demo&lt;/a&gt; ·
  &lt;a href="https://devpost.com/software/penumbra-sp4gtd" rel="nofollow noopener noreferrer"&gt;Devpost&lt;/a&gt; ·
  &lt;a href="https://github.com/Anh-08/penumbra#try-it" rel="noopener noreferrer"&gt;Try it&lt;/a&gt; ·
  &lt;a href="https://github.com/Anh-08/penumbra#voice-command" rel="noopener noreferrer"&gt;Voice command&lt;/a&gt; ·
  &lt;a href="https://github.com/Anh-08/penumbra#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; ·
  &lt;a href="https://github.com/Anh-08/penumbra#run-it-locally" rel="noopener noreferrer"&gt;Run it&lt;/a&gt;
&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;A satellite in low Earth orbit spends part of every orbit in Earth's shadow, running on its battery. If its power system fails there, operators may not hear about it until the next ground pass, and that pass gives them a few minutes to send the right command.&lt;/p&gt;
&lt;p&gt;Penumbra is mission ops for that moment. A Gemini agent reads the fleet and proposes recovery plans, and a physics simulator checks every plan against every plausible cause before anyone can act on it. A human then approves the exact plan that passed. Nobody can approve a plan that failed verification, the AI included.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Anh-08/penumbra/docs/media/approve-flow.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnh-08%2Fpenumbra%2FHEAD%2Fdocs%2Fmedia%2Fapprove-flow.gif" alt="SAT-07: the plan-by-hypothesis matrix shows plan P-1 passing every cause; the operator approves and the plan executes." width="88%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Try it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Open &lt;a href="https://bullsinorbit.study/" rel="nofollow noopener noreferrer"&gt;bullsinorbit.study&lt;/a&gt; and press &lt;strong&gt;START INCIDENT&lt;/strong&gt; to replay the recorded fault storm. Three satellites start failing. SAT-12 is fixed under a pre-approved flight rule, SAT-07 waits…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Anh-08/penumbra" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Partner Technologies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Gemini API
&lt;/h3&gt;

&lt;p&gt;Gemini runs the agent through function calling. The model can act only through nine tools, and a safety layer checks every call before it reaches a satellite. The same API turns a push-to-talk voice clip into the satellites and the fault for a new incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  ElevenLabs
&lt;/h3&gt;

&lt;p&gt;Control rooms run on spoken callouts, so ElevenLabs reads the alerts and the agent's narration aloud during the demo. We cache every clip, so the public server needs no voice key and the demo still talks offline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vultr
&lt;/h3&gt;

&lt;p&gt;Judges needed a link they could open, so the public demo runs in one Docker container on a small Vultr instance (2 vCPUs, 4 GB of RAM). We ran our scale benchmark there too. A full check of all 24 satellites takes about a quarter of a second on that machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  GoDaddy Registry
&lt;/h3&gt;

&lt;p&gt;The demo lives at bullsinorbit.study, for UB's Bulls in orbit.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rest of the stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Skyfield computes orbits, eclipses and ground passes from cached public data, so nothing calls the network at runtime.&lt;/li&gt;
&lt;li&gt;NumPy runs the verifier's power model on many simulated runs at once, which keeps a full check fast.&lt;/li&gt;
&lt;li&gt;OR-Tools CP-SAT fits commands into short ground passes and keeps two uplinks from using the same station at once.&lt;/li&gt;
&lt;li&gt;FastAPI and WebSockets stream each of the agent's tool calls to the browser as it happens.&lt;/li&gt;
&lt;li&gt;React, Vite and CesiumJS draw the 3D globe, with the map imagery bundled so the UI works offline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gemini kept writing numbers. The agent's narration may not contain digits: the model writes placeholders and the backend fills in the real values. Our first live narration was rejected three times, and at 3:41 on Sunday morning we merged a fix for "0.30 km km", because Gemini had repeated the unit after the placeholder.&lt;/li&gt;
&lt;li&gt;The free tier allowed five requests a minute, and one incident takes seven to thirteen. The app waits as long as the server asks, then falls back to a recorded run. We record every live run, so the public demo replays a real Gemini session.&lt;/li&gt;
&lt;li&gt;The same plan hashed differently on our laptops and on CI, because timestamps carried tiny floating-point differences. Rounding them to whole seconds fixed it, and recorded runs now replay the same way on any machine.&lt;/li&gt;
&lt;li&gt;The scheduler gave the most urgent satellite the lowest priority. A fleet test caught it.&lt;/li&gt;
&lt;li&gt;In 1 of 42 test storms, the verifier passed a plan that our separate truth simulator then broke. We left that miss in the published results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hackathon Experience
&lt;/h2&gt;

&lt;p&gt;We built Penumbra at UB Hacking 2026 in Buffalo, from Saturday 1 pm to the Sunday noon deadline. The team was me and &lt;a href="https://github.com/Anh-08" rel="noopener noreferrer"&gt;Anh-08&lt;/a&gt;, plus four AI coding sessions (two Claude Code, two Codex). Each session owned its own folders, CI rejected any pull request outside them, and the sessions handed work to each other through a pinned GitHub issue. We shipped 48 issues that way, even after two sessions ran out of quota overnight and the others took over their work.&lt;/p&gt;

&lt;p&gt;UB Hacking was packed and stressful. Everyone around us seemed to be building something I could not have come up with myself. We did not win a prize. Penumbra is still live at &lt;a href="https://bullsinorbit.study/" rel="noopener noreferrer"&gt;bullsinorbit.study&lt;/a&gt; if you want to try it.&lt;/p&gt;

</description>
      <category>mlhacks</category>
      <category>devchallenge</category>
      <category>hackathon</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
