<?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: tushar shah</title>
    <description>The latest articles on DEV Community by tushar shah (@tushar_shah).</description>
    <link>https://dev.to/tushar_shah</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%2F2147148%2F2447f70c-8e61-41fe-b1c0-be8d24cd42a7.jpg</url>
      <title>DEV Community: tushar shah</title>
      <link>https://dev.to/tushar_shah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tushar_shah"/>
    <language>en</language>
    <item>
      <title>Nishi Daak: an Arduino UNO Q that calls your name twice when you come home late (local Gemma, no cloud)</title>
      <dc:creator>tushar shah</dc:creator>
      <pubDate>Fri, 30 Oct 2026 06:36:02 +0000</pubDate>
      <link>https://dev.to/tushar_shah/nishi-daak-an-arduino-uno-q-that-calls-your-name-twice-when-you-come-home-late-local-gemma-no-4ib7</link>
      <guid>https://dev.to/tushar_shah/nishi-daak-an-arduino-uno-q-that-calls-your-name-twice-when-you-come-home-late-local-gemma-no-4ib7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;In Bengal they say that late at night, someone may call your name from outside in the voice of a person you love. Do not answer. Not the first time, not the second. The &lt;strong&gt;Nishi&lt;/strong&gt; (নিশি, "the night one") only ever calls twice, and whoever answers follows it into the dark and is never seen again. A real person, the story goes, will always call a third time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nishi Daak&lt;/strong&gt; ("the night call") puts that spirit on a shelf. It is an Arduino UNO Q that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watches the door, without a camera.&lt;/strong&gt; It notices when a family member's phone rejoins the home Wi-Fi late at night after being away for a while.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calls their name, twice.&lt;/strong&gt; The 8×13 LED matrix spells the name letter by letter, like a planchette, then spells it again. Never a third time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes what followed them home.&lt;/strong&gt; Gemma, running &lt;em&gt;on the board itself&lt;/em&gt; through llama.cpp, writes a short eerie (but harmless) tale and saves it to a night log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holds séances.&lt;/strong&gt; Open the board's web page from any phone on the home Wi-Fi, ask the Nishi a question, and it answers in character while a short version is spelled on the matrix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stays awake all day as two glowing eyes.&lt;/strong&gt; They blink, and their brightness ("spirit energy") follows how many Bluetooth devices are nearby.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No cloud, no accounts, no API keys. Nothing leaves the house.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&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%2Fnrks03lxyvkq0sjn7mdr.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%2Fnrks03lxyvkq0sjn7mdr.gif" alt="The LED matrix spelling TUSHAR, then TUSHAR again" width="560" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The night call: the name, letter by letter, twice. Never a third time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;🎬 &lt;strong&gt;&lt;a href="https://drive.google.com/file/d/1u8dVJ5TkOvXM-yOYtAKwM5j3VuGyJZYs/view?usp=sharing" rel="noopener noreferrer"&gt;Watch the 74-second demo video (Google Drive)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The human scenes in the video are free stock footage from Pexels (a Kolkata evening, a walk home, a door at night). The board's behaviour, the timings and the numbers come from the real build. The tale typed out in the video is the few-shot example from the prompt; real ones from the board's night log are below.&lt;/p&gt;

