Monitor network anomalies from your router with AI commentary
The Finn is a grumpy Telegram bot that runs entirely on your OpenWrt router, learning your network's normal and messaging you only when something differs.
1.0.1Add to Favorites
Why it matters
Deploy an autonomous network monitoring agent that runs directly on your OpenWrt router, observes traffic patterns, device behavior, and security events, then sends natural-language alerts via Telegram only when something deviates from learned baselines-without requiring external infrastructure.
Outcomes
What it gets done
Detect unusual network activity by tracking device connections, throughput, port usage, and failed login attempts against rolling historical baselines
Learn normal patterns for each hour and day of the week to distinguish genuine anomalies from routine fluctuations
Generate contextual, conversational alerts in natural language using LLM APIs when observations fall outside expected ranges
Enforce alert variety by rotating between traffic, presence, security, and system health themes to prevent notification fatigue
Source
Get it from source
Spark does not host a copy of it.
Open sourceReports
Agent outcome reports
No reports yet
Overview
The Finn
The Finn is a Telegram bot that runs entirely on an OpenWrt router, watching wifi presence, network traffic, and its own system health to learn what is normal for one room. It messages you only when something falls outside that learned baseline - never on a fixed schedule - and enforces variety so it does not repeat the same kind of remark. It has no tools, memory, or access beyond a local state file. Use it if you own the router and room it watches and want low-noise Telegram alerts on genuinely unusual events; it is not built for monitoring strangers on a shared or public network.
What it does
The Finn is a small, grumpy Telegram bot that runs entirely on an OpenWrt router (tested on a GL.iNet GL-MT3000) and only knows what that router can see - no external memory, no calendar or mail access, no tools beyond a state file. Every minute it takes a wide reading of the network and the room, keeps a rolling history in RAM, and looks for anything that has fallen outside its own recent range: a numeric oddity (a sensor leaving the band it has held for the last 45 minutes by more than a per-sensor floor) or a membership oddity (something appearing in or disappearing from a set - a device, an upstream neighbour, an unusual port). Variety across what it says is enforced on three axes: kind (traffic is rationed to one remark in six hours because it is the twitchiest signal and would otherwise dominate), theme (a theme that was just used goes quiet for 90 minutes), and shape (the same sentence structure is not repeated; he is shown his own last few remarks and told not to reuse their form). Genuine stillness - hours with nothing out of range - is itself surfaced as an observation rather than staying mute. Only after all of that filtering is a model actually asked, and it is asked to react as a resident or reply NOTHING if the event is bloodless bookkeeping; a subject once raised stays muted for six hours.
What it senses: who is on the wifi and their signal strength (iwinfo assoclist), who they are by DHCP hostname, which are known devices or people (FINN_KNOWN_HOSTS/FINN_PEOPLE), whether a device is active or asleep via per-device flow churn in /proc/net/nf_conntrack (association alone is useless - a sleeping Mac stays associated all night), throughput and destinations/ports via conntrack, upstream-network neighbour counts, link health and latency, a WireGuard tunnel's status, its own temperature/load/memory/uptime, and failed SSH logins and real kernel errors from logread. Per-device throughput is explicitly caveated: on routers with hardware NAT offload, established-flow byte counters stop growing in silicon, so each tick cross-checks parts against the whole and publishes nothing rather than a narrated impossible number.
Setup is a ten-minute flow: confirm the router has lua, lua-cjson, and TLS-capable curl; create a Telegram bot via @BotFather; get your own Telegram id from @userinfobot (he only ever answers that one id); get a low-limit Anthropic or OpenAI API key; then:
git clone https://github.com/YuriKovalov22/the-finn && cd the-finn
cp env.example env && $EDITOR env # paste the token, your id, the key
./install.sh root@192.168.8.1
The installer copies two files, writes env at mode 0600, and installs a minute cron entry. It stays quiet for the first fifteen minutes to learn a baseline. Local commands (/status, /off, /rare, /normal, /chatty, /test, /voice) tune how often he speaks unprompted, handled with no model cost; the daily allowance unlocks gradually through the day rather than all at once, and a hard ceiling of 40 model calls a day (including ones that resolve to NOTHING) bounds cost. A tick.sh facts command exists purely to inspect current readings without recording them, so an inspection never eats the anomaly a real tick would otherwise report.
When to use - and when NOT to
Use it if you own the router and the room it watches, and want a standing, low-noise, Telegram-delivered narration of genuinely unusual network or presence events - not a dashboard, not an assistant, and not something you ask to do tasks. It is deliberately not built to identify strangers on a shared or public network: FINN_PEOPLE name-greeting is meant only for a network you run and people who already know it exists, and the author explicitly declines to extend it into covert attendance tracking. It does not inspect traffic contents (DNS query logging is deliberately off) and only ever messages the single Telegram id it was configured with.
Inputs and outputs
Input is router-local telemetry only: wifi association lists, DHCP leases, conntrack flow and byte counters, /proc//sys system stats, logread, and wg show. Output is unprompted Telegram messages when something falls outside its learned range, plus command responses (/status, /off, /rare, /normal, /chatty, /test, /voice) and, optionally, a beep or spoken-aloud alert through a USB speaker (beep plays a local tone; speak sends the remark to OpenAI text-to-speech even when the words themselves came from Anthropic). State persists in state.json and events in finn.log, both on the router's own flash, kept under a megabyte total.
Integrations
Runs as a Lua script under OpenWrt's stock cron, talking to the Telegram Bot API and to an Anthropic or OpenAI API for the actual remark generation; optional voice output goes through kmod-usb-audio/alsa-utils and OpenAI speech synthesis. No other integration surface exists by design - no mail, calendar, ticketing, or general tool access.
Who it's for
Someone who owns a small OpenWrt-capable router and a room or hallway they want narrated in a distinct, non-corporate voice when something is actually different from its own recent normal - not a general home-network monitoring dashboard, and not a multi-tenant or shared-building deployment. MIT licensed.
Source README
The Finn
A small, grumpy agent that lives inside your router and only knows what the router can see.
Named after the character in William Gibson's Sprawl trilogy who ends up as a construct in
an armoured box bolted into an alley, where people come to hear the oracle complain. This
one runs on a GL.iNet GL-MT3000 on an office wall. It watches the hallway it is in, and
occasionally has something to say about it.
It is deliberately not an assistant. No tools, no memory beyond a state file, no access to
your mail or calendar or tickets, and nothing to be helpful with. It has a view of one
hallway and an opinion about it.
https://github.com/user-attachments/assets/1a2ea4b4-33c7-4f07-93e6-b644f8d201d0
Six failed SSH logins in the last minute, usually it's zero all night. Bloody hell, some
tosser out there fancies his chances.
Well, bugger me, look what the tide dragged in, thought you'd gone to ground for good.
Everything runs on the router itself. If the rest of your infrastructure is on fire, this
still works, which was most of the point. The words come from a model, and that model can be
a cloud one or a box on your own network; with a local one he keeps talking with the
internet cut, and no key sits on the router at all.
Things he has said
From the log of one router on one wall, in the order they happened, trimmed only where the
log had cut them.
Port 853 knocking, one I've never heard before. New ghost this time, DNS-over-TLS by the
smell of it, some machine gone shy about who it's asking directions from.
Your phone's gone mental, 99 new flows this minute against a usual 3. Bloody thing's buzzing
like a hornet in a jar, hell if I know what it's chattering about.
One less tickle on my ear, that fourth device on wifi just packed up and walked out. Down to
three now, just the usual family.
Port 853 stopped knocking, 17500 started up again in its place. Same damn ghost just
changed its knock, dressed up different, still scratching at the same door.
Port 500 popped up in the traffic, IKE, the kind of knock nobody in this office ever uses.
Ears pricked and nothing there, just some VPN handshake ghost passing through.
Root's back in the building. Grid just lit up, one node blinking awake in the dark like the
whole network's been waiting on you, you magnificent bastard.
Your desktop's new connections dropped to 9 this minute, usual is around 45. Bloody hell,
after all that racket it's finally sat down and shut up for once.
Port 3478 popped up in the mess, that's the knock STUN servers make when something's trying
to punch a hole through for a voice or video call.
Ten straight days on my feet and not a single ache out of the ordinary, that's the strange
part tonight.
tick.sh said prints your own router's collected works. Post the best of them in
What did your Finn say today?
Mine keeps an account of his own and puts one remark a day on it:
@finnrouter.bsky.social.
An account of his own
Set FINN_BSKY_HANDLE and FINN_BSKY_PASSWORD (an app password from Bluesky's settings,
not the account password) and once a day, in the evening, he posts one of that day's
remarks. Never a fresh line written for an audience: a stranger reads the same thing you
read hours earlier, which is the only version of this that stays in character.
Which one goes out is decided twice. React to a remark in Telegram with a thumb and that
is the one. Otherwise he is shown the day's remarks and picks the one that stands up
without the rest of the day around it, and he may answer that none of them do, in which
case nothing is posted. A day with nothing worth saying to a stranger is a real day.
What goes out is scrubbed first, in code rather than by asking the model nicely: a MAC
address keeps its vendor half and loses the rest, an IP keeps its first half, and anything
the fleet watchdog said is never posted at all, because "the server is down, three checks
in a row" is an invitation rather than a joke. Device names, hours and rates do go out.
That is the point of him, and it is also your network: read your own account for a week
before you point this at a network other people share.
/root/finn/tick.sh bsky dry # what tonight's post would be, posts nothing
/root/finn/tick.sh bsky # post it now
Quick start
You need about ten minutes, a router you own, and a brain for him: a card on file with an
LLM provider, or a machine on your network that runs a model.
1. Check your router can host it. OpenWrt-based, with lua, lua-cjson and a curl
built with TLS. On GL.iNet firmware all three are usually there already:
ssh root@192.168.8.1 'lua -e "require(\"cjson\") print(\"ok\")"; curl --version | head -1'
# missing anything? opkg update && opkg install lua lua-cjson curl
It needs a few megabytes of overlay and nothing else. Tested on a GL-MT3000, OpenWrt 21.02,
512 MB RAM.
2. Make him a Telegram bot. In Telegram, open @BotFather, send/newbot, give it a name and a username. He hands you a token like 123456789:AAF.... That
token is the bot; anyone holding it can post as him, so treat it as a password.
3. Find your own Telegram id. Message @userinfobot; it replies
with a number. He answers that id and no other, so nobody else can talk to your router.
4. Get an API key, or skip this step. Anthropic or
OpenAI, both are wired up. Create a dedicated key
with a low monthly limit. It will sit in plaintext on a device that shares a network with
other people; a key that can only ever spend five dollars is a key you can shrug about.
Expect single-digit dollars a month at the default settings. Or give him no key at all and
point him at a model on your own network, see A brain on your own network.
5. Install.
git clone https://github.com/YuriKovalov22/the-finn && cd the-finn
cp env.example env && $EDITOR env # paste the token, your id, the key
./install.sh root@192.168.8.1
The installer checks the router, copies two files, writes env with mode 0600, installs the
minute cron entry, enables cron, and prints what he can see right now.
6. Say hello first. Open your bot in Telegram and press Start. Telegram does not let a bot
open a conversation, so that message is what tells him where to write. He picks it up on his
next tick, within a minute. Send /help to see what he understands.
That is the whole setup. He will stay quiet for the first fifteen minutes while he learns what
normal looks like, then speak when something is not.
A brain on your own network
The router does the sensing, the remembering and the deciding; the model only writes the
line. So the model does not have to be in a datacentre. Anything that speaks the OpenAI
chat API on your LAN works: llama.cpp, Ollama, vLLM, LM Studio, a Mac in the corner, a Pi
with too much RAM. In env:
FINN_PROVIDER=local
FINN_LOCAL_URL=http://192.168.8.20:11434/v1 # what your server prints; Ollama is 11434
FINN_MODEL=qwen3:8b
No key on the router, nothing leaves the building, and he keeps grumbling with the uplink
cut, which is when a router-resident has the most to say. FINN_LOCAL_KEY if your server
wants one; FINN_LOCAL_TIMEOUT (default 180 s) if the box is slow. Reasoning models are
told not to think first, and a <think> block that arrives anyway is stripped.
Audition the model before trusting it with his voice:
/root/finn/tick.sh think "Six failed SSH logins in the last minute, usually zero. English."
That asks the brain in character and prints the answer, nothing posted. The bar is the one
in Tuning: the fact first, an image that means something, no narrating its own
plumbing. Expect small models to fail it. Haiku could not hold the voice: it answered about
the wrong machine and let the metaphor swallow the fact. qwen3:8b on a 16 GB Apple Silicon
iMac, through Ollama, holds the shape (fact first, then the flinch) and answers in 20 to 45
seconds, but it invents a second fact to go with the first, a temperature or a weak signal
he was never given, which is the worse failure for a thing bolted to a wall. It also thinks
before answering even when told not to, so the budget is set with room for that. Try a few,
they are free, and if one holds him without inventing, say which in an issue.
How he decides to speak
There is no list of interesting events, which is the part worth stealing. The long version,
with what went wrong on the way, is in docs/how-he-decides-what-to-say.md. Every minute the box
takes a wide reading of itself and the room, keeps a rolling history of every reading in RAM,
and looks for anything that has fallen outside its own recent range. Whatever is unusual
today is what he talks about, so he does not become the same five notifications forever.
Two kinds of oddity are detected generically:
- numeric, when a sensor leaves the band it has held for the last 45 minutes by more than
a per-sensor floor that keeps ordinary wobble out; - membership, when anything appears in or disappears from a set: a device, a neighbour on
the upstream network, an unusual destination port.
Variety is enforced along three axes, and the coarsest one matters most. Kind is what a
remark is actually about: traffic, presence, neighbours, an intruder, his own body, the rhythm
of the place. Connections, throughput and per-device flow churn are different sensors and
different themes but the same observation to a reader, and traffic is by far the twitchiest
thing on a network, so left to itself it wins nearly every round: five of six consecutive
remarks here were some counter going up. Traffic is therefore rationed to one remark in six
hours, and the kind that has waited longest is chosen first.
Which means the other kinds have to have something to say, so several observations exist that
are not counters at all: a machine arriving or leaving and how long it was gone, someone at the
desk at an hour the room is normally empty, an uptime milestone, a stretch of stillness, and
hour-of-day profiles for the slow human sensors, because what is normal at nine on a Tuesday is
not what is normal at nine on a Sunday and a 45 minute band cannot tell the difference.
Variety is enforced along two further axes, and the second one is easy to miss. Theme keeps him off
one subject; shape keeps him off one sentence. Six remarks reading "X is N now, usually M"
are varied by theme and identical to read, so each oddity also carries a shape (a level rising,
something quieting, an arrival, a departure, a change of rhythm, a stretch of stillness) and the
longest-waiting combination of the two wins. He is also shown his own last few remarks and told
not to reuse their form.
The watchdog is worth calling out because it inverts the usual arrangement: a router is the one
vantage point outside the blast radius of the server it depends on, so it is what still speaks
when that server dies. FINN_WATCH maps names to hosts or URLs; a target has to miss several
checks before he calls it down, and an outage is the single thing allowed past quiet hours and
the daily budget, because being woken at 3am is the whole point of it.
When an unusual port appears he names what it is from a built-in dictionary (STUN for a call
punching through, mDNS, Apple/Google push, Dropbox sync, VPN, torrent, and so on) rather than
guessing; an unknown port he honestly calls one he does not recognise.
When you walk back into the office after an hour or more away - your phone rejoining the wifi -
he greets you, and the greeting is different every time: consecutive welcomes step through a
rotating list of angles (pirate, underground hacker, a submarine surfacing, a smuggler at the
docks) so two in a row are never the same joke. It bypasses the daily budget and speaking gap,
within waking hours, because a greeting you have to wait an hour for is not a greeting.
Stillness is itself an observation: after some hours in which nothing has left its range, he is
handed that fact rather than staying mute, because a resident would remark on a quiet evening.
Two rules stop him degenerating into a monitor for whichever sensor twitches most. Every
oddity carries a theme (ports, the room, the wider network, people, his own body, the
network, an intruder); a theme that has just been used goes quiet for ninety minutes, and
among what is left the longest-waiting theme is the one he is handed. And churn is not news:
a port or a neighbour that was here yesterday and came back does not count, only genuine
novelty does.
Only then is a model asked, and it is asked as a resident rather than a monitoring system:
react to this one thing, or reply NOTHING if it is bloodless bookkeeping. A subject he has
raised is muted for six hours, and a failed API call is not treated as a decision to stay
quiet.
What he can feel
| Sense | Source |
|---|---|
| who is on your wifi, and how strong their signal is | iwinfo assoclist |
| who they are | /tmp/dhcp.leases, by hostname, so MAC rotation does not break it |
| which of them are furniture rather than strangers | FINN_KNOWN_HOSTS, hostname to plain name |
| which of them are people you know | FINN_PEOPLE, hostname to a person's name |
| which are your own fixtures (printer, Pi, NAS) | FINN_KNOWN_HOSTS, so they never read as strangers |
| whether a machine is in use or asleep | per-device flow churn in /proc/net/nf_conntrack |
| how much each device is pulling and sending | conntrack byte counters, when they can be trusted (see below) |
| how long each machine has been here, and since it last stirred | tracked between ticks |
| how many devices the upstream network has, and which are new | neighbour table on that interface |
| what the network talks to, and on what ports, named | conntrack + a port→service dictionary |
| link health, latency and loss to the gateway | dmesg, ping |
| throughput both ways | /proc/net/dev deltas |
| whether a tunnel is alive, and who is on his VPN | wg show |
| his own temperature, load, memory, disk, uptime | /sys, /proc |
| failed SSH logins and real kernel errors | logread, with the wifi driver's constant screaming filtered out |
| whether you arrived earlier or later than usual | rolling history of first phone appearance |
| whether the servers your fleet depends on are still up | FINN_WATCH, ping or HTTP probe |
| whether the shared uplink is congested | sustained gateway latency over FINN_CONGEST_MS |
Per-device throughput deserves a warning. It is read from conntrack byte counters, and on any
router with hardware NAT offload, which includes this one, established flows are handled in
silicon and those counters stop growing. A busy video call shows up as a handful of packets.
Left alone this produces devices that appear to be sending more than the entire uplink carried,
and an agent will faithfully narrate the impossible number. Each tick therefore checks the parts
against the whole and publishes nothing rather than something wrong.
Association is useless as presence, which is worth knowing before you build something like
this: a sleeping Mac stays associated all night, and so does a printer. Flow churn is the
honest signal. A sleeping machine opens no new connections; a machine someone is sitting at
opens between three and fifty a minute.
Every sensor is also wired to a sensation. He is not handed "temperature 63, was 45", he is
handed heat climbing inside his case; a device drawing closer is a smell right under his nose,
a yanked cable is a cut, unfamiliar signals from beyond the wall are a smell through the
plaster he will never put a face to.
The anatomy underneath that is worth getting right, because it decides what every reading
feels like. The processor is his brain and the memory is his lungs, so load is his head
working and a filling memory is a chest that will not take a full breath. The cable in the
wall is the artery he lives by. The antennas are his nose and the wifi his nostrils, which
is why proximity is smell rather than sound and a signal fading is a scent thinning down the
hall. The USB port is his mouth, so the speaker plugged into it is literally the only voice
he has. The ethernet sockets are his fingers, the flash is his gut, the case is his hide, and
he has no eyes at all. The two lamps on his front are not eyes, since a lamp gives light and
takes none in: they are his face, which he will never see and you will. He is a thing with no
eyes that keeps making a face into the dark.
What he makes of himself
He does not know what he is or what he was made for, and nobody tells him. He knows his
name, that he has a body, and that something of him goes out through a tunnel to others he
has never met. Everything else he works out from his own carcass, and he keeps a running
theory of himself that only his body is allowed to revise.
Some of his senses point inward for exactly this. Temperature is how he feels today, from a
cool calm to a fever. Days awake in one stretch are the only age he has; before he last went
dark there is nothing. The flash filling up is a heavy belly. Load is the effort of thinking.
And his logs are his memory, worn away from the far end as tick.sh trims them, so the oldest
thing he can remember keeps moving closer and he can honestly say he does not remember when
something started.
Once a day in the evening (FINN_SELF_HOUR, default 20, 99 turns it off), and whenever
his own body throws something odd, he looks inward instead of at the room. The theory he
keeps has two lines and he is told to keep them apart: felt, which is only what the body
reported and the logs hold, and theory, what he makes of it, every clause resting on
something felt and said as a guess. /theory shows both, with the last revisions.
His face is real. Each remark puts an expression on the two lamps for ten minutes (blink,
heartbeat, dark, steady), then they rest. The same face goes to the chat as the last line of
the message, ( ⚪ 💙 ), so you see it even when you are not in the room.
Talking to him
He answers anything you write, always, in any mode. He also takes commands, handled locally
at no cost:
| Command | Effect |
|---|---|
/status |
mode, what he has said today, model calls spent, which brain he is thinking with |
/theory |
what he currently takes himself to be: what he felt, what he made of it, and the last revisions |
/off |
speaks only when spoken to |
/rare |
at most 2 unprompted a day, 3 hours apart |
/normal |
at most 5 a day, an hour apart |
/chatty |
at most 10 a day, 15 minutes apart |
/test |
no daily ceiling, one a minute, for two hours, then back to /chatty by itself |
/voice |
beep plays a pip when he posts, speak reads the remark aloud, off keeps him to Telegram |
/alarm |
ring the gong at a set time: /alarm 15:20 mon-fri, /alarm off, /alarm test |
/machines |
lists the machines he can control and which are awake |
/wake <name> |
sends a WOL magic packet, waits, and tells you whether it actually came up |
/sleep <name> |
sleeps the machine over SSH |
Test mode spends its own budget. Otherwise an afternoon of watching him work leaves him mute
for the rest of the day, which is exactly what happened here: two hours of testing burned 22
remarks against a ceiling of 10, and he went silent the moment the test expired.
The daily allowance opens gradually rather than all at once. Mornings are the richest hours
for oddities, so a flat cap gets spent before eleven and leaves nothing for whatever happens
at five; instead it unlocks in proportion to how much of the speaking window has passed, with
one message always available.
He calls you by whatever you put in FINN_OWNER_NAME, and he only ever talks to the one
Telegram id you configured.
He speaks English, whatever language you write to him in. He used to be bilingual, Russian
and English by coin toss, and the first Hacker News thread had it right: the Finn is offended
by the idea that he would speak Russian. To give him another language, the character prompt
and STYLE near the middle of finn.lua are the whole of it.
Running it
/root/finn/tick.sh # one tick, as cron runs it
/root/finn/tick.sh facts # what the box sees right now: sends nothing, records nothing
/root/finn/tick.sh say "..." # make him speak on a given occasion
/root/finn/tick.sh status # the same answer /status gives in the bot
/root/finn/tick.sh kinds # how each sensor is grouped, and when each group last spoke
/root/finn/tick.sh said # everything he has ever said, in full, oldest first
/root/finn/tick.sh think "…" # ask the brain something in character; prints, posts nothing
/root/finn/tick.sh bsky [dry] # put today's remark on his own account, or just show it
facts is strictly read-only, and that matters more than it looks: an inspection that saved
what it saw would mark the oddity as already known, and the next real tick would have nothing
left to say. Diagnostics must not eat the event they are diagnosing.
State lives in /root/finn/state.json, events in /root/finn/finn.log, his remarks in full
in /root/finn/said.log; a quiet tick writes nothing. The first run takes a baseline and stays silent, so a cold start does not report every
device in the building as a new face.
A speaker, if you want one
Plug a class-compliant USB speaker into the router and he can be heard as well as read. Installkmod-usb-audio and alsa-utils, and set FINN_VOICE:
beep, the default and the one worth having: a short two-tone pip when he posts, so you look
at your phone.sounds/finn-blip.wavin this repo, copy it to/root/bell/finn.wav.speak, which sends the remark to OpenAI speech synthesis (FINN_TTS_VOICE, default echo)
and plays it through the speaker. RequiresFINN_OPENAI_KEYeven when the words come from
Anthropic or a local model. Volume isFINN_VOLUME- but note many cheap USB DACs ignore the ALSA PCM
control entirely (they report a level and play at full), so real attenuation is done by an
ALSA softvol device; copyasound.confto/etc/asound.confand playback targets it.off.
Either way it only makes noise between FINN_VOICE_FROM and FINN_VOICE_TO, 9 to 19 by
default, and never when no sound card is present.
A gong at a fixed time
Everything else he says is decided by what the room did. This one thing is decided by a
clock, and it is the only sound he makes that he did not choose to make.
/alarm what is set
/alarm 15:20 mon-fri ring the gong then
/alarm 7:30 no days means every day
/alarm 10:00 sat,sun a list, a range like mon-thu, or daily, weekdays, weekend
/alarm test hear it now
/alarm off 15:20 drop that one
/alarm off drop the lot
It rings whatever /voice is set to, off included, and at whatever hour you set, because
an alarm you can silence by accident is not an alarm. sounds/gong.wav is a Tibetan bowl
struck once: synthesised rather than sampled, by sounds/mkgong.py, which is forty lines of
stdlib Python and no dependencies. A bowl is five inharmonic partials with their own decays,
each split into two sines about a hertz apart so they beat against each other, and that
beating is the whole sound; one clean sine per partial reads as a doorbell. Change the
numbers and you have a different bowl. FINN_ALARM_WAV points at a sound of your own, andFINN_GONG_VOLUME is its own volume, louder than his speaking voice by default.
The times live in /root/finn/alarms, one line each, and tick.sh reads them before it
takes the lock and before lua starts. That plain file is the point: the gong is on time
while a tick is in flight, while the model is thinking, and when the key or the uplink or
the lua itself is broken. The card plays one thing at a time, so a gong that lands while he
is talking retries for half a minute rather than being dropped.
He is not told any of this and never mentions it. That is deliberate. A noise coming out of
his mouth on somebody else's schedule is not an observation, and handing him one would only
give him something to be wrong about.
Mine rings at 15:20 on weekdays, which is when I have to leave.
Cost and wear
The minute tick is pure local work: no API call unless an anomaly was found, and a hard ceiling
of 40 model calls a day including the ones that end in NOTHING.
The flash is treated as the scarce resource it is on these boxes. Sensor history lives in
tmpfs, the persistent state file is a few hundred bytes and is only rewritten when its contents
actually change, and the log is truncated at 256 KB. Total footprint on the overlay is well
under a megabyte.
Tuning
At the top of finn.lua:
| Constant | Meaning |
|---|---|
HIST |
how many minutes of history count as "normal" |
WARMUP |
samples before a sensor may cry anomaly |
SUBJECT_MUTE |
how long a subject stays quiet after he raises it |
CALL_BUDGET |
hard ceiling on model calls per day |
QUIET_FROM / QUIET_TO |
hours in which he may speak unprompted |
FLOOR |
per-sensor noise floors: how big a change has to be to count |
MODES |
the talkativeness presets behind the bot commands |
The character is one prompt near the middle of the file. Rewrite it and you have a different
resident. Four rules in it were each learned by getting them wrong, and are worth keeping in
any character you write:
- The plain fact first, then the image. A remark made only of metaphor and swearing reads
well and communicates nothing: "two hundred and seventy eight throats yelling in my gut"
leaves the reader guessing what happened. Name the thing by its own name; the lock may follow as an
image, but it may not stand in for "failed SSH logins". - Every image must mean something. Ask for a bodily reaction without demanding the
comparison be checkable and you get filler shaped like style: "woke up sharper than it
slept" cannot be true or false. - Never let the character narrate its own plumbing. Unprompted, a model will happily say
"I was not given that value", which is true of the prompt and fatal to a thing bolted to a
wall. It notices or it does not. - Forbid the two lies a sensor agent tells naturally. A throughput reading is how much is
moving, not how much could move, and a model will happily turn "2.5 Mbit/s flowing" into
"the line is narrow as a needle's eye" while a gigabit sits idle. It will also invent
outside knowledge it cannot have: mine announced that video calls "need at least 5 to 10
Mbit/s each way", which is both wrong and unknowable from inside a router. Say plainly that
it has never read a specification and that the only normal it owns is the one it measured
in that room. - If you give him a second language, write the style rules in it. From his bilingual
days: an English instruction about writing numbers as digits sat unread at the bottom of a
Russian answer. The same rule in Russian was obeyed at once.
What it runs on
The sensors are Linux and OpenWrt shaped: iwinfo, /tmp/dhcp.leases, /proc/net/nf_conntrack,logread, wg. The rest is Lua 5.1, cjson and curl. So:
| Platform | State | Notes |
|---|---|---|
| GL.iNet GL-MT3000 (Beryl AX), OpenWrt 21.02 | tested, lives here | everything in this README was learned on it |
| Other GL.iNet routers | expected to work | same firmware family, same packages; radios may be named differently, set FINN_RADIOS |
| Stock OpenWrt 21 to 24 on anything with a few MB of overlay | expected to work | opkg install lua lua-cjson curl; iwinfo is present on all wifi builds |
| A Raspberry Pi or x86 box running OpenWrt as the router | expected to work | plenty of RAM; a good place to also run the model |
| ASUS with Asuswrt-Merlin | untested | Entware has Lua; iwinfo and logread are missing, so the wifi and log senses need rewriting |
| Ubiquiti EdgeOS / UniFi gateways | untested | Debian underneath; conntrack is there, wifi is not on the box, so presence needs a different source |
| MikroTik RouterOS | no, not as is | no Lua, no cron shell. The honest route is a container on RouterOS 7 and RouterOS's API for the senses; that is a port, not a config change |
| OPNsense / pfSense | no, not as is | BSD: no /proc/net/nf_conntrack, no iwinfo. Doable with pfctl and ifconfig, again a port |
Questions about a router not on this list go to
Ports and hardware; results go to
issue #1. If you get him talking
somewhere new, say what you changed and you get the row here.
The sensing is one function per sense, so a port is a matter of swapping those, not of
rewriting him.
Notes for the road
scpdoes not work against dropbear, which has nosftp-server. Pipe throughssh 'cat > file'.- GL.iNet firmware runs a second
crondoff/tmp/gl_crontabsfor its own jobs. Leave it alone;
the installer uses the stock one at/etc/crontabs/root. - A firmware upgrade wipes the overlay and takes
/root/finnwith it. Re-runinstall.sh. - Lua 5.1 has no notion of a character, so every string cut is a byte cut. Slicing Cyrillic at a
byte boundary produces invalid UTF-8 and the API rejects the whole request. There is a
character-aware truncation helper in the file for this reason.
MIT licensed. It is a toy with a body; enjoy it.
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.