<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Self-Hosting · haunt.house</title><link>https://blog.haunt.house/tags/self-hosting/</link><description>Research, ideas, and announcements from haunt.house.</description><language>en-us</language><lastBuildDate>Wed, 05 Aug 2026 00:00:00 -0500</lastBuildDate><atom:link href="https://blog.haunt.house/tags/self-hosting/index.xml" rel="self" type="application/rss+xml"/><item><title>Developer Mode takes your app. It gives you back the printer.</title><link>https://blog.haunt.house/developer-mode-takes-the-app/</link><pubDate>Wed, 05 Aug 2026 00:00:00 -0500</pubDate><guid>https://blog.haunt.house/developer-mode-takes-the-app/</guid><description>Putting a Bambu X1C in LAN Developer Mode costs you the phone app, the cloud history, and one-click MakerWorld. What you get in exchange is three open ports — and, if you build on them, a print dialog that any application can call.</description><content:encoded><![CDATA[<p>Turning on LAN-only + Developer Mode is presented as a settings toggle. It is
closer to a trade, and it is worth being honest about which side of it hurts.</p>
<p><strong>What you give up.</strong> Remote monitoring and control from outside your house.
Sending a plate straight from the slicer through the cloud. One-click printing
from the model library. Push notifications when a print finishes or fails. The
cloud-side print history. And — the part people underestimate — a polished
first-party app that someone else maintains, tests, and ships to your phone
for free.</p>
<p>That is a real loss. Anyone who tells you it isn&rsquo;t hasn&rsquo;t used the app.</p>
<p><strong>What you get.</strong> Three ports.</p>
<table>
  <thead>
      <tr>
          <th>Port</th>
          <th>Protocol</th>
          <th>What it carries</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>8883</td>
          <td>MQTT over TLS</td>
          <td>full status push, and every write verb</td>
      </tr>
      <tr>
          <td>322</td>
          <td>RTSPS</td>
          <td>the chamber camera</td>
      </tr>
      <tr>
          <td>990</td>
          <td>FTPS</td>
          <td>job upload to the printer&rsquo;s SD card</td>
      </tr>
  </tbody>