&lt;p&gt;This is the board's own web page, screenshotted live from the UNO Q on my home Wi-Fi. The eyes and the "2" show the spirit energy (5 Bluetooth devices nearby), the night log holds real tales Gemma wrote on the board, and the footer is the board's health check: presence ok, Gemma 3 1B ok, last reply 15 s at 4.7 tokens/s.&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%2Fsowxizesoazs16e3dy9m.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%2Fsowxizesoazs16e3dy9m.png" alt="The Nishi Daak web page served by the UNO Q: glowing eyes, séance box, night log with three tales, health line" width="640" height="1010"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three real entries from &lt;code&gt;night_log.json&lt;/code&gt;, written by Gemma 3 1B on the board:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A single, crimson leaf drifted down the path, landing softly beside his feet. It mirrored a forgotten melody, a scent of rain and old roses. The Nishi smiles, a fleeting glimmer in the gloom.&lt;/em&gt;&lt;br&gt;
(10.0 s, 4.7 tokens/s)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A single jasmine flower, bruised and drooping, drifted past the doorway. The rain smelled of dust and regret, a faint, silver light reflecting on it. It paused, a single tear falling: a beautiful sorrow, like a forgotten song.&lt;/em&gt;&lt;br&gt;
(15.0 s, 4.7 tokens/s)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A single, pale moonbeam traced a path down the stairs. It watched a small, worn book open. A faded map, perhaps? The Nishi smiles, a fleeting shadow.&lt;/em&gt;&lt;br&gt;
(12.6 s, 4.7 tokens/s)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each log entry records the time the board caught the phone. Tap the small red icon next to a name (a flask for a test call, Wi-Fi for a real one) and a card shows how it was caught, how long the person was away, and how fast Gemma wrote the tale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&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/tusharshah21" rel="noopener noreferrer"&gt;
        tusharshah21
      &lt;/a&gt; / &lt;a href="https://github.com/tusharshah21/Nishi-Daak" rel="noopener noreferrer"&gt;
        Nishi-Daak
      &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;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Nishi Daak&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;em&gt;It calls your name twice. Never answer.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;An Arduino UNO Q that lives on a shelf and behaves like a Nishi, the night spirit of Bengali folklore. Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 4 ("Ghost in the Machine"), Arduino category.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The folklore&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;In Bengal they say the Nishi comes late at night and calls your name in the voice of someone you love. It calls only twice. If you answer, you follow it into the dark and are never seen again. So if you hear your name at night, wait for the third call: a real person will always call a third time.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idle:&lt;/strong&gt; two glowing eyes on the board's 8x13 LED matrix. They blink and flicker, and their brightness ("spirit energy") follows how many Bluetooth devices are nearby. With no energy, only a faint stray light flickers now and then.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Night&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tusharshah21/Nishi-Daak" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Runs on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matrix eyes and planchette&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sketch/sketch.ino&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the UNO Q's STM32 microcontroller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App logic and web API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python/main.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App Lab container on the Linux side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web page (séance, night log)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assets/index.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App Lab &lt;code&gt;web_ui&lt;/code&gt; brick, port 7000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presence scanner&lt;/td&gt;
&lt;td&gt;&lt;code&gt;host/presence_service.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Debian host, systemd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The spirit's voice&lt;/td&gt;
&lt;td&gt;llama.cpp &lt;code&gt;llama-server&lt;/code&gt; + &lt;code&gt;gemma-3-1b-it&lt;/code&gt; Q4_K_M&lt;/td&gt;
&lt;td&gt;Debian host, systemd&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&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%2F44lzr0krex4xerezmu8w.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%2F44lzr0krex4xerezmu8w.jpg" alt="Wi-Fi presence, then the LED matrix, then local Gemma, all on one Arduino UNO Q" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The UNO Q is two computers on one board: an STM32 microcontroller that drives the LED matrix, and a quad-core Linux side (4 GB of RAM on mine) running Debian. App Lab connects them with a Bridge, so Python on the Linux side can call &lt;code&gt;Bridge.call("spell", "TUSHAR   TUSHAR")&lt;/code&gt; and the sketch fades the letters in and out on the matrix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma on the board
&lt;/h3&gt;

