Home office redesign · plan for review · updated Sep 25, 2026
IR control link
Carries IR control from the side table to the racks over one slim Cat6 cable, between an IR sender at the side table and an IR receiver at the racks. Nothing is built yet. The parts list has a column for what's already on hand.
Requirements
- The four remote boxes (ZP3, Arcam, Yamaha, Flex) are never opened; firmware changes are fine. Each still works as a standalone IR remote: switch it to emitter mode and plug a 3.5 mm mono emitter cable into its jack.
- The amp selector is the orchestration brain. It already holds the routing (
AUDIO_PATHS[],select_gear,fire_ir). It gets rebuilt and stops sending IR itself. - The sender and receiver hold no orchestration. They carry what they're given.
- No wireless in the control path. The desk box and office power controller may depend on Home Assistant.
- No soldering: breadboards, pinned jumpers and screw terminals. First flash over USB, then Wi-Fi.
System
- IR signal
- serial data
- trunk cable
- ZP3 12 V trigger
- Remote boxes → sender. Each box plugs into a jack on the sender with a 3.5 mm male–male mono cable. In link mode the box sends its command pulses with no carrier. Behind each jack a PC817 optocoupler passes those pulses to the ESP32. Only light crosses inside the chip, so the boxes share no ground with the sender or with each other.
- Sender. Captures each input's timing and relays it raw, so it never decodes RC-5 or its toggle bit. The input port sets the carrier the receiver adds back: 36 kHz for the Arcam, 38 kHz for the rest.
- Selector → sender. Short RJ45 patch, 3.3 V serial. The selector sends NEC commands addressed by port number.
- Sender → receiver. RS-485, one direction. The sender writes one frame at a time, so the serial buffer is the queue.
- Receiver. Jacks 9–12 each have their own transmit channel and can fire at the same time. Jacks 1–6 share one channel through the mux, as the selector does today; that channel finishes each frame before the mux switches.
Remote box modes
| Mode | Jack output | Plug into | LED blinks |
|---|---|---|---|
| Emitter | Command pulses on a 38 kHz carrier (36 kHz for the Arcam), as today | an IR emitter cable | 2 |
| Link | Command pulses, no carrier | the sender | 4 |
- Hold the power button 5 s to switch modes. A short press now sends power on release instead of on press, so the hold never sends power. The power buttons are APIELE 16 mm mushroom-head momentaries (1NO, M16 × 1, 16 mm hole): the contact stays closed while held, so the firmware times the hold.
- The mode survives reboots, and the box blinks its mode at boot. A fresh flash starts in emitter mode, so every box behaves as today until it's switched.
- A box in link mode does nothing on an emitter cable, and a box in emitter mode sends the sender a signal it can't read. The boot blinks show which mode a box is in.
Firmware changes, all four boxes, over Wi-Fi
- A saved mode setting, the 5 s gesture and the blink patterns.
- Each IR send picks its carrier from the mode: 38 kHz, 36 kHz or none. The Arcam box's RC-5 codes already set their carrier this way; the
transmit_necandtransmit_rc5actions become the same kind of code block. - The ZP3, Yamaha and Arcam boxes get the paced volume scripts the Flex already uses (100 ms after each send). Today Home Assistant can drive their volume fast enough to send frames back to back, which the sender would capture as one oversized frame.
- Bench test before relying on it: with the carrier off, the output is a clean unmodulated pulse train.
Cables to the racks: 2
Drawn to scale: slim Cat6 0.11″ (2.8 mm, from its spec sheet) and glass TOSLINK ≈0.2″. The Cat6 carries the IR link and the ZP3 trigger; the optical carries the D10s's digital feed.
Trunk pinout, T568B straight-through
| Pins | Pair | Use |
|---|---|---|
| 1 / 2 | 2, orange | RS-485 A / B, sender → receiver |
| 3 / 6 | 3, green | 3 = +3.3 V from the sender, powering the receiver's MAX3485; 6 = spare |
| 4 / 5 | 1, blue | ZP3 12 V trigger: 4 = tip (+12 V), 5 = sleeve (ZP3 ground) |
| 7 / 8 | 4, brown | RS-485 signal common, and the return for pin 3 |
The trigger pair runs from a screw terminal to a 3.5 mm panel jack at each end and touches nothing else: not the breadboard, not either box's ground. At the side table it still lands only on the ZP3 box's optocoupler, as today. Each RJ45 is a screw-terminal breakout board whose own jack pokes through the panel, so there are no couplers or internal patch cords. The trunk is unshielded (UTP), so the jacks' metal shells connect to nothing; check with a meter that no shell is tied to one of the 8 terminals.
Selector ↔ sender patch, 1–3 ft
| Pin | Use |
|---|---|
| 1 | Selector TX → sender RX |
| 2, 6 | Ground |
| 3 | Sender TX → selector RX (reserved for acknowledgements) |
| 4, 5, 7, 8 | Spare |
Sender, at the side table
ESP32-WROOM-32U DevKitC V4 straddling two half-size breadboards, which also hold the four input circuits, the MAX3485 and the fuse, in a Parts Express 320-482, the same case as the amp selector, stacked with it. Every cable enters at the rear panel; the front carries two SH1122 OLEDs, the left showing the active preamp and amp with the sample rate and format, the right the link status, Wi-Fi and the selector's channel (also the Douk's input), speaker pair and BlueRigger input, with a FILN button at each end: the left toggles the screens, the right advances the Douk.
| Input | Remote box | Parts | GPIO |
|---|---|---|---|
| 1 | Fosi ZP3 | PC817, 150 Ω, 4.7 kΩ pull-up | 34 |
| 2 | Yamaha R-N800A | PC817, 150 Ω, 4.7 kΩ pull-up | 35 |
| 3 | Arcam AVR20 | PC817, 150 Ω, 4.7 kΩ pull-up | 36 (VP) |
| 4 | miniDSP Flex | PC817, 150 Ω, 4.7 kΩ pull-up | 39 (VN) |
| 5–8 | future remote boxes | same | 25, 16, 17, 21 |
- During a pulse the box's pin goes high, about 8 mA lights the PC817's LED, and the phototransistor pulls the GPIO low. Between pulses the 4.7 kΩ pulls it back up. GPIO34–39 have no internal pull-ups, so each input needs its own.
- The PC817 switches in about 20–50 µs with a 4.7 kΩ pull-up, under 10% of the shortest pulse (NEC's 560 µs). A 1 kΩ pull-up switches faster if the bench test shows distortion.
- Each box's sleeve goes only to its own PC817 pin 2: never to the sender's ground or another input's sleeve. On the breadboard, keep each box's two wires on the pin 1–2 side and the sender's wiring on the pin 3–4 side.
- Capture:
remote_receiver×4,inverted: true,idle: 10ms(a 20-bit RC-5X frame has a gap of up to 5.3 ms mid-frame),rmt_symbols: 64;on_rawbuilds a frame with that input's carrier. - Displays (2 × SH1122, SPI, one bus): SCK GPIO18, MOSI GPIO23, DC GPIO4, CS GPIO5 (left) and GPIO13 (right), both resets to EN.
- Left display: the first column of today's selector screen, the preamp and amp names over the sample rate and format. Right display: link status and a Wi-Fi icon, plus the selector's channel number (the same as the Douk's input), speaker pair and BlueRigger input; its layout is still to be worked out. The selector sends everything it owns as
Dframes over the serial link; link status and Wi-Fi are the sender's own. - Front buttons, FILN 12 mm, internal pull-ups: left (screens) on GPIO19, right (Douk) on GPIO26. Their LEDs, through 470 Ω as on the selector, on GPIO22 and GPIO14; each lights briefly on a press, for feedback only.
- Left button: a press turns both displays off or back on. The displays, and the receiver's, also go off after 5 min without activity: a gear or OPT change, the active path's knob, a volume change on CH 3 or CH 4 (including the Eversolo's or Arcam's own knob and the Apple TV remote), or either button. Right button: each press puts one Douk input-cycle command (NEC
0xBBAA,0x0D) on the trunk for jack 5. It corrects drift from a missed IR frame, so the selector's count of the Douk's input stays as it is; press until the Douk's input matches the channel number on the right display. The sender writes the frame itself, between relayed frames, so patch pin 3 stays reserved. - Selector link on the left header, next to each other: serial in on 32 (selector RJ45 terminal 1), serial out on 33 (terminal 3), ground on the left header's GND (terminals 2 and 6). The ESP32 routes its UART to any pin, so these replace 16 and 17, which join the future-input pins with 25 and 21. RS-485 out on GPIO27 into the MAX3485 module's DI, with EN tied high. These modules name TXD and RXD from their own side (TXD is RO, RXD is DI; found with a continuity beep on the receiver's), so 27 goes to RXD. The sender's 3V3 pin also feeds trunk pin 3, through an RXEF005 resettable fuse, which powers the receiver's MAX3485.
- The ZP3 trigger pair lands on its own panel jack. A short 3.5 mm male–male goes to the ZP3 box's trigger jack.
Enclosure: Parts Express 320-482, stacked with the selector
- The second of the two cases on hand; the 320-482 is no longer sold. 224.8 × 39.9 × 165.1 mm outside; Parts Express gives the usable inside as 197 × 27 × 149 mm (7.75 × 1.06 × 5.88″).
- Panels are the selector's flat 218 × 34.5 × 1.5 mm plates (
hammond-enclosure-panels/plastic-white-black-enclosure/pla/), printed in PETG. - 27 mm is the limit: the ESP32 on the breadboards stands ≈20 mm. Keep jumpers flat.
- Plastic body: nothing to short against, and no Wi-Fi shielding (the SMA antenna stays for range). Double-sided tape holds the boards.
| End | Contents | Width used |
|---|---|---|
| Rear | RJ45 from the selector, 4 input jacks, ZP3 trigger jack, USB-B, SMA, RJ45 trunk, in one row with an RJ45 in each corner | ≈183 of 218 mm |
| Front | 2 × SH1122 side by side, centred; a FILN button at 16 and at 202 mm | ≈205 mm |
Inside the 320-482, top view to scale
The two half breadboards sit side by side, 110 mm across and 82 mm deep, ≈6 mm in front of the RJ45 boards and ≈24 mm behind the displays. The ESP32 straddles their joint with one header row in each board, leaving free holes beside every pin, its USB end toward the rear panel's USB-B. The PC817s, MAX3485 and fuse use the ≈25 mm of columns in front of it. Everything has to stay under the 27 mm ceiling: lay jumpers flat, and a strip of tape keeps them seated. The two RJ45 boards lie flat on the floor behind the rear panel. Corner screw posts drawn at approximate positions; Parts Express's usable 197 mm is the width between them.
Sender end panels, to scale
Rear, left to right seen from behind: the RJ45 from the selector at 30 mm, the same position as the selector's RJ45 to the sender, so the two jacks line up when the cases are stacked; the four inputs at 52, 67, 82 and 97 mm (ZP3, Yamaha, Arcam, Flex, matching inputs 1–4) and the ZP3 trigger jack at 112, next in the row at the same 15 mm pitch and marked by a different-coloured panel nut; USB-B at 139 (screws at 123.75 and 154.25); SMA at 167; and the trunk RJ45 at 188. The floor's mounting bosses leave room for one RJ45 breakout in each rear corner: measured, the board's outer edge sits 17.5 mm from the panel's outer edge, so the jack is at ≈30 mm, and the selector's breakout sits in the same corner against the same bosses. Front: two SH1122s with the selector's window and mounting-hole pattern, centred as a pair, and Ø12.2 button holes at 16 and 202 mm, the same positions as the selector's outer buttons directly below; each button nut clears its display board by ≈4.5 mm. Jack holes Ø6.4 mm. RJ45 cutouts 18 × 15.5 mm, about 1 mm clear of the ≈16 × 13.5 mm jack on every side: the breakout is taped to the floor, so the cutout only has to clear the jack, and the slack absorbs measuring and print error. Measured from the case's outer edges, the jack centres sit 33 mm (left) and 33.5 mm (right) in, ≈30 mm from each panel edge. The jack's top is 19.5 mm above the case's outside bottom, so with the breakout taped to the floor its centre is ≈12.75 mm up, below the 17.25 mm centreline the other parts use. Measured from the floor of the groove the panel sits in, the jack's top is 17.2 mm up (the groove floor is 2.3 mm above the case's outside bottom), so its centre is ≈10.45 mm above the panel's bottom edge and the cutout runs from ≈2.7 to 18.2 mm. The selector's RJ45 cutout uses the same height.
RJ45 breakout terminals, seen from inside the box
From the board's copper layout: the jack's square pad marks its pin 1, and the visible traces run terminal 3 to pin 3, 5 to 5 and 8 to 8, so the terminal block follows the jack's order. Seen from inside the box, terminal 1 is on the right and terminal 8, at the square-pad end, on the left. Confirm pin 1 once with a continuity beep against a T568B patch cable's orange-white wire. The same order applies to both breakouts.
Receiver, at the racks
ESP32-WROOM-32U DevKitC V4 straddling a half-size and a full-size breadboard, in a Hammond 1455U2201 (extruded aluminium, 191 × 66 mm end panels, 220 mm long) with 3D-printed end panels. It sits on the ZP3 on the east rack's upper shelf, fronts aligned, its back overhanging the ZP3 by ≈3″. Powered from an always-on outlet on the PDU through the rear USB-B: its 5 V and ground feed the breadboard rails and the ESP32's 5V pin, and its data pair goes to the Nano Every, so the Nano can be reflashed from outside the box. The ESP32 was flashed once over its own USB and updates over Wi-Fi (192.168.4.48); its USB stays unplugged while the rail is powered. Every cable enters at the rear panel; the front panel carries only the display.
| Jack | Target | Drive | Signal |
|---|---|---|---|
| 1 | Fosi LC30 | 74HC4051 pin 12, 120 Ω | NEC, 38 kHz |
| 2 | NFJ&FX PW-6 #1 | 74HC4051 pin 13, 120 Ω | NEC, 38 kHz |
| 3 | NFJ&FX PW-6 #2 | 74HC4051 pin 14, 120 Ω | NEC, 38 kHz |
| 4 | Luxman AS-50R | 74HC4051 pin 15, 120 Ω | NEC, 38 kHz |
| 5 | Douk VFD253-PRO | 74HC4051 pin 1, 120 Ω | NEC, 38 kHz |
| 6 | BlueRigger | 74HC4051 pin 2, 120 Ω | NEC, 38 kHz |
| 9 | Fosi ZP3 | GPIO19, 100 Ω | raw, 38 kHz |
| 10 | Arcam AVR20 | GPIO21, 100 Ω | raw, 36 kHz |
| 11 | Yamaha R-N800A | GPIO22, 100 Ω | raw, 38 kHz |
| 12 | miniDSP Flex | GPIO23, 100 Ω | raw, 38 kHz |
| 13–15 | future remote boxes | from GPIO13, 18, 4, 16, 17 |
- 74HC4051 mux: carrier GPIO32 → pin 3; select inputs GPIO33, 25 and 26 → pins 11, 10 and 9; pins 6, 7 and 8 to ground; pin 16 to the ESP32's 3.3 V. Jacks 1–4 take output pins 12–15 and jacks 5–6 take pins 1 and 2; pins 4 and 5 are unused. Only the eight output pins (1, 2, 4, 5, 12–15) are interchangeable; the firmware holds the jack-to-output table. A second mux later shares pins 3 and 9–11 and gets its own pin 6 from the ESP32.
- Jacks number from 1, and the link frames carry the jack number: LC30 1, PW-6 2 and 3, AS-50R 4, Douk 5, BlueRigger 6. The selector's
fire_ircalls change to match. - RS-485 in on GPIO34 (input only), through the MAX3485 + 6N137 stage below.
- Captured codes: Luxman AS-50R in
esphome/docs/luxman-ir-codes.md(NEC0x7682), Douk VFD253-PRO inesphome/docs/douk-ir-codes.md(NEC0xBBAA). The Douk has only an input-cycle command (0x0D); it took 20 in a row at 75 ms spacing without dropping one, and keeps its input through power loss. Neither address collides with anything else on the link. - Display: the SSD1322 ships set for 8-bit parallel, so an Arduino Nano Every drives it (see the display section below). The ESP32 sends it text on GPIO27 (UART TX), through a spare TXS0108E channel, and needs no display pins of its own.
- Free: GPIO13, 18, 4, 16 and 17 (jacks 13–15 and a second mux's /E, later), and the input-only GPIO35. GPIO0, 2, 5, 12 and 15 are boot strapping pins and GPIO14 pulses at boot, so they stay unused; VP and VN pick up glitches while Wi-Fi runs; GPIO6–11 belong to the flash.
- The receiver's ground stays separate from the side table's: its MAX3485 runs on the sender's power and ground, and the data crosses to the ESP32 through a 6N137 optocoupler. The brief's "no galvanic path between the side table and the racks" stays true even if the receiver later gets a wired connection at the rack.
RS-485 input: MAX3485 + 6N137
| Pin | Connects to | Side |
|---|---|---|
| Module VCC / GND (5-pin row) | Trunk pin 3 (+3.3 V) / trunk 7–8 | sender's |
| Module A / B | Trunk pins 1 / 2 | sender's |
| Module EN | The module's GND in the A/B row (receive only) | sender's |
| Module TXD (RO) | 6N137 pin 3 (LED cathode) | sender's |
| Module RXD (DI) | nothing | sender's |
| 6N137 pin 2 (LED anode) | 220 Ω to trunk pin 3 | sender's |
| 6N137 pins 8 (VCC) and 7 (enable) | Receiver's 5V rail; 0.1 µF from pin 8 to pin 5 | receiver's |
| 6N137 pin 5 | Receiver's GND | receiver's |
| 6N137 pin 6 (output) | GPIO34, with 1 kΩ pull-up to the ESP32's 3.3 V | receiver's |
- The modules (MAX3485ESA, marked "RS485 V2.05") have one EN pin in place of DE and /RE: low receives, high transmits. They name TXD and RXD from their own side: a continuity beep found TXD on the chip's pin 1 (RO).
- The serial line idles high: MAX3485 output high, LED off, 6N137 output pulled high. A low bit lights the LED and pulls GPIO34 low, so the polarity comes out unchanged.
- 220 Ω gives the LED ≈6–7 mA; the 6N137 needs up to 5 mA to switch. It switches in well under 1 µs against 8.7 µs bits at 115200 baud. The PC817s used in the sender are too slow for this.
- The receiver's MAX3485 and LED draw ≈8 mA over the trunk: ≈0.05 V drop on 32 AWG plus up to ≈0.16 V across the fuse, leaving ≈3.1 V, inside the MAX3485's 3.0–3.6 V range. The RXEF005 (50 mA hold, ≈100 mA trip) at the sender opens if the trunk is shorted, so a crushed cable can't pull down the sender's 3.3 V rail; it resets once the short is gone.
Display: Nano Every + SSD1322 in parallel mode
The SSD1322 boards shipped with R6 + R8 fitted, which is 8080 parallel; ESPHome only drives the SSD1322 over SPI, so a Nano Every drives the panel. The receiver's display is the sender's left panel without its status lines (VOL and the number centred in the right column): the sender passes the selector's D frames down the trunk, the receiver's ESP32 draws them with its own copy of the sender's panel code (office-ir-receiver-panel.h; the sender's is office-ir-left-panel.h), and its nano_mirror display sends each finished 256 × 64 frame to the Nano at 500 kbaud (A5 'F', 2048 bytes in U8g2's buffer order, CRC-8), on every change and again every 2 s. The Nano only copies the frame into U8g2's buffer and puts it on the panel, writing the bus through its ports directly. Layout changes therefore go out over Wi-Fi to the sender and receiver and reach both panels together; the Nano needs reflashing only if the frame format changes. Two TXS0108E modules shift the Nano's 5 V lines to the display's 3.3 V.
| From | Through | To |
|---|---|---|
| ESP32 GPIO27 (UART TX) | TXS0108E #2, A4 → B4: at 5 V the Nano's chip needs 3.5 V to read a high | Nano RX (D0, Serial1) |
| Nano D9, D8 … D2 | TXS0108E #1, B1–B8 → A1–A8 | SSD1322 pins 4, 6, 8, 10, 5, 7, 9, 11 in that order, so D0–D7 come from Nano D9, D5, D8, D4, D7, D3, D6, D2 |
| Nano A5 | TXS0108E #2, B1 → A1 | SSD1322 WR (pin 13) |
| Nano A6 | TXS0108E #2, B2 → A2 | SSD1322 DC (pin 14) |
| Nano A7 | TXS0108E #2, B3 → A3 | SSD1322 RES (pin 15) |
| ESP32 3.3 V | — | SSD1322 RD (pin 12, held high), VCC (pin 2); both TXS VA and OE |
| GND | — | SSD1322 CS (pin 16, held low: the only device) and pin 1; TXS GND; Nano GND; ESP32 GND |
| Panel USB-B: 5 V, GND, D+, D− | a cut micro-USB cable, spliced on the breadboard | Nano USB (power and data) |
| Nano 5V pin | — | both TXS VB, so the B side matches the Nano's logic level |
- Power the SSD1322 from the ESP32 board's 3.3 V regulator; the Nano's 3V3 pin supplies too little for an OLED.
- The ESP32 sends the same kind of
Dframes the selector sends the sender: display line, then text. - This board (marked "3.12″ OLED Ver 2.1", set for 80XX by R6 and R8) uses ZJY's header layout: 1 GND, 2 VCC, 3 NC, 4–11 D0–D7, 12 RD, 13 WR, 14 DC, 15 RES, 16 CS. Checked 2026-10-14 by beeping each header pin to the panel's 30-pin ribbon connector: D0–D7 on ribbon 13–6, RD 14, WR 15, DC 18, CS 19, RES 20, ground on 2 and 28. Seen from the back with the header at the left, the odd pins are the right-hand row, 1 at the bottom.
- Moving R6 to R5 later (4-wire SPI) retires the Nano and the two shifters: the display then runs from the ESP32 with ESPHome's
ssd1322_spi.
Inside the 1455U2201, top view to scale
Inside sizes from Hammond's 1455U drawing (the extrusion is made for a 186 mm wide circuit board). Module and breakout sizes are typical; the layout gets redrawn once the parts are in hand.
Receiver end panels, to scale
The end panels sit inside the bezel edge to edge at 191 × 66 mm, so they can't be made smaller, and at that size a panel needs 181.7 mm square at 45°, more than the A1 Mini's 180 mm bed. Each is printed in two halves that butt together down the centre (each half reaches 0.75 mm past the centre line, which closed a 2 mm gap the first print left in the bezel), held in place by the bezel (and on the front by the display board, which spans the seam); both halves print flat together (≈95 × 142 mm). Corner holes Ø4.3 at 181.5 × 58.02 mm (Hammond: Ø4.19 through); on the front they are drawn 0.5 mm in from each side, after a fit check. Both are 2 mm against Hammond's 1.63 mm, so the stock corner screws still reach. Corners are rounded R4.5 to match the bezel. Both must be printed plastic: each jack's threaded bushing is its sleeve contact, so a metal panel would short every sleeve together and tie the ZP3 trigger's ground to the receiver's. Front: the screen window (78.5 × 22 mm, the edge of the black glass) is centred, with a 1 mm bevel on the outside; the SSD1322 sits on four Ø4 × 3 mm posts on the inside (95 × 29 mm apart, M2.5 × 5 button heads), its board 0.25 mm right of and 1.75 mm below the window's centre, so the glass rests against the panel. Rear, seen from behind: jacks (PJ-30920-D, Ø6.4) on 15 mm centres from 13 mm, rows 26 mm apart, so the seam falls midway between jacks 6/7 and 14/15; the ZP3 trigger continues the top row at the same pitch. The RJ45 cutout is 18 mm wide from 5.5 to 22 mm above the bottom edge: the jack's top measured 20 mm, plus room for another layer of foam tape. USB-B is the rectangular-cutout type used on the sender and selector. OpenSCAD: ryans-office/ir-link/panels/receiver_front_panel.scad and receiver_rear_panel.scad; the SSD1322 measurements are from 2026-10-11.
Rear panel construction
The rear panel is 2 mm PETG, flat: under the jacks' 4.5 mm M6 thread with room for the nut, so no pockets are needed. The two halves butt together at the seam, between jacks 6/7 and 14/15, and the bezel holds them in place; a tongue and groove was tried and dropped, because the tongue printed stringy and didn't seat.
- Print in PETG. PLA softens around 55–60 °C and creeps under steady load, and the air behind a rack of amplifiers runs warm; PETG holds to about 75–80 °C and is less brittle when a cable is yanked.
- Tie the cables to the rack with a little slack, so the panel isn't carrying their weight or the pull from plugging and unplugging.
Link frames
Binary, 115200 baud 8N1 on both links. The sender forwards the selector's frames unchanged. Defined once in a shared header, office-ir-link.h, with the port table as one struct array.
| Byte | Field |
|---|---|
| 0 | Sync, 0xA5 |
| 1 | Type: N (NEC), R (raw), or one of the display frames below (D, Q, W, S) |
| 2 | Receiver jack, 1–15 |
| N | Address (2 bytes), command (2 bytes), high byte first: ESPHome's 16-bit values, e.g. LC30 Amp A is FF 00 FD 02 |
| R | Carrier in kHz (1 byte), count (1 byte), then count × 2-byte durations in µs, alternating mark and space from a mark |
| D | Byte 2 is the field: 0 preamp, 1 amp, 2 rate/format, 3 volume, 4 channel number, 8 the MiniDSP Flex's volume in dB and 9 the speaker pair's number (the left panel; under the volume, "sub: -5.0" centred (the number monospaced), then "ch: 4" left and "sp: 1" right along the bottom), 5 speaker pair name, 6 BlueRigger input and 7 the switchers' positions (the right display). The selector sends them on every change, when the sender asks, and every 30 s; then text length (1 byte) and the text (UTF-8). The sender draws its panels and passes the frame on to the receiver, which draws its version of the left panel; it also re-sends the left panel's fields down the trunk every 10 s |
| Q | Sender → selector on the patch's return pair: A5 'Q' and the CRC, sent at startup and every 5 s until text arrives. The selector answers with every D field |
| W | Selector → sender: A5 'W' and the CRC, on a gear or OPT change and on the active path's knob. The sender turns the displays on and restarts their 5 min timeout |
| S | Sender → receiver: A5 'S', 1 (on) or 0 (off), and the CRC, when the sender's displays go on or off and every 10 s. The receiver's display follows |
| last | CRC-8 (polynomial 0x07, initial 0) over bytes 1 to n. An N frame is 8 bytes: A5 4E 01 FF 00 FD 02 8F |
A selector NEC frame is 8 bytes and takes under 1 ms. A relayed raw NEC frame is about 140 bytes, about 12 ms, and goes out once the IR frame ends and 10 ms of silence confirms it. The rack emitter starts roughly 90 ms later than a directly wired emitter would.
Amp selector
Changes
- Done 2026-10-11: the IR transmitter (GPIO38), the 74HC4051 and its select lines (GPIO39/40/41), the five jacks and the OLED are out. The mux chip and 120 Ω resistors move to the receiver.
- RJ45 screw-terminal breakout for the sender link, in the rear left corner like the sender's: serial out on 1 (terminal 1), in on 2 (terminal 3, pulled up), ground on terminals 2 and 6. 38 stays unused: on this board it drives the onboard RGB LED.
- Every cable enters at the rear panel; the front carries the four channel buttons, the input rotary and the four speaker-pair buttons. The selector's OLED goes: its preamp, amp and rate/format move to the sender's left display and its channel and BlueRigger input to the right one, sent over the serial link. Rear panel: the five IR jacks go and the sender-link RJ45 takes their space; USB-B (174.15 mm) and the antenna (204.7 mm) stay; the legacy Ø22.5 mm RJ45 coupler, reserved for a signal-rate detector that was never built, comes out.
fire_ir(ch, addr, cmd)keeps its signature andmode: queued; its body writes anNframe for portch.select_geardoesn't change; the BlueRigger rotary handlers change only their port, 6 → 5.- Channel renumbering, done 2026-10-15: CH 3 is Eversolo DMP-A6 → Classé CA-100 and CH 4 is Arcam AVR20 → HK Signature 1.5 (CH 1's preamp reads Fosi ZD3 + ZP3). The Arcam takes the BlueRigger chain on its CD input (digital coax from the INDAY splitter), the 2.1 music input; the 4K Blu-ray player on BD (HDMI); and the TV over eARC on its Display input. The Douk's inputs follow the channel numbers (Eversolo RCA on input 3, Arcam sub out on input 4), and PW-6 #2's codes stay as they were: its A1 now carries CH 3 and A2 CH 4. The MiniDSP Flex is no longer a channel; the selector sends it preset N on every change to channel N (jack 12). CH 3's power and volume control is still to be designed, so Home Assistant routes nothing for it yet.
- Pushing the firmware change auto-flashes the selector and ends its IR output, so it's committed with
[skip deploy]and deployed in the same session as the hardware change. - Power, 2026-10-16: a USB-C cable from the brick runs through the rear panel's USB-B hole straight into the S3's COM port (the right-hand port, with the ports at the bottom). On the panel USB-B and its pigtail to the breadboard's 5 V rail, every new firmware image reset with a brownout on its first boot and rolled back.
- Eversolo volume, set by the selector through the Eversolo's API: on a change to CH 3, from any source, the Eversolo goes to the saved listening volume before anything else switches (−40 dB until one is saved). Any volume change while on CH 3 is saved as the listening volume. With the Eversolo as the source on CH 1, 2 or 4, it goes to 0 dB: 2 s after a channel change, so full volume never reaches the Classé, or at once after an input change. The Eversolo remote's knob works only on CH 3.
Speaker-pair buttons and front panel layout
- Four FILN LED buttons select the speaker pair, one button per pair: 1 = LC30 speaker 1 + AS-50R A; 2 = LC30 speaker 1 + AS-50R B; 3 = LC30 speaker 1 + AS-50R C; 4 = LC30 speaker 2. A press sends the pair's full state to the Luxman AS-50R (jack 4) and the Fosi LC30 (jack 1). The AS-50R codes are in
esphome/docs/luxman-ir-codes.md(NEC0x7682: power, A, B, C). - The selector holds the speaker-pair state and lights the current pair's LED, whichever surface changed it: a button, Home Assistant or voice. Pressing the lit button sends nothing; pressing any other sends its full state, so a press also re-syncs the gear.
- After a change the selector ignores speaker presses for about a second, so a run of presses makes one change and the speakers aren't switched in and out rapidly.
- The input selector stays a C&K A10405RNCQ rotary with three stops.
- Front panel, left to right, all on the 17.25 mm centreline: channel buttons at 16, 36, 56 and 76 mm (20 mm spacing, as today; the first clears the left corner post), the input rotary at 109 (the panel's centre), and the speaker-pair buttons at 142, 162, 182 and 202, mirroring the channel buttons.
- Behind the panel the rotary's Ø26.19 housing spans 95.9–122.1 mm, ≈10.6 mm clear of the nearest button nuts (ending at 85.25 and starting at 132.75). The button at 202 sits as far from the right corner post as the one at 16 does from the left.
- Pins, by board label: channel buttons 4–7 with LEDs 15–18 (unchanged); input rotary 8, 9, 10; speaker-pair switches 42, 41, 40, 39 with LEDs 45, 48, 47, 21 through 470 Ω. 45 is a strapping pin; its LED and resistor hold it low at reset. LC30 speaker 1/2 codes are in
esphome/docs/fosi-ir-codes.md(0xFF00:0xFC03/0xFB04). - The Douk follows the channel: on each change the selector steps it from its stored input (kept through reboots) to the channel's input with input-cycle commands 100 ms apart. The sender's Douk button corrects drift without changing that count: a short press sends one input step on release. Held 2 s, it instead cycles the brightness of both NFJ&FX PW-6 displays with the Fosi LC30's Amp A code (address
0xFF00, command0xFD02) on jacks 2 and 3; the PW-6 remote has no dimmer, and the LC30's emitter is taped so its Amp A doesn't reach the PW-6s. - The Douk advance button moves to the sender's front panel. The channel buttons work as today, except that a short press on the lit button no longer toggles a display; the 1 s hold still re-sends the path's commands.
- Knob: Kaish Ø18 mm, ¼″ bore with an M4 set screw on the shaft flat. Its Ø15 × 3 mm recess can hide the panel nut if it measures under 15 mm across corners.
Inside the selector, top view to scale
Parts Express 320-482, 224.8 × 39.9 × 165.1 mm outside; usable inside 197 × 27 × 149 mm per Parts Express. Behind the front panel: button nuts (Ø18.5 across corners) and the Ø26.19 rotary housing, which just fits the 27 mm height as today's input rotary does. Corner screw posts drawn at approximate positions; Parts Express's usable 197 mm is the width between them.
Selector end panels, to scale
Flat 218 × 34.5 × 1.5 mm plates (hammond-enclosure-panels/plastic-white-black-enclosure/pla/). Front: Ø12.2 button holes at 16, 36, 56, 76 (channels) and 142, 162, 182, 202 mm (speaker pairs) with their Ø14 bezels dashed; a Ø9.80 rotary hole at 109 mm with the Ø18 Kaish knob dashed; no display. Rear: the five IR jacks and the legacy Ø22.5 RJ45 coupler are gone; the sender-link RJ45 takes the jacks' space; USB-B and antenna stay where the current panel has them. The enclosure is a Parts Express 320-482.
Build and cutover order
- Prove link mode on the bench. Flash one remote box with the mode firmware, switch it to link mode, and capture through a PC817 with
remote_receiverdump: raw. Pulse widths within about 10% of the box's own. Include the Arcam box's RC-5 frames. - Flash the mode firmware to all four boxes. They start in emitter mode, so nothing changes yet.
- Build the receiver and sender, flash over USB, test on the bench with spare emitters. Sender and selector done 2026-10-11: both flashed, the patch carries frames both ways with valid checksums for every channel, speaker-pair and rotary command, and the sender's inputs capture clean NEC envelopes. Receiver built 2026-10-14 (192.168.4.48, over Wi-Fi since the first USB flash): every jack (1–6, 9–12) fires its test frame, checked with an IR learner, and the display shows the ESP32's status rows. Link tested end to end on the bench the same day: the sender passes the selector's checked frames on to the trunk (77 relayed from a round of selector presses, plus its own test and Douk frames), and the receiver logged every one with no bad frames. Installed 2026-10-14: the receiver is at the racks with every emitter moved over, the selector's channel and speaker-pair buttons reach all six of its targets, and the four remote boxes run in link mode through the sender, which relays their timing as R frames to jacks 9–12. Still to test: the ZP3 box's volume knob.
- Run the trunk and the optical cable to the racks.
- Move one remote box at a time: switch it to link mode, plug it into its sender input, and move its emitter to its receiver port. Reverting a box is switching it back to emitter mode and plugging an emitter cable into its jack.
- Selector cutover in one session: rebuild its IR section, deploy the firmware, move the five infra emitters.
- Move the ZP3 trigger onto trunk pair 1.
- Update the cable map, brief and room notes.
Verification
- Trunk continuity on all 8 conductors. Trigger pair above 10 MΩ to every other conductor and to both boxes' grounds.
- Mode switch on each box: the 5 s hold switches mode without sending power, the blinks match, and the mode survives a power cycle.
- Every jack 20 times from cold, 100% response.
- Arcam: standby toggle 10 times, and two identical presses in a row both register (the RC-5 toggle bit survives the relay).
- PW-6 pair: fire #1's A1 and #2 doesn't move; then the reverse.
- Press a gear button while turning the ZP3 knob. Every command lands.
- Turn the desk box knob fast on each path. Every step lands, with no oversized-frame errors in the sender's log.
- Fire each remote box and confirm only its own sender input relays (checks the wiring).
- Power-cycle the ZP3:
binary_sensor.fosi_zp3_remote_12v_triggerfollows both edges with no chatter. - Hum check on every audio path against today.
- Revert test: one remote box switched to emitter mode on an emitter cable works with no other change.
Later, separately
- The selector calls the Eversolo's local HTTP API itself for path switches.
Parts list
Jumper wires assumed on hand.
| # | Item | Qty | For | Notes | Have? |
|---|---|---|---|---|---|
| 1 | ESP32-WROOM-32U DevKitC V4, 38-pin, with antenna | 2 | receiver, sender | Amazon or Elecbee boards | have |
| 2 | 38-pin DevKitC screw-terminal expansion board | 2 | not used: both ESP32s straddle two breadboards | ordered | ordered |
| 3 | Full-size breadboard | 1 | receiver | have | |
| 4 | Half-size breadboard | 2 | sender, side by side under the ESP32 | have | |
| 5 | Hammond 1455U2201 enclosure | 1 | receiver | end panels 3D-printed | have |
| 6 | Parts Express 320-482 desktop case | 1 | sender | the second one on hand; same case as the selector, no longer sold | have |
| 7 | 74HC4051, DIP-16 | 1 | receiver | have | |
| 8 | 120 Ω ¼ W resistor | 6 | receiver jacks 1–6 | or reuse the selector's five | have |
| 9 | 100 Ω ¼ W resistor | 4 | receiver jacks 9–12 | have | |
| 10 | PC817 optocoupler, DIP-4 | 4 + 4 spare | sender inputs | have | |
| 11 | 150 Ω ¼ W resistor | 4 | sender inputs, LED side | have | |
| 12 | 4.7 kΩ ¼ W resistor | 4 | sender input pull-ups | plus a few 1 kΩ in case faster switching is needed | have |
| 13 | 3.5 mm panel jack with leads | 18 | receiver 12 + trigger; sender 4 + trigger | Lsgoodcare PJ-30920-D (M6 × 0.5), on hand. Stereo: a mono plug uses tip and sleeve, so the ring lead stays unconnected; confirm which lead is tip with a meter | have |
| 14 | 3.5 mm mono male–male cable, 1–2 ft | 6 | 4 boxes → sender; ZP3 trigger at both ends | have | |
| 15 | IR emitter cable, 3.5 mm mono, 1.5–3 m | 10 | receiver → gear | have | |
| 16 | 6N137 optocoupler, DIP-8 | 1 + spares | receiver RS-485 input | keeps the receiver isolated; PC817s are too slow for 115200 baud | have |
| 17 | 220 Ω and 1 kΩ resistors, 0.1 µF capacitor | 1 each | receiver 6N137 stage | 0.1 µF: ceramic marked "104", 50 V X7R, 2.54 mm leads | have |
| 18 | Littelfuse RXEF005 resettable fuse (PTC) | 1 + 1 spare | sender, in series with the +3.3 V feed to trunk pin 3 | 50 mA hold, radial leads | have |
| 19 | C&K A10405RNCQ rotary switch | 3 | desk box's planned second switch; spares | same part as the existing input rotary | ordered |
| 20 | Kaish Ø18 mm knob, ¼″ set screw | 1+ | amp selector input rotary | Ø15 × 3 mm recess under the knob | ordered |
| 21 | SH1122 2.08″ 256 × 64 OLED (SPI) | 2 | sender front panel | the selector's own OLED is dropped | have |
| 22 | FILN 12 mm LED button | 6 | selector: speaker-pair buttons (4); sender: screens and Douk (2) | same as the four channel buttons; green or blue LEDs; 470 Ω each | have |
| 23 | TTL ↔ RS-485 module, 3.3 V (MAX3485) | 2 | sender, receiver | MAX3485ESA module marked "RS485 V2.05": EN in place of DE and /RE (high at the sender, low at the receiver); TXD is RO and RXD is DI on this board. Not MAX485: its 5 V output isn't safe for the ESP32 | have |
| 24 | RJ45 8-pin screw-terminal breakout | 4 | receiver 1, sender 2, selector 1 | board-mounted RJ45 jack with 8 screw terminals and a 2.54 mm header row; the jack pokes through the panel, so no coupler or internal patch | have (8) |
| 25 | Ultra-slim Cat6 UTP patch cable, 25 ft | 1 + 1 spare | trunk | 2.8 mm OD, 32 AWG stranded, 4 twisted pairs, T568B straight through | ordered |
| 26 | Cat6 patch, 1–3 ft | 1 | selector → sender | have | |
| 27 | USB-B panel pigtail | 2 | receiver, sender | have | |
| 28 | USB power brick, 5 V, ≥1 A | 2 | receiver, sender | have | |
| 29 | SMA bulkhead pigtail (U.FL) + antenna | 2 | receiver, sender | often included with the 32U | have |
| 30 | M3 nylon standoffs and screws | kit | expansion boards | or double-sided tape on a printed base plate | have |
| 31 | 3D-printed end panels | 4 | receiver 2, sender 2 | PETG; receiver rear panel ribbed (see Rear panel construction) | have (PETG) |
| 32 | SSD1322 3.12″ 256 × 64 OLED | 1 | receiver front panel | shipped set for 8-bit parallel; driven by the Nano Every | have |
| 33 | Arduino Nano Every | 1 | receiver display driver | U8g2, 8080 parallel | have |
| 34 | TXS0108E 8-channel level shifter module, headers fitted | 2 (pack of 3) | receiver: Nano 5 V → display 3.3 V | HW-221 board | have (3) |
| 35 | M2.5 × 5 button-head screws | 4 | SSD1322 onto the receiver front panel's posts | 1.6 board + ≈3.4 into the post; 6 mm bottoms out | have |