<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>nth.io</title>
    <link rel="self" type="application/atom+xml" href="https://www.nth.io/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://www.nth.io"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-07-29T00:00:00+00:00</updated>
    <id>https://www.nth.io/atom.xml</id>
    <entry xml:lang="en">
        <title>mattertimectl</title>
        <published>2026-07-29T00:00:00+00:00</published>
        <updated>2026-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/mattertimectl/"/>
        <id>https://www.nth.io/luke/projects/mattertimectl/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/mattertimectl/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;mattertimectl&#x2F;mattertimectl-alpstuga-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;mattertimectl&#x2F;mattertimectl-alpstuga-medium.jpeg&quot; alt=&quot;An IKEA ALPSTUGA on a wood desk showing 1:12 PM on its LED dot-matrix display&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;&#x2F;strong&gt; — &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;mattertimectl&#x2F;&quot;&gt;mattertimectl&lt;&#x2F;a&gt; is a tiny, single-binary Matter controller that keeps
device clocks correct. It joins an existing Matter fabric as an extra administrator and pushes the time through the
standard Time Synchronization cluster, on any ecosystem, with no Home Assistant or extra hub required.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;the-problem-complex-inconsistency&quot;&gt;The problem: complex inconsistency&lt;&#x2F;h2&gt;
&lt;p&gt;The IKEA ALPSTUGA air-quality monitor is actually mainly a little desk clock. But, it has no battery-backed clock inside
so every power cycle resets it. Its as smart as a microwave clock. Matter luckily allows for Matter-based time sync so
long as the controllers (Apple, Google, Amazon, etc.) support it.&lt;&#x2F;p&gt;
&lt;p&gt;Unfortunately not all of the major Matter controllers fully support time sync, and some not at all. It&#x27;s a recurring
complaint: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;MatterProtocol&#x2F;comments&#x2F;1v54oao&#x2F;who_is_responsible_for_restoring_time_after_a&#x2F;&quot;&gt;who is responsible for restoring the time&lt;&#x2F;a&gt;,
whether &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;tradfri&#x2F;comments&#x2F;1v54o2c&#x2F;does_alpstuga_request_the_time_again_after_a&#x2F;&quot;&gt;the device re-requests it after a reboot&lt;&#x2F;a&gt;,
how to get &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;homeassistant&#x2F;comments&#x2F;1q0y8nk&#x2F;ikea_alpstuga_time_sync_with_matter_over_thread&#x2F;&quot;&gt;ALPSTUGA time sync working over Thread&lt;&#x2F;a&gt;,
and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;homeassistant&#x2F;comments&#x2F;1q23x64&#x2F;matter_sync_time_with_matter_devices_eg_ikea&#x2F;&quot;&gt;syncing Matter device clocks&lt;&#x2F;a&gt; in general.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;it-depends-entirely-on-the-hub&quot;&gt;It depends entirely on the hub&lt;&#x2F;h3&gt;
&lt;p&gt;Whether a device ever gets the time back is decided by which hub it happens to be paired to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IKEA&#x27;s own DIRIGERA hub does it&lt;&#x2F;strong&gt; (and Homey Pro), re-pushing the time after every power-off. The common advice in
those threads is, more or less, &quot;buy a IKEA Dirigera hub.&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SmartThings is unreliable&lt;&#x2F;strong&gt; with some never get the clock back and there&#x27;s no button to trigger a sync. Others say
it eventually syncs but takes a long time.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Apple Home apparently doesn&#x27;t set the time at all.&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Home Assistant&#x27;s built-in Matter integration can&#x27;t write the time cluster&lt;&#x2F;strong&gt; in the first place, which is exactly why
the cottage industry of fixes below exists.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;the-software-fix-is-a-pile-of-home-assistant&quot;&gt;The software fix is a pile of Home Assistant&lt;&#x2F;h3&gt;
&lt;p&gt;When the hub doesn&#x27;t do it, the answer today is Home Assistant plus glue. It started as a Python script that opens a
WebSocket to HA&#x27;s Matter Server and sends &lt;code&gt;SetUTCTime&lt;&#x2F;code&gt;; that grew into a
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Loweack&#x2F;Matter-Time-Sync&quot;&gt;custom component&lt;&#x2F;a&gt; and a stack of automations people trade around,
including gnarly ones that special-case the daylight-saving &quot;duplicate hour&quot; and rate-limit the flood of devices all
reconnecting at once after an outage.&lt;&#x2F;p&gt;
&lt;p&gt;It works, and it&#x27;s clever. But it&#x27;s a lot of house to keep one clock correct. Running Home Assistant, its Matter Server
add-on, a custom integration, an exposed WebSocket port, and a timezone-aware automation, all so a little display shows
the right time.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;and-the-time-itself-is-genuinely-fiddly&quot;&gt;And the time itself is genuinely fiddly&lt;&#x2F;h3&gt;
&lt;p&gt;The reason this keeps tripping people up is that the devices are picky about the write. The ALPSTUGA rejects
&lt;code&gt;SetUTCTime&lt;&#x2F;code&gt; unless it is handed Matter-epoch microseconds, the right granularity, and a time source. Time zone and DST
are their own minefield: in those threads people hit &lt;code&gt;ConstraintError (0x87)&lt;&#x2F;code&gt; the moment they use a non-European zone
(Australia&#x2F;Sydney, Asia&#x2F;Tokyo, America&#x2F;Toronto), and found that if the DST offset isn&#x27;t set &lt;em&gt;just&lt;&#x2F;em&gt; right, the time zone
is silently ignored.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-solution-mattertimectl&quot;&gt;The solution: mattertimectl&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;mattertimectl&#x2F;&quot;&gt;mattertimectl&lt;&#x2F;a&gt; is a single Rust binary that does this one job, correctly, on any
Matter setup, without adopting a home-automation platform.&lt;&#x2F;p&gt;
&lt;p&gt;It structured as a secondary controller which joins the devices&#x27; existing fabric as an additional Matter administrator
(Matter &quot;multi-admin&quot;) on its own fabric, and pushes the time through the standard cluster. The primary ecosystem (Apple
Home, SmartThings, Google, etc.) keeps working untouched. mattertimectl is just one more admin on the fabric so it&#x27;s
ecosystem-agnostic by construction.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Any time zone, not just European ones.&lt;&#x2F;strong&gt; DST offsets come straight from the IANA time-zone database, so they&#x27;re
correct for Sydney or Toronto the same as for Paris, and the DST list is bounded to what the device can actually store.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;It speaks the device&#x27;s language.&lt;&#x2F;strong&gt; Matter-epoch microseconds only at the wire, with the right granularity and time
source, so the write is accepted, then it reads the clock back to confirm the device took it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;One binary, one job.&lt;&#x2F;strong&gt; No daemon, no database, no runtime. A ~3 MB binary and a small JSON config.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Runs from a timer.&lt;&#x2F;strong&gt; Each run connects, sets the clock, verifies, and exits; a systemd timer fires it hourly, and
staleness is bounded by the interval.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Safe by default.&lt;&#x2F;strong&gt; It won&#x27;t push time unless the host&#x27;s own clock is NTP-trusted.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Thread without the baggage.&lt;&#x2F;strong&gt; No Thread radio, no border router of its own, and no BLE; it reaches Thread devices
over IPv6 through the border routers already on the network (ex: HomePods, an Apple TV, etc.).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Since it&#x27;s a simple binary, it can be run from MacOS as a one off as well.&lt;&#x2F;p&gt;
&lt;p&gt;It builds and runs on Linux and macOS today. Because it&#x27;s already a small, single-purpose binary with a tiny state
footprint, a natural next step is a port to the ESP32, so the whole job could fit on a cheap microcontroller instead of
a Raspberry Pi.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;using-it&quot;&gt;Using it&lt;&#x2F;h2&gt;
&lt;p&gt;Commission each device once, inside a pairing window opened in the primary app, then let a timer drive it:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt; -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;u&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; mattertimectl&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; mattertimectl&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; commission&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;pairing-cod&lt;&#x2F;span&gt;&lt;span&gt;e&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; systemctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt; -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;-now&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; mattertimectl.timer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That&#x27;s the whole setup. The ALPSTUGA gets the right time a couple of minutes after boot and every hour after, with
nothing left to think about.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s built on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;rs-matter&quot;&gt;rs-matter&lt;&#x2F;a&gt;, the official CSA Rust Matter stack. Source is at
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;mattertimectl&quot;&gt;src.nth.io&#x2F;luke&#x2F;mattertimectl&lt;&#x2F;a&gt; (with a
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lukehoersten&#x2F;mattertimectl&quot;&gt;GitHub mirror&lt;&#x2F;a&gt;), and prebuilt aarch64 Linux and macOS binaries are on
the releases page.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ESP32 Ethernet Display</title>
        <published>2026-07-18T00:00:00+00:00</published>
        <updated>2026-07-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/esp32-ethernet-display/"/>
        <id>https://www.nth.io/luke/projects/esp32-ethernet-display/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/esp32-ethernet-display/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-ethernet-display&#x2F;esp32-ethernet-display-final-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-ethernet-display&#x2F;esp32-ethernet-display-final-medium.jpeg&quot; alt=&quot;The finished display mounted by the door, showing the live doorbell feed&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;esp32-poe-scrypted-viewport&quot;&gt;src code&lt;&#x2F;a&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;esp32-poe-scrypted-viewport&#x2F;tree&#x2F;README.md&quot;&gt;README&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The problem&lt;&#x2F;h3&gt;
&lt;p&gt;My front door has no window or peephole so when the doorbell rings I have to pull out my phone and open the Unifi
Protect app. Unifi sells a wall display for this, the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;door-access-intercoms&#x2F;products&#x2F;ua-intercom-viewer&quot;&gt;Unifi Access Intercom
Viewer&lt;&#x2F;a&gt; at $199, but it&#x27;s built
for the Unifi &lt;em&gt;Access&lt;&#x2F;em&gt; ecosystem and won&#x27;t display generic Protect cameras or doorbells like the Doorbell Lite I have.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-partial-fix-apple-tv&quot;&gt;A partial fix: Apple TV&lt;&#x2F;h3&gt;
&lt;p&gt;The Apple TV actually pops up a live camera feed when a doorbell rings or motion is detected but the Apple TV has to be
on for this and it&#x27;s specific to Apple. The Unifi Protect accessories are bridged into Apple Home through Scrypted. I
want that same glanceable doorbell view in other parts of the house whether or not the TV is on.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-prototype&quot;&gt;The prototype&lt;&#x2F;h3&gt;
&lt;p&gt;I built the first version as a &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-unifi-protect-viewport&#x2F;&quot;&gt;Raspberry Pi Unifi Protect doorbell
viewport&lt;&#x2F;a&gt; but it was a prototype running on a full Linux
computer, running &lt;code&gt;mpv&lt;&#x2F;code&gt;, a Python daemon speaking Unifi Protect&#x27;s WebSocket protocol, and a stack of Raspberry Pi
display quirks (&lt;code&gt;fkms&lt;&#x2F;code&gt; connector names, &lt;code&gt;fbcon&lt;&#x2F;code&gt; holding the DRM device, hardware decode segfaults) held together with
Ansible.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;esp32-solution&quot;&gt;ESP32 Solution&lt;&#x2F;h2&gt;
&lt;p&gt;I set out to build a more robust solution on an ESP32 devboard with the same screen I used with the RPI.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-display-knows-nothing&quot;&gt;The display knows nothing&lt;&#x2F;h3&gt;
&lt;p&gt;The RPI version handled a lot of logic on the display device. It authenticated against Unifi Protect, had to know the
doorbell&#x27;s camera ID, and how to parse a proprietary WebSocket event stream, pull an RTSP feed, decode H.264, and rotate
video to portrait.&lt;&#x2F;p&gt;
&lt;p&gt;When I rebuilt it as the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;esp32-poe-scrypted-viewport&quot;&gt;&lt;code&gt;esp32-poe-scrypted-viewport&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; appliance,
I moved as many of those concerns off the device and into &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;scrypted.app&quot;&gt;Scrypted&lt;&#x2F;a&gt; as practical. Scrypted
handles authentication, camera selection, video decoding, scaling, rotation, overlays, and layout. It renders finished
pixels and pushes them to the display. The display just paints them and reports when the touchscreen is tapped.&lt;&#x2F;p&gt;
&lt;p&gt;The whole design follows from one rule:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;If it changes &lt;strong&gt;what&lt;&#x2F;strong&gt; should be shown, it belongs in the controller (Scrypted). If it changes &lt;strong&gt;how&lt;&#x2F;strong&gt; pixels reach
the screen, it belongs in the display (ESP32).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;the-hardware&quot;&gt;The hardware&lt;&#x2F;h3&gt;
&lt;p&gt;The hardware appliance is a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.waveshare.com&#x2F;esp32-p4-eth.htm&quot;&gt;Waveshare ESP32-P4-ETH&lt;&#x2F;a&gt; driving a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;dp&#x2F;B0CXTFN8K9&quot;&gt;Hosyond 5&quot;
800×480 IPS touchscreen&lt;&#x2F;a&gt; over MIPI-DSI.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-ethernet-display&#x2F;esp32-ethernet-display-hardware-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-ethernet-display&#x2F;esp32-ethernet-display-hardware-medium.jpeg&quot; alt=&quot;ESP32-P4-ETH with PoE hat and the 5-inch panel&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The ESP32-P4 is an awesome fit, and unusually for an ESP32 it has &lt;strong&gt;no Wi-Fi at all&lt;&#x2F;strong&gt;. For a permanently mounted
appliance I see that as a feature:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ethernet, with optional PoE.&lt;&#x2F;strong&gt; A single cable, and with PoE it carries power too. No batteries, no Wi-Fi
credentials, no wireless coverage worries. Plug it into a trusted VLAN and it works.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Hardware JPEG decode.&lt;&#x2F;strong&gt; The P4 has a dedicated JPEG engine, so the controller can push compressed frames and the
display decodes them without touching the CPU.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;MIPI-DSI + PSRAM.&lt;&#x2F;strong&gt; Enough memory bandwidth to triple-buffer a real display and paint full-motion video tear-free.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The device boots, pulls a DHCP lease, advertises itself over mDNS (&lt;code&gt;_scrypted-viewport._tcp&lt;&#x2F;code&gt;), and waits. It has no
configuration UI, no authentication, and no business logic. All configuration is pushed to it by the controller after
discovery.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;plug-and-play&quot;&gt;Plug and play&lt;&#x2F;h3&gt;
&lt;p&gt;A key design goal is that this is built for a plain trusted residential network. Onboarding a display is simply plugging
in one Ethernet cable:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Plug in.&lt;&#x2F;strong&gt; PoE is optional, the architecture only needs Ethernet, but with a cable already run to the wall, one drop
carrying power and data beats a separate USB adapter.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Get an address.&lt;&#x2F;strong&gt; The display pulls a DHCP lease the moment the link comes up.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Announce itself.&lt;&#x2F;strong&gt; It publishes an mDNS service (&lt;code&gt;_scrypted-viewport._tcp&lt;&#x2F;code&gt;) advertising its name, address, and
capabilities.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Get discovered.&lt;&#x2F;strong&gt; Controllers browse for that service, pick the display up automatically, and push it its
configuration. Nothing is typed into the device.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;That&#x27;s the whole setup. There&#x27;s no companion app, no BLE pairing, no QR code scanning, no handing over Wi-Fi
credentials, and no Thread commissioning. To be clear, NFC or BLE provisioning is genuinely great for devices that mesh
wirelessly. But a wired display just doesn&#x27;t need to be that complex on a trusted wired network.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;enclosure&quot;&gt;Enclosure&lt;&#x2F;h3&gt;
&lt;p&gt;I designed a flush-mounted, single cable, roughly the footprint of the standard light switch wallplates. The enclosure
is a two-piece 3D-printed design built around a standard electrical single-gang rough-in.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-ethernet-display&#x2F;esp32-ethernet-display-case-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-ethernet-display&#x2F;esp32-ethernet-display-case-medium.jpeg&quot; alt=&quot;The two enclosure parts laid out in the slicer: the bracket with its board box on the left, the screen carrier with dovetail feet on the right&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bracket.&lt;&#x2F;strong&gt; A base that fits over a standard &lt;strong&gt;1-gang low-voltage mounting box&lt;&#x2F;strong&gt;. The ESP32-P4-ETH board tucks neatly
inside the wall cavity behind it so the only thing outside the wall is the display screen.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Screen carrier.&lt;&#x2F;strong&gt; A separate front bracket that holds the 5&quot; panel and has a &lt;strong&gt;dovetail on its back&lt;&#x2F;strong&gt; to mate with
the mounting bracket. The carrier slides down over a matching dovetail on the bracket, locking the screen to the wall
while keeping the board and cabling fully concealed behind it.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;the-protocol&quot;&gt;The protocol&lt;&#x2F;h3&gt;
&lt;p&gt;The display&#x27;s network API is intentionally tiny. It exposes a handful of HTTP endpoints and one raw TCP socket, and
everything is pushed &lt;em&gt;to&lt;&#x2F;em&gt; it:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;POST &#x2F;frame&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — paint a single JPEG. This is the whole interaction for still content: a calendar, a weather panel,
a dashboard. Render a picture, POST it, tap-to-view.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A raw TCP stream socket&lt;&#x2F;strong&gt; — for live content, the controller opens a socket and pushes framed MJPEG straight at the
panel with no per-frame HTTP overhead. This is how doorbell video gets to 24 fps.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;POST &#x2F;state&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — wake or sleep. The backlight is fully off when idle, and the controller wakes it when something&#x27;s
worth showing.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;POST &#x2F;config&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — the controller pushes name, orientation, brightness, and its own callback URL after discovery.
Nothing is typed into the device.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;&#x2F;strong&gt; As of the time of writing this, no ESP32 chip has hardware H.264 decode (though the P4 &lt;em&gt;does&lt;&#x2F;em&gt; have H.264
encode) which is why I went with MJPEG.&lt;&#x2F;p&gt;
&lt;p&gt;Touch goes the other way: a tap toggles wake&#x2F;sleep and the device POSTs the resulting state back to the controller as a
peer, not a request&#x2F;response. The display owns its own state, and the controller and the display are symmetric peers
that each serialize their own writes and trust idempotency to converge. There&#x27;s no auth and no TLS. It&#x27;s a LAN-only
appliance on a trusted VLAN, just like the trust model of a networked printer.&lt;&#x2F;p&gt;
&lt;p&gt;The key property: &lt;strong&gt;the display performs virtually no translation.&lt;&#x2F;strong&gt; It renders pixels to the framebuffer and reports
taps. Whether those pixels are a live doorbell or a static dashboard is entirely the controller&#x27;s concern.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-esp32-p4-firmware-zero-copy-and-tear-free&quot;&gt;The ESP32-P4 firmware: zero-copy and tear-free&lt;&#x2F;h3&gt;
&lt;p&gt;Keeping a 5-inch panel fed at 24 fps on a microcontroller comes down to two rules: never copy a frame, and never block
the network. The firmware meets both with two three-buffer rings and a single pass through memory. Each JPEG is read off
the socket into the &lt;strong&gt;network ring&lt;&#x2F;strong&gt;, decoded once by the P4&#x27;s hardware JPEG engine straight into a &lt;strong&gt;framebuffer ring&lt;&#x2F;strong&gt;
that the panel scans out of, and never copied again. The network ring keeps the socket draining on its own core while
the decoder works on the other; the framebuffer ring lets the decoder write directly into display memory, so presenting
a finished frame is a pointer swap rather than a &lt;code&gt;memcpy&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Draining the socket.&lt;&#x2F;strong&gt; The raw TCP stream lands on a dedicated FreeRTOS receive task whose only job is to drain the
socket continuously. It hands each JPEG to a separate decode task through a three-buffer PSRAM ring: the receive task
fills one buffer, the decode task works on a second, and the third is either free or holds the next frame waiting
between them. The receive task never blocks on the decoder, and if decode ever falls behind, the ring keeps only the
newest frame and drops the stale one. Old unrendered frames are just dropped. An earlier single-task version that did
&lt;code&gt;recv&lt;&#x2F;code&gt;, decode, and paint in one loop stalled the socket for a few milliseconds every frame, which throttled the sender
into a stop-and-go crawl against the small default TCP window. Splitting receive from decode was the change that
unlocked the full frame rate.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Painting without a copy.&lt;&#x2F;strong&gt; The hardware JPEG engine decodes straight into one of three DSI framebuffers that the
display controller owns. There is no separate decoder output buffer and no memcpy anywhere: presenting a frame is a
cache writeback plus a pointer swap, on the order of tens of microseconds. That swap is deferred, though. The DSI DMA
only picks up the new buffer at the end of the scan pass it is in the middle of, roughly every 21 ms. With only two
buffers, decoding the next frame immediately would write into the buffer the DMA is still reading out, and the result
tears. So the three framebuffers rotate through three roles, scanning (the DMA is reading it), pending (flipped, shown
at the next boundary), and free, and the decoder is always handed the free one. The firmware tracks which buffer is
scanning from the display driver&#x27;s end-of-refresh interrupt, so a safe decode target always exists. The result is
tear-free by construction with no waiting on vsync, at the cost of one extra 1.15 MB framebuffer in PSRAM.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Where the time goes.&lt;&#x2F;strong&gt; With both rings in place the per-frame budget is lopsided in the right direction. Receiving the
JPEG off the wire is about 70% of each frame, the hardware decode is about 25%, and the actual paint is under a fifth of
a percent. The decode-and-paint stage sits idle most of the time. The wire, not the display, sets the pace, which is the
right bottleneck for a device whose whole job is to render whatever a controller sends.&lt;&#x2F;p&gt;
&lt;p&gt;The Unifi Protect cameras are natively 24fps, the 100mbps ethernet, the 5&quot; screen size and medium resolution, all kind
of luckily worked out to have no real bottlenecks. The full pipeline works well at full 24fps. I suspect a higher
resolution, a larger screen, etc. would cause lower framerates.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;scrypted-and-doorbell-video&quot;&gt;Scrypted and doorbell video&lt;&#x2F;h3&gt;
&lt;p&gt;The first concrete controller is a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;scrypted.app&quot;&gt;Scrypted&lt;&#x2F;a&gt; integration
(&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;esp32-poe-scrypted-viewport&quot;&gt;&lt;code&gt;scrypted&#x2F;scrypted-viewport.ts&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; in the repo). Scrypted already
knows how to talk to Unifi Protect, and dozens of other camera systems, so instead of teaching the display about
cameras, I taught Scrypted about displays.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-ethernet-display&#x2F;esp32-ethernet-display-scrypted-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-ethernet-display&#x2F;esp32-ethernet-display-scrypted-medium.jpeg&quot; alt=&quot;The Scrypted plugin&amp;#39;s per-display settings: orientation, brightness, idle timeout, and stream tuning, all pushed to the device over &#x2F;config&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;On a doorbell press or camera motion event, Scrypted wakes the display, spawns one &lt;code&gt;ffmpeg&lt;&#x2F;code&gt; child that pulls the
camera&#x27;s prebuffered substream, scales and rotates it to the panel&#x27;s native resolution, and streams framed MJPEG over
the raw TCP socket.  Opening &lt;code&gt;ffmpeg&lt;&#x2F;code&gt; on an already-buffered keyframe is what cuts wake-to-first-frame from ~5–6 seconds
down to about 0.7 seconds. One catch: Scrypted&#x27;s rebroadcast prebuffer is only enabled on the high-resolution stream by
default, and the display pulls the medium substream, so prebuffering has to be turned on for the medium stream too.
&lt;strong&gt;Without it the fast start silently falls back to a cold ~5–6 second open.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;On the wire it sustains &lt;strong&gt;painted = sent = 24 fps&lt;&#x2F;strong&gt; with sub-50 ms glass-to-glass latency, the source frame rate rather
than the device being the limit.&lt;&#x2F;p&gt;
&lt;p&gt;Compared to the Pi prototype this is dramatically simpler &lt;em&gt;on the display device&lt;&#x2F;em&gt;: no &lt;code&gt;mpv&lt;&#x2F;code&gt;, no RTSP parsing, no Protect
WebSocket handling, no software H.264 decode, no OS. All of that complexity moved to Scrypted, where it belongs and
where it&#x27;s shared across every display.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-hardwired-path&quot;&gt;A hardwired path&lt;&#x2F;h3&gt;
&lt;p&gt;There&#x27;s a compounding benefit once the whole chain is wired. My cameras and doorbell are PoE, the Apple TV is on
Ethernet, and the displays are PoE. When an event occurs, a doorbell press or motion at a side gate, there&#x27;s no wireless
hop anywhere between the sensor and the glass. The stream is already prebuffered on a wired NVR and it lands on a wired
display across a wired switch, so it loads instantly instead of stalling the way a Wi-Fi camera feeding a Wi-Fi tablet
does. Latency and reliability stop being things I think about, because there&#x27;s no radio in the path to be congested,
roamed off of, or dropped.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Emacs claude-code-context.el</title>
        <published>2026-04-26T00:00:00+00:00</published>
        <updated>2026-04-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/emacs-claude-code-context/"/>
        <id>https://www.nth.io/luke/projects/emacs-claude-code-context/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/emacs-claude-code-context/">&lt;p&gt;I like to use Claude Code in my terminal &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ghostty.org&quot;&gt;Ghostty&lt;&#x2F;a&gt; alongside Emacs. Claude can already read and
edit files directly, and Emacs auto-reverts when it does. But Claude doesn&#x27;t know my buffer state like which file I have
open, which line im at, and what I have highlighted.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;claude-code-context&#x2F;tree&#x2F;README.md?h=main&quot;&gt;claude-code-context&lt;&#x2F;a&gt;
(&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lukehoersten&#x2F;claude-code-context&quot;&gt;github&lt;&#x2F;a&gt;) does just that.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How It Works&lt;&#x2F;h2&gt;
&lt;p&gt;The package runs an idle timer that writes your current buffer state to a JSON file every 2 seconds:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;buffer&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&#x2F;Users&#x2F;luke&#x2F;project&#x2F;src&#x2F;main.py&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;line&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt; 42&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;column&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt; 8&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;modified&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#B58900, #B58900);&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;selection&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;def process_data(items):&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;    return [x * 2 for x in items]&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A &lt;code&gt;UserPromptSubmit&lt;&#x2F;code&gt; hook in Claude Code&#x27;s &lt;code&gt;~&#x2F;.claude&#x2F;settings.json&lt;&#x2F;code&gt; injects that file into every prompt automatically.
Claude sees your cursor position and active selection without you having to copy-paste anything.&lt;&#x2F;p&gt;
&lt;p&gt;It works in both file buffers and transient buffers like &lt;code&gt;*Messages*&lt;&#x2F;code&gt; and &lt;code&gt;*compilation*&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The timer only fires when Emacs is idle so there&#x27;s no performance impact during active editing. Use &lt;code&gt;C-c C-l d&lt;&#x2F;code&gt; to
optionally append flymake diagnostics so Claude can see specific errors. All commands are also available manually for
full control without the timer.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;emacs-lisp&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;add-to-list&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;load-path&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&#x2F;path&#x2F;to&#x2F;claude-code-context&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;require&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;claude-code-context&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;claude-code-context-mode &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#D33682, #D33682);&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Add to &lt;code&gt;~&#x2F;.claude&#x2F;settings.json&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;hooks&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;    &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;UserPromptSubmit&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;      &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;hooks&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;        &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;type&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;command&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;        &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;command&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;CONTEXT_FILE=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;${XDG_CONFIG_HOME:-$HOME&#x2F;.config}&#x2F;emacs&#x2F;claude-code-context.json&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;; [ -f &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;$CONTEXT_FILE&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; ] || CONTEXT_FILE=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;$HOME&#x2F;.emacs.d&#x2F;claude-code-context.json&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;; if [ -f &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;$CONTEXT_FILE&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; ]; then echo &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\\&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;n---&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\\&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;n## Emacs Context&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\\&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;n&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;; cat &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;$CONTEXT_FILE&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;; echo &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\\&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;n---&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#CB4B16, #CB4B16);&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;; fi&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The hook detects both &lt;code&gt;~&#x2F;.config&#x2F;emacs&lt;&#x2F;code&gt; (XDG) and traditional &lt;code&gt;~&#x2F;.emacs.d&lt;&#x2F;code&gt; automatically.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keybindings&quot;&gt;Keybindings&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Key&lt;&#x2F;th&gt;&lt;th&gt;Action&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;C-c C-l u&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Manually update context&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;C-c C-l d&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Include flymake diagnostics&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;C-c C-l c&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Clear context&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;C-c C-l m&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Toggle auto-update mode&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The package is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;melpa&#x2F;melpa&#x2F;pull&#x2F;9983&quot;&gt;submitted to MELPA&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Raspberry Pi Unifi Protect Viewport</title>
        <published>2026-04-18T00:00:00+00:00</published>
        <updated>2026-04-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/raspberrypi-unifi-protect-viewport/"/>
        <id>https://www.nth.io/luke/projects/raspberrypi-unifi-protect-viewport/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/raspberrypi-unifi-protect-viewport/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-unifi-protect-viewport&#x2F;unifi-protect-viewport-live-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-unifi-protect-viewport&#x2F;unifi-protect-viewport-live-medium.jpeg&quot; alt=&quot;Live doorbell camera feed on the display&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;&#x2F;h2&gt;
&lt;p&gt;My front door doesn&#x27;t have a peephole so when someone rings the doorbell, you can&#x27;t see who it is without pulling out
your phone and looking at the doorbell app. Unifi makes a doorbell viewer device, the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;door-access-intercoms&#x2F;products&#x2F;ua-intercom-viewer&quot;&gt;Unifi Access Intercom
Viewer&lt;&#x2F;a&gt; for $199. It&#x27;s a PoE wall
display for viewing Protect cameras and intercoms but it&#x27;s designed for the Unifi Access ecosystem and don&#x27;t work with
their doorbells. For a home with a standard Protect doorbell, like mine, it&#x27;s not really an option as it requires
something like the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;products&#x2F;uvc-g6-entry&quot;&gt;G6 Entry&lt;&#x2F;a&gt; for $249 to work natively. Standard
Protect doorbells like the Doorbell Lite aren&#x27;t compatible. If you&#x27;re not running a full Unifi Access deployment,
there&#x27;s no path to a dedicated viewer leaving me to build one.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;Unifi stack&lt;&#x2F;th&gt;&lt;th&gt;This build&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Doorbell&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;G6 Entry ($249)&lt;&#x2F;td&gt;&lt;td&gt;Doorbell Lite ($99)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Display&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Intercom Viewer ($199)&lt;&#x2F;td&gt;&lt;td&gt;DIY screen ($125)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;$448&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;$224&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Beyond the cost, Unifi Protect sends push notifications to the app when someone rings, but I wanted something more
immediate and passive: a dedicated display near the door that shows live video when someone rings without needing to
pull out a phone. Something closer to a traditional peephole or intercom panel that&#x27;s always there and requires no
interaction.&lt;&#x2F;p&gt;
&lt;p&gt;My design constraints were:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;No Home Assistant or Scrypted, just native Unifi Protect&lt;&#x2F;li&gt;
&lt;li&gt;No browser-based playback&lt;&#x2F;li&gt;
&lt;li&gt;No backlight when idle. The screen must be completely off when not in use.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;hardware&quot;&gt;Hardware&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Component&lt;&#x2F;th&gt;&lt;th&gt;Price&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;products&#x2F;uvc-doorbell-lite&quot;&gt;Unifi Doorbell Lite&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;$99.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.raspberrypi.com&#x2F;products&#x2F;raspberry-pi-4-model-b&#x2F;&quot;&gt;Raspberry Pi 4&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;$55.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;dp&#x2F;B09FYQ8GCT&quot;&gt;Raspberry Pi PoE+ HAT&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;$34.99&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;dp&#x2F;B0CXTFN8K9&quot;&gt;Hosyond 5&quot; DSI Touchscreen&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;$34.99&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;$224&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;I already had the RPI4 and POE hat from another project so prices are likely higher now due to AI-driven RAM demand. The
display connects over DSI for video and USB for touch input. Mounted portrait, the logical resolution is 480×800 with
system-level rotation applied via mpv. The PoE HAT powers the Pi directly from the wall switch, keeping the cable run to
a single Ethernet drop.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-unifi-protect-viewport&#x2F;unifi-protect-viewport-hardware-top-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-unifi-protect-viewport&#x2F;unifi-protect-viewport-hardware-top-medium.jpeg&quot; alt=&quot;Pi 4 and DSI display from above&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-unifi-protect-viewport&#x2F;unifi-protect-viewport-hardware-side-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-unifi-protect-viewport&#x2F;unifi-protect-viewport-hardware-side-medium.jpeg&quot; alt=&quot;Pi 4 and DSI display from the side&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;software-stack&quot;&gt;Software Stack&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Layer&lt;&#x2F;th&gt;&lt;th&gt;Choice&lt;&#x2F;th&gt;&lt;th&gt;Reason&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Rendering&lt;&#x2F;td&gt;&lt;td&gt;DRM&#x2F;KMS (fkms)&lt;&#x2F;td&gt;&lt;td&gt;No X11; framebuffer direct to DSI display&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Player&lt;&#x2F;td&gt;&lt;td&gt;mpv &lt;code&gt;--vo=drm&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Low-latency RTSP, software decode, no display server&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Backlight&lt;&#x2F;td&gt;&lt;td&gt;sysfs &lt;code&gt;&#x2F;sys&#x2F;class&#x2F;backlight&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Kills backlight entirely without disrupting DRM&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Touch input&lt;&#x2F;td&gt;&lt;td&gt;evdev&lt;&#x2F;td&gt;&lt;td&gt;Direct kernel input, no display server required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Doorbell events&lt;&#x2F;td&gt;&lt;td&gt;Unifi Protect WebSocket&lt;&#x2F;td&gt;&lt;td&gt;Native Protect API, no polling&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Daemon&lt;&#x2F;td&gt;&lt;td&gt;Python 3 asyncio&lt;&#x2F;td&gt;&lt;td&gt;Clean state machine, concurrent event handling&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Supervision&lt;&#x2F;td&gt;&lt;td&gt;systemd&lt;&#x2F;td&gt;&lt;td&gt;Start on boot, auto-restart, journald logging&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Config&lt;&#x2F;td&gt;&lt;td&gt;Ansible&lt;&#x2F;td&gt;&lt;td&gt;Auto-deployable with no compiling or binaries&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;state-machine&quot;&gt;State Machine&lt;&#x2F;h2&gt;
&lt;p&gt;The daemon has two states: &lt;strong&gt;IDLE&lt;&#x2F;strong&gt; and &lt;strong&gt;ACTIVE&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;IDLE:   screen backlight OFF, no playback, waiting for triggers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ACTIVE: screen ON, live RTSP stream playing, 45s sliding timeout&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;State transitions:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Doorbell press&lt;&#x2F;strong&gt; → IDLE becomes ACTIVE (or resets the 45s timer if already ACTIVE)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Touch while IDLE&lt;&#x2F;strong&gt; → becomes ACTIVE&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Touch while ACTIVE&lt;&#x2F;strong&gt; → immediately stops playback and turns screen off&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;45s timeout&lt;&#x2F;strong&gt; → stops playback, turns screen off, returns to IDLE&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;backlight-control&quot;&gt;Backlight Control&lt;&#x2F;h2&gt;
&lt;p&gt;The hardest constraint was &quot;completely dark when idle.&quot; A black framebuffer isn&#x27;t sufficient because the backlight is
still on. The daemon controls brightness exclusively through sysfs:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;sys&#x2F;class&#x2F;backlight&#x2F;rpi_backlight&#x2F;brightness&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Writing &lt;code&gt;0&lt;&#x2F;code&gt; kills the backlight entirely. Writing &lt;code&gt;max_brightness&lt;&#x2F;code&gt; restores it. This approach has two key properties: it
doesn&#x27;t interfere with the DRM&#x2F;KMS rendering pipeline (unlike &lt;code&gt;vcgencmd display_power&lt;&#x2F;code&gt;, which resets DRM planes and
breaks video output), and it works without root because the Ansible role adds a udev rule granting the &lt;code&gt;video&lt;&#x2F;code&gt; group
write access to the brightness file.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;fkms&lt;&#x2F;code&gt; driver exposes the DSI panel as the &lt;code&gt;DSI-1&lt;&#x2F;code&gt; connector. &lt;code&gt;mpv&lt;&#x2F;code&gt; is configured with &lt;code&gt;--drm-connector=DSI-1&lt;&#x2F;code&gt; so it
targets the correct display without needing to guess device indices.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;unifi-protect-integration&quot;&gt;Unifi Protect Integration&lt;&#x2F;h2&gt;
&lt;p&gt;The daemon authenticates directly against Unifi Protect&#x27;s local API (no cloud) and holds a persistent WebSocket
connection to the event stream. It filters for &lt;code&gt;ring&lt;&#x2F;code&gt; events on the configured camera ID and ignores motion events
entirely.&lt;&#x2F;p&gt;
&lt;p&gt;One nuance is Protect&#x27;s WebSocket can deliver multiple binary-framed packets concatenated in a single message. A naive
implementation that reads only the first packet per message will silently drop events. The daemon loops through the full
payload, extracting each packet until the buffer is exhausted.&lt;&#x2F;p&gt;
&lt;p&gt;Each packet has an 8-byte header:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[packet_type: 1B][payload_format: 1B][compression: 1B][unused: 1B][payload_size: 4B]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Payload format &lt;code&gt;1&lt;&#x2F;code&gt; is JSON (action&#x2F;data frames). Compression flag &lt;code&gt;1&lt;&#x2F;code&gt; means zlib-deflate. Ring events appear in action
frames with &lt;code&gt;action: &quot;add&quot;&lt;&#x2F;code&gt; and &lt;code&gt;modelKey: &quot;event&quot;&lt;&#x2F;code&gt;, and in the corresponding data frame with &lt;code&gt;type: &quot;ring&quot;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Connection handling is automatic reconnect with exponential backoff, no manual intervention required after a network
disruption.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;video-playback&quot;&gt;Video Playback&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;mpv&lt;&#x2F;code&gt; plays the RTSP stream from Unifi Protect in fullscreen DRM&#x2F;KMS mode with no window chrome. On the RPi4 with the
fkms driver, hardware decode via V4L2M2M causes segfaults, so the daemon uses &lt;code&gt;--hwdec=no&lt;&#x2F;code&gt; and software H.264 decode
which is fast enough for a 480p stream on a Pi 4.&lt;&#x2F;p&gt;
&lt;p&gt;The stream starts fresh on each ACTIVE transition as a cold start. &lt;code&gt;mpv&lt;&#x2F;code&gt; is launched as a subprocess and the daemon
waits for it to exit before returning to IDLE, whether from timeout, touch dismiss, or signal.&lt;&#x2F;p&gt;
&lt;p&gt;A few &lt;code&gt;mpv&lt;&#x2F;code&gt; flags that matter:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--vo=drm --drm-connector=DSI-1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--video-rotate=270          # portrait orientation&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--hwdec=no                  # software decode required on fkms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--cache=yes --demuxer-max-bytes=1M&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--loop=no                   # never replay cached content&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--fullscreen --no-border --no-osc&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;touch-input&quot;&gt;Touch Input&lt;&#x2F;h2&gt;
&lt;p&gt;Touch events are read via &lt;code&gt;evdev&lt;&#x2F;code&gt;. The daemon discovers the touch device dynamically by scanning all input devices and
matching on multitouch capability (&lt;code&gt;ABS_MT_POSITION_X&lt;&#x2F;code&gt;) and &lt;code&gt;BTN_TOUCH&lt;&#x2F;code&gt; but not on &lt;code&gt;&#x2F;dev&#x2F;input&#x2F;event*&lt;&#x2F;code&gt; index, which can
shift after reboot. An optional name-match pattern (&lt;code&gt;unifi_protect_viewport_touch_match&lt;&#x2F;code&gt;) lets you pin to a specific
device if multiple touch devices are present.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ansible-role&quot;&gt;Ansible Role&lt;&#x2F;h2&gt;
&lt;p&gt;The entire deployment is
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;ansible-roles&#x2F;tree&#x2F;unifi-protect-viewport&quot;&gt;&lt;code&gt;ansible-roles&#x2F;unifi-protect-viewport&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;. A single
&lt;code&gt;ansible-playbook&lt;&#x2F;code&gt; run on a fresh Ubuntu Server image:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Creates a &lt;code&gt;unifi-protect-viewport&lt;&#x2F;code&gt; system user with &lt;code&gt;video&lt;&#x2F;code&gt;, &lt;code&gt;input&lt;&#x2F;code&gt;, &lt;code&gt;render&lt;&#x2F;code&gt;, and &lt;code&gt;tty&lt;&#x2F;code&gt; groups&lt;&#x2F;li&gt;
&lt;li&gt;Installs &lt;code&gt;mpv&lt;&#x2F;code&gt;, &lt;code&gt;python3-evdev&lt;&#x2F;code&gt;, &lt;code&gt;python3-requests&lt;&#x2F;code&gt;, &lt;code&gt;python3-websockets&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Writes the daemon binary, systemd service, and environment file from templates&lt;&#x2F;li&gt;
&lt;li&gt;Adds a &lt;code&gt;udev&lt;&#x2F;code&gt; rule granting the service user write access to the sysfs backlight&lt;&#x2F;li&gt;
&lt;li&gt;Configures &lt;code&gt;fbcon=map:99&lt;&#x2F;code&gt; in &lt;code&gt;cmdline.txt&lt;&#x2F;code&gt; to free the DRM device from the framebuffer console&lt;&#x2F;li&gt;
&lt;li&gt;Enables &lt;code&gt;dtoverlay=vc4-fkms-v3d&lt;&#x2F;code&gt; for the DSI connector&lt;&#x2F;li&gt;
&lt;li&gt;Optionally configures PoE HAT fan temperature thresholds&lt;&#x2F;li&gt;
&lt;li&gt;Enables and starts the service&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;debug-tool&quot;&gt;Debug Tool&lt;&#x2F;h2&gt;
&lt;p&gt;The deployed system includes &lt;code&gt;unifi-protect-viewport-debug&lt;&#x2F;code&gt; for testing without ringing the actual doorbell:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;unifi-protect-viewport-debug&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; show&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;          #&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; turn display on&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;unifi-protect-viewport-debug&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; hide&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;          #&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; turn display off&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;unifi-protect-viewport-debug&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; test-display&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;  #&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; cycle backlight off -&amp;gt; 3s -&amp;gt; on&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;unifi-protect-viewport-debug&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; test-touch&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;    #&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; list evdev touch devices and capabilities&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;unifi-protect-viewport-debug&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; test-stream&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;   #&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; fetch RTSP URL and play via mpv&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;unifi-protect-viewport-debug&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; test-protect&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;  #&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; verify Protect API auth and camera info&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;bringup-notes&quot;&gt;Bringup Notes&lt;&#x2F;h2&gt;
&lt;p&gt;A few things that weren&#x27;t obvious during hardware bring-up:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;fbcon holds the DRM device.&lt;&#x2F;strong&gt; Without &lt;code&gt;fbcon=map:99&lt;&#x2F;code&gt; in &lt;code&gt;cmdline.txt&lt;&#x2F;code&gt;, the framebuffer console claims the DRM device
and mpv exits with rc=2 (device busy). The kernel parameter redirects fbcon to a nonexistent VT, freeing card0 for mpv.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;fkms uses DSI-1, not HDMI-A-1.&lt;&#x2F;strong&gt; The firmware KMS driver uses different connector names than full KMS. Getting this
wrong means mpv opens &lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;card0&lt;&#x2F;code&gt; but outputs to nothing.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;vcgencmd display_power resets DRM planes.&lt;&#x2F;strong&gt; Using it to blank the display tears down the rendering pipeline,
breaking subsequent mpv output. Sysfs brightness control avoids this entirely.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hardware decode segfaults on fkms.&lt;&#x2F;strong&gt; V4L2M2M hardware decode crashes mpv on the Pi 4 with the firmware KMS
driver. &lt;code&gt;--hwdec=no&lt;&#x2F;code&gt; is required. Software decode handles 480p without issue.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Protect WebSocket is multi-packet.&lt;&#x2F;strong&gt; A single WebSocket message may contain multiple concatenated binary
packets. Reading only the first will silently drop ring events that arrive bundled with other packets.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;The system boots to idle with the screen fully dark within a few seconds of the Pi coming up. A doorbell press to live
video with one to two seconds for the RTSP stream to begin rendering. The display stays dark under normal conditions.&lt;&#x2F;p&gt;
&lt;p&gt;Compared to the &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-homekit-intercom&#x2F;&quot;&gt;ESP32 HomeKit intercom&lt;&#x2F;a&gt; (which handles the legacy building buzzer and lock),
this system handles the newer Unifi PoE doorbell. The two projects complement each other with the ESP32 handles the
1970s wired intercom hardware and the Pi viewport handles the modern networked camera.&lt;&#x2F;p&gt;
&lt;p&gt;Next up is designing a custom one-gang low voltage bracket mount to make the device look like a proper wall panel that&#x27;s
flush-mounted and a similar footprint to the Unifi Intercom Viewer it replaces in spirit.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>HomeLab Mini Rack v2: Full 2.5G with 10G Backhaul</title>
        <published>2026-01-05T00:00:00+00:00</published>
        <updated>2026-01-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/homelab-mini-rack-v2/"/>
        <id>https://www.nth.io/luke/projects/homelab-mini-rack-v2/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/homelab-mini-rack-v2/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;homelab-mini-rack-v2&#x2F;mini-rack-v2-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;homelab-mini-rack-v2&#x2F;mini-rack-v2-medium.jpeg&quot; alt=&quot;Mini Rack v2&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;My &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;homelab-mini-rack&#x2F;&quot;&gt;original mini rack build&lt;&#x2F;a&gt; had most devices on 2.5Gbps but had a few gaps like the gateway, the cluster
switch, and the access points were still gigabit. My mini rack v2 fills those gaps.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;network&quot;&gt;Network&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;cloud-gateways-compact&#x2F;products&#x2F;ucg-fiber&quot;&gt;UCG-Fiber&lt;&#x2F;a&gt; anchors the rack with a
10G WAN port (my modem and WAN are only 2.5Gb), a built-in 4-port 2.5GbE switch, and 1TB NVMe for Protect camera
recording.&lt;&#x2F;p&gt;
&lt;p&gt;Both switches are
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;switching-utility&#x2F;products&#x2F;usw-flex-2-5g-8-poe&quot;&gt;USW-Flex-2.5G-8-PoE&lt;&#x2F;a&gt; with all 8
ports at 2.5Gbps. Uplink to the gateway via 10G SFP+.&lt;&#x2F;p&gt;
&lt;p&gt;Each of the three indoor floors has a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;wifi-wall&#x2F;products&#x2F;u7-pro-wall&quot;&gt;U7 Pro Wall&lt;&#x2F;a&gt; connected at
2.5Gbps over the existing cat6 runs. The two half-U patch panels give 24 home runs covering APs, cameras, Apple TVs, and
bedroom drops.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;components&quot;&gt;Components&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rack&lt;&#x2F;strong&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a.co&#x2F;d&#x2F;dSsgOyO&quot;&gt;NavePoint 9U 10&quot;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Shelves&lt;&#x2F;strong&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a.co&#x2F;d&#x2F;bAZv4lL&quot;&gt;DIGITUS Shelf 1U 10&quot;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Keystones&lt;&#x2F;strong&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;dp&#x2F;B07PGDJTV2&quot;&gt;VCE Cat6 Keystone Jacks&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;U&lt;&#x2F;th&gt;&lt;th&gt;Component&lt;&#x2F;th&gt;&lt;th&gt;Model&lt;&#x2F;th&gt;&lt;th&gt;Notes&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;9&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Modem&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a.co&#x2F;d&#x2F;aXYdRn0&quot;&gt;Hitron CODA56&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;DOCSIS 3.1 + 2.5Gbps&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Gateway&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;cloud-gateways-compact&#x2F;products&#x2F;ucg-fiber&quot;&gt;UCG-Fiber&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;10G + integrated 4-port 2.5GbE switch + 1TB NVMe&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AP Switch&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;switching-utility&#x2F;products&#x2F;usw-flex-2-5g-8-poe&quot;&gt;USW-Flex-2.5G-8-PoE&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;PoE APs and cameras, 2.5G cat6 drops. 10G SFP+ backhaul.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Patch Panel&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;dp&#x2F;B0F3DFDW8V&quot;&gt;GeeekPi 0.5U 12 Port&lt;&#x2F;a&gt; ×2&lt;&#x2F;td&gt;&lt;td&gt;Two half-U panels for 24 home runs total.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Cluster Switch&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;switching-utility&#x2F;products&#x2F;usw-flex-2-5g-8-poe&quot;&gt;USW-Flex-2.5G-8-PoE&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;PoE powered from AP switch. 10G SFP+ backhaul.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&#x2F;4&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RPi 4 Cluster&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.thingiverse.com&#x2F;thing:4078710&quot;&gt;russross 2U mount&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;Modified OpenSCAD model for PoE and M.2 hat sizes. See the &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homelab&#x2F;&quot;&gt;RPi homelab post&lt;&#x2F;a&gt;.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;M4 Mac mini&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;en-us&#x2F;121555&quot;&gt;M4 Mac mini&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;On a DIGITUS shelf. Custom 3D printed mount planned.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PDU&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;dp&#x2F;B0BCTBFPNM&quot;&gt;Tupavco TP1713&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>HomeLab Mini Rack: Unifi 2.5G 9U</title>
        <published>2025-06-01T00:00:00+00:00</published>
        <updated>2025-06-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/homelab-mini-rack/"/>
        <id>https://www.nth.io/luke/projects/homelab-mini-rack/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/homelab-mini-rack/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;homelab-mini-rack&#x2F;mini-rack-large.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;homelab-mini-rack&#x2F;mini-rack-medium.png&quot; alt=&quot;Mini-Rack&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;motivation&quot;&gt;Motivation&lt;&#x2F;h2&gt;
&lt;p&gt;With the emergence of the 10 inch network rack, Jeff Geerling organized a community around the concept of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mini-rack.jeffgeerling.com&quot;&gt;mini-racks&lt;&#x2F;a&gt;. I moved my homelab to a mini rack in early 2024 and the new community was a great
excuse for me to document and publish my latest homelab&#x2F;mini rack setup. The following is a re-post of my
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;geerlingguy&#x2F;mini-rack&#x2F;issues&#x2F;79&quot;&gt;mini-rack.jeffgeerling.com mini-rack submission&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This mini rack serves two main purposes:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;running PoE APs and cameras&lt;&#x2F;li&gt;
&lt;li&gt;running the 4x Rpi4 cluster with PoE and M.2 hats.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;This has been a long term and ongoing project starting in 2019. This mini rack grew out of my earlier
&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homelab&#x2F;&quot;&gt;Raspberry Pi HomeLab&lt;&#x2F;a&gt;, whose writeup explains more of the
motivation and specifics of the Pi cluster. Cluster Ansible roles and provisioning can be browsed here:
https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;ansible-roles&#x2F;file&lt;&#x2F;p&gt;
&lt;h2 id=&quot;components&quot;&gt;Components&lt;&#x2F;h2&gt;
&lt;p&gt;The entire rack is fed from one power cable and one coax cable, excluding the ethernet drops, of course.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rack&lt;&#x2F;strong&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a.co&#x2F;d&#x2F;dSsgOyO&quot;&gt;NavePoint 9U 10”&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Shelves&lt;&#x2F;strong&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a.co&#x2F;d&#x2F;bAZv4lL&quot;&gt;DIGITUS Shelf 1U 10&quot;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Keystones&lt;&#x2F;strong&gt; - &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a.co&#x2F;d&#x2F;7BU4oaL&quot;&gt;ZOERAX Keystone Couplers&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;U&lt;&#x2F;th&gt;&lt;th&gt;Component&lt;&#x2F;th&gt;&lt;th&gt;Model&lt;&#x2F;th&gt;&lt;th&gt;Notes&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;9&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Modem&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a.co&#x2F;d&#x2F;aXYdRn0&quot;&gt;Hitron CODA56&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;DOCSIS 3.1 + 2.5Gbps&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Gateway&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;cloud-gateways-compact&#x2F;collections&#x2F;cloud-gateway-max&quot;&gt;UCG-Max&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;2.5Gbps + 512 GB NVMe&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AP Switch&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;category&#x2F;switching-utility&#x2F;products&#x2F;usw-flex-2-5g-8-poe&quot;&gt;USW-Flex-2.5G-8-PoE&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;Runs PoE access points and cameras, as well as cat6 drops.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Patch Panel&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a.co&#x2F;d&#x2F;86c6cFx&quot;&gt;Enmane 12 Port&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;I prefer this specific panel because of its ratchet-based cable support&#x2F;strain relief.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Cluster Switch&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;store.ui.com&#x2F;us&#x2F;en&#x2F;collections&#x2F;pro-ultra&#x2F;products&#x2F;usw-ultra&quot;&gt;USW-Ultra&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;PoE powered from the AP Switch given the PDU limits.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3-4&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RPI 4 Cluster&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.thingiverse.com&#x2F;thing:4078710&quot;&gt;russross 2U mount&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;I modified the OpenSCAD model to fit my specific PoE and M.2 hat sizes.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;M4 Mac mini&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;en-us&#x2F;121555&quot;&gt;M4 Mac mini&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;I plan to make a custom M4 Mac mini 3d printed rack mount.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PDU&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;dp&#x2F;B0BCTBFPNM?psc=1&amp;amp;ref=ppx_yo2ov_dt_b_product_details&quot;&gt;Tupavco TP1713&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;em&gt;Note: None of these are affiliate links and only provided for ease of reference.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>3D Printed Metal Vintage Door Latch Parts</title>
        <published>2023-02-11T00:00:00+00:00</published>
        <updated>2023-02-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/3d-printed-metal-vintage-door-latch-parts/"/>
        <id>https://www.nth.io/luke/projects/3d-printed-metal-vintage-door-latch-parts/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/3d-printed-metal-vintage-door-latch-parts/">&lt;p&gt;My family and I moved into a home built in 1895 and thankfully a lot of the original hardware and woodwork had been retained. Unfortunately, one of the bedroom door latches was missing its internals.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-1-large.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-1-medium.png&quot; alt=&quot;original B.L.W. latch&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;After disassembling, cleaning up, and investigating the mortise latch, including another that had all its parts, &quot;B.L.W.&quot; branding on the mortise casing lead to Branford Lock Works, a lock company from the mid 1800s in Branford, Connecticut.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-2-large.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-2-medium.png&quot; alt=&quot;exploded B.L.W. latch&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;A number of vintage hardware resellers carried B.L.W. but it seemed doubtful that exact replacement parts would be locatable, let alone for a reasonable price.&lt;&#x2F;p&gt;
&lt;p&gt;Instead, hoping to have the parts machined locally here in Chicago, I &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;395840-vintage-door-latch-inner-parts&quot;&gt;model the missing parts&lt;&#x2F;a&gt; in Fusion 360 and 3D printed a plastic prototype. To my surprise, the prototype worked extremely well even when made with plastic.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-5-large.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-5-medium.png&quot; alt=&quot;3D modeled B.L.W. latch inner parts&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Machine shops need technical drawings so I used a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;GUDhet2TKHQ&quot;&gt;Clough42 YouTube&lt;&#x2F;a&gt; video to learn how to make them from my Fusion 360 model. Unfortunately my job was just too small to be worth it for a machine shop and never received a quote.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-6-large.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-6-medium.png&quot; alt=&quot;replica B.L.W. latch technical drawings&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Given the plastic prototype functioned so well, I finally had the idea to get the model printed in steel by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;shapeways.com&quot;&gt;Shapeways&lt;&#x2F;a&gt;. I received an instant quote for steel sintering for about $60, much cheaper than buying vintage replacement parts if they could even be found, and had the finished steel part in hand within a week! The part works perfectly and will hopefully last another 120 years.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-3-large.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-3-medium.png&quot; alt=&quot;original, 3D printed plastic, and 3D printed steel B.L.W. latch parts&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-4-large.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-metal-vintage-door-latch-parts&#x2F;blw-latch-4-medium.png&quot; alt=&quot;B.L.W. latch with new 3D printed steel parts&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Raspberry Pi Unifi Controller on Arm64 </title>
        <published>2022-11-05T00:00:00+00:00</published>
        <updated>2022-11-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/raspberrypi-unifi-controller/"/>
        <id>https://www.nth.io/luke/projects/raspberrypi-unifi-controller/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/raspberrypi-unifi-controller/">&lt;p&gt;Ubiquiti wireless access points are configured via a web app called Unifi Controller which can be self hosted. Unfortunately, Unifi Controller relies on some pretty outdated packages, making them hard to find and maintain alongside modern Linux distributions versions. Adding to the complexity, the packages required are old enough that Arm64 packaging was not yet commonplace. The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;help.ui.com&#x2F;hc&#x2F;en-us&#x2F;articles&#x2F;220066768-UniFi-How-to-Install-and-Update-via-APT-on-Debian-or-Ubuntu&quot;&gt;official Unifi docs&lt;&#x2F;a&gt; do not cover installation in such a scenario.&lt;&#x2F;p&gt;
&lt;p&gt;After quite a bit of digging around in public apt repos, I found the right combination of packages to backport with Ubuntu 22.04.1 LTS (Jammy). Specifically, Unifi relies on the outdated packages MongoDB 3.6 and libssl1.1. Again, these packages are so outdated that Arm64 packaging was not readily available. &lt;strong&gt;I&#x27;ve developed an &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;ansible-roles&#x2F;tree&#x2F;unifi-controller&quot;&gt;Unifi Ansible role&lt;&#x2F;a&gt; to codify and automate my findings&lt;&#x2F;strong&gt; which are covered below.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mongo-db&quot;&gt;Mongo DB&lt;&#x2F;h2&gt;
&lt;p&gt;With the first dependency of Unifi, MongoDB, the latest version Unifi seems to support is 3.6 while 6.0 is the latest at the time of publishing. The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mongodb.com&#x2F;docs&#x2F;manual&#x2F;tutorial&#x2F;install-mongodb-on-ubuntu&#x2F;&quot;&gt;official MongoDB docs&lt;&#x2F;a&gt; recommend using their apt repo instead of what comes packaged with Ubuntu or Debian. Also, &quot;starting in MongoDB 5.0, mongod, mongos, and the legacy mongo shell no longer support arm64 platforms which do not meet this minimum microarchitecture requirement.&quot; By specifying &lt;code&gt;arm64&lt;&#x2F;code&gt; as the architecture in the Xenial Mongo 3.6 apt repo, a compatible MongoDB 3.6 is available for Arm64 Ubuntu.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lib-ssl-1-1&quot;&gt;Lib SSL 1.1&lt;&#x2F;h2&gt;
&lt;p&gt;This old version of MongoDB relies on libssl1, while Ubuntu now uses libssl3. Ubuntu actually maintains a backport of libssl1.1 to which I was only able to find a direct deb package link. The usual &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;packages.ubuntu.com&#x2F;bionic&#x2F;arm64&#x2F;libssl1.1&#x2F;download&quot;&gt;Ubuntu package index&lt;&#x2F;a&gt; links do not seem populated anymore.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;unifi&quot;&gt;Unifi&lt;&#x2F;h2&gt;
&lt;p&gt;Finally, Unifi can be installed on a Raspberry Pi with Ubuntu Arm64 by specifying &lt;code&gt;armhf&lt;&#x2F;code&gt; as the architecture. Unfortunately &lt;code&gt;arm64&lt;&#x2F;code&gt; is not available as a specified architecture but, because the application is Java, it&#x27;s a universal package.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;There was a lot of material online with discussion of people solving this same problem for different distros and architectures but were unfortunately out of date for Ubuntu Jammy Arm64. Specifically, a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;station.eciton.net&#x2F;running-unifi-controller-on-arm64-or-ppc64el.html&quot;&gt;blog post by Leif Lindholm&lt;&#x2F;a&gt; discussed a lot of the issues as well as a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;davecoutts&#x2F;5ccb403c3d90fcf9c8c4b1ea7616948d&quot;&gt;Gist by Dave Coutts&lt;&#x2F;a&gt; and subsequent discussion section.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ESP32 HomeKit Intercom</title>
        <published>2022-09-07T00:00:00+00:00</published>
        <updated>2022-09-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/esp32-homekit-intercom/"/>
        <id>https://www.nth.io/luke/projects/esp32-homekit-intercom/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/esp32-homekit-intercom/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-homekit-intercom&#x2F;esp32-intercom-components-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-homekit-intercom&#x2F;esp32-intercom-components-medium.jpeg&quot; alt=&quot;ESP32 and relay featherwing components&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;from-raspberry-pi-to-esp32&quot;&gt;From Raspberry Pi to ESP32&lt;&#x2F;h2&gt;
&lt;p&gt;In 2018 I wired a &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;&quot;&gt;Raspberry Pi Zero W into my 1970s apartment intercom&lt;&#x2F;a&gt; to make it Siri-controllable. The Pi ran HAP-NodeJS, which worked, but always felt like overkill: a full Linux computer, an npm dependency tree, and Node.js runtime just to toggle a relay and sample an ADC pin. When Espressif released their &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;espressif&#x2F;esp-homekit-sdk&quot;&gt;ESP32 HAP SDK&lt;&#x2F;a&gt; — a native C implementation of the HomeKit Accessory Protocol running on FreeRTOS — it was an obvious upgrade path. The full project source is available on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;esp-homekit-intercom&quot;&gt;src.nth.io&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-esp32&quot;&gt;Why ESP32&lt;&#x2F;h2&gt;
&lt;p&gt;The original Pi-based implementation had a few pain points:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Boot time.&lt;&#x2F;strong&gt; Linux takes 30+ seconds to come up. A doorbell event during that window is silently dropped.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Fragility.&lt;&#x2F;strong&gt; HAP-NodeJS was difficult to install correctly and the Node&#x2F;npm stack introduced a lot of surface area for things to break after an &lt;code&gt;apt upgrade&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Power.&lt;&#x2F;strong&gt; A Pi Zero W consumes around 400mA at 5V. An ESP32 at idle is under 10mA.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The ESP32 runs FreeRTOS, boots in under a second, and the entire firmware is a single compiled binary. No package manager, no interpreter, no OS updates.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;components&quot;&gt;Components&lt;&#x2F;h2&gt;
&lt;p&gt;Both boards use Adafruit&#x27;s Feather form factor, so the Relay FeatherWing stacks directly on top of the HUZZAH32 with no additional wiring between them.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Component&lt;&#x2F;th&gt;&lt;th&gt;Price&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.adafruit.com&#x2F;product&#x2F;3619&quot;&gt;Adafruit HUZZAH32 – ESP32 Feather Board w&#x2F; Stacking Headers&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;$21.95&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.adafruit.com&#x2F;product&#x2F;2895&quot;&gt;Adafruit Non-Latching Mini Relay FeatherWing&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;$7.95&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;$29.90&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-homekit-intercom&#x2F;esp32-intercom-circuit-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-homekit-intercom&#x2F;esp32-intercom-circuit-medium.jpeg&quot; alt=&quot;Assembled protoboard circuit&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-intercom-circuit&quot;&gt;The Intercom Circuit&lt;&#x2F;h2&gt;
&lt;p&gt;The building intercom runs at 52V AC (60Hz) with a 23V DC bias component shared across all units on a tier. The same two wires carry both signals simultaneously. This makes voltage sensing non-trivial.&lt;&#x2F;p&gt;
&lt;p&gt;For the &lt;strong&gt;doorbell detection&lt;&#x2F;strong&gt; circuit, I built a resistive voltage divider to bring the signal into the ESP32&#x27;s ADC range:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;R1: 100kΩ&lt;&#x2F;li&gt;
&lt;li&gt;R2: 10kΩ + 2.2kΩ&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The AC component is filtered with a 4.7µF capacitor, leaving only the DC component for the ADC to sample. When the lobby buzzer is triggered, the DC voltage drops approximately 20% — from a nominal ~23V down to roughly 18V, which maps to a 12-bit ADC reading shifting from ~3000 down into the 2300–2400 range.&lt;&#x2F;p&gt;
&lt;p&gt;For the &lt;strong&gt;door lock&lt;&#x2F;strong&gt;, a relay on GPIO 21 controls the existing solenoid circuit. The intercom panel&#x27;s lock switch still works in parallel.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gpio-architecture&quot;&gt;GPIO Architecture&lt;&#x2F;h2&gt;
&lt;p&gt;GPIO pin cannot simultaneously be used for hardware interrupts and ADC sampling. I solved this by bridging two GPIO pins to the same voltage node:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPIO 27&lt;&#x2F;strong&gt; — interrupt input (&lt;code&gt;GPIO_INTR_NEGEDGE&lt;&#x2F;code&gt;), used for fast doorbell detection&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GPIO 33&lt;&#x2F;strong&gt; — ADC1 channel 5, used for voltage verification sampling&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;When the interrupt fires on GPIO 27, a FreeRTOS task wakes up and samples GPIO 33 fifteen times at 20-tick intervals to confirm the doorbell voltage is in range before triggering the HAP event. This eliminates false positives from noise spikes on the intercom line.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#586E75, #93A1A1);font-weight: bold;&quot;&gt;void&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; bell_read_task&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#586E75, #93A1A1);font-weight: bold;&quot;&gt;void&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt;p&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;    for&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;        ulTaskNotifyTake&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;pdTRUE&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span&gt; portMAX_DELAY&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; &#x2F;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; block until ISR&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;        sample_bell_adc&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;                       &#x2F;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; confirm with ADC&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;homekit-services&quot;&gt;HomeKit Services&lt;&#x2F;h2&gt;
&lt;p&gt;The firmware exposes two HomeKit services on a single accessory:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Doorbell service&lt;&#x2F;strong&gt; — uses &lt;code&gt;hap_serv_doorbell_create()&lt;&#x2F;code&gt; with &lt;code&gt;HAP_CHAR_UUID_PROGRAMMABLE_SWITCH_EVENT&lt;&#x2F;code&gt;. When the bell rings, it fires a single-press event to iOS, which generates a notification.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Lock mechanism service&lt;&#x2F;strong&gt; — uses &lt;code&gt;hap_serv_lock_mechanism_create()&lt;&#x2F;code&gt;. A write callback handles lock&#x2F;unlock commands and triggers an auto-relock timer matching the physical spring-back behavior of the original switch.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;hap_acc_add_serv&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;intercom_accessory&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; bell_service_init&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;hap_acc_add_serv&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;intercom_accessory&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; lock_service_init&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The accessory presents itself with &lt;code&gt;HAP_CID_DOOR&lt;&#x2F;code&gt; so iOS correctly categorizes it in the Home app.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;debouncing&quot;&gt;Debouncing&lt;&#x2F;h2&gt;
&lt;p&gt;The building intercom generates a messy signal — adjacent units ring simultaneously and the AC component causes voltage flutter. After the bell fires a HAP event, a FreeRTOS timer blocks further bell events for a configurable timeout period. This prevents a single lobby press from generating a burst of notifications.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#586E75, #93A1A1);font-weight: bold;&quot;&gt;void&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; bell_rang&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;    hap_char_update_val&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;bell_current_state&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; &amp;amp;&lt;&#x2F;span&gt;&lt;span&gt;HAP_PROGRAMMABLE_SWITCH_EVENT_SINGLE_PRESS&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    is_bell_blocked &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#B58900, #B58900);&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;    xTimerReset&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span&gt;bell_block_timer_handle&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span&gt; pdFALSE&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#586E75, #93A1A1);font-weight: bold;&quot;&gt;void&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; bell_block_timer_cb&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;TimerHandle_t&lt;&#x2F;span&gt;&lt;span&gt; timer&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    is_bell_blocked &lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#B58900, #B58900);&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;partition-sizing&quot;&gt;Partition Sizing&lt;&#x2F;h2&gt;
&lt;p&gt;The HAP SDK binary is large enough that the default ESP-IDF single-app partition table doesn&#x27;t fit it. The factory partition needs to be bumped from 1MB to 1.5MB in &lt;code&gt;partitions_hap.csv&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;csv&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;nvs,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;      data,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; nvs,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;     ,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; 0x6000,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;phy_init,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt; data,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; phy,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt;     ,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; 0x1000,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;factory,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#859900, #859900);&quot;&gt;  app,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;  factory,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#93A1A1, #586E75);font-style: italic;&quot;&gt; ,&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; 0x180000,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-homekit-intercom&#x2F;esp32-intercom-installed-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;esp32-homekit-intercom&#x2F;esp32-intercom-installed-medium.jpeg&quot; alt=&quot;ESP32 installed inside the intercom panel&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;result&quot;&gt;Result&lt;&#x2F;h2&gt;
&lt;p&gt;The ESP32 intercom boots in under a second, uses a fraction of the power of the Pi, and has been running without intervention since installation. iOS doorbell notifications arrive within a second of the lobby button being pressed. The lock control works reliably via Siri, the Home app, and automations.&lt;&#x2F;p&gt;
&lt;p&gt;The main trade-off versus the Pi version is the development workflow — cross-compiling C with the ESP-IDF toolchain is more involved than editing a Python file and restarting a systemd service. But for a device that runs 24&#x2F;7 and should never need touching, the embedded approach is clearly the right one.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;..&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-3-large.jpeg&quot;&gt;&lt;img src=&quot;..&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-3-medium.jpeg&quot; alt=&quot;In-unit intercom panel with ESP32 installed&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;future-work&quot;&gt;Future Work&lt;&#x2F;h2&gt;
&lt;p&gt;The &quot;talk&quot; and &quot;listen&quot; functions of the intercom panel remain unwired. Two-way audio over HomeKit is a possible next step, though the HAP audio spec is considerably more complex than doorbell events.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Haskell.Social Fediverse</title>
        <published>2021-07-25T00:00:00+00:00</published>
        <updated>2021-07-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/haskell-social-fediverse/"/>
        <id>https://www.nth.io/luke/projects/haskell-social-fediverse/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/haskell-social-fediverse/">&lt;p&gt;&lt;strong&gt;Communities need associative hosting&lt;&#x2F;strong&gt;. Following Freenode&#x27;s well documented &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.kline.sh&#x2F;&quot;&gt;collapse&lt;&#x2F;a&gt;, we were motivated to set up &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;haskell.social&quot;&gt;haskell.social&lt;&#x2F;a&gt;, an experimental web, matrix, and pleroma set of services to give the haskell-adjacent community an alternative to centralized services like Meetup, Slack&#x2F;IRC&#x2F;Discord, and Twitter, etc.&lt;&#x2F;p&gt;
&lt;p&gt;Federation enables people to bring their own identities and connect with other communities online. A recent &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dataswamp.org&#x2F;~solene&#x2F;2021-07-23-why-selfhosting-is-important.html&quot;&gt;blog post&lt;&#x2F;a&gt; highlights the importance of self and associative hosting.&lt;&#x2F;p&gt;
&lt;p&gt;Matrix and pleroma certainly have their downsides and IRC and Twitter are pretty hard to beat, but they have their upsides as well. Indeed, much of the haskell community is already supported by the new &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;libera.chat&#x2F;&quot;&gt;Libera Chat IRC&lt;&#x2F;a&gt;. &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;haskell.social&quot;&gt;haskell.social&lt;&#x2F;a&gt; is an alternative, enabling experimentation with newer federated approaches to online communities.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Raspberry Pi HomeLab</title>
        <published>2020-08-01T00:00:00+00:00</published>
        <updated>2020-08-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/raspberrypi-homelab/"/>
        <id>https://www.nth.io/luke/projects/raspberrypi-homelab/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/raspberrypi-homelab/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homelab&#x2F;homelab-large.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homelab&#x2F;homelab-medium.png&quot; alt=&quot;HomeLab&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;This was my first homelab. I later rebuilt it into a &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;homelab-mini-rack&#x2F;&quot;&gt;Mini Rack&lt;&#x2F;a&gt;, then upgraded it in &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;homelab-mini-rack-v2&#x2F;&quot;&gt;Mini Rack v2&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;motivation&quot;&gt;Motivation&lt;&#x2F;h2&gt;
&lt;p&gt;I became more interested in personal privacy after my Roku started spying on what we were watching outside the Roku itself, our mesh WiFi router switched to a subscription model for &quot;AI&quot; and &quot;cloud&quot; features, and our smart home switches required access to &quot;the cloud&quot; just to turn on lights. TVs, WiFi routers, and smart home devices are all driving prices down by supplementing hardware sales revenue with personal data sales.&lt;&#x2F;p&gt;
&lt;p&gt;On top of that, after &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;&quot;&gt;creating a custom smart lock&lt;&#x2F;a&gt;, I saw first hand how Google and Amazon&#x27;s smart home infrastructures are built around selling cloud services and capturing my personal data, while Apple HomeKit is designed to work without any internet access at all.&lt;&#x2F;p&gt;
&lt;p&gt;Given these consideration, I wanted a more robust router and firewall between my home network and the internet. I wanted to be able to completely block smart home devices from accessing the internet. And I wanted to do everything as cheaply as possible while maintaining compute resource (CPU, RAM, disk) separation between self-hosted services.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;1x $59 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;Ubiquiti-Networks-ER-X-Router&#x2F;dp&#x2F;B0144R449W&#x2F;ref=sr_1_2?dchild=1&amp;amp;keywords=edge+router+x&amp;amp;qid=1596318469&amp;amp;s=electronics&amp;amp;sr=1-2&quot;&gt;Ubiquiti EdgeRouter X&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;1x $70 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;TP-LINK-8-Port-Gigabit-Switch-4-Port&#x2F;dp&#x2F;B01BW0AD1W&#x2F;ref=sr_1_6?dchild=1&amp;amp;keywords=tp-link+8+poe+switch&amp;amp;qid=1596318548&amp;amp;s=electronics&amp;amp;sr=1-6&quot;&gt;TP-Link 8 Port PoE Switch&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;4x $55 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;chicagodist.com&#x2F;products&#x2F;raspberry-pi-4-model-b-4gb&quot;&gt;Raspberry Pi 4 Model B 4GB&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;4x $20 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;chicagodist.com&#x2F;products&#x2F;raspberry-pi-poe-hat&quot;&gt;Raspberry Pi PoE HAT&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;3x $48 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;Seagate-Portable-External-Hard-Drive&#x2F;dp&#x2F;B07CRG7BBH&#x2F;ref=sr_1_3?dchild=1&amp;amp;keywords=usb+hard+drive&amp;amp;qid=1596318600&amp;amp;s=electronics&amp;amp;sr=1-3&quot;&gt;Seagate 1TB USB HDD&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Total: $573&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The Ubiquiti EdgerRouter X is the router and firewall while the mesh WiFi is in &quot;bridge&quot; mode, effectively operating as a switch. IP addresses are assigned in ranges and firewall settings are used to block all devices from the internet except those that need it (Apple TV, Laptops, Phones, etc.).&lt;&#x2F;p&gt;
&lt;p&gt;Four Raspberry Pis host all self-hosted services. &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.nth.io&quot;&gt;Public services&lt;&#x2F;a&gt; like code hosting, federated social networks, and a bitcoin node. And private services like DNS-based ad and tracker blocking. (After using Pi-Hole for a while, I switched to AdGuard Home, which is just simpler and easier to maintain.)&lt;&#x2F;p&gt;
&lt;p&gt;Finally, power over Ethernet (PoE) with a PoE switch is used to reduce the cords to the Raspberry Pis.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;custom-racking&quot;&gt;Custom Racking&lt;&#x2F;h2&gt;
&lt;p&gt;A downside of not using a standard rack-mounted host is the non-standard form factors of the Raspberry Pis and hard drives etc.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homelab&#x2F;assembly-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homelab&#x2F;assembly-medium.jpeg&quot; alt=&quot;Assembling the Raspberry Pi rack mount&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;To handle this, I &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.thingiverse.com&#x2F;thing:4078710&quot;&gt;3D printed a Raspberry Pi 2U rack mount&lt;&#x2F;a&gt;. It&#x27;s not used in a rack configuration but it&#x27;s actually just a great way to have easy, uniform, and modular access to the Pis.&lt;&#x2F;p&gt;
&lt;p&gt;Hardware was purchased from McMaster-Carr.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1x $3.33 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mcmaster.com&#x2F;catalog&#x2F;92290A062&quot;&gt;Super-Corrosion-Resistant 316 Stainless Steel Socket Head Screw, M2.5 x 0.45 mm Thread, 12 mm Long&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;2x $5.66 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mcmaster.com&#x2F;catalog&#x2F;90024A224&quot;&gt;18-8 Stainless Steel Threaded Rod, M5 x 0.8 mm Thread Size, 300 mm Long&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;1x $3.79 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mcmaster.com&#x2F;catalog&#x2F;93935A330&quot;&gt;Super-Corrosion-Resistant 316 Stainless Steel Thin Hex Nut, M5 x 0.8 mm Thread, 2.7 mm High&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Total: $18.44&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For the hard drives, I designed and 3D printed a custom stand.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;configuration-management&quot;&gt;Configuration Management&lt;&#x2F;h2&gt;
&lt;p&gt;Host configuration is managed with &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;ansible.com&quot;&gt;Ansible&lt;&#x2F;a&gt;. The roles are written to be minimally invasive and optimized for low maintenance.&lt;&#x2F;p&gt;
&lt;p&gt;The Ansible roles are &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;ansible-roles&#x2F;tree&quot;&gt;open source&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;layout&quot;&gt;Layout&lt;&#x2F;h2&gt;
&lt;p&gt;For clarity, my specific Raspberry Pi Ansible playbook in provided below:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; h&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;osts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;pis&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;  r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;oles&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;pi-base&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; a&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;pt-cacher&#x2F;client&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;rometheus&#x2F;rpi-client&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; h&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;osts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; a&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;dmin.local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;  r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;oles&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; a&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;dguard-home&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; a&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;pt-cacher&#x2F;server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;rometheus&#x2F;server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; h&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;osts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; b&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;tc.local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;  r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;oles&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; b&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;lock-device&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; b&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;itcoind&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; l&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;nd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; b&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;itcoind-prometheus-exporter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; h&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;osts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; m&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;edia.local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;  r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;oles&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; b&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;lock-device&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;lex&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; t&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;ransmission&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; h&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;omebridge&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; m&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;inecraft&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; n&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;ginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt; h&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;osts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; w&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;eb.local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;  r&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#268BD2, #268BD2);&quot;&gt;oles&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; b&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;lock-device&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;ostgresql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;leroma&#x2F;aws-s3-backup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;leroma&#x2F;otp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; w&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;ritefreely&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; m&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;ercurial&#x2F;aws-s3-backup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; m&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;ercurial&#x2F;web&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; o&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;ragono&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; p&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;rosody&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt; n&lt;&#x2F;span&gt;&lt;span style=&quot;color: light-dark(#2AA198, #2AA198);&quot;&gt;ginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;Using a hardwired router as the articulation point between the internet and the rest of a home network is a great way to get privacy, security, and self-hosting without really investing much.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Mercurial + WSGI + Nginx</title>
        <published>2020-07-02T00:00:00+00:00</published>
        <updated>2020-07-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/mercurial-wsgi-nginx/"/>
        <id>https://www.nth.io/luke/projects/mercurial-wsgi-nginx/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/mercurial-wsgi-nginx/">&lt;p&gt;I wanted to host my code without a lot of extra infrastructure, apps, and metadata to operate and maintain. I just wanted a simple way to share my repos. Contributors can easily send patches via email without a whole new login and user system. I saw &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;hg.prosody.im&quot;&gt;&lt;code&gt;hg.prosody.im&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; and was inspired by it&#x27;s simplicity.&lt;&#x2F;p&gt;
&lt;p&gt;Given the overwhelming popularity of Git and full-service solutions like GitLab, there are only a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.x64architecture.com&#x2F;posts&#x2F;running-hgweb-with-nginx-and-uwsgi&#x2F;&quot;&gt;couple&lt;&#x2F;a&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@nicdoye&#x2F;installing-mercurial-with-nginx-and-uwsgi-625dfced9b27&quot;&gt;helpful&lt;&#x2F;a&gt;, but slightly outdated, guides for Mercurial + Nginx. It was enough to get me up and running, though, and I wanted to document how I got things working.&lt;&#x2F;p&gt;
&lt;p&gt;Mercurial can run a fork of cgit via WSGI and hosted by Nginx.  Check &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;ansible-roles&#x2F;tree&#x2F;mercurial&#x2F;web&quot;&gt;the repo&lt;&#x2F;a&gt; for the most up-to-date version of these configs in an Ansible role. The setup is as follows for my code hosted at the subdomain &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&quot;&gt;&lt;code&gt;src.nth.io&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Nginx -&amp;gt; UNIX domain socket -&amp;gt; WSGI -&amp;gt; Mercurial repos&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;dependencies&quot;&gt;Dependencies&lt;&#x2F;h2&gt;
&lt;p&gt;This particular setup is running Ubuntu 20.04.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mercurial&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nginx&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;uwsgi&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;uwsgi-plugin-python3&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;python3-pygments&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;nginx-config&quot;&gt;Nginx Config&lt;&#x2F;h2&gt;
&lt;p&gt;Path: &lt;code&gt;&#x2F;etc&#x2F;nginx&#x2F;sites-enabled&#x2F;src.nth.io.conf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;server {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    listen 80;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    listen [::]:80;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    server_name src.nth.io;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    return 301 https:&#x2F;&#x2F;$host$request_uri;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;server {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    listen 443 ssl http2;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    listen [::]:443 ssl ipv6only=on;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    server_name src.nth.io;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ssl_certificate &#x2F;etc&#x2F;letsencrypt&#x2F;live&#x2F;src.nth.io&#x2F;fullchain.pem;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ssl_certificate_key &#x2F;etc&#x2F;letsencrypt&#x2F;live&#x2F;src.nth.io&#x2F;privkey.pem;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    include &#x2F;etc&#x2F;letsencrypt&#x2F;options-ssl-nginx.conf;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ssl_dhparam &#x2F;etc&#x2F;letsencrypt&#x2F;ssl-dhparams.pem;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ssl_stapling on;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ssl_stapling_verify on;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    location &#x2F; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        include     uwsgi_params;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        uwsgi_param REMOTE_PORT     $remote_port;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        uwsgi_param SERVER_PORT     $server_port;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        uwsgi_param SERVER_PROTOCOL $server_protocol;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        uwsgi_param UWSGI_SCHEME    $scheme;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        uwsgi_param SCRIPT_NAME     &#x2F;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        uwsgi_param AUTH_USER       $remote_user;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        uwsgi_param REMOTE_USER     $remote_user;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        uwsgi_pass  unix:&#x2F;run&#x2F;uwsgi&#x2F;app&#x2F;hgweb&#x2F;socket;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;uwsgi-config&quot;&gt;UWSGI Config&lt;&#x2F;h2&gt;
&lt;p&gt;Path: &lt;code&gt;&#x2F;etc&#x2F;uwsgi&#x2F;apps-enabled&#x2F;hgweb.ini&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Note the &lt;code&gt;socket&lt;&#x2F;code&gt; key matches the &lt;code&gt;uwsgi_pass&lt;&#x2F;code&gt; parameter from the Nginx config above.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[uwsgi]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;processes = 2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;max-requests = 10240&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;max-requests-delta = 1024&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;max-worker-lifetime = 604800&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;socket = unix:&#x2F;run&#x2F;uwsgi&#x2F;app&#x2F;hgweb&#x2F;socket&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;chdir = &#x2F;var&#x2F;www&#x2F;src.nth.io&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;wsgi-file = hgweb.wsgi&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;uid = www-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;gid = www-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;plugins = python3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The max request lines allow the uwsgi server to restart the workers and mitigate memory leaks in the HG Web script without incurring downtime.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hgweb-script&quot;&gt;HgWeb Script&lt;&#x2F;h2&gt;
&lt;p&gt;Path: &lt;code&gt;&#x2F;var&#x2F;www&#x2F;src.nth.io&#x2F;hgweb.wsgi&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;config = &amp;quot;&#x2F;var&#x2F;www&#x2F;src.nth.io&#x2F;hgweb.config&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;from mercurial import demandimport; demandimport.enable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;from mercurial.hgweb import hgweb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;application = hgweb(config.encode())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;hgweb-config&quot;&gt;HgWeb Config&lt;&#x2F;h2&gt;
&lt;p&gt;Path: &lt;code&gt;&#x2F;var&#x2F;www&#x2F;src.nth.io&#x2F;hgweb.config&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Note the actual Mercurial repo path at &lt;code&gt;&#x2F;var&#x2F;hg&#x2F;repos&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[paths]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F; = &#x2F;var&#x2F;hg&#x2F;repos&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[web]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;deny_push = *&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;allow_archive = gz bz2 zip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;encoding = UTF-8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;style = gitweb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[extensions]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;hgext.highlight =&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;running&quot;&gt;Running&lt;&#x2F;h2&gt;
&lt;p&gt;With this all set up, &lt;code&gt;sudo systemctl start uwsgi&lt;&#x2F;code&gt; and &lt;code&gt;sudo systemctl start nginx&lt;&#x2F;code&gt;. If something doesn&#x27;t work, check the logs. Ensure the UWSGI user and group allows for read access to the repo directory.&lt;&#x2F;p&gt;
&lt;p&gt;Both the Nginx and uWSGI Ubuntu packages are set to actually have the configs at &lt;code&gt;{sites|apps}-available&lt;&#x2F;code&gt; and then symlinked to the respective &lt;code&gt;{sites|apps}-enabled&lt;&#x2F;code&gt; directories, as documented on their websites.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;theming&quot;&gt;Theming&lt;&#x2F;h2&gt;
&lt;p&gt;Check out the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mercurial-scm.org&#x2F;wiki&#x2F;Theming&quot;&gt;Mercurial docs&lt;&#x2F;a&gt; for instructions on theming the website.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>3D Printed Intercom Panel</title>
        <published>2019-05-01T00:00:00+00:00</published>
        <updated>2019-05-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/3d-printed-intercom-panel/"/>
        <id>https://www.nth.io/luke/projects/3d-printed-intercom-panel/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/3d-printed-intercom-panel/">&lt;p&gt;After &lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;&quot;&gt;wiring a Raspberry Pi Zero W into my 1970s apartment
intercom&lt;&#x2F;a&gt; to make it Siri-controllable, I needed a way to mount
the Pi inside the panel. My first solution was a small piece of wood screwed into some unused screw terminals. It was
nonconductive, quick, and easy enough to start with. But cutting a precise piece to fit in a few millimeters of
clearance, inside an apartment with no workshop, was difficult. The wood grain made clean cuts even harder. It worked,
but it always felt like a prototype. That got me thinking about 3D printing, a process I had zero experience with.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;original-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;original-medium.jpeg&quot; alt=&quot;The original 1970s intercom panel&quot; &#x2F;&gt;&lt;&#x2F;a&gt;
&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;final-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;final-medium.jpeg&quot; alt=&quot;Final panel reprinted in grey with switch caps installed&quot; &#x2F;&gt;&lt;&#x2F;a&gt;
&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;printed-mount-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;printed-mount-medium.jpeg&quot; alt=&quot;3D printed mount replacing the wood, fitted into the original panel&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;getting-started-with-3d-printing&quot;&gt;Getting Started with 3D Printing&lt;&#x2F;h2&gt;
&lt;p&gt;I bought the cheapest and smallest printer I could find, a Monoprice, specifically to remake that wood mount in
plastic. I was never able to get it working properly and eventually upgraded to a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.prusa3d.com&#x2F;category&#x2F;original-prusa-i3-mk3s&#x2F;&quot;&gt;Prusa i3
MK3&lt;&#x2F;a&gt; kit. My first task was learning to model. Blender was too
freeform for precise measurements. Fusion 360 turned out to be the right tool: parametric, measurement-driven, and
well-covered on YouTube.&lt;&#x2F;p&gt;
&lt;p&gt;The mount itself was a good first project. A 3D printer making its own replacement parts sounds janky, but it&#x27;s actually
one of its best features: you can iterate and improve your own setup as you go. The opposite of planned obsolescence.&lt;&#x2F;p&gt;
&lt;p&gt;With that success I started thinking: why not remake the whole nasty-looking injection-molded panel? I started by
printing and modeling other simpler things around the house. The full panel became my most challenging 3D model yet.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;remaking-the-panel&quot;&gt;Remaking the Panel&lt;&#x2F;h2&gt;
&lt;p&gt;The hardest part was the inner wall cutout. The original panel was mortised into the wall, and the recess dimensions
were practically impossible to measure accurately. Calipers couldn&#x27;t fit into the corners. I made many prototypes until
I found the right shape. 3D printing&#x27;s fast iteration cycle was exactly what this required.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;fit-check-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;fit-check-medium.jpeg&quot; alt=&quot;Sizing and fit check on an early prototype&quot; &#x2F;&gt;&lt;&#x2F;a&gt;
&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;wall-fit-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;wall-fit-medium.jpeg&quot; alt=&quot;Test fitting the prototype in the wall cutout&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;installation&quot;&gt;Installation&lt;&#x2F;h2&gt;
&lt;p&gt;With a solid prototype shape, I mounted the Raspberry Pi and all the hardware into the panel and started the install.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;first-install-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;first-install-medium.jpeg&quot; alt=&quot;Fully mounted Pi getting installed as the first full prototype&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The three stages of the design, from rough mount to panel prototype to final panel, made the progression clear.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;three-stages-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;three-stages-medium.jpeg&quot; alt=&quot;Three stages of the panel design evolution&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;One problem I hadn&#x27;t anticipated: whoever originally installed the panel had crudely chiseled the wall recess by hand
and it wasn&#x27;t remotely square. Getting the fully assembled Pi to fit required chiseling out even more material to make
room.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;chiseling-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;3d-printed-intercom-panel&#x2F;chiseling-medium.jpeg&quot; alt=&quot;Fitting issues — the wall recess needed more chiseling to accommodate the mounted Pi&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;final-design&quot;&gt;Final Design&lt;&#x2F;h2&gt;
&lt;p&gt;The final panel hides the front screw holes (my wife&#x27;s idea) and echoes the original vertical slat pattern so it
doesn&#x27;t look out of place on the wall. I reprinted it in grey and added switch caps for a cleaner finish.&lt;&#x2F;p&gt;
&lt;p&gt;The print files are available on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;3553-lobby-door-buzzer-panel&quot;&gt;Printables&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Raspberry Pi HomeKit Door Lock</title>
        <published>2018-01-19T00:00:00+00:00</published>
        <updated>2018-01-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            Luke Hoersten
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.nth.io/luke/projects/raspberrypi-homekit-door-lock/"/>
        <id>https://www.nth.io/luke/projects/raspberrypi-homekit-door-lock/</id>
        
        <content type="html" xml:base="https://www.nth.io/luke/projects/raspberrypi-homekit-door-lock/">&lt;h2 id=&quot;apple-homekit-with-raspberry-pi&quot;&gt;Apple HomeKit with Raspberry Pi&lt;&#x2F;h2&gt;
&lt;p&gt;I made my 1970s (?) apartment intercom system Siri controlled and internet accessible with a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.raspberrypi.org&#x2F;products&#x2F;raspberry-pi-zero-w&#x2F;&quot;&gt;Raspberry Pi&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;shop.pimoroni.com&#x2F;products&#x2F;automation-hat-mini?variant=31478878077011&quot;&gt;Pimoroni Automation pHAT&lt;&#x2F;a&gt;. The full project code is available on a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;homekit-door&quot;&gt;src repo&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-1-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-1-medium.jpeg&quot; alt=&quot;Final build mounted and ready to be reinstalled&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;&#x2F;h2&gt;
&lt;p&gt;My apartment building lobby door lock is controlled by an intercom panel in my unit. To unlock the door, the switch completes a circuit to an electromagnet in the lobby. And for someone to ring my unit&#x27;s doorbell, a switch in the lobby completes a circuit to power a really annoying buzzer in my unit. I can also talk and listen to a speaker in the lobby.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-3-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-3-medium.jpeg&quot; alt=&quot;In-Unit Intercom Panel&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If I&#x27;m at work I can&#x27;t get a package delivered or receive guests. Also, I&#x27;d been looking for a fun Raspberry Pi project for a few years and finally it struck me: use the Raspberry Pi to control these switches.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;intercom-layout&quot;&gt;Intercom Layout&lt;&#x2F;h2&gt;
&lt;p&gt;To start, I took the panel off the wall to see how it worked and used a voltmeter to test what all the wires mapped to and at what voltages. I only cared about two functions: unlocking the lobby door and receiving a doorbell signal. There are five wires and four functions, leaving the fifth wire as ground, all at 52V:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Unlock door&lt;&#x2F;li&gt;
&lt;li&gt;Listen for doorbell&lt;&#x2F;li&gt;
&lt;li&gt;Talk to lobby speaker&lt;&#x2F;li&gt;
&lt;li&gt;Listen to lobby speaker&lt;&#x2F;li&gt;
&lt;li&gt;Ground&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-5-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-5-medium.jpeg&quot; alt=&quot;Back of Intercom Panel flipped Upside Down&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;raspberry-pi&quot;&gt;Raspberry Pi&lt;&#x2F;h2&gt;
&lt;p&gt;When selecting a Raspberry Pi or Arduino to control these circuits, I went with the Raspberry Pi because it&#x27;s a full linux computer and seemed the most flexible. I picked the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.raspberrypi.org&#x2F;products&#x2F;raspberry-pi-zero-w&#x2F;&quot;&gt;Raspberry Pi Zero W&lt;&#x2F;a&gt; for the smaller form factor and builtin WIFI. SSH access is provided via ethernet emulation over one of it&#x27;s USB ports.&lt;&#x2F;p&gt;
&lt;p&gt;I bought the following for one day shipping from Amazon:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;amzn.to&#x2F;2DiTsVF&quot;&gt;CanaKit Raspberry Pi Zero W (Wireless) with Official Case and Power Supply&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;amzn.to&#x2F;2DleN0F&quot;&gt;SanDisk Mobile Class4 MicroSDHC Flash Memory Card- SDSDQM-B35A Without Adapter 16GB&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;amzn.to&#x2F;2DPuKxl&quot;&gt;DIKAVS Break-away 0.1&quot; 2x20-pin Strip Dual Male Header for Raspberry Pi Zero GPIO (Pack of 10)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;raspberry-pi-add-on-board&quot;&gt;Raspberry Pi Add-On Board&lt;&#x2F;h3&gt;
&lt;p&gt;Now I needed a way to switch and read the intercom circuits. &quot;HAT&quot; is the name of a full-sized add-on board and &quot;pHAT&quot; is the name of the Pi Zero form factor add-on board. &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pinout.xyz&#x2F;boards&quot;&gt;Pinout&lt;&#x2F;a&gt; is a great resource with a fairly comprehensive list of pHAT boards. I figured I needed a &quot;relay&quot; to unlock the door and a &quot;analog to digital converter&quot; to read the doorbell signal. One board fit the requirements perfectly:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;shop.pimoroni.com&#x2F;products&#x2F;automation-hat-mini?variant=31478878077011&quot;&gt;Pimoroni Automation pHAT&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-5-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-5-medium.jpeg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;raspbian-setup&quot;&gt;Raspbian Setup&lt;&#x2F;h3&gt;
&lt;p&gt;With the hypothesis of how all this would fit together, I bought the components and started on the software.&lt;&#x2F;p&gt;
&lt;p&gt;I followed two guides to set up a headless Raspian with WIFI enabled and SSH over USB emulated ethernet:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.thepolyglotdeveloper.com&#x2F;2016&#x2F;02&#x2F;use-your-raspberry-pi-as-a-headless-system-without-a-monitor&#x2F;&quot;&gt;Use Your Raspberry Pi As A Headless System Without A Monitor&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.thepolyglotdeveloper.com&#x2F;2016&#x2F;06&#x2F;connect-raspberry-pi-zero-usb-cable-ssh&#x2F;&quot;&gt;Connect To A Raspberry Pi Zero With A USB Cable And SSH&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;writing-the-software&quot;&gt;Writing the Software&lt;&#x2F;h2&gt;
&lt;p&gt;With the hardware and OS sorted, I started experimenting with controlling the Automation pHAT using their &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pimoroni&#x2F;automation-hat&quot;&gt;online docs&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pimoroni&#x2F;automation-hat&#x2F;blob&#x2F;master&#x2F;documentation&#x2F;REFERENCE.md&quot;&gt;Python function reference&lt;&#x2F;a&gt;. I used LEDs and resistors to experiment with the functionality in isolation from the intercom.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;phat-python-server&quot;&gt;pHAT Python Server&lt;&#x2F;h2&gt;
&lt;p&gt;The pHAT Python API access must be single threaded but because I need to poll for the doorbell and for commands to unlock the door in parallel, I made a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;homekit-door&#x2F;tree&#x2F;npmjs-package&#x2F;python&#x2F;doord.py&quot;&gt;multithreaded Python server&lt;&#x2F;a&gt; with a message queue for commands using &lt;code&gt;stdio&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;doorbell&quot;&gt;Doorbell&lt;&#x2F;h3&gt;
&lt;p&gt;The doorbell goes into the Analog-Digital-Converter (ADC) and a resistive divider is used to bring the 52V into the requisite 24V range.  The ADC digital reading of 6.0--6.3 is when the doorbell is ringing for whatever reason. Everything else is noise. Here&#x27;s some example output from a one second polling Python test:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color-scheme: light dark; color: light-dark(#657B83, #839496); background-color: light-dark(#FDF6E3, #002B36);&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.65&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.65&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.62&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.62&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;6.08  &amp;lt;-- doorbell starts ringing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;6.18&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;6.19&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;6.18&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;6.19&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.62  &amp;lt;-- doorbell stops ringing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.62&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.62&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.63&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;7.63&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;door-lock&quot;&gt;Door Lock&lt;&#x2F;h3&gt;
&lt;p&gt;The door lock is controlled by the single relay available on the pHAT which doesn&#x27;t require bringing the voltage down with a resistor.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;apple-homekit-integration&quot;&gt;Apple HomeKit Integration&lt;&#x2F;h2&gt;
&lt;p&gt;I then created a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;KhaosT&#x2F;HAP-NodeJS&quot;&gt;HAP-NodeJS&lt;&#x2F;a&gt; accessory to expose the Door Python server to Apple HomeKit Protocol (HAP) for Siri and Home App control. I use &lt;code&gt;systemd&lt;&#x2F;code&gt; to keep the HAP server running.  I also use the HAP accessory to timeout the door being unlocked and relock it. This is to mimic the behavior of the physical switches which spring back to neutral (locked) when not being held.&lt;&#x2F;p&gt;
&lt;p&gt;Representing a door lock with &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;homekit-door&#x2F;tree&#x2F;npmjs-package&#x2F;src&#x2F;door.js&quot;&gt;HAP-NodeJS in HomeKit&lt;&#x2F;a&gt; was easy but getting the doorbell event to work was very fragile. Now I get iOS notifications when the doorbell rings!&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-4-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-4-medium.jpeg&quot; alt=&quot;Doorbell Ringing iOS Notification&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It was also extremely difficult getting NodeJS and all the dependencies installed correctly. Once I got everything installed and working, I wrote &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;homekit-door&#x2F;tree&#x2F;roles&quot;&gt;Ansible roles&lt;&#x2F;a&gt; to codify exactly what is required.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;See the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;src.nth.io&#x2F;luke&#x2F;homekit-door&quot;&gt;src repo&lt;&#x2F;a&gt; for the full install layout.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-6-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-6-medium.jpeg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;final-build&quot;&gt;Final Build&lt;&#x2F;h2&gt;
&lt;p&gt;I was able to attach the final buildout to the preexisting circuit board screw terminals so the intercom panel still works as before but with the addition of Siri integration! Siri and Apple HomeKit turned out to be a huge win. It meant I didn&#x27;t need to run any external servers to expose my services to the internet, write any mobile apps, or do anything special to integrate with Siri. Alexa was much more complex and I ended up removing that support.&lt;&#x2F;p&gt;
&lt;p&gt;I was able to use my Apple TV to bridge all of my HomeKit devices, including my custom Lobby Door, to my iOS devices through the internet.  No more missed packages and a fun project over the holidays.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-2-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-2-medium.jpeg&quot; alt=&quot;Final Build Hanging from the Wall&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-0-large.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;www.nth.io&#x2F;luke&#x2F;projects&#x2F;raspberrypi-homekit-door-lock&#x2F;rpi-door-lock-0-medium.jpeg&quot; alt=&quot;Final build mounted on wood&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;future-tasks&quot;&gt;Future Tasks&lt;&#x2F;h2&gt;
&lt;p&gt;Right now the Automation pHAT must poll for doorbell events. I believe there is a way to use the Raspberry Pi GPIO pins to create an event-driven approach.&lt;&#x2F;p&gt;
&lt;p&gt;Perhaps I&#x27;d also like to attempt to get the &quot;talk&quot; and &quot;listen&quot; functionality working with an additional audio-focused pHAT.&lt;&#x2F;p&gt;
&lt;p&gt;HomeKit supports BTLE and I believe the Raspberry Pi Zero W does as well. It would be nice to use the lighter-weight protocol instead of WiFi.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