&lt;p&gt;llama.cpp builds on the board itself (47 minutes with &lt;code&gt;-j2&lt;/code&gt;; 3.6 GB of RAM was enough). &lt;code&gt;llama-server&lt;/code&gt; runs as a systemd service with &lt;strong&gt;Gemma 3 1B instruct, Q4_K_M&lt;/strong&gt; (a 769 MB file):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Measured on the board&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generation speed&lt;/td&gt;
&lt;td&gt;4.7 to 4.9 tokens/s, 4 threads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One séance answer&lt;/td&gt;
&lt;td&gt;8.5 to 9.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A night tale&lt;/td&gt;
&lt;td&gt;10 to 15 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First reply after a restart&lt;/td&gt;
&lt;td&gt;38.8 s (it processes the persona once)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 1B model does not follow rules well, but it copies examples beautifully. So instead of a long list of "never do X", the prompt is a short persona plus two few-shot turns: one night tale and one dodged request for advice. A small &lt;code&gt;tidy()&lt;/code&gt; pass then strips markdown, emojis and any "as an AI" talk, and keeps at most three sentences, so whatever Gemma says always fits the spirit. If Gemma is down, the Nishi falls back to a canned line, so the call never fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catching a phone without a camera
&lt;/h3&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%2F0ofbrfs9nul674yl8tv0.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%2F0ofbrfs9nul674yl8tv0.jpg" alt="A silhouette at the front door, a &amp;quot;Joined Home&amp;quot; Wi-Fi notification, and the board's log line: phone seen, away 3 h 12 min, it is night, call TUSHAR" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From the demo video: the moment the phone rejoins the home Wi-Fi and the board decides to call. The footage is stock and the times are illustrative; the decision logic is the real code below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Nishi needs to know &lt;em&gt;who&lt;/em&gt; came home, but I did not want cameras or an app on anyone's phone. Phones randomize their Bluetooth addresses constantly, so Bluetooth only feeds the "energy" count. Wi-Fi is different: Android and iOS keep one stable private MAC per network. A small root service on the host runs &lt;code&gt;arp-scan&lt;/code&gt; on the home network and serves the result as JSON, and the app turns it into arrivals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;arrivals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;macs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;(name, seconds away) for known people whose phone reappeared after being away.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;back&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CFG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;people&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mac&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;macs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;away_seconds&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;  &lt;span class="c1"&gt;# first sighting after app start never counts as an arrival
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then reality got in the way, twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Locked phones vanish.&lt;/strong&gt; A locked phone in Wi-Fi power save answers ARP after up to 2 seconds, and the default &lt;code&gt;arp-scan&lt;/code&gt; gives up after 500 ms. With &lt;code&gt;--retry=4 --timeout=1500&lt;/code&gt; it went from 4 of 6 scans to 6 of 6. But during a 30-minute test with the phone locked on the table, it was seen &lt;strong&gt;once in 62 polls&lt;/strong&gt;: a sleeping phone drops off Wi-Fi entirely, and no scan can see a disconnected phone. So the "away" threshold is 3 hours, not 20 minutes. Otherwise the Nishi would call my name every time my phone took a nap next to me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The board kept dying after about 15 hours.&lt;/strong&gt; Ping worked, SSH refused, and only a power cycle brought it back. The journal of the dead boot had the answer: &lt;code&gt;The maximum number of active connections for UID 0 has been reached (max_connections_per_user=256)&lt;/code&gt;. Each BLE scan used &lt;code&gt;asyncio.run()&lt;/code&gt;, a fresh event loop every time, and bleak opened a new system D-Bus connection for each loop without closing the old one. The connections piled up until root hit the limit, and from then on nothing running as root could talk to D-Bus. The fix is one event loop for every scan:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BLE_LOOP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_event_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# one loop for every scan: bleak keeps a D-Bus connection per loop
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_ble&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BLE_LOOP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BleakScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BLE_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_adv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Before the fix, the service's socket count grew by one with every scan. After it, the count stayed flat at 6.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;This project only makes sense with an open model running locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data is my family's comings and goings.&lt;/strong&gt; The core signal is &lt;em&gt;which phone came home, and when&lt;/em&gt;. That is exactly the kind of data I do not want on someone else's server. With Gemma running on the board, the phone addresses, the timestamps and every tale stay in a JSON file on a shelf in my house.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works with the internet off.&lt;/strong&gt; Ghost stories are for power cuts and bad nights. The Nishi only needs the home Wi-Fi router; unplug the internet and it still calls your name and still tells you what followed you home.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No per-request bill for a spirit that might speak a dozen times a night, and no API key to leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spirit is mine to change.&lt;/strong&gt; The persona is a few lines of text and two examples. I can swap the model for a smaller Gemma on a 2 GB board or a bigger one somewhere else, change the voice, or change the language in &lt;code&gt;config.json&lt;/code&gt;, and nothing depends on a hosted model's mood that week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It lasts.&lt;/strong&gt; Folklore survives because it is passed down, not hosted. A Nishi that depends on a service that might shut down someday is a very short-lived ghost.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with Claude Code as a pair, running the board over SSH from my laptop. The session below is a curated slice of the real build: the board setup, the locked-phone tests, the crash that turned out to be a D-Bus leak, and the night log's details card.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building Nishi Daak: an Arduino UNO Q night spirit with local Gemma&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I haven't run any commands on the board yet. Instead of me copying outputs back and forth, I want you to run the board commands yourself over SSH from my Windows laptop.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;adb shell: check OS, RAM, disk, Bluetooth and Wi-Fi on the UNO Q&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ adb shell: check OS, RAM, disk, Bluetooth and Wi-Fi on the UNO Q&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Debian 13 (trixie), aarch64, 3.6 GiB RAM, hci0 present, wlan0 DOWN with no saved network&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The board isn't on your Wi-Fi yet, and SSH is disabled out of the box, so I got in over USB with adb and put the laptop's SSH key on it. Two things need you: your Wi-Fi password and your sudo password.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;ssh nishi "tail -5 ~/install.log" ... [100%] Built target llama-server ... Created symlink nishi-presence.service ...&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;check llama-server health and the presence service on the board&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ check llama-server health and the presence service on the board&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;nishi-presence active, nishi-llm active, llama-server listening on 0.0.0.0:8080 with gemma-3-1b-it Q4_K_M&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The first install worked: llama.cpp built on the board in about 47 minutes and Gemma 3 1B (769 MB) is loaded. My cleanup then deleted libllama-server-impl.so, which is a real library, not a dev symlink, so the board is rebuilding. Lesson recorded: keep build/bin untouched.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Correction: my phone's Wi-Fi was ON the whole time. I never turned it off, so the off/on test result is invalid. I read my phone's address directly from its Wi-Fi settings.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Then the 'disappeared' MAC was a coincidence: devices on this LAN drop in and out of arp-scan on their own, so a single diff proves nothing. I've put the address you read from the phone into config.json and started a locked-phone test. Lock your phone now.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;check now with the phone it is locked for some time now&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;6 rounds a minute apart: default arp-scan, patient arp-scan, ping&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ 6 rounds a minute apart: default arp-scan, patient arp-scan, ping&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;default 4/6, --retry=4 --timeout=1500 6/6, ping 6/6&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;A drowsy phone in Wi-Fi power save answers ARP after up to 2 s, and the default scan gives up after 500 ms. With --retry=4 --timeout=1500 it is 6 of 6. But after a long lock your phone dropped off Wi-Fi completely, and no scan can see a disconnected phone.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;phone locked now, check after 30 minutes&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Over 30 minutes locked, the board saw your phone once in 62 checks. A locked phone is effectively invisible, so the scanner can't tell 'asleep at home' from 'away'. Options: a much longer away time, or a phone-side setting.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;cool we can continue the build now&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;nmcli: static IP 192.168.1.200, ignore IPv6 DNS; arduino-app-cli app start us...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ nmcli: static IP 192.168.1.200, ignore IPv6 DNS; arduino-app-cli app start user:nishi-daak&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;first start: fatal error: MsgPack.h: No such file or directory; second: lookup downloads.arduino.cc ... network is unreachable; third: running&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Nishi Daak is running on the board. Fixes on the way: a static IP (DHCP had moved the board), the Bridge libraries missing from sketch.yaml, and IPv6 DNS servers the board couldn't reach. As you chose, someone counts as 'arrived' only after 3 hours unseen.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;the UI is not loading can you check it&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;journalctl -b -1 on the board after the power cycle&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ journalctl -b -1 on the board after the power cycle&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;The maximum number of active connections for UID 0 has been reached (max_connections_per_user=256)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Root cause of both 'board stuck' episodes: the presence service ran every BLE scan with asyncio.run(), a new event loop per scan, and bleak opened a new system D-Bus connection for each loop without closing the old one. At 256 root connections D-Bus refused everyone. Fix: one event loop reused for every scan. Socket count before: grew by one per scan. After: flat at 6 across 8 scans. I also made Nishi Daak the default App Lab app so it starts on boot.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;on the call can we see the time stamp too at what time does the arduino caught it?&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;python tests/test_main.py&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ python tests/test_main.py&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;ok&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Each call in the night log now records the time of the presence scan that saw the phone, not when the slow spell and Gemma's tale finished (that could be a minute late). It also records how long the person was away and whether it was a test call. Checked with a real test call on the board.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;the details should be seperate not there on the page have an icon for the same&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The log now shows only time and name plus a small red icon (Wi-Fi for a real catch, a flask for a test call). Tapping it opens a native dialog, 'The call': name, caught at, how, away for, and how fast local Gemma wrote the tale.&lt;/p&gt;

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

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        18 of 20 messages
          · some content was hidden by the author
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The spookiest moment wasn't a ghost story. It was the board going silent after 15 hours: it still answered a ping, but nothing else. Something was still home, but it wouldn't come to the door. It turned out to be 256 D-Bus connections that never closed, which is a very modern kind of haunting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is an old story, living in a small board, made out of open pieces anyone can read and change.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It calls your name twice. Never answer.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Arduino:&lt;/strong&gt; the whole project is an Arduino UNO Q acting as a physical agent that senses (Wi-Fi presence, Bluetooth energy) and acts (the LED matrix planchette), with the language model running on the board itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 3 1B instruct (Q4_K_M) runs locally on the UNO Q through llama.cpp and writes every night tale and séance answer, at 4.7 tokens/s, with no cloud.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>arduino</category>
      <category>gemma</category>
    </item>
    <item>
      <title>Bahar: the best hour today to go outside, forecast by TabPFN and told by Gemma</title>
      <dc:creator>tushar shah</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:20:40 +0000</pubDate>
      <link>https://dev.to/tushar_shah/bahar-the-best-hour-today-to-go-outside-forecast-by-tabpfn-and-told-by-gemma-3nk4</link>
      <guid>https://dev.to/tushar_shah/bahar-the-best-hour-today-to-go-outside-forecast-by-tabpfn-and-told-by-gemma-3nk4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the DEV Hacktoberfest Open-Source AI Challenge, Week 1: Touch Grass.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Quick question before you scroll: &lt;strong&gt;when did you last step outside on purpose?&lt;/strong&gt; Not to get to the car. Not to grab a delivery. Just to be outside.&lt;/p&gt;

