<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  
  <title>Matt Steele</title>
  <subtitle>The personal blog of Matt Steele</subtitle>
  <link href="https://www.steele.blue/atom.xml" rel="self" />
  <link href="https://www.steele.blue/" />
  <updated>2026-04-10T00:00:00Z</updated>
  <id>https://www.steele.blue/</id>
  <author>
    <name>Matt Steele</name>
    <email>matt@steele.blue</email>
  </author>
  <entry>
    <title>My house alerts me when my cat climbs into the ceiling</title>
    <link href="https://www.steele.blue/ceiling-cat-presence-detection/" />
    <updated>2026-04-10T00:00:00Z</updated>
    <id>https://www.steele.blue/ceiling-cat-presence-detection/</id>
    <content type="html">&lt;p&gt;We have an orange cat who loves to climb into our basement&#39;s drop ceiling and wander around.
It&#39;s especially adorable when she hangs out in an alcove and surveys her domain.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/cK4kjfUUxy-600.jpeg&quot; alt=&quot;An orange cat standing proudly in a crawlspace of a drop ceiling&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; srcset=&quot;https://www.steele.blue/img/cK4kjfUUxy-600.jpeg 600w, https://www.steele.blue/img/cK4kjfUUxy-1000.jpeg 1000w, https://www.steele.blue/img/cK4kjfUUxy-3024.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;This does not happen on any predictable schedule, as per the whims of a cat. But I didn&#39;t want to miss out on any chances to see her perched up there.&lt;/p&gt;
&lt;p&gt;So, I wired some cheap IoT devices together to notify us when she hops into the ceiling.&lt;/p&gt;
&lt;video controls=&quot;&quot; preload=&quot;none&quot; muted=&quot;true&quot; poster=&quot;https://www.steele.blue/images/cat-light-sidebyside-snap.png&quot;&gt;
  &lt;source src=&quot;https://www.steele.blue/videos/cat-light-sidebyside.mp4&quot; type=&quot;video/mp4&quot;&gt;
&lt;/video&gt;
&lt;h2&gt;The workflow&lt;/h2&gt;
&lt;p&gt;The goal of the project was to turn on a light in the living room any time a cat is detected in the drop ceiling alcove.&lt;/p&gt;
&lt;p&gt;To do this, I used a few Internet of Things devices I had on-hand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A cheap Wi-Fi &amp;quot;security&amp;quot; camera (Wyze)&lt;/li&gt;
&lt;li&gt;An even cheaper Wi-Fi RGB light (Merkury)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Out of the box these devices don&#39;t talk to each other, and are no longer supported by their manufacturers.
And I wouldn&#39;t want to rely on various cloud vendors for a mission-critical workload like this, so this was a perfect job for &lt;a href=&quot;https://www.home-assistant.io/&quot;&gt;Home Assistant&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&#39;ve used Home Assistant in the past for &lt;a href=&quot;https://www.steele.blue/esp8266-chevy-bolt-fob-homeassistant&quot;&gt;similarly dubious projects&lt;/a&gt;, so I had some familiarity with its capabilities. It&#39;s got fairly extensive automation capabilities, where a change in one device&#39;s state can trigger a workflow to modify other devices, so this seemed very much in its wheelhouse.&lt;/p&gt;
&lt;h2&gt;The devices&lt;/h2&gt;
&lt;p&gt;Integrating the light wasn&#39;t too much of a challenge, though it took some sleuthing to discover that the device used Tuya firmware, which had a straightforward integration.
I was hoping to be able to modify it to not require connections to Tuya&#39;s cloud servers via a tool like &lt;a href=&quot;https://github.com/tuya-cloudcutter/tuya-cloudcutter/&quot;&gt;tuya-cloudcutter&lt;/a&gt;, but this wasn&#39;t available as a predefined, easily hackable device, so it became a project for a later date.&lt;/p&gt;
&lt;p&gt;Adding camera feeds was done by integrating the open-source &lt;a href=&quot;https://frigate.video/&quot;&gt;Frigate NVR&lt;/a&gt;. As per &lt;a href=&quot;https://docs.frigate.video/configuration/object_detectors/&quot;&gt;Frigate&#39;s docs&lt;/a&gt; I set it up for object detection, for both people and cats:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;ffmpeg&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;  hwaccel_args&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;preset-vaapi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;objects&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;  track&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;person&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;cat&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;detectors&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;  ov_0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    type&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;openvino&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    device&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;GPU&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;cameras&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;  wyze_ceiling&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    enabled&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    ffmpeg&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;      inputs&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;rtsp://id:password@192.168.200.4/live&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;          roles&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;            - &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;detect&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    detect&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;      enabled&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;      width&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1280&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;      height&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;720&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/hLz6JpO8OG-600.png&quot; alt=&quot;A screenshot of Frigate, showing a matrix of cats from object detection&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2086&quot; height=&quot;1282&quot; srcset=&quot;https://www.steele.blue/img/hLz6JpO8OG-600.png 600w, https://www.steele.blue/img/hLz6JpO8OG-1000.png 1000w, https://www.steele.blue/img/hLz6JpO8OG-2086.png 2086w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Frigate has a &lt;a href=&quot;https://docs.frigate.video/integrations/home-assistant/&quot;&gt;Home Assistant plugin&lt;/a&gt; that works quite well, connecting to the same MQTT broker I was already running for other IoT devices.&lt;/p&gt;
&lt;p&gt;Hooking this into Home Assistant gave me an few data streams I can key off, like any other sensor.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/HZxSNG7wmu-600.png&quot; alt=&quot;A screenshot of a Home Assistant history panel, showing times a cat has occupied the frame of the ceiling&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;832&quot; height=&quot;690&quot; srcset=&quot;https://www.steele.blue/img/HZxSNG7wmu-600.png 600w, https://www.steele.blue/img/HZxSNG7wmu-832.png 832w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Integration&lt;/h2&gt;
&lt;p&gt;Using the automation tools built into Home Assistant, I can toggle the light pretty easily:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/VQPD0M_2Dj-600.png&quot; alt=&quot;Home assistant workflow UI&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1033&quot; height=&quot;882&quot; srcset=&quot;https://www.steele.blue/img/VQPD0M_2Dj-600.png 600w, https://www.steele.blue/img/VQPD0M_2Dj-1000.png 1000w, https://www.steele.blue/img/VQPD0M_2Dj-1033.png 1033w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Or in code:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;Cat Presence&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;description&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;triggers&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;occupied&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    device_id&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;b86f8493d0516cb5f9afe35ad61effcf&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    entity_id&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;5a7f5d90cdaaf72fff39387fe4e377ab&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    domain&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;binary_sensor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    trigger&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;device&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;conditions&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;actions&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;action&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;light.turn_on&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    metadata&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    target&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;      entity_id&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;light.merkury_bw901_bulb&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;      brightness_pct&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;100&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;single&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Overall the project went really well, and I&#39;m still delighted when the orange light turns on while I&#39;m hanging out in the living room.
There are a few things I&#39;d like to improve with the workflow, but it&#39;s primarily around my hosting and object detection (I&#39;d like to move Frigate to a Raspberry Pi with an AI hat, so I can add more cameras and detect cats elsewhere)&lt;/p&gt;
&lt;p&gt;As dumb as a project like this is, it still lends credence that integration of disparate devices can bring value greater than the sum of their parts. No one is going to offer a commercial &amp;quot;ecosystem&amp;quot; that would give me these capabilities, especially not with cheap consumer hardware that&#39;s been abandoned by the manufacturers.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/nha78HfkH1-600.jpeg&quot; alt=&quot;An orange light shining in my living room&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; srcset=&quot;https://www.steele.blue/img/nha78HfkH1-600.jpeg 600w, https://www.steele.blue/img/nha78HfkH1-1000.jpeg 1000w, https://www.steele.blue/img/nha78HfkH1-3024.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The Year Of The Linux Desktop (for fitness games)</title>
    <link href="https://www.steele.blue/linux-desktop-zwift-beat-saber/" />
    <updated>2026-01-25T00:00:00Z</updated>
    <id>https://www.steele.blue/linux-desktop-zwift-beat-saber/</id>
    <content type="html">&lt;h2&gt;A tale told in memes&lt;/h2&gt;
&lt;p&gt;I&#39;m &lt;a href=&quot;https://xeiaso.net/notes/2026/year-linux-desktop/&quot;&gt;joining the bandwagon&lt;/a&gt; and declaring that 2026 will be the Year of the Linux Desktop. Specifically for the subset of fitness gaming apps, it&#39;s no longer necessary (or preferred) to be tethered to Windows.&lt;/p&gt;
&lt;p&gt;Two long-term trends have led to this moment.&lt;/p&gt;
&lt;p&gt;Windows has been on a &amp;quot;slowly, then all at once&amp;quot; descent in quality that &lt;a href=&quot;https://www.windowscentral.com/microsoft/windows-11/2025-has-been-an-awful-year-for-windows-11-with-infuriating-bugs-and-constant-unwanted-features&quot;&gt;really started ramping up last year&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://surfin.dog/@finn/115102306400374113&quot;&gt;&lt;img src=&quot;https://www.steele.blue/img/UG9RBhfwWz-600.png&quot; alt=&quot;Mastodon post from user finn: &amp;quot;very funny that linux didn&#39;t have to get better to gain desktop market share, the alternatives just had to get worse&amp;quot;&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;309&quot; srcset=&quot;https://www.steele.blue/img/UG9RBhfwWz-600.png 600w, https://www.steele.blue/img/UG9RBhfwWz-940.png 940w&quot; sizes=&quot;100vw&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Working with our client sysadmins on a daily basis, I get to see first-hand the OOB patches and hotfixes we&#39;ve been scrambling to apply (including this weekend when Patch Tuesday &lt;a href=&quot;https://www.thurrott.com/windows/331777/emergency-windows-11-updates-are-out-to-address-shutdown-and-remote-connection-issues&quot;&gt;broke core RDP functionality&lt;/a&gt;) and pray don&#39;t make the experience worse. Combined with a &amp;quot;Continuous Innovation&amp;quot; and &amp;quot;Controlled Feature Rollout&amp;quot; strategy that launches features when you least expect it, and it&#39;s made everything feel ephemeral and ready to break at any moment. When was the last time you got excited for an update, rather than dread it?&lt;/p&gt;
&lt;p&gt;I don&#39;t think it&#39;s any surprise this corresponds with Microsoft&#39;s headfirst embrace of generative AI. Both internally (Satya claims 30% of Microsoft&#39;s code is AI-written) and externally (the &amp;quot;everything is Copilot&amp;quot; meme, the rebranding of Edge as an agentic browser), it&#39;s inescapable.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/ld0-K6xbrO-600.jpeg&quot; alt=&quot;&amp;quot;My body is a machine&amp;quot; meme making fun of Microsoft&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;899&quot; srcset=&quot;https://www.steele.blue/img/ld0-K6xbrO-600.jpeg 600w, https://www.steele.blue/img/ld0-K6xbrO-828.jpeg 828w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;At the same time, desktop Linux has been steadily improving and growing more reliable. Some of this has to be spillover from investment in downstream OSes like ChromeOS, Android, and Steam OS, all based to some degree or other on Linux. Core peripherals like Bluetooth work across all my devices I&#39;ve tried it on, on machines both &lt;a href=&quot;https://frame.work/linux&quot;&gt;designed to run Linux&lt;/a&gt; and on PCs with random hardware I didn&#39;t even check compatibility with before installing.&lt;/p&gt;
&lt;h2&gt;My Distro&lt;/h2&gt;
&lt;p&gt;One meme I&#39;ve struggled with is the tendancy for Linux desktops to start off strong, but degrade in reliability over time.&lt;/p&gt;
&lt;p&gt;Historically this feels like a problem I&#39;m also culpable for. A common experience is for userland changes applied to make a Linux desktop functional would inevitably break some unrelated part of the system. Install a new Python 3 runtime for a project, and somehow you&#39;ve broken your virtual desktop setup, because it relied on 2.7.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mastodon.social/@jk/113877803289325585&quot;&gt;&lt;img src=&quot;https://www.steele.blue/img/TPOsydsXH_-600.png&quot; alt=&quot;Social media post from Mastodon user jk: &amp;quot;a big reason i use linux is because it gives me Control over the computer. no windows dark pattern spying shit. i can set up everything to look and function exactly how i want. but every couple months there&#39;s a package update that overrides or changes something so it no longer works how i configured it. they just reached in and broke something i worked hard on. and i have no choice in this, because you need to eventually update packages to install new ones. i don&#39;t really feel in Control at all&amp;quot;&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;281&quot; srcset=&quot;https://www.steele.blue/img/TPOsydsXH_-600.png 600w, https://www.steele.blue/img/TPOsydsXH_-629.png 629w&quot; sizes=&quot;100vw&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The critical drive lately has been the rise of atomic desktops. These distros are &amp;quot;immutable&amp;quot;, in that core filesystems are read-only and cannot be modified, even as root. The OS (and supporting packages) are updated all-at-once, and can be rolled back to a previous release if anything goes wrong.&lt;/p&gt;
&lt;p&gt;Almost all userland work is accomplished with high amounts of isolation. GUI apps are Flatpak, and CLIs use Homebrew, so they don&#39;t interfere with system packages. Want to setup a development environment? On Bluefin, you&#39;re pushed to use devcontainers, so everything happens inside an OCI (here Podman) container.&lt;/p&gt;
&lt;p&gt;It&#39;s pretty incredible how far you can take this approach. Bazzite (a gaming-focused distro) works so well that it&#39;s very common to as a drop-in replacement on Steam Deck (replacing an Arch-based OS). I&#39;m running it on my gaming desktop and spent more time fighting Windows UEFI bugs than I did getting a fully-working setup.&lt;/p&gt;
&lt;p&gt;Bazzite ships with Steam and its Proton compatibility layer, but my games need to run outside Steam. How does that behave? Turns out, great!&lt;/p&gt;
&lt;h2&gt;Zwift&lt;/h2&gt;
&lt;p&gt;My favorite &lt;a href=&quot;https://www.steele.blue/zwift-greenscreen/&quot;&gt;cycling-themed MMORPG&lt;/a&gt; doesn&#39;t have a native Linux port and probably never will. It also has quite a few Windows system dependencies (its launcher relies on an Edge-based WebView2), and of course it needs access to a GPU and peripherals like Bluetooth/ANT+. I&#39;ve been trying to get Zwift running for years with tools like Wine/Lutris, but every attempt at getting it running failed, and I&#39;d inevitably corrupt my environment installing incompatible, conflicting versions of Visual C++ Redistributables.&lt;/p&gt;
&lt;p&gt;This has been almost completely fixed with Kim Eik&#39;s &lt;a href=&quot;https://github.com/netbrain/zwift&quot;&gt;netbrain/zwift repo&lt;/a&gt; - an all-in-one Docker container that setups an isolated Wine environment, installs Zwift, and applies a known set of working patches. The result is a setup that&#39;s easier to run than Windows.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/dKbdrGnXIY-600.png&quot; alt=&quot;Screenshot of Zwift running on a Bazzite Linux desktop&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; srcset=&quot;https://www.steele.blue/img/dKbdrGnXIY-600.png 600w, https://www.steele.blue/img/dKbdrGnXIY-1000.png 1000w, https://www.steele.blue/img/dKbdrGnXIY-1920.png 1920w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Not everything works yet. In particular, Bluetooth (needed to interact with the trainer, heart rate monitor, etc) isn&#39;t available, though &lt;a href=&quot;https://github.com/netbrain/zwift/issues/188&quot;&gt;there&#39;s ongoing development&lt;/a&gt; that I&#39;m hopeful for.&lt;/p&gt;
&lt;p&gt;In the meantime, I&#39;m setting things up using alternate means. My trainer &lt;a href=&quot;https://support.wahoofitness.com/hc/en-us/articles/9211851310738-Using-Wi-Fi-with-a-KICKR-trainer-BIKE-or-RUN&quot;&gt;supports Wi-Fi connections&lt;/a&gt;, so no Bluetooth is needed for power, cadence, and resistance. I even set up the trainer on an isolated VLAN (as it&#39;s essentially an expensive IoT device I don&#39;t have full control over), and Podman was able to discover it after setting &lt;code&gt;networking=host&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Other peripherals (controllers, HR monitors) still require Bluetooth, so I&#39;ve just got an old Android device running &lt;a href=&quot;https://www.zwift.com/companion&quot;&gt;Zwift Companion&lt;/a&gt; sitting on my trainer desk. Honestly this is the most &amp;quot;non-native&amp;quot; part of my entire setup, but I&#39;m still using supported tools (and was having to use the Companion even on Windows after an update broke BLE connections).&lt;/p&gt;
&lt;p&gt;Zwift runs at a clean 60fps, and I&#39;m able to &lt;a href=&quot;https://stream.steele.blue/&quot;&gt;stream to Owncast&lt;/a&gt; via OBS just like before.&lt;/p&gt;
&lt;h2&gt;Beat Saber&lt;/h2&gt;
&lt;p&gt;This one I thought would be more challenging. As a VR rhythm game, I&#39;d expect significant hurdles to a clean, performant experience.&lt;/p&gt;
&lt;p&gt;My setup has always been a little unorthodox: on Windows I&#39;ve been running a modded Beat Saber, installed using &lt;a href=&quot;https://www.bsmanager.io/&quot;&gt;BSManager&lt;/a&gt; to setup extra songs/mods, and control which version I run. The game then streams to a Quest 2 headset, and I pray the latency gods are favorable. I spent the better part of last year getting this setup working well on Windows, and settled on tweaking Virtual Desktop to stream the game, limiting the resolution and frame rate along the way. Other tools like Oculus Link and Steam&#39;s own wireless streaming just weren&#39;t cutting it.&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;l6kEJ94mC3I&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;Turns out, Linux VR gaming is pretty solid these days. The &lt;a href=&quot;https://lvra.gitlab.io/&quot;&gt;LVRA community&lt;/a&gt; has been compiling tools and best practices for years, and many things are turnkey now. Need to stream to a Quest headset? Pick between &lt;a href=&quot;https://lvra.gitlab.io/docs/steamvr/alvr/&quot;&gt;ALVR&lt;/a&gt; and &lt;a href=&quot;https://lvra.gitlab.io/docs/fossvr/wivrn/&quot;&gt;WiVRn&lt;/a&gt;. And again, both are installed as Flatpaks, so if you get it wrong, it&#39;s super easy to change course!&lt;/p&gt;
&lt;p&gt;I went with WiVRn, as it bundles its own, Steam-independent OpenXR runtime, which has been supported by Beat Saber for years. And with a few small changes to the &lt;a href=&quot;https://github.com/WiVRn/WiVRn/blob/master/docs/steamvr.md&quot;&gt;command-line arguments in Steam&lt;/a&gt;, I had vanilla Beat Saber running on my headset almost immediately.
Interestingly, performance seemed to be even better on Linux than Windows; I was streaming at 120fps (on Windows I limited to 72) and my audio latency was down by nearly a third (~60ms compared to 90 on Windows).
This isn&#39;t uncommon, quite a few benchmarks a game running on Linux can &lt;a href=&quot;https://arstechnica.com/gaming/2025/06/games-run-faster-on-steamos-than-windows-11-ars-testing-finds/&quot;&gt;outperform its Windows counterpart&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, I wanted to get modded Beat Saber running. Luckily BSManager provides a Linux flatpak that installed without issue, and I could download and mod versions just like on Windows.
Getting it running on the headset proved to be a much bigger challenge. Out of the box, the WiVRn app list on the device wasn&#39;t detecting the modded Beat Saber, so I couldn&#39;t start it.&lt;/p&gt;
&lt;p&gt;With some help from the BSManager Discord, I found the set of environment variables that needed set (within BSManager) to get it to launch. But the workflow was pretty horrendous: start WiVRn on the PC, then launch the client on the headset. Then take the headset &lt;em&gt;off&lt;/em&gt; and launch Beat Saber on the PC. Then, put the headset back on and start playing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/zUKLvPJTjP-600.png&quot; alt=&quot;Screenshot of Beat Saber running on a Bazzite Linux desktop&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; srcset=&quot;https://www.steele.blue/img/zUKLvPJTjP-600.png 600w, https://www.steele.blue/img/zUKLvPJTjP-1000.png 1000w, https://www.steele.blue/img/zUKLvPJTjP-1920.png 1920w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;It turns out there were a few bugs in both BSManager and WiVRn that prevented the app from being discovered, and launching properly. With some trial and error (and reading a bit more C++ than I was ready for), I was able to get it running. The details are in &lt;a href=&quot;https://github.com/Zagrios/bs-manager/issues/968#issuecomment-3766039142&quot;&gt;this GitHub Issue&lt;/a&gt; - and fixes have already landed in the WiVRn codebase.&lt;/p&gt;
&lt;p&gt;This feels like a telling anecdote where the Linux gaming ecosystem is at: the fundamental components to play even complicated games are solid, and have been steadily improving. And when things don&#39;t work, there&#39;s a community to help address it. Hopefully more of this knowledge will get pulled out of private Discords and into public knowledge bases (or simply fixed at the root).&lt;/p&gt;
&lt;h2&gt;I can tinker with Linux when I need to cool down&lt;/h2&gt;
&lt;p&gt;I feel like I&#39;ve just scratched the surface of what a Linux gaming PC can do. My library is pretty small, but I expect to grow it, especially with the &lt;a href=&quot;https://store.steampowered.com/sale/steamframe&quot;&gt;Steam Frame&lt;/a&gt; inbound, offering another VR headset running Linux directly.&lt;/p&gt;
&lt;p&gt;Of course there are other neat features you can try with a Linux machine with a decent GPU. Bluefin&#39;s AI strategy is one of local operation and control. So if you want to experiment with LLMs, you can &lt;a href=&quot;https://docs.projectbluefin.io/ai/#ramalama&quot;&gt;install Ramalama&lt;/a&gt;, pull down a model from Huggingface you&#39;re comfortable with, and use your own GPU and resources. It feels like a breath of fresh air to have control of what&#39;s running on your machine, compared to the Windows experience of &amp;quot;what fresh hell with this restart bring today&amp;quot;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Hacking custom GIFs onto an LED Mask with mitmproxy</title>
    <link href="https://www.steele.blue/hacking-led-mask-mitmproxy/" />
    <updated>2025-12-01T00:00:00Z</updated>
    <id>https://www.steele.blue/hacking-led-mask-mitmproxy/</id>
    <content type="html">&lt;p&gt;&lt;lite-youtube videoid=&quot;D9mcNBhYvoA&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;This Halloween I went as Rorschach, the Watchmen &#39;hero&#39;.
I don&#39;t have much of a connection to the character; it was mostly an excuse to play with an LCD face mask I bought a few weeks earlier.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/iBpzZdEJyb-256.gif&quot; alt=&quot;animated gif of rorschach inkblots&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;256&quot; height=&quot;141&quot;&gt;&lt;/p&gt;
&lt;p&gt;Since I was on a time crunch to implement the face mask in time for a Halloween party I was attending, I decided to go for the simplest approach:
use the official Android app to upload my custom GIF onto the mask.&lt;/p&gt;
&lt;p&gt;Fortunately, through my day job I&#39;ve been introduced to a number of tools that help provide the shims necessary to make this happen, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mitmproxy.org/&quot;&gt;mitmproxy&lt;/a&gt; to capture and inspect Bluetooth packets&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apktool.org/&quot;&gt;apktool&lt;/a&gt; to decompile the &amp;quot;official&amp;quot; Android app to hunt for interesting strings&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://punchthrough.com/lightblue/&quot;&gt;LightBlue&lt;/a&gt; to test sending Bluetooth commands to a device&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wireshark.org/&quot;&gt;Wireshark&lt;/a&gt; to evaluate sending Bluetooth packets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The actual code I had to write ended up being an extremely small &lt;a href=&quot;https://www.steele.blue/hacking-led-mask-mitmproxy/&quot;&gt;mitmproxy add-on&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;flow&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; flow.request.url.endswith(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;.gif&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;        # Brute force. Replace all .gif images the app downloads with my GIF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;        img = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;open&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;rorschach-mask.gif&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;rb&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;).read()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;        flow.response.content = img&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It was a fun exercise to get an animated GIF onto a device that clearly didn&#39;t want to be hacked like this!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/vwJi3FIKNX-600.jpeg&quot; alt=&quot;still image of me in my rorschach costume&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; srcset=&quot;https://www.steele.blue/img/vwJi3FIKNX-600.jpeg 600w, https://www.steele.blue/img/vwJi3FIKNX-1000.jpeg 1000w, https://www.steele.blue/img/vwJi3FIKNX-3024.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Keep Your Running Cadence With a Garmin Music Playlist</title>
    <link href="https://www.steele.blue/running-cadence-playlist/" />
    <updated>2025-09-19T00:00:00Z</updated>
    <id>https://www.steele.blue/running-cadence-playlist/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/EVFJQoDy4q-600.jpeg&quot; alt=&quot;The author running&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2036&quot; height=&quot;1459&quot; srcset=&quot;https://www.steele.blue/img/EVFJQoDy4q-600.jpeg 600w, https://www.steele.blue/img/EVFJQoDy4q-1000.jpeg 1000w, https://www.steele.blue/img/EVFJQoDy4q-2036.jpeg 2036w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I&#39;ve been running more this year, and one of the aspects I&#39;ve been working on is running at a consistently higher cadence.
Most experts recommend &lt;a href=&quot;https://www.trainingpeaks.com/blog/finding-your-perfect-run-cadence/&quot;&gt;running at a pace of 170-180 strides per minute&lt;/a&gt;, but it can be rough maintaining that over time!
And while some fitness devices have &lt;a href=&quot;https://www8.garmin.com/manuals/webhelp/instinct/EN-US/GUID-7C3A8FE0-035D-43D2-8E5E-6B0FC75DD00C.html&quot;&gt;metronomes&lt;/a&gt; that can help you keep pace, I&#39;ve found that listening to music with a matching tempo.&lt;/p&gt;
&lt;p&gt;Christopher McDougall recommends the &lt;a href=&quot;https://marathonhandbook.com/how-to-run-faster-farther-forever/&quot;&gt;Rock Lobster approach&lt;/a&gt; - at 180 beats per minute, it&#39;s a suggested cadence for many runners. But my wife hates that song, my cadence is a little lower, and I certainly can&#39;t listen to it for an entire running session!&lt;/p&gt;
&lt;p&gt;There are plenty of &lt;a href=&quot;https://open.spotify.com/playlist/37i9dQZF1DWZUTt0fNaCPB&quot;&gt;online playlists&lt;/a&gt; that target a specific running cadence, but I don&#39;t want to subscribe to a service just to keep a tempo! I&#39;ve got plenty of MP3s I&#39;ve collected over the years, why can&#39;t I use those?&lt;/p&gt;
&lt;p&gt;Ahead of a half-marathon I had signed up for, I wrote a script that does just that. Give it a folder of music files, and it&#39;ll find all of them within a tempo range, and create a playlist for you to drop onto a compatible device. I use a Garmin watch, but it should work for other devices as well.&lt;/p&gt;
&lt;p&gt;The script to generate cadence playlists &lt;a href=&quot;https://gist.github.com/mattdsteele/082fd77c3e65faa1332a36962c11da78&quot;&gt;is available here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Measuring Tempo&lt;/h2&gt;
&lt;p&gt;I tried a few approaches to grab BPM data for my files.
My first hope was to pull the data from an authoritative source such as &lt;a href=&quot;https://beta.musicbrainz.org/&quot;&gt;MusicBrainz&lt;/a&gt;, but almost none of the songs in my library had a tagged tempo value.&lt;/p&gt;
&lt;p&gt;I also tried &lt;a href=&quot;https://www.pogo.org.uk/~mark/bpm-tools/&quot;&gt;bpm-tools&lt;/a&gt;, which is available on most Linux distributions.
This pulled back a cadence metric, but when spot-checking a few files, I noticed the data was pretty inaccurate; about 50% of the files deviated significantly from the calculated tempo.
This was made worse by the lack of a confidence metric, so I couldn&#39;t apply a low-pass filter to remove obvious errors.&lt;/p&gt;
&lt;p&gt;I ended up using the &lt;a href=&quot;https://bleu.green/deeprhythm/&quot;&gt;deeprhythm&lt;/a&gt; library, which uses a convolutional neural network to detect tempo.
It performed really well, both to quickly detect thousands of songs, and also run accurately.
After spot-checking a few songs, I decided it was accurate enough for my usage, and wrote up a script to process my library.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;tempo&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;91.0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;confidence&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0.65030837059021&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;file&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;/mnt/data/music/beets-library/Carly Rae Jepsen/E•MO•TION/12 - When I Needed You.mp3&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;tempo&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;111.0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;confidence&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0.7165337204933167&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;file&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;/mnt/data/music/beets-library/Carly Rae Jepsen/E•MO•TION/06 - Boy Problems.mp3&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;tempo&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;145.0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;confidence&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0.6137411594390869&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;file&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;/mnt/data/music/beets-library/Carly Rae Jepsen/E•MO•TION/15 - Favourite Colour.mp3&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;tempo&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;112.0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;confidence&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0.9051607251167297&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;file&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;/mnt/data/music/beets-library/Carly Rae Jepsen/E•MO•TION/13 - Black Heart.mp3&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;tempo&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;118.0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;confidence&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0.7597530484199524&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;file&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;/mnt/data/music/beets-library/Carly Rae Jepsen/E•MO•TION/01 - Run Away With Me.mp3&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Garmin watches can load a playlist in M3U format, so the script builds a simple playlist, and consolidates everything into a folder you can drag &amp;amp; drop onto a device.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0:/MUSIC/cadence/03 - Star Quality.m4a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0:/MUSIC/cadence/04 - Lucifer&#39;s Jigsaw.mp3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0:/MUSIC/cadence/03 - Brothaz.m4a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0:/MUSIC/cadence/02 - Such Great Heights.m4a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0:/MUSIC/cadence/11 - Paper Lanterns.mp3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0:/MUSIC/cadence/DSP - Le Weeknd de Nemo.mp3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0:/MUSIC/cadence/28 - The Room Where It Happens.m4a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;uv is neat&lt;/h2&gt;
&lt;p&gt;I&#39;ve struggled running Python apps in the past, especially if they pulled in dependencies.
As chaotic as the JavaScript ecosystem is, using a library is (usually) as easy as an &lt;code&gt;npm install&lt;/code&gt;.
With Python, I&#39;ve never been able to make heads nor tails of pip, pipx, requirements.txt, poetry, virtualenv, and the other tools in the ecosystem.&lt;/p&gt;
&lt;p&gt;A coworker pointed me toward Astral&#39;s &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt;, which claims to be the One Tool needed to manage the chaos.
And at least for this case, it works great!&lt;/p&gt;
&lt;p&gt;I especially like its support for &lt;a href=&quot;https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata&quot;&gt;inline dependencies (PEP 723)&lt;/a&gt;, which lets me specify all my project&#39;s dependencies with the script itself:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;#!/usr/bin/env python3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# /// script&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# dependencies = [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;#   &quot;click&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;#   &quot;deeprhythm&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# ///&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; click&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; deeprhythm &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; DeepRhythmPredictor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; os&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this, I can simply &lt;code&gt;uv run &amp;lt;script&amp;gt;.py&lt;/code&gt;, and it&#39;ll create a virtual environment, download required dependencies (or use a local cache), and execute the script.&lt;/p&gt;

&lt;div id=&quot;asc-player-1ad119&quot;&gt;&lt;/div&gt;
&lt;script webc:keep=&quot;&quot; defer=&quot;&quot;&gt;
const el = document.querySelector(&#39;#asc-player-1ad119&#39;);
AsciinemaPlayer.create(&#39;/casts/cadence.cast&#39;, el, {});
&lt;/script&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;matt@ORTHO&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; ~/mu&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;s&gt; &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; bpm.py&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; --dir&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; /mnt/c/Users/Matt/Music/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;⠹&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; Preparing&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; packages...&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (38/53)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;⠦&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; Preparing&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; packages...&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (43/53)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;nvidia-cuda-nvrtc-cu12&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;   ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 70.92&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/83.96&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;triton&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;                   ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 71.99&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/148.33&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;nvidia-cufft-cu12&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;        ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 71.36&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/184.17&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;nvidia-cusolver-cu12&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;     ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 71.01&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/255.11&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;nvidia-cusparselt-cu12&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;   ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 71.12&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/273.89&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;nvidia-cusparse-cu12&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;     ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 70.68&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/274.86&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;nvidia-nccl-cu12&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;         ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 70.77&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/307.43&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;nvidia-cublas-cu12&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;       ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 70.95&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/566.81&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;nvidia-cudnn-cu12&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;        ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 70.77&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/674.02&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;torch&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;                    ------------------------------&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 72.24&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB/846.92&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; MiB&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;                        &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does feel a little ridiculous pulling down PyTorch and a gigabyte of dependencies for a hundred-line shell script, but at least it&#39;s fast and easy!&lt;/p&gt;
&lt;p&gt;uv also lets you execute scripts from a URL, so you can run this fom a command-line and begin processing:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; https://gist.githubusercontent.com/mattdsteele/082fd77c3e65faa1332a36962c11da78/raw/bbe07ad1fe8737153a49362cbe70f91d51a75fb8/cadence-playlist.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Maybe tempo isn&#39;t the only factor I should consider&lt;/h2&gt;
&lt;p&gt;I had a fun time with the playlist, and was able to keep a decent cadence going. You can even see where a song change with a significant delta occurred.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/Oq1DlZlCWH-600.png&quot; alt=&quot;A graph of running cadence over time. There are several blocks of consistent pace, correlating to &quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1865&quot; height=&quot;342&quot; srcset=&quot;https://www.steele.blue/img/Oq1DlZlCWH-600.png 600w, https://www.steele.blue/img/Oq1DlZlCWH-1000.png 1000w, https://www.steele.blue/img/Oq1DlZlCWH-1865.png 1865w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;With nothing but tempo to drive the playlist, I got a real eclectic mix in my ears. Some of the artists I experienced on the run:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rancid (ok!)&lt;/li&gt;
&lt;li&gt;Audioslave (nice)&lt;/li&gt;
&lt;li&gt;The Lonely Island (uh..)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Hamilton&lt;/em&gt; soundtrack (still slaps!)&lt;/li&gt;
&lt;li&gt;Jack Johnson (..I can&#39;t believe it captured BPM for an acoustic guitar)&lt;/li&gt;
&lt;li&gt;Death Cab for Cutie (maybe not the right context to feel emo)&lt;/li&gt;
&lt;li&gt;Brother Ali (..but this came at the right time)&lt;/li&gt;
&lt;li&gt;Norah Jones (oh boy)&lt;/li&gt;
&lt;li&gt;Jamiroquai (this made me virtually insane)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So yeah, maybe I should have looked through the songs in the playlist first.
Or only pull in certain genres by querying &lt;a href=&quot;https://beets.io/&quot;&gt;beets&lt;/a&gt; (a very cool tool, but probably a whole other post of its own).&lt;/p&gt;
&lt;p&gt;It might not have been the ideal soundtrack for a PR (my actual finishing time was pretty middling), but it at least wasn&#39;t as bad &lt;a href=&quot;https://www.instagram.com/p/DDArgXQRn8n/&quot;&gt;as this playlist&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Thank Java for Better JavaScript Dates</title>
    <link href="https://www.steele.blue/java-script-dates/" />
    <updated>2025-04-15T00:00:00Z</updated>
    <id>https://www.steele.blue/java-script-dates/</id>
    <content type="html">&lt;p&gt;I gave a lightning talk at &lt;a href=&quot;https://nebraskajs.com&quot;&gt;NebraskaJS&lt;/a&gt; about the complex, decades-long relationship between JavaScript and Java&#39;s Date APIs:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;AZ83qcYn_vE&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;The first half was a bit of a troll; I used a Java REPL (anonymized to look like Node) to demonstrate some of the more famously unexpected behavior in the &lt;code&gt;Date&lt;/code&gt; class:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; Date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;March 20 2025&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;getMonth&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// returns 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;getDay&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// returns 3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;getYear&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// returns 125??&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code is syntactically correct Java and JavaScript, and &lt;strong&gt;behaves identically in both languages&lt;/strong&gt;.
One of the gifts of being told to &lt;a href=&quot;https://maggiepint.com/2017/04/09/fixing-javascript-date-getting-started/&quot;&gt;&amp;quot;make it like Java&amp;quot;&lt;/a&gt; when adding dates to a ten-day-old language.&lt;/p&gt;
&lt;p&gt;Java, of course, got markedly improved Date/Time APIs in version 8. &lt;a href=&quot;https://jcp.org/aboutJava/communityprocess/pfd/jsr310/JSR-310-guide.html&quot;&gt;Spearheaded&lt;/a&gt; by the lead developer of the de facto third-party date/time library, the &lt;code&gt;java.time&lt;/code&gt; APIs were a breath of fresh air, and the new standard tool for all time-related matters.&lt;/p&gt;
&lt;p&gt;JavaScript has been following the same path, with Maggie Pint (one of the maintainers of Moment.js) working to standardize the Temporal API.
It&#39;s been a long journey, with it reaching Stage 3 of the TC39 process way back in 2021, but hasn&#39;t made its way to a browser engine just yet.&lt;/p&gt;
&lt;p&gt;But it&#39;s &lt;em&gt;finally&lt;/em&gt; &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal#browser_compatibility&quot;&gt;making its way into browsers&lt;/a&gt;! Safari, Firefox, and Deno have it in preview release, with Chromium browsers in active development.&lt;/p&gt;
&lt;p&gt;So it&#39;s a great time to refamiliarize yourself with the API, start &lt;a href=&quot;https://www.npmjs.com/package/@js-temporal/polyfill&quot;&gt;checking out the polyfill&lt;/a&gt;, and start building fun stuff, like &lt;a href=&quot;https://www.steele.blue/js-temporal/&quot;&gt;my gravel racing time estimator&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>This blog&#39;s comments are powered by Webmentions</title>
    <link href="https://www.steele.blue/webmentions/" />
    <updated>2025-01-19T00:00:00Z</updated>
    <id>https://www.steele.blue/webmentions/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://indieweb.org/Webmention&quot;&gt;Webmentions&lt;/a&gt; are a cool, standards-compliant mechanism to let your corner of the Web interact with other sites. What better way to add a comments system to my new &lt;a href=&quot;https://www.steele.blue/gatsby-to-eleventy&quot;&gt;Eleventy-powered blog&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;At its core, Webmentions are just a means for one site to notify another that it&#39;s been mentioned.
That said, webmentions aren&#39;t the most intuitive concept to grasp, and implementing them requires additional consideration.&lt;/p&gt;
&lt;h1&gt;Collecting mentions&lt;/h1&gt;
&lt;p&gt;Webmentions are sent via simple HTTP message with a &lt;code&gt;source&lt;/code&gt; and &lt;code&gt;target&lt;/code&gt; parameter. These are discovered based on a &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag on your page. You can host this endpoint yourself, but I&#39;m using the excellent &lt;a href=&quot;https://webmention.io/&quot;&gt;Webmention.io&lt;/a&gt; to capture all webmentions for my domain:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;link&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; rel&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;webmention&quot;&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; webc:keep&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; href&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;https://webmention.io/steele.blue/webmention&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Webmention.io provides an API that returns all the mentions for a page (or domain).
I hit this at build-time, and create a new Eleventy collection with all the entries:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; url&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`https://webmention.io/api/mentions.jf2?token=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;apiKey&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&amp;#x26;per-page=1000`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; EleventyFetch&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;duration:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;1d&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;type:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;json&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; grouped&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;groupBy&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;children&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;wm-target&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; baseData&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = {};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; key&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; Object&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;keys&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;grouped&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    baseData&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)] = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;grouped&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; baseData&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Displaying Webmentions&lt;/h1&gt;
&lt;p&gt;On each page, I include a Liquid component that finds any Webmentions for the URL, and renders each category (likes/reposts/comments):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/3wrsi2U6sg-600.png&quot; alt=&quot;Example of reposts/likes/webmentions on a blog post&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;996&quot; srcset=&quot;https://www.steele.blue/img/3wrsi2U6sg-600.png 600w, https://www.steele.blue/img/3wrsi2U6sg-828.png 828w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;This felt like the perfect place for a WebC-based component, but I ran into issues as I needed to pass in complex data as props, which isn&#39;t supported.
Luckily WebC lets you mix in other templating languages, again showcasing its flexibility and pragmatism.&lt;/p&gt;
&lt;p&gt;Since this only gets generated at build-time, it doesn&#39;t capture new mentions until I rebuild the site, which is fine for me. If you want more dynamism; you could setup a cron job to rebuild their site daily to pull in new mentions.&lt;/p&gt;
&lt;p&gt;Webmention.io also supports &lt;a href=&quot;https://webmention.io/settings/webhooks&quot;&gt;outgoing webhooks&lt;/a&gt;, so you could directly trigger a rebuild when a new mention comes in.&lt;/p&gt;
&lt;p&gt;And some people just pull in webmentions by fetching them client-side, and rendering interactions via JavaScript.
&lt;a href=&quot;https://seia.js.org/&quot;&gt;Seia&lt;/a&gt; looks like a pretty straightforward Web Component providing drop-in support with Webmention.io.&lt;/p&gt;
&lt;h1&gt;Bridging from the Fediverse&lt;/h1&gt;
&lt;p&gt;Surprisingly, not 100% of online communication occurs via sites sending webmentions to each other.
We&#39;d also like to capture comments occurring on Mastodon (or other channels), even if they don&#39;t natively support webmentions.
In IndieWeb parlance, this is known as a &lt;a href=&quot;https://indieweb.org/backfeed&quot;&gt;backfeed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&#39;m using Ryan Barrett&#39;s &lt;a href=&quot;https://brid.gy/about&quot;&gt;Bridgy&lt;/a&gt; service for this.
You can authorize Bridgy to poll your Mastodon account (or social networks) to discover likes/reposts/replies to your toots, and send them back to your site as Webmentions.&lt;/p&gt;
&lt;p&gt;In addition, the Bridgy Cinematic Universe supports a number of other features, including posting to Mastodon when you publish new blog entries.
And &lt;a href=&quot;https://fed.brid.gy/&quot;&gt;Bridgy Fed&lt;/a&gt; lets you create Mastodon/Bluesky/Indieweb accounts from any other permutation; a great way to implement &lt;a href=&quot;https://indieweb.org/POSSE&quot;&gt;POSSE&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Sending Outgoing Webmentions&lt;/h1&gt;
&lt;p&gt;As I&#39;m writing posts, I want to send Webmentions to other sites which I reference. This requires a different set of tools, but again the Indieweb has provided a solid foundation to build atop.&lt;/p&gt;
&lt;p&gt;I&#39;m using Remy Sharp&#39;s &lt;code&gt;webmention&lt;/code&gt; CLI tool, which parses my existing Atom feed, finds all links which support mentions, and delivers a webmention to each.&lt;/p&gt;
&lt;p&gt;As noted in his &lt;a href=&quot;https://remysharp.com/2019/06/18/send-outgoing-webmentions&quot;&gt;introductory post&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The ability to send Webmentions needs to be a part of an automated workflow - the same way as posting a new WordPress blog post automatically sent pingbacks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I use a GitHub Action to trigger this, via a single line addition to my existing build:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;npx webmention _site/atom.xml --limit 1 --send&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can create a more robust interactivity by enhancing your page&#39;s markup with &lt;a href=&quot;https://indieweb.org/microformats&quot;&gt;microformats&lt;/a&gt;, enabling &lt;a href=&quot;https://indieweb.org/comments#How_to_display&quot;&gt;comments&lt;/a&gt; and more.&lt;/p&gt;
&lt;h1&gt;TMTOWTDI&lt;/h1&gt;
&lt;p&gt;One exciting facet to integrating Webmentions into your workflow is that there&#39;s no one best way to do it.
You can use existing tools and services to collect and deliver mentions, or customize them to your own liking.&lt;/p&gt;
&lt;p&gt;This does make for a more complicated, &amp;quot;choose your own adventure&amp;quot; aspect to the process, but I find this to be a feature, not a bug.
Mixing prebuilt tools with my own code has given me a better understanding of what works well and what I could improve on in the future.
I can optimize for certain aspects (like zero-runtime JavaScript) when they tradeoff with others (real-time updates), while using the same protocol as someone who would make a different choice.&lt;/p&gt;
&lt;p&gt;I found these sites valuable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keith Grant&#39;s &lt;a href=&quot;https://keithjgrant.com/posts/2019/02/adding-webmention-support-to-a-static-site/&quot;&gt;Adding webmentions to a static site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Bob Monsour&#39;s &lt;a href=&quot;https://bobmonsour.com/blog/adding-webmentions-to-my-site/&quot;&gt;Adding webmentions to my site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Robb Knight&#39;s &lt;a href=&quot;https://rknight.me/blog/adding-webmentions-to-your-site/&quot;&gt;Adding Webmentions to your Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Paul Kinlan&#39;s &lt;a href=&quot;https://paul.kinlan.me/using-web-mentions-in-a-static-sitehugo/&quot;&gt;Using Web Mentions in a static site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Max Böck&#39;s &lt;a href=&quot;https://mxb.dev/blog/using-webmentions-on-static-sites/&quot;&gt;Using Webmentions in Eleventy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sia Karamalegos &lt;a href=&quot;https://sia.codes/posts/webmentions-eleventy-in-depth/&quot;&gt;An In-Depth Tutorial of Webmentions + Eleventy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Send me a Mention&lt;/h1&gt;
&lt;p&gt;So how do you actually comment on this post? You&#39;ve got a few options:&lt;/p&gt;
&lt;p&gt;Reply to me &lt;a href=&quot;https://carhenge.club/@mattdsteele&quot;&gt;on the Fediverse&lt;/a&gt;, and your post will show up here.&lt;/p&gt;
&lt;p&gt;Or even better, write a blog post of your own and send me an outgoing webmention.
Websites: the cool and underground social network that you shouldn&#39;t sleep on in 2025.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>From Gatsby to Eleventy</title>
    <link href="https://www.steele.blue/gatsby-to-eleventy/" />
    <updated>2024-11-25T00:00:00Z</updated>
    <id>https://www.steele.blue/gatsby-to-eleventy/</id>
    <content type="html">&lt;p&gt;I try not to rebuild my site too frequently, as it&#39;s one of the classic blunders, alongside getting involved in a land war in Asia. But the &lt;a href=&quot;https://en.wikipedia.org/wiki/Six-year_itch&quot;&gt;Six-Year Itch&lt;/a&gt; is real, and it was time for a change.&lt;/p&gt;
&lt;h2&gt;The (Not So Great) Gatsby&lt;/h2&gt;
&lt;p&gt;My site has been powered by Gatsby since Dustin Schau (former coworker and Gatsby emeritus) &lt;a href=&quot;https://github.com/mattdsteele/steele.blue/pull/20&quot;&gt;ported it over&lt;/a&gt; as an early Christmas gift in 2018.
I&#39;m not a React guy, but it was easy to work with, and I could see the appear of a framework built around collecting arbitrary sources, and unifying them into a single data structure queryable by GraphQL.&lt;/p&gt;
&lt;p&gt;Unfortunately, Gatsby isn&#39;t in the &lt;a href=&quot;https://changelog.com/jsparty/325#transcript-43&quot;&gt;healthiest state these days&lt;/a&gt;.
It wasn&#39;t able to achieve the liftoff that&#39;s expected from venture-funded frameworks. As it&#39;s fallen out of favor in the past few years, the OSS side of the framework has continued to languish since its acquisition by Netlify. Lately, I&#39;ve spent more time fighting with dependencies than I do blogging, and it&#39;s felt more like a mill around my neck than a productive authoring tool.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/8uIYRovBpv-600.png&quot; alt=&quot;A list of PRs in GitHub referencing dependency updates&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;654&quot; srcset=&quot;https://www.steele.blue/img/8uIYRovBpv-600.png 600w, https://www.steele.blue/img/8uIYRovBpv-688.png 688w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Turning it up to Eleventy&lt;/h2&gt;
&lt;p&gt;I wanted to switch to something that&#39;s built for the long haul, and &lt;a href=&quot;https://11ty.dev&quot;&gt;Eleventy&lt;/a&gt; fits the charge.&lt;/p&gt;
&lt;p&gt;Created by Nebraskan (and former coworker) Zach Leatherman, it&#39;s a static site generator with a focus on simplicity, sustainability, and not getting in your way. It&#39;s heavily inspired by Jekyll, which was how my site was &lt;a href=&quot;https://www.steele.blue/a-fresh-coat-of-paint/&quot;&gt;built ten years ago&lt;/a&gt;, so it feels like I&#39;ve come full circle.&lt;/p&gt;
&lt;p&gt;In addition to its familiarity as a tool, I appreciate what the Eleventy project prioritizes.
Rather than a laundry list of features mean to check boxes, Zach advertises &lt;a href=&quot;https://youtu.be/bPtQmsjXMuo?si=8k_fzWNl8s2OPKks&quot;&gt;how stable its API is&lt;/a&gt; and how easy upgrades are, with major versions focused on removing dependencies and modernizing the codebase.
I trust Zach when he says he doesn&#39;t tie Eleventy to a bundler, because he wants it to &lt;a href=&quot;https://changelog.com/jsparty/266#transcript-38&quot;&gt;outlive the current bundlers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Zach has also been at the vanguard of working through &lt;a href=&quot;https://www.zachleat.com/web/monetization/&quot;&gt;sustainability models for Eleventy&lt;/a&gt;, as it&#39;s been maintained as an OSS passion project, to one funded by VC-backed companies, to a crowdfunded model. It&#39;s currently found a home at Font Awesome, which feels like it&#39;s finally aligned from its values, and has a history of stewarding open source projects such as &lt;a href=&quot;https://changelog.com/jsparty/322&quot;&gt;Shoelace/Web Awesome&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With the recent 3.0 release of Eleventy (now powered by ES Modules), it felt like a great time to start fresh.&lt;/p&gt;
&lt;h2&gt;From WebC to Shining WebC&lt;/h2&gt;
&lt;p&gt;I didn&#39;t want to lose out on the component model when moving off Gatsby.
I&#39;ve struggled with the template-first model that Jekyll, Hugo, and other SSGs use when it&#39;s not driven by a framework.
And while I &lt;a href=&quot;https://www.steele.blue/web-components-arent-weird-anymore/&quot;&gt;really like Web Components&lt;/a&gt;, it didn&#39;t feel like the right tool for a primarily static site, without building my own server-side rendering framework along the way.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.11ty.dev/docs/languages/webc/&quot;&gt;WebC&lt;/a&gt; is a new authoring format, loosely coupled with Eleventy, and hit the sweet spot. It&#39;s got a similar feel to Single File Component models popularized by Vue and Astro, but it outputs markup akin to Web Components.
Using WebC, I was able to rebuild most of the Gatsby components, and not feel like I was spaghettifying my codebase.&lt;/p&gt;
&lt;p&gt;I&#39;m using &amp;quot;Web Components&amp;quot; loosely here, as the markup is highly configurable. Most of the components simply get converted into the HTML they encapsulate, and are never generated client-side or &lt;code&gt;extend HTMLElement&lt;/code&gt;.
But if you want to render components as &amp;quot;real&amp;quot; Custom Elements with &lt;a href=&quot;https://www.11ty.dev/docs/languages/webc/#css-and-js-(bundler-mode)&quot;&gt;Declarative Shadow DOM&lt;/a&gt;, you can do so!&lt;/p&gt;
&lt;p&gt;I&#39;m hoping WebC sees a life beyond Eleventy projects, because it feel like a utilitarian tool designed for real problems. As it stands, this is probably the part of the codebase that&#39;s most tied to Eleventy, but if I have to rebuild the dozen-ish &lt;code&gt;.webc&lt;/code&gt; files at some point, it shouldn&#39;t be too difficult to port to a &#39;vanilla&#39; Custom Element, or whatever comes next.&lt;/p&gt;
&lt;h2&gt;Four Hundo Blaze It&lt;/h2&gt;
&lt;p&gt;It&#39;s not all conceptual benefits, one immediate advantage I see are improved build times (production builds are 2-3x faster, and the dev server starts immediately, rather than after ~30 seconds).&lt;/p&gt;
&lt;p&gt;I&#39;ve done little to optimize the site. Yet without much effort, I&#39;m awful close to the coveted Lighthouse Four Hundos:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/IDKshSXiiz-600.png&quot; alt=&quot;Lighthouse score, with Performance at 99, all other metrics at 100&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;628&quot; height=&quot;228&quot; srcset=&quot;https://www.steele.blue/img/IDKshSXiiz-600.png 600w, https://www.steele.blue/img/IDKshSXiiz-628.png 628w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is in stark contract to my Gatsby blog; I had to do quite a bit of work to make it perform like I wanted, including applying a plugin to &lt;a href=&quot;https://www.gatsbyjs.com/plugins/gatsby-plugin-no-javascript/&quot;&gt;delete all the client-side JavaScript&lt;/a&gt; used to hydrate components that should have been static anyway.&lt;/p&gt;
&lt;p&gt;The site is fast by default, and the &lt;a href=&quot;https://www.w3.org/Provider/Style/URI&quot;&gt;URIs remain Cool&lt;/a&gt;.
A few features are missing from the Gatsby era (namely, custom OpenGraph images), but I feel like I&#39;ve got a solid foundation to iterate on.&lt;/p&gt;
&lt;p&gt;It feels good, like I&#39;m working with the grain of the Web, rather than against it. The codebase is at &lt;a href=&quot;https://github.com/mattdsteele/steele.blue&quot;&gt;https://github.com/mattdsteele/steele.blue&lt;/a&gt;. Here&#39;s to the next six years (and beyond).&lt;/p&gt;
&lt;!-- Not even the first Matt Steele to have an 11ty based blog https://mattsteele.dev/ --&gt;
</content>
  </entry>
  <entry>
    <title>Side projects should be fun</title>
    <link href="https://www.steele.blue/side-projects-should-be-fun/" />
    <updated>2024-04-28T00:00:00Z</updated>
    <id>https://www.steele.blue/side-projects-should-be-fun/</id>
    <content type="html">&lt;p&gt;Connell McCarthy&#39;s post about &amp;quot;overengineering everything at his wedding&amp;quot; was a fun read: &lt;a href=&quot;https://connellmccarthy.com/article/wedding/&quot;&gt;https://connellmccarthy.com/article/wedding/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I can relate to “this wedding would be a great opportunity to build everything bespoke”, as I:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.steele.blue/indieweb-wedding-livestream&quot;&gt;live-streamed our wedding&lt;/a&gt; on an alpha build of Owncast&lt;/li&gt;
&lt;li&gt;built a &lt;a href=&quot;https://www.steele.blue/photo-booth&quot;&gt;custom photo booth&lt;/a&gt; from a Raspberry Pi&lt;/li&gt;
&lt;li&gt;built our website and RSVP system with a Firebase and Airtable backend (probably should have blogged about that too!)&lt;/li&gt;
&lt;li&gt;crafted, painted, and stained the welcome signage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/c2Hmsaqluf-600.jpeg&quot; alt=&quot;Wedding Signage&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/c2Hmsaqluf-600.jpeg 600w, https://www.steele.blue/img/c2Hmsaqluf-1000.jpeg 1000w, https://www.steele.blue/img/c2Hmsaqluf-3024.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;By most objective measures, this was a colossal waste of time.&lt;/p&gt;
&lt;p&gt;Some advice I received after getting engaged was to accept that you won&#39;t be able to maintain focus and attention on every aspect of the wedding. Trying to &amp;quot;make your mark&amp;quot; on decorations, seating order, invitation typography, etc. was a recipe for spreading yourself too thin, and doing none of them well. A more prudent approach is to drill into a few things you really care about, and make those great.&lt;/p&gt;
&lt;p&gt;Most aspects of the wedding-industrial-complex have been fully commodified and are an easy thing to purchase and check off your to-do list. Even if you don&#39;t want to use a wedding-branded website builder like The Knot, you could also knock it out in an afternoon with Squarespace or one of a thousand WordPress templates.&lt;/p&gt;
&lt;p&gt;So why build your own, less efficiently? One potential reason might be that the scope of your event dropped by an order of magnitude, due to having scheduled your wedding during a global pandemic. With essentially no in-person attendees, we were free to focus on the purely digital aspects.&lt;/p&gt;
&lt;p&gt;Another reason: we both like to program, and it seemed like this would be a fun way to spawn a bunch of side projects.&lt;/p&gt;
&lt;p&gt;Some of my favorite memories of the early part of the pandemic were pairing with my fiance on building a simple carousel with silly photos of us. Or testing the live stream software by using it to host a bad movie night with friends. Or learning just enough Python to make a custom NeoPixel ring for the bespoke photo booth.&lt;/p&gt;
&lt;p&gt;I was reminded of these fun projects after seeing &lt;a href=&quot;https://digipres.club/@foone/112339899135647678&quot;&gt;this post from Foone&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;the world needs more recreational programming.
like, was this the most optimal or elegant way to code this?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;no, but it was the most fun to write.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There&#39;s plenty of reasons to work on a side project: maybe you want to learn something new to make yourself more marketable. Or perhaps it&#39;s a &lt;a href=&quot;https://www.steele.blue/neverending-side-project/&quot;&gt;ritual&lt;/a&gt; you do each year to see how the ecosystem has changed. But my favorite kinds of projects are the ones that are just for the fun of it.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Mutation Testing with StrykerJS</title>
    <link href="https://www.steele.blue/mutation-testing-with-strykerjs/" />
    <updated>2024-04-21T00:00:00Z</updated>
    <id>https://www.steele.blue/mutation-testing-with-strykerjs/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://www.thecakecodes.online/&quot;&gt;Jessica Codr&lt;/a&gt; and I gave a talk at &lt;a href=&quot;https://nebraskajs.com/&quot;&gt;NebraskaJS&lt;/a&gt; about &lt;a href=&quot;https://stryker-mutator.io/docs/stryker-js/introduction/&quot;&gt;StrykerJS&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;JPJbK5lT5IY&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;Stryker is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Mutation_testing&quot;&gt;mutation testing&lt;/a&gt; tool - it makes a change to your code, then runs your tests. If everything passes, then your tests aren&#39;t catching bugs you could be introducing over time. Think of it as code coverage, with specific areas to focus on.&lt;/p&gt;
&lt;p&gt;In some ways it&#39;s similar to fuzzing: by making large number of pseudo-random changes to a codebase, you can start to get a feel for which parts are solid, and which could use more attention.&lt;/p&gt;
&lt;p&gt;As a means to gauge the quality of your tests, I&#39;m not sure why so many folks terminate their assessment using code coverage when mutation testing is a more robust metric. There&#39;s some additional overhead (most frameworks don&#39;t incorporate mutation testing out of the box) and the test runs are slower, but you can let your CI job run it async (or setup a nightly job) and you&#39;ll get far more robust metrics.&lt;/p&gt;
&lt;p&gt;I&#39;ve had some experience with testing Java code with &lt;a href=&quot;https://pitest.org/&quot;&gt;PIT&lt;/a&gt;, and Ruby has had &lt;a href=&quot;https://github.com/mbj/mutant&quot;&gt;mutant&lt;/a&gt; for a while, but Stryker was a new tool to me, even though it&#39;s been around for nearly a decade. I&#39;m hoping it becomes a useful tool in the belt!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>My favorite water bottles</title>
    <link href="https://www.steele.blue/my-favorite-water-bottles/" />
    <updated>2024-03-24T00:00:00Z</updated>
    <id>https://www.steele.blue/my-favorite-water-bottles/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/sDmTUqY1RM-600.jpeg&quot; alt=&quot;A collection of water bottles&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3212&quot; height=&quot;2315&quot; srcset=&quot;https://www.steele.blue/img/sDmTUqY1RM-600.jpeg 600w, https://www.steele.blue/img/sDmTUqY1RM-1000.jpeg 1000w, https://www.steele.blue/img/sDmTUqY1RM-3212.jpeg 3212w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Inspired by &amp;quot;my favorite mugs&amp;quot; posts from &lt;a href=&quot;https://www.cassey.dev/favorite-mugs/&quot;&gt;Cassey&lt;/a&gt; and &lt;a href=&quot;https://www.benji.dog/articles/my-favorite-mugs/&quot;&gt;Benji&lt;/a&gt;, here&#39;s a few of my favorite drinkware.
While I do have a set of mugs I love, I probably get more use out of my water bottles.
They pull double-duty as a convenient vessel for daily drinking, and for any activities on a bike.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/7qtkcpikDX-600.jpeg&quot; alt=&quot;Gravel Worlds&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2264&quot; height=&quot;2264&quot; srcset=&quot;https://www.steele.blue/img/7qtkcpikDX-600.jpeg 600w, https://www.steele.blue/img/7qtkcpikDX-1000.jpeg 1000w, https://www.steele.blue/img/7qtkcpikDX-2264.jpeg 2264w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I&#39;ve raced &lt;a href=&quot;https://www.gravel-worlds.com/&quot;&gt;Gravel Worlds&lt;/a&gt; a half dozen times, and get a new bottle every year.
It&#39;s both functional (at 24oz, it&#39;s a decent size to throw on a bike) and stylish, and a mainstay of my bottle collection.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/Y0NGFFQTd4-600.jpeg&quot; alt=&quot;Elite Fly MTB 950&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2031&quot; height=&quot;2031&quot; srcset=&quot;https://www.steele.blue/img/Y0NGFFQTd4-600.jpeg 600w, https://www.steele.blue/img/Y0NGFFQTd4-1000.jpeg 1000w, https://www.steele.blue/img/Y0NGFFQTd4-2031.jpeg 2031w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.elite-it.com/en/products/water-bottles/sport/fly-mtb&quot;&gt;Liter-sized bottles&lt;/a&gt; are a mainstay of my gravel races.
The extra few ounces have kept me from having to carry a hydration pack, and let me make full use of the two bottle cages on my bike.
I especially like these ones from Elite with a protective cap, which helps keep the lid free from gravel dust.
I&#39;ve eaten enough dirt through the years!&lt;/p&gt;
&lt;p&gt;Runner-up: the &lt;a href=&quot;https://www.zefal.com/en/bottles/545-magnum.html&quot;&gt;Zefal Magnum&lt;/a&gt; 1L bottles have traveled the most miles on my bike.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/Dl_SPVqczq-600.jpeg&quot; alt=&quot;Powersauce&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2182&quot; height=&quot;2182&quot; srcset=&quot;https://www.steele.blue/img/Dl_SPVqczq-600.jpeg 600w, https://www.steele.blue/img/Dl_SPVqczq-1000.jpeg 1000w, https://www.steele.blue/img/Dl_SPVqczq-2182.jpeg 2182w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;This one&#39;s substantially smaller and doesn&#39;t actually get on the bike frequently, but it makes me smile every time I break it out.
I don&#39;t buy many products from &lt;a href=&quot;https://thrillpool.com/&quot;&gt;meme influencers&lt;/a&gt;, but I make an exception for an especially good Simpsons reference.
It makes me want to &lt;a href=&quot;https://simpsonswiki.com/wiki/Powersauce&quot;&gt;conquer the Murderhorn&lt;/a&gt;, even when I fill it with apple cores and Chinese newspapers.&lt;/p&gt;
&lt;h2&gt;Postscript - Bottle Longevity&lt;/h2&gt;
&lt;p&gt;One endless conversation that I&#39;ve never heard a satisfactory answer is bottle longevity.
Advice is all over the place, from &amp;quot;replace your bottles every season&amp;quot; to &amp;quot;use them until mold is visible and unremovable&amp;quot;.&lt;/p&gt;
&lt;p&gt;I tend to be pragmatic; I&#39;ll give them a good cleaning with soap and a bottle brush, and they&#39;ll be good for years.
If I start to notice a funk/aftertaste, they&#39;ll get replaced.
This is probably more psychological than scientific; most of the advocacy I&#39;ve seen around microplastics, etc. tend to be from the same groups that are focused on trends and style, not sustainability.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Web Push is almost usable with iOS 17</title>
    <link href="https://www.steele.blue/web-push-ios17/" />
    <updated>2023-09-21T00:00:00Z</updated>
    <id>https://www.steele.blue/web-push-ios17/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/expWjFltGU-600.png&quot; alt=&quot;Screenshot of a successful push event&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;436&quot; srcset=&quot;https://www.steele.blue/img/expWjFltGU-600.png 600w, https://www.steele.blue/img/expWjFltGU-828.png 828w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Apple got a lot of positive sentiment earlier this year when they launched betas of iOS 16.4 and announced support for Web Push, along with other features.
&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2022/10098/?time=869&quot;&gt;At WWDC they declared&lt;/a&gt;  &amp;quot;As long as you&#39;ve coded to the standards and use feature detection, so you don&#39;t unwittingly exclude Safari, your users will already get the benefit of Web Push&amp;quot;.&lt;/p&gt;
&lt;p&gt;Having attempted to implement Web Push in a recent project I can attest that the reality was pretty different.
In addition to a number of buggy and inconsistent runtime errors, there were several user-hostile limitations that effectively excluded it from being used by anyone but the most dedicated users.&lt;/p&gt;
&lt;p&gt;As iOS 17 rolls out, we&#39;re seeing forward progress.
Several of the restrictions have been removed, and the behavior appears to be more consistent, with APIs that can be feature-detected as one would expect.&lt;/p&gt;
&lt;h2&gt;No longer requires Feature Flags/Experimental Features&lt;/h2&gt;
&lt;p&gt;The big issue I encountered when trying to support Web Push was that the APIs were disabled by default in iOS 16.
It was an &amp;quot;available&amp;quot; option, but tucked behind the Settings -&amp;gt; Safari -&amp;gt; Advanced -&amp;gt; Experimental Features setting, alongside dozens of other toggles.
Functionally this killed Web Push&#39;s adoption, as you couldn&#39;t reasonably expect a general audience to modify their feature flags to support this.&lt;/p&gt;
&lt;p&gt;The good news is that as of iOS 17, this is no longer required, and the relevant APIs have been enabled by default.
There is still an option for &amp;quot;Notifications&amp;quot; in the (now renamed) Feature Flags list, but it doesn&#39;t seem to be required to enable support.&lt;/p&gt;
&lt;h2&gt;Add To Homescreen still required, still not promoted&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/bPBiCk4zfC-600.png&quot; alt=&quot;Screenshot of a push request&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;900&quot; srcset=&quot;https://www.steele.blue/img/bPBiCk4zfC-600.png 600w, https://www.steele.blue/img/bPBiCk4zfC-828.png 828w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Another user-facing limitation: only webapps which have been added to (and launched from) the Home Screen have all the APIs needed to enable Web Push.
This is clearly an intentional decision by Apple to prevent abuse of a powerful feature, but it also has a similarly limiting effect of the usability of the feature.&lt;/p&gt;
&lt;p&gt;As &lt;a href=&quot;https://tinyletter.com/firt/letters/webpush-for-ios-chatgpt-for-web-devs-apple-vision-pro-updates-to-chrome-and-more&quot;&gt;Maximiliano Firtman notes&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Users will never know that adding that web app to the home screen will give it more power. Apple doesn&#39;t support app banners, an installation API, or a menu term change (such as &amp;quot;Install&amp;quot;), maintaining a classic Apple decision to hide PWAs from the user while still technically available.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This requirement still exists. Given my anecdotal observation that I&#39;ve never seen a non-developer iOS users add a website to their home screen, it&#39;ll take pretty dedicated in-app tutorials to guide users.&lt;/p&gt;
&lt;h2&gt;You can now (mostly) feature-detect support&lt;/h2&gt;
&lt;p&gt;One big hassle with Web Push on iOS 16 was that APIs would be available, but weren&#39;t effectively implemented when the limitations above were in place.
So if you were trying to check for the existence of particular APIs before working with them, you&#39;d only see errors at runtime for an app that wasn&#39;t added to the home screen, for example.&lt;/p&gt;
&lt;p&gt;Luckily, this appears to have been cleared up. These combinations of checks should be sufficient:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// Service Workers supported&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (!&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;serviceWorker&#39;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; navigator&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// Web Push supported&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; registration&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; navigator&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;serviceWorker&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;ready&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (!&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;pushManager&#39;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; registration&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A site with an installed Service Worker which is installed on the home screen will consistently pass these checks.
Sites which are running in &amp;quot;standard&amp;quot; Safari won&#39;t pass the second check, and you can bail.&lt;/p&gt;
&lt;p&gt;If you&#39;re looking to give users some guidance that the feature is available, you&#39;ll need to add some additional checks in your code; as &lt;code&gt;navigator.serviceWorker&lt;/code&gt; will exist, but not &lt;code&gt;registration.pushManager&lt;/code&gt;.
But, since other browsers support Service Workers and not Web Push, I wasn&#39;t able to find a clean way to feature detect this, and resorted to browser sniffing, so I could show the help context.&lt;/p&gt;
&lt;h2&gt;It&#39;s workable, for some definition of workable&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/LmRMeHeBRS-600.png&quot; alt=&quot;Screenshot of a lock screen with lots of push notifications&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;758&quot; srcset=&quot;https://www.steele.blue/img/LmRMeHeBRS-600.png 600w, https://www.steele.blue/img/LmRMeHeBRS-828.png 828w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Ultimately I ended up using this for my &lt;a href=&quot;https://www.steele.blue/geofence-pizza-ordering/&quot;&gt;gravel bike tracking website&lt;/a&gt;, in order to send updates to folks watching at home as I made it to each stop, which I had bounded with a geofence.
I also setup a push notification to go to my phone when it &amp;quot;successfully&amp;quot; ordered a pizza. And while the push notification made it to my phone, the pizza did not ultimately get ordered.&lt;/p&gt;
&lt;p&gt;So maybe there&#39;s the lesson: with enough dedication and grit, you can get push notifications to show up on iOS devices, and it&#39;s a little easier with iOS 17.
And depending on what you&#39;re trying to integrate with, it may not even be the most brittle link in the chain anymore.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/81g86QTqTB-600.jpeg&quot; alt=&quot;A push notification indicating pizza was delivered&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;2133&quot; srcset=&quot;https://www.steele.blue/img/81g86QTqTB-600.jpeg 600w, https://www.steele.blue/img/81g86QTqTB-1000.jpeg 1000w, https://www.steele.blue/img/81g86QTqTB-1600.jpeg 1600w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>I wired up my bike&#39;s GPS to order me pizza during a gravel race 🍕</title>
    <link href="https://www.steele.blue/geofence-pizza-ordering/" />
    <updated>2023-09-10T00:00:00Z</updated>
    <id>https://www.steele.blue/geofence-pizza-ordering/</id>
    <content type="html">&lt;p&gt;As harvest season begins here in the Midwest, I once again celebrate by grinding Nebraska gravel at the &lt;a href=&quot;https://www.gravel-worlds.com/the-long-voyage&quot;&gt;Gravel Worlds Long Voyage&lt;/a&gt; bike race.
As in previous years (&lt;a href=&quot;https://www.steele.blue/gravel-worlds&quot;&gt;2021&lt;/a&gt;) (&lt;a href=&quot;https://www.steele.blue/serverless-bike-gps&quot;&gt;2022&lt;/a&gt;), I spent more time writing code for a marginally-useful project than I did training. But hey, I finished!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/IGsn_QHxwQ-600.jpeg&quot; alt=&quot;Matt riding on Gravel Worlds&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4005&quot; height=&quot;2670&quot; srcset=&quot;https://www.steele.blue/img/IGsn_QHxwQ-600.jpeg 600w, https://www.steele.blue/img/IGsn_QHxwQ-1000.jpeg 1000w, https://www.steele.blue/img/IGsn_QHxwQ-4005.jpeg 4005w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Fueled by Pizza&lt;/h2&gt;
&lt;p&gt;My goal this year involved optimizing my food choices during the race: pizza from &lt;a href=&quot;https://www.caseys.com/&quot;&gt;Casey&#39;s General Store&lt;/a&gt;.
These convenience stores are S-Tier options when out in the middle of nowhere.
In addition to snacks and drinks, most Casey&#39;s have a kitchen, and have pretty decent grab &#39;n go slices of pizza.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/LoK_UKm3Kt-600.jpeg&quot; alt=&quot;A sample of the pizza on offer at a Casey&#39;s&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;679&quot; srcset=&quot;https://www.steele.blue/img/LoK_UKm3Kt-600.jpeg 600w, https://www.steele.blue/img/LoK_UKm3Kt-650.jpeg 650w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;My problem: last year, there were so many faster riders ahead of me, that &lt;strong&gt;all the pizza was taken by the time I made it to the stops.&lt;/strong&gt; This is an outrage!&lt;/p&gt;
&lt;p&gt;This year, I knew I had to do better. But with time winding down on improving my fitness, I had to resort to the latent superpower of software hackery.
My thought: &lt;strong&gt;why not have a fresh pizza ordered ahead of time, scheduled precisely for when I arrived?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;More precisely, I could write a script that ordered a pizza for me, GPS-triggered by my bike leaving a geofence about an hour from the stop. Building this &lt;a href=&quot;https://www.steele.blue/serverless-bike-gps&quot;&gt;on top of the serverless GPS tracker I made last year&lt;/a&gt; should fit into the architecture pretty well.&lt;/p&gt;
&lt;h2&gt;Casey&#39;s Pizza API When&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/gYvTPiomB2-600.png&quot; alt=&quot;Architecture diagram&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;691&quot; height=&quot;335&quot; srcset=&quot;https://www.steele.blue/img/gYvTPiomB2-600.png 600w, https://www.steele.blue/img/gYvTPiomB2-691.png 691w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;The overall design: I setup a geofence within the AWS Location service, which was monitoring my GPS tracker.
When my tracker exited the geofence, it would trigger a Lambda function that calculates an ETA for my next stop, and orders the pizza.&lt;/p&gt;
&lt;p&gt;The problem: Casey&#39;s doesn&#39;t have a public API for online ordering.
So I had to resort to alternate approaches. More specifically, I fell back to &lt;strong&gt;screen scraping the website&lt;/strong&gt;, everyone&#39;s favorite hack.
I&#39;ve had to screen-scrape for other projects (such as &lt;a href=&quot;https://www.steele.blue/secret-strava&quot;&gt;updating privacy on Strava activities&lt;/a&gt;), but since Casey&#39;s website was a complex React app that rendered everything on the client, I had to use a more powerful scraper; this time powered by &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Getting Playwright to run in a Lambda was An Experience. Full writeup &lt;a href=&quot;https://www.steele.blue/playwright-on-lambda&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To keep track of the status, I also setup a push notification to be delivered to my phone (and watch) on a success or failure. Configuring Web Push to work on iOS devices is probably worthy of its own post.&lt;/p&gt;
&lt;h2&gt;Pizza False Positive&lt;/h2&gt;
&lt;p&gt;I had the triggering geofence configured around mile 180 of the race, with the pizza setup to be delivered at mile 200.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/81g86QTqTB-600.jpeg&quot; alt=&quot;A push notification indicating pizza was delivered&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;2133&quot; srcset=&quot;https://www.steele.blue/img/81g86QTqTB-600.jpeg 600w, https://www.steele.blue/img/81g86QTqTB-1000.jpeg 1000w, https://www.steele.blue/img/81g86QTqTB-1600.jpeg 1600w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;As I left the geofence, I got a push notification on my watch saying that the pizza had been successfully ordered.&lt;/p&gt;
&lt;p&gt;But when I made it to the stop, &lt;strong&gt;there was nothing at the counter, and they had no record of an order&lt;/strong&gt;. And sure enough, I checked my account, and no order had been placed.
&lt;strong&gt;False positive.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There were a few pre-made slices available, so I picked those up. They left a bitter taste in my mouth, not just because they weren&#39;t especially fresh. Through the rest of the 300-mile race, all I could think about was what might have went wrong with my function.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/AY11cW32Kp-600.jpeg&quot; alt=&quot;A pair of dirty legs, eating slices of pre-made pizza&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;2133&quot; srcset=&quot;https://www.steele.blue/img/AY11cW32Kp-600.jpeg 600w, https://www.steele.blue/img/AY11cW32Kp-1000.jpeg 1000w, https://www.steele.blue/img/AY11cW32Kp-1600.jpeg 1600w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;After finishing the race, I made it to a computer and quickly checked the logs to see what had gone wrong. But to my chagrin, there was nothing in the logs indicating what caused the failure; it was just a silent success. I had nothing to go on to try and debug.&lt;/p&gt;
&lt;p&gt;A few days later, I enhanced the Lambda to capture a video of the browser in action and upload it to an S3 bucket for analysis. Running a test of the updated behavior, &lt;strong&gt;it finally worked&lt;/strong&gt;. I picked up a fresh Hawaiian pizza, and we enjoyed the pie from the comfort of our home.&lt;/p&gt;
&lt;p&gt;I&#39;m still not entirely sure why it worked. My going theory is that the Lambda had terminated processing as soon as the final &lt;code&gt;form.submit()&lt;/code&gt; went through in the embedded Playwright browser. It&#39;s quite possible that the online ordering website saw that the browser never received a Success response, and didn&#39;t fully process the order.
My guess is that the additional time spent processing and uploading the video gave the browser sufficient time to clean up properly, with serendipity in timing.&lt;/p&gt;
&lt;h2&gt;Pizza Lessons&lt;/h2&gt;
&lt;p&gt;While I was bummed the pizza ordering functionality didn&#39;t run, I think it&#39;s in a good spot to try again in upcoming races.
I also learned a lot while building this out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consistently screen-scraping a React client-side app with a browser running in the cloud: possible, but boy howdy is it finicky. If I were to redo project, I may opt for reverse engineering one of their native apps, focusing on triggering their APIs directly&lt;/li&gt;
&lt;li&gt;Having a good workflow to simulate geospatial behavior is necessary. At first, I setup a geofence around my house for testing, but having to leave for a walk at 11pm gets pretty old, pretty fast&lt;/li&gt;
&lt;li&gt;That said, don&#39;t skimp on real-world testing. Prior to the race, I was mostly testing the function by running it on my local workstation, not in a Lambda. The successes of the local functions gave me false confidence that everything was working as expected&lt;/li&gt;
&lt;li&gt;If you&#39;re worried about running out of Casey&#39;s pizza during a gravel race, another option is to just let the groups ahead of you get more than 30 minutes ahead, so they have plenty of time to make more slices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code is available on GitHub: https://github.com/mattdsteele/spot-tracker-tracker/tree/main/pizza-function&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Running a Playwright script on AWS Lambda</title>
    <link href="https://www.steele.blue/playwright-on-lambda/" />
    <updated>2023-09-09T00:00:00Z</updated>
    <id>https://www.steele.blue/playwright-on-lambda/</id>
    <content type="html">&lt;p&gt;I had a hell of a time getting a &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright script&lt;/a&gt; to successfully run in an AWS Lambda.
There&#39;s a few guides on various ways to handle this, but none of them worked out of the box.
For posterity, here&#39;s what worked for me.&lt;/p&gt;
&lt;h2&gt;Use a Docker Container&lt;/h2&gt;
&lt;p&gt;The two primary ways I&#39;ve seen Playwright and Puppeteer implemented are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Using the default Node runtime, with the Chromium browser installed as a separate layer. This is the approach taken by the &lt;a href=&quot;https://www.npmjs.com/package/@sparticuz/chromium&quot;&gt;chromium-lambda&lt;/a&gt; npm package.&lt;/li&gt;
&lt;li&gt;Using a Lambda Docker runtime, building off a Microsoft-provided base layer with Chromium pre-installed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I was never able to make the Node runtime work, but did have success with the Docker runtime, so I&#39;d recommend using that architecture.&lt;/p&gt;
&lt;p&gt;Most of this started from Lari Haataja&#39;s &lt;a href=&quot;https://larihaataja.com/running-e2e-tests-playwright-aws-lambda/&quot;&gt;excellent blog post&lt;/a&gt; on the various options and configuration needed to get started.&lt;/p&gt;
&lt;h2&gt;Write a standard Playwright script&lt;/h2&gt;
&lt;p&gt;You can follow the basic guidelines &lt;a href=&quot;https://playwright.dev/docs/api/class-playwright&quot;&gt;in the Playwright docs&lt;/a&gt; to launch your script.&lt;/p&gt;
&lt;p&gt;I found it was helpful to move the Playwright code to a shared file, and have an entry point import it to run locally, and a second file with the Lambda specifics:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;/* shared.ts contains Playwright logic */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;LaunchOptions&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;playwright&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; main&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;launchOptions&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;LaunchOptions&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;): &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;Promise&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;Result&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;  // launches the function&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;/* start-local.ts runs in &quot;headful&quot; mode */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;./shared&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; main&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({ &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;headless:&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;/* lambda.ts runs in &quot;headless&quot; mode */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;APIGatewayProxyResult&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;EventBridgeEvent&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Handler&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;aws-lambda&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;./shared&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; args&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;  &#39;--autoplay-policy=user-gesture-required&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;  &#39;--disable-background-networking&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;  // More flags configured, see below&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; handler&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;Handler&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  event&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;EventBridgeEvent&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;Location Geofence Event&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;GeofenceType&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  context&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;): &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;Promise&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;APIGatewayProxyResult&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; results&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; main&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    args&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    headless:&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&#39;ll notice a number of &lt;code&gt;args&lt;/code&gt; I passed into the Lambda - these are Chromium flags I found I had to enable to have Chromium launch successfully on the Lambda.
You can see a full list of them &lt;a href=&quot;https://github.com/mattdsteele/spot-tracker-tracker/blob/main/pizza-function/src/lambda.ts#L7-L46&quot;&gt;in my repo&lt;/a&gt;, which I found from Vikash Loomba&#39;s GitHub.&lt;/p&gt;
&lt;h2&gt;Dockerfile&lt;/h2&gt;
&lt;p&gt;Microsoft provides &lt;a href=&quot;https://playwright.dev/docs/docker&quot;&gt;Docker base images&lt;/a&gt; with browsers and dependencies already installed; so this will be the starting point for our containers.&lt;/p&gt;
&lt;p&gt;Mostly this is following &lt;a href=&quot;https://larihaataja.com/running-e2e-tests-playwright-aws-lambda/#the-dockerfile&quot;&gt;the guide from Lari&lt;/a&gt;, but I did have to pin down the specific version of the base layer so it corresponded to the Playwright version I was using (1.37.0), so my base layer looked like:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; mcr.microsoft.com/playwright:v1.37.0-focal &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; build-image&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The container definition also bundles AWS&#39;s &lt;a href=&quot;https://github.com/aws/aws-lambda-runtime-interface-emulator/&quot;&gt;Runtime Interface Emulator&lt;/a&gt;, which supports starting up the container locally, using Lambda&#39;s APIs.
I ended up not using this very often since I had a local, non-containerized entry point I could quickly run, but it was still a nice feature to have.&lt;/p&gt;
&lt;p&gt;You can see the complete Dockerfile &lt;a href=&quot;https://github.com/mattdsteele/spot-tracker-tracker/blob/main/pizza-function/Dockerfile&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There&#39;s also an entrypoint bash script which is mostly boilerplate, and can be seen &lt;a href=&quot;https://github.com/mattdsteele/spot-tracker-tracker/blob/main/pizza-function/entrypoint.sh&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Build and deploy the container&lt;/h2&gt;
&lt;p&gt;To deploy to AWS, you&#39;ll need to build the container, and deploy it to a container registry in your AWS tenant.
Most of this is boilerplate with the AWS CLI, and is documented on &lt;a href=&quot;https://larihaataja.com/running-e2e-tests-playwright-aws-lambda/#deploying-to-aws-lambda&quot;&gt;Lari&#39;s post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I found that I was running the build/deploy frequently enough that I added it to a shell script, which you can see &lt;a href=&quot;https://github.com/mattdsteele/spot-tracker-tracker/blob/main/pizza-function/deploy.sh&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;On my (weak) machine it took about 5 minutes to build and push the container.
Once available, you can create a new Lambda function, and define it as a Container Image as documented &lt;a href=&quot;https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/&quot;&gt;on AWS&#39;s page&lt;/a&gt;, and deploy using the &lt;code&gt;latest&lt;/code&gt; tag of your container.&lt;/p&gt;
&lt;p&gt;I had to make a few other changes to the container&#39;s configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Increase memory&lt;/strong&gt; - The default memory allocated wasn&#39;t high enough to launch and run Chromium, so I upped the memory at runtime to 3GB. You may be able to optimize this better to save some money.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Increase timeout&lt;/strong&gt; - My script took about 2 minutes to finish running, so I had to increase the timeout so Lambda wouldn&#39;t kill it after a few seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update &lt;code&gt;HOME&lt;/code&gt; environment variable&lt;/strong&gt; When Chromium launches, it tries to put files in a subdirectory of &lt;code&gt;$HOME&lt;/code&gt;, which isn&#39;t writable by default in a Lambda runtime. To get around this, you can set &lt;code&gt;HOME=/tmp&lt;/code&gt; in the environment variables configuration. Thanks to &lt;a href=&quot;https://github.com/alixaxel/chrome-aws-lambda/issues/131#issuecomment-629541023&quot;&gt;this GitHub issue&lt;/a&gt; for the workaround.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once it&#39;s all configured, you can treat this like any other function; and define triggering events using the AWS console, or whatever method you prefer.
You can also run test events through to validate the page is invoking successfully.&lt;/p&gt;
&lt;h2&gt;Why go through all this? Would jsdom/WebDriver/Puppeteer be easier?&lt;/h2&gt;
&lt;p&gt;Having to start up an entire web browser to screen-scrape a site without an API is a giant pain, and I wouldn&#39;t recommend it as the primary approach to automating an action.
But some websites don&#39;t have an API, and depend heavily on JavaScript to render their site (this was a React SPA), so running a full browser was the only choice I could rely on.&lt;/p&gt;
&lt;p&gt;There&#39;s a plethora of other automation tools out there, but I wanted to try out Playwright, and found their API surprisingly usable for humans. I&#39;d recommend &lt;a href=&quot;https://changelog.com/jsparty/253&quot;&gt;this JS Party&lt;/a&gt; episode for a deeper introduction to Playwright.&lt;/p&gt;
&lt;h2&gt;Bonus: Record and review video playbacks&lt;/h2&gt;
&lt;p&gt;I found it difficult to debug what was actually happening when my function would run. I could add additional &lt;code&gt;console.log&lt;/code&gt; statements, but I found it was just easier to save off a recording of the automation run and upload it to an S3 bucket for later review.&lt;/p&gt;
&lt;p&gt;This was fairly straightforward with Playwright&#39;s &lt;a href=&quot;https://playwright.dev/docs/videos#record-video&quot;&gt;video recording API&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;readFile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;fs/promises&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;S3Client&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;PutObjectCommand&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;@aws-sdk/client-s3&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; S3_BUCKET&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;your-bucket-id&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; uploadVideo&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pathToVideo&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; Key&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;basename&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pathToVideo&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; Body&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; readFile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pathToVideo&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; S3Client&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; upload&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; PutObjectCommand&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    Bucket:&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; S3_BUCKET&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    Body&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    Key&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;send&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;upload&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, for each run you have a full playback video to inspect!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Overly Complicate Starting Your Vehicle, Using Home Assistant, an ESP8266, and a Spare Car Fob</title>
    <link href="https://www.steele.blue/esp8266-chevy-bolt-fob-homeassistant/" />
    <updated>2023-07-02T00:00:00Z</updated>
    <id>https://www.steele.blue/esp8266-chevy-bolt-fob-homeassistant/</id>
    <content type="html">&lt;video controls=&quot;&quot; preload=&quot;none&quot; muted=&quot;true&quot; poster=&quot;https://www.steele.blue/images/car-lock-snap.png&quot;&gt;
  &lt;source src=&quot;https://www.steele.blue/videos/car-lock-demo.mp4&quot; type=&quot;video/mp4&quot;&gt;
&lt;/video&gt;
&lt;p&gt;I recently bought a new vehicle (the now-discontinued Chevy Bolt), it&#39;s great! Coming from a 14 year old car, everything about it feels space-age.&lt;/p&gt;
&lt;p&gt;This is the first vehicle I&#39;ve owned that has a remote start button on the keyfob, which I&#39;m fascinated by.  It also integrated with a mobile app, which enabled locking &amp;amp; remote start from a phone, but requires a monthly subscription.&lt;/p&gt;
&lt;p&gt;I wanted to see if I could build something similar with &amp;quot;standard&amp;quot; IoT and home automation tools. It would only work when the car is at home, but it would accomplish my goal of letting me remote start my car on a cold day without having to find my keys.&lt;/p&gt;
&lt;p&gt;I had a few goals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Don&#39;t do anything warranty-voiding. That means no messing with the car&#39;s wiring or internal computer&lt;/li&gt;
&lt;li&gt;Everything runs on-prem - because &lt;a href=&quot;https://www.iot-inc.com/the-s-in-iot-stands-for-security-article/&quot;&gt;the &amp;quot;S&amp;quot; in IoT stands for Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Integrate into an existing Home Assistant setup, so I can tie into rest of the home automation ecosystem I&#39;ve been building up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A few others have gone down this route, by wiring up a spare fob to a microcontroller (Arduino, Raspberry Pi, etc), and &amp;quot;pressing&amp;quot; the unlock/lock/start buttons electronically. Seems feasible!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://hackaday.com/2021/01/06/automating-your-car-with-a-spare-fob-and-an-esp8266/&lt;/li&gt;
&lt;li&gt;https://www.hackster.io/user03583/ok-google-start-my-car-7088dd&lt;/li&gt;
&lt;li&gt;https://gitlab.com/milagrofrost/esp8266-car-key-fob-iot/&lt;/li&gt;
&lt;li&gt;https://github.com/Radacon/ESP_Remote_Start&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Hardware&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/cdgMGSzJjC-600.jpeg&quot; alt=&quot;Pinout of the board&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;3478&quot; srcset=&quot;https://www.steele.blue/img/cdgMGSzJjC-600.jpeg 600w, https://www.steele.blue/img/cdgMGSzJjC-1000.jpeg 1000w, https://www.steele.blue/img/cdgMGSzJjC-3024.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spare Car Fob&lt;/strong&gt; - I picked up a cheap aftermarket fob on AliExpress, though it took a bit of time confirming compatibility.
I ordered this one for the 2023 Bolt: https://www.aliexpress.us/item/3256804168747183.html&lt;/p&gt;
&lt;p&gt;The Bolt lets you program additional fobs at home, without needing a dealer or locksmith. The metal &amp;quot;blade&amp;quot; isn&#39;t cut, but I&#39;m never using it for &amp;quot;actual&amp;quot; car operations, so I&#39;m not too worried.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESP8266&lt;/strong&gt; - Any microcontroller that can output 3.3V should work, and I&#39;d been wanting to experiment with ESP devices for a while. I used this: https://www.amazon.com/dp/B07HF44GBT&lt;/p&gt;
&lt;p&gt;I wired everything up using a &lt;a href=&quot;https://www.amazon.com/dp/B08D3FF6WY&quot;&gt;breakout board&lt;/a&gt;, since the circuits went straight from GPIO pins to the fob buttons.&lt;/p&gt;
&lt;p&gt;Some other projects I saw added diodes in the circuitry to prevent flyback voltage to the buttons, but I didn&#39;t find it was necessary. Let&#39;s see if future me regrets this!&lt;/p&gt;
&lt;h2&gt;Wiring Up the Fob&lt;/h2&gt;
&lt;p&gt;This took a bit of experimenting. Using a multimeter, I found the buttons were letting 3.3V run through, which went down to 0V when it was pressed down.&lt;/p&gt;
&lt;p&gt;The buttons are surface mounted on the fob&#39;s PCB. Removing them would be pretty easy with the right tools, such as a heat gun. I did not have said tools, so I just cranked up my soldering iron and jabbed at it until the buttons disintegrated. Hopefully you&#39;re better at this.&lt;/p&gt;
&lt;p&gt;Once I exposed the bare pads on the fob, there was a bit of trial and error to find which lead was connected to the fob, and which to ground (I used a breadboard and just tapped the pads while seeing if the car responded). From there, I soldered a wire between the connected pad, and onto a GPIO pin on the ESP8266, &lt;a href=&quot;https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/&quot;&gt;via this pinout&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/2LGynXHbd4-600.jpeg&quot; alt=&quot;Finished product&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3221&quot; height=&quot;1923&quot; srcset=&quot;https://www.steele.blue/img/2LGynXHbd4-600.jpeg 600w, https://www.steele.blue/img/2LGynXHbd4-1000.jpeg 1000w, https://www.steele.blue/img/2LGynXHbd4-3221.jpeg 3221w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Home Assistant&lt;/strong&gt; - I already had an instance running on a Raspberry Pi, so this was easy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESPHome&lt;/strong&gt; - This is a &lt;a href=&quot;https://esphome.io/&quot;&gt;really neat project&lt;/a&gt; that exposes an ESP8266 (or ESP32) as a device in Home Assistant. You upload their firmware on your ESP, and it&#39;ll securely integrate with your HA environment. You can then configure its operation via simple YAML, so you don&#39;t have to maintain any code or event loop.&lt;/p&gt;
&lt;p&gt;Here&#39;s the &amp;quot;Unlock&amp;quot; section of my configuration:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;output&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;platform&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;gpio&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    pin&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;      number&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;GPIO5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;      inverted&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    id&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;UnlockButton&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;platform&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;output&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;Unlock&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    output&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;UnlockButton&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;    duration&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;400ms&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;inverted: true&lt;/code&gt; - I found that fob buttons are &amp;quot;normally open&amp;quot;, so a button press brings it back down to 0V.&lt;/p&gt;
&lt;p&gt;Another thing I really liked about ESPHome: they have a &lt;a href=&quot;https://esphome.github.io/esp-web-tools/&quot;&gt;web-based flashing tool&lt;/a&gt; to upload the custom firmware onto your ESP8266.
Once flashed, everything else is done wirelessly via the onboard Wi-Fi chip.
This is &lt;em&gt;way&lt;/em&gt; more convenient than the Arduino or MicroPython workflows I&#39;d experimented with; which required driver installation and a terminal emulator.&lt;/p&gt;
&lt;p&gt;Once you&#39;ve setup the appropriate buttons in the ESPHome configuration section (I found 400ms worked for lock/unlock, and 3000ms was needed for remote start), install to the ESP via an OTA update, and Home Assistant should recognize the new devices.&lt;/p&gt;
&lt;p&gt;I then created a Dashboard in Home Assistant so I had easy access to the buttons.&lt;/p&gt;
&lt;h2&gt;A few things I learned&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/ttkCFSv_rD-600.jpeg&quot; alt=&quot;Finished product&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2346&quot; height=&quot;2215&quot; srcset=&quot;https://www.steele.blue/img/ttkCFSv_rD-600.jpeg 600w, https://www.steele.blue/img/ttkCFSv_rD-1000.jpeg 1000w, https://www.steele.blue/img/ttkCFSv_rD-2346.jpeg 2346w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;While testing the fob buttons, I discovered the “start vehicle” and “trigger alarm” buttons were reversed from how they were labeled. As did all my neighbors!&lt;/li&gt;
&lt;li&gt;Soldering on a surface-mount board is hard! I ended up bricking my first fob because I got a little solder on some adjoining transistors, which short-circuited the entire device and rendered it useless. I suppose this gets better with practice, but when you treat hardware projects &lt;a href=&quot;https://www.steele.blue/hardware-is-the-new-geocities/&quot;&gt;with the same rigor as a Geocities website&lt;/a&gt;, it&#39;s not something you improve at quickly.&lt;/li&gt;
&lt;li&gt;The ESP8266 is a really cool device; and it&#39;s fascinating to have something so easily programmable be at a price point where I can just treat the microcontroller as a dedicated component to this project.  With a Raspberry Pi or Arduino I&#39;d be sure to salvage after I was done with the project, but at less than 5 bucks, I don&#39;t feel bad soldering directly onto it.&lt;/li&gt;
&lt;li&gt;There&#39;s a few home automation/IoT aspects I haven&#39;t gotten to yet; designing and 3D printing a custom enclosure would be a nice way to finish the project. For now, it&#39;s just sitting on a shelf.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What is it good for, absolutely nothing&lt;/h2&gt;
&lt;p&gt;This setup has a few limitations. In particular, it only works when I&#39;m on-network in my house, and the car is in physical proximity to the fob, but I consider that a feature rather than a bug. Adding some physical safeguards helps me sleep a bit better.&lt;/p&gt;
&lt;p&gt;I still don&#39;t really know what I&#39;m going to do with the project. Since it&#39;s tied into home automation, I could easily setup a few interesting workflows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatically lock the vehicle at 10pm&lt;/li&gt;
&lt;li&gt;If the temperature drops below freezing and it&#39;s a workday, start the car 10 minutes before my commute&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&#39;m fully ready to admit these might be solutions in search of a problem, but I still had a fun time working on this. And maybe, the real vehicle automation is the friends we made along the way.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Generating Custom OpenGraph Cards with Gatsby and the 11ty Screenshot Service</title>
    <link href="https://www.steele.blue/gatsby-opengraph-11ty-screenshots/" />
    <updated>2023-03-13T00:00:00Z</updated>
    <id>https://www.steele.blue/gatsby-opengraph-11ty-screenshots/</id>
    <content type="html">&lt;p&gt;I made the sharable &lt;a href=&quot;https://www.opengraph.xyz/&quot;&gt;OpenGraph images&lt;/a&gt; on this site better by generating custom cards contextual to each post:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/gatsby-opengraph-11ty-screenshots/7CWUTxYXTW-600.png&quot; alt=&quot;OpenGraph social card for blog post&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;861&quot; height=&quot;861&quot; srcset=&quot;https://www.steele.blue/gatsby-opengraph-11ty-screenshots/7CWUTxYXTW-600.png 600w, https://www.steele.blue/gatsby-opengraph-11ty-screenshots/7CWUTxYXTW-861.png 861w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;The core concept is to create a sidecar page for each post that looks like an OpenGraph card, then take a snapshot of the page, and set it as the &lt;code&gt;og:image&lt;/code&gt; meta tag on the original post.&lt;/p&gt;
&lt;p&gt;There are a &lt;a href=&quot;https://www.gatsbyjs.com/plugins/gatsby-plugin-react-social-cards/&quot;&gt;few Gatsby plugins&lt;/a&gt; that attempt to help, but they rely on Puppeteer to generate screenshots at build-time, which can slow down a build pretty significantly, and adds a heavy dependency to the toolchain.&lt;/p&gt;
&lt;p&gt;The approach I took leverages Gatsby&#39;s dynamic page generation, but leaves the image rendering to external services, only generating as-needed at runtime.&lt;/p&gt;
&lt;p&gt;You can &lt;a href=&quot;https://www.steele.blue/gatsby-opengraph-11ty-screenshots/social-card/&quot;&gt;view the generated card for this post&lt;/a&gt; to get a feel for what&#39;s getting generated, and see a full implementation diff &lt;a href=&quot;https://github.com/mattdsteele/steele.blue/compare/f1fcdf0955fb6c4211c4d8073fc16024b3377572...5e2cd5047619d4da85c3a2809364dd1b1a47ed5c&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;React Card Component&lt;/h1&gt;
&lt;p&gt;Like other Gatsby pages, this is a React component you&#39;ll set props via Gatsby. I placed it in &lt;code&gt;components/social-card.js&lt;/code&gt;.
Since OpenGraph images are generally a fixed size, you can target your design for those specific dimensions.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; React&lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;react&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// It&#39;s a Gatsby component, so pull in whatever assets are useful for the page&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; avatar&lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;../../content/images/avatar-transparent.png&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// I&#39;m using CSS Modules for styles, but you do you&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;card&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardTitle&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardExcerpt&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardDate&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardMetadata&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardAuthor&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardAvatar&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardTransparency&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;./social-card.module.css&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; SocialCard&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = ({ &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pageContext&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;excerpt&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } }) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;card&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;h1&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardTitle&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;h1&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardExcerpt&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;excerpt&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;img&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardAvatar&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;avatar&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; alt&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;Avatar&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardMetadata&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardTransparency&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;].&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;join&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardDate&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cardAuthor&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;steele.blue&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;      &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; SocialCard&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Gatsby Card Pages&lt;/h1&gt;
&lt;p&gt;In &lt;code&gt;gatsby-node.js&lt;/code&gt;, enhance the &lt;code&gt;onCreateNode&lt;/code&gt; method (or wherever you&#39;re currently calling &lt;code&gt;createPage&lt;/code&gt;) to generate a new page, which will be for your component&#39;s social card.&lt;/p&gt;
&lt;p&gt;First, query for all blog posts, pulling the props you&#39;ll want:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;  // Query for all blog posts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; socialCardQuery&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; graphql&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;  allMarkdownRemark {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;    nodes {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      excerpt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      fields {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;        slug&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;        date(formatString: &quot;MMM DD&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      frontmatter {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;        title&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;then&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create a Social Card page for each post, passing in the relevant props:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; pageContexts&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;socialCardQuery&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;allMarkdownRemark&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;nodes&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;node&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    slug:&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; node&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fields&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;slug&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    pageContext:&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;      title:&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; node&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;frontmatter&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;      excerpt:&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; node&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;excerpt&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;      date:&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; node&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fields&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; socialCard&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;resolve&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;src/components/social-card.js&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pageContexts&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;forEach&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;page&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;createPage&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    component:&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; socialCard&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    path:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; `&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;page&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;slug&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;/social-card`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    context:&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    ...&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;page&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pageContext&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once these are getting generated, you can just add &lt;code&gt;/social-card&lt;/code&gt; to the end of any permalink to see what&#39;s getting generated, and iterate until you&#39;re happy.&lt;/p&gt;
&lt;h1&gt;OpenGraph Screenshots via 11ty API&lt;/h1&gt;
&lt;p&gt;Ultimately OpenGraph expects an image, and we currently have a website.
Fortunately for us, the Eleventy project supports a service that will &lt;a href=&quot;https://www.11ty.dev/docs/services/screenshots/&quot;&gt;generate screenshots of pages via URLs&lt;/a&gt;, with presets for OpenGraph dimensions.&lt;/p&gt;
&lt;p&gt;In your blog posts, you&#39;ll want to define the &lt;code&gt;og:image&lt;/code&gt; meta tag based on these properties. Here I&#39;m using React Helmet, but you could also do it with the &lt;a href=&quot;https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-head/&quot;&gt;Gatsby Head API&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// Helper methods to configure the Eleventy Screenshot Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; socialCardUrl&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; `&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;/social-card/`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; screenshotUrl&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;uri&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; encoded&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;encodeURIComponent&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;socialCardUrl&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;uri&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;  // https://github.com/11ty/api-screenshot/#manual-cache-busting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; cacheBust&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; Date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;getTime&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; `https://v1.screenshot.11ty.dev/&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;encoded&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;/opengraph/_&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cacheBust&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// Then in your blog post&#39;s component, set the value:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;Helmet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  meta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    property:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;og:image&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    content:&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; screenshotUrl&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;    // etc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;  ]&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, you&#39;re off to the races! As Zach put it, perhaps folks will start clicking on my posts &lt;a href=&quot;https://www.zachleat.com/web/automatic-opengraph/&quot;&gt;if I work really hard on the OpenGraph images&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Keeping your Fediverse followers when moving Owncast servers</title>
    <link href="https://www.steele.blue/moving-owncast-followers-fediverse/" />
    <updated>2023-03-12T00:00:00Z</updated>
    <id>https://www.steele.blue/moving-owncast-followers-fediverse/</id>
    <content type="html">&lt;p&gt;So you&#39;ve got an Owncast server running, and it&#39;s &lt;a href=&quot;https://owncast.online/docs/social/&quot;&gt;part of the Fediverse&lt;/a&gt;, so your Mastodon friends can be notified when you start a stream. Neat!&lt;/p&gt;
&lt;p&gt;If you want to switch hosting providers, or install from scratch, you&#39;ll want to make sure your followers continue to receive notifications. This could be easy, or a bit of a challenge. Either way, here&#39;s what to do!&lt;/p&gt;
&lt;h1&gt;The easy way - Copy over the database&lt;/h1&gt;
&lt;p&gt;The simple solution is to just copy over the &lt;code&gt;data/&lt;/code&gt; folder from your old server, onto the new one. This includes the previous database, and assets like logos, etc.&lt;/p&gt;
&lt;p&gt;This will work if you&#39;re moving to the same (or a newer) version of Owncast.&lt;/p&gt;
&lt;h1&gt;If that doesn&#39;t work&lt;/h1&gt;
&lt;p&gt;In my case, I couldn&#39;t migrate over because I was &lt;a href=&quot;https://www.steele.blue/moving-owncast-followers-fediverse/owncast-raspberry-pi-4/&quot;&gt;moving to an older Owncast version&lt;/a&gt;, for complicated reasons.&lt;/p&gt;
&lt;p&gt;So, I had to manually copy over just the Fediverse/ActivityPub data from the old instance.&lt;/p&gt;
&lt;p&gt;To finish this, you&#39;ll need the sqlite3 client on both instances. On Debian, this is &lt;code&gt;sudo apt install sqlite3&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Extract ActivityPub tables and configuration values&lt;/h2&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# On the old server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;sqlite3&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; owncast.db&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &quot;.dump ap_followers ap_outbox ap_accepted_activities datastore&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; &gt; &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;owncast-activitypub.sql&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the &lt;code&gt;owncast-activitypub.sql&lt;/code&gt; file off your old server using whatever means you like, scp, etc.&lt;/p&gt;
&lt;h2&gt;Tweak import script&lt;/h2&gt;
&lt;p&gt;I had to make these changes to the sql file manually:&lt;/p&gt;
&lt;h3&gt;Remove table schema creation&lt;/h3&gt;
&lt;p&gt;By default the &lt;code&gt;.dump&lt;/code&gt; operation includes &lt;code&gt;CREATE TABLE&lt;/code&gt; lines. Remove them from the script so you&#39;re just left with &lt;code&gt;INSERT&lt;/code&gt; statements.&lt;/p&gt;
&lt;h3&gt;Accomodate data differences&lt;/h3&gt;
&lt;p&gt;Since I was moving onto an older version of Owncast, the database had a different schema, so you&#39;ll have to tweak the &lt;code&gt;INSERT&lt;/code&gt; statements to reflect the version you&#39;re moving to. i.e., we&#39;re performing a &amp;quot;backwards migration&amp;quot;.&lt;/p&gt;
&lt;p&gt;You can see which database version you&#39;re running with:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# On the new instance&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;sqlite3&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; owncast.db&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &quot;select value from config&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check whether any migrations have happened to the &lt;code&gt;ap_*&lt;/code&gt; tables; you can view the &lt;a href=&quot;https://github.com/owncast/owncast/blob/develop/core/data/migrations.go&quot;&gt;Forward migrations&lt;/a&gt; file for details.
In particular, I was moving down to version &lt;code&gt;3&lt;/code&gt;, and so I had to remove the &lt;code&gt;request_object&lt;/code&gt; column from the &lt;code&gt;ap_followers&lt;/code&gt; line:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;-- Convert this:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; ap_followers &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;https://carhenge.club/users/mattdsteele&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;https://carhenge.club/users/mattdsteele/inbox&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;Matt Steele&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;mattdsteele@carhenge.club&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;https://s3-us-east-2.amazonaws.com/carhengeclub/accounts/avatars/000/022/282/original/238f5dcb7409495b.jpeg&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;https://carhenge.club/79be21a3-3c15-4174-9cc2-116440ccda04&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;2022-11-22 04:39:12&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;2022-11-22 04:39:12.733741659+00:00&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;NULL&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,X&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;somelongvalue&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;-- Into this:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; ap_followers &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;https://carhenge.club/users/mattdsteele&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;https://carhenge.club/users/mattdsteele/inbox&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;Matt Steele&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;mattdsteele@carhenge.club&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;https://s3-us-east-2.amazonaws.com/carhengeclub/accounts/avatars/000/022/282/original/238f5dcb7409495b.jpeg&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;https://carhenge.club/79be21a3-3c15-4174-9cc2-116440ccda04&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;2022-11-22 04:39:12&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;2022-11-22 04:39:12.733741659+00:00&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;NULL&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Depending on the version you&#39;re migrating to/from, you may have to make additional changes, or none at all!&lt;/p&gt;
&lt;h3&gt;Extract config table values&lt;/h3&gt;
&lt;p&gt;Remove all the &lt;code&gt;INSERT INTO datastore&lt;/code&gt; lines except for the following:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;federation_enabled&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;federation_go_live_message&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;federation_username&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These need to be the same on the new host, in order for clients to validate identity.&lt;/p&gt;
&lt;h2&gt;Import data into new instance&lt;/h2&gt;
&lt;p&gt;After making these changes, copy the &lt;code&gt;owncast-activitypub.sql&lt;/code&gt; file onto the new host, place it in the &lt;code&gt;data&lt;/code&gt; folder, and run:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# On the new server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;sqlite3&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; owncast.db&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &quot;.read owncast-activitypub.sql&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that, you should be able to test creating a new stream, or &lt;a href=&quot;https://owncast.online/docs/social/#composing-messages-to-your-followers&quot;&gt;compose a new post&lt;/a&gt; in the Admin header, and you&#39;re off to the races!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Running Owncast with Hardware Acceleration on a Raspberry Pi 4</title>
    <link href="https://www.steele.blue/owncast-raspberry-pi-4/" />
    <updated>2023-03-05T00:00:00Z</updated>
    <id>https://www.steele.blue/owncast-raspberry-pi-4/</id>
    <content type="html">&lt;p&gt;Owncast is a self-hosted platform for streaming video, which I&#39;ve been using for sharing all kinds of events, from &lt;a href=&quot;https://www.steele.blue/zwift-greenscreen&quot;&gt;Zwift racing&lt;/a&gt; to &lt;a href=&quot;https://www.steele.blue/indieweb-wedding-livestream&quot;&gt;my wedding&lt;/a&gt;.
It&#39;s been really pleasant to use as a Twitch alternative.&lt;/p&gt;
&lt;p&gt;Historically, I&#39;ve run my instance (https://stream.steele.blue) on a virtual server in The Cloud (specifically, a $6/month DigitalOcean instance), but wanted to explore moving it on-prem, using my own hardware.
I had a few goals in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Performance&lt;/em&gt; - Owncast uses ffmpeg under the hood to encode videos. If you have access to &#39;native&#39; hardware you can enable hardware acceleration to improve performance by offloading rendering to a GPU. This isn&#39;t just a theoretical gain; on the VPS I can only encode a single medium-quality stream before I peg the CPU.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Utility&lt;/em&gt; - I had a Raspberry Pi lying in storage after finishing up &lt;a href=&quot;https://www.steele.blue/photo-booth&quot;&gt;an earlier project&lt;/a&gt;, which feels like an incredible waste of resources. Imagine an entire Linux computer, just sitting in a drawer!&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Cost&lt;/em&gt; - Six bucks a month for a VPS isn&#39;t exorbitant, but it adds up. Within a year, I could buy a brand new Raspberry Pi (even at their inflated current prices)!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Owncast docs includes a nice reference on &lt;a href=&quot;https://owncast.online/docs/codecs/#raspberry-pi&quot;&gt;configuring a Raspberry Pi&lt;/a&gt; with hardware encoding.&lt;/p&gt;
&lt;p&gt;I used a Raspberry Pi 4 Model B with 2GB of RAM, and a 32GB MicroSD card I had leftover from previous projects.&lt;/p&gt;
&lt;p&gt;It&#39;s connected to my home network via Wi-Fi; which is also where the source RTMP stream is at.&lt;/p&gt;
&lt;h1&gt;tl;dr&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Here&#39;s what currently works&lt;/strong&gt;, as of March 2023:&lt;/p&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;p&gt;You&#39;ll need to &lt;a href=&quot;https://downloads.raspberrypi.org/raspios_full_armhf/images/raspios_full_armhf-2021-05-28/2021-05-07-raspios-buster-armhf-full.zip&quot;&gt;run an old version of Raspbian OS&lt;/a&gt;.
This is a 32-bit OS, even though the Pi 4 has a 64-bit CPU. More on that later.&lt;/p&gt;
&lt;p&gt;You&#39;ll also need a copy of ffmpeg which supports the &lt;a href=&quot;https://en.wikipedia.org/wiki/OpenMAX&quot;&gt;OpenMAX&lt;/a&gt; (omx) codec. The version available from the package manager will be sufficient: &lt;code&gt;sudo apt install ffmpeg&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The remainder of the setup from the &lt;a href=&quot;https://owncast.online/quickstart/configure/&quot;&gt;Owncast quickstart&lt;/a&gt; is sufficient. For example, I enabled HTTPS by running an instance of &lt;a href=&quot;https://caddyserver.com/&quot;&gt;Caddy&lt;/a&gt; on the Pi.&lt;/p&gt;
&lt;h2&gt;Configuration&lt;/h2&gt;
&lt;p&gt;Set up Owncast like the quickstart tells you. In the Admin console, under &amp;quot;Advanced Settings&amp;quot;, change the Video Codec to &amp;quot;OpenMax (omx) for Raspberry Pi&amp;quot;.&lt;/p&gt;
&lt;p&gt;My video configuration has two stream outputs defined:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1200kbps, Low hardware usage, 24fps (Low quality)&lt;/li&gt;
&lt;li&gt;4100kbps, Medium hardware usage, 60fps (High quality)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Feel free to experiment with &lt;a href=&quot;https://owncast.online/docs/video/&quot;&gt;other configurations&lt;/a&gt;, but this worked for me.&lt;/p&gt;
&lt;p&gt;I still serve the videos from S3 storage, which I had hosted in AWS, but &lt;a href=&quot;https://owncast.online/docs/storage/&quot;&gt;any of the S3 options should suffice&lt;/a&gt;. S3 is cheap and easily scalable, so there wasn&#39;t a need to pull that on-prem&lt;/p&gt;
&lt;h1&gt;What went wrong&lt;/h1&gt;
&lt;p&gt;As described above, I had to use an older, 32-bit version of Raspberry Pi OS, as well as an older version of Owncast, and won&#39;t be able to upgrade either of them as new releases come out.
That sucks! Here&#39;s the issues I ran into when trying to use current versions. Again, this is as current as of March 2023; I&#39;m hoping to try again in a few months.&lt;/p&gt;
&lt;h2&gt;Newer Raspberry Pi OSs don&#39;t have turnkey hardware encoding&lt;/h2&gt;
&lt;p&gt;Support for OMX on newer Raspberry Pi OS versions has gotten worse. Previously it wasn&#39;t supported on 64-bit Pi OS, but now it&#39;s gone from bullseye distros even on 32bit: https://github.com/raspberrypi/firmware/issues/1366#issuecomment-1034726587&lt;/p&gt;
&lt;p&gt;So I took a look at enabling the new option for hardware encoding (Video4Linux). After hours of investigation, I wasn&#39;t able to find a version of ffmpeg that would let me successfully encode/decode videos with V4L hardware acceleration.
The full gory details are &lt;a href=&quot;https://github.com/owncast/owncast/issues/1379#issuecomment-1445502469&quot;&gt;in Issue #1379&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;I&#39;m still pretty happy with this setup&lt;/h1&gt;
&lt;p&gt;Even with the issues I ran into getting this all running, I like where it ended up. I got a better-performing instance running on cheaper hardware, at the cost of a few hours of experimentation.
If I ever need to reconfigure things, it should be a 20-minute setup.&lt;/p&gt;
&lt;p&gt;Ideally I&#39;d have this running in a Docker container, but I didn&#39;t want to &lt;a href=&quot;https://butyoudontlooksick.com/articles/written-by-christine/the-spoon-theory/&quot;&gt;run out of spoons&lt;/a&gt; and was ready to declare victory and just use the software.&lt;/p&gt;
&lt;p&gt;Over the last few months I&#39;ve been reevaluating my use of cloud services, with a particular focus on virtual servers.
They occupy a &amp;quot;muddy middle&amp;quot; in terms of management responsibilities. Sure, Linode is going to spin up the instance, but I&#39;m still responsible for installing my app, applying OS patches, isolating it within my tenant, etc. All the while, I&#39;m &lt;a href=&quot;https://world.hey.com/dhh/five-values-guiding-our-cloud-exit-638add47&quot;&gt;paying as much to rent the donkey as it would cost to buy it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wouldn&#39;t want to run a big distributed project on hardware I have at home, but then again, I wouldn&#39;t want to run any home project that requires a distributed setup.&lt;/p&gt;
&lt;p&gt;But with the advent of simple Docker management tools like &lt;a href=&quot;https://www.portainer.io/&quot;&gt;Portainer&lt;/a&gt;, I&#39;ve been able to move a number of apps previously hosted on DigitalOcean or Linode out of the cloud, and onto machines running in my living room.
They&#39;ve got enough resources that I can scale vertically for quite a while, or pick up another Raspberry Pi or two if I want more &amp;quot;single app appliances&amp;quot;.
And with tools like &lt;a href=&quot;https://containrrr.dev/watchtower/&quot;&gt;Watchtower&lt;/a&gt; to help keep my containers up to date, and easier at-home network segmentation with &lt;a href=&quot;https://help.ui.com/hc/en-us/articles/9761080275607&quot;&gt;Unifi VLANs&lt;/a&gt;, it&#39;s never been easier to get a simple, secure home server up and running.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Vite is Neat</title>
    <link href="https://www.steele.blue/vite-is-neat/" />
    <updated>2023-01-31T00:00:00Z</updated>
    <id>https://www.steele.blue/vite-is-neat/</id>
    <content type="html">&lt;p&gt;I spoke at &lt;a href=&quot;https://nebraskajs.com&quot;&gt;NebraskaJS&lt;/a&gt; about &lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;NXCWH0syq8E&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;Aside from a scratchy voice initially (and having to present remotely due to a Covid recovery), it went well! I was excited to share my excitement for ES Module-centric toolchains, and the speed they afford day-to-day development.&lt;/p&gt;
&lt;p&gt;As an example of a Vite-driven website, I showcased the &lt;a href=&quot;https://www.steele.blue/serverless-bike-gps&quot;&gt;GPS bike tracking website&lt;/a&gt;, which used Vite on its frontend.
The UI isn&#39;t especially complicated, but it demonstrates the flexibility tools like Vite afford.
I didn&#39;t need a full framework, and most of the functionality was Vanilla JS, along with a few geospatial tools and a mapping library.&lt;/p&gt;
&lt;p&gt;Simply going &lt;a href=&quot;https://www.steele.blue/toolchainless&quot;&gt;toolchainless&lt;/a&gt; and using &lt;a href=&quot;https://www.skypack.dev/&quot;&gt;skypack&lt;/a&gt; might have been the easiest approach, but I still wanted some type-safety for the code I was authoring, and Vite offered an easy way to integrate TypeScript.&lt;/p&gt;
&lt;p&gt;I do wonder if Vite and its tools are just steps along the path toward full &lt;a href=&quot;https://modern-web.dev/guides/going-buildless/getting-started/&quot;&gt;buildless development&lt;/a&gt;.
It&#39;s exciting to see tools embracing new primitives, while also meeting developers where they&#39;re at, and enabling productivity along the way.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Serverless Bike Tracking with a SPOT Tracker, AWS Location + Friends</title>
    <link href="https://www.steele.blue/serverless-bike-gps/" />
    <updated>2022-10-10T00:00:00Z</updated>
    <id>https://www.steele.blue/serverless-bike-gps/</id>
    <content type="html">&lt;p&gt;I recently participated in the Gravel Worlds &lt;a href=&quot;https://www.gravel-worlds.com/the-long-voyage&quot;&gt;Long Voyage&lt;/a&gt; bike race. Last year I &lt;a href=&quot;https://www.steele.blue/gravel-worlds&quot;&gt;tried, and failed&lt;/a&gt;, to finish the 300 mile course, and &lt;a href=&quot;https://www.steele.blue/js-temporal&quot;&gt;built a pacing calculator along the way&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This year, I felt better prepared, rode smarter, and actually finished! My intent was to simply complete the race within the time limit, with a stretch goal of finishing before sundown. I ended up making it with about an hour of sunlight to go, and I couldn&#39;t be happier.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/fsEyE_MGqZ-600.jpeg&quot; alt=&quot;gw-finish&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2479&quot; height=&quot;1588&quot; srcset=&quot;https://www.steele.blue/img/fsEyE_MGqZ-600.jpeg 600w, https://www.steele.blue/img/fsEyE_MGqZ-1000.jpeg 1000w, https://www.steele.blue/img/fsEyE_MGqZ-2479.jpeg 2479w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I credit some of my success this year to spending more time riding through the year, both on longer gravel base rides, and structured intervals on the trainer. But I still had enough time to build another dubiously useful website!&lt;/p&gt;
&lt;p&gt;That&#39;s what I want to share today; a site that captures data from a GPS tracker, and makes it available for folks to track my progress. There are a number of public versions of this (they call it &lt;a href=&quot;https://www.cyclist.co.uk/in-depth/10221/what-is-dotwatching&quot;&gt;dot watching&lt;/a&gt;), but I wanted to add some fancier features, such as making geofences at expected stops, and capturing enter/exit times.&lt;/p&gt;
&lt;p&gt;I built out the site using AWS serverless architecture (Location Services, Lambda, and others).
The code is available at https://github.com/mattdsteele/spot-tracker-tracker, and you can see the page for my ride &lt;a href=&quot;https://track.steele.blue/?course=gw-2022&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/6cLbbLmWQo-600.png&quot; alt=&quot;map-overall&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;803&quot; height=&quot;1012&quot; srcset=&quot;https://www.steele.blue/img/6cLbbLmWQo-600.png 600w, https://www.steele.blue/img/6cLbbLmWQo-803.png 803w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Capturing GPS Pings&lt;/h2&gt;
&lt;p&gt;I used a &lt;a href=&quot;https://en.wikipedia.org/wiki/SPOT_Satellite_Messenger&quot;&gt;SPOT Messenger&lt;/a&gt; device; which all racers were required to carry with them.
SPOT devices are neat; they were designed with fairly easy-to-use APIs, along with a set of prebuilt maps you can share with friends.&lt;/p&gt;
&lt;p&gt;To get geofencing and other features, I had to ingress each GPS ping into the AWS Location data store. This required the use of a Lambda function, scheduled to run at the same frequency that GPS pings were emitted (5 minutes).&lt;/p&gt;
&lt;p&gt;This (as well as all the other Lambdas in the project) were built as Go functions.&lt;/p&gt;
&lt;p&gt;Calls to SPOT&#39;s history API returned all observations over the past 24 hours, so I needed to find a way to only send in calls that hadn&#39;t been indexed before. I ended up keeping track of the latest timestamp via an object in DynamoDB, and only returning newer data. &lt;a href=&quot;https://www.reddit.com/r/aws/comments/o9kntm/converting_a_location_history_rest_api_into_aws/&quot;&gt;There might be more efficient ways to do this&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once the pings were stored as an asset in AWS Location, they remain for 30 days.&lt;/p&gt;
&lt;h2&gt;Geofencing&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/6htB4SJJ_8-600.png&quot; alt=&quot;geofences&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;949&quot; height=&quot;749&quot; srcset=&quot;https://www.steele.blue/img/6htB4SJJ_8-600.png 600w, https://www.steele.blue/img/6htB4SJJ_8-949.png 949w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;AWS Location also supports &lt;a href=&quot;https://docs.aws.amazon.com/location/latest/developerguide/geofence-tracker-concepts.html&quot;&gt;geofences&lt;/a&gt;. For each GPS ping that arrives, it can be evaluated against a set of predefined geofences. These are emitted via EventBridge, and you can react to them with Lambda functions as well.&lt;/p&gt;
&lt;p&gt;I stored entrance/exit events for each geofence in another Dynamo table.&lt;/p&gt;
&lt;h2&gt;Routes&lt;/h2&gt;
&lt;p&gt;Cycling routes are traditionally saved as &lt;code&gt;.fit&lt;/code&gt; files, among other formats. These files aren&#39;t usable in mapping libraries directly; they have to be converted to GeoJSON or another similar technology.&lt;/p&gt;
&lt;p&gt;To do this, I used Tormod Erevik Lea&#39;s &lt;a href=&quot;https://github.com/tormoder/fit&quot;&gt;fit Go library&lt;/a&gt;, which made easy work processing the .fit files.&lt;/p&gt;
&lt;p&gt;I wanted to make this easy to use, so I made an S3 bucket so I could upload &lt;code&gt;.fit&lt;/code&gt; files as I set courses up. I then built a Lambda that found the latest file in the bucket, and returned the coordinates in an easy to process JSON format for the UI.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;lambda&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;Start&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;events&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;LambdaFunctionURLResponse&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    config&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;_&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; := &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;config&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;LoadDefaultConfig&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    files&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; := &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;spot&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;GetFilesInBucket&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;config&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    latestFile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; := &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;files&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    fileContents&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; := &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;spot&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;DownloadFile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(*&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;latestFile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Key&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;config&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    fitFile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; := &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;spot&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;ParseFitData&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;bytes&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;NewReader&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fileContents&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    course&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; := &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;toCourse&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fitFile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    j&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; := &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;Marshal&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;course&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;REST Services&lt;/h2&gt;
&lt;p&gt;AWS Location has a set of REST services and JavaScript APIs, but I found them to either be very heavyweight (using AmplifyJS), or required setting up a multitude of additional security and IAM authorization policies, which I wasn&#39;t confident I could implement properly. All I wanted to do was provide some read-only data to a website!&lt;/p&gt;
&lt;p&gt;I ended up exposing the Lambda functions as REST endpoints. This is a &lt;a href=&quot;https://serverlessland.com/patterns/cloudfront-lambda-urls&quot;&gt;new-to-me feature&lt;/a&gt;, which makes it dead-simple to provide read-only access to Geolocation data, or any other AWS resource.
The URLs aren&#39;t pretty, and by default are on another origin, such as https://6f7w2jqblnebkk75folo4zv7j40qvxfp.lambda-url.us-east-2.on.aws/. I&#39;d love to host these under the same domain as my UI to avoid cross-origin requests, but &lt;a href=&quot;https://github.com/simonw/public-notes/issues/1&quot;&gt;it appears to be quite complicated&lt;/a&gt;, so I&#39;ll live with CORS.&lt;/p&gt;
&lt;h2&gt;Website&lt;/h2&gt;
&lt;p&gt;The app itself is pretty barebones; it loads a full-screen MapLibreGL map, and adds the appropriate layers for routes, geofences, and GPS pings.
There&#39;s a few geospatial functions to calculate how far on the course you&#39;ve gone &lt;a href=&quot;https://www.reddit.com/r/gis/comments/tvrtzw/bike_mapping_given_a_gpxtcx_route_can_i_map_match/&quot;&gt;using the nearestPointOnLine function&lt;/a&gt;, but there&#39;s nothing too fancy there.&lt;/p&gt;
&lt;p&gt;I built the app using &lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt; as my build tool and scaffolding (using their vanilla-ts template). I&#39;ve been very impressed by the modern, ESM-first tools, which don&#39;t bundle anything in development, leading to a fast feedback loop.
I still was able to use TypeScript and other modern affordances, and could pull in modules like date-fns as needed, without feeling overwhelmed by tooling. It&#39;s not quite &lt;a href=&quot;https://www.steele.blue/toolchainless&quot;&gt;toolchainless&lt;/a&gt;, but it&#39;s close!&lt;/p&gt;
&lt;p&gt;The UI it built via GitHub Actions on each push, and deployed to Netlify. I also have branch deploys configured, which allows me to save off the service calls as JSON files for posterity.&lt;/p&gt;
&lt;h2&gt;Cost&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/-kPArJDIjH-600.png&quot; alt=&quot;architecture&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;752&quot; height=&quot;646&quot; srcset=&quot;https://www.steele.blue/img/-kPArJDIjH-600.png 600w, https://www.steele.blue/img/-kPArJDIjH-752.png 752w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Since none of the resources I&#39;m using are persistent, the app is stupid cheap to run. My current AWS bill is about 77 cents per month. Location Services cost 15 cents, S3 storage is another 7 pennies. I&#39;m no longer on the AWS free tier, but at these prices I might as well be.&lt;/p&gt;
&lt;p&gt;I came into this project believing that serverless architectures are great for small hobby projects like this, and I remain more convinced this is the case. Since everything scales to zero, I can leave everything running and ready for next year.&lt;/p&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;There&#39;s plenty of room for improvement here. I&#39;d love to make it easy for someone else to deploy, via a Terraform or other infrastructure-as-code options. Tools like https://github.com/GoogleCloudPlatform/terraformer may make this easy.&lt;/p&gt;
&lt;p&gt;I&#39;d also like to find ways to send push notifications to interested parties when I enter/exit geofences. This feels like it should be possible using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Push_API&quot;&gt;Web Push&lt;/a&gt;, even if it&#39;s fallen out of favor as an API over time.&lt;/p&gt;
&lt;p&gt;Overall I&#39;m pretty happy with this project. I&#39;m planning on trying the Long Voyage course again next year, so I&#39;ll have plenty of opportunities to procrastinate training by hacking on this further!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>ACEing Ocarina of Time</title>
    <link href="https://www.steele.blue/sgdq/" />
    <updated>2022-07-11T00:00:00Z</updated>
    <id>https://www.steele.blue/sgdq/</id>
    <content type="html">&lt;p&gt;The Summer Games Done Quick event recently ended, and I&#39;m still thinking about an Ocarina of Time run:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;2x_pqyrf9lA&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;It&#39;s best if you go in without too much knowledge of the run, but if you grew up playing and reading rumors on Geocities about what was possible, you&#39;ll love the process.&lt;/p&gt;
&lt;p&gt;The team did a great job &lt;a href=&quot;https://gettriforce.link/&quot;&gt;explaining the run&lt;/a&gt;, which relies on Arbitrary Code Execution and other tricks.&lt;/p&gt;
&lt;p&gt;The summary video is great as well:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;1_RighmL04g&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;This particular run achieve the level of art, but here&#39;s a few other runs I enjoyed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=vi2DgHDAIcU&amp;amp;time_continue=30&quot;&gt;Mario Maker 2 Relay Race&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=mKfdfNxnrw0&amp;amp;time_continue=0&quot;&gt;StepMania Couples Showcase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=4Za1tPbPxlo&amp;amp;time_continue=0&quot;&gt;Celeste.SMC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Advent of Code as Soulcraft</title>
    <link href="https://www.steele.blue/advent-of-code/" />
    <updated>2021-12-26T00:00:00Z</updated>
    <id>https://www.steele.blue/advent-of-code/</id>
    <content type="html">&lt;p&gt;As we close out another &lt;a href=&quot;https://www.adventofcode.com/&quot;&gt;Advent of Code&lt;/a&gt; season, this year the Meta Discourse is less around any of the puzzles, and more around whether you should &lt;a href=&quot;https://twitter.com/keystonelemur/status/1472024156577288195&quot;&gt;feel pressured to participate&lt;/a&gt;.
Especially if you see your GitHub activity feed filling up with others starting &lt;code&gt;advent-of-code&lt;/code&gt; repos every December, it can feel like you&#39;re missing out if you don&#39;t want to, or aren&#39;t able to participate.&lt;/p&gt;
&lt;p&gt;In general I&#39;m sympathetic to these arguments. I&#39;m not into the &amp;quot;rise and grind&amp;quot; and hustle side of software development. And while I&#39;ve resonated with the idea that &lt;a href=&quot;https://www.goodreads.com/book/show/6399113-the-passionate-programmer&quot;&gt;passion for programing&lt;/a&gt; was an essential part of having a career in software, I&#39;m less beholden to that over time. It&#39;s totally fine to treat coding as any other job, and not have it encompass your off-hours time too!&lt;/p&gt;
&lt;p&gt;I&#39;d like to offer a limited defense of Advent of Code and other programming puzzles (Project Euler, Code Retreat, and the like). These particular challenges recharge me and get me excited for my day job, and I think that&#39;s a more sustainable way to approach the puzzles than &amp;quot;you should always be learning new things, or you&#39;ll fall behind&amp;quot;.&lt;/p&gt;
&lt;p&gt;For me, Advent of Code really scratches an itch to do test-driven-development in as ideal an environment as one could cook up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The problems are well-defined and don&#39;t contain business-logic ambiguity&lt;/li&gt;
&lt;li&gt;Sample inputs and outputs make for simple test definitions&lt;/li&gt;
&lt;li&gt;The two-part nature of each puzzles encourages clean code and the red/green/refactor programming cycles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When I get into a TDD loop, it feels &lt;em&gt;fun&lt;/em&gt;, &lt;a href=&quot;http://www.kytrinyx.com/talks/therapeutic-refactoring/&quot;&gt;almost therapeutic&lt;/a&gt;. It&#39;s rare that I get these opportunities during the work week, so getting the chance for fresh puzzles is a really great feeling.&lt;/p&gt;
&lt;p&gt;If this isn&#39;t the case for you, that&#39;s fine! And Advent of Code doesn’t have to be performative, you can make your repo private (or just delete the code when you&#39;re done!). And the puzzles are around all year, you can use them as a pallets cleanser throughout the year, they’re still around!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Gravel Worlds - Long Voyage</title>
    <link href="https://www.steele.blue/gravel-worlds/" />
    <updated>2021-08-23T00:00:00Z</updated>
    <id>https://www.steele.blue/gravel-worlds/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/r7xe7X-Cxc-600.jpeg&quot; alt=&quot;long voyage&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;1536&quot; srcset=&quot;https://www.steele.blue/img/r7xe7X-Cxc-600.jpeg 600w, https://www.steele.blue/img/r7xe7X-Cxc-1000.jpeg 1000w, https://www.steele.blue/img/r7xe7X-Cxc-2048.jpeg 2048w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I participated in the inaugural Gravel Worlds &lt;a href=&quot;https://www.gravel-worlds.com/the-long-voyage&quot;&gt;Long Voyage&lt;/a&gt; bike race last weekend. It was my fifth year racing a Gravel Worlds event, and I&#39;m not planning on stopping any time soon. Corey, Craig, and the other organizers do a great job to make everyone welcome and accepted.&lt;/p&gt;
&lt;p&gt;I applied for the 300-mile event partially for personal redemption. In 2018 I raced &lt;a href=&quot;https://transiowa.blogspot.com/&quot;&gt;Trans Iowa&lt;/a&gt;, but only made it about halfway through the 330+ mile course, primarily due to poor equipment and clothing choices. After T.I. ended, I wanted to give it another go, and trusted the Pirate Cycling League to keep the spirit alive.&lt;/p&gt;
&lt;p&gt;I pulled up to the starting line having trained with teammates &lt;a href=&quot;https://www.instagram.com/rdoloto78/&quot;&gt;Rafal&lt;/a&gt; and &lt;a href=&quot;https://www.instagram.com/alexander_g_sanchez/&quot;&gt;Alex&lt;/a&gt;, both of whom are stronger riders and helped pull me through summer Bacon Rides. I also got to chat with multiple other folks who came out for the send-off, including Patrick Davlin, Robbie Benton, Jason Faas, Michelle Cleasby, Curtis Wilson, and Dustin Slivka. Seeing these folks after a year of of isolation and Zwift riding was really heartwarming.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/JJMmokPBuc-600.jpeg&quot; alt=&quot;long voyage&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2571&quot; height=&quot;1928&quot; srcset=&quot;https://www.steele.blue/img/JJMmokPBuc-600.jpeg 600w, https://www.steele.blue/img/JJMmokPBuc-1000.jpeg 1000w, https://www.steele.blue/img/JJMmokPBuc-2571.jpeg 2571w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Prior to the race starting I spent most of the previous few days worrying about the weather. The race included 15 miles of dirt/minimum maintenace roads, which can easily become unrideable when wet. The forecast called for thunderstorms the evening of the race, which would have made a long day even longer. As the race started, the weather was beautiful (though humid), so I was excited we had avoided the storms. Turns out, not the case!&lt;/p&gt;
&lt;p&gt;As the race began I was feeling good. The race didn&#39;t include any intermediate cutoffs, but to finish you had to maintain a 10mph pace, including any stops/breaks. I was riding around 15mph, so I had some time banked. The gravel north of Lincoln is quite sandy, but was quite rideable on my Cannonball 42mm tires.&lt;/p&gt;
&lt;p&gt;Around 25 miles in, the skies opened up and it began pouring. Luckily there was no ride-delaying lightning or thunderstorms, those all appeared to be south of us. I just got completely drenched, and ate a bunch of limestone that collected on my water bottles. As the sun began to set, the storms continued to be visible on the horizon through Omaha, and it was quite the experience to see the spider lightning from afar. I hit one section of &lt;a href=&quot;https://flickr.com/photos/nickfaiello/4743812399&quot;&gt;MMR&lt;/a&gt; which was a little damp but completely rideable, and thought I was in the clear. How wrong I was!&lt;/p&gt;
&lt;p&gt;At mile 54 we refuled in Weeping Water, and continued on to the east and south of Lincoln. Remember how I was so excited that the majority of the rain missed us to the south? Apparently I wasn&#39;t thinking straight, and was unpleasantly surprised when I started to pick up more mud on the next MMR, though I was able to shed it without too much hassle.&lt;/p&gt;
&lt;p&gt;Night riding was pretty pleasant, the weather was great with a nearly full moon helping illuminate the corn and soybeans surrounding me, and cicadas helped to drown out the sound of crunching limestone beneath me.&lt;/p&gt;
&lt;p&gt;At mile 90 we hit another mile of MMR which was partially unrideable, though I felt pretty confident just mashing through on my singlespeed as I had fewer parts to fail. I came across one rider with a completely blown deraillieur after attempting to shift while covered in mud, which unfortunately ended her event.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/FVGmUlniOs-600.jpeg&quot; alt=&quot;long voyage&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;1536&quot; srcset=&quot;https://www.steele.blue/img/FVGmUlniOs-600.jpeg 600w, https://www.steele.blue/img/FVGmUlniOs-1000.jpeg 1000w, https://www.steele.blue/img/FVGmUlniOs-2048.jpeg 2048w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Finally around mile 130 I hit a dirt road that was completely unrideable, and so muddy that it was impossible to even push the bike through; mud would just get caught on the tires and jam up in the fork, seizing up the wheels. The only solution here is to hike-a-bike. I have some experience from years of racing cyclocross, but the optimal strategy of shouldering in the triangle wasn&#39;t feasible with frame bags and multiple bottle cages. So I threw the bike over my shoulder, and just tried to slog on. Those two miles of road took nearly an hour to traverse, and sapped a ton of energy I was hoping to preserve for later in the race.&lt;/p&gt;
&lt;p&gt;We hit one more dirt road that evening, but we were able to push the bike through a grassy ditch. As dawn broke, I was praying to the gravel gods that sunlight would help dry out the remaining 7 miles of dirt on the course.&lt;/p&gt;
&lt;p&gt;While riding overnight I also encountered a new terrain I hadn&#39;t seen in years of gravel riding - slatted bridges with gaps wider than my tires; which could easily swallow a wheel. I was probably more cautious than I needed to be when traversing them, but didn&#39;t want to take any chances suffering a mechanical because I was in Full Send mode.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/zJHdP6szbo-600.jpeg&quot; alt=&quot;long voyage&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;1365&quot; srcset=&quot;https://www.steele.blue/img/zJHdP6szbo-600.jpeg 600w, https://www.steele.blue/img/zJHdP6szbo-1000.jpeg 1000w, https://www.steele.blue/img/zJHdP6szbo-2048.jpeg 2048w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;As dawn broke I continued to ride with a couple folks from Pennsylvania and Michigan. As the headwinds began to come into effect, we formed a paceline and began to take pulls pretty effectively, considering we hadn&#39;t ridden together before. A few of them pulled the plug in Crete, and I rode out to Milford with another singlespeeder from Kansas. We encountered a few dirt roads, but the sunlight and wind dried them out and they were as fast as pavement.&lt;/p&gt;
&lt;p&gt;Coming to Milford at mile 215, the math wasn&#39;t looking great for making the 11pm deadline, my pace just wasn&#39;t high enough to make the cutoff, especially since the final 50 miles would be going through the hilly &lt;a href=&quot;https://en.wikipedia.org/wiki/Bohemian_Alps&quot;&gt;Bohemian Alps&lt;/a&gt;. I wanted to push through more, and rode solo out to Seward, only stopping once to chat with a farmer whose driveway the course accidentally cut through. I think he was more shocked that any cyclists were out in the area, than he was upset we were technically trespassing.&lt;/p&gt;
&lt;p&gt;I made it to Seward at mile 234 and decided to pull the plug. I probably had enough gas in the tank to make the next town at mile 267, but exhaustion after 25 hours of riding was beginning to set in, and I was fairly certain I was in the lantern rouge position and wouldn&#39;t have anyone checking on me in case something went wrong.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/27JJemTMkd-600.jpeg&quot; alt=&quot;long voyage&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;1536&quot; srcset=&quot;https://www.steele.blue/img/27JJemTMkd-600.jpeg 600w, https://www.steele.blue/img/27JJemTMkd-1000.jpeg 1000w, https://www.steele.blue/img/27JJemTMkd-2048.jpeg 2048w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Things that worked well&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Equipment - I was pretty happy with all the stuff I carried. For night riding, I modded a &lt;a href=&quot;https://budgetlightforum.com/node/69826&quot;&gt;Lumintop EDC18&lt;/a&gt; flashlight with an elliptical lens, and carried multiple extra 18650 batteries to swap out through the night, this gave me plenty of light and the confidence to keep a reasonable speed. Also bringing a clean change of socks was a huge morale booster.&lt;/li&gt;
&lt;li&gt;Singlespeed gearing - I rode an easier gearing (42x22) than I generally ride gravel (42x19), with the assumption that I&#39;d need the extra teeth as fatigue and hills crept up throughout the day. This was a good choice; I&#39;d have been walking far more hills near the end had I chosen a &amp;quot;faster&amp;quot; cog.&lt;/li&gt;
&lt;li&gt;Route planning - I spent the days prior to the race checking out the Nebraska DOT&#39;s &lt;a href=&quot;https://dot.nebraska.gov/travel/map-library/county/&quot;&gt;surface type maps&lt;/a&gt; to try and gauge where the MMR was on the course. Knowing how much of a break I&#39;d have before the next potential hike-a-bike was a good boost of self-confidence and cleared a potential point of anxiety for me. Additionally, &lt;a href=&quot;https://steele.blue/js-temporal/&quot;&gt;building a route estimator&lt;/a&gt; helped keep me in tune with how I was progressing based on my moving speeds.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Things I&#39;d work on improving&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Bike fit - If anything was going to give on this ride it would have been my back and palms, and I think a more precise fitting and adjusting saddle position, etc. would help alleviate these issues.&lt;/li&gt;
&lt;li&gt;Prepping for hike-a-bike - If there&#39;s a good chance I&#39;ll be shouldering a bike for multiple miles, I&#39;m going to strip off the bottle cages on the bike frame, and wear a Camelback instead. It&#39;ll help with proper form, and the hydration pack should provide some padding for the steel frame digging into my shoulder (which is still sore!)&lt;/li&gt;
&lt;li&gt;Applying more sunscreen - I got burned after not applying more sunscreen on the second day of riding, and sunburns traditionally sap a ton of energy from me.&lt;/li&gt;
&lt;li&gt;Less stopped time - At each town I stopped at convenience stores anywhere from 15 to 50 minutes; this obviously eats into my elapsed pace, and is probably the lowest-hanging fruit.&lt;/li&gt;
&lt;li&gt;General fitness - Near the end of my ride I was in dead last place of those still riding. For races with a cutoff, that&#39;s not good enough. Some structured training, sweet-spot intervals, and the like are the standard solutions here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Overall I was pretty happy with my race; I set new personal records for the longest and farthest I&#39;ve ridden in one go, it was my first completely overnight ride, and I made some new friends along the way. But I&#39;d love to finish one of these endurance races, so I have a 2022 goal already in mind.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Making a Raspberry Pi Photo Booth with Pibooth, NeoPixels, and Giant Buttons</title>
    <link href="https://www.steele.blue/photo-booth/" />
    <updated>2021-08-17T00:00:00Z</updated>
    <id>https://www.steele.blue/photo-booth/</id>
    <content type="html">&lt;p&gt;&lt;lite-youtube videoid=&quot;tNagjSGzJYA&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;I put a homemade photo booth together for a wedding reception we postponed for a year, on account of, &lt;em&gt;gestures everywhere&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/-nujtxSdzM-600.jpeg&quot; alt=&quot;photo booth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/-nujtxSdzM-600.jpeg 600w, https://www.steele.blue/img/-nujtxSdzM-1000.jpeg 1000w, https://www.steele.blue/img/-nujtxSdzM-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;It was a fun project that let me play with new hardware and software. Here&#39;s a few photos of the project, and some of the things I learned.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/fJOZsJWJ2U-600.jpeg&quot; alt=&quot;photo booth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3007&quot; height=&quot;3286&quot; srcset=&quot;https://www.steele.blue/img/fJOZsJWJ2U-600.jpeg 600w, https://www.steele.blue/img/fJOZsJWJ2U-1000.jpeg 1000w, https://www.steele.blue/img/fJOZsJWJ2U-3007.jpeg 3007w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;The guts of the project were driven by a Raspberry Pi 4 running &lt;a href=&quot;https://www.steele.blue/photo-booth/pibooth&quot;&gt;pibooth&lt;/a&gt;. It&#39;s a great project that works well out of the box, and has a plugin system if you want to anything.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/LqZnrushnE-600.jpeg&quot; alt=&quot;photo booth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/LqZnrushnE-600.jpeg 600w, https://www.steele.blue/img/LqZnrushnE-1000.jpeg 1000w, https://www.steele.blue/img/LqZnrushnE-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;The Pi, wiring, and breadboard and wiring went into a small box, along with a few buttons. The big button started captures, and the small one printed the last photo.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/D9Gcb9VH1r-600.jpeg&quot; alt=&quot;photo booth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3004&quot; height=&quot;2396&quot; srcset=&quot;https://www.steele.blue/img/D9Gcb9VH1r-600.jpeg 600w, https://www.steele.blue/img/D9Gcb9VH1r-1000.jpeg 1000w, https://www.steele.blue/img/D9Gcb9VH1r-3004.jpeg 3004w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I tried various means of connecting up the buttons; speaker wire worked surprisingly well! But in the end a few small alligator clips and jumper wires were sufficient.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/dnX0eSQzPa-600.jpeg&quot; alt=&quot;photo booth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/dnX0eSQzPa-600.jpeg 600w, https://www.steele.blue/img/dnX0eSQzPa-1000.jpeg 1000w, https://www.steele.blue/img/dnX0eSQzPa-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;We setup a couple backdrops and had a modeling light to provide standard illumination.
I would have loved to use a greenscreen along with an OBS virtual camera, but the Pi wasn&#39;t powerful enough to perform chroma keying in real-time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/fd1H-0lCs0-600.jpeg&quot; alt=&quot;photo booth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/fd1H-0lCs0-600.jpeg 600w, https://www.steele.blue/img/fd1H-0lCs0-1000.jpeg 1000w, https://www.steele.blue/img/fd1H-0lCs0-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Photos were taken using a webcam. Originally I was hoping to use a higher-quality mirrorless camera, but &lt;a href=&quot;https://github.com/pibooth/pibooth/issues/184&quot;&gt;gphoto2 has issues with Sony cameras&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/tW7shIahpy-600.jpeg&quot; alt=&quot;photo booth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; srcset=&quot;https://www.steele.blue/img/tW7shIahpy-600.jpeg 600w, https://www.steele.blue/img/tW7shIahpy-1000.jpeg 1000w, https://www.steele.blue/img/tW7shIahpy-3024.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Printing was done using a small Canon printer, which we had on a stand below the image. I had never used a dye-sublimation printer before, but I was really impressed with it!
The guests were really excited to see &lt;a href=&quot;https://www.youtube.com/watch?v=DA2yJe3o8s0&quot;&gt;each color get applied sequentially&lt;/a&gt;, and took home a postcard-sized souvenier from the night.&lt;/p&gt;
&lt;p&gt;We also configured each photo to get uploaded to a shared Google Photos album and had a QR code to let folks view their photos after the event; this was also supported by a few Pibooth plugins.&lt;/p&gt;
&lt;h2&gt;Hardware&lt;/h2&gt;
&lt;p&gt;I wouldn&#39;t recommend this as a shopping list; this sort of project works best reusing any pieces you have around your house.
If you were to source all these parts exclusively for this project, it would be substantially more expensive than using an off-the-shelf photo booth (or renting one).&lt;/p&gt;
&lt;p&gt;That said, here&#39;s the parts I used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Computer: Rasperry Pi 4&lt;/li&gt;
&lt;li&gt;Camera: &lt;a href=&quot;https://mevo.com/pages/mevo-camera&quot;&gt;Mevo Start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Camera attachment: &lt;a href=&quot;https://tethertools.com/product/rock-solid-heavy-duty-superflex-arm/&quot;&gt;Flex Arm&lt;/a&gt; with tripod adapter&lt;/li&gt;
&lt;li&gt;Display: HP 27&amp;quot; monitor&lt;/li&gt;
&lt;li&gt;Display attachment: &lt;a href=&quot;https://tethertools.com/product/studio-vu-monitor-mount/&quot;&gt;VESA monitor mount&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Printer: &lt;a href=&quot;https://www.usa.canon.com/internet/portal/us/home/products/details/printers/support-inkjet-printer/selphy-series/selphy-cp510/selphy-cp510&quot;&gt;Canon Selphy CP510&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Modeling light and tripod - &lt;a href=&quot;https://interfitphoto.com/products/honey-badger-320ws-2-light-kit&quot;&gt;Interfit Honey Badger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Buttons: &lt;a href=&quot;https://www.adafruit.com/product/1185&quot;&gt;Adafruit arcade buttons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ring light: &lt;a href=&quot;https://www.aliexpress.com/item/4000761092272.html?spm=a2g0s.9042311.0.0.3e044c4dDgMsfe&quot;&gt;WS2812B Neopixel clone with 48 LEDs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;p&gt;Most of the software was taken care of by &lt;a href=&quot;https://www.steele.blue/photo-booth/pibooth&quot;&gt;pibooth&lt;/a&gt;, with a few additional plugins and customizations.&lt;/p&gt;
&lt;p&gt;You can see the configuration and other customizations here: https://github.com/mattdsteele/pibooth-config&lt;/p&gt;
&lt;p&gt;The primary mod I built was a custom plugin which provided support for the ring light, using Adafruit&#39;s &lt;a href=&quot;https://learn.adafruit.com/neopixels-on-raspberry-pi/python-usage&quot;&gt;neopixel Python library&lt;/a&gt;.
It setup an &amp;quot;attract mode&amp;quot; with a rainbow cycle, as well as a countdown timer and a virtual &amp;quot;flash&amp;quot; while capturing images.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/sH8-ke9bt3-451.gif&quot; alt=&quot;rainbow&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;451&quot; height=&quot;455&quot;&gt;&lt;/p&gt;
&lt;p&gt;I learned a bit about Python multithreading along the way too!&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;@pibooth.hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; state_preview_enter&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;app&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    app.pixels.fill((&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    app.pixels.show()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    proc = threading.Thread(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;target&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=countdown, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=[&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, app.pixels])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    proc.daemon = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;True&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    proc.start()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    app.neopixels_proc = proc&lt;/span&gt;&lt;span style=&quot;color:#CD3131;--shiki-dark:#F44747&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Along with our &lt;a href=&quot;https://www.steele.blue/indieweb-wedding-livestream&quot;&gt;wedding livestreamed with Owncast&lt;/a&gt;, I had a great time geeking out with my wife. Way more fun than stressing out about caterers and dress fittings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/i-yKXxPSQN-600.jpeg&quot; alt=&quot;pibooth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2400&quot; height=&quot;3600&quot; srcset=&quot;https://www.steele.blue/img/i-yKXxPSQN-600.jpeg 600w, https://www.steele.blue/img/i-yKXxPSQN-1000.jpeg 1000w, https://www.steele.blue/img/i-yKXxPSQN-2400.jpeg 2400w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>3 Things I Learned Trying out the JavaScript Temporal API</title>
    <link href="https://www.steele.blue/js-temporal/" />
    <updated>2021-08-11T00:00:00Z</updated>
    <id>https://www.steele.blue/js-temporal/</id>
    <content type="html">&lt;p&gt;I&#39;ve been preparing for a &lt;a href=&quot;https://www.gravel-worlds.com/the-long-voyage&quot;&gt;303-mile endurance gravel bike race&lt;/a&gt;. Not by training or improving fitness, of course, but by &lt;a href=&quot;https://longvoyage.steele.blue/&quot;&gt;building a PWA to estimate and track checkpoint ETAs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By adjusting the sliders for my day and evening paces, the app estimates when I&#39;ll be able to take breaks, reach hazards, etc.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/rq6Ys3Fmlg-600.png&quot; alt=&quot;Long Voyage Screenshot&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;1792&quot; srcset=&quot;https://www.steele.blue/img/rq6Ys3Fmlg-600.png 600w, https://www.steele.blue/img/rq6Ys3Fmlg-828.png 828w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;It&#39;s been a while since I built something on the front-end that did lots of date manipulation, but I thought this would be a nice time to try out the Temporal API, which is &lt;a href=&quot;https://tc39.es/proposal-temporal/docs/&quot;&gt;now in Stage 3&lt;/a&gt; and has a &lt;a href=&quot;https://www.npmjs.com/package/@js-temporal/polyfill&quot;&gt;nice polyfill available&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&#39;s a few things I&#39;ve learned:&lt;/p&gt;
&lt;h2&gt;It&#39;s pretty similar to Java Time API&lt;/h2&gt;
&lt;p&gt;The existing JavaScript &lt;code&gt;Date&lt;/code&gt; API sucks. And like many things, Java is the cause of, and solution to it.&lt;/p&gt;
&lt;p&gt;The APIs were &lt;a href=&quot;https://maggiepint.com/2017/04/09/fixing-javascript-date-getting-started/&quot;&gt;originally ported wholesale from the Java 1.1 era&lt;/a&gt;, with all the bugs and warts therein. Months in the year are zero-indexed, but days of the month are one-indexed? You better believe it.&lt;/p&gt;
&lt;p&gt;Java&#39;s date/time APIs sucked badly enough, and longly enough, that most developers switched to userland libraries, in particular &lt;a href=&quot;https://www.joda.org/joda-time/&quot;&gt;Joda Time&lt;/a&gt;. Eventually the lead maintainer of Joda Time was recruited to create a &lt;a href=&quot;https://jcp.org/aboutJava/communityprocess/pfd/jsr310/JSR-310-guide.html&quot;&gt;new standard date and time library&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Similarly, a thousand Node libraries/flowers have bloomed, all trying to provide a saner operations on dates (including, incidentally, a &lt;a href=&quot;https://js-joda.github.io/js-joda/&quot;&gt;port of Joda Time&lt;/a&gt;). And &lt;a href=&quot;https://maggiepint.com/&quot;&gt;Maggie Johnson-Pint&lt;/a&gt;, the core maintainer of Moment.js, has been championing the new Temporal API standard.&lt;/p&gt;
&lt;p&gt;Having worked with both of these new &amp;quot;standards&amp;quot;, I&#39;m pretty impressed at the conceptual convergence! Most of the base classes are available in both libraries: from &lt;a href=&quot;https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZonedDateTime.html&quot;&gt;ZonedDateTime&lt;/a&gt; to &lt;a href=&quot;https://tc39.es/proposal-temporal/docs/#Temporal-Duration&quot;&gt;Duration&lt;/a&gt;, to &lt;a href=&quot;https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Instant.html&quot;&gt;Instant&lt;/a&gt;. Combined with the polyfill&#39;s &lt;a href=&quot;https://www.npmjs.com/package/@js-temporal/polyfill&quot;&gt;nice TypeScript definitions&lt;/a&gt;, it&#39;s really straightforward to just start working with the library, and autocomplete your way to development.&lt;/p&gt;
&lt;p&gt;It&#39;s another example of &lt;a href=&quot;https://www.steele.blue/typescript-for-javaers&quot;&gt;why Java folks should feel at home with TypeScript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Seriously, this is really nice to use:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; dayDelta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;startTime&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;until&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;nightTime&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; totalHours&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;dayDelta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;total&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;unit:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &quot;hours&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; nightDelta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;nightTime&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;until&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;morningTime&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; totalNightHours&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;nightDelta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;total&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;unit:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &quot;hours&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// how far can you get by sundown?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; dayDistance&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;totalHours&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pace&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;day1&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;distance&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; &amp;#x3C; &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;dayDistance&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; hoursDelta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;distance&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; / &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pace&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;day1&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;toPrecision&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; eta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;startTime&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;add&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`PT&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;hoursDelta&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;H`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; eta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;It doesn&#39;t support parsing human-readable dates?&lt;/h2&gt;
&lt;p&gt;One feature I was expecting, and which the Java Time API supports, is parsing and formatting &amp;quot;human readable dates&amp;quot;, via a &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html&quot;&gt;DateTimeFormatter&lt;/a&gt;. This doesn&#39;t appear to be in scope for Temporal API! From what I can tell, if you&#39;ve got anything that isn&#39;t a flavor ISO8601, you&#39;ll need to pull in another date parsing library.&lt;/p&gt;
&lt;p&gt;I&#39;d love to be wrong about this, so if you know this API better than me, save me from having to pull in date-fns in the future.&lt;/p&gt;
&lt;h2&gt;It does format human-readable dates, using the Intl API&lt;/h2&gt;
&lt;p&gt;On the formatting side, it took a bit of digging but I was able to convert a &lt;code&gt;ZonedDateTime&lt;/code&gt; to a human-readable time (think displaying just the &lt;code&gt;HH:MM&lt;/code&gt;), using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat&quot;&gt;Intl.DateTimeFormat&lt;/a&gt; API.&lt;/p&gt;
&lt;p&gt;Unlike the Java formatter, which does take a literal &lt;code&gt;HH:mm&lt;/code&gt; string, this API requires you to pass in an object of the fields you want to display:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; f&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; Intl&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;DateTimeFormat&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;en-us&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  hour:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &quot;2-digit&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  minute:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &quot;2-digit&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; etahhmm&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;eta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;toInstant&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;epochMilliseconds&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a little unorthodox compared to the Java Time APIs, or Moment/date-fns/etc, but once you try it out it&#39;s pretty straightforward.&lt;/p&gt;
&lt;p&gt;And it&#39;s not nearly as painful to use as the &lt;a href=&quot;https://golang.org/src/time/format.go&quot;&gt;Golang formatter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Overall the Temporal API has been really pleasant to use. I can&#39;t wait to use it as it gets built into browsers and Node, and not have to pull in another third-party Date library again.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Vaccinated</title>
    <link href="https://www.steele.blue/vaccinated/" />
    <updated>2021-03-11T00:00:00Z</updated>
    <id>https://www.steele.blue/vaccinated/</id>
    <content type="html">&lt;p&gt;I got vaccinated today, and my primary side effect was feeling conflicted about it.&lt;/p&gt;
&lt;p&gt;By my standards, I should still be waiting for months to get immunized. I’m healthy, in my 30s, and have been working remote for virtually the whole last year. But I work for a railroad, which are classified by the state as essential employees in the transportation sector.&lt;/p&gt;
&lt;p&gt;I was really torn about this. There are still so many high-risk folks who won’t be getting a chance for at least another month or two. In the state I live, immunocompromised folks are lumped in with the general public, and get no accommodations. And the math is ultimately zero-sum: every dose taken by someone who can manage their risk is a dose that can’t be given to someone more vulnerable.&lt;/p&gt;
&lt;p&gt;But ultimately I couldn’t identify a clear distinction between my hesitation, and the more general vaccine hesitancy that’s resulting in large numbers of essential workers. And every recent story of an &lt;a href=&quot;https://twitter.com/stoddardOWH/status/1369686881542299648?s=20&quot;&gt;outbreak in a nursing home&lt;/a&gt;, where 40% of the unvaccinated nurses contracted Covid while 0% of those vaccinated did, solidified the decision.&lt;/p&gt;
&lt;p&gt;I really hope we’ll be able to get out shit together and &lt;a href=&quot;https://xkcd.com/2409/&quot;&gt;steepen the curve&lt;/a&gt;, and that we flip from being supply constrained to needing to seek out the remaining folks. But I’m ready to start turning the corner from the last hellish year.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Green Screen Zwifting with retroreflective fabric</title>
    <link href="https://www.steele.blue/zwift-greenscreen/" />
    <updated>2021-01-11T00:00:00Z</updated>
    <id>https://www.steele.blue/zwift-greenscreen/</id>
    <content type="html">&lt;p&gt;As group bike rides are doubly out this pandemic-laden winter, I&#39;ve been doing more bike racing on Zwift, and playing with livestreaming races.&lt;/p&gt;
&lt;p&gt;Zwift streams in-world aren&#39;t especially exciting. It&#39;s way more fun when you can watch a video of the person riding and suffering along.
And if you&#39;re going to be suffering, you want to increase the production value.&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;iqXJv3f0VhE&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;I&#39;ve been having a great time adding chroma key (green screen) to my video. Traditionally this has been a pain, requiring significant investments in lighting and screen positioning to do well.
But with some LEDs and retroreflective fabric, you can make a well-functioning green screen on the cheap.&lt;/p&gt;
&lt;p&gt;Most green screens use, well, green fabric. But this approach uses a different strategy: putting a ring of green LEDs around a camera and pointing it at &lt;a href=&quot;https://en.wikipedia.org/wiki/Retroreflector#Other_uses&quot;&gt;retroreflective fabric&lt;/a&gt;, so the green light shines directly back at the source.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/XROqbJpmJx-562.jpeg&quot; alt=&quot;green4&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;341&quot;&gt;&lt;/p&gt;
&lt;p&gt;For the most part, I followed &lt;a href=&quot;https://www.brainy-bits.com/post/making-a-green-screen-that-doesn-t-require-any-lighting&quot;&gt;this guide on Brainy Bits&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I was able to build this all out for about $40, which is a steal compared to other green screens.&lt;/p&gt;
&lt;h2&gt;Materials Needed&lt;/h2&gt;
&lt;p&gt;I presume you have a webcam already.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Retroreflective fabric: I picked up four yards from an &lt;a href=&quot;https://www.ebay.com/itm/SILVER-REFLECTIVE-FABRIC-sew-on-material-width-39-inch-1-meter/111778351514?ssPageName=STRK%3AMEBIDX%3AIT&amp;amp;var=410769250135&amp;amp;_trksid=p2057872.m2749.l2649&quot;&gt;eBay seller in Canada&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Some way to hang your fabric: I used some magnets, but you could sew it around a PVC frame, or just tape it to a wall&lt;/li&gt;
&lt;li&gt;LED Ring: &lt;a href=&quot;https://www.aliexpress.com/item/1005001579299841.html?spm=a2g0s.9042311.0.0.189d4c4dksRwrm&quot;&gt;16 LED NeoPixel clone from AliExpress&lt;/a&gt; works well&lt;/li&gt;
&lt;li&gt;Something to drive the LEDs: I used an Arduino I had lying around, but anything that can drive a NeoPixel works, such as &lt;a href=&quot;https://www.amazon.com/gp/product/B075SXMD9Z/ref=ppx_yo_dt_b_asin_title_o00_s01?ie=UTF8&amp;amp;psc=1&quot;&gt;this IR remote&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Building it&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/CMBIdr4VZl-600.jpeg&quot; alt=&quot;green1&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/CMBIdr4VZl-600.jpeg 600w, https://www.steele.blue/img/CMBIdr4VZl-1000.jpeg 1000w, https://www.steele.blue/img/CMBIdr4VZl-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you&#39;re using an Arduino to drive the LEDs, you should be able to hook up 5V, ground, and a data pin to the LED rings. A simple app using &lt;a href=&quot;https://github.com/adafruit/Adafruit_NeoPixel&quot;&gt;Adafruit&#39;s library&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;#include&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &amp;#x3C;Adafruit_NeoPixel.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;#define&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; PIN        &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;6&lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt; // On Trinket or Gemma, suggest changing this to 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;#define&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; NUMPIXELS &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;16&lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt; // Popular NeoPixel ring size&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;Adafruit_NeoPixel&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; pixels&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;NUMPIXELS&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;PIN&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;NEO_GRB&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; + &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;NEO_KHZ800&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; setup&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  pixels&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;begin&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt; // INITIALIZE NeoPixel strip object (REQUIRED)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; loop&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  pixels&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;clear&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt; // Set all pixel colors to &#39;off&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; i=&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;; i&amp;#x3C;NUMPIXELS; i++) {&lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt; // For each pixel...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    pixels&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;setPixelColor&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(i, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pixels&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;Color&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    pixels&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;show&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;   // Send the updated pixel colors to the hardware.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Place the ring around your webcam; I used some Velcro stickers so I can easily remove it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/EQaOrE5mb3-600.jpeg&quot; alt=&quot;green2&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/EQaOrE5mb3-600.jpeg 600w, https://www.steele.blue/img/EQaOrE5mb3-1000.jpeg 1000w, https://www.steele.blue/img/EQaOrE5mb3-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Hang up the fabric; it should be far enough away from your bike that it won&#39;t get in the way while you&#39;re riding, but close enough that you don&#39;t need to buy a ton of it.
I ended up cutting the fabric to make two 3&#39;x6&#39; banners, and hanging on the ceiling with neodymium magnets.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/8LM3kPvXhE-600.jpeg&quot; alt=&quot;green3&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/8LM3kPvXhE-600.jpeg 600w, https://www.steele.blue/img/8LM3kPvXhE-1000.jpeg 1000w, https://www.steele.blue/img/8LM3kPvXhE-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Chroma Key in OBS&lt;/h2&gt;
&lt;p&gt;Power the LEDs to a pure green. You can probaby run it at a low power; if you go too high you&#39;ll get some green on you, which isn&#39;t ideal. I have mine at 25% of max.&lt;/p&gt;
&lt;p&gt;This can be set in OBS as a Filter on your webcam Source. You can play with the properties, but I was able to get by with the default values.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/BqmUK610IK-600.png&quot; alt=&quot;chroma-key&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;863&quot; height=&quot;757&quot; srcset=&quot;https://www.steele.blue/img/BqmUK610IK-600.png 600w, https://www.steele.blue/img/BqmUK610IK-863.png 863w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then setup your stream layout. A decent placement is in the bottom left of the screen, Zwift doesn&#39;t put anything useful in there.&lt;/p&gt;
&lt;p&gt;Happy Zwifting! Your FTP won&#39;t be any higher, but at least you&#39;ll look a little more stylish riding in Watopia.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Getting the PPPoE Credentials from your C4000XG without calling CenturyLink</title>
    <link href="https://www.steele.blue/c4000xg-ppoe-password/" />
    <updated>2020-08-24T00:00:00Z</updated>
    <id>https://www.steele.blue/c4000xg-ppoe-password/</id>
    <content type="html">&lt;p&gt;If you&#39;ve switched to CenturyLink recently, you might want to hook up your own router directly to the ONT, and bypass the router they provide/rent to you.&lt;/p&gt;
&lt;p&gt;This is pretty easy to do! See &lt;a href=&quot;https://www.reddit.com/r/centurylink/comments/ic4asm/howto_you_may_not_need_that_c4000xg_or_whatever/&quot;&gt;this Reddit post for more details&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One thing you&#39;ll need to get from your ISP is the PPPoE userid and password the router uses to connect up to CenturyLink.
Normally this requires a phone call, but if they provided you a Greenwave C4000XG, you can grab it from the router&#39;s admin console.&lt;/p&gt;
&lt;p&gt;Simply log in to the C4000XG&#39;s web UI, and go to Advanced Setup -&amp;gt; WAN Settings, with your DevTools open.
Look for the XHR request &lt;code&gt;/cgi/cgi_get?Object=Device.PPP.Interface&lt;/code&gt;, and root around the JSON to find the credentials:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/NnkIvrGR2c-600.png&quot; alt=&quot;clink credentials&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2999&quot; height=&quot;1485&quot; srcset=&quot;https://www.steele.blue/img/NnkIvrGR2c-600.png 600w, https://www.steele.blue/img/NnkIvrGR2c-1000.png 1000w, https://www.steele.blue/img/NnkIvrGR2c-2999.png 2999w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Or, just paste this into your console:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;/cgi/cgi_get?Object=Device.PPP.Interface&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  headers:&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; Headers&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;    &#39;X-Requested-With&#39;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;XMLHttpRequest&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;then&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; d&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;then&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Objects&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;find&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;x&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; x&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;ObjName&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; === &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;Device.PPP.Interface.1&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;      .&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Param&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;find&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;x&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; x&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;ParamName&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; === &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;Username&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; pass&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;find&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;x&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; x&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;ParamName&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; === &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;Password&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    console&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`User: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;ParamValue&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    console&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`Pass: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;pass&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;ParamValue&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not sure if this is service journalism or just a reminder to myself when I forget in a month!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Setting up a Livestream with Owncast</title>
    <link href="https://www.steele.blue/livestream-setup/" />
    <updated>2020-08-17T00:00:00Z</updated>
    <id>https://www.steele.blue/livestream-setup/</id>
    <content type="html">&lt;p&gt;So you&#39;ve decided you want to livestream your event and &lt;a href=&quot;https://www.steele.blue/indieweb-wedding-livestream&quot;&gt;run the server yourself&lt;/a&gt; instead of relying on Twitch?
Neat! Here&#39;s the recipe I used for my wedding.&lt;/p&gt;
&lt;p&gt;This is a single-camera setup, streaming through &lt;a href=&quot;https://obsproject.com/&quot;&gt;OBS&lt;/a&gt; to your server and a Zoom room.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/IhvU6i2ysg-600.jpeg&quot; alt=&quot;owncast-2&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; srcset=&quot;https://www.steele.blue/img/IhvU6i2ysg-600.jpeg 600w, https://www.steele.blue/img/IhvU6i2ysg-1000.jpeg 1000w, https://www.steele.blue/img/IhvU6i2ysg-1920.jpeg 1920w&quot; sizes=&quot;100vw&quot;&gt;
&lt;em&gt;Photo credit: Justin Duster&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Hardware&lt;/h2&gt;
&lt;p&gt;To run the livestream, you&#39;ll need:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Digital camera&lt;/strong&gt; - We used a Canon DSLR, connected to a computer via USB. You can also use the HDMI out along with a capture card if your device supports a &lt;a href=&quot;https://1.shortstack.com/r2zfS5&quot;&gt;&amp;quot;clean&amp;quot; video out&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It needs to be discoverable as a video input in OBS; not all digital cameras are!
Our first attempt was with a 2015-era Canon camcorder, but it wasn&#39;t readable as an input source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audio input&lt;/strong&gt; - If you have access to a mixer at the event, you can patch into the output and get high-quality from multiple sources.
In our case, we had audio from a few microphones as well as another laptop running the soundtrack.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Server running Owncast&lt;/strong&gt; - I used a DigitalOcean VPS for this.
Setting up Owncast is &lt;a href=&quot;https://github.com/gabek/owncast/blob/master/doc/quickstart.md&quot;&gt;documented in its Quickstart&lt;/a&gt;. I had it running in a Docker container.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Computer broadcasting via OBS&lt;/strong&gt; - Hook up the audio/video sources into &lt;a href=&quot;https://obsproject.com/&quot;&gt;OBS&lt;/a&gt; and broadcast a stream to your Owncast server.&lt;/p&gt;
&lt;p&gt;Depending on what else you have going on concurrently (such as simulcasting to Zoom), you may need a relatively powerful workstation for this.
Our first attempt at broadcasting was on my i5 laptop, and it began dropping frames almost immediately! Switching to a gaming laptop resolved the issue.&lt;/p&gt;
&lt;h2&gt;Streaming pipeline&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/9JTsKeKlP4-600.png&quot; alt=&quot;owncast flowchart&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;336&quot; srcset=&quot;https://www.steele.blue/img/9JTsKeKlP4-600.png 600w, https://www.steele.blue/img/9JTsKeKlP4-773.png 773w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Owncast supports serving video files directly from the server, but we ran into constraints both on bandwidth and CPU utilization.
A better approach is to configure Owncast to upload files to an S3-compatible storage and serve them via HTTP.&lt;/p&gt;
&lt;p&gt;DigitalOcean&#39;s Spaces, an S3-compatible offering, fit the bill. You can follow Owncast&#39;s &lt;a href=&quot;https://github.com/gabek/owncast/blob/master/doc/S3.md&quot;&gt;S3 guidelines&lt;/a&gt; for the storage provider of your choice.&lt;/p&gt;
&lt;h2&gt;User Interface&lt;/h2&gt;
&lt;p&gt;You can simply point folks to the Owncast&#39;s web UI. It&#39;s got chat integrated and is pretty nice!&lt;/p&gt;
&lt;p&gt;I just wanted to emed the video onto our website, so we were responsible for embedding the HLS stream on our website.&lt;/p&gt;
&lt;p&gt;You&#39;ll need a library to read the HLS playlist (from the Owncast server at &lt;code&gt;/hls/stream.m3u8&lt;/code&gt;).
We used &lt;a href=&quot;https://videojs.com/&quot;&gt;videojs&lt;/a&gt; for a nicer &amp;quot;live&amp;quot; UI, but you can also use a native &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element alongside the &lt;a href=&quot;https://github.com/video-dev/hls.js&quot;&gt;hls.js&lt;/a&gt; library.&lt;/p&gt;
&lt;h2&gt;Simulcasting to Zoom&lt;/h2&gt;
&lt;p&gt;Basically I followed &lt;a href=&quot;https://streamgeeks.us/how-to-connect-zoom-obs/&quot;&gt;this guide&lt;/a&gt;, which configures OBS&#39;s output as a &amp;quot;virtual camera&amp;quot; to feed into a Zoom room.&lt;/p&gt;
&lt;p&gt;You also have the option to have &lt;a href=&quot;https://support.zoom.us/hc/en-us/articles/115001777826-Live-Streaming-Meetings-or-Webinars-Using-a-Custom-Service&quot;&gt;Zoom be the &amp;quot;driver&amp;quot; of the stream&lt;/a&gt; to an RTMP server, but that puts an ugly Zoom watermark on the stream.
If you have the bandwidth, it&#39;s better to upload via OBS and also have Zoom running.&lt;/p&gt;
&lt;h2&gt;Running the stream&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/47V4BOZbCp-600.jpeg&quot; alt=&quot;Not what you want to see 10 minutes before your wedding&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/47V4BOZbCp-600.jpeg 600w, https://www.steele.blue/img/47V4BOZbCp-1000.jpeg 1000w, https://www.steele.blue/img/47V4BOZbCp-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;We put together a Google doc with the playbook:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;30 minutes before the ceremony:&lt;/strong&gt; Launch OBS and configure input sources. Test audio levels, etc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10 minutes before:&lt;/strong&gt; Begin streaming a &amp;quot;Starting soon&amp;quot; scene to Owncast and Zoom&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5 minutes before the ceremony:&lt;/strong&gt; Begin recording the stream in OBS (to save a local copy)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start of ceremony&lt;/strong&gt; - Switch OBS from &amp;quot;Starting soon&amp;quot; scene to &amp;quot;real&amp;quot; video input&lt;/p&gt;
&lt;p&gt;If you&#39;re simulcasting to Zoom, I also recommend deputizing someone as co-host, so they can admit viewers, mute noisy folks, etc.&lt;/p&gt;
&lt;h2&gt;Stuff I learned along the way&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Test your stream beforehand&lt;/strong&gt; - I did a few test streams in the weeks leading up to the event (I streamed some movies with friends), and the tests paid off like crazy.
You don&#39;t want to be figuring on your event date what your server&#39;s optimal encoding settings are, or whether you need to host your video files via S3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Communicate with collaborators&lt;/strong&gt; - Ten minutes prior to my wedding starting, I was SSHed into the server, recompiling the Owncast container to switch off CDN file serving.
Why? Because I miscommunicated with my videographer where a &amp;quot;test URL&amp;quot; would be setup, which resulted in an improper OBS configuration. Oops.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For short events, scaling your server vertically is cheap and easy&lt;/strong&gt; - I started with a cheap $5/month server on DigitalOcean, but increased its CPU and RAM substantially (to their $160 server) to support transcoding 3 quality levels concurrently.&lt;/p&gt;
&lt;p&gt;Since DO bills by the hour, it was still cheap! I only ran the server at this level for a day during testing and the ceremony, so it didn&#39;t actually cost more than a couple bucks to run. Way cheaper than spending a ton of time tuning server settings to reach optimum levels.&lt;/p&gt;
&lt;p&gt;Thanks again to Justin Duster for helping run the event, so I didn&#39;t have to stress out too much on my wedding day!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Indieweb Livestreaming your Wedding with Owncast</title>
    <link href="https://www.steele.blue/indieweb-wedding-livestream/" />
    <updated>2020-08-12T00:00:00Z</updated>
    <id>https://www.steele.blue/indieweb-wedding-livestream/</id>
    <content type="html">&lt;p&gt;I got married! It looked a little different than we originally planned, due to it being, um, 2020.
Venue restrictions due to the global pandemic reduced the number of guests we could invite by 90%. And we had no desire to put our friends and family in harm&#39;s way.&lt;/p&gt;
&lt;p&gt;But hey, it&#39;s 2020! With new tools like &lt;a href=&quot;https://gabekangas.com/blog/2020/06/owncast-a-project-to-take-control-over-your-own-live-streaming/&quot;&gt;Owncast&lt;/a&gt;, you can run a livestream for all the guests who can&#39;t make it to your event in person.
And you can do so without giving up control of your content, or acceding to the whims of companies who might not have your best interest at heart.&lt;/p&gt;
&lt;p&gt;I&#39;ll writeup the technical recipe we used in a later post, but thought the desire to host a livestreaming server was worth writing about separately.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/gWUjEbu3Yz-600.jpeg&quot; alt=&quot;Watching the livestream&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1536&quot; srcset=&quot;https://www.steele.blue/img/gWUjEbu3Yz-600.jpeg 600w, https://www.steele.blue/img/gWUjEbu3Yz-1000.jpeg 1000w, https://www.steele.blue/img/gWUjEbu3Yz-1536.jpeg 1536w&quot; sizes=&quot;100vw&quot;&gt;
&lt;em&gt;Photo credit: Ben Turner&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Twitch meets Indieweb&lt;/h2&gt;
&lt;p&gt;One of the lessons I&#39;ve seen on the Web the past few years has been the cost to the increasing consolidation and siloing of your own content.
Writing on Medium instead of hosting your own blog, for example, means you&#39;re &lt;a href=&quot;https://indieweb.org/Medium#Issues&quot;&gt;subject to their nagware&lt;/a&gt;, interstials to drive readers toward their native app, and other user-hostile actions. Your content is there for the purpose of increasing Medium&#39;s engagement and MAU numbers.&lt;/p&gt;
&lt;p&gt;Streaming sites are just as subject to this nefarios behavior.
Twitch videos are routinely subject to &lt;a href=&quot;https://twitter.com/TwitchSupport/status/1269851779790929921&quot;&gt;DMCA takedowns&lt;/a&gt;, YouTube prevents you from streaming &lt;a href=&quot;https://support.google.com/youtube/answer/2853834?hl=en&quot;&gt;unless you have a requisite number of subscribers&lt;/a&gt;, and Facebook takes down DJ sets so routinely that &lt;a href=&quot;https://web.archive.org/web/20200921195316/https://www.papermag.com/instagram-live-copyright-dj-censoring-2645789312.html?rebelltitem=16&quot;&gt;DJs are forced to mix faster to avoid detection algorithms&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And content disappears. When Twitch&#39;s predecessor justin.tv pivoted to gaming, its &lt;a href=&quot;https://arstechnica.com/gaming/2014/08/streaming-video-site-justin-tv-announces-closure-effective-immediately/&quot;&gt;millions of videos&lt;/a&gt; were all taken offline. &lt;a href=&quot;https://mixer.com/&quot;&gt;Mixer just shut down last month&lt;/a&gt;. More sites to add to the &lt;a href=&quot;https://indieweb.org/site-deaths&quot;&gt;internet&#39;s cemetary&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This was a real concern for our wedding: we were going to have copyrighted music before the ceremony, and didn&#39;t want to just hope the &lt;a href=&quot;https://www.washingtonpost.com/entertainment/music/copyright-bots-and-classical-musicians-are-fighting-online-the-bots-are-winning/2020/05/20/a11e349c-98ae-11ea-89fd-28fb313d1886_story.html&quot;&gt;takedown bots wouldn&#39;t find us&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But through cheap cloud VPS hosting and open-source tooling, we now have the tools to take back control of our streaming world.&lt;/p&gt;
&lt;h2&gt;Streaming with Owncast&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://gabekangas.com/blog/2020/06/owncast-a-project-to-take-control-over-your-own-live-streaming/&quot;&gt;Owncast&lt;/a&gt; project couldn&#39;t have arrived at a better time.
Billed as &amp;quot;Twitch in a box&amp;quot;, it lets you start up a server to publish an RTMP video stream, transcode it to various qualities, and host an HLS-compatible livestream that will automatically choose the proper bitrate for the user&#39;s bandwidth.
It also provides a website to view the stream alongside a chat feed.
Plus it&#39;s &lt;a href=&quot;https://github.com/gabek/owncast&quot;&gt;written in Go&lt;/a&gt;, so it was easy to start contributing new features and bugfixes.&lt;/p&gt;
&lt;p&gt;Combined with tools like &lt;a href=&quot;https://obsproject.com/&quot;&gt;OBS&lt;/a&gt; to publish a stream, you can easily start broadcasting with a $5 VPS from the cloud provider of your choice.&lt;/p&gt;
&lt;p&gt;And since you own the server, you can customize it to your liking.
We just wanted the video and weren&#39;t interested in chat, so we embedded the stream directly on the wedding website.
We also were able to use the Owncast status API to detect when the stream was live, and disabled the embed while the stream wasn&#39;t running.&lt;/p&gt;
&lt;h2&gt;Publish Once, Syndicate Everywhere&lt;/h2&gt;
&lt;p&gt;Some of our wedding guests asked for a Zoom link, so we set up a simulcast to broadcast to a Zoom room, alongside our wedding website.
This was a convenient way to meet folks where they already were.&lt;/p&gt;
&lt;p&gt;In total we had 47 folks streaming via the Owncast server, and another 50 or so on the Zoom call.
It&#39;s a nice way to garner the benefits of the &lt;a href=&quot;https://indieweb.org/POSSE&quot;&gt;POSSE&lt;/a&gt; philosophy, and be pragmatic about garnering the reach social networks afford without relinquishing control of your content.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Maybe mashups in 2020 are just Jamstack sites</title>
    <link href="https://www.steele.blue/jamstack-mashups/" />
    <updated>2020-07-06T00:00:00Z</updated>
    <id>https://www.steele.blue/jamstack-mashups/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;&lt;em&gt;Millhouse voice&lt;/em&gt;&lt;/em&gt; Remember mashups? They&#39;re back, in Jamstack form.&lt;/p&gt;
&lt;p&gt;Recommended listening for this post:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;uWzkK7tUjaU&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;A decade ago the Big Idea in front-end development was the &lt;a href=&quot;https://en.wikipedia.org/wiki/Mashup_%28web_application_hybrid%29&quot;&gt;mashup&lt;/a&gt;; taking disparate data sources and combining them via JavaScript trickery to create something novel. Want to pull down the current weather and compare it to the climates on Star Wars planets? &lt;a href=&quot;https://www.slashfilm.com/star-wars-weather-compares-local-weather-conditions-to-star-wars-planets/&quot;&gt;Hell yeah, dawg&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Did you know there was a nonprofit consortium dedicated to enterprise mashup standards? &lt;a href=&quot;https://en.wikipedia.org/wiki/Open_Mashup_Alliance&quot;&gt;Open Mashup Alliance&lt;/a&gt;, we need you now more than ever!&lt;/p&gt;
&lt;p&gt;As time went on, data providers started to claw back and restrict access to their data. And it&#39;s a one-way wrench: the trajectory of most companies is to start with an open set of APIs, and then gradually restrict it to first-party apps, and only with OAuth token workflows that cannot be implemented safely in the browser.&lt;/p&gt;
&lt;p&gt;As RSS feeds disappeared and unauthenticated REST endpoints vanished, it became impossible to technically implement the mashup of your dreams. But the dream never died, it just moved server-side.&lt;/p&gt;
&lt;p&gt;I think that&#39;s why I&#39;m excited by static site generators, and Jamstack in general. It&#39;s trivial to integrate multiple data sources with tools like Gatsby or Eleventy. And since it all runs server-side, you can integrate using whatever arcane restrictions, API keys, or other tomfoolery that services require you to go through to acccess the data that&#39;s legally yours.&lt;/p&gt;
&lt;p&gt;Lately I&#39;ve been playing with Strava&#39;s datasets, which are inaccessible in the browser. But by moving processing to the server, I was able to build out &lt;a href=&quot;https://trails.steele.blue/&quot;&gt;Omaha Trail Status&lt;/a&gt;, which integrates segment ride information with GeoJSON maps, and heatmap histograms of ride data.&lt;/p&gt;
&lt;p&gt;The whole thing is hosted on a CDN, populates a serverless database via a serverless functions, and is easily in the free tier of cloud services.&lt;/p&gt;
&lt;p&gt;So get yourself back in the 2009 mindset, throw on a &lt;a href=&quot;https://www.youtube.com/watch?v=vU62x2PnSO4&quot;&gt;Girl Talk&lt;/a&gt; album, and build the mashup of your dreams.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Secret Strava</title>
    <link href="https://www.steele.blue/secret-strava/" />
    <updated>2020-06-15T00:00:00Z</updated>
    <id>https://www.steele.blue/secret-strava/</id>
    <content type="html">&lt;p&gt;I built a thing that I&#39;m not sure anyone else will find useful, but it was fun and I learned a few things.&lt;/p&gt;
&lt;h2&gt;Strava needs better privacy controls&lt;/h2&gt;
&lt;p&gt;As a social network for athletes, &lt;a href=&quot;https://strava.com&quot;&gt;Strava&#39;s&lt;/a&gt; pretty good. It might be the only good one around these days!
But their privacy controls are lacking. You can set each activity to be visible to everyone, only your followers, or just yourself, but it&#39;s a manual tag, which can be especially cumbersome if your activities are auto-uploaded from your GPS device.&lt;/p&gt;
&lt;p&gt;Ideally you&#39;d be able to set some heuristics on your activities, such that &amp;quot;commutes&amp;quot; stay private, but races and long weekend rides are more visible. This isn&#39;t possible out of the box, but you can cobble something together with &lt;a href=&quot;https://developers.strava.com/&quot;&gt;their API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So that&#39;s what &lt;a href=&quot;https://github.com/mattdsteele/secret-strava&quot;&gt;Secret Strava&lt;/a&gt; does: &lt;strong&gt;every time you upload an activity, it checks against a set of rules on whether it should be made public&lt;/strong&gt;, such as the distance ridden, if it&#39;s tagged as a commute, etc. This is done using a combination of a webhook subscription, their REST API, and some screen-scraping glue.&lt;/p&gt;
&lt;h2&gt;Building the app&lt;/h2&gt;
&lt;p&gt;One of the top Strava client libraries is @dblock&#39;s &lt;a href=&quot;https://github.com/dblock/strava-ruby-client&quot;&gt;strava-ruby-client&lt;/a&gt;, so I built an Ruby app so I could use it.&lt;/p&gt;
&lt;p&gt;The app itself runs as a few Sinatra endpoints, running on GCP as a Cloud Run app, with OAuth tokens stored in a Fauna database.&lt;/p&gt;
&lt;h3&gt;A few things I enjoyed about this stack&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cloud Run for serverless containers&lt;/strong&gt; - I don&#39;t like maintaining servers for side projects, but Ruby isn&#39;t natively supported with Lambda, Cloud Functions, or any of the other major serverless providers.&lt;/p&gt;
&lt;p&gt;So I was happy to hear about &lt;a href=&quot;https://knative.dev/&quot;&gt;Knative&lt;/a&gt;, and Google&#39;s Cloud Run implementation, which provides similar managed, autoscaling, scales-to-zero-instances to any Docker container.&lt;/p&gt;
&lt;p&gt;It was pretty neat to take my running app from my workstation, and push to GCP just by creating a Dockerfile, without having to make any other app changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database as a service&lt;/strong&gt; - This has gotten a lot easier since I last had to persist anything. Fauna&#39;s model lets you create a store and query it just by defining a GraphQL schema. Even compared to other managed databases this is pretty straightforward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CI/CD with GitHub Actions&lt;/strong&gt; - These are still great. I&#39;m not sure I&#39;ll go back to anything else for OSS projects.&lt;/p&gt;
&lt;h3&gt;Stuff I had trouble with&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GraphQL queries on the server&lt;/strong&gt; - Maybe this is better in other server-side languages, but this part was rough. Even using GitHub&#39;s &lt;a href=&quot;https://web.archive.org/web/20200526015829/https://github.com/github/graphql-client&quot;&gt;graphql-client&lt;/a&gt;, I missed out on most of the things I get from Apollo+TypeScript, such as development-time syntax verification. Compared to other ORM approaches it felt very messy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ruby&lt;/strong&gt; - It&#39;s been almost 10 years since I wrote any Ruby, so I&#39;m definitely a little rusty, but everything felt sloppy compared to the TypeScript/Java/Go I&#39;m used to coding in.&lt;/p&gt;
&lt;p&gt;Without any basic editor assistance like method autocomplete, auto-imports, or syntax errors, I had to rely much more on unit tests (which luckily are still great in Ruby!) and manual scripts.&lt;/p&gt;
&lt;p&gt;But Ruby&#39;s reliance on building DSLs, and monkey-patching existing classes consistently kept me guessing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strava&#39;s API, and social APIs generally&lt;/strong&gt; - As I started to build this out, Strava made some pretty significant changes to their API, with the intention of &lt;a href=&quot;https://www.dcrainmaker.com/2020/05/strava-cuts-off-leaderboard-for-free-users-reduces-3rd-party-apps-for-all-and-more.html&quot;&gt;converting more users into subscribers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Even prior to these recent change, they&#39;ve been reducing the power of their APIs, such as &lt;a href=&quot;https://groups.google.com/d/topic/strava-api/L_zZNdgV24c/discussion&quot;&gt;removing the ability to change an activity&#39;s privacy&lt;/a&gt;, so I had to resort to screen-scraping.&lt;/p&gt;
&lt;p&gt;I have very little faith that Strava has third-party developers in mind when building their platform, but this appears to be the bargain we&#39;ve struck for acess to any data from within the walled gardens of a social network.&lt;/p&gt;
&lt;p&gt;Tom Scott has a good overview of the tradeoffs we&#39;ve decided as an industry to make (and RIP to the legendary Yahoo Pipes):&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;BxV14h0kFs0&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;h2&gt;Heroku was ahead of its time&lt;/h2&gt;
&lt;p&gt;I really liked building and deploying to GCP using Cloud Run. The abstraction for general-purpose HTTP servers feels right, and the auto-scaling means the price is right.&lt;/p&gt;
&lt;p&gt;But at the end of the day, I&#39;m not sure it&#39;s any more advanced than Heroku&#39;s original PaaS product from like, 10 years ago. It&#39;s crazy how advanced Heroku was compared to the other toolchains at the time.&lt;/p&gt;
&lt;p&gt;Has the last decade of Cloud Native tooling really just been a process of standardizing the offering across other languages, and making the PaaS tooling work with Docker/Kubernetes?&lt;/p&gt;
&lt;p&gt;I&#39;d love to know more about what other PaaS offerings are like. From my perspective, Cloud Foundry/Heroku/OpenShift/Knative are all pretty interchangeable. If there&#39;s more nuance here, let me know!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>&lt;bt-device&gt; and Renderless Web Components</title>
    <link href="https://www.steele.blue/renderless-web-components/" />
    <updated>2020-05-04T00:00:00Z</updated>
    <id>https://www.steele.blue/renderless-web-components/</id>
    <content type="html">&lt;p&gt;I keep buying &lt;a href=&quot;https://www.steele.blue/web-bluetooth&quot;&gt;dumb Bluetooth devices&lt;/a&gt; and hooking them up to websites, but there&#39;s just enough boilerplate to make integrating them a hassle.
So, I &lt;a href=&quot;https://github.com/mattdsteele/bt-device/&quot;&gt;made a little Custom Element&lt;/a&gt; to help automate some of the boilerplate.&lt;/p&gt;
&lt;p&gt;You&#39;d use &lt;code&gt;&amp;lt;bt-device&amp;gt;&lt;/code&gt; like any other HTML element:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;&amp;#x3C;!-- Connects to a Thermos Smart Lid water bottle --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;bt-device&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  service&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;40fc0000-8a8d-4a32-a455-c1148e24a9f1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  characteristic&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;40fc0001-8a8d-4a32-a455-c1148e24a9f1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  notifications&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;bt-device&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then grab the element and connect up to receive notifications:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; device&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;document&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;bt-device&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; device&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;connect&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;device&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;data&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  console&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;detail&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&#39;s basically it! Building this makes it easy to add more advanced, cross-cutting features like automatic reconnect, exponential backoff, and the like.&lt;/p&gt;
&lt;h1&gt;Renderless Web Components&lt;/h1&gt;
&lt;p&gt;This is a Web Component, but doesn&#39;t use Shadow DOM, Stencil, or any library. In fact, it doesn&#39;t render anything to the DOM at all.
Rather, it&#39;s just distributed as a Custom Element to make for easy integration with just a script tag and some HTML; just like the good old days.&lt;/p&gt;
&lt;p&gt;I&#39;m not sure if this pattern has a formal name, but I&#39;ve been calling them &amp;quot;renderless web components&amp;quot;. I&#39;ve seen a handful of these in the wild, such as Polymer&#39;s &lt;a href=&quot;https://www.webcomponents.org/element/@polymer/iron-ajax&quot;&gt;iron-ajax&lt;/a&gt; for making Ajax calls, or &lt;a href=&quot;https://www.webcomponents.org/element/PolymerElements/app-pouchdb/elements/app-pouchdb-query&quot;&gt;app-pouchdb-query&lt;/a&gt; to handle database queries.&lt;/p&gt;
&lt;p&gt;I&#39;m no React expert, but there are a few interesting renderless components out there, such as &lt;a href=&quot;https://rena.to/react-powerplug/#/&quot;&gt;powerplug&lt;/a&gt;. Components that delegate to a render prop (such as &lt;a href=&quot;https://kentcdodds.com/blog/introducing-downshift-for-react&quot;&gt;Downshift&lt;/a&gt;) seem to be operate with a similar philosophy.&lt;/p&gt;
&lt;p&gt;For web-facing libraries I quite like the idea of building them with HTML, rather than JavaScript, as the starting point.
For no other reason, It&#39;s pretty neat getting Bluetooth functionality in a browser without leaving the &amp;quot;HTML&amp;quot; tab in JSBin!
And it fits in nicely with declarative, component-based architecture; I&#39;ve integrated this component into Angular and LitElement apps super easily.&lt;/p&gt;
&lt;p&gt;I&#39;m not sure all web utilities can be built this way, but it&#39;s something to consider when building your next library!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Going Dark (Mode)</title>
    <link href="https://www.steele.blue/dark-mode/" />
    <updated>2019-10-14T00:00:00Z</updated>
    <id>https://www.steele.blue/dark-mode/</id>
    <content type="html">&lt;p&gt;Inspired by Jeremy Keith&#39;s &lt;a href=&quot;https://adactio.com/journal/15941&quot;&gt;post on implementing Dark Mode on the web&lt;/a&gt;, I set it up on my blog.
It&#39;s really easy!&lt;/p&gt;
&lt;p&gt;The diff &lt;a href=&quot;https://github.com/mattdsteele/steele.blue/commit/18ee028de01b96508962360d03cb5588f7181de9&quot;&gt;nearly fits on one screen&lt;/a&gt;; most of which is converting colors over to CSS custom properties. Once that&#39;s configured, Dark Mode is easy as pi:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;@media&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (prefers-color-scheme: dark) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#D7BA7D&quot;&gt;  :root&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;    --primary-color&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0451A5;--shiki-dark:#CE9178&quot;&gt;#efefef&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;    --secondary-color&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0451A5;--shiki-dark:#CE9178&quot;&gt;#1a1a21&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;    --link-color&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0451A5;--shiki-dark:#CE9178&quot;&gt;#1f8dba&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;m developing on Windows these days, which has supported Dark Mode for a while. So testing it out was pretty easy:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/oEaZnmhomE-600.gif&quot; alt=&quot;darkmode&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1475&quot; height=&quot;1776&quot; srcset=&quot;https://www.steele.blue/img/oEaZnmhomE-600.gif 600w, https://www.steele.blue/img/oEaZnmhomE-1000.gif 1000w, https://www.steele.blue/img/oEaZnmhomE-1475.gif 1475w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Building Fast, Tiny GitHub Actions with Go and Docker</title>
    <link href="https://www.steele.blue/tiny-github-actions/" />
    <updated>2019-09-23T00:00:00Z</updated>
    <id>https://www.steele.blue/tiny-github-actions/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://github.com/features/actions&quot;&gt;GitHub Actions&lt;/a&gt; are sweet! It&#39;s still in beta but it&#39;s a great way to automate tasks after things happen like code pushes, comments on Issues, pull requests, etc.&lt;/p&gt;
&lt;p&gt;As an author of a GitHub Action, I&#39;m really enamored by the architecture: each &amp;quot;build&amp;quot; is run on a virtual machine in Azure, with each action running inside a Docker container that GitHub executes. As Kyle Daigle mentions on &lt;a href=&quot;https://changelog.com/podcast/331#transcript-94&quot;&gt;a Changelog podcast:&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As a small developer my entire side business could be a Docker container. Not running it, not supporting the payment for it, just a Docker container.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GitHub does this by building and executing your Docker container on the fly. So if you want your Action to finish quickly, you&#39;ll want to spend some time optimizing.&lt;/p&gt;
&lt;p&gt;These are lessons I&#39;ve learned building an action to &lt;a href=&quot;https://github.com/marketplace/actions/particle-function&quot;&gt;execute a Particle function&lt;/a&gt;. I&#39;m not an expert, but they worked for me!&lt;/p&gt;
&lt;h2&gt;Optimization 1: Use a container deployed to Docker Hub&lt;/h2&gt;
&lt;p&gt;You can reference an action by pointing to a GitHub repository containing its source:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# Compiled just-in-time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;mattdsteele/particle-action@master&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, the first step for each of your user&#39;s workflow will be to checkout the action&#39;s source and compile the container. This takes time!&lt;/p&gt;
&lt;p&gt;So, if you build the container and publish it to a Docker registry like &lt;a href=&quot;https://hub.docker.com/&quot;&gt;Docker Hub&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# Precompiled, users just pull the container&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;docker://mattdsteele/particle-github-action:latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For my particular action, this improves builds by &lt;strong&gt;over 60 seconds!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Of course, you&#39;ll need to build and upload your container each time you push a change to your action. Luckily, &lt;a href=&quot;https://github.com/marketplace/actions/build-tag-publish-docker&quot;&gt;there&#39;s a GitHub Action for that&lt;/a&gt; :)&lt;/p&gt;
&lt;h2&gt;Optimization 2: Trim your Docker images&lt;/h2&gt;
&lt;p&gt;When referencing a Docker registry, GitHub has to download the image each time. And if you&#39;re not careful, Docker containers can be massive, which can slow down how fast your action executes. So the smaller you can make your container, the better.&lt;/p&gt;
&lt;h3&gt;Don&#39;t rely on a big parent image&lt;/h3&gt;
&lt;p&gt;My first version of the action relied on &lt;code&gt;curl&lt;/code&gt;, which even on a large-ish base like Debian wasn&#39;t available by default. So here&#39;s where I started:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; docker&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; images&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;REPOSITORY&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;                SIZE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;particle-bash&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;             90.8MB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Switching from Debian to a smaller Linux base like alpine is a great way to start optimizing:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;REPOSITORY&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;                SIZE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;particle-alpine&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;           12.6MB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Better!&lt;/p&gt;
&lt;h3&gt;Build a self-contained executable in Go&lt;/h3&gt;
&lt;p&gt;Going further, we can use Go to build a single, self-contained executable that doesn&#39;t rely on any operating system tools! So, I rewrote the action, switched the base to &lt;code&gt;FROM golang:latest&lt;/code&gt;, and got this:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;REPOSITORY&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;                SIZE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;particle-golang&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;           810MB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yikes! This is due to Docker images being additive; so the container includes the entire Go build toolchain.&lt;/p&gt;
&lt;h3&gt;Use a Docker multi-stage build&lt;/h3&gt;
&lt;p&gt;Let&#39;s fix this by setting up a &lt;a href=&quot;https://docs.docker.com/develop/develop-images/multistage-build/&quot;&gt;Docker multi-stage build&lt;/a&gt;, where we build the Go binary in one stage, and copy it into the &lt;a href=&quot;https://docs.docker.com/develop/develop-images/baseimages/&quot;&gt;empty &amp;quot;scratch&amp;quot; image&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# build stage&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; golang:alpine &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; build-env&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;RUN&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; apk --no-cache add build-base git gcc ca-certificates&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;COPY&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; go.mod particle.go /src/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;RUN&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; cd /src &amp;#x26;&amp;#x26; go build -o main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;# final stage&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; scratch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;COPY&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; --from=build-env /src/main /&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;COPY&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;ENTRYPOINT&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;/main&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(I also had to copy over the SSL certificates to the scratch image, since I was making HTTPS calls.)&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;REPOSITORY&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;                SIZE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;particle-scratch&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;          7.27MB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nice!&lt;/p&gt;
&lt;h3&gt;Optimize the Go binary&lt;/h3&gt;
&lt;p&gt;We can optimize the image size further by optimizing the Go binary with a couple flags:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;RUN&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; cd /src &amp;#x26;&amp;#x26; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;-w -s&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; -o main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another improvement:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;REPOSITORY&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;                SIZE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;particle-scratch-optim&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;    5.33MB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In total, these optimizations took my GitHub action from a &lt;strong&gt;6+ minute run down to 30 seconds&lt;/strong&gt;. Pretty sweet when you&#39;re &lt;a href=&quot;https://web.archive.org/web/20190922131203/https://twitter.com/mattdsteele/status/1173628386742345728&quot;&gt;waiting for the action to complete to get candy&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Why I Built Blumhouse to Purge My Twitter History</title>
    <link href="https://www.steele.blue/blumhouse/" />
    <updated>2019-03-20T00:00:00Z</updated>
    <id>https://www.steele.blue/blumhouse/</id>
    <content type="html">&lt;p&gt;Or: why would I delete gold like this?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/xvYQ0UjUSk-600.png&quot; alt=&quot;bad tweets&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1204&quot; height=&quot;305&quot; srcset=&quot;https://www.steele.blue/img/xvYQ0UjUSk-600.png 600w, https://www.steele.blue/img/xvYQ0UjUSk-1000.png 1000w, https://www.steele.blue/img/xvYQ0UjUSk-1204.png 1204w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I&#39;ve been on Twitter for eleven years and in that time amassed a trail of thousands of tweets. I couldn&#39;t tell you what 99% of them are about, because Twitter is meant to be ephemeral.
Unless you&#39;re &lt;a href=&quot;https://web.archive.org/web/20190704195024/https://twitter.com/pixelatedboat/status/741904787361300481&quot;&gt;Milkshake Duck&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Every week or two someone&#39;s old Twitter history gets weaponized against them. Sometimes it&#39;s &lt;a href=&quot;https://web.archive.org/web/20180825061230/https://money.cnn.com/2016/10/14/technology/ken-bone-reddit/&quot;&gt;Ken Bone&lt;/a&gt; or &lt;a href=&quot;https://www.washingtonpost.com/news/the-intersect/wp/2018/07/18/josh-haders-all-star-game-controversy-shows-how-online-ghosts-will-haunt-us-forever/?utm_term=.4855f2e8eaa1&quot;&gt;Josh Hader&lt;/a&gt;&#39;s history cropping up after achieving minor Internet fame.&lt;/p&gt;
&lt;p&gt;But more often it&#39;s the latest form of &lt;a href=&quot;https://www.washingtonpost.com/news/the-intersect/wp/2018/07/30/theres-no-good-reason-to-keep-old-tweets-online-heres-how-to-delete-them/?utm_term=.6da146165ecb&quot;&gt;oppo research&lt;/a&gt;; wherein trolls like Mike Cernovich dig up old tweets, and use them in bad faith to smear their ideological opponents. Trevor Noah, Sarah Jeong, Patton Oswalt - all fell victim to this in the last year.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/XSzJKUoRyf-600.png&quot; alt=&quot;old tweets&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1097&quot; height=&quot;539&quot; srcset=&quot;https://www.steele.blue/img/XSzJKUoRyf-600.png 600w, https://www.steele.blue/img/XSzJKUoRyf-1000.png 1000w, https://www.steele.blue/img/XSzJKUoRyf-1097.png 1097w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;So yeah, I want Twitter to behave more like Snapchat or Instagram Stories.&lt;/p&gt;
&lt;p&gt;Twitter will probably never provide this out of the box. So I built &lt;a href=&quot;https://github.com/mattdsteele/blumhouse&quot;&gt;Blumhouse&lt;/a&gt;, an app that purges (get it?) old tweets from your timeline. Before the purge, it also archives them in a private database and generates a static website for your old takes to live out their lives in privacy.&lt;/p&gt;
&lt;h1&gt;Turnkey Userland&lt;/h1&gt;
&lt;p&gt;There are a number of prebuilt tools to this problem. &lt;a href=&quot;https://tweetdelete.net/&quot;&gt;TweetDelete&lt;/a&gt; is a one-button approach that requires zero programming.
There are also self-hosted tools like &lt;a href=&quot;https://github.com/mholt/timeliner&quot;&gt;timeliner&lt;/a&gt; or &lt;a href=&quot;https://github.com/victoriadotdev/ephemeral&quot;&gt;ephemeral&lt;/a&gt; that provide archive or delete capabilities.&lt;/p&gt;
&lt;p&gt;You can also &lt;a href=&quot;https://web.archive.org/web/20190320025155/https://twitter.com/settings/your_twitter_data&quot;&gt;request an archive&lt;/a&gt; of your data from Twitter, if you like messing with giant zips of XML.&lt;/p&gt;
&lt;p&gt;But in true NIH fashion, none of them did exactly what I wanted so it became my latest side project. This also gave me an opportunity to play with Google&#39;s Cloud Platform, which recently announced &lt;a href=&quot;https://cloud.google.com/blog/products/application-development/cloud-functions-go-1-11-is-now-a-supported-language&quot;&gt;Serverless support for Go&lt;/a&gt;, among other goodies.&lt;/p&gt;
&lt;p&gt;But if you&#39;re cool with setting up Twitter API keys and a bit of cloud provisioning, feel free to run this yourself. Details are in the &lt;a href=&quot;https://github.com/mattdsteele/blumhouse#if-you-do-want-to-run-this-on-your-account&quot;&gt;README&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;This really should be built-in&lt;/h1&gt;
&lt;p&gt;The system isn&#39;t perfect; my tweets are still in the Internet Archive, the Library of Congress, and probably a bunch of other systems. But at least folks would have to put in a little work to root around in the past.&lt;/p&gt;
&lt;p&gt;Ideally Twitter would provide this feature. Maybe something like &amp;quot;Hide all tweets older than X days from your timeline&amp;quot;. Ideally old tweets would be purged from Twitter entirely.&lt;/p&gt;
&lt;p&gt;There&#39;s no intrinsic need for this lengthy paper trail. Even if your data is the product, Twitter&#39;s whole appeal to advertisers is centered around the &lt;em&gt;right now&lt;/em&gt;, not about my ephemera in 2011.&lt;/p&gt;
&lt;p&gt;Other systems come closer - Mastodon lets you specify per-toot privacy levels. If these private settings were editable, you could write a simple script to mark old posts as private.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Toolchainless</title>
    <link href="https://www.steele.blue/toolchainless/" />
    <updated>2019-02-26T00:00:00Z</updated>
    <id>https://www.steele.blue/toolchainless/</id>
    <content type="html">&lt;p&gt;I have a complicated relationship with build tools. I like &lt;a href=&quot;https://www.steele.blue/typescript-for-javaers&quot;&gt;TypeScript&lt;/a&gt;, Gulp, and the like. I think that &lt;a href=&quot;https://tomdale.net/2017/09/compilers-are-the-new-frameworks/&quot;&gt;compilers are the new frameworks&lt;/a&gt;.
Heck, even this static site is built with Gatsby; an overcomplicated toolchain to generate HTML files if there ever was one.&lt;/p&gt;
&lt;p&gt;But build tools are a &lt;a href=&quot;https://hannahatkin.com/roach-motel/&quot;&gt;Roach Motel&lt;/a&gt; in your stack: once you add them in, you&#39;re unlikely to ever abandon &#39;em. And it&#39;s never been easier to start a project with a ludicrously complicated toolchain. You&#39;re just one &lt;code&gt;npx create-react-app&lt;/code&gt; away from a running project, powered by 1023 dependencies in node_modules. &lt;a href=&quot;https://www.infoq.com/presentations/Simple-Made-Easy&quot;&gt;Easy, but not simple&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That&#39;s the bargain we&#39;ve made to improve our ergonomics. React and Vue and Angular are so incredible that we use them in spite of their toolchains, not because of them.
But I&#39;m excited about new methodologies that are breaking that link, and letting us &lt;strong&gt;develop without tools&lt;/strong&gt; for the first time since the good ol&#39; jQuery days.&lt;/p&gt;
&lt;h2&gt;You might not need build tools&lt;/h2&gt;
&lt;p&gt;Some tools have gone away because browsers got better. With features like CSS Custom Properties, &lt;a href=&quot;https://web.archive.org/web/20191218025653/https://hospodarets.com/you-might-not-need-a-css-preprocessor/&quot;&gt;you might not need Sass&lt;/a&gt; anymore.
And you can decompose your app into ES Modules and load as needed using import/export statements; no Webpack or Rollup required.&lt;/p&gt;
&lt;p&gt;Some build tools went away because your editor got smarter. I love TypeScript, but Visual Studio Code&#39;s inference works pretty damn well for JavaScript as well; providing &lt;a href=&quot;https://code.visualstudio.com/Docs/languages/javascript&quot;&gt;autocomplete and lots of other goodies&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some build tools can be removed because CDNs are making a comeback. &lt;a href=&quot;https://unpkg.com/&quot;&gt;unpkg&lt;/a&gt; delivers up all of NPM via a simple URL, so you don&#39;t have to pull in Webpack just to load a dependency.&lt;/p&gt;
&lt;p&gt;And new libraries are taking advantage of the toolchainless approach. Take &lt;a href=&quot;https://github.com/developit/htm&quot;&gt;htm&lt;/a&gt; - an in-browser project that uses ES6 tagged template literals to generate Hyperscript in the browser.
You can drop it into a Preact app and remove the JSX transpilation step:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;render&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({ &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;page&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; }) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; html&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;    &amp;#x3C;div class=&quot;app&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Header&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; name=&quot;ToDo&#39;s (&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;page&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;)&quot; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      &amp;#x3C;button onClick=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;addTodo&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&gt;Add Todo&amp;#x3C;/button&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Footer&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&gt;footer content here&amp;#x3C;//&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;    &amp;#x3C;/div&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;  `&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In fact, you can get a full Preact + htm project up and running with a single import statement:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  html&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  Component&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  render&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;https://unpkg.com/htm/preact/standalone.mjs&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drop that into a script tag in your &lt;code&gt;index.html&lt;/code&gt; file and you&#39;ve got a working Preact app that can run anywhere.&lt;/p&gt;
&lt;h2&gt;Pizza Compass&lt;/h2&gt;
&lt;p&gt;I recently rebuilt an old project, &lt;a href=&quot;https://pizza.steele.blue/&quot;&gt;Pizza Compass&lt;/a&gt;, a PWA that points you to the closest pizza. &lt;a href=&quot;https://web.archive.org/web/20190321204022/http://pizza-compass.com/&quot;&gt;The app I cloned&lt;/a&gt; claims to be the most important app ever made, which I can&#39;t deny.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mattdsteele/device-apis/blob/master/js/pizza.js&quot;&gt;The first version&lt;/a&gt; of Pizza Compass was built in 2013 using jQuery, before toolchains took over. When I decided to rebuild it using modern UI components, I went with a toolchainless approach.&lt;/p&gt;
&lt;p&gt;Built with the Preact + htm stack above, the codebase is modern and clean. And it felt &lt;em&gt;great&lt;/em&gt;. I can still build with components:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; PizzaCompass&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = ({ &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;loc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;heading&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;currentLoc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; b&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;bearing&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;currentLoc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;loc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; headingDelta&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;180&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; - (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;heading&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; - &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;b&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; distance&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Math&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;floor&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;distanceFrom&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;currentLoc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;loc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) * &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) / &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; html&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;    &amp;#x3C;div class=&quot;app&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      &amp;#x3C;header&gt;&amp;#x3C;h1&gt;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;loc&lt;/span&gt;&lt;span style=&quot;color:#000000FF;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&amp;#x3C;/h1&gt;&amp;#x3C;/header&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Pizza&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; rotation=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;headingDelta&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;      &amp;#x3C;footer&gt;&amp;#x3C;h1&gt;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;distance&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; km&amp;#x3C;/h1&gt;&amp;#x3C;/footer&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;    &amp;#x3C;/div&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;  `&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But I&#39;m not beholden to any toolchain. Builds are instantaneous because there&#39;s nothing to build. The project doesn&#39;t even contain a &lt;code&gt;package.json&lt;/code&gt; file. Hell, I can run the whole thing using Python&#39;s SimpleHTTPServer.&lt;/p&gt;
&lt;p&gt;The whole thing is 150-ish lines of Preact code, and when I&#39;m done I can push to GitHub and have Netlify deploy the folder directly. It takes less than a second.&lt;/p&gt;
&lt;h2&gt;How far does this scale?&lt;/h2&gt;
&lt;p&gt;I don&#39;t know. Eventually you&#39;ll probably reach a point where you want to add in tooling for asset optimization, or smarter bundling, or supporting IE11.&lt;/p&gt;
&lt;p&gt;But for a weekend project like this, it worked great. And if it ever gets big enough, I can check in the tooling Roach Motel then.&lt;/p&gt;
&lt;p&gt;I like this approach because it aligns with the &lt;a href=&quot;https://web.archive.org/web/20190216032625/http://www.w3.org:80/DesignIssues/Principles.html&quot;&gt;Principle of Least Power&lt;/a&gt;. Not every project needs to have 1023 dependencies in their &lt;code&gt;node_modules&lt;/code&gt;. And the more you default to using an opaque toolchain for everything, the more likely you&#39;ll get &lt;a href=&quot;https://daverupert.com/2019/01/angular-autoprefixer-ie11-and-css-grid-walk-into-a-bar/&quot;&gt;bit with opaque errors&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Toolchains are a chainsaw. And these days, you don&#39;t have to use a chainsaw for everything.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Web Components Aren&#39;t Weird Anymore</title>
    <link href="https://www.steele.blue/web-components-arent-weird-anymore/" />
    <updated>2018-08-08T00:00:00Z</updated>
    <id>https://www.steele.blue/web-components-arent-weird-anymore/</id>
    <content type="html">&lt;p&gt;I gave a talk at &lt;a href=&quot;https://barcampomaha.org/&quot;&gt;Barcamp Omaha&lt;/a&gt; on Web Components:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;CVTcbvhI0GU&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;In it, I tried to first answer the question: &lt;strong&gt;why aren&#39;t Web Components as popular as React?&lt;/strong&gt;
They both offer reusable components; building blocks which let you assemble sites easily and without rework.
But React (and Vue, Angular, etc) took off, and Web Components never did. Why aren&#39;t we Using The Platform (tm)?&lt;/p&gt;
&lt;p&gt;There are a number of factors, but my take: for nearly their entire existence, Web Components were &lt;strong&gt;super weird&lt;/strong&gt; to use.&lt;sup&gt;&lt;a href=&quot;https://www.steele.blue/web-components-arent-weird-anymore/#sub-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;A few years ago, to get a Web Component on the screen, you had to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load up four giant (and slow) polyfills, because only Chrome implemented it natively&lt;/li&gt;
&lt;li&gt;Use them via the Polymer framework, which looked different than your current codebase and didn&#39;t interop with it easily&lt;/li&gt;
&lt;li&gt;Pull them in via bespoke (HTML imports) and outdated (Bower) methods&lt;/li&gt;
&lt;li&gt;Bundle them using Vulcanizer and other tools that don&#39;t work with Webpack&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This sucks! And even if you were sold on the promise of Web Components, a person can only take so much weirdness before they give up and move onto a reliable toolchain.&lt;/p&gt;
&lt;p&gt;But over the last year, Web Components have slowly lost their weirdness. Because the controversial parts of the spec were jettisoned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They&#39;ve been implemented in all mobile and most desktop browsers&lt;/li&gt;
&lt;li&gt;You can use standard tools like NPM and Webpack to build and publish your components&lt;/li&gt;
&lt;li&gt;They &lt;a href=&quot;https://custom-elements-everywhere.com/&quot;&gt;interop seamlessly&lt;/a&gt; with most frameworks&lt;/li&gt;
&lt;li&gt;Your framework probably exports them (Angular, Vue, and Dojo do natively; and React can with a wrapper)&lt;/li&gt;
&lt;li&gt;There&#39;s lots of &amp;quot;Web Component Native&amp;quot; frameworks beyond Polymer, including my favorite &lt;a href=&quot;https://stenciljs.com/&quot;&gt;Stencil&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&#39;re finally at the point were it&#39;s &lt;strong&gt;easier&lt;/strong&gt; to add a Custom Element into any app than it is a React component. Now that&#39;s weird.&lt;/p&gt;
&lt;p id=&quot;sub-1&quot;&gt;1. Essentially, this entire talk is a rehash of the &quot;Weirdness Budget&quot; concept the Polymer folks &lt;a href=&quot;https://www.youtube.com/watch?v=7CUO7PyD5zA&amp;feature=youtu.be&amp;t=5m37s&quot;&gt;talk about here&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Rube Goldberg Machines</title>
    <link href="https://www.steele.blue/rube-goldberg-youtube/" />
    <updated>2018-04-16T00:00:00Z</updated>
    <id>https://www.steele.blue/rube-goldberg-youtube/</id>
    <content type="html">&lt;p&gt;Lately, 80% of my YouTube usage has been watching Rube Goldberg machines. Here&#39;s a few of my favorite:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;Hmb0Q0Q_7jo&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Blue Marble&lt;/strong&gt; - This entire channel is so well done.&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;auIlGqEyTm8&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Cake Server&lt;/strong&gt; - I love the mechanics as metaphor here. The food theme is infused throughout, and never ceases to delight.&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;IvUU8joBb1Q&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wintergatan Marble Machine&lt;/strong&gt; - Not a Rube Goldberg machine &lt;em&gt;per se&lt;/em&gt;, but I think it embodies the essence of the concept. As &lt;a href=&quot;https://daverupert.com/2018/04/prototyping-wintergatan-marble-machine-x/&quot;&gt;Dave Rupert&lt;/a&gt; notes, &amp;quot;It’s raw, creaky, clumsy, and beautiful.&amp;quot;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Welcome to RSS Club</title>
    <link href="https://www.steele.blue/rss-club/" />
    <updated>2018-04-15T00:00:00Z</updated>
    <id>https://www.steele.blue/rss-club/</id>
    <content type="html">&lt;p&gt;RSS is the most exciting technology of 2018.&lt;/p&gt;
&lt;p&gt;I don&#39;t it&#39;s that much hyperbole. A decade after its heydey, RSS embodies some of the best parts of the Web.&lt;/p&gt;
&lt;p&gt;Dave Rupert coined the idea, and is collecting sites with RSS-exclusive content &lt;a href=&quot;https://daverupert.com/rss-club/&quot;&gt;here&lt;/a&gt;. You should join. It&#39;s fun.&lt;/p&gt;
&lt;p&gt;I presume you&#39;ve already got an RSS setup you like. I recommend &lt;a href=&quot;https://newsblur.com/&quot;&gt;NewsBlur&lt;/a&gt;. I&#39;m happy to pay for it.&lt;/p&gt;
&lt;h2&gt;Why RSS Can Save The World&lt;/h2&gt;
&lt;p&gt;From Wired&#39;s &lt;a href=&quot;https://www.wired.com/story/rss-readers-feedly-inoreader-old-reader/&quot;&gt;It&#39;s Time For an RSS Revival&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;There are multiple approaches to connecting to news. Social felt pretty interesting at first, but when you mix social and algorithmic, you can easily get into these noise bubbles, or areas where you don&#39;t necessarily feel 100 percent in control of the algorithm,&amp;quot; says Edwin Khodabakchian, cofounder and CEO of popular RSS reader Feedly. &amp;quot;A tool like Feedly gives you a more transparent and controllable way to connect to the information you need.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I really like this sentiment. Most RSS feeds operate on a &amp;quot;pull&amp;quot; model, compared to social media&#39;s algorithmic &amp;quot;push&amp;quot; approach. And push approaches are designed to maximize engagement by constantly recommending new, &amp;quot;interesting content&amp;quot;.&lt;/p&gt;
&lt;p&gt;This is dangerous. From &lt;a href=&quot;https://idlewords.com/talks/build_a_better_monster.htm&quot;&gt;Maciej Cegłowski&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One problem is that any system trying to maximize engagement will try to push users towards the fringes. You can prove this to yourself by opening YouTube in an incognito browser (so that you start with a blank slate), and clicking recommended links on any video with political content. When I tried this experiment last night, within five clicks I went from a news item about demonstrators clashing in Berkeley to a conspiracy site claiming Trump was planning WWIII with North Korea, and another exposing FEMA’s plans for genocide.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;RSS has technical defects, but social media is defective by design.&lt;/p&gt;
&lt;p&gt;Lately, folks have been recommending &amp;quot;hacks&amp;quot; to make Twitter friendlier and less damaging.
Most of these, like &lt;a href=&quot;https://slate.com/technology/2018/03/the-demetricator-will-change-how-you-think-about-twitter-and-facebook.html&quot;&gt;&amp;quot;demetricating&amp;quot;&lt;/a&gt; or &lt;a href=&quot;https://www.theatlantic.com/magazine/archive/2018/04/the-case-against-retweets/554078/&quot;&gt;disabling retweets&lt;/a&gt;, are built into RSS. So why not just use the better tool?&lt;/p&gt;
&lt;h2&gt;Join The Club&lt;/h2&gt;
&lt;p&gt;As Dave mentioned, there are only a few rules to RSS Club:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1st rule of RSS Club is “Don’t Talk About RSS Club”.&lt;/li&gt;
&lt;li&gt;2nd rule of RSS Club is “Don’t Share on Social Media”.&lt;/li&gt;
&lt;li&gt;3rd rule of RSS Club is “Provide Value”.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I use Jekyll, with essentially the same setup as Dave. Put &lt;code&gt;rss_only: true&lt;/code&gt; in a post&#39;s front matter, and &lt;a href=&quot;https://github.com/mattdsteele/steele.blue/commit/5269fe5e0da28fa6686e8b7e304bea23cfa022de&quot;&gt;filter it from your main page&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>JavaScript Gardening with the Particle Photon</title>
    <link href="https://www.steele.blue/pi-gardening/" />
    <updated>2018-03-18T00:00:00Z</updated>
    <id>https://www.steele.blue/pi-gardening/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/1nZZCkgmsN-600.jpeg&quot; alt=&quot;Soil Sensors&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/1nZZCkgmsN-600.jpeg 600w, https://www.steele.blue/img/1nZZCkgmsN-1000.jpeg 1000w, https://www.steele.blue/img/1nZZCkgmsN-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;The last few years I&#39;ve been trying my hand at starting a vegetable garden from seed. Previous years have left a lot to be desired; normally I end forgetting to water my anemic-looking seedlings, feeling sad, and harvesting a single ear of corn at the end of the season. 🌽&lt;/p&gt;
&lt;p&gt;This year, I want to have some data when things go wrong. And I wanted to see what&#39;s new in &lt;a href=&quot;https://steele.blue/hardware-is-the-new-geocities/&quot;&gt;the new Geocities&lt;/a&gt;. So, time to build some Arduino-based sensor systems in JavaScript!&lt;/p&gt;
&lt;h2&gt;The Sensors&lt;/h2&gt;
&lt;p&gt;I wanted to keep track of the soil moisture as the seedlings were growing indoors. This can be done with a &lt;a href=&quot;http://a.co/ePXTcah&quot;&gt;Soil Moisture Sensor&lt;/a&gt;, which just sends a reading to an analog GPIO pin.&lt;/p&gt;
&lt;p&gt;I also picked up a few &lt;a href=&quot;http://a.co/ePXTcah&quot;&gt;DHT11 thermometer/humidity sensors&lt;/a&gt;. These are a little tricker to use directly, but the &lt;a href=&quot;https://github.com/adafruit/DHT-sensor-library&quot;&gt;Adafruit DHT Library&lt;/a&gt; make it easy to collect readings.&lt;/p&gt;
&lt;h2&gt;Raspberry Pi and Photon Farming&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/TXtKsYRZ9X-600.jpeg&quot; alt=&quot;Soil Sensors&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; srcset=&quot;https://www.steele.blue/img/TXtKsYRZ9X-600.jpeg 600w, https://www.steele.blue/img/TXtKsYRZ9X-1000.jpeg 1000w, https://www.steele.blue/img/TXtKsYRZ9X-3024.jpeg 3024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;The microcontroller world has advanced quite a bit since I last checked! In 2014 I used the &lt;a href=&quot;https://web.archive.org/web/20180319101150/http://johnny-five.io:80/&quot;&gt;Johnny-Five&lt;/a&gt; library to get sensor data into a Node app, but it required running Node on a &amp;quot;full&amp;quot; computer, then connecting to a USB-tethered Arduino. Tethering, &lt;em&gt;ugh&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Nowadays, you can run Node directly on a Raspberry Pi, and connect to the Pi&#39;s GPIO pins using a &lt;a href=&quot;https://web.archive.org/web/20180327095828/http://johnny-five.io:80/platform-support/&quot;&gt;J5 plug-in&lt;/a&gt;. Sinc the Pi 3 has integrated Wi-Fi, the only cord you need is power.&lt;/p&gt;
&lt;p&gt;Unfortunately, the Pi doesn&#39;t have analog GPIO inputs, which are needed for the soil moisture sensor. A few minicomputers such as the &lt;a href=&quot;https://web.archive.org/web/20180327095828/http://johnny-five.io:80/platform-support/&quot;&gt;Next Thing C.H.I.P.&lt;/a&gt; can do the job.&lt;/p&gt;
&lt;p&gt;But in the end I was smitten with the &lt;a href=&quot;https://docs.particle.io/guide/getting-started/intro/photon/&quot;&gt;Particle Photon&lt;/a&gt;, a Wi-Fi-enabled Arduino. It&#39;s super neat and crazy cheap. You connect it up to your wireless network, and emit events to the Particle cloud. They have a JavaScript SDK, which lets you receive these events in a Node app.&lt;/p&gt;
&lt;p&gt;I can&#39;t tell you how satisfying it is to remotely flash new code to an Arduino while in another room, and have your app get new data 30 seconds later. You can install Arduino libraries, and it&#39;s low-powered enough that a 10k mAH USB battery runs the whole contraption for 3 days or so. Plus, it fits in a breadboard.&lt;/p&gt;
&lt;h2&gt;At least I&#39;m sprouting a dataset&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/WLXs3K1r2l-600.png&quot; alt=&quot;Soil Sensors&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1363&quot; height=&quot;773&quot; srcset=&quot;https://www.steele.blue/img/WLXs3K1r2l-600.png 600w, https://www.steele.blue/img/WLXs3K1r2l-1000.png 1000w, https://www.steele.blue/img/WLXs3K1r2l-1363.png 1363w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;To handle the sensor data, I pulled in RxJS, which &lt;a href=&quot;https://steele.blue/reactive-programming-bike-sensors/&quot;&gt;I&#39;ve done before&lt;/a&gt; and find to be a super-solid approach. This let me take the Particle-provided EventEmitter, convert it to an reactive stream, and create a new stream out of the last 5 readings, averaged:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;getEventStream&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;./photon-stream&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; event$&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;getEventStream&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; stream&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;eventName&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; event$&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;filter&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; d&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; === &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;eventName&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;value:&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; parseFloat&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;bufferCount&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;vals&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; vals&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;reduce&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;prev&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;curr&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; prev&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; + &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;curr&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) / &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;vals&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;        value&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;        time:&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; Date&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;      };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; soil$&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;stream&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;soilMoisture&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This went straight into an InfluxDB instance, and mapped using Grafana. The whole thing is running in a few Docker containers on a DigitalOcean VPS, which was pretty nice to setup.&lt;/p&gt;
&lt;h2&gt;Seems like a lot of work&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/5q3g-wwtmr-600.jpeg&quot; alt=&quot;Soil Sensors&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; srcset=&quot;https://www.steele.blue/img/5q3g-wwtmr-600.jpeg 600w, https://www.steele.blue/img/5q3g-wwtmr-1000.jpeg 1000w, https://www.steele.blue/img/5q3g-wwtmr-4032.jpeg 4032w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;It was a fun side project, but since I can now check on the sensor levels at all hours, I&#39;m probably not reducing the number of mental cycles I&#39;m thinking about the seedlings. And in the time it took me to build and test the hardware, I probably could have tilled my yard or done something more directly beneficial to my garden.&lt;/p&gt;
&lt;p&gt;But I&#39;m thinking it can pay off in the long-run. Once the seedlings get moved outside to the garden, I can transplant it outside, and control a &lt;a href=&quot;https://www.sparkfun.com/products/10456&quot;&gt;solenoid&lt;/a&gt; valve to automate watering throughout the growing season.&lt;/p&gt;
&lt;p&gt;So yeah, JavaScript hardware is very possible, still cool, and has only gotten easier in the last 5 years. Go and build something cool with it!&lt;/p&gt;
&lt;p&gt;You can view the code &lt;a href=&quot;https://github.com/mattdsteele/pi-garden&quot;&gt;here&lt;/a&gt;, and see the charts &lt;a href=&quot;https://garden.steele.blue/d/Y5Y6Q2Rmk/gardening?orgId=1&quot;&gt;here&lt;/a&gt; (user/user).&lt;/p&gt;
&lt;p&gt;Inspiration comes from John Hobbs&#39;s &lt;a href=&quot;https://incubator.velvetcache.org/&quot;&gt;homemade chicken incubator&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The Neverending Side Project</title>
    <link href="https://www.steele.blue/neverending-side-project/" />
    <updated>2018-02-06T00:00:00Z</updated>
    <id>https://www.steele.blue/neverending-side-project/</id>
    <content type="html">&lt;p&gt;I love side projects. I keep track of what I&#39;d like to work on in a &lt;a href=&quot;https://github.com/mattdsteele/side-projects/issues/&quot;&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Most side projects don&#39;t need much oversight. That&#39;s kind of the idea: you spend a few hours building something fun, blog about it, and move onto something else.&lt;/p&gt;
&lt;p&gt;But I think there&#39;s space for another kind of project: one that spans years. The formula&#39;s simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build a site/app/whatever&lt;/li&gt;
&lt;li&gt;Let it sit on a shelf for a year&lt;/li&gt;
&lt;li&gt;Dust off the cobwebs and make some enhancement, switch frameworks, whatever&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Codebase of Theseus&lt;/h3&gt;
&lt;p&gt;One project I&#39;ve been building is a statistics app for &lt;a href=&quot;https://web.archive.org/web/20180206183833/http://www.superbowlsquares.org:80/how-to-play&quot;&gt;Super Bowl Squares&lt;/a&gt;, a mostly-random gambling endeavor. It&#39;s a dumb game, worthy of a dumb side project. You can &lt;a href=&quot;https://squares.steele.blue/&quot;&gt;view the latest version here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/neverending-side-project/biXStQLC-_-600.jpeg&quot; alt=&quot;squares&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;540&quot; srcset=&quot;https://www.steele.blue/neverending-side-project/biXStQLC-_-600.jpeg 600w, https://www.steele.blue/neverending-side-project/biXStQLC-_-960.jpeg 960w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I built my first version of the app back in 2011 as a fresh-faced front-end dev, with nothing but spaghetti jQuery.&lt;/p&gt;
&lt;p&gt;Over the years, I&#39;ve migrated and rebuilt the app numerous times:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Original jQuery version (2011)&lt;/li&gt;
&lt;li&gt;Added a build toolchain with Webpack and npm (2014)&lt;/li&gt;
&lt;li&gt;Rewrote using AngularJS 1.x (2015)&lt;/li&gt;
&lt;li&gt;Migrated to Angular 2 beta (2016)&lt;/li&gt;
&lt;li&gt;Rebuilt with TypeScript and the Angular CLI (2018)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Inheriting Your Legacy&lt;/h3&gt;
&lt;p&gt;A long-lived side project gives you the chance to confront your old habits and see how far you&#39;ve progressed. I started with jQuery because that&#39;s all I knew. I can still see parts of me in the old codebase, but I also see how my coding style has evolved.&lt;/p&gt;
&lt;p&gt;A long-lived side project also gives you breathing room to ask how much stock to put into trends. &lt;a href=&quot;https://github.com/mattdsteele/football-squares/blob/jquery/js/squares.js&quot;&gt;My original jQuery app&lt;/a&gt; still loads faster, has 60% less code, and (to my mind) is more understandable than &lt;a href=&quot;https://github.com/mattdsteele/football-squares/tree/master/src&quot;&gt;my latest version built atop Angular 5&lt;/a&gt;. Have I actually made things better? Have we as an industry?&lt;/p&gt;
&lt;p&gt;I didn&#39;t get to all the updates I wanted to make to the app this year; I wanted to add some Redux-style state management with ngrx. But it&#39;s okay, I know what I&#39;ll be working on next year.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Light Up Your Bike with Web Bluetooth and StencilJS</title>
    <link href="https://www.steele.blue/web-bluetooth-bike-leds/" />
    <updated>2017-12-17T00:00:00Z</updated>
    <id>https://www.steele.blue/web-bluetooth-bike-leds/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/1jgdzj-zfB-600.jpeg&quot; alt=&quot;Bike DeLights testing&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;768&quot; srcset=&quot;https://www.steele.blue/img/1jgdzj-zfB-600.jpeg 600w, https://www.steele.blue/img/1jgdzj-zfB-1000.jpeg 1000w, https://www.steele.blue/img/1jgdzj-zfB-1024.jpeg 1024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Every year the Omaha cycling community puts on the Bike De&#39;Lights ride: an opportunity to illuminates your bike and tour the city&#39;s Christmas lights. And every year I try to find a new way to drive a five meter RGB light strip.&lt;/p&gt;
&lt;p&gt;This year I&#39;ve been most excited about Web Bluetooth, and Web Components. Each technology has the opportunity to open previously closed ecosystems, and make them broadly accessible, and I wanted to use them to build a lighting system that anyone could control with their phone; no apps required.&lt;/p&gt;
&lt;h2&gt;Hardware&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B00DTOAWZ2/ref=oh_aui_search_detailpage?ie=UTF8&amp;amp;psc=1&quot;&gt;RGB 12V LED Light Strip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B00ZQVWU2O/ref=oh_aui_search_detailpage?ie=UTF8&amp;amp;psc=1&quot;&gt;Bluetooth LED controller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/SMAKN%C2%AE-8PCS-Battery-Holder-Black/dp/B01F6LHMR6/ref=sr_1_4?ie=UTF8&amp;amp;qid=1513571511&amp;amp;sr=8-4&amp;amp;keywords=12v+aa+battery+holder&quot;&gt;12V of AA batteries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The controller and batteries fit in the bike&#39;s saddlebag, and the light strip had sticky tape that stuck to the frame. It&#39;s a pretty simple setup.&lt;/p&gt;
&lt;h2&gt;Reverse Engineering the Bluetooth Controller&lt;/h2&gt;
&lt;p&gt;The Bluetooth LED controller was intended to be controlled via a native Android app, but it&#39;s garbage. But since it&#39;s a Bluetooth Low Energy device, &lt;a href=&quot;https://www.steele.blue/web-bluetooth/&quot;&gt;it&#39;s hackable&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;To reverse engineer the controller, I ended up using the strategy described &lt;a href=&quot;https://medium.com/@urish/reverse-engineering-a-bluetooth-lightbulb-56580fcb7546&quot;&gt;in Uri&#39;s post&lt;/a&gt;: recording interacting with the app and playing it back on my laptop using Wireshark.&lt;/p&gt;
&lt;p&gt;I found one Bluetooth service/characteristic you could send two types of commands via &lt;code&gt;Uint8Array&lt;/code&gt;s: an arbitrary RGB color, or a preset color scheme (mostly fades).&lt;/p&gt;
&lt;p&gt;The code to control the device is available &lt;a href=&quot;https://github.com/mattdsteele/web-bluetooth-bike-leds/blob/master/src/components/bluetooth-strip/bluetooth-strip.tsx#L16&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Building with Web Components and StencilJS&lt;/h2&gt;
&lt;p&gt;Web Bluetooth only works on Chrome devices, so building the controller app using Web Components was an easy decision and used no polyfills.&lt;/p&gt;
&lt;p&gt;I&#39;ve been pretty enamored with &lt;a href=&quot;https://stenciljs.com/&quot;&gt;StencilJS&lt;/a&gt; lately, and built the app with a handful of web components. Even the code to control the Bluetooth device is a &lt;code&gt;&amp;lt;bluetooth-strip&amp;gt;&lt;/code&gt; &lt;a href=&quot;https://github.com/mattdsteele/web-bluetooth-bike-leds/blob/master/src/components/bluetooth-strip/bluetooth-strip.tsx&quot;&gt;web component&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Stencil made it really simple to build a fast site, and fast. Since it&#39;s all TypeScript-based, I could create a mock component that implemented the same &lt;code&gt;interface&lt;/code&gt; as the &lt;code&gt;&amp;lt;bluetooth-strip&amp;gt;&lt;/code&gt; component, and test the app without having to continuously connect to a real device.&lt;/p&gt;
&lt;h2&gt;Broadcasting the URL via the Physical Web&lt;/h2&gt;
&lt;p&gt;Once I built the site, I deployed to a static host, but my goal was to let other riders control my lights using their phone. And it&#39;s a huge bummer to have them open Chrome and type in a giant URL.&lt;/p&gt;
&lt;p&gt;This is where the &lt;a href=&quot;https://google.github.io/physical-web/&quot;&gt;Physical Web&lt;/a&gt; comes in - a device can broadcast a URL, and nearby devices can be notified and interact with it.&lt;/p&gt;
&lt;p&gt;I installed Node on a &lt;a href=&quot;https://web.archive.org/web/20170930101728/https://docs.getchip.com/chip.html&quot;&gt;Next Thing CHIP&lt;/a&gt;, and used the &lt;a href=&quot;https://www.npmjs.com/package/eddystone-beacon&quot;&gt;eddystone-beacon&lt;/a&gt; module to broadcast my StencilJS URL.&lt;/p&gt;
&lt;p&gt;Fun fact: the USB battery was way larger than the computer it was powering!&lt;/p&gt;
&lt;p&gt;Code is available &lt;a href=&quot;https://github.com/mattdsteele/web-bluetooth-bike-leds/tree/master/broadcast&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/azdaYjHG36-600.jpeg&quot; alt=&quot;I&#39;m behind the cooler setup&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;684&quot; height=&quot;709&quot; srcset=&quot;https://www.steele.blue/img/azdaYjHG36-600.jpeg 600w, https://www.steele.blue/img/azdaYjHG36-684.jpeg 684w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Overall the project went really well! Unlike previous lighting systems, no soldering was required, and I didn&#39;t run into any technical glitches during the ride. These two are likely related.&lt;/p&gt;
&lt;p&gt;One issue I noticed was that folks don&#39;t understand how the Physical Web stuff works.
I told a few cyclists about my setup, but had to work with them to pull out their phones, and show where the link appeared in their Notifications dropdown.&lt;/p&gt;
&lt;p&gt;My primary goal with the project was to let others control the lights, and I succeeded with that.
My secondary goal was to have a random cyclist connect and control my lights. That didn&#39;t happen, and I think the issues there are intrinsic to the design of the Physical Web.&lt;/p&gt;
&lt;p&gt;You can play with the site &lt;a href=&quot;https://projects.steele.blue/bike-lights/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;See previous lighting projects &lt;a href=&quot;https://www.steele.blue/raspberry-pi-bike/&quot;&gt;here&lt;/a&gt; and &lt;a href=&quot;https://www.steele.blue/arduino-bike-lights/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Every Java Developer Should Learn TypeScript</title>
    <link href="https://www.steele.blue/typescript-for-javaers/" />
    <updated>2017-11-01T00:00:00Z</updated>
    <id>https://www.steele.blue/typescript-for-javaers/</id>
    <content type="html">&lt;p&gt;I gave a talk to the Omaha Java Users Group about &lt;a href=&quot;http://typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;7v9GxHR2Ffg&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;I think there&#39;s a lot of things TypeScript has to offer Java developers, namely:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static Typing&lt;/strong&gt; - This one&#39;s obvious; if you&#39;re used to a compiler providing thousands of tiny continuously-running unit tests, you&#39;re going to miss that on the front-end.&lt;/p&gt;
&lt;p&gt;But what&#39;s even cooler is TypeScript&#39;s features of &lt;em&gt;incremental&lt;/em&gt; and &lt;em&gt;inferred&lt;/em&gt; typing. You can achieve full type safety in this code:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; MathFns&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  static&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;val&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;      square&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; val&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;val&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; fns&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;MathFns&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;console&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fns&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;square&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By adding a single type token:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; MathFns&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  static&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;val&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;number&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;      square&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; val&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;val&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; fns&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;MathFns&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;console&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fns&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;square&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inferred typing means you add drastically fewer tokens than you would to make Java code typesafe; conservatively you&#39;d have to add least six type tokens in an equivalent Java program. Incremental typing means you can convert existing JavaScript code into TypeScript without it being all-or-nothing.&lt;/p&gt;
&lt;p&gt;I also appreciate TypeScript&#39;s &lt;em&gt;structural type system&lt;/em&gt;, which also provides strong type-safety without having to declare that a function implements a particular interface, or extends from a parent class, etc. &lt;a href=&quot;https://www.triplet.fi/blog/type-system-differences-in-typescript-structural-type-system-vs-c-java-nominal-type-system/&quot;&gt;This article&lt;/a&gt; goes deep into the differences.&lt;/p&gt;
&lt;p&gt;It&#39;s also easy to &lt;strong&gt;integrate into a Java workflow&lt;/strong&gt;, with &lt;a href=&quot;https://github.com/gnkoshelev/typescript-maven-plugin&quot;&gt;Maven&lt;/a&gt; and &lt;a href=&quot;https://github.com/sothmann/typescript-gradle-plugin&quot;&gt;Gradle&lt;/a&gt; plugins if you wanna use them, as well as tooling for Eclipse, IntelliJ, and other IDEs.&lt;/p&gt;
&lt;p&gt;I&#39;m still getting over the cognitive dissonance that a tool from Microsoft, created by the C# language designer, might be the best way to ease Java developers into the front-end. But now that I&#39;m used to static typing, I get &lt;em&gt;angry&lt;/em&gt; when I have to go back to vanilla JavaScript, and I have TypeScript to thank for that.&lt;/p&gt;
&lt;p&gt;Regardless of what &lt;a href=&quot;https://www.youtube.com/watch?v=2V1FtfBDsLU&quot;&gt;Rich Hickey&lt;/a&gt; says.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Marginal Revolutions</title>
    <link href="https://www.steele.blue/marginal-revolutions/" />
    <updated>2017-03-26T00:00:00Z</updated>
    <id>https://www.steele.blue/marginal-revolutions/</id>
    <content type="html">&lt;p&gt;Weekend illnesses are a great opportunity to work through an Instapaper queue.
I&#39;ll blame the head cold, but I see connective tissue across several articles.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;First, Scott Alexander makes a compelling case that &lt;a href=&quot;http://slatestarcodex.com/2017/03/24/guided-by-the-beauty-of-our-weapons/&quot;&gt;Trump supporters aren&#39;t immune to logic and truth&lt;/a&gt;.
One &lt;em&gt;presumes&lt;/em&gt; they are, because when&#39;s the last time you debated a Trumpist and changed their minds?&lt;/p&gt;
&lt;p&gt;But that&#39;s because these debates usually entail a handful of drive-by &amp;quot;CHECKMATE ATHEISTS&amp;quot; encounters.
Rather, one should focus on a longer-term, convince-one-person-over-five-years approach:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Am I saying that if you met with a conservative friend for an hour in a quiet cafe to talk over your disagreements, they’d come away convinced? No. I’ve changed my mind on various things during my life, and it was never a single moment that did it. It was more of a series of different things, each taking me a fraction of the way. As the old saying goes, “First they ignore you, then they laugh at you, then they fight you, then they fight you half-heartedly, then they’re neutral, then they then they grudgingly say you might have a point even though you’re annoying, then they say on balance you’re mostly right although you ignore some of the most important facets of the issue, then you win.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead, we throw up our hands and proclaim we&#39;re in a post-truth era, because the longer-term approach is &lt;em&gt;hard&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Improving the quality of debate, shifting people’s mindsets from transmission to collaborative truth-seeking, is a painful process. It has to be done one person at a time, it only works on people who are already almost ready for it, and you will pick up far fewer warm bodies per hour of work than with any of the other methods.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Next, in &lt;a href=&quot;http://www.newyorker.com/magazine/2017/01/23/the-heroism-of-incremental-care&quot;&gt;The Heroism of Incremental Care&lt;/a&gt; Atul Gawande argues that our health care valorizes specialties with obvious, immediate results (think: surgery, orthopedics) over longer, more incremental approaches (internal medicine, immunology).
And yet, the longer-term approach saves more lives:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the United Kingdom, where family physicians are paid to practice in deprived areas, a ten-per-cent increase in the primary-care supply was shown to improve people’s health so much that &lt;strong&gt;you could add ten years to everyone’s life and still not match the benefit.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The internists&#39; secret weapon: access to way more data:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“It’s the relationship,” they’d say. I began to understand only after I noticed that the doctors, the nurses, and the front-desk staff knew by name almost every patient who came through the door. Often, they had known the patient for years and would know him for years to come. &lt;strong&gt;In a single, isolated moment of care for, say, a man who came in with abdominal pain, Asaf looked like nothing special.&lt;/strong&gt; But once I took in the fact that patient and doctor really knew each other—that the man had visited three months earlier, for back pain, and six months before that, for a flu—I started to realize the significance of their familiarity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Finally, my hometown of Omaha is looking to solve its public transit problems by &lt;a href=&quot;http://www.omaha.com/news/metro/assessment-puts-cost-of-omaha-streetcar-at-million-suggests-ways/article_e18effd5-df6c-543a-b6df-5f09ebf74c17.html&quot;&gt;building a new $150M streetcar system&lt;/a&gt;.
It&#39;s a massive, big-bang solution to public transit woes, which is odd, given that Nebraska &lt;a href=&quot;https://twitter.com/peraun/status/839926603484389377&quot;&gt;spends less on public transit than any other state&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Similarly, the city has let its road infrastructure crumble so much over the years that &lt;a href=&quot;https://www.nytimes.com/2017/03/07/us/omahas-answer-to-costly-potholes-go-back-to-gravel-roads.html?_r=0&quot;&gt;it&#39;s converting some roads back to gravel&lt;/a&gt;, and declaring them &amp;quot;unmaintained&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/CBeq814Dve-600.jpeg&quot; alt=&quot;Potholes&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;810&quot; srcset=&quot;https://www.steele.blue/img/CBeq814Dve-600.jpeg 600w, https://www.steele.blue/img/CBeq814Dve-1000.jpeg 1000w, https://www.steele.blue/img/CBeq814Dve-1080.jpeg 1080w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;In each of these fields, the more successful approach is one that emphasizes slow, incremental changes.
No one doctor appointment resolves a chronic condition.
But the aggregate of routine visits captures more data, and solves more problems, than the alternative.&lt;/p&gt;
&lt;p&gt;You see it in software, too. Most orgs celebrate the dramatic rewrite of an aging codebase.
But to accomplish it they&#39;ll move resources away from longer-term projects, like making testing and continuous integration more consistent across the enterprise.&lt;/p&gt;
&lt;p&gt;Making those long-term investments is hard. And it&#39;s an uphill battle. When short-term solutions work they&#39;re more dramatic, are easier to measure, and are more fun. So of course folks prioritize them.&lt;/p&gt;
&lt;p&gt;But it&#39;s not just that the longer-term approach is more effective. It&#39;s that the alternative is often &lt;em&gt;actively harmful&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;If you don&#39;t think you can rationally convince a Trumpist they&#39;re wrong, then what&#39;s your alternative?
Sophistry, emotional appeals, or shutting down discourse a la the antifa folks?
But these approaches can be wielded by both sides; your side will only win by coincidence of having more effective storytellers.
And Trump&#39;s camp sure seems to have more expertise in this area.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;So try embracing the slow burn. This post might not convince you of its efficacy, but I&#39;ll leave it to you to find the one weird trick that does.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Web Bluetooth Is Your New Squeeze</title>
    <link href="https://www.steele.blue/web-bluetooth/" />
    <updated>2017-03-13T00:00:00Z</updated>
    <id>https://www.steele.blue/web-bluetooth/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/Lx17hSXYSK-600.jpeg&quot; alt=&quot;Web Bluetooth&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1598&quot; height=&quot;1024&quot; srcset=&quot;https://www.steele.blue/img/Lx17hSXYSK-600.jpeg 600w, https://www.steele.blue/img/Lx17hSXYSK-1000.jpeg 1000w, https://www.steele.blue/img/Lx17hSXYSK-1598.jpeg 1598w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;One of the side-effects to living that Internet-Of-Things lifestyle is that you end up with a lot of pseudo-smart devices, mostly collecting dust.
I wanted to see if Web Bluetooth could breathe new life into them.&lt;/p&gt;
&lt;p&gt;Turns out, hacking Bluetooth Low Energy toys is &lt;em&gt;way&lt;/em&gt; more fun than actually using them.
&lt;a href=&quot;https://www.steele.blue/web-bluetooth/nebraskajs.com&quot;&gt;I spoke at NebraskaJS&lt;/a&gt; about what I learned, and why I&#39;m excited about hooking Bluetooth up to the Web.&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;LPAKy9Rc4rA&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;h1&gt;Getting Started&lt;/h1&gt;
&lt;p&gt;Watch the talk, then check out these links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web&quot;&gt;Interact with Bluetooth Devices on the Web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@urish/start-building-with-web-bluetooth-and-progressive-web-apps-6534835959a6#.2pqsde5i8&quot;&gt;Bluetooth and Progressive Web Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.contextis.com/resources/blog/hacking-unicorns-web-bluetooth/&quot;&gt;Hacking Unicorns with Web Bluetooth&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Devices&lt;/h1&gt;
&lt;h2&gt;BBQ Thermometer - &lt;a href=&quot;https://github.com/mattdsteele/web-bluetooth/blob/master/src/bt/bbq.js&quot;&gt;Code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I picked this one up from &lt;a href=&quot;https://meh.com/deals/grill-right-bluetooth-bbq-thermometer-1&quot;&gt;Meh&lt;/a&gt;.
No specs available, but you can reverse engineer the temperature if you know some Big Endian notation.&lt;/p&gt;
&lt;p&gt;For part of the talk I advanced my slides by dropping the thermometer into glasses of hot &amp;amp; cold water.
I called it TDD - &lt;em&gt;Thermometer-Driven-Development&lt;/em&gt;.
Uncle Bob was right, TDD truly is the pinnacle of professional software development.&lt;/p&gt;
&lt;h2&gt;Elfy Smart Light - &lt;a href=&quot;https://github.com/mattdsteele/web-bluetooth/blob/master/src/bt/elfy.js&quot;&gt;Code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://web.archive.org/web/20170224201620/http://en.emie.com:80/emie-elfy-smart-light&quot;&gt;Elfy&lt;/a&gt; is a color-changing night light I picked up from a rando site in China. It looks the part.
You can hit it, and it&#39;ll alternate between a set of colors.&lt;/p&gt;
&lt;p&gt;For one demo, I hooked up an &lt;code&gt;&amp;lt;input type=&amp;quot;color&amp;quot;&amp;gt;&lt;/code&gt; to the Elfy.
I also wired it up to change color every time the BBQ Thermometer registered a new temperature.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://twitter.com/jcake09&quot;&gt;Jessica Codr&lt;/a&gt; and I also &lt;a href=&quot;https://github.com/JCake/toasty-timer&quot;&gt;built a timer&lt;/a&gt; for Toastmasters competitions.
This app is a great example of using Web Bluetooth for &lt;strong&gt;progressive enhancement&lt;/strong&gt;.
For browsers that don&#39;t support Web Bluetooth, the app shows the color with a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; on the screen.
If there&#39;s a Elfy nearby, you also get it on the device.&lt;/p&gt;
&lt;p&gt;No specs on the Bluetooth services, so I had to reverse engineer it using Wireshark, as described in the video.&lt;/p&gt;
&lt;h2&gt;Bicycle Speed/Cadence Sensor - &lt;a href=&quot;https://github.com/mattdsteele/web-bluetooth/blob/master/src/bt/cycling.js&quot;&gt;Code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I used a &lt;a href=&quot;http://www.wahoofitness.com/devices/wahoo-blue-sc-speed-and-cadence-sensor&quot;&gt;Wahoo Bluetooth/ANT+&lt;/a&gt; sensor. Luckily this used well-known Bluetooth GATT profiles, so hooking it up was a piece of cake; just required a little math.&lt;/p&gt;
&lt;p&gt;I took some cues from &lt;a href=&quot;https://github.com/chromakode/bicyclejs-talk&quot;&gt;Max Goodman&#39;s Bicycle.js&lt;/a&gt; talk.
But his Progressive Web App was far too useful, so I just built a Flappy Bird clone. You know how we do.&lt;/p&gt;
&lt;h2&gt;Sphero BB-8 - &lt;a href=&quot;https://github.com/mattdsteele/bb8-simon/blob/master/src/sphero-bb8.js&quot;&gt;Code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;http://www.sphero.com/starwars/bb8&quot;&gt;hottest Christmas toy of 2015&lt;/a&gt; meets the technical stylings of 1985.
&lt;a href=&quot;https://github.com/operasoftware/bb8&quot;&gt;Opera&#39;s devrel team&lt;/a&gt; originally wrote the code to control the toy.
I removed the code that made it spin off my desk, and instead just had it change colors.
It&#39;s a testament to the remix culture of the web.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Building Custom Elements That Work With AngularJS 1.x and Angular</title>
    <link href="https://www.steele.blue/custom-elements-angularjs/" />
    <updated>2016-09-08T00:00:00Z</updated>
    <id>https://www.steele.blue/custom-elements-angularjs/</id>
    <content type="html">&lt;p&gt;So Web Components! They&#39;ve recently gotten &lt;a href=&quot;https://medium.com/dev-channel/the-case-for-custom-elements-part-1-65d807b4b439#.inbchipy8&quot;&gt;a lot of love&lt;/a&gt;, and &lt;a href=&quot;https://medium.com/@tomdale/that-would-be-nice-but-in-my-experience-framework-agnostic-components-are-a-long-way-off-8c1cd5efcb7#.2sexknttl&quot;&gt;a lot of hate&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let&#39;s talk about Custom Elements in particular.
This is the technology that lets you drop &lt;code&gt;&amp;lt;relative-time&amp;gt;&lt;/code&gt; on a page, and it renders into something usable and pretty. No framework required.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://staltz.com/react-could-love-web-components.html&quot;&gt;Web Components proponents propose composing&lt;/a&gt; Web Components into existing frameworks, particularly as leaf nodes.
So you&#39;d write your Angular/React/whatever app that contained your business logic, and its templates would be made up of Custom Elements, standard HTML, and other Angular/React/whatever components.&lt;/p&gt;
&lt;p&gt;I haven&#39;t seen this interop story in action (or documented) in many places.
In particular, &lt;strong&gt;how do you build a Custom Element that&#39;ll work in both AngularJS (1.x) and Angular 2+ apps&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: I&#39;ll be using on the new &amp;quot;V1&amp;quot; version of the Custom Elements spec. &lt;a href=&quot;https://developers.google.com/web/fundamentals/primers/customelements/&quot;&gt;Here&#39;s a good intro article&lt;/a&gt;, and &lt;a href=&quot;https://github.com/WebReflection/document-register-element&quot;&gt;here&#39;s a polyfill&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Building The Custom Element (&lt;a href=&quot;https://github.com/mattdsteele/countdown-timer-element&quot;&gt;View on GitHub&lt;/a&gt;)&lt;/h2&gt;
&lt;p&gt;Let&#39;s make a &lt;code&gt;&amp;lt;countdown-timer&amp;gt;&lt;/code&gt; Custom Element.&lt;/p&gt;
&lt;p&gt;You give it the number of seconds you want the timer to run, and it&#39;ll spit out an event (with a message) when the countdown ends.&lt;/p&gt;
&lt;p&gt;We&#39;ll use a &amp;quot;one-way data flow&amp;quot; architecture - the element will accept its inputs via properties, and spit out DOM Events for its outputs.
This is the architecture Angular uses, (and &lt;a href=&quot;http://www.angular2patterns.com/&quot;&gt;the recommended approach for modern Angular 1 apps&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&#39;s the element in all its dumb glory:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; CountdownTimer&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; HTMLElement&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;  connectedCallback&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; template&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;			&amp;#x3C;button class=&quot;countdown-start&quot;&gt;Start the countdown&amp;#x3C;/button&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;			&amp;#x3C;span class=&quot;seconds-left&quot;&gt;&amp;#x3C;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;			`&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;innerHTML&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;template&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;    // Useful references&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;.countdown-start&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;secondsDisplay&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;.seconds-left&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;    // Initialize&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;click&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, () &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;handleClick&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;  handleClick&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;updateTimer&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;disabled&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;innerHTML&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;YOU DID IT&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;updateTimer&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt; counter&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;setInterval&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;      this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;--;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;      this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;updateTimer&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; === &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;        window&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;clearInterval&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;counter&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;        console&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;BOOM&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    }, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;  updateTimer&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;secondsDisplay&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;innerHTML&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;customElements&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;define&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;countdown-timer&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;CountdownTimer&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not much to it - initialize stuff in the &lt;code&gt;connectedCallback&lt;/code&gt; hook, and then add your functionality.&lt;/p&gt;
&lt;h2&gt;Angular (&lt;a href=&quot;https://web.archive.org/web/20201026050938/http://plnkr.co/edit/CBbCeyDkoWhwGuyy8pYI?p=preview&quot;&gt;View Demo&lt;/a&gt;)&lt;/h2&gt;
&lt;p&gt;Consuming this in Angular is pretty straightforward: you use the &lt;code&gt;[prop]=&amp;quot;value&amp;quot;&lt;/code&gt; syntax to bind to a property, and the &lt;code&gt;(event)=&amp;quot;handler()&amp;quot;&lt;/code&gt; syntax to bind to events.&lt;/p&gt;
&lt;p&gt;A component that uses it might have a template that looks like:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;label&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;    &gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;How long to count down? &lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; [(ngModel)]&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;secondsLeft&quot;&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;  &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;countdown-timer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  [seconds]&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;secondsLeft&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  (countdown-ended)&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;handleCountdownEnded($event.detail)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;countdown-timer&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing to watch out for, if you get an error like this:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Can&#39;t bind to &#39;seconds&#39; since it isn&#39;t a known property of &#39;countdown-timer&#39;.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. If &#39;countdown-timer&#39; is an Angular component and it has &#39;seconds&#39; input, then verify that it is part of this module.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. If &#39;countdown-timer&#39; is a Web Component then add &quot;CUSTOM_ELEMENTS_SCHEMA&quot; to the &#39;@NgModule.schema&#39; of this component to suppress this message.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&#39;ll need to add &lt;code&gt;CUSTOM_ELEMENTS_SCHEMA&lt;/code&gt; to your &lt;code&gt;@NgModule&lt;/code&gt; declaration, via:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;NgModule&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;CUSTOM_ELEMENTS_SCHEMA&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;@angular/core&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;NgModule&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;  // module boilerplate&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;	schemas:&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; [ &lt;/span&gt;&lt;span style=&quot;color:#0070C1;--shiki-dark:#4FC1FF&quot;&gt;CUSTOM_ELEMENTS_SCHEMA&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;AngularJS (1.x) (&lt;a href=&quot;https://plnkr.co/edit/3N3Kk7bSJVgPsSImYjrv?p=preview&quot;&gt;View Demo&lt;/a&gt;)&lt;/h2&gt;
&lt;p&gt;Out of the box, our Custom Element won&#39;t play nicely with AngularJS. There are two problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Its templating system binds to an element&#39;s &lt;em&gt;attributes&lt;/em&gt;, while our component updates the element&#39;s &lt;em&gt;properties&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;AngularJS isn&#39;t aware of the events your Custom Element fires, and doesn&#39;t hook into the normal &lt;code&gt;&amp;amp;&lt;/code&gt; callback bindings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So! We can fix this in a couple different ways:&lt;/p&gt;
&lt;h3&gt;Option 1: Change the Custom Element&lt;/h3&gt;
&lt;p&gt;On the input side, we can add some code to our Custom Element that looks for attribute changes.
There are lifecycle hooks in the spec for this:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;static&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; get&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; observedAttributes&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;seconds&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;attributeChangedCallback&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;oldVal&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;newVal&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; === &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;seconds&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;newVal&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then bind to the attribute in our template (note the use of &lt;code&gt;ng-attr&lt;/code&gt; to prevent the element from seeing the raw ``):&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;countdown-timer&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; ng-attr-seconds&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;{{$ctrl.secondsLeft}}&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;countdown-timer&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the output side, we can bind to the event manually in our controller, and wrap it in &lt;code&gt;$scope.$apply()&lt;/code&gt; to make sure a digest runs:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;$element&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;countdownEnded&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;  // If we don&#39;t do a digest, this doesn&#39;t get picked up immediately&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  $scope&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;$apply&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;message&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;detail&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;message&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But this has limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Binding to attributes means you&#39;re limited to passing String expressions to your Custom Element&lt;/li&gt;
&lt;li&gt;You lose encapsulation by binding to the Custom Element&#39;s events in your Angular controller. If you wanted to write idiomatic AngularJS, you&#39;d have to create a wrapper component that provided an &lt;code&gt;on-countdown-ended&lt;/code&gt; attribute, and run a digest manually. But now you&#39;re writing wrapper components for &lt;em&gt;every&lt;/em&gt; Custom Element you import, and we were trying to get away from that!&lt;/li&gt;
&lt;li&gt;Plus, you&#39;re modifying your supposedly framework-agnostic Custom Element to satisfy a particular framework&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So yeah, that sucks. Hope there&#39;s a better way!&lt;/p&gt;
&lt;h3&gt;Option 2: Use a Glue Directive&lt;/h3&gt;
&lt;p&gt;Should&#39;ve read ahead in my post, I guess.&lt;/p&gt;
&lt;p&gt;Rob Dodson wrote a &lt;a href=&quot;https://github.com/robdodson/angular-custom-elements&quot;&gt;set of directives to help Custom Elements interop with Angular 1&lt;/a&gt;. It&#39;s labelled for use with the Polymer project, but it&#39;ll work for any Custom Element, including ours.&lt;/p&gt;
&lt;p&gt;Since we&#39;re using a one-way data flow, we can add the &lt;code&gt;ce-one-way&lt;/code&gt; directive to our AngularJS template:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;countdown-timer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  ce-one-way&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  seconds&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;$ctrl.secondsLeft&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt;  on-countdown-ended&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;$ctrl.countdownEnded()&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;countdown-timer&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We did have to make one tweak to our Custom Element to make this work:
we renamed the Event to &lt;code&gt;countdown-ended&lt;/code&gt;, to match the naming pattern library expects.
Could be worse, I suppose.&lt;/p&gt;
&lt;h2&gt;Framework Migration, and Framework Independence&lt;/h2&gt;
&lt;p&gt;At work, we maintain an enterprise component library, like many large orgs do.
It&#39;s currently built as a set of Angular 1 directives.
As we begin to investigate Angular, we want to make the migration process smooth as butter.&lt;/p&gt;
&lt;p&gt;We &lt;em&gt;could&lt;/em&gt; rebuild the component library as a set of Angular components, and
AngularJS interoperability could come through the &lt;a href=&quot;https://angular.io/docs/ts/latest/guide/upgrade.html&quot;&gt;ngUpgrade module&lt;/a&gt;, probably.&lt;/p&gt;
&lt;p&gt;But this begs the question: what happens when we ditch Angular? Our components would again be dependent on a single framework, and they&#39;d have to be rewritten once again.&lt;/p&gt;
&lt;p&gt;Building your company&#39;s component library on Custom Elements solves multiple problems.&lt;/p&gt;
&lt;p&gt;Short-term, it helps you migrate from AngularJS 1.x to Angular, since both applications can use the same component library.&lt;/p&gt;
&lt;p&gt;Long-term, it helps isolate your company&#39;s component library from the Sturm und Drang of front-end frameworks.
So long as a framework interacts with the DOM (and they all do), they can use your components.&lt;/p&gt;
&lt;p&gt;So you can support that weird Ember team, the stodgy server-rendered JSP folks, and even the framework-less static page hipsters.&lt;/p&gt;
&lt;p&gt;So yeah, &lt;strong&gt;Custom Elements are awesome&lt;/strong&gt; and you should give them a looskie.
As Dion Almaer noted: &lt;a href=&quot;https://medium.com/ben-and-dion/web-components-building-web-tools-for-future-dion-1d0e731c96d2#.inn076mvm&quot;&gt;How would the component landscape look if we weren’t all rebuilding our own houses?&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to Run a Board Game Library At Your Conference After-Party</title>
    <link href="https://www.steele.blue/conference-board-games/" />
    <updated>2016-08-28T00:00:00Z</updated>
    <id>https://www.steele.blue/conference-board-games/</id>
    <content type="html">&lt;p&gt;We just wrapped up another sold-out &lt;a href=&quot;https://nejsconf.com/&quot;&gt;NEJS CONF&lt;/a&gt;. It was pretty great, and we learned a lot our second year around.&lt;/p&gt;
&lt;p&gt;One of the changes we made this year was to offer &lt;strong&gt;board games&lt;/strong&gt; at the conference after-party, rather than just setting attendees loose with dinner and drinks.&lt;/p&gt;
&lt;p&gt;I thought it worked pretty well, but I haven&#39;t seen many other tech conferences run with this concept.
&lt;strong&gt;Here&#39;s how we set it up&lt;/strong&gt;, what we learned along the way, and how you can incorporate games into your conference.&lt;/p&gt;
&lt;p&gt;tl;dr:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collect games from your friends&#39; libraries&lt;/li&gt;
&lt;li&gt;Take some simple precautions to make sure nothing gets lost&lt;/li&gt;
&lt;li&gt;If you&#39;ve got the budget, raffle off a couple games at the end of the night&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why board games?&lt;/h2&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; data-lang=&quot;en&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;YASSSSS! Collaborative game play at &lt;a href=&quot;https://twitter.com/hashtag/NEJSConf?src=hash&quot;&gt;#NEJSConf&lt;/a&gt; afterparty. &lt;a href=&quot;https://t.co/OG9MdMNdKD&quot;&gt;pic.twitter.com/OG9MdMNdKD&lt;/a&gt;&lt;/p&gt;&amp;mdash; Andrea Goulet (@andreagoulet) &lt;a href=&quot;https://twitter.com/andreagoulet/status/769307707815845888&quot;&gt;August 26, 2016&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;Because they&#39;re catalysts for a memorable after-party:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Games facilitate new connections among attendees&lt;/strong&gt; - We encouraged attendees to interact with out speakers and conference organizers, and games work marvelously for this.
A first-time conference attendee might worry they&#39;ll run out of things to talk to an experienced speaker about, but they&#39;ll probably be more comfortable playing a game of &lt;a href=&quot;https://web.archive.org/web/20160826085701/http://boardgamegeek.com/boardgame/178900/codenames&quot;&gt;Codenames&lt;/a&gt; with them, and it&#39;ll likely be more memorable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&#39;s an accessible alternative to a boozefest&lt;/strong&gt; - Lots of conferences market their evening events as an opportunity to blow off steam.
This often translates to: we have an open bar, so get shitfaced on our dime.
These can be fun, but they exclude entire sections of attendees if that&#39;s the only entertainment you offer.
Amanda Harlin reminded us of this &lt;a href=&quot;https://www.youtube.com/watch?v=pXn9WxzVbFM&quot;&gt;at our own conference last year&lt;/a&gt;, and we took it to heart.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&#39;s cheap and easy to organize&lt;/strong&gt; - Looking over the other budget line items for the after-party, this was dirt-cheap to setup.
Compared to renting the venue, and organizing food, bar, and decorations, providing board games is a rounding error on both your money and time.&lt;/p&gt;
&lt;h2&gt;Collecting the game library&lt;/h2&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; data-conversation=&quot;none&quot; data-lang=&quot;en&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;preeeeety sure &lt;a href=&quot;https://twitter.com/nejsconf&quot;&gt;@nejsconf&lt;/a&gt; is a board game conference &lt;a href=&quot;https://t.co/Vuq4NefEo7&quot;&gt;pic.twitter.com/Vuq4NefEo7&lt;/a&gt;&lt;/p&gt;&amp;mdash; Matt Steele (@mattdsteele) &lt;a href=&quot;https://twitter.com/mattdsteele/status/769264790715764736&quot;&gt;August 26, 2016&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;If you&#39;re a board gamer, you might have a personal library you can use to see the board game table.
If so, great! But if you&#39;re like me and mooch off your buddies&#39; collections, you&#39;ll probably need to ask others to borrow their games for the evening.&lt;/p&gt;
&lt;p&gt;Talk to your friends and ask if they&#39;ve got a set of board games they&#39;d be willing to lend out.
Given the rising popularity of board gaming, you probably have a few friends with the hobby.&lt;/p&gt;
&lt;p&gt;If any of them are planning to attend the conference, even better!
We offered to discount donors&#39; admission as a way of saying thanks.&lt;/p&gt;
&lt;h2&gt;Good kinds of games&lt;/h2&gt;
&lt;p&gt;You want to get games that meet a few criteria:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Short&lt;/strong&gt; - preferably under an hour to setup and play.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to learn&lt;/strong&gt; - Folks don&#39;t want to pour through manuals, they want to be unboxing and playing in a few minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to play&lt;/strong&gt; - Hardcore strategy games like &lt;a href=&quot;https://boardgamegeek.com/boardgame/2651/power-grid&quot;&gt;Power Grid&lt;/a&gt; are tons of fun, but probably overkill for most attendees at this point in the day.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also grabbed a few train-based games, to match our conference&#39;s theme (the venue was an old railroad station).&lt;/p&gt;
&lt;p&gt;Here&#39;s some games that fit this criterion (and got played a lot):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20160902172940/http://boardgamegeek.com/boardgame/36218/dominion&quot;&gt;Dominion&lt;/a&gt; - A fun deck-builder that&#39;ll scratch the itch of the ex-Magic: The Gathering players that sneak into your conference.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20160826011444/http://boardgamegeek.com/boardgame/20100/wits-wagers&quot;&gt;Wits &amp;amp; Wagers&lt;/a&gt; - A trivia game where you can win even if you don&#39;t know any answers? Sold.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://boardgamegeek.com/boardgame/30549/pandemic&quot;&gt;Pandemic&lt;/a&gt; - Cooperative games are particularly great at generating tables of cheers and high-fives.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://boardgamegeek.com/boardgame/147151/concept&quot;&gt;Concept&lt;/a&gt; - A great word association game. Another nice feature: folks can join at any time and don&#39;t have to wait for a new table to start up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/mattdsteele/21d3bb8386e566ce6743be15e87ea1b0&quot;&gt;Here&#39;s the complete list of games we collected for NEJS CONF&lt;/a&gt;, if you&#39;re curious.&lt;/p&gt;
&lt;h2&gt;Keep them safe&lt;/h2&gt;
&lt;p&gt;Since you&#39;ll likely be consolidating a collection worth hundreds or thousands of dollars, folks will want to know that it&#39;s in good hands. &lt;a href=&quot;http://fox42kptm.com/news/local/hundreds-of-dollars-of-board-games-stolen-from-mans-car&quot;&gt;And bad things can happen&lt;/a&gt;, unfortunately. A few things you want to have a plan for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Make an inventory&lt;/strong&gt; - We used a Google Spreadsheet to keep track of people&#39;s games. I made the spreadsheet a few days before and shared it with the game donors, and then re-inventoried all the games during the conference.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Have a plan for duplicate games&lt;/strong&gt; - There were a few games the library had multiple copies of. Make a note of these, and mark them appropriately.
Ask the owners if there are any expansions, sleeved cards, or other identifiable markings in the box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Return games at end of the night&lt;/strong&gt; - Donors want their games back sooner rather than later, and you don&#39;t want to keep a boatload of games in your car, anyhow.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Games Ambassadors&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/qrT-F70xnX-600.jpeg&quot; alt=&quot;Geekway To The West&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;720&quot; srcset=&quot;https://www.steele.blue/img/qrT-F70xnX-600.jpeg 600w, https://www.steele.blue/img/qrT-F70xnX-960.jpeg 960w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.facebook.com/GeekwaytotheWest/photos/a.10151887467747320.1073741827.51610637319/10153302091137320/?type=3&amp;amp;theater&quot;&gt;Geekway To The West&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You&#39;ll want to recruit a &lt;strong&gt;Game Ambassador&lt;/strong&gt; or two - these are volunteers who facilitate play among non-gamers.
We recruited the folks who donated their personal library to be Ambassadors during the after-party.
You can also ask for volunteers at your friendly local gaming store.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the secret sauce to running a successful game table.&lt;/strong&gt;
Most attendees won&#39;t be gamers, and don&#39;t want to read through pages of rules when they could be socializing.
Having a Game Ambassador around to help run a table removes this risk, and helps introduce more games to more people.&lt;/p&gt;
&lt;p&gt;We probably doubled the number of tables playing board games as a result of our Ambassadors floating around, checking out games, and teaching them.&lt;/p&gt;
&lt;h2&gt;Giveaways&lt;/h2&gt;
&lt;p&gt;We worked with &lt;a href=&quot;http://spielbound.org/&quot;&gt;Spielbound&lt;/a&gt;, our friendly local board game cafe, to purchase a dozen board games, and we raffled them off at the end of the night.
This was the most expensive part of the event, and ran about $300.&lt;/p&gt;
&lt;p&gt;You&#39;ll want to announce the raffle rules ahead of time, and have answers to common questions. For example: Who&#39;s eligible for the raffle? Do you get to choose your game or is it first-come-first-serve? Do you have to be present for the raffle to win?&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; data-lang=&quot;en&quot;&gt;&lt;p lang=&quot;pl&quot; dir=&quot;ltr&quot;&gt;NEJS CONF board game raffle info: &lt;a href=&quot;https://t.co/8CwXSPkEdd&quot;&gt;pic.twitter.com/8CwXSPkEdd&lt;/a&gt;&lt;/p&gt;&amp;mdash; NEJS CONF (@nejsconf) &lt;a href=&quot;https://twitter.com/nejsconf/status/769310739865571328&quot;&gt;August 26, 2016&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;Be aware of gaps in the drawing. For example, we told folks if they attended the after-party, you were registered.
We reused a sign in sheet to get drink tickets from the bar, but quickly realized that &lt;strong&gt;not everyone was drinking, so our list was incomplete.&lt;/strong&gt;
So, 10 minutes before the raffle, we had to scramble and hand out raffle tickets from table to table. It wasn&#39;t pretty.&lt;/p&gt;
&lt;p&gt;For the raffle drawing/winners yourself, you can announce them via the PA, or in Twitter/Slack.&lt;/p&gt;
&lt;p&gt;(One option: We kept the games in shrinkwrap during the after-party, but you could also use them as part of your games library.
This is similar to the &lt;a href=&quot;http://geekwaytothewest.com/playandwin.html&quot;&gt;Play and Win&lt;/a&gt; feature popularized by other conferences like Geekway.
We ended up not doing this, as we had enough library games that it wasn&#39;t worth the extra hassle to set it up.&lt;/p&gt;
&lt;blockquote class=&quot;instagram-media&quot; data-instgrm-captioned=&quot;&quot; data-instgrm-permalink=&quot;https://www.instagram.com/p/BJluCU2DyII/?utm_source=ig_embed&amp;amp;utm_medium=loading&quot; data-instgrm-version=&quot;12&quot; style=&quot; background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:540px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);&quot;&gt;&lt;div style=&quot;padding:16px;&quot;&gt; &lt;a href=&quot;https://www.instagram.com/p/BJluCU2DyII/?utm_source=ig_embed&amp;amp;utm_medium=loading&quot; style=&quot; background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;&quot; target=&quot;_blank&quot;&gt; &lt;div style=&quot; display: flex; flex-direction: row; align-items: center;&quot;&gt; &lt;div style=&quot;background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot;display: flex; flex-direction: column; flex-grow: 1; justify-content: center;&quot;&gt; &lt;div style=&quot; background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot; background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;padding: 19% 0;&quot;&gt;&lt;/div&gt;&lt;div style=&quot;display:block; height:50px; margin:0 auto 12px; width:50px;&quot;&gt;&lt;svg width=&quot;50px&quot; height=&quot;50px&quot; viewBox=&quot;0 0 60 60&quot; version=&quot;1.1&quot; xmlns=&quot;https://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;https://www.w3.org/1999/xlink&quot;&gt;&lt;g stroke=&quot;none&quot; stroke-width=&quot;1&quot; fill=&quot;none&quot; fill-rule=&quot;evenodd&quot;&gt;&lt;g transform=&quot;translate(-511.000000, -20.000000)&quot; fill=&quot;#000000&quot;&gt;&lt;g&gt;&lt;path d=&quot;M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;div style=&quot;padding-top: 8px;&quot;&gt; &lt;div style=&quot; color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;&quot;&gt; View this post on Instagram&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;padding: 12.5% 0;&quot;&gt;&lt;/div&gt; &lt;div style=&quot;display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;&quot;&gt;&lt;div&gt; &lt;div style=&quot;background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);&quot;&gt;&lt;/div&gt; &lt;div style=&quot;background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot;background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;margin-left: 8px;&quot;&gt; &lt;div style=&quot; background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;&quot;&gt;&lt;/div&gt; &lt;div style=&quot; width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;margin-left: auto;&quot;&gt; &lt;div style=&quot; width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);&quot;&gt;&lt;/div&gt; &lt;div style=&quot; background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);&quot;&gt;&lt;/div&gt; &lt;div style=&quot; width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/a&gt; &lt;p style=&quot; margin:8px 0 0 0; padding:0 4px;&quot;&gt; &lt;a href=&quot;https://www.instagram.com/p/BJluCU2DyII/?utm_source=ig_embed&amp;amp;utm_medium=loading&quot; style=&quot; color:#000; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none; word-wrap:break-word;&quot; target=&quot;_blank&quot;&gt;Best conference ever? Breaking out the boardgames at #nejsconf with new pals! @NEJS #fridaynightnerdnight&lt;/a&gt;&lt;/p&gt; &lt;p style=&quot; color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;&quot;&gt;A post shared by &lt;a href=&quot;https://www.instagram.com/drlieber/?utm_source=ig_embed&amp;amp;utm_medium=loading&quot; style=&quot; color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px;&quot; target=&quot;_blank&quot;&gt; Dania Lieberthal&lt;/a&gt; (@drlieber) on &lt;time style=&quot; font-family:Arial,sans-serif; font-size:14px; line-height:17px;&quot; datetime=&quot;2016-08-26T22:52:58+00:00&quot;&gt;Aug 26, 2016 at 3:52pm PDT&lt;/time&gt;&lt;/p&gt;&lt;/div&gt;&lt;/blockquote&gt; &lt;script async=&quot;&quot; src=&quot;https://www.instagram.com/embed.js&quot; webc:keep=&quot;&quot;&gt;&lt;/script&gt;
&lt;h2&gt;Then, run the event&lt;/h2&gt;
&lt;p&gt;At this point, running the library is pretty straightforward. A few other bits to consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shelving&lt;/strong&gt; - Putting games on tables is lame and you&#39;ll run into a mess fast.
Get some actual shelves, and put your games on &#39;em.
&lt;a href=&quot;https://web.archive.org/web/20160724114340/http://www.lowes.com:80/pd/Blue-Hawk-72-in-H-x-36-in-W-x-18-in-D-5-Tier-Plastic-Freestanding-Shelving-Unit/50436504?&quot;&gt;I picked up these at Lowe&#39;s&lt;/a&gt; for $30.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check in/out&lt;/strong&gt; - If someone wanted a game, we just took their conference namebadge and put it in a box.
When they returned the game they got it back. Simple.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Other than that, the event pretty much runs itself.
You might have to wait a while for the last table to finish up their games before you can pack up, but otherwise it&#39;s easy as pie.&lt;/p&gt;
&lt;p&gt;Happy gaming!&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; data-lang=&quot;en&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;&lt;a href=&quot;https://twitter.com/mattdsteele&quot;&gt;@mattdsteele&lt;/a&gt; &lt;a href=&quot;https://twitter.com/nejsconf&quot;&gt;@nejsconf&lt;/a&gt; I feel like I&amp;#39;ve made a mistake by not planning to be there&lt;/p&gt;&amp;mdash; Nick (@programmerman) &lt;a href=&quot;https://twitter.com/programmerman/status/768867428709330944&quot;&gt;August 25, 2016&lt;/a&gt;&lt;/blockquote&gt;
&lt;script webc:keep=&quot;&quot; async=&quot;&quot; src=&quot;https://platform.twitter.com/widgets.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;
&lt;script webc:keep=&quot;&quot; async=&quot;&quot; defer=&quot;&quot; src=&quot;https://platform.instagram.com/en_US/embeds.js&quot;&gt;&lt;/script&gt;
</content>
  </entry>
  <entry>
    <title>The Languages Which Almost Became CSS (Recap)</title>
    <link href="https://www.steele.blue/languages-almost-css/" />
    <updated>2016-07-06T00:00:00Z</updated>
    <id>https://www.steele.blue/languages-almost-css/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://nebraskajs.com&quot;&gt;NebraskaJS&lt;/a&gt; recently held a lightning talk night, entitled &amp;quot;Present a Blog Post&amp;quot;. Cool gimmmick, bro.
And you didn&#39;t even need to write the post you were presenting on!&lt;/p&gt;
&lt;p&gt;Fortunately, I read a fascinating article by Zack Bloom on &lt;a href=&quot;https://eager.io/blog/the-languages-which-almost-were-css/&quot;&gt;The Languages Which Almost Became CSS&lt;/a&gt;: a rip-roaring tour through the process by which we got to the styling language we all know and love (for various definitions of &#39;love&#39;).&lt;/p&gt;
&lt;p&gt;I also included a couple lessons I think we can take from the whole experience. And if you listen closely, there&#39;s a &lt;a href=&quot;http://deremilitari.org/2013/01/strategies-of-war-in-westeros/&quot;&gt;Game of Thrones&lt;/a&gt; allusion thrown in near the end.&lt;/p&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;173573313&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
&lt;p&gt;Thanks to Zack for researching and posting such a cool article!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Upgrading to Angular 2 using ngUpgrade</title>
    <link href="https://www.steele.blue/upgrading-to-ng2-with-ngupgrade/" />
    <updated>2016-04-27T00:00:00Z</updated>
    <id>https://www.steele.blue/upgrading-to-ng2-with-ngupgrade/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/vktbwg0Wdl-600.jpeg&quot; alt=&quot;Me, Presenting&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;787&quot; height=&quot;441&quot; srcset=&quot;https://www.steele.blue/img/vktbwg0Wdl-600.jpeg 600w, https://www.steele.blue/img/vktbwg0Wdl-787.jpeg 787w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I spoke at &lt;a href=&quot;https://web.archive.org/web/20160501021547/http://www.ng-nebraska.com:80/&quot;&gt;ng-nebraska&lt;/a&gt; on how the ngUpgrade module can help bridge the gap between Angulars 1 and 2.&lt;/p&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;164378615&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;http://projects.steele.blue/projects/superbowl-squares/&quot;&gt;Demo App (upgraded to ng2)&lt;/a&gt;&lt;/h2&gt;
&lt;h2&gt;&lt;a href=&quot;https://github.com/mattdsteele/football-squares&quot;&gt;Code on GitHub&lt;/a&gt;&lt;/h2&gt;
&lt;h2&gt;Slides&lt;/h2&gt;
&lt;p&gt;&lt;iframe src=&quot;https://speakerdeck.com/player/3b01eb70e7154a33bbcefd10a6bf49d1&quot; allowfullscreen=&quot;&quot; scrolling=&quot;no&quot; allow=&quot;autoplay; encrypted-media&quot; width=&quot;600&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Reacting to Heart Rate and Bike Sensors With RxJS</title>
    <link href="https://www.steele.blue/reactive-programming-bike-sensors/" />
    <updated>2016-01-03T00:00:00Z</updated>
    <id>https://www.steele.blue/reactive-programming-bike-sensors/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/yq6w3yPSDt-600.png&quot; alt=&quot;Observables&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2591&quot; height=&quot;1009&quot; srcset=&quot;https://www.steele.blue/img/yq6w3yPSDt-600.png 600w, https://www.steele.blue/img/yq6w3yPSDt-1000.png 1000w, https://www.steele.blue/img/yq6w3yPSDt-2591.png 2591w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Recently I &lt;a href=&quot;https://www.steele.blue/raspberry-pi-bike/&quot;&gt;hooked up a Raspberry Pi to my bike&lt;/a&gt; and made an LED light strip synchronize with my heart rate and pedal speed.
I got some great feedback on the project, including &amp;quot;overly complex&amp;quot;, &amp;quot;Rube Goldberg wannabe&amp;quot;, and &amp;quot;you need another hobby&amp;quot;.&lt;/p&gt;
&lt;p&gt;One challenge with this project was how to effectively manage sensor data from &lt;strong&gt;multiple sources&lt;/strong&gt; (a heart rate monitor and a bicycle speed/cadence sensor) and have it control a single output (the LED strip).
Additionally, I needed to &lt;strong&gt;manipulate the data&lt;/strong&gt; before I could send it to the LED light strip.
I also wanted to be able to &lt;strong&gt;test my system with &amp;quot;fake&amp;quot; inputs&lt;/strong&gt;, so I wouldn&#39;t have to strap on my heart rate monitor every time I wanted to tweak the app.&lt;/p&gt;
&lt;p&gt;It turns out that Reactive Programming techniques are perfect for a use case like this.&lt;/p&gt;
&lt;h2&gt;Reactive Programming&lt;/h2&gt;
&lt;p&gt;If you&#39;re new to the concept of reactive programming, check out these guides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://rxmarbles.com/&quot;&gt;RxMarbles&lt;/a&gt; - you could just play around on this site and learn most of what you need&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20160304013439/https://egghead.io/series/introduction-to-reactive-programming&quot;&gt;Introduction to Reactive Programming&lt;/a&gt; course on egghead.io&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Creating the streams&lt;/h2&gt;
&lt;p&gt;Sensor input was grabbed from the &lt;a href=&quot;https://github.com/Loghorn/ant-plus&quot;&gt;ant-plus&lt;/a&gt; library.
It emits a &lt;a href=&quot;https://nodejs.org/api/events.html&quot;&gt;Node-style Event&lt;/a&gt; periodically (every 250ms for the heart rate monitor, and on every pedal stroke for the cadence sensor).&lt;/p&gt;
&lt;p&gt;RxJS makes it easy to convert these to a stream:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; Rx&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;require&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;rx&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//`sensor` emits &#39;cadenceData&#39; events periodically&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; rawCadence&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Rx&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Observable&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;fromEvent&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;sensor&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;cadenceData&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This produces an Observable stream of cadence events.
Each event in the stream gives me a full data object but I only need one element (the currently calculated cadence).
I also filter out some noisy events (initially you get some events without the calculated cadence set):&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; calculatedCadence&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;rawCadence&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;stream&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; stream&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;CalculatedCadence&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;filter&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cadence&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;isNaN&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;cadence&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;calculatedCadence&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rendering the stream&lt;/h2&gt;
&lt;p&gt;The LED lights were represented by a class that stored the light&#39;s current state:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; BikeLights&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;intensity&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//controlled by heart rate data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;color&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = { &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;r:&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;g:&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;b:&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; }; &lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//controlled by cadence data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;  setIntensity&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;intensity&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) { &lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//led light strip code }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;  setRgb&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;percent&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) { &lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//led light strip code }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And in the application:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;calculatedCadence&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; remap&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;25&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;subscribe&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; lights&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;setRgb&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here I&#39;m using an &lt;a href=&quot;https://web.archive.org/web/20160114022814/https://www.arduino.cc/en/Reference/Map&quot;&gt;Arduino-style &lt;code&gt;remap&lt;/code&gt;&lt;/a&gt; function to convert my cadence range (25rpm during easy pedaling, 100rpm during fast sprints) to a 0-1 range, which the lights class expects.
Then we pass that data to the LED lights.&lt;/p&gt;
&lt;h2&gt;Faking out the stream&lt;/h2&gt;
&lt;p&gt;Creating a fake stream for testing was a piece of cake. In a separate file:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//mock-cadence-stream.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;// Start emitting 30rpm, then switch to 90rpm after 5 seconds&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; mockedCadenceStream&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Rx&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Observable&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;interval&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;timeInterval&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; &gt;= &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; ? &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; : &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;90&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;mockedCadenceStream&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my production code, I simply commented out the stream I wanted to use:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; cadenceStream&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;require&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;./mock-cadence-stream&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//Uncomment for real ANT+ data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//let cadenceStream = require(&#39;./cadence-stream&#39;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, but it gets the job done.&lt;/p&gt;
&lt;h2&gt;A stream of hearts&lt;/h2&gt;
&lt;p&gt;Getting data from the heart rate monitor was similar but required a few additional RxJS tricks.
I used these Reactive functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://rxmarbles.com/#distinctUntilChanged&quot;&gt;&lt;code&gt;distinctUntilChanged&lt;/code&gt;&lt;/a&gt; to only emit when my HR changed&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rxmarbles.com/#debounce&quot;&gt;&lt;code&gt;debounce&lt;/code&gt;&lt;/a&gt; as 250ms was too rapid for my use&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://reactivex.io/documentation/operators/flatmap.html&quot;&gt;&lt;code&gt;flatMap&lt;/code&gt; and &lt;code&gt;flatMapLatest&lt;/code&gt;&lt;/a&gt; to convert the event data (beats per minute) into a stream of &amp;quot;heartbeat&amp;quot; events&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see the full implementation on &lt;a href=&quot;https://github.com/mattdsteele/raspberry-pi-bike-leds/blob/master/src/boot.js#L33-L35&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;I guess this was a weekend hack&lt;/h2&gt;
&lt;p&gt;This was my first experience using RxJS and Reactive Programming, and it shows.
I didn&#39;t write any automated tests to verify the behavior, but I think it would have been simple to do.
You can even mock out (and speed up) the passage of time using a &lt;a href=&quot;https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/schedulers/virtualtimescheduler.md&quot;&gt;Virtual Time Scheduler&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also coded up some pretty &lt;a href=&quot;https://github.com/mattdsteele/raspberry-pi-bike-leds/blob/master/src/boot.js#L70-L81&quot;&gt;terrible error handling code&lt;/a&gt; to switch the lights to &#39;idle&#39; mode after 5 seconds without sensor data.
After thinking about it for a bit, I&#39;m pretty sure I could have used a standard &lt;a href=&quot;https://reactivex.io/documentation/operators/debounce.html&quot;&gt;debounce operator&lt;/a&gt; rather than the recursive monstrosity I created.&lt;/p&gt;
&lt;p&gt;But even as a non-expert in reactive paradigms I really liked using RxJS!
If you need to manage asynchronous events at a higher level than callbacks or &lt;code&gt;setTimeout&lt;/code&gt; allow, give it a shot.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Driving an LED Light Strip with Heart Rate and Bike Sensors</title>
    <link href="https://www.steele.blue/raspberry-pi-bike/" />
    <updated>2015-12-12T00:00:00Z</updated>
    <id>https://www.steele.blue/raspberry-pi-bike/</id>
    <content type="html">&lt;video controls=&quot;&quot; muted=&quot;true&quot;&gt;
  &lt;source src=&quot;https://www.steele.blue/videos/bdl-hrsensor.mp4&quot; type=&quot;video/mp4&quot;&gt;
&lt;/video&gt;
&lt;p&gt;I recently partook in &lt;a href=&quot;https://www.facebook.com/events/1093828353982844/&quot;&gt;Bike De&#39;Lights&lt;/a&gt;, Omaha&#39;s two-wheeled tour of the Christmas lights on display in Midtown.
Cyclists are encouraged to decorate their bikes with battery-powered lights, jingle bells, and get into the festive spirit.&lt;/p&gt;
&lt;p&gt;I treat this as an opportunity to build an overly-complex hardware project.
&lt;a href=&quot;https://www.steele.blue/arduino-bike-lights/&quot;&gt;Last year I wired up an Arduino&lt;/a&gt; to generate remote-control signals to drive an LED light strip.
It was fun, but there were some opportunities to make it better (or just further complect the system), which is what I did!&lt;/p&gt;
&lt;p&gt;My goal for the project: hook up an LED light strip to my bike frame, and have it respond to my heart rate and pedaling cadence, using the ANT+ sensors on my bike.
Specifically, I wanted the lights to &lt;strong&gt;change color from green to red as I pedaled faster&lt;/strong&gt;, and have the &lt;strong&gt;lights blink in sync with my heart rate&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I used a Raspberry Pi and an app written in JavaScript to do this. The code is on &lt;a href=&quot;https://github.com/mattdsteele/raspberry-pi-bike-leds&quot;&gt;GitHub&lt;/a&gt;. It was rickety, but it worked! Here&#39;s how I put it together.&lt;/p&gt;
&lt;h2&gt;Hardware&lt;/h2&gt;
&lt;p&gt;The core of the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.raspberrypi.org/blog/raspberry-pi-zero/&quot;&gt;Raspberry Pi Zero&lt;/a&gt; and a 4GB micro SD card&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20201119215030/https://www.amazon.com/gp/product/B00DTOAWZ2&quot;&gt;5M Waterproof RGB LED Light Strip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20160313113230/http://www.amazon.com:80/Energizer-Portable-Smartphone-Charger-smartphones/dp/B0092MD8P6&quot;&gt;USB portable charger&lt;/a&gt; and micro-USB cable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To power the LED light strip:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20200920214123/https://www.radioshack.com/products/tip120-transistor?variant=5717612869&quot;&gt;TIP120 Transistors (3x)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20200920200324/https://www.radioshack.com/products/radioshack-8-aa-battery-holder?variant=5717214213&quot;&gt;8 AA battery holder&lt;/a&gt; and &lt;a href=&quot;https://web.archive.org/web/20200920214249/https://www.radioshack.com/products/radioshack-heavy-duty-9v-snap-connectors?variant=5717208197&quot;&gt;9V snap connector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A handful of jumper wires and a prototyping board&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To capture sensor input from the bike:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B004YJSD20&quot;&gt;ANT+ USB receiver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/Garmin-Speed-Cadence-Bike-Sensor/dp/B000BFNOT8&quot;&gt;Speed/Cadence Bike Sensor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://buy.garmin.com/en-US/US/shop-by-accessories/fitness-sensors/hrm-run-/prod133715.html&quot;&gt;Heart Rate Monitor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B015XA3W0G&quot;&gt;Micro-USB to USB adapter&lt;/a&gt; (the Pi Zero has micro-USB inputs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I had most of these items on hand from previous projects, but had to buy the LED Light Strip and transistors.
In total this project uses less than $20 of consumable parts.&lt;/p&gt;
&lt;p&gt;Everything fit in my bike&#39;s saddle bag; this worked really well due to the super-tiny Pi Zero.
I placed the Pi and the circuit board in a makeshift enclosure I made from a to-go container, and held it down using electrical tape.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/GVcFqD9k1I-600.jpeg&quot; alt=&quot;Container&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1328&quot; srcset=&quot;https://www.steele.blue/img/GVcFqD9k1I-600.jpeg 600w, https://www.steele.blue/img/GVcFqD9k1I-1000.jpeg 1000w, https://www.steele.blue/img/GVcFqD9k1I-2000.jpeg 2000w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Schematic&lt;/h3&gt;
&lt;p&gt;The circuit to power the LEDs is pretty straightforward:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/87Qv9CLSfU-524.svg&quot; alt=&quot;Schematic&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;524&quot; height=&quot;422&quot;&gt;&lt;/p&gt;
&lt;p&gt;The Pi sends PWM signals to the red/green/blue input wires, and the 12V of batteries goes to the LED light strip.&lt;/p&gt;
&lt;p&gt;Also note: the dimensions are off in the diagram - the circuitboard was actually bigger than the Raspberry Pi Zero!&lt;/p&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;p&gt;The Raspberry Pi was running &lt;a href=&quot;https://www.raspberrypi.org/downloads/raspbian/&quot;&gt;Raspbian&lt;/a&gt; and a program I wrote in Node.js.&lt;/p&gt;
&lt;p&gt;I used Thomas Sarlandie&#39;s &lt;a href=&quot;https://github.com/sarfata/pi-blaster&quot;&gt;pi-blaster&lt;/a&gt; library to send PWM signals to the LED strip.
The library creates a &lt;code&gt;/dev/pi-blaster&lt;/code&gt; endpoint on the filesystem, which you write to and it send the signals to the GPIO pins.
There&#39;s even a &lt;a href=&quot;https://github.com/sarfata/pi-blaster.js&quot;&gt;Node.js wrapper&lt;/a&gt; which made adding it to my app super simple.&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;M45KM8725Os&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;To capture ANT+ data, I used Alessandro Vergani&#39;s &lt;a href=&quot;https://github.com/Loghorn/ant-plus&quot;&gt;ant-plus&lt;/a&gt; library, which emits Node-style events when new sensor data comes in.
This worked well, but only the heart rate monitor was supported out of the box.
So the majority of the coding I did for the project was implementing the ANT+ speed/cadence sensor into the library.
The &lt;a href=&quot;https://github.com/Loghorn/ant-plus/pull/4&quot;&gt;pull request&lt;/a&gt; shows the fun involved there.&lt;/p&gt;
&lt;p&gt;The actual code to integrate sensor input and have it drive the LEDs was another fun learning experience.
Since the heart rate and cadence sensors produced a stream of events, I pulled in &lt;a href=&quot;https://github.com/Reactive-Extensions/RxJS&quot;&gt;RxJS&lt;/a&gt; to treat them as Observables, and used reactive programming techniques to map the inputs into a stream that could &amp;quot;render&amp;quot; an LED strip.
That&#39;s probably another blog post in itself.&lt;/p&gt;
&lt;p&gt;Once I had a working protytpe, I configured a &lt;code&gt;systemd&lt;/code&gt; service to start the Node application when the Pi booted up.
This was especially useful as I had to reboot the Pi multiple times during the ride.&lt;/p&gt;
&lt;h2&gt;Running the Lights&lt;/h2&gt;
&lt;p&gt;Overall, I&#39;d say the results were a solid B/B+.
The lights were super bright, and lots of folks complemented them as I rode.
It was especially cool to see the lights start flashing like crazy as we climbed a big hill and my heart rate spiked.
But some parts could have worked better.&lt;/p&gt;
&lt;p&gt;The cadence sensor connection was buggy as hell.
About 50% of the time it wouldn&#39;t connect to the sensor on startup.
I programmed the lights to fade to blue after 5 seconds any pedaling events, for times when I was coasting downhill or stopped.
But when the sensor didn&#39;t connect, the lights stayed blue until I could stop, reboot the Pi, and try it again.
I&#39;m not sure why this happened.
My guess is it had to do with interference from other sensors, since the problem was especially pronounced at stops and locations with lots of other cyclists.&lt;/p&gt;
&lt;p&gt;I would also have fastened everything down better.
Omaha streets are often potholed and bumpy, and there were several times when the batteries fell out of the enclosure, shutting all the lights off.
I also just used jumper wires to connect the circuit board to the LED strip, and those fell out a number of times.
A little more soldering and electrical tape would have fixed these issues.
A shakedown ride or two would have also helped; most of my testing was done on an indoor bike trainer.&lt;/p&gt;
&lt;p&gt;Also, I would have have given myself more time to build it out.
I was working on the cadence sensor code until 4am the previous night, which probably contributed to the overall bugginess of the code.&lt;/p&gt;
&lt;p&gt;But with those caveats in mind, I was happy with the result!
In particular I liked that I could use a Raspberry PI and write JavaScript code to power hardware, using tools I was already familiar with.
This solidifies my belief that &lt;a href=&quot;https://www.steele.blue/hardware-is-the-new-geocities&quot;&gt;hardware is the new Geocities&lt;/a&gt;, and that it&#39;s possible to build pretty slick (if a little rickety) physical projects using JavaScript.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;p&gt;All the code I used is on &lt;a href=&quot;https://github.com/mattdsteele/raspberry-pi-bike-leds&quot;&gt;GitHub&lt;/a&gt;.
If you use this as the basis for your project, shoot me a note, I&#39;d love to hear how it goes!&lt;/p&gt;
&lt;p&gt;Other pages I found useful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20160128205310/http://mitchtech.net/raspberry-pi-pwm-rgb-led-strip/&quot;&gt;mitchtech.net/raspberry-pi-pwm-rgb-led-strip/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20151221072146/http://willmakesthings.com:80/color-my-desk/&quot;&gt;willmakesthings.com/color-my-desk/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/k1sul1/Raspberry-Pi-PHP-LED-controller#whatsneeded&quot;&gt;github.com/k1sul1/Raspberry-Pi-PHP-LED-controller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://elfboimakingstuff.tumblr.com/post/132956410578/raspberry-pi-pwm-rgb-led-strip&quot;&gt;elfboimakingstuff.tumblr.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Bringing ArnoldC to JavaScript</title>
    <link href="https://www.steele.blue/arnoldc-to-js/" />
    <updated>2015-05-12T00:00:00Z</updated>
    <id>https://www.steele.blue/arnoldc-to-js/</id>
    <content type="html">&lt;p&gt;At &lt;a href=&quot;http://nebraskajs.com/&quot;&gt;NebraskaJS&lt;/a&gt; I introduced my dumbest project yet: a compiler which converts &lt;a href=&quot;https://github.com/lhartikk/ArnoldC/wiki/ArnoldC&quot;&gt;ArnoldC&lt;/a&gt; programs into JavaScript, with full support for &lt;a href=&quot;https://www.steele.blue/source-maps&quot;&gt;Source Maps&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;Jv52vFLnn54&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;(Start at 17:46 for the ArnoldC action)&lt;/p&gt;
&lt;h1&gt;ArnoldC&lt;/h1&gt;
&lt;p&gt;ArnoldC is a game changer in the world of software development.
It&#39;s a procedural language where every keyword is a catchphrase from an Arnold Schwarzenegger movie.&lt;/p&gt;
&lt;p&gt;Here&#39;s &lt;code&gt;helloworld.arnoldc&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;IT&#39;S SHOWTIME&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  TALK TO THE HAND &quot;hello world&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;YOU HAVE BEEN TERMINATED&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, to write &lt;code&gt;a = (b + 5) * 2&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;GET TO THE CHOPPER a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  HERE IS MY INVITATION b&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  GET UP 5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  YOU&#39;RE FIRED 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ENOUGH TALK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Note the unique order of operations in ArnoldC: mathematical operations are applied as a left-to-right stack, with no operator hierarchy.)&lt;/p&gt;
&lt;p&gt;ArnoldC has been around for a few years, but it had one fatal flaw: the &lt;a href=&quot;https://github.com/lhartikk/ArnoldC/wiki/ArnoldC&quot;&gt;reference implementation&lt;/a&gt; compiles &lt;strong&gt;JVM bytecode&lt;/strong&gt;.
And while Write Once Run Anywhere might have been portable enough for 1998, my programs need to run on the &lt;a href=&quot;https://www.youtube.com/watch?v=PlmsweSNhTw&quot;&gt;Assembly Language of the Web&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;ArnoldC-to-JavaScript&lt;/h2&gt;
&lt;p&gt;To be honest, I didn&#39;t write most of the compiler.
I built on the excellent work done by &lt;a href=&quot;http://thomascrvsr.github.io/&quot;&gt;Thomas Crevoisier&lt;/a&gt;, who did most of the heavy lifting with his &lt;a href=&quot;https://github.com/ThomasCrvsr/arnoldc-to-js&quot;&gt;arnoldc-to-javascript&lt;/a&gt; project.
I simply added Source Maps and did some refactoring. But I learned lots along the way!&lt;/p&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://projects.steele.blue/source-maps/example3.html#/arnoldc&quot;&gt;Here&#39;s an AngularJS controller written in ArnoldC&lt;/a&gt;, that simply prints out FizzBuzz up to 100. Hit F12 and check it out in its source-mapped glory.
Note that there are a few esoteric things ArnoldC can&#39;t do, such as understand what &lt;code&gt;this.value&lt;/code&gt; means.
But thanks to a liberal use of &lt;code&gt;eval()&lt;/code&gt;, anything is possible in arnoldc.js!
Just don&#39;t tell Douglas Crockford.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mattdsteele/sourcemaps-presentation/blob/master/examples/src/arnoldc/fizzbuzz.arnoldc&quot;&gt;Source code is available here&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Build your own compiler&lt;/h1&gt;
&lt;p&gt;I never took a compilers course in school, but friends who graduated from better CS programs than me used descriptive phrases like &amp;quot;brain-melting&amp;quot; or &amp;quot;nightmare-ish&amp;quot;.
Luckily, &lt;strong&gt;you don&#39;t need to know how to write a compiler to write a compiler in JavaScript!&lt;/strong&gt;
If you understand a few key concepts, you can take advantage of libraries and tooling that make writing your own compiler a breeze.&lt;/p&gt;
&lt;p&gt;I registered for a &lt;a href=&quot;https://class.coursera.org/compilers/lecture&quot;&gt;Stanford compilers course&lt;/a&gt;, but only watched up through the &amp;quot;Parsing&amp;quot; section.
That&#39;s probably all you need to get started.&lt;/p&gt;
&lt;p&gt;Broadly, a compiler works in two phases: lexing and parsing.
The &lt;em&gt;lexer&lt;/em&gt; takes your source code and converts it into tokens, such as &lt;code&gt;START_IF_STATEMENT&lt;/code&gt;, or &lt;code&gt;ADDITION_OPERATOR&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You then run your code through a &lt;em&gt;parser&lt;/em&gt;, which is where the tokens get assembled into an Abstract Syntax Tree.
From here, you have a structured program and can write a translation from the input program into JavaScript.&lt;/p&gt;
&lt;h2&gt;Jison&lt;/h2&gt;
&lt;p&gt;Writing lexers and parsers is difficult, so I&#39;ve been told. But you don&#39;t have to do it!
Instead, you can define your language (keywords and structure) using regular expressions, and use &lt;a href=&quot;https://zaach.github.io/jison/docs/&quot;&gt;Jison&lt;/a&gt; to generate the lexer and parser automatically.&lt;/p&gt;
&lt;p&gt;Jison was written by Mozilla&#39;s Zach Carter, and forms the spine of the ArnoldC compiler. Jison is a port of the C program &lt;a href=&quot;https://www.gnu.org/software/bison/&quot;&gt;Bison&lt;/a&gt;, which performs a similar role using a less fun language. But many of its &lt;a href=&quot;https://web.archive.org/web/20150519045501/http://dinosaur.compilertools.net:80/bison/bison_4.html&quot;&gt;docs&lt;/a&gt; might be useful to peruse.&lt;/p&gt;
&lt;p&gt;Here&#39;s part of &lt;a href=&quot;https://github.com/mattdsteele/arnoldc.js/blob/master/lib/arnoldc.jison&quot;&gt;ArnoldC&#39;s lexer&lt;/a&gt;, written in Jison:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;%lex&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;%%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&#92;s+                             /* skip whitespaces */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;IT&#39;S SHOWTIME&quot;                 return &#39;BEGIN_MAIN&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;YOU HAVE BEEN TERMINATED&quot;      return &#39;END_MAIN&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&#92;-?[0-9]+                       return &#39;NUMBER&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;TALK TO THE HAND&quot;              return &#39;PRINT&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;@I LIED&quot;                       return &#39;FALSE&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;&amp;#x3C;EOF&gt;&gt;                         return &#39;EOF&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/lex&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keywords go on the left, and the name of the token is on the right.
The keywords are regular expressions, so they can be as complicated as you need.&lt;/p&gt;
&lt;p&gt;The parser then takes these keywords and creates a language out of them. Here&#39;s a higher-level ArnoldC concept of a &amp;quot;statement&amp;quot;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;statement&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  : PRINT integer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      { $$ = new yy.PrintExpression(@1.first_line, @1.first_column, $2); }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  | PRINT string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      { $$ = new yy.PrintExpression(@1.first_line, @1.first_column, $2); }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  | DECLARE_INT variable SET_INITIAL_VALUE integer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      { $$ = new yy.IntDeclarationExpression(@1.first_line, @1.first_column, $2, $4); }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;//etc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;integer&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, etc each have their own definitions, as well.
There&#39;s a bunch of Jison-specific code here, but the gist is that a &lt;code&gt;statement&lt;/code&gt; can look like any of these expressions.
And for each expression, you can write a JavaScript function that knows how to handle that code.
You can use Jison-specific variables like &lt;code&gt;$2&lt;/code&gt;, which simply represents the actual value of &lt;code&gt;integer&lt;/code&gt; in the source code (since it&#39;s the second token in the &lt;code&gt;PRINT integer&lt;/code&gt; expression.&lt;/p&gt;
&lt;p&gt;You can also see the use of &lt;code&gt;first_line&lt;/code&gt; and &lt;code&gt;first_column&lt;/code&gt;, which is the basis for the source maps the compiler generates.&lt;/p&gt;
&lt;p&gt;So from there, you can assemble a bunch of &lt;code&gt;statement&lt;/code&gt; expressions in a row, and create a &lt;code&gt;statements&lt;/code&gt; meta-expression:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;statements&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  : statements statement&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    { $$ = $1.concat($2); }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    { $$ = []; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a recursive definition that, at its root, creates an empty array. And for each statement in a row it finds, it just adds it to the array.
This recursive pattern tends to show up a lot Jison definitions.&lt;/p&gt;
&lt;p&gt;So now, you can define an entire program like this:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;program&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  : methods BEGIN_MAIN statements END_MAIN methods EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      { return $1.concat($5).concat(new yy.MainExpression($3, @2.first_line, @2.first_column, @4.first_line, @4.first_column)); }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So you&#39;ve got a &lt;code&gt;BEGIN_MAIN&lt;/code&gt; keyword, a set of statements, an &lt;code&gt;END_MAIN&lt;/code&gt; keyword, and on either side a set of &lt;code&gt;methods&lt;/code&gt; expressions (which are defined similarly to &lt;code&gt;statements&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;Parsing with Source Maps&lt;/h2&gt;
&lt;p&gt;The actual parsing and conversion to source maps is just standard JavaScript. You get your inputs (in this case, a set of tokens from an ArnoldC program), and return the JavaScript code that represents that section of code.&lt;/p&gt;
&lt;p&gt;You have a few choices on what to return from your parser functions. The original ArnoldC compiler returned JavaScript Strings that simply got concatenated into the final &lt;code&gt;.js&lt;/code&gt; file.
Or, you can use Mozilla&#39;s excellent &lt;a href=&quot;https://github.com/mozilla/source-map&quot;&gt;source-map&lt;/a&gt; library to return both the generated .js, as well as a Source Map you can define along the way.&lt;/p&gt;
&lt;p&gt;I followed &lt;a href=&quot;https://hacks.mozilla.org/2013/05/compiling-to-javascript-and-debugging-with-source-maps/&quot;&gt;Mozilla&#39;s guide&lt;/a&gt; essentially word-for-word, so I&#39;ll just link to that excellent article; you&#39;ll want to read it and re-read it.
It uses Jison, and also shows how to integrate Source Maps into a compiler.
This is where the bulk of the actual compiler work is done, and will be unique to each language.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mattdsteele/arnoldc.js/blob/master/lib/ast.js&quot;&gt;ArnoldC&#39;s parser functions&lt;/a&gt; each returns a &lt;code&gt;SourceNode&lt;/code&gt; object, which can contain other &lt;code&gt;SourceNode&lt;/code&gt; objects. This is done for each expression in the abstract syntax tree you&#39;ve built out.&lt;/p&gt;
&lt;p&gt;Once you&#39;ve parsed the entire AST, you can use the library&#39;s &lt;code&gt;toStringWithSourceMap&lt;/code&gt; function - it returns an object with &lt;code&gt;map&lt;/code&gt; and &lt;code&gt;code&lt;/code&gt; properties, which can then be saved off to the file system.&lt;/p&gt;
&lt;p&gt;Here&#39;s an example of the final code for transpiling ArnoldC&#39;s print expression &lt;code&gt;TALK TO THE HAND&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;PrintExpression&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;prototype&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;compile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;indent&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fileName&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;_sn&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;indent&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fileName&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;console.log( &#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;add&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;compile&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;indent&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;fileName&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;add&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39; );&lt;/span&gt;&lt;span style=&quot;color:#EE0000;--shiki-dark:#D7BA7D&quot;&gt;&#92;n&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All of this might sound complicated (and at first it was), but I found that there&#39;s a pattern to it all, and once you learn that pattern, everything kind of falls into place.&lt;/p&gt;
&lt;h2&gt;Other Resources&lt;/h2&gt;
&lt;p&gt;These videos from 2013 Front-Trends were super helpful for me to wrap my head around these concepts: &lt;a href=&quot;https://vimeo.com/68477808&quot;&gt;Zachary Carter&#39;s talk on Jison&lt;/a&gt;, and &lt;a href=&quot;https://vimeo.com/68680320&quot;&gt;Nick Fitzgerald&#39;s overview of Source Maps&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The original &lt;a href=&quot;https://github.com/lhartikk/ArnoldC/wiki/ArnoldC&quot;&gt;ArnoldC&lt;/a&gt; port has lots of great examples, including solutions to Project Euler problems.&lt;/p&gt;
&lt;p&gt;Next up? &lt;a href=&quot;http://torso.me/chicken&quot;&gt;Chicken.js&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Source Maps in 2015</title>
    <link href="https://www.steele.blue/source-maps/" />
    <updated>2015-05-11T00:00:00Z</updated>
    <id>https://www.steele.blue/source-maps/</id>
    <content type="html">&lt;p&gt;I gave a talk at &lt;a href=&quot;http://www.nebraskajs.com&quot;&gt;NebraskaJS&lt;/a&gt; about my journey with source maps:&lt;/p&gt;
&lt;h2&gt;Video&lt;/h2&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;Jv52vFLnn54&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;h2&gt;Demos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://projects.steele.blue/source-maps/example1.html&quot;&gt;Sass, Concatenation, Minification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://projects.steele.blue/source-maps/example2.html&quot;&gt;CoffeeScript, TypeScript, ECMAScript 6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://projects.steele.blue/source-maps/example3.html&quot;&gt;ArnoldC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The source is available &lt;a href=&quot;https://github.com/mattdsteele/sourcemaps-presentation/tree/master/examples&quot;&gt;on GitHub.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Slides&lt;/h2&gt;
&lt;p&gt;&lt;iframe src=&quot;https://speakerdeck.com/player/4f8df47ac14f48bfbf1de9ca31717f05&quot; allowfullscreen=&quot;&quot; scrolling=&quot;no&quot; allow=&quot;autoplay; encrypted-media&quot; width=&quot;600&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h1&gt;We&#39;ve complected things with tools&lt;/h1&gt;
&lt;p&gt;You probably don&#39;t deploy your JavaScript or CSS to production in the same format as you write it.
Or rather, you probably shouldn&#39;t.
Maybe you&#39;re concatenating files, or minifying the assets, or even compiling from another language like Sass or CoffeeScript.
Tools like Rails&#39; Asset Pipeline do this automatically.&lt;/p&gt;
&lt;p&gt;But! Debugging concatenated, minified code is a huge pain.
Prior to Source Maps, the best tool available was Chrome&#39;s &lt;a href=&quot;https://developer.chrome.com/devtools/docs/javascript-debugging#pretty-print&quot;&gt;Pretty Print&lt;/a&gt;.
But it&#39;s only a partial solution, as it doesn&#39;t restore variable names, show code transformations, etc. It&#39;s a slog working through the generated code and try to figure out what&#39;s going on.&lt;/p&gt;
&lt;h1&gt;The solution? More tools!&lt;/h1&gt;
&lt;p&gt;I&#39;d heard of Source Maps a few years ago.
At the time, it was being touted as the solution to the tooling problem we created.
Way back in 2012, Ryan Seddon &lt;a href=&quot;https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/&quot;&gt;published an article&lt;/a&gt; outlining all the cool stuff you could do with the technology.
But &lt;em&gt;it didn&#39;t seem to catch on&lt;/em&gt;; I hardly ever noticed a site with source maps enabled.&lt;/p&gt;
&lt;p&gt;So what gives? From my perspective, the problem wasn&#39;t with individual &lt;em&gt;tools&lt;/em&gt;, it was with the &lt;em&gt;toolchain&lt;/em&gt;.
If any step in your app&#39;s asset pipeline didn&#39;t properly support source maps, everything fell apart.
So while you may have source maps configured for Sass, if you then ran it against &lt;a href=&quot;https://github.com/postcss/autoprefixer&quot;&gt;Autoprefixer&lt;/a&gt;, you&#39;re at the mercy of Autoprefixer&#39;s support.&lt;/p&gt;
&lt;p&gt;And the more complicated your asset pipeline, the more likely one link was going to fail.
Supporting Source Maps right is &lt;em&gt;hard&lt;/em&gt;. Check out the release notes for &lt;a href=&quot;https://github.com/gruntjs/grunt-contrib-uglify#release-history&quot;&gt;Grunt&#39;s uglify plugin&lt;/a&gt;. Nearly every release in 2013 and 2014 was fixing or enhancing something with source maps.&lt;/p&gt;
&lt;h1&gt;An end to the teething years&lt;/h1&gt;
&lt;p&gt;But I think &lt;em&gt;we&#39;re at a turning point&lt;/em&gt;. For my Node-based toolchain, source maps are finally working across the board.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Concatenate? &lt;a href=&quot;https://github.com/gruntjs/grunt-contrib-concat/pull/59&quot;&gt;Added less than a year ago&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Uglify? &lt;a href=&quot;https://github.com/mishoo/UglifyJS2&quot;&gt;Took a complete rewrite to support&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Sass? Just got consistently working maps in node-sass &lt;a href=&quot;https://github.com/sass/libsass/releases/tag/3.2.0&quot;&gt;last week&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of this credit has got to go to Nick Fitzgerald and the excellent work Mozilla has done on the &lt;a href=&quot;https://www.npmjs.com/package/source-map&quot;&gt;source-map&lt;/a&gt; library, which provides the bulk of the code folks need to enable source maps without having to dive deep into the spec. It works great, has a simple and flexible API, and &lt;a href=&quot;https://www.npmjs.com/browse/depended/source-map&quot;&gt;over 200 npm modules&lt;/a&gt; depend on it.&lt;/p&gt;
&lt;h1&gt;It works for The Terminator&lt;/h1&gt;
&lt;p&gt;The source-map library works so well, I took it and implemented &lt;a href=&quot;https://github.com/mattdsteele/arnoldc.js&quot;&gt;a new language that compiles to JavaScript&lt;/a&gt;, with full source map support.
Granted, the language is &lt;a href=&quot;https://github.com/lhartikk/ArnoldC&quot;&gt;ArnoldC&lt;/a&gt; and quite possibly the dumbest project ever conceived, but it works, damnit!
And I never once had to open the &lt;a href=&quot;https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit&quot;&gt;Source Maps specification&lt;/a&gt; to learn its mysterious secrets.&lt;/p&gt;
&lt;p&gt;(ArnoldC should probably be detailed in its own post, this has gotten &lt;em&gt;way&lt;/em&gt; too long.)&lt;/p&gt;
&lt;p&gt;So if you&#39;ve been waiting to investigate Source Maps, now might be a great time to start. You have nothing to lose but your one-character named minified functions.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>We&#39;re hosting a JavaScript conference!</title>
    <link href="https://www.steele.blue/announcing-nejsconf/" />
    <updated>2015-02-15T00:00:00Z</updated>
    <id>https://www.steele.blue/announcing-nejsconf/</id>
    <content type="html">&lt;p&gt;On August 7, we&#39;ll be throwing &lt;a href=&quot;http://nejsconf.com/&quot;&gt;the first JavaScript conference ever held in Nebraska&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://nejsconf.com/&quot;&gt;&lt;img src=&quot;https://www.steele.blue/announcing-nejsconf/R2Dk-zO_9H-488.svg&quot; alt=&quot;Logo&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;488&quot; height=&quot;412&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&#39;m pretty sure this is the first technical conference held at &lt;a href=&quot;https://web.archive.org/web/20150216015606/http://www.omahazoo.com:80/&quot;&gt;a world-class zoo&lt;/a&gt;, and that&#39;s just the start. We&#39;re planning on a killer lineup of local and nationally-known speakers, as well as showcasing what makes Nebraska such a great state to live and write code.&lt;/p&gt;
&lt;p&gt;I&#39;m stoked to be on the organizing committee, alongside folks far more talented than I. If we&#39;ve assembled the Justice League of Nebraska JavaScripters, I&#39;d be Aquaman.&lt;/p&gt;
&lt;p&gt;We&#39;re still getting everything ready, but you can &lt;a href=&quot;http://nejsconf.com/&quot;&gt;sign up for the conference&#39;s mailing list&lt;/a&gt; right now.&lt;/p&gt;
&lt;p&gt;Hope to see you there!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Building a Handcrafted IR Blaster for Arduino-Powered Bike Lights</title>
    <link href="https://www.steele.blue/arduino-bike-lights/" />
    <updated>2014-12-14T00:00:00Z</updated>
    <id>https://www.steele.blue/arduino-bike-lights/</id>
    <content type="html">&lt;video controls=&quot;&quot; muted=&quot;true&quot;&gt;
  &lt;source src=&quot;https://www.steele.blue/videos/bdl-ir-blaster.mp4&quot; type=&quot;video/mp4&quot;&gt;
&lt;/video&gt;
&lt;p&gt;I recently participated in the &lt;a href=&quot;https://www.facebook.com/events/364954490330534/&quot;&gt;Bike De&#39;Lites&lt;/a&gt;, a casual bike tour around Omaha&#39;s Christmas-decorated neighborhoods.&lt;/p&gt;
&lt;p&gt;I like building &lt;a href=&quot;https://twitpic.com/28u48r&quot;&gt;silly electronics&lt;/a&gt; for rides like this, and was inspired by an LED light strip &lt;a href=&quot;https://www.zachleat.com/web/bike-lights/&quot;&gt;Zach Leatherman&lt;/a&gt; made recently.
Zach attached 5 meters of LED lights to his bike frame, but I wanted to take it a step further. Static lights are cool, but &lt;em&gt;I wanted to make them dance.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I wired the lights and attached them to my bike using the method Zach outlined, so it&#39;s worth &lt;a href=&quot;https://www.zachleat.com/web/bike-lights/&quot;&gt;checking out his setup&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;LED light strips are great: they&#39;re cheap (about $10), super bright, and can be battery-powered. (There are &amp;quot;smart&amp;quot; strips that have individually addressible lights, but they&#39;re quite pricy.)&lt;/p&gt;
&lt;p&gt;I picked up the &lt;a href=&quot;https://www.amazon.com/gp/product/B00JX6SUWM/&quot;&gt;E-Goal 3528 RGB LED strip&lt;/a&gt; off Amazon. It included a remote control to adjust the lights.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/un2RfTcEf6-600.jpeg&quot; alt=&quot;The Remote&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;680&quot; srcset=&quot;https://www.steele.blue/img/un2RfTcEf6-600.jpeg 600w, https://www.steele.blue/img/un2RfTcEf6-1000.jpeg 1000w, https://www.steele.blue/img/un2RfTcEf6-1024.jpeg 1024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;(The colors are a misnomer - the &amp;quot;purple&amp;quot; option simply turns the red and blue LEDs on at the same time.)&lt;/p&gt;
&lt;p&gt;The remote control was nice, but I didn&#39;t want to be holding it and pushing buttons while riding. So I reverse-engineered the remote and made an automated one using an infrared LED and an Arduino.&lt;/p&gt;
&lt;h2&gt;Seeing Infrared&lt;/h2&gt;
&lt;p&gt;Infrared remotes are finicky. They send pulses of IR light at specific frequencies (usually 38Khz, but not always) to a demodulator that understands only those codes. Each manufacturer has their own style, and many of the manufacturers attempt to make their signals more &amp;quot;robust&amp;quot; by sending the same command two or three times in succession. &lt;a href=&quot;http://www.sbprojects.com/knowledge/ir/index.php&quot;&gt;Here&#39;s lots more on IR theory&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It doesn&#39;t take much to build a homemade IR blaster - first you need to decode the signal from the original remote, then replay it using an infrared LED (available at your local Radio Shack, if it hasn&#39;t shut down yet).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/9CWRfx32xR-600.jpeg&quot; alt=&quot;LED Strip&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;680&quot; srcset=&quot;https://www.steele.blue/img/9CWRfx32xR-600.jpeg 600w, https://www.steele.blue/img/9CWRfx32xR-1000.jpeg 1000w, https://www.steele.blue/img/9CWRfx32xR-1024.jpeg 1024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Debugging the remote&lt;/h2&gt;
&lt;p&gt;I shouldn&#39;t have expected much from a $10 gadget, so it&#39;s no surprise that the light strip and remote did not come with any instructions or data sheet.&lt;/p&gt;
&lt;p&gt;After learning about infrared, I borrowed a &lt;a href=&quot;http://dangerousprototypes.com/docs/USB_Infrared_Toy&quot;&gt;USB Infrared Toy&lt;/a&gt;, which provided a convenient way to both send and recieve IR signals. Surprisingly, it had the best support on &lt;a href=&quot;https://winlirc.sourceforge.net/&quot;&gt;Windows&lt;/a&gt;, which I wasn&#39;t setup for.&lt;/p&gt;
&lt;p&gt;I used &lt;a href=&quot;https://www.lirc.org/&quot;&gt;LIRC&lt;/a&gt;, which looked like the best-supported Linux package to read and send IR commands. LIRC&#39;s remote database already had over 2000 devices available but the LED strips were not on there. Luckily the &lt;code&gt;irrecord&lt;/code&gt; command lets you create your own device configurations, using a CLI-based wizard.&lt;/p&gt;
&lt;p&gt;Aside: if you want to get the most features out of the IR Toy on Linux, don&#39;t use the LIRC version available out of the default repository - &lt;a href=&quot;https://launchpad.net/~forage/+archive/ubuntu/lirc&quot;&gt;get this package instead&lt;/a&gt;. Otherwise it&#39;s stuck in the inferior &amp;quot;IRman&amp;quot; format.&lt;/p&gt;
&lt;p&gt;Here&#39;s the configuration it generated:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;begin remote&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  name  remote.conf&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  bits           16&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  flags SPACE_ENC|CONST_LENGTH&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  eps            30&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  aeps          100&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  header       8899  4526&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  one           538  1688&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  zero          538   558&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ptrail        554&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  repeat       8901  2261&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  pre_data_bits   16&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  pre_data       0xF7&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  gap          108279&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  toggle_bit_mask 0x0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  begin codes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    on                       0xC03F&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    off                      0x40BF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dimmer                   0x807F&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    brighter                 0x00FF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    flash                    0xD02F&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ...etc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  end codes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;end remote&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The header section defined the protocol for the code, and then it lists out the 16-bit hex values for each of the buttons. Pretty slick stuff!&lt;/p&gt;
&lt;h2&gt;IRduino&lt;/h2&gt;
&lt;p&gt;Next was configuring the Arduino to send the IR codes. As a JavaScript guy, I had hoped to use Johnny-Five but had to discount it. First, this had to fit inside my saddle bag, and J5 requires the Arduino be tethered to a computer. Second, the Firmata protocol &lt;a href=&quot;https://github.com/rwaldron/johnny-five/issues/257&quot;&gt;doesn&#39;t support pulse-based units&lt;/a&gt;. So I went back to Arduino&#39;s native C library.&lt;/p&gt;
&lt;p&gt;The circuit was pretty straightforward, just a single infrared LED and a resistor:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/Uo3fe8ZtEj-600.jpeg&quot; alt=&quot;Schematic&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;765&quot; srcset=&quot;https://www.steele.blue/img/Uo3fe8ZtEj-600.jpeg 600w, https://www.steele.blue/img/Uo3fe8ZtEj-1000.jpeg 1000w, https://www.steele.blue/img/Uo3fe8ZtEj-1024.jpeg 1024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I looked into sending the pulses using &amp;quot;pure&amp;quot; Arduino code, but found the &lt;a href=&quot;https://github.com/shirriff/Arduino-IRremote&quot;&gt;IRRemote&lt;/a&gt; library which abstracted lots of it for me.&lt;/p&gt;
&lt;p&gt;IRRemote even included configurations for several popular code formats (Sony, NEC, RC5) but these didn&#39;t work for me. IRRemote&#39;s backup is the &lt;a href=&quot;https://github.com/shirriff/Arduino-IRremote/wiki/IRremote-library-API#irsendsendrawbuf-len-hertz&quot;&gt;&lt;code&gt;sendRaw&lt;/code&gt;&lt;/a&gt; command, which lets you send a raw bit stream of data.&lt;/p&gt;
&lt;p&gt;Another caveat: I had hoped to use an &lt;a href=&quot;https://flic.kr/p/q3kZfw&quot;&gt;Arduino Leonardo clone&lt;/a&gt;, but the IRremote library &lt;a href=&quot;https://github.com/shirriff/Arduino-IRremote/pull/42&quot;&gt;has a bug&lt;/a&gt; with that model, so I switched to the Uno, which worked fine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/xg4y-WYBVm-600.jpeg&quot; alt=&quot;Arduino&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;680&quot; srcset=&quot;https://www.steele.blue/img/xg4y-WYBVm-600.jpeg 600w, https://www.steele.blue/img/xg4y-WYBVm-1000.jpeg 1000w, https://www.steele.blue/img/xg4y-WYBVm-1024.jpeg 1024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Monday Night RAW&lt;/h2&gt;
&lt;p&gt;Unfortunately the configuration file LIRC generated didn&#39;t work with this (the hex values were too abstract), so I had to re-run the &lt;code&gt;irrecord&lt;/code&gt; command using the &lt;code&gt;-f&lt;/code&gt; flag to create raw codes. They looked like this:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;begin raw_codes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  name on&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    8874    4565     511     597     533     554&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    533     575     511     575     554     554&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    554     554     511     597     533     554&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    554    1685     511    1685     533    1685&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    575    1706     533     554     554    1706&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    511    1685     533    1706     554    1685&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    554    1685     511     575     554     533&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    575     533     533     575     511     554&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    554     533     533     575     554     554&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    554    1685     533    1685     554    1685&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    575    1685     533    1685     533    1685&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    554&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  name off&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    //etc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ugly, but it generated properly.&lt;/p&gt;
&lt;p&gt;Then it was just a matter of converting these values to a C array and sending it IRRemote:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;IRsend irsend;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;unsigned&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; on&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;67&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;] = { &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;8874&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;4565&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;511&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;597&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;575&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;511&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;575&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;511&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;597&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;511&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;575&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1706&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1706&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;511&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1706&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;511&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;575&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;575&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;575&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;511&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;575&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;575&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;533&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1685&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;554&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;irsend.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;sendRaw&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(on, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;67&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;38&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt; //command, array length, khz&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After a bit of testing, it all worked! I whipped up a small program to go through a program of Christmas-y (red &amp;amp; green) light series, and sequenced through them after a random delay (from one to twenty seconds).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/j16MSUOfox-600.jpeg&quot; alt=&quot;LED Strip&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;768&quot; srcset=&quot;https://www.steele.blue/img/j16MSUOfox-600.jpeg 600w, https://www.steele.blue/img/j16MSUOfox-1000.jpeg 1000w, https://www.steele.blue/img/j16MSUOfox-1024.jpeg 1024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Scope Creep&lt;/h2&gt;
&lt;p&gt;Like most projects, if I had more time I&#39;d have made it more complicated. I was hoping to get the data from my bike&#39;s &lt;a href=&quot;https://buy.garmin.com/en-US/US/shop-by-accessories/fitness-sensors/speed-cadence-bike-sensor/prod1266.html&quot;&gt;cadence sensor&lt;/a&gt; over the ANT+ protocol. That way, each time I pedaled the lights could change colors.&lt;/p&gt;
&lt;p&gt;Alternatively, I could attach a &lt;a href=&quot;https://learn.adafruit.com/tilt-sensor/overview&quot;&gt;tilt sensor&lt;/a&gt; to the bike and have the lights turn red while climbing uphill, and green while going downhill.&lt;/p&gt;
&lt;p&gt;I also was a little disappointed with the IR signal - a handful of times the sensor wasn&#39;t aligned with the LED and would miss the signal.
My friend &lt;a href=&quot;https://twitter.com/jricesterenator&quot;&gt;Jonathan Rice&lt;/a&gt; suggested ditching the IR reciever and sending raw commands directly through the connectors.&lt;/p&gt;
&lt;p&gt;All in all I&#39;m pretty happy with this setup. It&#39;s way nicer than the battery-powered Christmas lights you&#39;d pick up in a store. If you&#39;ve got a night ride coming up, give it a shot!&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;All the code and configuration is available &lt;a href=&quot;https://gist.github.com/mattdsteele/c6c0504bdab640035f02&quot;&gt;in this Gist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://delicious.com/mattsteele/lirc&quot;&gt;Bookmarks tagged #lirc&lt;/a&gt; on Delicious&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.flickr.com/photos/orphum/sets/72157647434786353/&quot;&gt;More photos/videos of the lights&lt;/a&gt; on Flickr&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Responsive Images using &lt;picture&gt; and srcset/sizes</title>
    <link href="https://www.steele.blue/responsive-images-picture-srcset/" />
    <updated>2014-07-01T00:00:00Z</updated>
    <id>https://www.steele.blue/responsive-images-picture-srcset/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/o6vic7kvGc-600.jpeg&quot; alt=&quot;Me, Presenting&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;590&quot; srcset=&quot;https://www.steele.blue/img/o6vic7kvGc-600.jpeg 600w, https://www.steele.blue/img/o6vic7kvGc-1000.jpeg 1000w, https://www.steele.blue/img/o6vic7kvGc-1024.jpeg 1024w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I spoke at &lt;a href=&quot;https://www.meetup.com/coffeeandcode/&quot;&gt;Omaha Coffee &amp;amp; Code&lt;/a&gt; about the work the &lt;a href=&quot;http://responsiveimages.org/&quot;&gt;Responsive Images Community Group&lt;/a&gt; has been up to lately:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Big heavy images are responsible for most of the bloat on a responsive website, but it doesn&#39;t have to be that way. Here&#39;s the latest HTML elements you can use to keep your images small, and your site lean &amp;amp; fast.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Slides&lt;/h2&gt;
&lt;p&gt;&lt;iframe src=&quot;https://speakerdeck.com/player/1cbf82f0e3b001311c6d56c19b776c40&quot; allowfullscreen=&quot;&quot; scrolling=&quot;no&quot; allow=&quot;autoplay; encrypted-media&quot; width=&quot;600&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h2&gt;Screencast&lt;/h2&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;99683665&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
&lt;p&gt;Thanks to Eric Portis&#39;s &lt;a href=&quot;http://ericportis.com/posts/2014/srcset-sizes/&quot;&gt;amazing post on srcset/sizes&lt;/a&gt;, which really helped me understand how these new elements worked, and from which I borrowed liberally.&lt;/p&gt;
&lt;p&gt;Photo credit: &lt;a href=&quot;https://twitter.com/RebeccaStavick/status/483982587845541889&quot;&gt;Rebecca Stavick&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Responsive Web Design Workshop at Interface</title>
    <link href="https://www.steele.blue/responsive-web-design-workshop/" />
    <updated>2014-05-13T00:00:00Z</updated>
    <id>https://www.steele.blue/responsive-web-design-workshop/</id>
    <content type="html">&lt;p&gt;It&#39;s been nearly four years since Ethan Marcotte published a little piece &lt;a href=&quot;http://alistapart.com/article/responsive-web-design/&quot;&gt;A List Apart&lt;/a&gt; on Responsive Web Design.
It&#39;s the post that launched a thousand redesigns.
Like many, I was first introduced to RWD after seeing the &lt;a href=&quot;https://www.bostonglobe.com/&quot;&gt;Boston Globe&#39;s redesign&lt;/a&gt;, and I began to implement it at work (which now sports a solid responsive homepage) and on this blog.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://signalvnoise.com/posts/3745-responsive-design-works-best-as-a-nipntuck&quot;&gt;But not everyone is sold on the concept&lt;/a&gt;.
Lots of folks are building desktop-only sites, or siloing a fraction of their content off into an m-dot subdomain.
Others have tried, but struggle with building a high-performing responsive site, or think their users &amp;quot;don&#39;t want to do this on mobile&amp;quot;.&lt;/p&gt;
&lt;p&gt;That&#39;s why I&#39;m so excited to be working with the crazy-talented &lt;a href=&quot;http://jerodsanto.net/&quot;&gt;Jerod Santo&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/shonna_dorsey&quot;&gt;Shonna Dorsey&lt;/a&gt; to lead a workshop on Responsive Web Design at the &lt;a href=&quot;https://interfaceschool.com/&quot;&gt;Interface Web School&lt;/a&gt;. It&#39;ll be a 4 day course this July that spans the gamut of building for the responsive Web, starting with the &lt;a href=&quot;http://alistapart.com/article/responsive-web-design/&quot;&gt;3 key elements&lt;/a&gt; and moving onto tooling, performance, webapps, IE support, and lots more.
And it&#39;ll run in the evenings, so you don&#39;t even have to convince your boss to take time off work for it.&lt;/p&gt;
&lt;p&gt;You can &lt;a href=&quot;https://interfaceschool.com/course/responsive-web-design/&quot;&gt;register on Interface&#39;s website&lt;/a&gt;. It&#39;ll be fun!&lt;/p&gt;
&lt;p&gt;[&lt;img src=&quot;https://www.steele.blue/img/gIk49w0H0t-600.jpeg&quot; alt=&quot;Responsive Web Design Course&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;613&quot; height=&quot;422&quot; srcset=&quot;https://www.steele.blue/img/gIk49w0H0t-600.jpeg 600w, https://www.steele.blue/img/gIk49w0H0t-613.jpeg 613w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;(PS: This post is using &lt;a href=&quot;https://scottjehl.github.io/picturefill/&quot;&gt;Picturefill&lt;/a&gt;, which is one approach to responsive images I&#39;ll talk about during the course.)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Hardware is the new Geocities</title>
    <link href="https://www.steele.blue/hardware-is-the-new-geocities/" />
    <updated>2014-01-16T00:00:00Z</updated>
    <id>https://www.steele.blue/hardware-is-the-new-geocities/</id>
    <content type="html">&lt;p&gt;I gave a talk at &lt;a href=&quot;http://nebraskajs.com/&quot;&gt;NebraskaJS&lt;/a&gt; on hardware hacking, Johnny-Five, and the importance of amateur goofery.&lt;/p&gt;
&lt;p&gt;It was the most risky talk I&#39;ve ever given.
I demoed an Arduino-powered Power Glove, modified my slides on the fly with a WebSocket-enabled potentiometer, and flew a quadcopter drone over the audience using a Wiimote.&lt;/p&gt;
&lt;h2&gt;Video&lt;/h2&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;83907789&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
&lt;p&gt;I also gave a 20-minute version of this at &lt;a href=&quot;http://techomaha.com/2013/11/matt-steele-hardware-barcamp/&quot;&gt;Barcamp Omaha&lt;/a&gt;, which you can watch if you enjoy seeing quadcopters crash and burn.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vine: h3zEv1Qzl0l&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Video and more courtesy of Zach Leatherman at &lt;a href=&quot;http://nebraskajs.com/2014/hardware-geocities/&quot;&gt;NebraskaJS&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Go(lang) for Broke</title>
    <link href="https://www.steele.blue/golang-for-broke/" />
    <updated>2013-12-05T00:00:00Z</updated>
    <id>https://www.steele.blue/golang-for-broke/</id>
    <content type="html">&lt;p&gt;A few weeks ago I took part in the third &lt;a href=&quot;https://twitter.com/hackomaha&quot;&gt;Hack Omaha&lt;/a&gt;, a civic hackathon put on by &lt;a href=&quot;https://twitter.com/mattwynn&quot;&gt;Matt Wynn&lt;/a&gt;, &lt;a href=&quot;https://web.archive.org/web/20140413035547/http://opennebraska.io:80/&quot;&gt;Open Nebraska&lt;/a&gt;, and others.&lt;/p&gt;
&lt;p&gt;We built a website to &lt;a href=&quot;http://schools.opennebraska.io/&quot;&gt;visualize Omaha Public School&#39;s enrollment data&lt;/a&gt;. It was a blast to build, and not just because it helped answer burning questions of teammate, parent, and all-around cool guy &lt;a href=&quot;http://www.ketv.com/news/local-news/parents-worry-about-overpacked-classrooms-kids-education/-/9674510/21457722/-/trv6dnz/-/index.html?absolute=true&amp;amp;utm_source=dlvr.it&amp;amp;utm_medium=twitter&amp;amp;utm_campaign=ketv&quot;&gt;Alex Gates&lt;/a&gt;.
Alex wanted to know why his daughter&#39;s kindergarden class had 39 children in it when classes began.&lt;/p&gt;
&lt;p&gt;Here&#39;s what I learned.&lt;/p&gt;
&lt;h2&gt;Going Nuts&lt;/h2&gt;
&lt;p&gt;This was all &lt;a href=&quot;https://twitter.com/jerodsanto/&quot;&gt;Jerod Santo&lt;/a&gt;&#39;s fault.&lt;/p&gt;
&lt;p&gt;At the first Hack Omaha, Jerod amazed me by building his project in a &lt;a href=&quot;http://blog.jerodsanto.net/2012/04/confessions-of-a-meteor-newb/&quot;&gt;days-old JavaScript framework&lt;/a&gt;.
It was a high-wire act, but he convinced me that &lt;a href=&quot;http://blog.jerodsanto.net/2012/04/confessions-of-a-meteor-newb/#comment-507865511&quot;&gt;I should follow suit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So when Hack Omaha rolled around this year, Jerod and I decided to team up and try a language neither of us knew. His suggestion: &lt;a href=&quot;http://golang.org/&quot;&gt;Go&lt;/a&gt; - Google&#39;s new(ish) language. Juan Vazquez joined in, and &lt;a href=&quot;https://twitter.com/jerodsanto/status/394909959948754944&quot;&gt;a team based on ignorance was born&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Go&#39;s &lt;a href=&quot;http://golang.org/doc/&quot;&gt;docs&lt;/a&gt; describe the language:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It&#39;s a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/u6IsE9clGA-600.jpeg&quot; alt=&quot;Leah and Juan get to work&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;600&quot; srcset=&quot;https://www.steele.blue/img/u6IsE9clGA-600.jpeg 600w, https://www.steele.blue/img/u6IsE9clGA-800.jpeg 800w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Going to School&lt;/h2&gt;
&lt;p&gt;I spent some time learning the language, and the &lt;a href=&quot;https://tour.golang.org/&quot;&gt;Go tour&lt;/a&gt; was my main source.
Perhaps it&#39;s the Ken Thompson-inspired origins, but it was nice working with a language that didn&#39;t condescend to you and treat you as a programming neophyte.
I often feel like this about other guide, such as _why&#39;s Poigniant Guide to Ruby.&lt;/p&gt;
&lt;h2&gt;A Vodka Martini, Chilled&lt;/h2&gt;
&lt;p&gt;At this point, we had an idea that an app would be Go serving up an API on the backend, and a web frontend running AngularJS.
The Changelog also had a really helpful post outlining Go&#39;s &lt;a href=&quot;http://thechangelog.com/on-go-web-application-ecosystem&quot;&gt;web application ecosystem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Go has an impressive standard library and can serve HTTP easily out of the box, but we decided to apply a framework to provide a little structure.
We went with &lt;a href=&quot;http://martini.codegangsta.io/&quot;&gt;Martini&lt;/a&gt;; a Sinatra-esque library that is less than a month old, in a proud Hack Omaha tradition.&lt;/p&gt;
&lt;p&gt;Our services ended up with this structure.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;m&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;Get&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;/schools&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; http&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;ResponseWriter&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; render&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;allSchools&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should look similar to Sinatra and its bretheren.
Martini gives you the objects you need (like a ResponseWriter) via parameterized Dependency Injection, which is super convenient.&lt;/p&gt;
&lt;p&gt;JSON encoding, HTTP headers, enabling CORS, all worked out of the box.&lt;/p&gt;
&lt;p&gt;I&#39;ve heard it&#39;s also testable, but nary a unit test was written during the hackathon.
I should have known better, based on my &lt;a href=&quot;https://www.steele.blue/lessons-learned-from-the-first-hack-omaha/&quot;&gt;lessons from last time&lt;/a&gt;.
That said, Go has &lt;a href=&quot;http://golang.org/doc/code.html#Testing&quot;&gt;testing built into the language&lt;/a&gt;, so it&#39;ll be there when we&#39;re ready.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/_xAXgQK4cb-600.jpeg&quot; alt=&quot;Alex and Michael&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;600&quot; srcset=&quot;https://www.steele.blue/img/_xAXgQK4cb-600.jpeg 600w, https://www.steele.blue/img/_xAXgQK4cb-800.jpeg 800w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Databases, GORM Style&lt;/h2&gt;
&lt;p&gt;Go has database operations built into the standard library; just bring your own database-specific adapter.
We went with MySQL, which &lt;a href=&quot;https://github.com/go-sql-driver/mysql&quot;&gt;has a great Go adapter&lt;/a&gt;.
And writing queries is lame, so we tried out an ORM called &lt;a href=&quot;https://github.com/jinzhu/gorm&quot;&gt;Gorm&lt;/a&gt;.
This was a little more mature, as it was a full 6 weeks old.&lt;/p&gt;
&lt;p&gt;Using Gorm is pretty simple: define a &lt;code&gt;struct&lt;/code&gt; with types named like your database columns:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; School&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; struct&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Id&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; int64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Name&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; `sql:&quot;size:255&quot;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;CountyId&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; int64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;DistrictId&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; int64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Lat&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; float64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;Lon&lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt; float64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And populate them:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; school&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#267F99;--shiki-dark:#4EC9B0&quot;&gt;School&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;schoolId&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; := &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;params&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;db&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;Where&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&quot;id = ?&quot;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;schoolId&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;First&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;school&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ORMs are appealing, especially when your database tables are denormalized by design to allow simple querying.
But the additional complexity Gorm brought might not have been worth it.
ORMs are known as the &lt;a href=&quot;https://web.archive.org/web/20131031003739/http://blogs.tedneward.com/2006/06/26/The+Vietnam+Of+Computer+Science.aspx&quot;&gt;Vietnam of Computer Science&lt;/a&gt; for a reason.&lt;/p&gt;
&lt;p&gt;Throughout Friday and all of Saturday, we were unable to connect to the database using Gorm&#39;s classes.
It wasn&#39;t until nearly midnight on Saturday that we were able to figure out the correct incantations to get MySQL and Gorm &lt;a href=&quot;https://github.com/mattdsteele/hackomaha-ops/commit/fe841c50d8b9a0116bd8e94545730e75df46bd00#diff-34c6b408d72845d076d47126c29948d1R18&quot;&gt;to pull in data&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Clearer documentation might have helped, but in the end it turned out to be mostly user error.&lt;/p&gt;
&lt;p&gt;Error handling in Go is idiomatically performed by returning two values from a function: the result you were expecting and an &lt;code&gt;err&lt;/code&gt; parameter.
If the &lt;code&gt;err&lt;/code&gt; value is null, things went great. Otherwise, you have an error that needs handling.
As we were connnecting to databases in Gorm, we weren&#39;t retrieving that second parameter; so we never knew we had a badly formed database URI, even though the library was begging to tell us.&lt;/p&gt;
&lt;p&gt;Lesson: understand the idioms of a language you&#39;re futzing around with.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/N-aqhydSDa-600.jpeg&quot; alt=&quot;Jerod and Leah&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;600&quot; srcset=&quot;https://www.steele.blue/img/N-aqhydSDa-600.jpeg 600w, https://www.steele.blue/img/N-aqhydSDa-800.jpeg 800w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Go Fly a Kite&lt;/h2&gt;
&lt;p&gt;It wasn&#39;t all sunshine. Beyond database and library issues, here&#39;s a few problems I encountered with Go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I like functional collections, and using slices (Go&#39;s version of an array) in a functional manner proved impossible.
Go &lt;em&gt;appears&lt;/em&gt; to have functions for &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, and others, but we never got them working.
It appears that Go programmers are content to use &lt;code&gt;for&lt;/code&gt; loops for everything, which is what we ended up doing.&lt;/li&gt;
&lt;li&gt;Go provides strict checks at compile time. This includes failing the build when you have declared a variable or import that you haven&#39;t used.
Normally I love these checks, but this proved more annoying than useful in the iterative environment of a hackathon.&lt;/li&gt;
&lt;li&gt;As a user of third-party libraries, Go&#39;s package management is great (simply type &lt;code&gt;go get github.com/package&lt;/code&gt; and it&#39;s available).
But I never figured how to split apart our single-file Go program into a decent structure.
We settled on placing our &lt;code&gt;struct&lt;/code&gt;s in one file, and everything else in another.
They all shared the &lt;code&gt;main&lt;/code&gt; package, as well.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So you won, right?&lt;/h2&gt;
&lt;p&gt;Nope. &lt;a href=&quot;https://web.archive.org/web/20160826210702/http://douglas.ne.localboards.org:80/&quot;&gt;This project did.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;So we should blame Go?&lt;/h2&gt;
&lt;p&gt;Nah. The Boards team had a better product, and changing our backend wouldn&#39;t have made the difference.&lt;/p&gt;
&lt;p&gt;If anything, I&#39;m ready to double down on Go for the next hackathon.&lt;/p&gt;
&lt;p&gt;You get productivity out of the box with a great standard library, plus the performance of a natively-compiled app, plus a syntax that feels both classical and modern.&lt;/p&gt;
&lt;p&gt;So forget the &lt;a href=&quot;https://en.wikipedia.org/wiki/LAMP_(software_bundle)&quot;&gt;LAMP&lt;/a&gt; or &lt;a href=&quot;https://web.archive.org/web/20131207033011/http://mean.io/&quot;&gt;MEAN&lt;/a&gt; stacks.
Try building your next application on &lt;a href=&quot;https://twitter.com/jerodsanto/status/405744094510473216&quot;&gt;GAP: Go, AngularJS, and Postgres&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/mattdsteele/hackomaha-ops&quot;&gt;View the final code on GitHub&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;vine:hUKDzMbhOBx&lt;/code&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Clickbait HTTP Status Codes</title>
    <link href="https://www.steele.blue/clickbait-status-codes/" />
    <updated>2013-11-20T00:00:00Z</updated>
    <id>https://www.steele.blue/clickbait-status-codes/</id>
    <content type="html">&lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;en&quot;&gt;&lt;p&gt;502 Unbelievable Bad Gateways (with photos)&lt;/p&gt;&amp;mdash; Brian Essbe (@SortaBad) &lt;a href=&quot;https://twitter.com/SortaBad/statuses/402918987161292800&quot;&gt;November 19, 2013&lt;/a&gt;&lt;/blockquote&gt;
&lt;script webc:keep=&quot;&quot; async=&quot;&quot; src=&quot;https://platform.twitter.com/widgets.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;
&lt;p&gt;Nice. Here&#39;s more &lt;strong&gt;clickbait HTTP Status Codes:&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;These 302 Found Puppies Will Restore Your Faith in Humanity&lt;/h3&gt;
&lt;h3&gt;403 Insane Quirks You Won&#39;t Believe Aren&#39;t Forbidden&lt;/h3&gt;
&lt;h3&gt;Here&#39;s 200 GIFs of Lil John that will make you say &amp;quot;OK!&amp;quot;&lt;/h3&gt;
&lt;h3&gt;304 Unbelievable Twerking Photos We Swear Have Not Been Modified&lt;/h3&gt;
&lt;h3&gt;Here Are The 404 Most Essential Hide And Seek Tips To Ensure You Are Not Found&lt;/h3&gt;
&lt;h3&gt;Why Did Tina Fey Say These 406 Quotes Are Not Acceptable?&lt;/h3&gt;
&lt;h3&gt;410 Nostalgic Things 90s Kids Will Recognize As Gone&lt;/h3&gt;
&lt;h3&gt;Check Out These 418 Weird Tricks To Make You A Teapot&lt;/h3&gt;
</content>
  </entry>
  <entry>
    <title>A fresh coat of paint</title>
    <link href="https://www.steele.blue/a-fresh-coat-of-paint/" />
    <updated>2013-10-20T00:00:00Z</updated>
    <id>https://www.steele.blue/a-fresh-coat-of-paint/</id>
    <content type="html">&lt;p&gt;After years of using my website as a &lt;a href=&quot;https://web.archive.org/web/20131110185340/http://dynamic.matthew-steele.com:80/&quot;&gt;glorified landing page&lt;/a&gt; to other content, I&#39;ve taken the time to build something proper.
The link dump is gone, and I&#39;ve moved my blog from WordPress.com to my domain. &lt;a href=&quot;https://github.com/mattdsteele/matthew-steele.com&quot;&gt;The site is also available on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Google Reader expatriates: update your RSS feeds: &lt;a href=&quot;https://web.archive.org/web/20131110185405/http://www.matthew-steele.com:80/feed/atom.xml&quot;&gt;http://www.matthew-steele.com/feed/atom.xml&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Why redesign?&lt;/h2&gt;
&lt;p&gt;A few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Link dumps are lame. The meat of this site is on the blog, so it makes sense to show that up front. &lt;a href=&quot;https://web.archive.org/web/20131005120649/http://www.lukew.com/ff/entry.asp?1458&quot;&gt;No one likes to wait while they wait&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I began using WordPress.com 6 years ago, when I was working mostly on the back-end and didn&#39;t want to think about website layouts or CSS. Times have changed.&lt;/li&gt;
&lt;li&gt;It&#39;s theraputic. I might be the only person who finds joy in moving periodically in the physical world. It&#39;s a great time to trash old artifacts and make way for the new.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&#39;s said that gearheads never have their own car in working condition. Car enthusiasts always have some project they&#39;re working on, and use their personal ride as a experiment.
&lt;em&gt;I wanted a place to experiment on my own.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Goodbye to all that&lt;/h2&gt;
&lt;p&gt;I went with a &amp;quot;wait until it&#39;s needed&amp;quot; approach for most content. The site is &lt;em&gt;JavaScript free&lt;/em&gt; (save Google Analytics) and doesn&#39;t include any CSS libraries.
I love jQuery, Modernizr and Bootstrap, but sometimes the only tools you need are your bare hands.&lt;/p&gt;
&lt;p&gt;This has resulted in a substantial shrinking of page weight. A text-only post on this platform is fully &lt;em&gt;86% lighter on page load&lt;/em&gt; than its WordPress cousin.&lt;/p&gt;
&lt;p&gt;I&#39;ve also held off on including comments. I like the idea of hashing out conversations elsewhere (like Twitter or Google+) and then writing a new post with the results.
If that doesn&#39;t work out, Disqus is a simple solution.&lt;/p&gt;
&lt;h2&gt;Breaking it down&lt;/h2&gt;
&lt;p&gt;I&#39;ve ditched WordPress, PHP, and databases. &lt;em&gt;The site is now 100% statically generated and hosted on Amazon S3.&lt;/em&gt; Building it has been exciting.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; generates the static site, and I&#39;m using &lt;a href=&quot;https://gruntjs.com/&quot;&gt;Grunt&lt;/a&gt; to automate many of the other tasks needed to build the site.
I first heard of this combo from &lt;a href=&quot;https://www.zachleat.com/web/zachleat-is-dead/&quot;&gt;Zach Leatherman&#39;s redesign&lt;/a&gt;, and it seemed ingenious.
Even though the tools are written in different languages, they play really nicely thanks to the &lt;a href=&quot;https://github.com/sindresorhus/grunt-shell&quot;&gt;grunt-shell&lt;/a&gt; plugin.&lt;/p&gt;
&lt;p&gt;Grunt automates everything. Compiling LESS files, running a local server, deployments to Amazon S3; all a &lt;code&gt;grunt&lt;/code&gt; command away.&lt;/p&gt;
&lt;p&gt;Posts are written in Markdown, which &lt;em&gt;feels&lt;/em&gt; right and lets me blog in Vim.&lt;/p&gt;
&lt;p&gt;While designing, I went &lt;em&gt;mobile-first and responsive&lt;/em&gt;. With such a simple static page, I was able to just pull up &lt;code&gt;localhost&lt;/code&gt; on the iOS simulator and my Android tablet and call it good.
It also simplified my CSS by a pretty substantial amount.&lt;/p&gt;
&lt;p&gt;Since I now have control over the layout, I also took the time to add an &lt;a href=&quot;https://web.archive.org/web/20131012072030/http://iliveinomaha.com:80/&quot;&gt;I Live in Omaha&lt;/a&gt; banner to the page.
There&#39;s an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; version of the banner, but I decided to rewrite it using pure HTML and CSS3. This made it responsive and Retina-sharp.
On small screens, it&#39;s a footer at the bottom of the site. As you gain more real estate, it jumps to the top of the page, and eventually gets fixed to the top.
Since it&#39;s progressively enhanced, this also avoids many of the &lt;a href=&quot;http://bradfrostweb.com/blog/mobile/fixed-position/&quot;&gt;issues with position:fixed&lt;/a&gt; on mobile devices.&lt;/p&gt;
&lt;h2&gt;Talk amongst yourselves&lt;/h2&gt;
&lt;p&gt;Let me know what you think. &lt;a href=&quot;https://twitter.com/mattdsteele/&quot;&gt;I&#39;m on Twitter.&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Exploring the Device APIs</title>
    <link href="https://www.steele.blue/exploring-the-device-apis/" />
    <updated>2013-08-06T00:00:00Z</updated>
    <id>https://www.steele.blue/exploring-the-device-apis/</id>
    <content type="html">&lt;p&gt;I gave a lightning talk at &lt;a href=&quot;http://nebraskajs.com/&quot;&gt;NebraskaJS&lt;/a&gt; on some of the new features available through the &lt;a href=&quot;https://www.steele.blue/diving-into-the-device-api/&quot;&gt;Device API&lt;/a&gt;. More importantly, it marks the glorious return of the Wayne&#39;s World light sensor.&lt;/p&gt;
&lt;h2&gt;Slides&lt;/h2&gt;
&lt;p&gt;&lt;iframe src=&quot;https://speakerdeck.com/player/67add4e0e13e0130414402bec6403867&quot; allowfullscreen=&quot;&quot; scrolling=&quot;no&quot; allow=&quot;autoplay; encrypted-media&quot; width=&quot;600&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h2&gt;Screencast&lt;/h2&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;71864754&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Diving into the Device API</title>
    <link href="https://www.steele.blue/diving-into-the-device-api/" />
    <updated>2013-06-19T00:00:00Z</updated>
    <id>https://www.steele.blue/diving-into-the-device-api/</id>
    <content type="html">&lt;p&gt;I recently read Tim Wright&#39;s &lt;a href=&quot;http://alistapart.com/article/environmental-design-with-the-device-api&quot;&gt;article on A List Apart&lt;/a&gt; detailing the Device API; a collection of W3C standards that let you obtain access to a number of hardware sensors.&lt;/p&gt;
&lt;p&gt;I found this fascinating, and had to try them out. &lt;strong&gt;Here&#39;s what I&#39;ve learned&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Note: for most of these demos, you&#39;ll want to try them out in specific browsers (either Android Firefox or iOS Safari); as of today, browser support is limited. The most up-to-date information on browser implementation is on &lt;a href=&quot;http://www.w3.org/2009/dap/wiki/ImplementationStatus&quot;&gt;this page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All demo code is &lt;a href=&quot;https://github.com/mattdsteele/device-apis&quot;&gt;available on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Battery Status&lt;/h2&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;dLD7Ve5t5cI&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;http://www.matthew-steele.com/projects/device-apis/battery.html&quot;&gt;View Demo &lt;em&gt;(works in Android Firefox)&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
The &lt;a href=&quot;https://dvcs.w3.org/hg/dap/raw-file/tip/battery/Overview.html#introduction&quot;&gt;Battery Status API&lt;/a&gt; is straightforward - it adds a new object &lt;code&gt;window.navigator.battery&lt;/code&gt; that you can inspect and discover how much juice is left in the device for you to suck out (this is exposed by &lt;code&gt;battery.dischargingTime&lt;/code&gt; and is measured in seconds).
&lt;p&gt;&lt;code&gt;battery.charging&lt;/code&gt; returns a boolean. You can inspect the charged level using &lt;code&gt;battery.level&lt;/code&gt;, but the most interesting parts are the &lt;a href=&quot;https://dvcs.w3.org/hg/dap/raw-file/tip/battery/Overview.html#event-handlers&quot;&gt;events&lt;/a&gt;, which let you capture when a device starts charging, or reaches a threshold over 60%, etc.&lt;/p&gt;
&lt;p&gt;The demo captures the &lt;code&gt;chargingchange&lt;/code&gt; event and changes some background colors. but you could do lots of things with this data. For example, it might be prudent to turn off 3D CSS transformations at a certain battery threshold, as they&#39;re quite power-hungry. &lt;strong&gt;By treating power as a feature test, you can take progressive enhancement to a new level&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Availability&lt;/h3&gt;
Firefox currently supports this API on desktop and mobile Android. WebKit &lt;a href=&quot;https://bugs.webkit.org/show_bug.cgi?id=62698&quot;&gt;appears to have implemented&lt;/a&gt; it briefly last year, but it&#39;s currently disabled and work to re-enable it &lt;a href=&quot;https://bugs.webkit.org/show_bug.cgi?id=90538&quot;&gt;appears to have stalled&lt;/a&gt;. Similarly, &lt;a href=&quot;https://code.google.com/p/chromium/issues/detail?id=122593&quot;&gt;Chromium has a patch built&lt;/a&gt; but it doesn&#39;t seem to have ever landed.
&lt;h2&gt;Ambient Light Sensor&lt;/h2&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;YEkhmYXJAeY&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;http://www.matthew-steele.com/projects/device-apis/lightsensor.html&quot;&gt;View Demo &lt;em&gt;(works in Android Firefox)&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
Most phones have an ambient light sensor - it&#39;s mostly used to dim the screen in low-light environments. The &lt;a href=&quot;https://dvcs.w3.org/hg/dap/raw-file/tip/light/Overview.html&quot;&gt;Ambient Light Sensor API&lt;/a&gt; works with any sensors that can read light levels, including an embedded camera. There&#39;s lots of environment-specific modifications to a site you could perform with this.
&lt;h3&gt;Availability&lt;/h3&gt;
Firefox on Android has it, and it&#39;s implemented on the desktop in OS X. There&#39;s a patch for Windows 7 that &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=754199&quot;&gt;appears to have stalled&lt;/a&gt;. I don&#39;t see any evidence that other browsers are planning to implement it.
&lt;h2&gt;Device Orientation&lt;/h2&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;c7rWFcYSs1g&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt;A native app that recently captured my attention was the &lt;a href=&quot;http://pizza-compass.com/&quot;&gt;well-advertised Pizza Compass&lt;/a&gt; for iOS. It does exactly what you&#39;d expect. I wanted to try a version in pure HTML5. Right now it only points you to a pizza place in my city, but it meets my use case, so why abstract?&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;http://www.matthew-steele.com/projects/device-apis/pizza.html&quot;&gt;View Demo &lt;em&gt;(works in Mobile Safari/iOS Chrome)&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
There&#39;s a number of device location/orientation sensors in modern cell phones, and it&#39;s not always clear which HTML5 feature to use:
&lt;ul&gt;
	&lt;li&gt;Accelerometer/Device Tilt: &lt;a href=&quot;http://dev.w3.org/geo/api/spec-source-orientation&quot;&gt;DeviceOrientation&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Latitude/Longitude: &lt;a href=&quot;http://dev.w3.org/geo/api/spec-source.html&quot;&gt;Geolocation&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Compass: &lt;a href=&quot;http://dev.w3.org/geo/api/spec-source-orientation&quot;&gt;DeviceOrientation&lt;/a&gt; (using the alpha property)&lt;/li&gt;
&lt;/ul&gt;
There&#39;s been plenty of apps that use Latitude/Longitude; many use it to assist in a &quot;Store Locator&quot; feature. Google&#39;s also done a few &lt;a href=&quot;http://chrome.com/campaigns/rollit&quot;&gt;interesting experiments&lt;/a&gt; with DeviceOrientation, but I haven&#39;t seen many instances of its use in the wild.
&lt;h3&gt;Here Be Dragons&lt;/h3&gt;
The basic functionality works on current iOS browsers. However, I ran into a number of quirks on other platforms that also support the DeviceOrientation API; which means you can&#39;t build interoperable apps with it.
&lt;ul&gt;
	&lt;li&gt;Firefox &lt;strong&gt;increases&lt;/strong&gt; the alpha property as the device rotates clockwise. All other tested devices &lt;strong&gt;decrease&lt;/strong&gt; the property when rotating clockwise.&lt;/li&gt;
	&lt;li&gt;The alpha property&#39;s initial value is all over the place. Some browsers (iOS Mobile Safari) set it at 0 based on the initial orientation of the device. Other browsers set 0 to a particular orientation, but it&#39;s inconsistent (Android Browser is 0 at west, Firefox is 0 at north, Chrome is ambiguous).&lt;/li&gt;
	&lt;li&gt;The event provides an absolute property, assuming the alpha value is calibrated to formal &lt;a href=&quot;http://dev.w3.org/geo/api/spec-source-orientation#deviceorientation&quot;&gt;Euler Angles&lt;/a&gt; (Firefox and Chrome for Android implement this). There doesn&#39;t seem to be consistency even with this.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Availability&lt;/h3&gt;
Most mobile browsers support these APIs, with the caveats stated above.
&lt;h2&gt;Conclusion&lt;/h2&gt;
These features are really slick, but they&#39;re hampered by the same issues that hinder the web platform generally: &lt;strong&gt;platform fragmentation and buggy implementations&lt;/strong&gt;. But take the long view, and soon enough you&#39;ll be able to build real-world sites that capture environment input in lots of cool ways.
</content>
  </entry>
  <entry>
    <title>Unit Testing JavaScript when you&#39;re Afraid of JavaScript</title>
    <link href="https://www.steele.blue/unit-testing-javascript-when-youre-afraid-of-javascript/" />
    <updated>2013-03-16T00:00:00Z</updated>
    <id>https://www.steele.blue/unit-testing-javascript-when-youre-afraid-of-javascript/</id>
    <content type="html">&lt;p&gt;I spoke at &lt;a href=&quot;http://www.nebraskacodecamp.com/&quot;&gt;Nebraska Code Camp&lt;/a&gt; on how to unit test JavaScript when you&#39;re deathly afraid of the language.&lt;/p&gt;
&lt;blockquote&gt;We&#39;ve gone from writing toy programs in JavaScript to full-blown applications, but our testing skills haven&#39;t caught up. Weirdly, lots of us are coming to JavaScript from other languages that value and encourage testing. What happened?
&lt;p&gt;Turns out, unit testing JavaScript is different than other languages, and doing it well means you have to rethink how you write JS code.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Slides&lt;/h2&gt;
&lt;p&gt;&lt;iframe src=&quot;https://speakerdeck.com/player/307545d070ae0130463812313d1c9482&quot; allowfullscreen=&quot;&quot; scrolling=&quot;no&quot; allow=&quot;autoplay; encrypted-media&quot; width=&quot;600&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h2&gt;Screencast&lt;/h2&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;62004647&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>What Zelda Taught Me About Front End Engineering</title>
    <link href="https://www.steele.blue/what-zelda-taught-me-about-front-end-engineering/" />
    <updated>2012-12-06T00:00:00Z</updated>
    <id>https://www.steele.blue/what-zelda-taught-me-about-front-end-engineering/</id>
    <content type="html">&lt;p&gt;I spoke at the HaymarketDev meetup in Lincoln on treating JavaScript like a real language.&lt;/p&gt;
&lt;p&gt;I was coming down from an NES Zelda bender, and I think it shows.&lt;/p&gt;
&lt;h2&gt;Slides&lt;/h2&gt;
&lt;p&gt;&lt;iframe src=&quot;https://speakerdeck.com/player/756e552021880130997622000a1d01dc&quot; allowfullscreen=&quot;&quot; scrolling=&quot;no&quot; allow=&quot;autoplay; encrypted-media&quot; width=&quot;600&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h2&gt;Screencast&lt;/h2&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;54993676&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
&lt;h2&gt;Video:&lt;/h2&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;2whBsRpkt4Q&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Unit Testing in JavaScript with Jasmine</title>
    <link href="https://www.steele.blue/unit-testing-in-javascript-with-jasmine/" />
    <updated>2012-10-21T00:00:00Z</updated>
    <id>https://www.steele.blue/unit-testing-in-javascript-with-jasmine/</id>
    <content type="html">&lt;p&gt;I spoke at the Omaha Java User Group this month about what testing JavaScript is like for a JS neophyte:&lt;/p&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;51600238&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Town Tester - How well does your city unit test?</title>
    <link href="https://www.steele.blue/town-tester/" />
    <updated>2012-09-18T00:00:00Z</updated>
    <id>https://www.steele.blue/town-tester/</id>
    <content type="html">&lt;p&gt;In my talk &lt;a href=&quot;https://vimeo.com/49092644/&quot;&gt;Zen and the Art of TDD&lt;/a&gt;, I included a slide that showed only 43% of all Github repositories from Omaha developers included unit tests:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/1oedVTqVuN-600.png&quot; alt=&quot;Languages&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;935&quot; height=&quot;499&quot; srcset=&quot;https://www.steele.blue/img/1oedVTqVuN-600.png 600w, https://www.steele.blue/img/1oedVTqVuN-935.png 935w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Today I&#39;m releasing the code I used to obtain this data: a script called Town Tester.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/mattdsteele/town-tester&quot;&gt;View town-tester on Github&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It&#39;s a Ruby script that queries the Github API to do the following tasks:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Find users with their Location set to the value you provide&lt;/li&gt;
	&lt;li&gt;Clones all &quot;popular&quot; repos (not a dotfiles project, at least 1 watcher, not a fork)&lt;/li&gt;
	&lt;li&gt;Checks for the existence of tests (based on a filename that includes &quot;test&quot; or &quot;spec&quot;)&lt;/li&gt;
	&lt;li&gt;Creates a .csv with data on each repo, and prints statistics broken down by language&lt;/li&gt;
&lt;/ul&gt;
I&#39;m excited for the possibilities of this project:
&lt;ul&gt;
	&lt;li&gt;Want to showcase how awesome your local community is? Steal developers away from San Francisco by proving your city can test better than their piddling 56% rate.&lt;/li&gt;
	&lt;li&gt;Put an end to the Great Semicolon Debates. Whichever developer has more JavaScript repositories with tests wins all syntax arguments by default!&lt;/li&gt;
	&lt;li&gt;Gamification is all the rage. Why not track your city&#39;s testing ratio over time, and give XP to developers that increase the ratio the most? Each month, the winner could get free drinks at your local &lt;a href=&quot;http://www.beerandcode.org/&quot;&gt;Beer &amp;amp;&amp;amp; Code&lt;/a&gt;.&lt;/li&gt;
	&lt;li&gt;Just in time for the &lt;a href=&quot;http://globalday.coderetreat.org/&quot;&gt;Global Day of Coderetreat&lt;/a&gt;, you could lure &lt;a href=&quot;https://twitter.com/coreyhaines&quot;&gt;Corey Haines&lt;/a&gt; to your hometown by being the best tested city in the world. I&#39;m pretty sure he doesn&#39;t really want to go to Sydney or Honolulu anyway.&lt;/li&gt;
&lt;/ul&gt;
I know I&#39;m just scratching the surface of this venture.
&lt;p&gt;Also, please don&#39;t game the system by adding a &amp;quot;test.txt&amp;quot; file to the root of each of your repositories.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Zen and the Art of TDD</title>
    <link href="https://www.steele.blue/zen-and-the-art-of-tdd-barcamp/" />
    <updated>2012-09-09T00:00:00Z</updated>
    <id>https://www.steele.blue/zen-and-the-art-of-tdd-barcamp/</id>
    <content type="html">&lt;p&gt;Over the weekend at &lt;a href=&quot;http://barcampomaha.org&quot;&gt;Barcamp Omaha&lt;/a&gt; I gave a talk on why I practice test driven development. Though TDD is over a decade old, it still isn&#39;t a practice we do with much regularity. So this is my Top 5 list of reasons why I think TDD is an important skill to develop.&lt;/p&gt;
&lt;p&gt;While prepping this talk, I discovered that only 43% of GitHub repositories from Omaha developers have any tests at all. How many were built with TDD is certainly an even smaller fraction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Slides with Audio&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;lite-vimeo videoid=&quot;49092644&quot;&gt;&lt;/lite-vimeo&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Video + Slides&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;wBX28bJIPhQ&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;http://barcamp-omaha-tdd.herokuapp.com/&quot;&gt;Slides&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;http://www.matthew-steele.com/talks/barcamp-app/&quot;&gt;JavaScript demo app&lt;/a&gt; | &lt;a href=&quot;http://www.matthew-steele.com/talks/barcamp-app/specs/&quot;&gt;Jasmine test suite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The code for the &lt;a href=&quot;https://github.com/mattdsteele/js-tdd-demo&quot;&gt;app&lt;/a&gt; and the &lt;a href=&quot;https://github.com/mattdsteele/barcamp-tdd-slides&quot;&gt;slides&lt;/a&gt; are also available on Github.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Lessons Learned from the first Hack Omaha</title>
    <link href="https://www.steele.blue/lessons-learned-from-the-first-hack-omaha/" />
    <updated>2012-04-19T00:00:00Z</updated>
    <id>https://www.steele.blue/lessons-learned-from-the-first-hack-omaha/</id>
    <content type="html">&lt;p&gt;This weekend I took part in Hack Omaha - the city&#39;s first hackathon with a focus on making apps from public data. We built an app that &lt;a href=&quot;http://www.omahafoodfight.org/&quot;&gt;gameified health inspection data&lt;/a&gt;. It was awesome. Nate&#39;s already written up an &lt;a href=&quot;http://fullycroisened.com/omaha-food-fight-hackomaha-app/&quot;&gt;hour by hour recap&lt;/a&gt; of our team&#39;s experience, but I thought I&#39;d share specifics of what I learned.&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Pitch an idea, even if it&#39;s not a winner.&lt;/strong&gt; Nick Wertzburger started the pitch session off with a joke app (I think) that he &lt;a href=&quot;https://twitter.com/#!/rannick/status/187954462441218048&quot;&gt;tweeted earlier&lt;/a&gt;. It was one of only a handful of pitches, and his team ran with it and melded it into an awesome &lt;a href=&quot;http://www.safeomaha.org/&quot;&gt;heatmap &lt;/a&gt;page; my favorite project of the weekend.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Sinatra rocks&lt;/strong&gt;. My day job consists of writing Web Services, with a capital W and S. The process is often heavyweight, cumbersome, and requires numerous approval and manual configuration steps. It was beyond refreshing to just write a &lt;code&gt;get /matchup&lt;/code&gt; method, paste in some JSON, and have a working service. Prototyping service design before you&#39;ve even gotten a dataset gives you lots of flexibility to change your design on the fly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/La0nNUYVUy-600.jpeg&quot; alt=&quot;Food Fight 1&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2560&quot; height=&quot;1920&quot; srcset=&quot;https://www.steele.blue/img/La0nNUYVUy-600.jpeg 600w, https://www.steele.blue/img/La0nNUYVUy-1000.jpeg 1000w, https://www.steele.blue/img/La0nNUYVUy-2560.jpeg 2560w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Heroku is great, except when it isn&#39;t&lt;/strong&gt;. We ran the app&#39;s service layer on a shared (read: free) Heroku instance and a shared Postgres database. This was my first experience hosting apps on Heroku, so I relied on Steve&#39;s expertise. Pushing changes was simple as pie, but we ran into numerous issues getting Rake migrations to function correctly. We ended up creating databases on my machine, and using Heroku&#39;s backup/restore feature to load up production. It&#39;s not pretty but it got the job done.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Designers are worth their weight in gold&lt;/strong&gt;. With all due respect to Nate&#39;s work, we could have used someone to help with the usability, icon design and overall polish of our app. Most projects were in the same boat. But they were in extremely limited supply here.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;ORM flexibility is helpful&lt;/strong&gt;. Since you have no idea what tech stack you&#39;ll be working with, you don&#39;t want to require teammates to have a particular database already installed. For example, Steve didn&#39;t have Postgres installed on his MacBook, but we just configured a SQLite instance on his box, set up his ActiveRecord configuration to connect to it and he was off and running.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/aJrhno2KJp-600.jpeg&quot; alt=&quot;Food Fight 2&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;2560&quot; height=&quot;1920&quot; srcset=&quot;https://www.steele.blue/img/aJrhno2KJp-600.jpeg 600w, https://www.steele.blue/img/aJrhno2KJp-1000.jpeg 1000w, https://www.steele.blue/img/aJrhno2KJp-2560.jpeg 2560w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Don&#39;t let your VCS hold you back.&lt;/strong&gt; We decided to use &lt;a href=&quot;https://github.com/organizations/HackOmahaFoodInspectors/&quot;&gt;GitHub&lt;/a&gt; to host the source, but only half the team had any git experience. Rather than try to learn a crash-course on git, they used a shared Dropbox folder as the repository location.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Colocation isn&#39;t necessary&lt;/strong&gt;. We spent most of Saturday working from our individual houses, and we stood up a Google+ Hangout to help. The video chat and screensharing worked really smoothly. We probably had an advantage over the folks who worked at the hackathon venue, as wi-fi was spotty the whole weekend.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;We could have been more ambitious with our tech stack.&lt;/strong&gt; We were familiar with almost every piece of what we built. Then I look at &lt;a href=&quot;http://www.omahabountyhunter.com/&quot;&gt;Omaha Bounty Hunter&lt;/a&gt;, which was developed against a &lt;strong&gt;&lt;a href=&quot;http://www.meteor.com/&quot;&gt;five-day old JavaScript framework&lt;/a&gt;&lt;/strong&gt;, and I feel a little sad that we didn&#39;t try something farther out there. At the very least, we could have tried a document database like Mongo, given that nothing we were doing was relational.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Keep projects small and focused&lt;/strong&gt;. We were essentially finished with our app by 6pm on Saturday. After that, we spent the rest of the time play testing, tweaking the design, and adding features like analytics, win/loss counting, etc. But having a small, achievable project meant we weren&#39;t scrambling to get basic functionality working at the last minute.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/Ua_MlAOjfh-551.jpeg&quot; alt=&quot;Food Fight 3&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;551&quot; height=&quot;400&quot;&gt;&lt;/p&gt;
  &lt;ul&gt;
  	&lt;li&gt;&lt;strong&gt;I don&#39;t know Ruby very well&lt;/strong&gt;. I kept running into syntax issues, like trying to return early out of a block (which isn&#39;t allowed). I also spent a ton of time learning the methods on Enumerable, and figuring out how attributes in ActiveRecord models function. You only have to look at the number of &lt;code&gt;Hash.new&lt;/code&gt; and &lt;code&gt;Array.new&lt;/code&gt; in the codebase to see that we&#39;re still noobs at this.&lt;/li&gt;
  	&lt;li&gt;&lt;strong&gt;Untested code becomes legacy code, fast&lt;/strong&gt;. We cranked out the services with nary a unit test in sight. If it didn&#39;t cause a syntax error, we shipped it. By the end, all code was making into Sinatra routes, which meant we had to reload our web app each time we wanted to change a `group_by`, or see if our ActiveRecord query was right. This slowed us down noticeably, even by day 3 of the project.&lt;/li&gt;
  	&lt;li&gt;&lt;strong&gt;Not all prepwork is fruitful&lt;/strong&gt;. I tried to learn two new technologies before the hackathon: &lt;a href=&quot;http://neo4j.org/&quot;&gt;Neo4J&lt;/a&gt; and &lt;a href=&quot;http://www.postgis.org/&quot;&gt;PostGIS&lt;/a&gt;. I thought each might be helpful, and I spent weeks trying to learn just enough to fake knowing it for a weekend. But I ended up using neither. Rather, a 2-hour session with &lt;a href=&quot;http://twitter.github.com/bootstrap/&quot;&gt;Twitter Bootstrap&lt;/a&gt; proved far more useful than anything else I knew.&lt;/li&gt;
  &lt;/ul&gt;
  Much thanks to &lt;a href=&quot;https://twitter.com/#!/steven_a_s&quot;&gt;Steve&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/#!/fullycroisened&quot;&gt;Nate&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/#!/mikeask&quot;&gt;Mike&lt;/a&gt; for putting up with me this weekend. And many, many thanks to &lt;a href=&quot;https://twitter.com/mattwynn&quot;&gt;Matt&lt;/a&gt; for setting up the event. I&#39;m ready to do it again.
</content>
  </entry>
  <entry>
    <title>I have a cameo in The Clean Coder</title>
    <link href="https://www.steele.blue/i-have-a-cameo-in-the-clean-coder/" />
    <updated>2011-11-04T00:00:00Z</updated>
    <id>https://www.steele.blue/i-have-a-cameo-in-the-clean-coder/</id>
    <content type="html">&lt;p&gt;On my way back from India, I finally had a chance to read &lt;a href=&quot;http://www.amazon.com/gp/product/0137081073/ref=pd_lpo_k2_dp_sr_1?pf_rd_p=486539851&amp;amp;pf_rd_s=lpo-top-stripe-1&amp;amp;pf_rd_t=201&amp;amp;pf_rd_i=0132350882&amp;amp;pf_rd_m=ATVPDKIKX0DER&amp;amp;pf_rd_r=0T5SQJEBB1E869YQFG4E&quot;&gt;The Clean Coder&lt;/a&gt;, Uncle Bob&#39;s latest book. It was something I&#39;d been personally wanting to do for a while now; having been introduced to the tenants of software craftsmanship, professionalism, and test-driven-design as part of reading his previous book, Clean Code.&lt;/p&gt;
&lt;p&gt;It also was something I&#39;d wanted to do because last year, my company brought in Robert C. Martin for a two-day course on TDD for me and a dozen other employees. And it was amazing. My colleague Shawn compared it to &amp;quot;getting private guitar lessons from Jimi Hendrix&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/VnAK0421ML-600.jpeg&quot; alt=&quot;Uncle Bob&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;1200&quot; srcset=&quot;https://www.steele.blue/img/VnAK0421ML-600.jpeg 600w, https://www.steele.blue/img/VnAK0421ML-1000.jpeg 1000w, https://www.steele.blue/img/VnAK0421ML-1600.jpeg 1600w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;To my delight, this came up as an anecdote in Chapter 6 of his book, &amp;quot;Practice&amp;quot;:&lt;/p&gt;
&lt;blockquote&gt;Since then many programmers have adopted a martial arts metaphor for their practice sessions. The name Coding Dojo seems to have stuck. Sometimes a group of programmers will meet and practice together just like martial artists do. At other times, programmers will practice solo, again as martial artists do.
&lt;p&gt;&lt;strong&gt;About a year ago I was teaching a group of developers in Omaha. At lunch they invited me to join their Coding Dojo. I watched as twenty developers opened their laptops and, keystroke by keystroke, followed along with the leader who was doing The Bowling Game Kata.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
Reading this on the plane, I couldn&#39;t help but smile. Uncle Bob has had a tremendous affect on my development style and coding habits, so I&#39;m glad to hear that I had an influence on him, however small that might be.&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/vMicdvfMaq-600.jpeg&quot; alt=&quot;Clean Coder&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;2560&quot; srcset=&quot;https://www.steele.blue/img/vMicdvfMaq-600.jpeg 600w, https://www.steele.blue/img/vMicdvfMaq-1000.jpeg 1000w, https://www.steele.blue/img/vMicdvfMaq-1920.jpeg 1920w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Postscript: At the start of the session, we each introduced ourselves and explained what we wanted to get out of the course. &lt;a href=&quot;http://twitter.com/JCake09&quot;&gt;Jessica&lt;/a&gt;&#39;s conversation went like this:&lt;/p&gt;
&lt;blockquote&gt;&lt;strong&gt;Jessica&lt;/strong&gt;: My name&#39;s Jessica Codr [pronounced &quot;coder&quot;]
&lt;p&gt;&lt;strong&gt;Uncle Bob&lt;/strong&gt;: [pause] That&#39;s the best last name I&#39;ve ever heard. I need to put your name in my next book!&lt;/p&gt;&lt;/blockquote&gt;
I think this counts.&lt;p&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Test-driven source code formatting</title>
    <link href="https://www.steele.blue/test-driven-source-code-formatting/" />
    <updated>2011-05-02T00:00:00Z</updated>
    <id>https://www.steele.blue/test-driven-source-code-formatting/</id>
    <content type="html">&lt;p&gt;This morning, I had to fight a production error in an application I support. It turns out that using SELECT * statements with Spring JDBC can cause problems when you add new columns to the table. It has something to do with Oracle caching; I&#39;m not sure.&lt;/p&gt;
&lt;p&gt;So, SELECT * statements are evil. But how do you ensure your codebase doesn&#39;t contain any? Being a good test-driven developer, I wanted to have a failing test before making wide swaths of changes to my source code.
The solution I settled on was to write a new static analysis rule, using Checkstyle, to warn me when it identified a SELECT * statement. Here&#39;s what it looks like (you can put this in a Checkstyle v5 xml file):&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;RegexpSinglelineJava&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;property&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;format&quot;&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;SELECT.*[&#92;. ]&#92;*&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;property&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;ignoreComments&quot;&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;property&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;Do not use SELECT * statements&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s a regular expression that looks for a SELECT, followed by anything, and then either a dot or a space followed by the asterisk. This way, we can capture:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; table&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; t.* &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; table&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; t&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But not:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; count&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(*) &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; table&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding this check to our build immediately caught 13 instances where we performed this nefarious deed.  Using a continuous integration build like Hudson, it was easy to identify and track how we were progressing in removing these from the build:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/zRXmYXfeYS-500.png&quot; alt=&quot;png&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;200&quot;&gt;&lt;/p&gt;
&lt;p&gt;Of course, there a number of issues with this approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queries defined outside of a .java file aren&#39;t scanned&lt;/li&gt;
&lt;li&gt;The regex misses queries defined over multiple lines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But it&#39;s a nice quick solution to an immediate problem, and we can iterate to solve it.  I hadn&#39;t thought about using static analysis tools as a form of test-driven development, but it seems like a natural extension of the red/green/refactor cycle.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Scramble Squares puzzle game solver</title>
    <link href="https://www.steele.blue/scramble-squares-puzzle-game-solver/" />
    <updated>2010-12-26T00:00:00Z</updated>
    <id>https://www.steele.blue/scramble-squares-puzzle-game-solver/</id>
    <content type="html">&lt;p&gt;My folks got a golf tile puzzle game called &amp;quot;&lt;a href=&quot;http://www.b-dazzle.com/puzzdetail.asp?PuzzID=52&amp;amp;CategoryName=Hobbies%20and%20Activities%20Puzzles&amp;amp;CatID=8&quot;&gt;Scramble Squares&lt;/a&gt;&amp;quot;. I was tired of trying to solve it manually, so I wrote a little Ruby app to find all the valid solutions. The code&#39;s on GitHub:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mattdsteele/scramblesquares-solver&quot;&gt;https://github.com/mattdsteele/scramblesquares-solver&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After uploading it, I realized that quite a few others have already &lt;a href=&quot;http://www.google.com/search?sourceid=chrome&amp;amp;ie=UTF-8&amp;amp;q=scramblesquares+solver&quot;&gt;published solutions&lt;/a&gt;. But mine&#39;s the only one with unit tests :)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Omaha Public Library card symbology</title>
    <link href="https://www.steele.blue/omaha-public-library-card-symbology/" />
    <updated>2010-01-28T00:00:00Z</updated>
    <id>https://www.steele.blue/omaha-public-library-card-symbology/</id>
    <content type="html">&lt;p&gt;In case you&#39;re like me, use &lt;a href=&quot;http://www.mycardstar.com/&quot;&gt;Cardstar&lt;/a&gt;, and want to put your &lt;a href=&quot;http://www.omahapubliclibrary.org/&quot;&gt;Omaha Public Library&lt;/a&gt; card on your phone, here&#39;s the custom barcode you should setup:&lt;/p&gt;
&lt;p&gt;Symbology: Codabar&lt;/p&gt;
&lt;p&gt;Start code: A&lt;/p&gt;
&lt;p&gt;Stop code: C&lt;/p&gt;
&lt;p&gt;Inverted: no&lt;/p&gt;
&lt;p&gt;Hope Google&#39;s indexing this. Knowledge is power!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Now this is a story all about how my bike got flipped, turned upside down</title>
    <link href="https://www.steele.blue/bike-recovered/" />
    <updated>2009-10-18T00:00:00Z</updated>
    <id>https://www.steele.blue/bike-recovered/</id>
    <content type="html">&lt;p&gt;As &lt;a href=&quot;https://www.steele.blue/stolen-bike-reward-offered/&quot;&gt;previously noted&lt;/a&gt;, my bike was stolen a couple weeks ago. Fortunately, some stories have happy endings, and it is now residing back at my house. What follows is the harrowing tale of how I got it back (note: it&#39;s not all that harrowing).&lt;/p&gt;
&lt;p&gt;Union Pacific has two sets of bike racks around its HQ building:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/xOhtSXhECf-600.png&quot; alt=&quot;Bike Racks&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;855&quot; height=&quot;369&quot; srcset=&quot;https://www.steele.blue/img/xOhtSXhECf-600.png 600w, https://www.steele.blue/img/xOhtSXhECf-855.png 855w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Rack 1 is positioned next to the front door of the building, and is underneath an overhang that helps protect bikes from the elements. Rack 2, newly added this year, is out next to 13th Street and affords none of the amenities of the original set.  Needless to say, Rack 1 is as popular as a Homecoming queen, while Rack 2 gets rejected like a guy who just upgraded his D&amp;amp;D Avenger character up to level 6.&lt;/p&gt;
&lt;p&gt;On this particular day, I was late biking into work, and all the spots in Rack 1 were taken, including several people who had double-used the rack spaces. Rather than trying to force my way in, I decided to park my bike on Rack 2, which happened to be empty when I locked it up.&lt;/p&gt;
&lt;p&gt;Actually, &amp;quot;locked&amp;quot; is probably an inappropriate phrase. The previous week, I lost my old lock of several years on a ride home. So I went out to the Bike Rack and looked over their offerings, deciding on &lt;a href=&quot;https://www.kryptonitelock.com/products/ProductDetail.aspx?cid=1001&amp;amp;scid=1001&amp;amp;pid=1127&quot;&gt;this particular model&lt;/a&gt;. Kryptonite rates its security as a &amp;quot;1&amp;quot;, which they state is appropriate &amp;quot;if you live in the &#39;burbs and have a Rottweiler next to your bike&amp;quot;. A couple pieces of twine tied together probably would have offered only slightly more protection than what I was using.&lt;/p&gt;
&lt;p&gt;If there was ever a valid time for a lock to be guilty of EPIC FAIL, this was it. On the first day of its operation, it was broken and my bike was taken sometime while I was working. The thief didn&#39;t leave the broken lock, so I don&#39;t know whether they cut the cable, or found some way to crack the combination, though I&#39;m guessing they used a pair of bolt cutters to snap it.&lt;/p&gt;
&lt;p&gt;When I realized my bike was gone, I told a few people in the building about the theft, and got a ride home from a coworker. That evening, I called the police&#39;s theft report line and gave them a my phone number and a brief description. And of course I &lt;a href=&quot;http://twitter.com/orphum/status/4301225743&quot;&gt;tweeted&lt;/a&gt; about it.&lt;/p&gt;
&lt;p&gt;The next day, I got a call back from the police, and gave them more details about the theft. When the officer asked me if I had the bike&#39;s serial number and I replied &amp;quot;no&amp;quot;, she responded with a disappointed-sounding &amp;quot;Oh.&amp;quot;&lt;/p&gt;
&lt;p&gt;I also got some great advice from my coworkers &lt;a href=&quot;http://redd-shift.blogspot.com/&quot;&gt;Scott&lt;/a&gt; and &lt;a href=&quot;http://steel-cut.blogspot.com/&quot;&gt;Brady&lt;/a&gt;, the latter having had his own bike stolen earlier this year. Brady told me what he did to recover his bike, including handing out flyers to local businesses and pedestrians, checking in pawn shops in the area, and &lt;a href=&quot;http://steel-cut.blogspot.com/2009/07/old-yeller-stolen.html&quot;&gt;blogging about it&lt;/a&gt;. He also mentioned the best way of recovering a bike was if I had the serial number available, which made me doubly skeptical that I would ever see it again.&lt;/p&gt;
&lt;p&gt;Then for a long time, nothing happened.&lt;/p&gt;
&lt;p&gt;On a Sunday afternoon, about two weeks after the theft, I got a call from an officer working in the Pawn unit, saying he got a description of a bike similar to mine in an area pawn shop. Following up, I gave him some additional details about the bike (going off memory and the one photo I had available), and a few days later he sent me some &lt;a href=&quot;http://twitpic.com/klhvx&quot;&gt;photos&lt;/a&gt; of a bike that was 100%, definitely mine. I sent him a reply and asked him to contact me when I could pick it up.&lt;/p&gt;
&lt;p&gt;Then for a long time, nothing happened.&lt;/p&gt;
&lt;p&gt;A week and several voicemails later, I finally got a response from the police, who sent a letter to both me and the pawn shop, declaring that I was the rightful owner and could pick up the bike. So yesterday, I ventured downtown to the Mid City pawn shop, and paid the $35 fee needed to get my bike out of hock.&lt;/p&gt;
&lt;p&gt;I&#39;ve learned a lot of things through this whole ordeal, most of which are outlined in &lt;a href=&quot;http://steel-cut.blogspot.com/2009/07/bradys-got-his-groove-ride-back.html&quot;&gt;Brady&#39;s own post&lt;/a&gt; about recovering his bike; so I won&#39;t repeat them here.&lt;/p&gt;
&lt;p&gt;But the most important part is to&lt;strong&gt; record your bike&#39;s serial number.&lt;/strong&gt; If you have a bike, &lt;strong&gt;do it now&lt;/strong&gt;. I was a lucky bastard to have mine recovered without it, but everything I&#39;ve experienced tells me that it would have made the process much easier. Pawn shops are required to file a bike&#39;s serial number with the police before they can sell it. Without the serial number, I had to trust that the pawn shop and the police operated on the same wavelength, and would be able to identify the merchandise by my vague description: &amp;quot;It&#39;s light blue. Or is it teal? Man, I wish I had more than just &lt;a href=&quot;https://www.steele.blue/images/bike.jpg&quot;&gt;one picture&lt;/a&gt; of this thing.&amp;quot;&lt;/p&gt;
&lt;p&gt;So I&#39;m back in business! I picked up a new Kryptonite-built U-Lock from the Trek store and plan on riding into work tomorrow. No mere larceny can hold me down:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/7I5ZlzpvzA-600.jpeg&quot; alt=&quot;Bike&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;1200&quot; srcset=&quot;https://www.steele.blue/img/7I5ZlzpvzA-600.jpeg 600w, https://www.steele.blue/img/7I5ZlzpvzA-1000.jpeg 1000w, https://www.steele.blue/img/7I5ZlzpvzA-1600.jpeg 1600w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;I only ask that Omaha&#39;s thief population let me keep my bike for at least one week before attempting another swindling.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The King of Kong - A Fistful of Inaccuracies</title>
    <link href="https://www.steele.blue/the-king-of-kong-a-fistful-of-inaccuracies/" />
    <updated>2008-02-18T00:00:00Z</updated>
    <id>https://www.steele.blue/the-king-of-kong-a-fistful-of-inaccuracies/</id>
    <content type="html">&lt;p&gt;One of the more interesting films I watched last year was the &lt;a href=&quot;http://www.imdb.com/title/tt0923752/&quot;&gt;King of Kong&lt;/a&gt;, a documentary about the quest to break (or retain) the Donkey Kong high score world record. As Zach Leatherman &lt;a href=&quot;http://www.zachleat.com/2.0/2007/10/04/the-king-of-kong-2007/&quot;&gt;points out&lt;/a&gt;, there seemed to be some glaring (if trivial) inaccuracies in the film, such as the exclusion of a third contender for the high score title. Nothing to get too worked up over though; you have to edit the film to make it more enjoyable. That&#39;s show business.&lt;/p&gt;
&lt;p&gt;It looks like we&#39;ve been had. Jason Scott, a filmmaker working on his own arcade documentary, has some not-so-kind words to say:&lt;/p&gt;
&lt;blockquote&gt;[This is] a documentary that rips entire groups of good-hearted people as shadowy, conniving scumbags with razor-thin morality hurts the scene being portrayed and hurts the people themselves. All this effort, just to turn reality into a faked up drama worthy of a dime store pulp.&lt;/blockquote&gt;
Some of the core themes of the documentary apparently are conjured up by omitting inconvenient facts that would singlehandedly disprove the theme.  For example:
&lt;blockquote&gt;Billy denies Steve the satisfaction of playing one-on-one on Donkey Kong. &lt;i&gt;They&#39;d played Donkey Kong one-on-one a year before the documentary was filmed at a previous championship.&lt;/i&gt;&lt;/blockquote&gt;
&lt;a href=&quot;http://ascii.textfiles.com/archives/000574.html&quot;&gt;Read the whole thing.&lt;/a&gt;  For more, take a look at Twin Galaxies&#39; &lt;a href=&quot;http://www.twingalaxies.com/forums/viewforum.php?f=86&quot;&gt;verbose comments&lt;/a&gt; on the film, or &lt;a href=&quot;http://www.avclub.com/content/feature/the_king_of_kong_continued&quot;&gt;Billy Mitchell&#39;s interview&lt;/a&gt; with The A/V Club.
</content>
  </entry>
  <entry>
    <title>Inelegant code affects your reputation</title>
    <link href="https://www.steele.blue/inelegant-code-affects-your-reputation/" />
    <updated>2008-01-21T00:00:00Z</updated>
    <id>https://www.steele.blue/inelegant-code-affects-your-reputation/</id>
    <content type="html">&lt;p&gt;You&#39;ve probably heard about (or signed up with) &lt;a href=&quot;http://www.mint.com&quot;&gt;Mint&lt;/a&gt;, the startup which promises to liberate us all from the Quickens of the world and revolutionize personal finance. It&#39;s gotten quite a few high-profile &lt;a href=&quot;http://lifehacker.com/software/screenshot-tour/is-mint-ready-for-your-money-312083.php&quot;&gt;positive reviews&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Mint has an incredibly hard sell. Before handing over all of your financial information to a startup run by &lt;a href=&quot;http://www.mint.com/team.html&quot;&gt;five folks&lt;/a&gt;, they have to convince you to really, truly, &lt;i&gt;deeply&lt;/i&gt; trust them. You&#39;re handing over the user name, password, and any personal security questions for your banks and credit cards. Mint has to overcome barriers not seen since Paypal&#39;s startup. The site prominently displays &lt;a href=&quot;http://www.mint.com/safe.html&quot;&gt;page&lt;/a&gt; after &lt;a href=&quot;http://www.mint.com/security-faq.html&quot;&gt;page&lt;/a&gt; detailing their focus on security.&lt;/p&gt;
&lt;p&gt;I was eventually convinced and signed up. Mint&#39;s &lt;a href=&quot;http://lifehacker.com/photogallery/Mint-Tour/2870585&quot;&gt;account entry&lt;/a&gt; page is well designed and ajax-y; set up a new account, and it fires off a few &lt;a href=&quot;http://en.wikipedia.org/wiki/XMLHttpRequest&quot;&gt;XHR&lt;/a&gt;s, keeping you notified of login/signup activity. Because I&#39;m a curious fellow, I inspected the contents of one:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;ajax-response&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; id&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;	&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;timestamp&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;timestamp&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;id&quot;:xxxx,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;name&quot;:&quot;My Personal Bank&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;uri&quot;:&quot;https://www.mypersonalbank.com&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;fiId&quot;:12345,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;status&quot;:123,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;terminal&quot;:false,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;balance&quot;:9999.99,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;refreshed&quot;:&quot;01/21/2008 14:06:15&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;isFirst&quot;:false,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    &quot;html&quot;:&quot;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&#39;cardfi &#39;&lt;/span&gt;&lt;span style=&quot;color:#E50000;--shiki-dark:#9CDCFE&quot;&gt; id&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#CE9178&quot;&gt;&#39;pollElem-12341234&#39;&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;            .... html continues ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;&quot;}]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;  &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#569CD6&quot;&gt;ajax-response&lt;/span&gt;&lt;span style=&quot;color:#800000;--shiki-dark:#808080&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;ve cleaned up the response and stripped out any of my personal data; for security, you know?&lt;/p&gt;
&lt;p&gt;In any case, notice the data structure Mint&#39;s using to encapsulate its information. The entire content is wrapped in XML, with an &amp;lt;ajax-response&amp;gt; element as its envelope. There&#39;s a &amp;lt;timestamp&amp;gt; elemented appended too, but the bulk of the data is encoded in the &amp;lt;json&amp;gt; element.&lt;/p&gt;
&lt;p&gt;In &amp;lt;json&amp;gt;, basic bank information is encoded in key-value pairs, such as the bank&#39;s name, URI, etc. Then there&#39;s the &amp;quot;html&amp;quot; key. Raw HTML lives here, containing mostly data stored in a &amp;lt;table&amp;gt; block. This is the real meat and potatoes of the XHR; it shows the actual status of the connection.&lt;/p&gt;
&lt;p&gt;So backtracking a bit, notice how that data is sent across the server. The actual markup used to update the page is sent, wrapped in a JSON envelope, which is itself wrapped in an XML envelope. Mint &amp;lt;i&amp;gt;must&amp;lt;/i&amp;gt; be taking security seriously, as they require you to decode &amp;lt;i&amp;gt;three&amp;lt;/i&amp;gt; completely different markup wrappers in the scope of a single ajax call.&lt;/p&gt;
&lt;p&gt;In all seriousness, this isn&#39;t groundbreaking stuff. Code like this goes up in enterprise applications all the time, and worse kludges won&#39;t make it to the &amp;lt;a href=&amp;quot;http://thedailywtf.com/&amp;quot;&amp;gt;Daily WTF&amp;lt;/&amp;gt; any time soon.&lt;/p&gt;
&lt;p&gt;So why worry about it?&lt;/p&gt;
&lt;p&gt;As Web apps become more complicated and handle ever increasing amounts of sensitive data, companies can no longer merely claim they are designed with a focus on security. Fair or not, the code we write says just as much (maybe more!) about how much thought has gone into an application&#39;s design. If your company is already fighting against a torrent of criticism that perceives your product as rapidly designed and possibly insecure, something as simple as redundant nested data structures can have a profound effect on what people think about you and your software.&lt;/p&gt;
&lt;p&gt;Mint probably won&#39;t lose users explicitly because of the data format of their XHRs. However, it provides one more piece of ammunition critics can use to paint it as untrustworthy, and may make prospective users just a little more wary of handing over their personal data. In a hyper-competitive environment like startup software development, why risk it?&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Unintended consequences of the Internet</title>
    <link href="https://www.steele.blue/unintended-consequences-of-the-internet/" />
    <updated>2007-11-27T00:00:00Z</updated>
    <id>https://www.steele.blue/unintended-consequences-of-the-internet/</id>
    <content type="html">&lt;p&gt;So Scott Adams has decided to &lt;a href=&quot;http://dilbertblog.typepad.com/the_dilbert_blog/2007/11/going-forward.html&quot;&gt;cut down on his blogging&lt;/a&gt;, because he doesn&#39;t like losing money being misunderstood:&lt;/p&gt;
&lt;blockquote&gt;Every blog post reduced my income, even if 90% of the readers loved it. And a startling number of readers couldn’t tell when I was serious or kidding, so most of the negative reactions were based on misperceptions.&lt;/blockquote&gt;
Let me be the first to stake out a controversial position: this is terrible.  Not only is Scott one of the most clear and concise writers I&#39;ve read, he&#39;s popularized &lt;a href=&quot;http://basicinstructions.net/&quot;&gt;fantastic webcomics&lt;/a&gt; and allowed me at least one reprieve from work, every day.
&lt;p&gt;He doesn&#39;t need to retire.  I have a solution to his problem:&lt;/p&gt;
&lt;h1&gt;؟&lt;/h1&gt;
Introducing the &lt;a href=&quot;http://en.wikipedia.org/wiki/Irony_mark&quot;&gt;irony mark&lt;/a&gt;.  You use it, get this, to indicate when you&#39;re being ironic.  Examples include:
&lt;ul&gt;
	&lt;li&gt;Agile programming is nothing but writing code and complaining؟&lt;/li&gt;
	&lt;li&gt;Hillary Clinton should stop running for President and &lt;a href=&quot;http://www.facebook.com/group.php?gid=2233338482&quot;&gt;make me a sandwich&lt;/a&gt;؟&lt;/li&gt;
&lt;/ul&gt;
I expect Scott to start posting regularly once again, and my royalty check to arrive shortly thereafter.؟
</content>
  </entry>
  <entry>
    <title>Screen scraping Google Spreadsheets exported as HTML</title>
    <link href="https://www.steele.blue/screen-scraping-google-spreadsheets-exported-as-html/" />
    <updated>2007-11-03T00:00:00Z</updated>
    <id>https://www.steele.blue/screen-scraping-google-spreadsheets-exported-as-html/</id>
    <content type="html">&lt;p&gt;Let&#39;s say you had a wealth of information in a Google spreadsheet that you wanted to access using Javascript. And let&#39;s say you also forgot you could export the doc as a .csv file (or in my case, wanted to try your hand at javascript screen scraping). What&#39;s a super-simple way you could accomplish this?&lt;/p&gt;
&lt;p&gt;Glad you asked, because I&#39;ve got just the solution for you. It requires using Prototype, though you could use it with just about any other library which lets you use CSS selectors.&lt;/p&gt;
&lt;p&gt;First, create an array with which to store your data:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; items&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, you&#39;ll have want to create a function which takes the raw HTML input and parses it into a set of objects:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; parseData&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;googleDoc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; divItem&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;document&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;createElement&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;div&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  divItem&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;innerHTML&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;googleDoc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;  $&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;divItem&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;hide&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  document&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;appendChild&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;divItem&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; tblMain&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;tblMain&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;  $&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;tblMain&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;getElementsBySelector&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;td.rAll&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;each&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;n&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; == &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;//if you have a header row, you don&#39;t want to include it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;      var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; item&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;n&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;parentNode&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;      var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; kids&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;item&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;getElementsBySelector&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;td.g&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;      items&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; - &lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;] = &lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;parseRow&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;kids&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  document&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;removeChild&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;divItem&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; parseRow&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;kids&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; obj&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = {};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  obj&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;kids&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;].&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;innerHTML&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  obj&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;column1&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;kids&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;].&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;innerHTML&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;  obj&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;column2&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;kids&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#098658;--shiki-dark:#B5CEA8&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;].&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;innerHTML&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#008000;--shiki-dark:#6A9955&quot;&gt;  //...etc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#AF00DB;--shiki-dark:#C586C0&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; obj&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, just pull up the document with an Ajax call:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes light-plus dark-plus&quot; style=&quot;background-color:#FFFFFF;--shiki-dark-bg:#1E1E1E;color:#000000;--shiki-dark:#D4D4D4&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt; loadData&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;  var&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; req&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt; Ajax&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;Request&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt;&#39;url/to/googleDoc.html&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;    method:&lt;/span&gt;&lt;span style=&quot;color:#A31515;--shiki-dark:#CE9178&quot;&gt; &#39;get&#39;&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;    onComplete&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#0000FF;--shiki-dark:#569CD6&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;      googleDoc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;responseText&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#795E26;--shiki-dark:#DCDCAA&quot;&gt;      parseData&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#001080;--shiki-dark:#9CDCFE&quot;&gt;googleDoc&lt;/span&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#000000;--shiki-dark:#D4D4D4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, huh?&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>UI designs in Leopard that I really hope grow on me</title>
    <link href="https://www.steele.blue/ui-designs-in-leopard-that-i-really-hope-grow-on-me/" />
    <updated>2007-10-29T00:00:00Z</updated>
    <id>https://www.steele.blue/ui-designs-in-leopard-that-i-really-hope-grow-on-me/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;The Menu bar: Transparently awful&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/aYSnw_tvZf-600.png&quot; alt=&quot;Leopard 1&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;109&quot; srcset=&quot;https://www.steele.blue/img/aYSnw_tvZf-600.png 600w, https://www.steele.blue/img/aYSnw_tvZf-912.png 912w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;Whoever gave the go-ahead on adding transparencies to everything menu-related needs to get run over by a train. It&#39;s distracting and focuses your attention away from the text of the menu. This is a double whammy, considering you&#39;ve already been distracted from an application&#39;s window the menu. To make this less transparent is just downright cruel. It&#39;s like they took the worst feature from Vista&#39;s UI, gave it some rounded corners, and vomited it straight to your desktop.&lt;/p&gt;
&lt;p&gt;Context menus are no better:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/KEIwX56tCe-277.png&quot; alt=&quot;Leopard 5&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;277&quot; height=&quot;257&quot;&gt;&lt;/p&gt;
&lt;p&gt;Thanks for splitting my menu in half, jackass.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The New Dock : Let&#39;s get busy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I don&#39;t care so much about the &amp;quot;Shelf&amp;quot; vs &amp;quot;Dock&amp;quot; sound and fury that&#39;s been circling the web lately, but the screen reflections around the application&#39;s icons are super distracting:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/klUEFnsWZ4-600.png&quot; alt=&quot;Leopard 4&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;830&quot; height=&quot;56&quot; srcset=&quot;https://www.steele.blue/img/klUEFnsWZ4-600.png 600w, https://www.steele.blue/img/klUEFnsWZ4-830.png 830w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
&lt;p&gt;If that horizontal line slicing the dock background in half isn&#39;t enough to grind your gears, then you are less likely than I to fly off the handle over UI changes. Really, how do you sleep at night?&lt;/p&gt;
&lt;p&gt;The reflections under the icons are even worse, since they actively prevent you from seeing whether your application is in use. Was the little black triangle too usable, so they had to scale back my satisfaction?&lt;/p&gt;
&lt;p&gt;At least Leopard gives me the tools with which to express my frustration:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/WjgNDelW_D-600.jpeg&quot; alt=&quot;Leopard Angry&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;480&quot; srcset=&quot;https://www.steele.blue/img/WjgNDelW_D-600.jpeg 600w, https://www.steele.blue/img/WjgNDelW_D-640.jpeg 640w&quot; sizes=&quot;100vw&quot;&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The Thatcher Illusion</title>
    <link href="https://www.steele.blue/the-thatcher-illusion/" />
    <updated>2007-09-23T00:00:00Z</updated>
    <id>https://www.steele.blue/the-thatcher-illusion/</id>
    <content type="html">&lt;p&gt;Want to see something cool?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/f8mgxnr_L9-385.jpeg&quot; alt=&quot;Thatcher Illusion&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;385&quot; height=&quot;277&quot;&gt;&lt;/p&gt;
&lt;p&gt;Aside from upside down, pretty normal, right? Compare it to these:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.steele.blue/img/gjM6hCrtm6-385.jpeg&quot; alt=&quot;Thatcher Illusion&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; width=&quot;385&quot; height=&quot;277&quot;&gt;&lt;/p&gt;
&lt;p&gt;What the heck happened here, and why does the left photo look like something you&#39;d find on the pages of Fark? Consider this: the two pairs of photos are &lt;strong&gt;exactly the same&lt;/strong&gt;, except rotated 180 degrees.&lt;/p&gt;
&lt;p&gt;This is the best example of the &lt;a href=&quot;http://en.wikipedia.org/wiki/Thatcher_effect&quot;&gt;Thatcher Illusion&lt;/a&gt;, an optical illusion I stumbled upon this weekend. People don&#39;t tend to recognize faces holistically; rather, we focus on local features like the eyes or mouth. As a result, if you invert the photo but leave the eyes rotated &amp;quot;normally&amp;quot;, it looks fine. But do the inverse and you&#39;ve midwived a monstrosity. You can see more examples (including a polarity-reversed photo of Tony Blair) &lt;a href=&quot;http://scienceblogs.com/mixingmemory/2007/09/cool_visual_illusions_the_tony.php&quot;&gt;here.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This reminds me of my favorite facial optical illusion:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid=&quot;QbKw0_v2clo&quot;&gt;&lt;/lite-youtube&gt;&lt;/p&gt;
</content>
  </entry>
</feed>