{"id":550,"date":"2026-09-26T21:21:29","date_gmt":"2026-09-26T20:21:29","guid":{"rendered":"https:\/\/www.fabidouille.com\/?p=550"},"modified":"2026-09-26T21:21:29","modified_gmt":"2026-09-26T20:21:29","slug":"pioneer-avr","status":"publish","type":"post","link":"https:\/\/www.fabidouille.com\/?p=550","title":{"rendered":"Le Port Est Mort, Vive le Port"},"content":{"rendered":"<p>Ask the internet what the Ethernet port on a 2009 Pioneer AV receiver is for, and you get a confident, unanimous answer: internet radio and firmware updates. The manual says it. Forums repeat it. Years of received wisdom, all in agreement, and none of it tested. I own a Pioneer SC-LX73 (around 2009), and I got tired of walking across the room to power it on for movie night. So I plugged a cable into that \u00ab\u00a0dead\u00a0\u00bb port and actually looked at the wire.<\/p>\n<p>What I found was not a radio tuner. It was a full RS-232 control surface, complete with a web server, a telnet port, and zero authentication, quietly waiting for someone to notice. My receiver now powers on, selects the right input, and takes its volume orders from Kodi, all by itself. This is how a dead port became the hardest-working connector in the living room.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/09\/front.jpg\" alt=\"Front of the Pioneer SC-LX73 AV receiver: input selector and master volume knobs either side of the display, a fine layer of dust on top\"\/><figcaption>The receiver, a Pioneer SC-LX73 from around 2009, with the dust to prove it.<\/figcaption><\/figure>\n<h2 class=\"wp-block-heading\">A dead port that was just thirsty<\/h2>\n<p>The naive approach first: I ran a plain patch cable from the LibreELEC Pi that runs my Kodi setup straight into the AVR&rsquo;s RJ45. The link light came up, nothing else happened, and the receiver looked exactly as dead as the forums promised. Case closed, apparently.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/09\/lan-port.jpg\" alt=\"Rear panel of the receiver: a white Ethernet cable plugged into the LAN (10\/100) port, between the optical inputs and the adapter port\"\/><figcaption>The &lsquo;dead&rsquo; port: LAN (10\/100), squeezed between the optical inputs and the adapter port.<\/figcaption><\/figure>\n<p>Except LibreELEC has no tcpdump, and I am stubborn. A tiny Python sniffer built on AF_PACKET sockets showed what was really happening on that cable:<\/p>\n<pre class=\"wp-block-code\"><code>[ 23.4s] [the AVR]  UDP 68-&gt;67 [DHCP DISCOVER]\n[ 26.1s] [the AVR]  ARP op1\n[ 31.0s] [the AVR]  UDP 68-&gt;67 [DHCP DISCOVER]   &lt;- every ~18s<\/code><\/pre>\n<p>The other end was alive and shouting into the void. A MAC OUI lookup on 00:e0:36 came back as Pioneer Corporation, so this really was the AVR, and it was stuck in a DHCP DISCOVER loop. Of course it was: a direct cable has no DHCP server, so the poor thing never got an address and had nothing to say on IP at all. \u00ab\u00a0Unreachable\u00a0\u00bb was really \u00ab\u00a0unaddressed\u00a0\u00bb. The port was not dead. It was just thirsty.<\/p>\n<h2 class=\"wp-block-heading\">Becoming the DHCP server<\/h2>\n<p>The fix was to give it something to drink. I put a static address on the Pi&rsquo;s eth0 and wrote a small DHCP server in Python, about sixty lines, MAC-whitelisted to the Pioneer OUI so it could never hand a lease to anything else and poison the home LAN. On the next DISCOVER cycle:<\/p>\n<pre class=\"wp-block-code\"><code>DISCOVER from the AVR -&gt; OFFER\nREQUEST  from the AVR -&gt; ACK<\/code><\/pre>\n<p>The AVR took its address and went quiet, like a machine that had been waiting its whole life for someone to answer. First real finding, worth repeating: the port was never dead, it was waiting for DHCP. All that forum wisdom, undone by a missing lease.<\/p>\n<h2 class=\"wp-block-heading\">Port scan: greetings from 2009<\/h2>\n<p>With the AVR addressable, a quick TCP scan turned a curiosity into a project:<\/p>\n<pre class=\"wp-block-code\"><code>OPEN  23\/tcp     &lt;- telnet?!\nOPEN  80\/tcp     &lt;- web server?!\nOPEN  8102\/tcp   &lt;- unknown<\/code><\/pre>\n<p>Telnet. On a 2009 amplifier. I laughed out loud. The HTTP port answered with <code>Server: Nu-OS\/1.13<\/code> and served a page titled \u00ab\u00a0Pioneer Web Control System\u00a0\u00bb, a complete HTML remote-control UI for the whole receiver. The third port, 8102\/tcp, stayed silent to plain HTTP and remains unidentified, most likely the channel for the official app. I never needed it.<\/p>\n<h2 class=\"wp-block-heading\">The web UI is an RS-232 relay<\/h2>\n<p>The real unlock came from reading the UI&rsquo;s own JavaScript, <code>js\/common.js<\/code>, right off the receiver. Every button in that pretty web page builds a request like this:<\/p>\n<pre class=\"wp-block-code\"><code>a = \"EventHandler.asp?WebToHostItem=\";\nb = encodeURIComponent(query);           \/\/ \"BPO\", \"VU\", \"19ZT\"\nreq.open(\"get\", c, true);                \/\/ GET \/EventHandler.asp?WebToHostItem=BPO<\/code><\/pre>\n<p>and the page polls its state from <code>GET \/StatusHandler.asp<\/code>, which returns JSON. There is even a function honestly named <code>webCon.http.sndRs232cEvent(query)<\/code>. So every button in the web UI is simply an RS-232 command passed over HTTP. The whole command vocabulary fell straight out of the JavaScript:<\/p>\n<ul class=\"wp-block-list\">\n<li>Power: <code>BPO<\/code> on, <code>BPF<\/code> off<\/li>\n<li>Volume: <code>VU<\/code> up, <code>VD<\/code> down (main zone)<\/li>\n<li>Mute: <code>MO<\/code> mute, <code>MF<\/code> unmute<\/li>\n<li>Input select: <code>&lt;nn&gt;ZT<\/code>, where <code>&lt;nn&gt;<\/code> is the RS-232 input number; my theatre input is 19, so <code>19ZT<\/code><\/li>\n<li>Listening mode: <code>&lt;n&gt;SR<\/code>, for example <code>0001SR<\/code> stereo, <code>0010SR<\/code> standard surround, <code>0050SR<\/code> Home THX<\/li>\n<li>Status queries: <code>?AST<\/code>, <code>?RGC<\/code>, and friends<\/li>\n<\/ul>\n<p>This also explained the telnet port: 23\/tcp is the same RS-232 bus, exposed raw. <code>printf '?AST\\r' | nc &lt;avr&gt; 23<\/code> answered <code>AST0302\u2026<\/code>, and <code>?M<\/code> answered <code>MUT1<\/code>. No handshake, no session, CR-terminated ASCII, like it is still 2009. Because it is.<\/p>\n<h2 class=\"wp-block-heading\">Proof, not vibes<\/h2>\n<p>Reading state is easy; the entire point was control. For a first write test, I wanted something reversible, because I enjoy my amplifier and would like to keep it. Through the HTTP relay:<\/p>\n<pre class=\"wp-block-code\"><code>V=104  -&gt;  VD x3  -&gt;  V=101  -&gt;  VU x3  -&gt;  V=104<\/code><\/pre>\n<p>Each step was read back through <code>\/StatusHandler.asp<\/code>, and the volume round-tripped exactly. Power, input, and mute ride the same path, so control was proven rather than assumed. Rule of thumb: the first write to unfamiliar hardware should be one you can undo.<\/p>\n<h2 class=\"wp-block-heading\">Wiring it into Kodi<\/h2>\n<p>Reverse engineering only matters if it ships. The physical chain in the living room:<\/p>\n<pre class=\"wp-block-code\"><code>[LibreELEC Pi]  HDMI --&gt; [Pioneer SC-LX73] -- HDMI Out 2 --&gt; [projector]\n      |  eth0 == RJ45 direct cable (point-to-point)\n      +-- wlan0 = home LAN (SSH)<\/code><\/pre>\n<p>The Pi reaches the house over wifi. Its eth0 is dedicated to the AVR on an isolated point-to-point cable: no switch, not bridged onto the LAN.<\/p>\n<p>Three small pieces of software make it livable. A <code>pioneer-avr<\/code> systemd unit keeps a static address on eth0 and runs the MAC-whitelisted DHCP server. An <code>avr-ctl<\/code> wrapper exposes status, on, off, volup, voldown, mute, unmute, input, plus raw <code>cmd<\/code> and <code>rawquery<\/code> for anything exotic, using HTTP for writes and telnet for raw queries. And <code>kodi-avr-bridge.py<\/code> polls Kodi&rsquo;s JSON-RPC every half second and forwards intent to the receiver.<\/p>\n<p>The bridge is where the quality of life lives. A Kodi volume change becomes <code>VU<\/code>\/<code>VD<\/code> on the AVR, after which the bridge re-pins Kodi&rsquo;s volume to 100, so the amplifier is the one true volume knob and Kodi&rsquo;s internal volume stops mattering (it was inert for HDMI passthrough anyway). A Kodi mute toggle becomes <code>MO<\/code>\/<code>MF<\/code>. And when playback starts, the bridge sends <code>BPO<\/code> plus input 19, so the amp powers on and selects the theatre input by itself. Verified live: dragging Kodi from 100 to 90 drove the AVR from 104 down to 101, then Kodi snapped back to 100. Movie night now starts with one button.<\/p>\n<p>One honest confession: I lost a fight with connman, LibreELEC&rsquo;s network manager. It insists on managing eth0 as a normal \u00ab\u00a0Wired\u00a0\u00bb connection, adds its own link-local address, runs its own DHCP client, and sometimes the AVR gives up on my server and self-assigns a link-local address instead. The good news is that control works fine over link-local too, because the Nu-OS web server binds 0.0.0.0. So the practical rule is simple: never depend on a fixed AVR address. <code>avr-ctl<\/code> self-discovers whichever address the receiver currently holds, trying the known candidates and then the neighbour table. <code>connmanctl disable ethernet<\/code> turned out to be too heavy (it powers the NIC down entirely), so I let connman sulk and simply routed around it.<\/p>\n<p>Jeedom, my home-automation box, drives the amplifier as well. Since the AVR is deliberately off-LAN, a thin <code>avr_control.sh<\/code> on the Jeedom Pi SSHes to the Kodi box and runs <code>avr-ctl<\/code> there. That gives me scenarios: power the amp off when the house switches to absence mode, alert me if it is still on at odd hours. The AVR is slow, so fast scenarios get a small sleep after power and input commands.<\/p>\n<p>For the curious, the raw interface is three curl calls and a netcat:<\/p>\n<pre class=\"wp-block-code\"><code>curl -s http:\/\/&lt;avr&gt;\/StatusHandler.asp                        # full JSON status\ncurl -s \"http:\/\/&lt;avr&gt;\/EventHandler.asp?WebToHostItem=BPO\"     # power on\ncurl -s \"http:\/\/&lt;avr&gt;\/EventHandler.asp?WebToHostItem=19ZT\"    # theatre input\nprintf '?AST\\r' | nc -w 3 &lt;avr&gt; 23                            # raw RS-232 query<\/code><\/pre>\n<h2 class=\"wp-block-heading\">Honest limits<\/h2>\n<p>Some cold water, because this blog tries to tell the truth. No custom firmware exists for this receiver: it runs a closed, signed RTOS and has no modding scene. It does not need one, but if you dreamed of flashing your way to glory, dream otherwise. Pioneer&rsquo;s official iControlAV apps never supported the SC-LX73 either (2011 and newer models only), which is now irrelevant, since the built-in web control is fully scriptable and needs no app at all. The internet radio side, vTuner and friends, is dead upstream, and nobody mourns it.<\/p>\n<p>The serious caveat is security: there is no authentication on this link. Anyone on the wire owns the amplifier. That is acceptable only because eth0 is an isolated Pi-to-AVR cable with nothing else on it. Never bridge it to the LAN, and never expose it to the internet without protection in front.<\/p>\n<h2 class=\"wp-block-heading\">The moral<\/h2>\n<p>Years of received wisdom said the port did one boring thing. Looking at the wire said otherwise. The tools were a patch cable, sixty lines of Python, and the receiver&rsquo;s own JavaScript. Trust the packet capture, not the forum.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Every forum says the Ethernet port on a 2009 Pioneer receiver is dead. It was only waiting for DHCP, and behind it sat telnet and a full web remote.<\/p>\n","protected":false},"author":0,"featured_media":548,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-550","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-non-classe"],"_links":{"self":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/550","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=550"}],"version-history":[{"count":0,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/550\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/media\/548"}],"wp:attachment":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=550"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=550"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=550"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}