&lt;p&gt;If you had to think about it, this one's for you.&lt;/p&gt;

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

&lt;p&gt;Here's a morning you might recognise. You decide to go for a walk. You open the air quality app: PM2.5 is "Moderate", whatever that means for the next hour. You open the weather app: it'll feel like 38°C by noon. You open a third app to check for rain. Ten minutes later you're still on your phone, still inside.&lt;/p&gt;

&lt;p&gt;That loop is worse in a city like Hyderabad, where air quality changes a lot over a single day, summer afternoons can feel hotter than 40°C, and the monsoon brings sudden rain. So I built &lt;strong&gt;Bahar&lt;/strong&gt; ("outside" in Hindi and Urdu). It answers exactly one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When is the best hour today to get out of the house?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No charts to read. One time range, two sentences, and then it tells you to put your phone down.&lt;/p&gt;

&lt;p&gt;Under the hood, four steps:&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%2Fwz5j1db7wr7x1hzzgahk.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%2Fwz5j1db7wr7x1hzzgahk.png" alt="How Bahar works: look back, look ahead, weigh it up, say it" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;🌦️ &lt;strong&gt;Look back.&lt;/strong&gt; Pull about 18 months of hourly weather and PM2.5 history for the city from Open-Meteo (free, no key).&lt;/li&gt;
&lt;li&gt;🔮 &lt;strong&gt;Look ahead.&lt;/strong&gt; &lt;strong&gt;TabPFN&lt;/strong&gt; forecasts PM2.5 for every hour today from the weather forecast.&lt;/li&gt;
&lt;li&gt;⚖️ &lt;strong&gt;Weigh it up.&lt;/strong&gt; Score every hour: air quality by India's National AQI bands (55%), heat by the "feels like" temperature (30%), rain (15%). Pick the best 1 to 3 hour window between 5 AM and 7 PM.&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Say it like a friend.&lt;/strong&gt; &lt;strong&gt;Gemma 4&lt;/strong&gt;, running locally in Ollama, writes the plan in two or three sentences, and the app can read it aloud offline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of it runs on my laptop: Windows, an i7, 16 GB of RAM, no GPU. No paid API. The only network calls are the weather requests and TabPFN's license check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&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%2Fsignvezlakguad1ay7jf.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%2Fsignvezlakguad1ay7jf.gif" alt="A flow of how thedemo works" width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's play a quick game first. Here's tomorrow's PM2.5 forecast for Hyderabad, hour by hour. Bahar only plans between 5 AM and 7 PM.&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%2F4ct8npv7b6kf7fes4t5y.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%2F4ct8npv7b6kf7fes4t5y.png" alt="Your turn: which window would you pick?" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which window do you think Bahar picked?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  👉 Make your guess, then click to see Bahar's answer
  &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%2Fj918ytsy1zk68gy4ms8j.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%2Fj918ytsy1zk68gy4ms8j.png" alt="Bahar's answer: 5:00 to 8:00 AM" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5:00 AM to 8:00 AM.&lt;/strong&gt; Not the cleanest air of the day, but cooler hours: it feels like 28°C at 5 AM and already over 32°C by 9. Bahar weighs heat too. Here's the plan, word for word as Gemma wrote it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Wake up for your outdoor session between 5:00 AM and 8:00 AM when the air quality is Satisfactory. Go for a run, cycle, or play cricket before the worst stretch around 11:00 AM. Put your phone away and enjoy the Hyderabad morning.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p&gt;&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%2Ffrqjirhwtiwin6wpt6v7.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%2Ffrqjirhwtiwin6wpt6v7.png" alt="The Bahar app" width="799" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&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/tusharshah21" rel="noopener noreferrer"&gt;
        tusharshah21
      &lt;/a&gt; / &lt;a href="https://github.com/tusharshah21/bahar" rel="noopener noreferrer"&gt;
        bahar
      &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;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Bahar 🌿&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;The best hour today to go outside, then off your screen.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/tusharshah21/bahar/data/demo.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%2Ftusharshah21%2Fbahar%2FHEAD%2Fdata%2Fdemo.gif" alt="Bahar picks the best window and Gemma writes the plan"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Bahar ("outside" in Hindi/Urdu) forecasts hourly PM2.5 for your city with &lt;strong&gt;TabPFN&lt;/strong&gt;, combines it with heat and rain into a "go outside" score, finds the best window, and has &lt;strong&gt;Gemma&lt;/strong&gt; (running locally) tell you the plan in two sentences. Everything core runs on a laptop with no paid API.&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 1: &lt;em&gt;Touch Grass&lt;/em&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;fetch_data.py&lt;/code&gt; pulls ~18 months of hourly weather + PM2.5 for your city from Open-Meteo (free, no key).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model.py&lt;/code&gt; hands TabPFN a few thousand example hours (in-context learning, no training loop) and asks it to predict today's PM2.5 from the weather forecast.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;planner.py&lt;/code&gt; scores each hour: air quality (India's AQI bands) 55%, heat 30%, rain 15%, then picks the best 1 to 3 hour window.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;narrator.py&lt;/code&gt; asks Gemma via Ollama to write…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tusharshah21/bahar" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python fetch_data.py        &lt;span class="c"&gt;# once, about a minute&lt;/span&gt;
