Nishi Daak:搭載本地 Gemma 的 Arduino UNO Q 深夜呼喚回家者兩次
Nishi Daak: an Arduino UNO Q that calls your name twice when you come home late (local Gemma, no cloud)
Nishi Daak 是一個以 Arduino UNO Q 和本地 Gemma 製作的裝置,會在家人深夜手機重新連上家中 Wi-Fi 時呼喚其名字兩次。裝置透過 llama.cpp 執行 Gemma 3 1B,生成速度為 4.7 至 4.9 tokens/s,並會寫下夜間故事。整個系統無需雲端、帳戶或 API keys,資料不會離開家中。
This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
What I Built
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:
- Watches the door, without a camera. It notices when a family member's phone rejoins the home Wi-Fi late at night after being away for a while.
- Calls their name, twice. The 8×13 LED matrix spells the name letter by letter, like a planchette, then spells it again. Never a third time.
- Writes what followed them home. Gemma, running on the board itself through llama.cpp, writes a short eerie (but harmless) tale and saves it to a night log.
- Holds séances. 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.
- Stays awake all day as two glowing eyes. They blink, and their brightness ("spirit energy") follows how many Bluetooth devices are nearby.
No cloud, no accounts, no API keys. Nothing leaves the house.
Demo
The night call: the name, letter by letter, twice. Never a third time.
🎬 Watch the 74-second demo video (Google Drive)
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.
Code
Nishi Daak
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.
The folklore
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.
What it does
- Idle: 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.
- Night…
| 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 |
How I Built It
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.
Gemma on the board
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.
Catching a phone without a camera
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:
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.
Why Does Open Innovation Matter?
This project only makes sense with an open model running locally:
- The data is my family's comings and goings. The core signal is which phone came home, and when. 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.
- It works with the internet off. 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.
- It costs nothing to run. No per-request bill for a spirit that might speak a dozen times a night, and no API key to leak.
-
The spirit is mine to change. 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
config.json, and nothing depends on a hosted model's mood that week. - It lasts. 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.
My Agent Session
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 Spirit of It
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.
It calls your name twice. Never answer.
Prize Categories
- Best Use of Arduino: 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.
- Best Use of Gemma: 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.
來源:DEV Community · dev.to



