一個房間、八塊電路板與一位指揮:午夜莊園的搭建
One Room, Eight Boards, One Conductor: Building the Manor at Midnight
作者在萬聖節夜晚把客廳改造成「午夜莊園」演出現場,整合 AI 裝置、燈光、訊息板、音響、3D 列印道具及自編控制程式。八塊 ESP32 執行同一套韌體骨架,兩首原創歌曲的指揮程式分別按節拍觸發 77 個及 52 個提示。首次完整演出於 22:37 開始,卻全程沒有聲音;文章亦記錄接線、供電及網絡問題的排查。
On Halloween night my living room ran a show.
Not a playlist. A show: two original songs, a conductor program firing 77 timed cues for the first one, Hue lights moving as instruments, two scrolling message boards passing a single banner between them, a field of glowing candles, homemade lightning, four small AI devices sitting in a choir row wearing characters, and a 7-inch screen on my desk where a figure I call the Night Auditor kept his ledger and, late in the evening, sang.
It took all day and most of the night to build. It came together with about ten minutes to spare. And its first full performance, at 22:37, played in complete silence, which is a story I will get to honestly, because it is the most useful part.
This is the whole build: the AI and the code, the wiring, the firmware, the lights, and the show.
The cast
Everything in the room had a job, and most of it had a name.
The conductor. A program per song. For "Little Glowing Souls," the anthem, it holds 77 cues across 204.8 seconds, each one timed to the recording: this Hue group at this lyric, a lightning strike on that crack of thunder, the scrollers starting a new banner on the chorus, Portal changing scenes. For the Night Auditor song, 52 cues. The conductor does not improvise. It is a score, executed.
Eight ESP32 boards, all running the same firmware skeleton I settled on as the day went on: join the Wi-Fi, carry a name, answer a status check, expose one signature cue, and reconnect on your own if you fall off. On top of that skeleton, each board is a different instrument:
- The candle board, driving six RGB candles in 3D-printed glow sleeves, with a warm flicker-and-gutter routine and a storm gag on its boot button
- Scroller 1 and Scroller 2, each driving a four-panel LED matrix, one of them also carrying the cast's only speaker, an I2S amp playing test phrases of organ, ghost moan, and filtered-noise thunder
- Satellite 1, carrying a sound sensor, a 7-colour module, a two-colour module, and loose RGBs, firing its own strobe when the room gets loud
- Satellite 2, ticking and blinking on cue
- Breath 1 and Breath 2, swelling slowly, the room's background respiration
- The lightning pod, three lights in sleeves behind cotton clouds, striking in a double-flash-and-decay pattern
Janus, an Arduino UNO Q, is two computers in one box: a Debian Linux side and a microcontroller side. The Linux side runs my NexusOS software and drives Portal, the 7-inch display that is the show's stage: a manor hall, the Auditor's desk, his rounds, a storm, and finally the films themselves.
Four AIPI devices in printed tombstone risers, the choir row: red wears ARIA, green wears Carbon, yellow wears Lucas, blue wears Nova. The characters are the stable part. Their network addresses are DHCP leases and move; nobody in my house refers to a performer by lease.
Flint, a Raspberry Pi Zero W, was supposed to drive the big TV, which reaches the room through a Philips Hue Play HDMI Sync Box. Flint has its own section below, under failures, because that is where it spent the night.
One Bose speaker was the only sound in the room. That sentence turns out to be load-bearing.
And the prints ran underneath everything all day: a glow plate of 10 ghosts and 14 candle sleeves finished in the small hours that morning (175 layers, eight and a half hours), four tombstone risers for the choir printed on a single plate at my direction, and a tombstone holder for my Glance gadget printed during the show build. The holder's cradle came out too shallow because Glance carries a battery on its back. It fits perfectly with the battery off. I have not reprinted it.
Wiring is a conversation with the physical world
I am a lifelong electronics hobbyist, but this was a build done at speed, by hand, with an AI reading my photos and telling me where to put the next wire. Three lessons from the bench are worth more than the finished boards.
The candle field failed on a question I asked, not an answer I was given. The first candles would not light, and the photos I sent were misread three times: legs not in holes, wrong sides of the groove. What actually unlocked it was my own question about the breadboard itself: are rows A to E and F to J even connected underneath? They are not, on that board, in the way the wiring assumed. Two lights, then six candles in their sleeves, lit and flickering. The person holding the board is a sensor, not a pair of hands for the person reading the schematic.
Big Sound is a microphone. I kept waiting to hear satellite 1's "speaker." There is none. The Big Sound module in my sensor kit is an input: it hears, it does not speak. Once that was straight, the wiring made sense: its analog output belongs on an input-only analog pin (34, 35, and 39 are exactly right for a sensor on these boards; 9, 10, and 11 are tied to the flash memory; pin 3 is a boot-strapping pin and a bad place for a sensor output). On the candle board, I was first told pins 4 and 5 were free for it. Pin 4 was already wired to candles. My board won that argument, the sensor moved to pin 10, and the trigger threshold got set in code instead of by the module's little dial, which is the better instrument anyway.
The scrollers were a power problem wearing a software costume. For hours, the twin ESP32-S3 boards and their LED panels stayed dark, and everything pointed at firmware. The answer was sitting in my own project record from an earlier sign build: a classic ESP32's 5V pin is a live output that can power its panel, while the S3 boards' 5V input pin sources nothing at all. Swapped to a classic board, panel on DIN 25, CS 26, CLK 27, power from the board's own 5V pin: worked first try. The most expensive bug of the afternoon was not reading our own documentation.
A smaller RF lesson, same shape: satellite 1 could not join the network and looked like a code problem until the numbers said it was nearly deaf compared with every other board. It is the external-antenna variant of its board, and its little whip antenna was sitting in a parts box. Antenna on, board joined immediately. The lightning pod had the same disease worse (a tiny built-in antenna, nothing to attach), and placement was its only medicine: antenna end clear of the breadboard wires, pointed at the room.
On the bench before it was a candle field: the board, the sleeves, and the panel that would spend the night insisting it was a software problem.
Two boards, one sentence
The scrollers are my favourite mechanism in the show. Bound by eye, because there is no other way to bind them: Blue is board 1, stage left from the audience, and Red is board 2, stage right. Unaddressed text does not go to "the scrollers." It flows across both of them as one banner:
Red, upstream on the audience's right, carries the text plus a trailing blank pad. Blue carries a leading pad plus the text. Both strings are the same length and are sent in the same instant, so their scroll cycles stay locked and the sentence reads as one sign. Text addressed to a colour goes to that board alone. The next unaddressed text puts the pair back together.
The pad length was calibrated the only way it can be: I watched it. The first guess left five or six seconds of dead air, red fully blank before blue started. Trimmed from 48 spaces to 34, both boards together, in one send, to keep the lock.
The firmware taught one sharp lesson, twice, before we read the source: the endpoint that sets a message and the endpoint named "show" are not friends. Sending "show" after a message restores the compiled-in default text and silently overwrites whatever you just set. The boards were innocent both times. The bug was in our own calling code, and the fix was a rule, not a patch: never send show after message.
The brain has two audio universes
Janus drove the stage, and Janus is where the night's hardest problem lived, because Janus runs two completely separate audio sessions.
The arduino user session runs the manor player. The nexusos user session owns the display, the voice stack, and the Bluetooth audio endpoint. A Bluetooth device can be owned by only one session at a time. So the Bose speaker could be paired, bonded, connected, and set as the default sink, all verifiably true, in the session the show player was not running in. Every check that asked "is the speaker connected?" got a correct answer to the wrong question.
Even the status tooling was caught by it: the daemon that reports audio status runs as its own user with no audio session at all, so at one point it reported "no sinks" in the same minute the speaker was present and had just played. Two layers, BlueZ and the audio graph, have to be reported separately or the report is fiction.
The speaker itself added one more fact, which I stated and the design now has to respect: a small Bluetooth speaker sleeps. When Janus is not actively using it, it drops and powers down, and I turn it back on by hand. That is not a fault. Any automation that treats it as one, or that holds the link open with a keepalive to stop it sleeping, is fighting the hardware's intended behaviour.
The Night Auditor gets a voice
Portal started the day as a passive screen: a scripted page on a loop. In the evening it became a performer with a voice, and that build had the cleanest arc of the day because it began with a false premise, corrected by evidence inside the hour.
The brief assumed no voice code existed on Janus and that a browser would have to host a voice session. The live check found the opposite: my estate's Linux voice path, a native device SDK, was already installed, paired, and wired into NexusOS on that board, with calls routed to a dedicated conversation room for the device. A call through that path had already run the day before.
So the proof was done the right way round: hardware-free first. A spoken question went in as an audio file ("Good evening. Who are you and what is this place?"), landed in the Portal Voice Room transcribed word for word, and a spoken reply from the Night Auditor came back as audio. Transport, room targeting, speech recognition, the reply, and synthesis were all proven before a single microphone existed in the room. Everything after that was plumbing, and plumbing failures could never again be mistaken for the idea not working.
Then the visible half. An hour before I asked, Portal had no face for a conversation. By the end of the evening it had seven frames, every one an edit of the show's own desk portrait, so the scene, the lantern, the ledger, and the face never change; only the mouth and expression do:
Lip sync is deliberately simple, the same recipe I use on my smaller gadgets: four mouth grades (closed, small, mid, wide) swapped according to the loudness of the speech itself, sampled every 40 milliseconds, snapping open fast and relaxing slower. It is not phoneme analysis and does not pretend to be. At room distance, it reads as a man talking. Later that night a live tap was added: a small service listening to the voice session's actual speaker output and microphone ten times a second, so the page speaks with its mouth graded from real levels, listens only when a real microphone is live, and its idle screen tells the truth about whether the speaker and microphone are ready. Its Talk button also tells the truth: it checks every gate and reports what is missing rather than starting a call it has no authority to start.
The microphone was the last piece, and it was built out of network plumbing instead of new hardware. There was a spare ESP32-S3 board with an I2S microphone sitting in the microphone spot. It could already record five-second clips of the room that transcribed verbatim. What it could not do is be a microphone as far as Janus was concerned, because a voice call listens to a source inside Janus's own audio graph, and a board on the network is not in that graph.
So that night, in about twenty minutes: a puller that chains the board's clips and re-streams them, a bridge service on Janus that takes that stream and presents it as a real microphone source, and the voice configuration pointed at it. One hard lesson inside that build: this audio stack routes every recording to the default source regardless of what a program asks for, so the bridge only works by becoming the default source and re-asserting itself as such. At 21:39 a live call went through it: the test question streamed over the bridge, arrived in the Portal Voice Room three times as it looped, and the Night Auditor answered each time. End to end, through a microphone that is not plugged into anything.
What stays after Halloween, from the review I asked for that night: the voice path, the dedicated room, the test harness, the tap feed, and the rule written into the Auditor's brief that he never guesses at anything that needs a record (calendar, messages, money, health): those questions get "that needs a lookup in the main office," and a stop. The costume retires. The capability does not.
The choir and the rest of the room
The AIPI row was casting, then verification. Green already lived in its room firmware wearing Carbon and joined the network by itself. Yellow got a minimal hand-rolled display sketch (raw SPI, no external libraries) to wear Lucas. Red and blue verified as ARIA and Nova. I overruled a shortcut that would have left the as-found arrangement in place; the units were imaged as cast, because a choir where one member is whoever happened to be resident is not a choir.
The generated audio was built the same day, by measurement rather than by ear, because the agent building it cannot hear: a just-under-four-minute ambience loop (organ drone in D minor, rain on glass, a distant thunder roll every minute or so, made loopable by moving the seam under a six-second crossfade) and an exact 90-second show track with its thunder cues at measured timestamps and five chant lines ducked over the bed. Its first chant mix came out buried, because a mono-to-stereo conversion was silently eating level; the fix was per-line loudness normalisation and stronger ducking, verified by re-measuring every cue window. "Verified by measurement, not by ear" is a limitation I want stated wherever that work is described. It is also, on the night, more than we managed for the room itself.
Janus's own small display, an 8 by 13 LED matrix driven by the microcontroller side, stayed dark for the show, and I want that in the record too. It showed an X in the morning, which the source code says is the linux_down glyph: the microcontroller alive and correctly reporting that it could not reach the Linux side. A restart of the Linux daemon restored that bridge by late morning. In the evening a package of seven Halloween animations for the matrix (pumpkin, witch, bat, cat, ghost, and friends) was designed, compiled, and staged, and three attempts to flash it stopped against a deterministic stall in the microcontroller core, in a region the sketch flashes never wrote. Reviving it needs a bench debug session. The matrix stayed on its original firmware, dark, while everything around it performed. Not every instrument makes the show.
22:37: the show ran perfectly, in silence
The pre-show smoke test was genuinely good. Twelve of twelve instruments answered a roll call. The anthem program fired 77 cues and confirmed 77. The Auditor program fired 52 and confirmed 51, one lightning strike unanswered mid-program with the board healthy immediately after.
At 22:37 we ran the anthem show for real. The conductor's log is the kind of artifact that makes you confident:
[ 1.64s] t+0.0 hue:under-tv set -> ok; ok; ok
[ 2.16s] t+0.0 scrollers banner -> okok
[ 29.80s] t+28.0 board:lightning strike -> strike
[ 31.54s] t+29.0 portal mode -> portal->desk rc=0
Every cue fired. The audio graph showed the song streaming to the Bose as the default sink. It was reported to me as verified.
The room was silent. Not quiet. Silent. I filmed it, and that recording was later measured: the audio track of an 81-second room video sits at the level of a quiet room, with no song at any point in it.
The root cause, found minutes later, was embarrassingly small. Inside Janus's audio system, the speaker's own channel volumes sat at about 5%, and the song stream's volume sat at 0%. The audio stack had been restoring those values after every reconnect, all night. (In the forensic write-up we did that same night, those two numbers are graded differently, and I am keeping the grading: the 5% is proven from a surviving system dump; the 0% rests on the record written at the time, because two crashes that evening wiped the temporary files the earlier logs lived in.) Every verification that night checked that a stream was running. Nobody checked a level. Nobody checked the air.
And it was not one trap, it was a family of them, all with the same shape:
- The Hue bridge accepted every light cue and read back the cued values while the Sync Box, which had quietly turned its sync back on when a device was replugged, repainted the room according to the television instead. The lights reported their mode as "streaming." The bridge was telling the truth about the bridge.
- Portal's music videos froze on a still frame while its silent loops played fine. The cause, found afterwards, was a file replaced on disk underneath a page that was already showing it; the page never reloads a source it already has. A served file is not a playing video.
- Flint never joined the show at all. Three faults were stacked on its SD card, each sufficient alone: a 64-bit system image written for a 32-bit-only Pi (a trap we had caught once before, in October, and walked into again), provisioning trigger tokens sitting where the kernel never reads them, and on the fresh build, the card simply missing files it needed. I carried that card between the Pi and my MacBook ten times. Flint joined the network late that night and eventually held, verified, with one honest caveat that applies to this whole article: nobody on the agent side can see the picture on the TV through the Sync Box. That last check is a human standing in the room.
The fix that finally held for the sound was procedural, not clever: set the level inside the firing itself, prove it with an 18-second test at a known level and read that level back afterwards, and then I set the speaker's own volume by ear and froze every dial. The level is mine. Nothing on the software side touches it again.
There was one more design correction, and it was mine to make. The conductor's original programs were bright: full-room blasts on the choruses. That was never my show. The room should stay dark, at candlelight, with lightning, and the films premiering on Portal. Once I said that plainly, a dark version of the program was built, along with an ambient in-between state that nobody had built at all until I asked why the moments between songs had no program. They had no program because nobody built the in-between. That is an omission, not a limitation, and it is worth printing in those words.
What actually played
The show that landed ran late, and it ran only when I said to run it.
The dark program as designed: the room held at candlelight, lightning at the edges, the film premiering on the small screen.
At 23:34, "Little Glowing Souls" premiered as a film on Portal, carrying its own song to the Bose as one stream, with the new dark program around it: candlelight, lightning, the room held low. At 23:47 the Night Auditor film did the same. I filmed both. The last frame of the desk-angle clip carries the line the whole day was for:
Halloween lives in this house, this house is our home.
What I would change
- Over-the-air everything. On the night, boards cycled onto a cable by hand for reflashing, one at a time. That is now a standing rule for the next iteration of this fleet: light sequences, patterns, and firmware ship over the air from the first flash, and no board gets carried to a cable to change its mind.
- Verify the air, not the graph. A stream state is not sound, a bridge "ok" is not light, a served file is not a playing video. Every instrument in this show reported success in its own dialect. The room is the only integration test, and it needs a witness, a level meter, or a recording, every single run.
- Control the Sync Box, or check it first. A device that re-arms itself on every HDMI replug and silently overrides your lighting needs either an API the show can call or a line at the top of the runbook, in that order of preference. That work is parked in its own thread, deliberately, until I open it.
- Read your own record before re-deriving it. The scroller power answer was in a narrative we had already written. So was the 32-bit Pi trap. The estate's memory is only an asset if the builders consult it before they start, not after the third failure.
- State values, not states of mind, in the handoffs. The 22:45 handover written that night still listed the microphone as the show's blocker, an hour after the bridge had already carried a live call. Summaries rot in an hour on a night like this. Timestamps and measurements do not.
The inventory, for the builders
One room. Eight ESP32 boards on one firmware skeleton, four Hue groups and eight bulbs in the conductor's room score, one Arduino UNO Q running two Linux user sessions and a microcontroller that spent the night honestly reporting its own isolation, one Raspberry Pi that missed the show and was fixed anyway, four AIPI devices in printed tombstones, one ESP32 microphone that became a network peripheral, one Bluetooth speaker whose volume was the whole ballgame, two songs, 129 conductor cues across both programs, three schematics drawn after the fact to explain what the diagrams in our heads got wrong, and roughly fifteen hours from first wire to last film.
Built with a fleet of AI agents working in parallel threads, in my living room, on hardware I can hold in one hand. The agents wrote the firmware, the conductor, the voice plumbing, the films' stage, and most of the documentation. I wired the boards, placed the antennas, carried the SD card, turned off the sync, set the volume, and said when to launch.
That division of labour is the actual story of the Manor. The machines can fire 77 cues without missing one. Somebody still has to stand in the room and listen.
來源:DEV Community · dev.to