python evaluate.py          &lt;span class="c"&gt;# accuracy charts&lt;/span&gt;
python bahar.py             &lt;span class="c"&gt;# today's plan, printed and spoken&lt;/span&gt;
streamlit run app.py        &lt;span class="c"&gt;# the one-screen app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🏙️ &lt;strong&gt;Try it for your city.&lt;/strong&gt; Open &lt;code&gt;config.py&lt;/code&gt;, change &lt;code&gt;CITY&lt;/code&gt;, &lt;code&gt;LAT&lt;/code&gt;, &lt;code&gt;LON&lt;/code&gt; and &lt;code&gt;TIMEZONE&lt;/code&gt;, and run the four commands above. I'd love to see what Bahar says about yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used TabPFN
&lt;/h2&gt;

&lt;p&gt;TabPFN is a foundation model for tables, and it works differently from what I was used to. There's no training loop. You hand it example rows at prediction time and it learns from them in context, a bit like showing a friend a few hundred past days and asking "so what about tomorrow?"&lt;/p&gt;

&lt;p&gt;For Bahar, each row is one hour: temperature, humidity, wind (split into east and north components), rain, cloud cover, boundary layer height, time of day and season encoded as sine and cosine, and PM2.5 at the same hour yesterday. TabPFN sees 2,000 example hours, half from the most recent weeks and half sampled from the whole history, so it still remembers what last winter looked like.&lt;/p&gt;

