# Nishi Daak: an Arduino UNO Q that calls your name twice when you come home late (local Gemma, no cloud)

> Source: <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>
> Published: 2026-10-30 06:36:02+00:00

*This is a submission for the [Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine](https://dev.to/challenges/hacktoberfest-week4-2026-10-26)*

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 **Nishi** (নিশি, "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.

**Nishi Daak** ("the night call") puts that spirit on a shelf. It is an Arduino UNO Q that:

No cloud, no accounts, no API keys. Nothing leaves the house.

*The night call: the name, letter by letter, twice. Never a third time.*

🎬 [Watch the 74-second demo video (Google Drive)](https://drive.google.com/file/d/1u8dVJ5TkOvXM-yOYtAKwM5j3VuGyJZYs/view?usp=sharing)

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.

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.

Three real entries from `night_log.json`, written by Gemma 3 1B on the board:

*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.*

(10.0 s, 4.7 tokens/s)

*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.*

(15.0 s, 4.7 tokens/s)

*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.*

(12.6 s, 4.7 tokens/s)

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.

*It calls your name twice. Never answer.*

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.

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.

| Part | File | Runs on | 
|---|---|---|
| Matrix eyes and planchette | `sketch/sketch.ino` | the UNO Q's STM32 microcontroller | 
| App logic and web API | `python/main.py` | App Lab container on the Linux side | 
| Web page (séance, night log) | `assets/index.html` | App Lab `web_ui` brick, port 7000 | 
| Presence scanner | `host/presence_service.py` | Debian host, systemd | 
| The spirit's voice | llama.cpp `llama-server` +`gemma-3-1b-it` Q4_K_M | Debian host, systemd | 

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 `Bridge.call("spell", "TUSHAR   TUSHAR")` and the sketch fades the letters in and out on the matrix.

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

| What | Measured on the board | 
|---|---|
| Generation speed | 4.7 to 4.9 tokens/s, 4 threads | 
| One séance answer | 8.5 to 9.1 s | 
| A night tale | 10 to 15 s | 
| First reply after a restart | 38.8 s (it processes the persona once) | 

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 `tidy()` 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.

*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.*

The Nishi needs to know *who* 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 `arp-scan` on the home network and serves the result as JSON, and the app turns it into arrivals:

``` python
def arrivals(macs, now):
    """(name, seconds away) for known people whose phone reappeared after being away."""
    back = []
    for p in CFG["people"]:
        mac = p["mac"].lower()
        if mac not in macs:
            continue
        prev = last_seen.get(mac)
        if prev is not None and now - prev > away_seconds():
            back.append((p["name"], now - prev))
        last_seen[mac] = now  # first sighting after app start never counts as an arrival
    return back
```

Then reality got in the way, twice.

**Locked phones vanish.** A locked phone in Wi-Fi power save answers ARP after up to 2 seconds, and the default `arp-scan` gives up after 500 ms. With `--retry=4 --timeout=1500` 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 **once in 62 polls**: 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.

**The board kept dying after about 15 hours.** Ping worked, SSH refused, and only a power cycle brought it back. The journal of the dead boot had the answer: `The maximum number of active connections for UID 0 has been reached (max_connections_per_user=256)`. Each BLE scan used `asyncio.run()`, 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:

```
BLE_LOOP = asyncio.new_event_loop()  # one loop for every scan: bleak keeps a D-Bus connection per loop

def scan_ble():
    found = BLE_LOOP.run_until_complete(BleakScanner.discover(timeout=BLE_SECONDS, return_adv=True))
```

Before the fix, the service's socket count grew by one with every scan. After it, the count stayed flat at 6.

This project only makes sense with an open model running locally:

`config.json`, and nothing depends on a hosted model's mood that week.
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.

**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.**

It is an old story, living in a small board, made out of open pieces anyone can read and change.