</table>
<p>That looks like a downgrade. It is the opposite, and the rest of this post is
the argument for why — plus the ceiling we hit, which is real and which we
have not gotten past.</p>
<h2 id="the-ports-are-not-a-consolation-prize">The ports are not a consolation prize</h2>
<p>The first thing worth establishing is that this interface is <em>complete enough
to build on</em>, because a lot of writing on the subject assumes it isn&rsquo;t.</p>
<p>We verified this against our own X1C rather than trusting the folklore. Under
LAN-only + Developer Mode, in 2026, a plain <code>paho-mqtt</code> client authenticating
as <code>bblp</code> with the access code gets: full telemetry — temperatures, progress,
<code>layer_num</code>, AMS slot contents, <code>gcode_state</code>, <code>print_error</code>, HMS codes —
plus <code>print.stop</code> / <code>pause</code> / <code>resume</code>, chamber light control, AMS filament
load and unload, and AMS-mapped print starts.</p>
<p>Two beliefs we went in with turned out to be wrong, and both were load-bearing:</p>
<ul>
<li><strong>&ldquo;Post-2025 LAN commands must be RSA-signed.&rdquo;</strong> Refuted. Request signing
applies to default LAN mode with authorization on. Developer Mode is
specifically the escape hatch, and it leaves MQTT, the live stream, and FTP
open. No signing shim is required. Our raw client works.</li>
<li><strong>&ldquo;Home Assistant lost printer control in the update.&rdquo;</strong> Refuted.
Monitoring was never affected; control needs LAN + Developer Mode, which is
the same door everything else uses.</li>
</ul>
<p>The printer&rsquo;s status pushes are <em>partial</em> — you get deltas and have to
deep-merge them into a full state document yourself, with <code>pushall</code> to request
a fresh snapshot. That is the single biggest thing that makes naive
integrations flaky, and it is why the design below starts where it does.</p>
<h2 id="this-is-the-1990s-printer-problem-and-it-already-has-an-answer">This is the 1990s printer problem, and it already has an answer</h2>
<p>Paper printers used to work exactly the way 3D printers work now. Every vendor
shipped its own application. You wanted to print from a program, that program
had to know about your specific printer. Printing was a <em>feature of an app</em>.</p>
<p>Then it inverted. IPP and CUPS turned the printer into a <strong>service</strong>: it
publishes a capability document describing what it can do, you submit a job
ticket describing what you want, and the service owns everything in between.
The immediate consequence is that printing stopped being a feature of one
program and became <strong>File → Print</strong> in all of them.</p>
<p>3D printing is pre-inversion. The vendor app is the only real client, and every
integration is a bespoke reach into someone&rsquo;s cloud.</p>
<p>So the Bambu app is not the thing you lost. The Bambu app is the thing you were
<em>stuck with</em>. Developer Mode is the door out, and what&rsquo;s on the other side is
worth more than what you handed over — but only if you build the service layer
that IPP built for paper. That&rsquo;s what <a href="https://things.haunt.house">things.haunt.house</a>
is.</p>
<h2 id="the-design-in-one-constraint">The design, in one constraint</h2>
<p>The X1C tolerates very few concurrent MQTT sessions, and the camera has a
single slot. Every naive design — each browser tab opens its own connection —
falls over immediately, and worse, does so intermittently.</p>
<p>So: <strong>one gateway owns the single MQTT connection and is the source of
truth.</strong> It holds the merged state document, applies incoming partial pushes,
and fans deltas out to clients over WebSocket. The UI is a client of the
gateway. The API is a client of the gateway. Nothing else ever speaks MQTT to
the printer. That one decision is what makes everything after it possible.</p>
<p>Three things fell out of it that the cloud app cannot do:</p>
<p><strong>A camera you can scrub by layer.</strong> The X1C&rsquo;s stream is not seekable — the
printer keeps no buffer to rewind. &ldquo;Go back and look at layer 340&rdquo; only exists
if you record it yourself. The gateway is the only component that sees both the
video and <code>layer_num</code>, so it tees one JPEG per layer increment into a
layer-indexed store. The result isn&rsquo;t a livestream with a scrubber bolted on;
it&rsquo;s a <em>print timeline</em>, with chapter markers at pauses, resumes, filament
swaps, and HMS events. The recording and the timelapse are the same artifact.</p>
<p><strong>A print API.</strong> <code>/api/print/v1</code>: you send a raw 3MF and a sparse job ticket,
and the service owns preflight, slicing, FTPS staging, the MQTT start, and
progress. It publishes a capability document — every settings key at the pinned
slicer version, annotated with what you&rsquo;re allowed to write — which is the
direct <code>Get-Printer-Attributes</code> analog. Clients render that document. They
never hard-code a key list, so the vocabulary can grow without breaking them.</p>
<p><strong>Printers that don&rsquo;t exist.</strong> The fakes speak the real wire protocol —
MQTT-over-TLS, FTPS, RTSPS — on a private network, so the gateway connects to
them exactly as it would to hardware. The whole print loop is developable and
testable with no printer in the room and nothing at risk.</p>
<h2 id="the-part-that-actually-matters">The part that actually matters</h2>
<p>Here is the diagram that makes the case better than any feature list:</p>
<pre class="mermaid">graph LR
  A[&#34;Abacus Studio&lt;br/&gt;(abaci.one)&#34;] --&gt; P[&#34;@eink/print-dialog&#34;]
  B[&#34;Frame Studio&lt;br/&gt;(e-paper frames)&#34;] --&gt; P
  C[&#34;CLI · agents ·&lt;br/&gt;your app&#34;] --&gt; P
  P --&gt; S[&#34;things.haunt.house&lt;br/&gt;/api/print/v1&#34;]
  S --&gt; X[&#34;X1C over&lt;br/&gt;MQTT · FTPS · RTSPS&#34;]</pre>
<p><code>@eink/print-dialog</code> is a published package: the types for the capability
document and the job ticket, plus a transport client. It is deliberately
service-neutral — no host, no topology, no slicer name, not even a printer
brand appears anywhere in it. That constraint is mechanically enforced in CI
rather than left to discipline.</p>
<p>Two applications already print through it. One is a soroban learning platform
that designs a physical abacus. The other is an e-paper photo frame studio that
designs an enclosure. They were built for unrelated reasons by unrelated code
paths, and <strong>neither of them knows what a Bambu is.</strong> They speak capability
documents and job tickets, the same way a word processor speaks IPP and doesn&rsquo;t
know what a Brother laser is.</p>
<p>That is the whole return on the trade. You gave up an app. You got a printer
that any of your software can print to — including software that doesn&rsquo;t exist
yet, written by people who will never read the Bambu MQTT protocol notes.</p>
<h2 id="the-ceiling-we-did-not-get-past">The ceiling we did not get past</h2>
<p>House rule: publish the limit.</p>
<p>We wanted a shared AMS that could roam between printers. Three of the four
steps are commandable over stock LAN MQTT — retract, bus switch (hardware,
ours to build), and load on the new printer. The fourth is not. <strong>There is no
software command to make a printer re-detect an AMS without a reboot.</strong> It is
absent from every client library and from the reverse-engineered protocol
notes; RFID re-reads fire only on printer restart or physical filament
insertion.</p>
<p>The most promising candidate was an undocumented <code>reset</code> parameter on
<code>ams_control</code>. We fired it at the live printer with the AMS connected and
loaded. The printer answered <code>err_code 0 / result SUCCESS</code> about 114 ms after
publish — so it is a real, recognized, non-destructive command reachable over
plain LAN MQTT, which independently confirms the no-signing finding above. It
also did nothing: both modules stayed in <code>get_version</code> across three polls,
<code>ams_exist_bits</code> held, filament stayed loaded, zero new HMS entries.
<strong>Accepted, and inert.</strong></p>
<p>So the seamless no-reboot handoff is not confirmed on stock firmware. The
honest fallbacks are tolerating a power cycle between owners, or a hardware
shim that simulates a clean reconnect — and we found no evidence that an
off-the-shelf emulator for the classic X1C RS485 AMS exists. Two claims we
<em>wanted</em> to be true went 0–3 against the evidence.</p>
<p>The ceiling stands. We&rsquo;d rather say so than let you discover it yourself at
2 a.m.</p>
<h2 id="where-this-goes">Where this goes</h2>
<p>The interesting question isn&rsquo;t whether a self-hosted control panel can match a
polished first-party app. Feature-for-feature, for a while, it won&rsquo;t.</p>
<p>The interesting question is what you build once printing is something your
software can <em>call</em>. A design tool that hands you a finished part instead of a
download. A monitoring agent that pauses on a failure cue. A frame studio where
&ldquo;print this enclosure&rdquo; is a button and not a twelve-step export.</p>
<p>None of that is reachable through a phone app, no matter how good the phone app
is. All of it is reachable through three open ports and a service that turns
them into a print dialog.</p>
<p>If you&rsquo;re running a printer in Developer Mode and you&rsquo;ve been treating it as a
loss — what would you print, if any of your programs could just print?</p>
]]></content:encoded></item></channel></rss>