&lt;p&gt;The feature that helped most is one most people have never heard of: &lt;strong&gt;boundary layer height&lt;/strong&gt;. It's the height of the air layer that pollution mixes into. On calm winter nights it drops to a couple of hundred metres, and PM2.5 has nowhere to go. That's the smog you can taste.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plot twist: TabPFN lost. At first.
&lt;/h3&gt;

&lt;p&gt;I'll be honest about how this went, because it surprised me.&lt;/p&gt;

&lt;p&gt;My first test was the obvious one: train on everything up to three weeks ago, forecast the last three weeks. TabPFN lost to plain gradient boosting (6.90 vs 6.76 mean absolute error, in µg/m³). And it took &lt;strong&gt;20 minutes&lt;/strong&gt; on my laptop to get there.&lt;/p&gt;

&lt;p&gt;The 20 minutes turned out to be an easy fix. Setting &lt;code&gt;n_estimators=1&lt;/code&gt; gave almost the same accuracy and ran about 20 times faster. A full plan now takes about a minute.&lt;/p&gt;

&lt;p&gt;The loss was more interesting. When I re-ran the test with different random samples of history, TabPFN sometimes won and sometimes lost. Three weeks was simply too small a window to judge anything. So I asked a better question: &lt;strong&gt;how does each model do across a whole year of seasons?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built a rolling backtest: ten 3-week windows spread from July 2025 to August 2026, each model trained only on the hours before its window, so nothing peeks into the future. Two opponents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌲 &lt;strong&gt;Gradient boosting&lt;/strong&gt; (scikit-learn's HistGradientBoostingRegressor), trained on the &lt;strong&gt;full&lt;/strong&gt; history, not just 2,000 rows.&lt;/li&gt;
&lt;li&gt;🔁 &lt;strong&gt;Same as yesterday&lt;/strong&gt;: predict that each hour will match the same hour yesterday.&lt;/li&gt;
&lt;/ul&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%2Fbkntiep12r4xyo2038a5.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%2Fbkntiep12r4xyo2038a5.png" alt="Forecast error through the seasons" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at the shaded winter months on the chart. That's where it gets good:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Through the November to February smog season, TabPFN's forecast errors were 16% lower than gradient boosting's.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the stretch when choosing the right hour matters most for your lungs. Across the whole year TabPFN also had the lowest average error, about 5% better than gradient boosting, while only ever looking at 2,000 rows.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  📊 See every window (for the numbers people)
  &lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Window starts&lt;/th&gt;
&lt;th&gt;Hours of history&lt;/th&gt;
&lt;th&gt;TabPFN&lt;/th&gt;
&lt;th&gt;Gradient boosting&lt;/th&gt;
&lt;th&gt;Same as yesterday&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jul 2025&lt;/td&gt;
&lt;td&gt;2,160&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.51&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.80&lt;/td&gt;
&lt;td&gt;6.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 2025&lt;/td&gt;
&lt;td&gt;3,240&lt;/td&gt;
&lt;td&gt;3.19&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oct 2025&lt;/td&gt;
&lt;td&gt;4,320&lt;/td&gt;
&lt;td&gt;12.14&lt;/td&gt;
&lt;td&gt;11.98&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.76&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nov 2025&lt;/td&gt;
&lt;td&gt;5,400&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.27&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11.58&lt;/td&gt;
&lt;td&gt;10.64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 2026&lt;/td&gt;
&lt;td&gt;6,480&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11.30&lt;/td&gt;
&lt;td&gt;10.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb 2026&lt;/td&gt;
&lt;td&gt;7,560&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.69&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.19&lt;/td&gt;
&lt;td&gt;9.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apr 2026&lt;/td&gt;
&lt;td&gt;8,640&lt;/td&gt;
&lt;td&gt;7.76&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.61&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 2026&lt;/td&gt;
&lt;td&gt;9,720&lt;/td&gt;
&lt;td&gt;8.77&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.49&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 2026&lt;/td&gt;
&lt;td&gt;10,800&lt;/td&gt;
&lt;td&gt;3.56&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.31&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 2026&lt;/td&gt;
&lt;td&gt;11,880&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.47&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.65&lt;/td&gt;
&lt;td&gt;4.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.13&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.50&lt;/td&gt;
&lt;td&gt;7.60&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mean absolute error in µg/m³, lower is better. TabPFN's scores are averaged over three different samples of history.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;It isn't a clean sweep, and I don't want to pretend it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count the wins and it's a tie&lt;/strong&gt;, 5 windows each. Gradient boosting's wins are small and come later in the year, once it has more than 8,500 hours to learn from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October humbled everyone.&lt;/strong&gt; In the first weeks after the monsoon, "same as yesterday" beat both models. The air shifts in ways last year's pattern doesn't capture yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Used Gemma
&lt;/h2&gt;

&lt;p&gt;If TabPFN is Bahar's brain, Gemma is its voice.&lt;/p&gt;

&lt;p&gt;Gemma 4 (&lt;code&gt;gemma4:e4b-it-qat&lt;/code&gt;, the quantization-aware 4B-effective model) runs locally through Ollama and writes a plan in about 20 seconds on my CPU. I gave it a clear division of labour: the numbers come from the forecast, the words come from Gemma. The prompt hands it a short list of facts (the window, the air quality band and PM2.5, the "feels like" temperature, a suggested activity, the worst stretch of the day) and asks for two or three warm, direct sentences that end with "put the phone away".&lt;/p&gt;

&lt;p&gt;Then Gemma taught me two things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson one: Gemma can overthink.&lt;/strong&gt; Gemma 4 can reason before it answers. With a 120-token budget, it sometimes spent every single token thinking and handed back an empty reply. Passing &lt;code&gt;"think": false&lt;/code&gt; to Ollama fixed it. A two-sentence nudge doesn't need deep thought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson two: Gemma is too nice.&lt;/strong&gt; I'd written "do not invent other facts" in the prompt. Gemma still kept upgrading the air. Given "Satisfactory", it wrote "good", "perfect" or "best" in &lt;strong&gt;5 of 6&lt;/strong&gt; test runs. That sounds harmless until you think about someone with asthma, who would plan their day very differently around "Satisfactory" and "perfect".&lt;/p&gt;

&lt;p&gt;So I tightened the prompt: use the air quality label word for word, never upgrade it, and mention no times or numbers beyond the facts given. Same facts, 8 new runs: &lt;strong&gt;0 of 8&lt;/strong&gt; broke the rules. The plans are a little less bubbly now, and a lot more trustworthy.&lt;/p&gt;

&lt;p&gt;And if anything's missing, Bahar still shows up. No Ollama? A template sentence. No TabPFN? Gradient boosting. You always get a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The PM2.5 history comes from the CAMS atmospheric model via Open-Meteo, not ground sensors. It's free and has full hourly history, which makes it great for patterns. For ground truth, compare a few days against CPCB station readings for your area.&lt;/li&gt;
&lt;li&gt;The size of the winter win depends on where the test windows fall. The single January window in the table shows 30%, but in an earlier run with windows a few days apart it was 10%. That's why I quote the 16% winter average rather than any one window.&lt;/li&gt;
&lt;li&gt;With one winter in the data, I can't fully separate "TabPFN is better in winter" from "TabPFN is better when there's less history". Next winter will tell.&lt;/li&gt;
&lt;li&gt;Recent TabPFN versions download their weights only after you accept a license with a free Prior Labs account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every run appends to &lt;code&gt;data/plans_log.jsonl&lt;/code&gt;, so over time I can check whether the hour Bahar picked actually felt good outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn 👇
&lt;/h2&gt;

&lt;p&gt;I'd love to hear from you in the comments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What's the worst hour for air in your city?&lt;/strong&gt; Is it the morning rush, or the evening climb you can see in Hyderabad's forecast above?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you try Bahar for your city, what window did it pick?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And now, the most important instruction in this post: close this tab and go outside. 🌿&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>opensource</category>
      <category>ai</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>I Built a $0 AI Code Reviewer Using GitHub Copilot CLI (No Vendor Lock-In)</title>
      <dc:creator>tushar shah</dc:creator>
      <pubDate>Mon, 26 Jan 2026 11:09:03 +0000</pubDate>
      <link>https://dev.to/tushar_shah/i-built-a-0-ai-code-reviewer-using-github-copilot-cli-no-vendor-lock-in-57ja</link>
      <guid>https://dev.to/tushar_shah/i-built-a-0-ai-code-reviewer-using-github-copilot-cli-no-vendor-lock-in-57ja</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AI Code Reviews for $0.00? I Built a Provider-Agnostic Reviewer with GitHub Copilot CLI
&lt;/h1&gt;

&lt;p&gt;What if AI code reviews didn’t cost $50/month—or lock you into a single model?&lt;br&gt;&lt;br&gt;
I ran into this problem while maintaining side projects, so I built an &lt;strong&gt;AI Code Reviewer&lt;/strong&gt; that works with &lt;em&gt;any&lt;/em&gt; LLM provider and can cost &lt;strong&gt;literally $0 per PR&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;AI Code Reviewer&lt;/strong&gt; is an open-source GitHub Action that performs automated, AI-powered code reviews on every pull request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔌 Works with &lt;strong&gt;any OpenAI-compatible API&lt;/strong&gt; (Groq, DeepSeek, Mistral, Gemini, Anthropic, OpenAI, etc.)&lt;/li&gt;
&lt;li&gt;🧠 Uses a custom compression format (&lt;strong&gt;TOON encoding&lt;/strong&gt;) to reduce token usage by &lt;strong&gt;50–70%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;💸 Supports &lt;strong&gt;free LLM providers&lt;/strong&gt; → $0.00 reviews&lt;/li&gt;
&lt;li&gt;⚡ Setup time: ~30 seconds&lt;/li&gt;
&lt;li&gt;🔓 Fully open source (MIT)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This wasn’t just about building another AI tool—it was about &lt;strong&gt;giving developers back control&lt;/strong&gt; over cost, models, and infrastructure.&lt;/p&gt;




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

&lt;p&gt;📸 &lt;strong&gt;Github PR for sample:&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“&lt;a href="https://github.com/tusharshah21/ai-code-reviewer/pull/2%E2%80%9D" rel="noopener noreferrer"&gt;https://github.com/tusharshah21/ai-code-reviewer/pull/2”&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📸 &lt;strong&gt;Screenshot:&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Fljyn6r9m7zosnfj6vf90.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.amazonaws.com%2Fuploads%2Farticles%2Fljyn6r9m7zosnfj6vf90.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;“AI Code Reviewer leaving structured review comments on a GitHub PR”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;Most AI review tools today share the same problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vendor lock-in&lt;/li&gt;
&lt;li&gt;Opaque pricing&lt;/li&gt;
&lt;li&gt;Closed source&lt;/li&gt;
&lt;li&gt;Expensive for students, indie hackers, and OSS maintainers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted the opposite:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;You choose the model. You control the cost. You own the workflow.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  My Experience with GitHub Copilot CLI (The Real Game-Changer)
&lt;/h2&gt;

&lt;p&gt;I’m primarily a backend developer.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;GitHub Actions, Octokit, and CI ergonomics were new territory for me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of tab-hopping between docs, blog posts, and Stack Overflow, I stayed inside my terminal and used &lt;strong&gt;GitHub Copilot CLI as a learning companion&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concrete Examples
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Fetching PR diffs with Octokit
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
gh copilot suggest "how do i fetch pr diff using octokit"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
  </channel>
</rss>
