<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[chendo]]></title><description><![CDATA[chendo, web performance engineer. I ramble about web technologies, performance, things I have learned]]></description><link>https://chen.do/</link><image><url>https://chen.do/favicon.png</url><title>chendo</title><link>https://chen.do/</link></image><generator>Ghost 5.25</generator><lastBuildDate>Thu, 09 Apr 2026 13:41:07 GMT</lastBuildDate><atom:link href="https://chen.do/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[UniFi Travel Router]]></title><description><![CDATA[<p>I picked up one of these nifty gadgets recently in order to provide backup WAN options on my UDM Pro setup at home, as it provides the ability to bridge WiFi and USB tethering to Ethernet, similar to various travel routers that exist already but in a nice form factor</p>]]></description><link>https://chen.do/unifi-travel-router/</link><guid isPermaLink="false">69d0ad6d18ab3d0001b10423</guid><category><![CDATA[networking]]></category><category><![CDATA[gear]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Sat, 04 Apr 2026 06:47:35 GMT</pubDate><content:encoded><![CDATA[<p>I picked up one of these nifty gadgets recently in order to provide backup WAN options on my UDM Pro setup at home, as it provides the ability to bridge WiFi and USB tethering to Ethernet, similar to various travel routers that exist already but in a nice form factor and integration into the UniFi ecosystem.</p><p>It comes with a short USB-C to USB-C data cable for tethering, which is nice inclusion. Does not come with a AC power adapter.</p><p>The device is powered via USB-C at 5V, drawing about 1.3-1.7w on boot, then increasing to 3.0-3.5w after boot.</p><p>There&apos;s another USB-C port next to it that you can connect to a phone to tether. It does provide power to downstream devices but only at about 2.3w.</p><p>The little screen on it is nice and provides useful state on the device, including signal strength of the WiFi network it&apos;s connected to, however you must use the UniFi app to configure the device.</p><p>It can copy WiFi networks from a network on your UniFi account and also provides easy Teleport (VPN) functionality back to that same network, allowing one to easily be back in home/work network environment while travelling.</p><p>It gets quite warm during use. My thermal imaging camera clocked about 45C after a couple of hours of use, not concentrated on any particular area. This makes my idea of 3D printing a case that also stores the cable a bit trickier.</p><figure class="kg-card kg-image-card"><img src="https://chen.do/content/images/2026/04/image.png" class="kg-image" alt loading="lazy" width="534" height="670"></figure><p>Interestingly enough, when connected to my Starlink Mini&apos;s WiFi network to bridge to my UDM Pro, it pulls in Starlink&apos;s obstruction and dish latency stats, which is kinda neat. It&apos;s meant to automatically handle captive portal passwords for you too but I haven&apos;t tried this functionality yet.</p><p>It will be coming with me on my next trip.</p>]]></content:encoded></item><item><title><![CDATA[Fixing stuttering in iRacing with overlays, G-SYNC, and triple monitors]]></title><description><![CDATA[How I finally got buttery-smooth frames on my iRacing setup on triple monitors, G-SYNC, and overlays.]]></description><link>https://chen.do/fixing-stuttering-in-iracing-with-overlays-g-sync-and-triple-monitors/</link><guid isPermaLink="false">688d863708ad470001e62b49</guid><category><![CDATA[iracing]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Sat, 02 Aug 2025 04:34:57 GMT</pubDate><content:encoded><![CDATA[<p>This article is provided without warranty or support, YMMV etc. However, if you are a professional racer (sim or otherwise), I&apos;m willing to offer assistance on this issue for fee. Email me at [gsync AT chen DOT do].</p><p>I got into sim racing a couple of months ago and have been struggling to get overlays to work performantly with iRacing but I believe I&apos;ve figured out at least a bunch of the issues that can occur and I hope this can help others.</p><h1 id="my-setup">My setup</h1><ul><li>AMD 9800X3D, PBO, IF 2000MHZz</li><li>32GB DDR5 @ 6000</li><li>NVIDIA 5080</li><li>3x LG Ultragear 32GS75Q-B, running at 2560x1440 @ 180Hz</li><li>SimHub for overlays</li></ul><h1 id="the-problem">The problem</h1><p>G-SYNC was not reliably working, and this resulted in noticeable stuttering during races, especially during corners. It was unclear initially why G-SYNC would stop working, as it would be working with overlays, but restarting iRacing sometimes would cause it to not work. Disabling Multi-Plane Overlays did not help here.</p><p>Using Nvidia Surround would generally fix G-SYNC, but this would break when overlays were used.</p><p>I used a couple of different tools here to help diagnose the issue, including Special K (understanding monitor MPO state), Intel PresentMon (understanding rendering mechanism), and CapFrameX (recording frametimes to visualise stuttering). I used NVIDIA Profile Inspector to enable the G-SYNC support indicator, which gives more detailed information on G-SYNC status.</p><h1 id="measurement-of-gpu-wattage-can-cause-stuttering">Measurement of GPU wattage can cause stuttering</h1><p>Any tool that measures system metrics, especially GPU metrics, can cause stuttering. People have mentioned this especially applies to getting GPU power. In my case, the offending tool was MSI Afterburner which I was using to undervolt. CapFrameX visualised the stuttering quite clearly.</p><p>I was also using Nvidia&apos;s statistics overlay to render some metrics, including GPU power which I have since disabled, but this doesn&apos;t seem to reliably cause stuttering in my case.</p><h1 id="integrated-gpu-can-cause-stuttering">Integrated GPU can cause stuttering</h1><p>I noticed that the Nvidia statistics overlay would show drastically different FPS and FPS 1% metrics compared to iRacing, which was a hint that something wasn&apos;t quite right, especially with FPS 1% going as low as 15fps!</p><p>Analysing iRacing frametimes with CapFrameX showed stable frametimes after resolving stuttering caused by GPU metrics, but I was still able to see stuttering, which did confuse me at first...</p><p>I noticed in Task Manager that DWM (the desktop window manager/compositor) was running on my iGPU, and realised that even though I had SimHub and iRacing using the dGPU, any composition of windows would require the dGPU to copy 7680x1440 worth of image data to the iGPU, process composition on the iGPU, then copying this back to the dGPU to render. I estimate a frame to be about 33MB worth of data, and at 180hz, that&apos;s almost 12GB of framebuffer copies every second. When G-SYNC was working, the lower FPS meant that there was less frames being copied so the stuttering was less noticeable.</p><p>Unfortunately, it doesn&apos;t appear you can force Windows to use the dGPU for compositing. I tried adding DWM to the graphics settings and having it to use the 5080, but this had no effect.</p><p>Disabling the iGPU fixed stuttering when compositing was required, which happens when overlays being used. The downside is that you are now unable to use the iGPU for anything else. I hope Windows provides a mechanism to change this in the future.</p><h1 id="multi-plane-overlays">Multi-Plane Overlays</h1><p>Multi-Plane Overlays are a relatively new (2018?) GPU technology that provides a mechanism for OSes and apps to render overlays reaaaaally fast. However, it seems that this can cause problems and visual issues so most of the articles you find about it are about disabling it. However, MPOs seem pretty important to performant overlays, so I&apos;m confused as to why iRacing recommends it to be disabled.</p><p>Interestingly enough, Special K says that DISPLAY1 supports MPO, but Display 2 and 3 don&apos;t support it. I ensured that DISPLAY1 was the center screen, as that&apos;s the screen I want overlays to be performant on.</p><h1 id="g-sync-and-overlays">G-SYNC and Overlays</h1><p>I had some cases where G-SYNC was working (indicator was visible), but something was horribly broken, as iRacing was averaging about 30fps. Turns out what was happening was Nvidia drivers was trying to sync based on both iRacing and SimHub frame updates at the same time, and this resulted in heavy stuttering.</p><p>The fix here is to ensure that the global G-SYNC options are for Fullscreen only, then use NVIDIA Profile Inspector to set iRacing to have G-SYNC support for Fullscreen and Windowed.</p><p>My monitors have an onboard FPS overlay. I used this to verify that G-SYNC was actually working, as the FPS display will fluctuate. </p><h1 id="g-sync-not-working-after-sleepwake">G-SYNC not working after sleep/wake</h1><p>The final hurdle was G-SYNC was not working after a sleep / wake. I found some comments and articles that mentioned some monitors have issues on sleep/wake and G-SYNC state, so I tried disabling deep sleep on my monitors which resolved the issue. Working theory is deep sleep + wake on monitors does not properly restore G-SYNC state.</p><h1 id="v-sync-off">V-Sync off</h1><p>I don&apos;t understand why there are people saying to turn on V-sync in NVIDIA Control Panel but disable it in game. My understanding is that this effectively disables G-SYNC, and when I turn it on, iRacing FPS is clamped to an integer divisor of monitor refresh rate, so in my case [180, 90, 45].</p><h1 id="set-in-game-fps-limit-to-a-smidge-under-your-monitor-refresh-rate">Set in-game FPS limit to a smidge under your monitor refresh rate</h1><p>You may see tearing if your FPS is higher than the monitor refresh rate. I set mine to 175.</p><h1 id="good-luck">Good luck!</h1><p>There could be other factors at play in your setup that could cause issues. </p><p>If this article has helped you, please reference or link to this post.</p>]]></content:encoded></item><item><title><![CDATA[Using an existing DSN key in a fresh Sentry self-hosted instance]]></title><description><![CDATA[<p>TL;DR: It&apos;s possible, and not too hard, but has oddities a restart will address.</p><p>Due to my self-hosted Sentry instance getting into a weird state (I couldn&apos;t get an upgrade to work due not being able to make it past a hard-stop upgrade process), I</p>]]></description><link>https://chen.do/using-an-existing-dsn-key-in-a-fresh-sentry-self-hosted-instance/</link><guid isPermaLink="false">67ecfad508ad470001e62aca</guid><category><![CDATA[protip]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Wed, 02 Apr 2025 09:10:24 GMT</pubDate><content:encoded><![CDATA[<p>TL;DR: It&apos;s possible, and not too hard, but has oddities a restart will address.</p><p>Due to my self-hosted Sentry instance getting into a weird state (I couldn&apos;t get an upgrade to work due not being able to make it past a hard-stop upgrade process), I decided the best option forward was to build a new VM and reinstall a fresh Sentry setup. However, I wanted my existing apps out there to still report in (without updating), which meant that I had to ensure that the DSNs out in the wild would still work.</p><p>Thankfully, this wasn&apos;t too hard. Given that the Postgres database of my current instance wasn&apos;t in the right state, and I didn&apos;t have anything important in the existing install, I decided just ensuring the existing DSN URLs worked was what I wanted, rather than dealing with copying the database over.</p><p>Disclaimer: I don&apos;t work for Sentry, and this might not work for you, and I&apos;m not providing warranty or support of the below.</p><p>This worked for me on Sentry 25.3.0.</p><h2 id="getting-the-data-you-need-from-the-existing-instance">Getting the data you need from the existing instance</h2><p>Get the contents of the <code>sentry_project</code> and <code>sentry_projectkey</code> table: <code>docker compose exec postgres psql -U postgres -c &quot;select * from sentry_project;&quot;</code></p><pre><code>docker compose exec postgres psql -U postgres -c &quot;select * from sentry_project;&quot; &gt; project.txt
docker compose exec postgres psql -U postgres -c &quot;select * from sentry_projectkey;&quot; &gt; projectkey.txt</code></pre><h2 id="set-up-new-sentry-instance">Set up new Sentry instance</h2><p>Follow the instructions provided by Sentry to install this on your fresh VM.</p><h2 id="rebuild-the-projects">Rebuild the projects</h2><p>Now, you&apos;ll need to ensure the project ID and the public key are the same for this to work.</p><p>I used the Sentry interface to create the projects in the right order to ensure the project IDs were correct. However, I made a mistake and had to resort to resetting the sequence with <code>SELECT setval(&apos;sentry_project_id_seq&apos;, &lt;largest current id&gt;);</code> You probably don&apos;t have to do it to <code>sentry_projectkey</code> but I did.</p><h2 id="update-the-keys">Update the keys</h2><p>To update the keys, set the <code>public_key</code> and <code>secret_key</code> (probably don&apos;t need it but I figured it couldn&apos;t hurt) to the values.</p><pre><code>docker compose exec psql -U postgres

UPDATE sentry_projectkey SET public_key = &apos;&lt;public key&gt;&apos;, secret_key = &apos;&lt;secret key&gt;&apos; WHERE project_id = &lt;project id&gt;;</code></pre><h2 id="restart-sentry">Restart Sentry</h2><p>There&apos;s some kind of caching around the <code>public_keys</code>, which makes sense as event ingestion is a hot path, so you&apos;ll need to restart the relevant process. I didn&apos;t know which one it was, so restarting Sentry completely worked for me in the end. I did see it working without restarting after one project but it wasn&apos;t reliable.</p><pre><code class="language-docker">docker compose restart

# nginx sometimes needs a kick
docker compose restart web

# if still no go, complete restart
docker compose stop
docker compose up -d</code></pre><h1 id="thats-it">That&apos;s it!</h1><p>I hope you&apos;ve found this useful. </p><p></p>]]></content:encoded></item><item><title><![CDATA[Fixing Selenium 4.x timeout errors when using multiple sessions]]></title><description><![CDATA[<p>TL;DR: Set <code>SE_NODE_MAX_SESSIONS=&lt;N&gt;</code> and <code>SE_NODE_OVERRIDE_MAX_SESSIONS=true</code> if you&apos;re running into timeout errors when starting a new session on Selenium 4.x.</p><p>I ran into an odd issue while trying to upgrade our Selenium containers to 4.10</p>]]></description><link>https://chen.do/selenium-timeout-multiple-sessions/</link><guid isPermaLink="false">64b8d008323f1c000130052f</guid><category><![CDATA[docker]]></category><category><![CDATA[protip]]></category><category><![CDATA[ruby]]></category><category><![CDATA[selenium]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Thu, 20 Jul 2023 08:33:03 GMT</pubDate><content:encoded><![CDATA[<p>TL;DR: Set <code>SE_NODE_MAX_SESSIONS=&lt;N&gt;</code> and <code>SE_NODE_OVERRIDE_MAX_SESSIONS=true</code> if you&apos;re running into timeout errors when starting a new session on Selenium 4.x.</p><p>I ran into an odd issue while trying to upgrade our Selenium containers to 4.10 from 4.0.1-beta, where our Capybara-based test suite would return <code>Net::ReadTimeout</code> that I narrowed down to tests that would use multiple sessions.</p><p>Turns out a change was made where the Selenium containers would limit themselves to a single session by default, so you&apos;ll need to set <code>SE_NODE_MAX_SESSIONS</code> to an appropriate number for your setup, and also <code>SE_NODE_OVERRIDE_MAX_SESSIONS=true</code>.</p><p>I still had some odd issues, but using <code>seleniarm/standalone-chromium:114.0-20230615</code> worked, even though our CI is still <code>amd64</code>.</p><p>Good luck.</p>]]></content:encoded></item><item><title><![CDATA[Reliable background geotagging for Sony Alpha cameras]]></title><description><![CDATA[I built an app that geotags photos on Sony cameras better than Sony's own apps.]]></description><link>https://chen.do/background-geotagging-for-sony-alpha-cameras/</link><guid isPermaLink="false">64a0e182323f1c00013004dc</guid><category><![CDATA[bluetooth]]></category><category><![CDATA[reverse engineering]]></category><category><![CDATA[sony]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Sun, 02 Jul 2023 02:44:15 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1499591934245-40b55745b905?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fGNhbWVyYSUyMG1hcHxlbnwwfHx8fDE2ODgyNjU4NDd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1499591934245-40b55745b905?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fGNhbWVyYSUyMG1hcHxlbnwwfHx8fDE2ODgyNjU4NDd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Reliable background geotagging for Sony Alpha cameras"><p>TL;DR: I got mad about Sony&apos;s unreliable geotagging apps, reverse engineered the protocol, and built my own: <a href="https://geotagalpha.app/">Geotag Alpha</a>. Get the beta <a href="https://testflight.apple.com/join/0Fht2ff6">here</a>.</p><p>I picked up a Sony a7 IV earlier this year to replace my aging a7R II, and I was initially pleased about the geotagging feature as I tend to shoot most of my photos while travelling, and it&apos;ll be nice to know exactly where I took them.</p><p>However, I quickly noticed that the Sony Creators&apos; app struggled to maintain a connection to my camera, especially after I turned the camera off (to save battery) and turning it back on. Having the app in the foreground worked, but I&apos;m not going to keep their app open in the foreground every time I wanted to connect.</p><p>I got real mad about it, to the point where I nerd-sniped myself the day before I was leaving for a trip to Japand and Taiwan, and set out to figure out how to build a more reliable geotagging solution. With a bunch of reverse engineering and help from other people&apos;s posts who&apos;ve tried to do something similar, I managed to hack together working prototype (where the UI was the default &quot;Hello world&quot; screen) by 11pm that evening.</p><p>I&apos;ve called it <a href="https://geotagalpha.app">Geotag Alpha</a> (currently in <a href="https://testflight.apple.com/join/0Fht2ff6">Testflight</a>), and it improves upon the Creators&apos; app functionality by:</p><ul><li>Properly handling connecting to the camera while in the background</li><li>Better energy efficiency. Location updates are only pushed when it changes or at the interval the camera needs, rather than the 7s internal Sony uses.</li><li>Support for geotagging multiple cameras simultaneously (coming soon!)</li></ul><p>I&apos;ve tested it with my own Sony a7 IV, and have confirmed it works on Sony a7R IV, Sony a7 IIIs so far.</p><p>If you geotag your cameras and you also struggle with the official Sony Creators and Imaging Edge Mobile apps not being reliable, give Geotag Alpha a go!</p><p><a href="https://testflight.apple.com/join/0Fht2ff6">Join the Testflight beta</a> and let me know if it works for you!</p>]]></content:encoded></item><item><title><![CDATA[PMSA003I reporting 0 for all values fix]]></title><description><![CDATA[<p>I have a couple of air quality sensors I&apos;ve made by cobbling various Adafruit boards together (Magtag, Funhouse, ESP32-C3) that report to my Home Assistant instance for monitoring. One of these stopped working, where the PMSA003I appeared to be working (fan spinning, no errors), but was reporting 0</p>]]></description><link>https://chen.do/pmsa003i-reporting-0-for-all-values-fix/</link><guid isPermaLink="false">6402c72b29517c00011382db</guid><category><![CDATA[protip]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Sat, 04 Mar 2023 04:28:33 GMT</pubDate><content:encoded><![CDATA[<p>I have a couple of air quality sensors I&apos;ve made by cobbling various Adafruit boards together (Magtag, Funhouse, ESP32-C3) that report to my Home Assistant instance for monitoring. One of these stopped working, where the PMSA003I appeared to be working (fan spinning, no errors), but was reporting 0 for all values.</p><p>I verified that the main sensor unit was the problem by switching out the StemmaQT board I have to connect it to the Adafruit boards.</p><p>I detactched the unit from the board and gave it a blast of air from a Datavac duster, and it is now working again!</p>]]></content:encoded></item><item><title><![CDATA[Docker in Docker (DIND) MTU fix for docker-compose]]></title><description><![CDATA[<p>If you&apos;re running into weird connection stalling issues when inside a Docker-in-Docker environment, it&apos;s rather likely MTU is the culprit. For example, when basic network connectivity works (ping works, <code>curl example.com</code> works) but <code>curl</code> to a <code>https</code> endpoint stalls at TLS handshake, this is likely</p>]]></description><link>https://chen.do/docker-in-docker-dind-mtu-fix-for-docker-compose/</link><guid isPermaLink="false">63f441e129517c00011382bb</guid><category><![CDATA[docker]]></category><category><![CDATA[networking]]></category><category><![CDATA[protip]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Tue, 21 Feb 2023 04:02:46 GMT</pubDate><content:encoded><![CDATA[<p>If you&apos;re running into weird connection stalling issues when inside a Docker-in-Docker environment, it&apos;s rather likely MTU is the culprit. For example, when basic network connectivity works (ping works, <code>curl example.com</code> works) but <code>curl</code> to a <code>https</code> endpoint stalls at TLS handshake, this is likely due your container unable to receive packets larger than a certain value.</p><p>Normally, the networking stack is able to discover the MTU using ICMP, however some endpoints choose to block ICMP which causes this to not work.</p><p>The solution is to change your container&apos;s MTU option by putting this in your <code>docker-compose.yml</code>:</p><pre><code>networks:
  default: # or whatever your networks are named
    driver: bridge
    driver_opts:
      com.docker.network.driver.mtu: 1450 # You may need to lower this value further</code></pre><p>The <code>--mtu</code> option passed to <code>dockerd</code> only affects the MTU used for pulls/pushes and does not affect containers themselves, which is rather annoying.</p>]]></content:encoded></item><item><title><![CDATA[Controlling LG MusicFlow Soundbars in Home Assistant]]></title><description><![CDATA[Automating LG MusicFlow Soundbars in Home Assistant using mufloctl, tcpdump, and netcat.]]></description><link>https://chen.do/controlling-lg-musicflow-soundbars-in-home-assistant/</link><guid isPermaLink="false">63a9569729517c0001138248</guid><category><![CDATA[home automation]]></category><category><![CDATA[home assistant]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Mon, 26 Dec 2022 10:46:32 GMT</pubDate><content:encoded><![CDATA[<p>I wanted to automate most of the bits of playing a vinyl on my record player, and had successfully automated muting/unmuting of the IKEA Symfonisk speakers in my lounge when the record player was being used (power draw spikes to &gt;1W during playback, and &lt;0.3W on idle), however I wanted to automate switching the input on the LG LA855M sound bar that it&apos;s connected to as well.</p><p>I did some searching and stumbled across <a href="https://github.com/mafredri/musicflow">https://github.com/mafredri/musicflow</a>, which is a CLI tool written in Go. It works well (although the interface is a bit eh), however I did not want to shave the yak that is getting that hooked up to Home Assistant, nor did I have the time to write a proper integration.</p><p>During my searching, I found <a href="https://community.home-assistant.io/t/sending-simple-tcp-packets/52070/2">https://community.home-assistant.io/t/sending-simple-tcp-packets/52070/2</a> where someone dumped the packets from the MusicFlow app and had success with using <code>nc</code> to send payloads.</p><p>I used <code>tcpdump</code> to capture the packets when I sent a command, then used Wireshark to inspect and grab the bytes I needed.</p><pre><code># on machine running mufloctl
sudo tcpdump -vv -XX -w portable.pcap port 9741

# in another shell, send the payload
echo &apos;{&quot;data&quot;:{&quot;type&quot;:2},&quot;msg&quot;:&quot;FUNCTION_SET&quot;}&apos; | mufloctl -addr &lt;IP of soundbar&gt;

# ^C to end the tcpdump</code></pre><p>Once I had the bytes, I wrote a quick script to convert the bytes to <code>\xXX</code> format, so I can use <code>echo</code> and <code>nc</code> to send to the soundbar.</p><pre><code># Switch to optical source

echo &quot;\x10\x00\x00\x00\x30\xec\x01\x16\x5d\x79\x2c\xfc\xcd\x89\x02\x77\x39\x2f\x3a\x33\x5f\xb3\xd5\x76\x24\xad\x84\x59\x71\xa1\x8b\xcd\xbe\xd6\xae\x0f\xfa\x14\x9e\x24\xac\x48\x6a\x4f\x18\xc9\xf5\xed\x0b\x45\xd8\x4b\x85&quot; | nc -vv &lt;ip&gt; 9741


# Switch to portable source

echo &quot;\x10\x00\x00\x00\x30\xec\x01\x16\x5d\x79\x2c\xfc\xcd\x89\x02\x77\x39\x2f\x3a\x33\x5f\xac\xcb\x2f\xb1\xeb\x99\xf6\xa4\x47\xb1\xa8\x0a\x01\x13\x64\x0d\x1d\x6f\xe7\xf5\x94\x5e\xec\xc9\x2b\x47\xda\x1d\xc4\x4e\xc8\x6e&quot; | nc -vv &lt;ip&gt; 9741</code></pre><p>I was rather stoked that this actually worked! I skimmed the <code>musicflow</code> source code and it appears messages are JSON payloads which are AES encrypted with a static key and IV, which makes this possible.</p><p>Unfortunately, Home Assistant does not let you use pipes in <code>shell_command</code>, so you will need to create a script that does so, as per <a href="https://community.home-assistant.io/t/sending-simple-tcp-packets/52070/2">https://community.home-assistant.io/t/sending-simple-tcp-packets/52070/2</a>.</p><p>Hope this helps.</p><p></p>]]></content:encoded></item><item><title><![CDATA[SwiftUI @State/@Binding objects not updating in Release configuration]]></title><description><![CDATA[<p>TL;DR: Reflection Metadata Level set to &quot;Off&quot; (potentially a default from older Xcode projects) can cause <code>@State/@Binding</code> to have issues around getting the variable to update.</p><p>After banging my head against the wall for a couple of hours, I managed to luck out on an issue</p>]]></description><link>https://chen.do/swift-state-binding-objects-not-updating-in-release-configuration/</link><guid isPermaLink="false">622dccb3bffe29000112b831</guid><category><![CDATA[swift]]></category><category><![CDATA[debugging]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Sun, 13 Mar 2022 11:08:12 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1621252179027-94459d278660?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGFuZ3J5JTIwY29tcHV0ZXJ8ZW58MHx8fHwxNjQ3MTY5NjQ1&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1621252179027-94459d278660?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGFuZ3J5JTIwY29tcHV0ZXJ8ZW58MHx8fHwxNjQ3MTY5NjQ1&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="SwiftUI @State/@Binding objects not updating in Release configuration"><p>TL;DR: Reflection Metadata Level set to &quot;Off&quot; (potentially a default from older Xcode projects) can cause <code>@State/@Binding</code> to have issues around getting the variable to update.</p><p>After banging my head against the wall for a couple of hours, I managed to luck out on an issue that seemed extremely fucking weird and I had much hate for SwiftUI until I figured out the problem.</p><p>I was in the middle of building a release to test update mechanism on Shortcat when I noticed the onboarding screen&apos;s <code>Next</code> button would not appear to do anything.</p><p>The root symptom was the <code>step</code> binding was somehow not being set to <code>.second</code>, and was stuck in <code>.first</code>, confirmed by print statements (trusted tool) before and after:</p><!--kg-card-begin: markdown--><pre><code>Button(action: {
    withAnimation {
        print(&quot;before: \(step.rawValue)&quot;)
        step = .second
        print(&quot;after: \(step.rawValue)&quot;)
    }
}) {
</code></pre>
<!--kg-card-end: markdown--><p>This resulted in:</p><!--kg-card-begin: markdown--><pre><code>before: first
after: first</code></pre>
<!--kg-card-end: markdown--><p>Which itself is weird as fuck, but what&apos;s weirder is it was only happening when being built for Release. For me, if a build setting is causing code to somehow have read-only variables, something is seriously cooked.</p><p>I cleaned my build folder, updated Xcode, made a new project and just put the relevant views into a fresh project, set it to Release, and... it worked fine.</p><p>What the fuck?</p><p>I tried cutting bits of code out that wasn&apos;t in the fresh project (dependencies etc) and nothing.</p><p>I had to resort to looking at seeing what build options were different between Development and Release, and eventually happened to change the <code>Reflection Metadata Level</code> option to <code>All</code>... and it worked!?</p><p>A quick search for <code>swift reflection metadata level</code>, I spotted a link that said &quot;<a href="https://stackoverflow.com/questions/61738376/swiftui-state-not-updated-in-older-project/68563396#68563396">SwiftUI state not updated in older project</a>&quot; and I figured that absolutely must be it.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2022/03/image.png" class="kg-image" alt="SwiftUI @State/@Binding objects not updating in Release configuration" loading="lazy"><figcaption>A Google search result that had the link to a StackOverflow article.</figcaption></figure><p>The question was 1 year and 10 months old (at time of writing). I gave the relevant answer and upvote, and figured I&apos;d write a post about it so hopefully someone using the keywords I used will also discover the answer one day.</p>]]></content:encoded></item><item><title><![CDATA[Fixing Service Topology with nginx-ingress]]></title><description><![CDATA[Trying to use Service Topology but nginx-ingress isn't respecting your topology? Turns out it's just an annotation missing on Ingress.]]></description><link>https://chen.do/using-service-topology-to-route-to-nearest-pod-in-kubernetes/</link><guid isPermaLink="false">60b0519c174f28000136b73b</guid><category><![CDATA[kubernetes]]></category><category><![CDATA[nginx]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Fri, 28 May 2021 02:21:36 GMT</pubDate><content:encoded><![CDATA[<p><strong>TL;DR:</strong> Service Topology wasn&apos;t working with nginx-ingress cause the <code>Ingress</code> needs <code>nginx.ingress.kubernetes.io/service-upstream=true</code>.</p><p>In order to ensure that our users have their requests serviced as fast as possible, we&apos;re moving towards a multi-region deployment where users are serviced by the nearest workloads.</p><p>Kubernetes introduced the concept of <a href="https://stackoverflow.com/questions/63399080/kubernetes-1-18-6-servicetopology-and-ingress-support">Service Topology routing</a> in 1.17 which enables Services to be able to define how they should be routed. My initial testing indicate that Service Topology worked as expected when using the Cluster IP of the Service, however <code>nginx-ingress</code> appeared to ignore Service Topology and would do its usual round robin behaviour.</p><p>I stumbled across a <a href="https://stackoverflow.com/questions/63399080/kubernetes-1-18-6-servicetopology-and-ingress-support">StackOverflow</a> thread which explained that <code>nginx-ingress</code> looks at the <code>Endpoint</code> structures by default and thus bypasses the Service Topology mechanism provided by Service.</p><p>The magical fix for this was to add an annotation to the <code>Ingress</code> in question: <code>nginx.ingress.kubernetes.io/service-upstream=true</code>.</p><p>This was all that was needed for Service Topology to work.</p>]]></content:encoded></item><item><title><![CDATA[Fixing the bugged elevator for Nocturne OP55N1 in Cyberpunk 2077]]></title><description><![CDATA[Can't get into the elevator to see Hanako for Nocturne OP55N1 in Cyberpunk 2077? This is how I fixed it for a friend.]]></description><link>https://chen.do/cyberpunk-2077-bugged-elevator-nocturne-op55n1/</link><guid isPermaLink="false">5ff461b5174f28000136b64c</guid><category><![CDATA[video games]]></category><category><![CDATA[modding]]></category><category><![CDATA[workaround]]></category><category><![CDATA[nerd snipe]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Tue, 05 Jan 2021 13:52:43 GMT</pubDate><media:content url="https://chen.do/content/images/2021/01/Screenshot-2021-01-06-005613.png" medium="image"/><content:encoded><![CDATA[<img src="https://chen.do/content/images/2021/01/Screenshot-2021-01-06-005613.png" alt="Fixing the bugged elevator for Nocturne OP55N1 in Cyberpunk 2077"><p>This post describes how I managed to fix a friend&apos;s save where he was trying meet Hanako at Embers, but the elevator/lift that&apos;s being guarded by two bouncers is marked &quot;Off&quot; and he couldn&apos;t enter it. I&apos;ve heard of another issue where the lift is open, but the bouncer is stopping you from entering. I&apos;m not sure if this helps with that issue.</p><p><strong>Please note that I am not providing support for this issue, and I am not responsible for any damage this can cause to your save game/computer/etc.</strong> Check out the <a href="https://discord.com/invite/Epkq79kd96">CP77 Modding Tools Discord</a> if you need help.</p><p>You need a recent <a href="https://github.com/yamashi/CyberEngineTweaks">CyberEngineTweaks</a> installed and working (see other resources for how to install as that&apos;s beyond the scope of this post), as this workaround requires poking at game state in the console that CET provides. Instructions tested below on v1.06 of the game and a CET build newer than the 5th of Jan, 2021.</p><ol><li>Go to the Embers lift with the two bouncers, and trigger the Point Of No Return dialog by walking up to the lift.</li><li>Open the CET Console with tilde (`) or whatever it is on your keyboard</li><li>Teleport inside the lift with: <code>Game.TeleportPlayerToPosition(-1794.316040,-535.862915,10.11386)</code></li><li>Look at the elevator control panel</li><li>Run <code>ts = Game.GetTargetingSystem(); lift = ts:GetLookAtObject(Game.GetPlayer(),false,false):GetDevicePS(); print(lift:GetDeviceState())</code></li><li>The console should display <code>EDeviceStatus : (4294967295)</code></li><li>Look away from elevator control panel</li><li>Run <code>lift:SetDeviceState(1)</code></li><li>Look at control panel, and it should update and allow you to go to the Embers floor.</li></ol><h1 id="things-that-didn-t-work">Things that didn&apos;t work</h1><ul><li>Thanks to <code>@SirBitesalot</code>&apos;s fact dump, I found a <code>q115_embers_elevator_unlocked</code> flag which I was certain that was it cause it was not set in friend&apos;s file. However, this didn&apos;t fix the issue.</li><li>Teleporting into Embers itself: NPCs were there, but do not react. I believe there&apos;s a trigger when the elevator doors open.</li><li>Gradually teleporting up the lift well: This does trigger a conversation with Johnny, but doesn&apos;t seem to progress further, even teleporting into Embers after that.</li></ul><p>Thanks to the modding community for making this remotely possible. No thanks to C++ which still doesn&apos;t have a nice way to join an array of strings with a delimiter in its standard library.</p><h1></h1>]]></content:encoded></item><item><title><![CDATA[Mitigating Octoprint print quality issues with BufferBuddy]]></title><description><![CDATA[<p>This is a continuation of my deep-dive into <a href="https://chen.do/diagnosing-reduced-print-quality-with-octoprint/">understanding print quality issues when printing over USB with Octoprint</a>, and <a href="https://chen.do/adding-buffer-monitoring-to-marlin/">adding buffer monitoring to Marlin</a>.</p><p>Now that I had an objective mechanism to measure planner underruns, which we know is the likely cause of print quality issues, we can attempt to</p>]]></description><link>https://chen.do/mitigating-print-quality-issues-with-bufferbuddy/</link><guid isPermaLink="false">5f94b7b0174f28000136b3f7</guid><category><![CDATA[3d printing]]></category><category><![CDATA[marlin]]></category><category><![CDATA[Octoprint]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Tue, 27 Oct 2020 01:03:53 GMT</pubDate><media:content url="https://chen.do/content/images/2020/10/image-11-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://chen.do/content/images/2020/10/image-11-1.png" alt="Mitigating Octoprint print quality issues with BufferBuddy"><p>This is a continuation of my deep-dive into <a href="https://chen.do/diagnosing-reduced-print-quality-with-octoprint/">understanding print quality issues when printing over USB with Octoprint</a>, and <a href="https://chen.do/adding-buffer-monitoring-to-marlin/">adding buffer monitoring to Marlin</a>.</p><p>Now that I had an objective mechanism to measure planner underruns, which we know is the likely cause of print quality issues, we can attempt to mitigate the issue.</p><p>My previous investigation with Octoprint&apos;s <code>comm.py</code> which manages the communication with the printer indicated that the default behaviour is to wait for an <code>ok</code> from the printer before sending the next command, which means that the command buffer is usually empty by the time the next command reaches the printer.</p><p>Generally, the planner buffer is usually kept full and does not usually underrun for the scenarios I have tested (apart from curves on Cura 4.7.1), however any potential delay which could be introduced by e.g. CPU load, resends (noise on USB cable) &#x2013; can cause planner buffer underruns.</p><h1 id="making-octoprint-send-multiple-commands-inflight">Making Octoprint send multiple commands inflight</h1><p>The core algorithm to keep the command buffers full is as follows:</p><ul><li>Check if the printer is reporting available capacity in the command buffer</li><li>Trigger Octoprint to send more commands</li></ul><p>So at the minimum, we need a way to detect the available capacity in the command buffers, and a mechanism to trigger Octoprint to send more commands. We know we can use Marlin&apos;s <code>ADVANCED_OK</code> output for undersanding the available buffer capacities, we need to figure out a mechanism to trigger sends.</p><p>Octoprint&apos;s core logic for sending commands is inside <code>comm.py</code>&apos;s <code>_send_loop</code>, which is running in a thread, and checks for <code>_send_queue</code> to have something to send, and once command has sent, waits for <code>_clear_to_send.wait()</code> which is a <code>CountedEvent</code> / mutex mechanism that lets other threads tell the thread that it&apos;s cool to send another command.</p><p><code>_clear_to_send.set()</code> in another thread is ultimately what causes the next command to be sent, so it looked like a good mechanism to start.</p><p>I wanted to add this buffer-filling functionality as a plugin because I feel a tad uncomfortable with introducing significant changes to <code>comm.py</code>, so I began with a rough naive plugin that inspects the <code>ADVANCED_OK</code> output and calls <code>_clear_to_send.set()</code> if there&apos;s capacity.</p><p>Turns out, this was too naive as the plugin would react to responses that do not reflect the current state of the buffers, and it doesn&apos;t know which lines it triggered, so it would cascade into serial buffer overruns extremely quickly. I also discovered the <code>ok</code> buffer size that determines the maximum <code>_clear_to_send</code> can buffer needs to be at least 2, otherwise calling <code>_clear_to_send.set()</code> won&apos;t do anything if <code>_clear_to_send</code> is already at 1.</p><p>My next attempt would keep track the number of commands inflight and use this as a basis to determine whether or not <code>_clear_to_send.set()</code> should be called, as added a minimum delay between triggering a send, and this worked pretty well considering the amount of code required:</p><pre><code class="language-Python">ADVANCED_OK = re.compile(r&quot;ok (N(?P&lt;line&gt;\d+) )?P(?P&lt;planner_buffer_avail&gt;\d+) B(?P&lt;cmd_buffer_avail&gt;\d+)&quot;)
LINE_NUMBER = re.compile(r&quot;N(?P&lt;line&gt;\d+) &quot;)

class BufferMonitorPlugin(octoprint.plugin.StartupPlugin):
    # ok buffer must be above 1
    def __init__(self):
        self.bufsize = 4
        self.max_inflight = self.bufsize
        self.last_cts = time.time()
        self.last_sent_line_number = 0

    def on_after_startup(self):
        self._logger.info(&quot;Hello World!&quot;)

    def gcode_sent(self, comm, phase, cmd, cmd_type, gcode, *args, **kwargs):
        self.last_sent_line_number = comm._current_line    
            
    def gcode_received(self, comm, line, *args, **kwargs):
        if &quot;ok &quot; in line:
            matches = ADVANCED_OK.search(line)

            if matches.group(&apos;line&apos;) is None:
                return line

            line_no = int(matches.group(&apos;line&apos;))
            buffer_avail = int(matches.group(&apos;cmd_buffer_avail&apos;))
            inflight = self.last_sent_line_number - line_no

            if inflight &gt; self.max_inflight:
                # too much in flight, scale it back a bit
                comm._clear_to_send.clear()
                self._logger.info(&quot;too much inflight, chill a bit&quot;)
                self._logger.info(&quot;Buffer avail: {} inflight: {} cts: {}&quot;.format(buffer_avail, inflight, comm._clear_to_send._counter))

            if buffer_avail &gt;= 1 and (time.time() - self.last_cts) &gt; 0.5 and inflight &lt; self.max_inflight:
                self._logger.info(&quot;sending more&quot;)
                queue_size = comm._send_queue._qsize()
                self._logger.info(&quot;Buffer avail: {} inflight: {} cts: {} queue: {}&quot;.format(buffer_avail, inflight, comm._clear_to_send._counter, queue_size))
                self.last_cts = time.time()
                comm._clear_to_send.set()

        return line
</code></pre><p>I was able to confirm with buffer monitoring via <code>M576</code> that it fills the buffers and decreases underruns. Graphs further below.</p><p>Note: I discovered that my Ender 3 v2 takes at minimum 9ms to respond to a command, where the timing was measured by inspecting the <code>serial.log</code> of Octoprint. This means that the default behaviour of waiting for an <code>ok</code> to send the next command was capped at ~111 commands a second, and I know that my Cura 4.7.1 sliced 3DBenchy will easily spike to 160 commands a second, which will most definitely cause underruns.</p><h1 id="making-a-plugin-in-octoprint">Making a plugin in Octoprint</h1><p>Now that the core idea has proven to be workable, I began refining the plugin logic and testing with both dry-run prints and real prints to determine reliability.</p><p>I followed the <a href="https://docs.octoprint.org/en/master/plugins/gettingstarted.html">Octoprint plugin guide</a> and developed the plugin against a Docker container on my local machine for speed, but ran into different behaviour with the Virtual Printer that comes with Octoprint, so I could only really test the plugin against my Ender 3 v2 on my Octopi setup.</p><p>I called the plugin &quot;<a href="https://github.com/chendo/BufferBuddy">BufferBuddy</a>&quot; after some deliberation cause the working title &quot;buffer-filler.py&quot; sounded a bit shit.</p><p>I ran into issues getting the plugin to load initially which eventually turned out to be a Python version specification issue. Once this was sorted, I was able to copy across the core logic from my prototype and began fleshing it out.</p><p>Understanding how to implement a UI took probably three times as long as implementing the core logic, which was hampered by needing to restart Octoprint to see changes. However, I eventually managed to make the UI behave the way I wanted!</p><h1 id="introducing-bufferbuddy">Introducing BufferBuddy</h1><p>It&apos;s probably easiest to explain the impact of the plugin with graphs.</p><p>The graphs below are from graphing <code>M576</code> output during a print of a 50% scale 3DBenchy sliced using Cura 4.6.2 and 4.7.1, and with BufferBuddy enabled/disabled, with the leading underrun artifacts caused by the purge line removed for clarity.</p><p>My printer is an Ender 3 v2 running Smith3D&apos;s Marlin fork which has improvements for the Ender 3 v2&apos;s LCD, with my own patches for M576, with <code>BUFSIZE=16, BLOCK_BUFFER_SIZE=16, USART_RX_BUF_SIZE=64, USART_TX_BUF_SIZE=64</code>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-7.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>Cura 4.6.2, BufferBuddy disabled</figcaption></figure><p>The graphs indicate that we consistently see command buffer underruns, but only see ~6 instances of planner buffer underruns, where the maximum detected period that the planner buffer was empty was under 50ms.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-8.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>Cura 4.6.2, BuffyBuddy enabled</figcaption></figure><p>With BufferBuddy enabled, command buffer underruns are mostly eliminated, with 13 instances of command underruns during print, and one planner buffer underrun with a max underrun period of 9ms.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-11.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>BufferBuddy output for Cura 4.6.2 print. This includes underruns from the starting gcode, which I&apos;m not sure how to best remove from the above statistics.</figcaption></figure><p>For Cura 4.7.1, which we know produces problematic gcode, it&apos;s another story.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-9.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>Cura 4.7.1, BufferBuddy disabled</figcaption></figure><p>It shows severe planner underruns, with delays easily going above 75ms. Commands per second easily surpasses 150 per second, which is above the max throughput of ~111 per second which we calculated by measuring command/ack latency above.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-10.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>Cura 4.7.1, BufferBuddy enabled</figcaption></figure><p>BufferBuddy eliminates most of the planner buffer underruns, but there is still command buffer underruns due to the sheer gcode density, although it halves the maximum delay the command buffers remain empty.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-6.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>BufferBuddy output for Cura 4.7.1 print. This includes underruns from the starting gcode, which I&apos;m not sure how to best remove from the above statistics.</figcaption></figure><p>Uploading to SD appears to behave differently with respect to having multiple lines inflight for more throughput, as the command buffer never gets filled, and it seems to be more dependent on serial RX buffer size which we can&apos;t easily detect, so this needs more work.</p><h2 id="actual-print-quality">Actual Print Quality</h2><p>So, now we know that we&apos;ve significantly mitigated planner underruns with BufferBuddy, we can now see if we&apos;ve resolved print quality issues with Cura 4.7.1 on an actual print.</p><p>Turns out Octoprint can detect when I print from SD from the Ender 3&apos;s interface, so I was able to get a &quot;control&quot; print with best case scenario for buffer filling.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-12.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>Cura 4.7.1, printing off SD card, with Commands Processed added in cause otherwise it would be a pretty boring and flat graph.</figcaption></figure><p>Printing from SD showed zero planner and command underruns during the print (tiny smidge at the end probably due to built-in print completion commands), but commands per second peaking above 300 shows that it&apos;s going to be extremely hard to keep buffers filled due to latency when it&apos;s super dense gcode.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-13.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>Cura 4.7.1 sliced 3DBenchy at 50%, printed over USB with BufferBuddy active</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-14.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>Cura 4.7.1 sliced 3DBenchy at 50%, printed directly off SD card</figcaption></figure><p>Printing of SD is a little bit better in the middle, but still exhibits over-extrusion on curves. The motors seem to make odd sounds during these curves, which is likely related.</p><p>For comparison, this is a 3DBenchy from Cura 4.6.2:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-15.png" class="kg-image" alt="Mitigating Octoprint print quality issues with BufferBuddy" loading="lazy"><figcaption>Cura 4.6.2 sliced 3DBenchy at 50%, printed over USB with BufferBuddy active.</figcaption></figure><p>Putting aside that iPhone cameras aren&apos;t amazing at capturing the detail I want (handheld, at least), the Cura 4.6.2 sliced Benchy looks pretty good by comparison still.</p><h1 id="summary-so-far">Summary.. so far</h1><p>So, it looks like BufferBuddy doesn&apos;t fully address the Cura 4.7.1 problem, but it still mitigates against potential planner underruns by keeping the command buffers full, which still should help against the occasional blip of load on lower-powered devices.</p><p>Want to check out the plugin? It&apos;s on my <a href="https://github.com/chendo/BufferBuddy">Github</a>, but it&apos;s still <strong>considered experimental and may cause your printer to lock up.</strong></p>]]></content:encoded></item><item><title><![CDATA[Adding buffer monitoring to Marlin]]></title><description><![CDATA[<p>This is a sequel of my post about <a href="https://chen.do/diagnosing-reduced-print-quality-with-octoprint/">diagnosing 3D print quality when printing with Octoprint.</a></p><p>Now that we have a working theory for the reduced print quality, the next step was to know for sure when the problem was occurring. If I could have the printer tell me when</p>]]></description><link>https://chen.do/adding-buffer-monitoring-to-marlin/</link><guid isPermaLink="false">5f8163c80d35f400018e061c</guid><category><![CDATA[3d printing]]></category><category><![CDATA[marlin]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Sat, 10 Oct 2020 09:53:31 GMT</pubDate><media:content url="https://chen.do/content/images/2020/10/index.png" medium="image"/><content:encoded><![CDATA[<img src="https://chen.do/content/images/2020/10/index.png" alt="Adding buffer monitoring to Marlin"><p>This is a sequel of my post about <a href="https://chen.do/diagnosing-reduced-print-quality-with-octoprint/">diagnosing 3D print quality when printing with Octoprint.</a></p><p>Now that we have a working theory for the reduced print quality, the next step was to know for sure when the problem was occurring. If I could have the printer tell me when it has no more instructions, it would be the first step to be able to objective measure how often the issue was occurring.</p><p>I dived into the source code for Marlin to begin understanding where I can hook into the relevant events, and what kind of metrics I could easily extract.</p><p>I consider myself mediocre with C/C++, and it&apos;s extremely rusty at best, and embedded C/C++ has its own shenanigans, so I may be wrong about how stuff works.</p><h1 id="understanding-what-to-change">Understanding what to change</h1><p>Marlin&apos;s core loop is as follows:</p><pre><code>/**
 * The main Marlin program loop
 *
 *  - Call idle() to handle all tasks between G-code commands
 *      Note that no G-codes from the queue can be executed during idle()
 *      but many G-codes can be called directly anytime like macros.
 *  - Check whether SD card auto-start is needed now.
 *  - Check whether SD print finishing is needed now.
 *  - Run one G-code command from the immediate or main command queue
 *    and open up one space. Commands in the main queue may come from sd
 *    card, host, or by direct injection. The queue will continue to fill
 *    as long as idle() or manage_inactivity() are being called.
 */
</code></pre><p>I traced <code>BUFSIZE</code> to <code>queue.cpp</code>, where the logic for reading off serial comms and the command queue is handled. This is how I think it works:</p><ul><li><code>idle()</code> (this seems poorly named?), calls <code>manage_inactivity()</code>, which in turns calls <code>queue.get_available_commands()</code> if there&apos;s enough room in <code>GCodeQueue::command_buffer</code> which has max <code>BUFSIZE</code> elements.</li><li><code>queue.get_available_commands()</code> pulls data from serial / SD card, performs basic parsing, checksum validation, early handling</li><li>If it&apos;s all good and it doesn&apos;t need to handle it early, it chucks it in the <code>command_buffer</code> ring buffer via <code>_enqueue</code>, with <code>say_ok</code> flag for that index set to <code>true</code>.</li><li>The <code>say_ok</code> flag is set in another array and not immediately sent to the host, which is not what I expected.</li><li>Other core tasks are run, such as timer checks, UI, auto-reporting, etc</li><li>Finally, <code>queue.advance()</code> is called, which invokes <code>process_next_command()</code>, which parses and executes the command</li><li><code>process_parsed_command()</code> is a behemoth of a switch statement, which figures out what function to run based on the gcode</li><li>Once it runs the relevant function, by default, it will call <code>queue.ok_to_send()</code>, which then sends the <code>ok</code> back to the host that Octoprint is waiting for.</li></ul><p>While trying to understand how the core loops worked, I spotted the <code>ADVANCED_OK</code> block that exposes the planner and command buffer capacity, which were <code>planner.moves_free()</code> and <code>BUFSIZE - [queue.]length</code>, which is a great start to report.</p><p>The problem with understanding buffer underruns with the <code>ADVANCED_OK</code> report, is it can only report the state of those buffers when the <code>ADVANCED_OK</code> is sent. We can infer if it returns <code>B(BUFSIZE - 1)</code>that the command buffer was empty before we sent the command, but we don&apos;t have much other information from this.</p><p>I considered adding more instrumentation to the <code>ADVANCED_OK</code> response, however it would increase serial comm load on both ends cause it&apos;s called on every command, so I figured I needed to add my own gcode that can report the data I wanted, optionally on an interval.</p><h1 id="implementing-a-buffer-monitoring-gcode">Implementing a buffer monitoring gcode</h1><p>I looked at the existing gcode list to see if there was anything similar to what I wanted to expose already, but there didn&apos;t appear to any I could easily extend. I decided to use <code>M576</code>, as <code>M575</code> was &quot;Set baud rate&quot;, which was somewhat relevant to buffer monitoring.</p><p>I used the auto temperature reporting module as a base, and hooked into <code>GCodeQueue::advance()</code> for my logic. A few iterations (which annoyingly requires me to flash via microSD), I had a working <code>M576</code> command that returns <code>M576 P&lt;nn&gt; B&lt;nn&gt; U&lt;nn&gt;</code>, where:</p><ul><li><code>P</code> is planner buffer available </li><li><code>B</code> is command buffer available (both from <code>ADVANCED_OK</code>)</li><li><code>U</code> is number of command buffer underruns since last report</li></ul><p>It also supported <code>M576 S&lt;n&gt;</code> where <code>n</code> is the number of seconds between automatic reports.</p><h1 id="testing-the-concept">Testing the concept</h1><p>I ran it through a dry-run version of a half-scale 3DBenchy gcode, where all the extrusion instructions were stripped out, and combined with a simple Octoprint plugin I had hacked together, I was able to observe the output of my newly minted gcode command!</p><p>Initial findings showed that there was many underruns when printing through Octoprint, anywhere from 5-30 per second. I realised it&apos;s not the number of buffer underruns that would be the issue, but how long the queue was in an empty state.</p><h1 id="iterating-and-adding-more-metrics">Iterating and adding more metrics</h1><p>I added <code>M&lt;nn&gt;</code> to represent the maximum time in milliseconds that the command buffer was empty between commands. This resulted in much more useful information regarding on how long Marlin may be waiting for a command.</p><p>However, I noticed when the max buffer empty time was as high as 100ms, the planner buffer generally remained full, which would mean that the printer would still have movement queued and thus not actually manifest in stalled motion.</p><p>I decided to also add planner buffer underrun metrics. This was more difficult to try to figure out where to make the change, as ideally we would detect if the queue was empty immediately after it was to be processed, however this logic was in a dedicated stepper ISR (some kind of interrupt handler) and it seemed like a bad idea to modify that, so I settled for hooking it into the <code>auto_report_buffer_statistics</code> that runs in a fairly tight loop.</p><p>I changed the output to the following:</p><pre><code> * When called, printer emits the following output:
 * &quot;M576 P&lt;nn&gt; B&lt;nn&gt; PU&lt;nn&gt; PD&lt;nn&gt; BU&lt;nn&gt; BD&lt;nn&gt;&quot;
 * Where:
 *   P: Planner buffers available 
 *   B: Command buffers available
 *   PU: Planner buffer underruns since last report
 *   PD: Maximum time in ms planner buffer was empty since last report
 *   BU: Command buffer underruns since last report
 *   BD: Maximum time in ms command buffer was empty since last report
</code></pre><p>Now we can tell when and how long the motion planner buffer was empty for!</p><h1 id="testing-methodology">Testing methodology</h1><p>To compare, I sliced a half-scale 3DBenchy with the same custom profile in both Cura 4.6.2 and Cura 4.7.1, as I know from personal experience that Cura 4.7 <a href="https://github.com/Ultimaker/Cura/issues/8321">introduced a bug</a> where it generates extremely dense gcode around curves. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image.png" class="kg-image" alt="Adding buffer monitoring to Marlin" loading="lazy"><figcaption>Gcode size comparison between Cura 4.6.2 and 4.7.1.</figcaption></figure><p>Cura 4.7.1 generates as much as double(!!) the amount of gcode for the same model and settings, which definitely will cause issues if Octoprint is unable to stream the gcode to the printer to be printed at the speed that it was designed to be printed at.</p><p>I ran it through my <code>dry-run.rb</code> and &quot;printed&quot; these with <code>M576</code> reporting every 2 seconds and chucked it into a log file for later processing. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-1.png" class="kg-image" alt="Adding buffer monitoring to Marlin" loading="lazy"><figcaption>Some output from the Cura 4.7.1 gcode.</figcaption></figure><p>I processed the logs into CSV and chucked it into Numbers to visualise the difference.</p><h1 id="the-results">The results</h1><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-2.png" class="kg-image" alt="Adding buffer monitoring to Marlin" loading="lazy"><figcaption>Cura 4.6.2</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-3.png" class="kg-image" alt="Adding buffer monitoring to Marlin" loading="lazy"><figcaption>Cura 4.7.1</figcaption></figure><p>The key metric here is <code>Planner Max Empty time</code>, represented in red and is in milliseconds, followed by <code>Planner Underruns</code> in yellow as a count.</p><p>In Cura 4.6.2, we see only 9 instances of planner buffer underruns, and the max time the buffer was empty was 36ms.</p><p>However, 4.7.1 is a whole &apos;nother story, with 200+ planner buffer underruns.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chen.do/content/images/2020/10/image-5.png" class="kg-image" alt="Adding buffer monitoring to Marlin" loading="lazy"><figcaption>Histogram of planner max empty time, with zero values removed.</figcaption></figure><p>I&apos;m not sure of the threshold where the motion planner buffer being empty results in visible print artifacts, but I feel like anything over 50ms is noticeable. Theoretically it should be able to test this by injecting pauses but that&apos;s for another time.</p><h1 id="next-steps">Next steps</h1><p>Now that we have the ability to have some hard data when the motion planner buffer underruns, we can now attempt to address the issue and measure if it helped or not.</p><p>I&apos;ll be opening a pull request for the <code>M576</code> gcode into Marlin.</p><p>Stay tuned for the next one!</p>]]></content:encoded></item><item><title><![CDATA[Diagnosing reduced 3D print quality when printing with Octoprint]]></title><description><![CDATA[<p>Update: See my post on <a href="https://chen.do/adding-buffer-monitoring-to-marlin/">Adding buffer monitoring to Marlin</a>.</p><p>I recently picked up a <a href="https://www.creality3dofficial.com/products/ender-3-v2-3d-printer">Creality Ender 3 v2</a> to finally check out 3D printing. Initially, I was very impressed with the quality of my prints for a budget printer, however, at some point during tinkering, I noticed zits on</p>]]></description><link>https://chen.do/diagnosing-reduced-print-quality-with-octoprint/</link><guid isPermaLink="false">5f6c5d5c0d35f400018e04b6</guid><category><![CDATA[3d printing]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Fri, 09 Oct 2020 12:21:06 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1597765654525-5cb60d312ef6?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1597765654525-5cb60d312ef6?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Diagnosing reduced 3D print quality when printing with Octoprint"><p>Update: See my post on <a href="https://chen.do/adding-buffer-monitoring-to-marlin/">Adding buffer monitoring to Marlin</a>.</p><p>I recently picked up a <a href="https://www.creality3dofficial.com/products/ender-3-v2-3d-printer">Creality Ender 3 v2</a> to finally check out 3D printing. Initially, I was very impressed with the quality of my prints for a budget printer, however, at some point during tinkering, I noticed zits on my prints, specifically around curves.</p><p>I eventually tracked this down to a combination of printing over USB in <a href="https://octoprint.org">Octoprint</a> vs printing from SD card, as well as switching from Creality&apos;s slicer (Cura 4.2) to Cura 4.7.1.</p><p>A bug (<a href="https://github.com/Ultimaker/Cura/issues/8321">#8321</a>) was introduced in Cura 4.7 where it was adding loads of tiny segments in curves, which in turn generates way more gcode for the same curve. This, combined with how Octoprint streams the gcode over the USB serial connection to the printer, results in the printer&apos;s buffers to empty, thus causing brief pauses as it waits for more gcode, which manifests in zits on the surface of prints as the pressure in the nozzle still causes extrusion to occur.</p><p>Even though printing via SD card is likely to resolve the issue, it removes a lot of the convenience and power of printing with Octoprint, such as the ability to selectively cancel certain regions of your print, saving time and reducing waste. Cura&apos;s segment issue is likely to be addressed at some point, which will decrease the likelihood of reduced print quality due to less gcode, however the issue with Octoprint&apos;s gcode streaming still exists, and has been <a href="https://github.com/OctoPrint/OctoPrint/issues/450">reported since 2014</a>.</p><h1 id="what-causes-print-artifacts-when-printing-over-usb">What causes print artifacts when printing over USB?</h1><p>There are a couple of factors at play that can affect the streaming of gcode to the printer when using Octoprint, especially on embedded-class devices like Raspberry Pis.</p><ul><li>CPU load: If the process in Octoprint that&apos;s responsible for streaming gcode doesn&apos;t get to run due to load on the system, then the printer is likely to be waiting for additional instructions, causing movement stutter and print artifacts. This can occur when <a href="https://github.com/OctoPrint/OctoPrint/issues/1241">just loading the Octoprint interface</a>. There is a reason why stuff like plugin management and timelapse processing is prevented while a print is in progress.</li><li>USB cable: I was using a longer cable I had lying around which occasionally would have disconnect issues, which was extremely noticable when I experimented with <a href="https://www.klipper3d.org/">Klipper</a> (more below). Switching to a shorter cable resolved this particular issue.</li><li>Communication speed: It&apos;s possible that the gcode is simply too dense for it to be communicated over the USB/serial connection at a rate that it&apos;s needed to be processed by the printer to perform the print as desired.</li></ul><h1 id="mitigations">Mitigations</h1><p>After learning the issue is likely to do with gcode density around curves, I tried to use the <a href="https://github.com/FormerLurker/ArcWelderPlugin">ArcWelder plugin</a> which makes gcode smaller by converting the straight <code>G0/G1</code> commands into <code>G2/G3</code> arc commands, which can reduce the resulting gcode by as much as 80%!</p><p>However, the stock firmware I was using did not have <code>ARC_SUPPORT</code> enabled, so I gave <a href="https://www.klipper3d.org/">Klipper</a> a shot. The idea of Klipper is to move the motion calculation off the printer&apos;s (usually underpowered) microcontrollers, and on to a more powerful device like a Raspberry Pi (compared to a microcontroller, of course). It supplies its own firmware for the printer, which takes a compressed data stream that in turn tells it how to do what to its various stepper motors. Klipper then exposes its own serial port to Octoprint which receives gcode commands. </p><p>Using Klipper noticably improved print quality for me, even when using Cura 4.7.x, however the printer display was just blank and not being able to control the printer with its built-in controls was a significant negative. I also ran into severe extruder chattering which caused filament grinding when I was using its pressure advance feature to handle better corners.</p><p>Marlin has a <code>DIRECT_STEPPING</code> option which is similar to how Klipper works, however it recommends 250k-500k baud and it doesn&apos;t seem to be used very much just yet.</p><p>My BLTouch bed levelling sensor eventually arrived so I followed <a href="https://smith3d.com/ender-3-v2-bltouch-firmware-installation-guide-by-smith3d-com/">Smith3D Ender 3 v2 BLTouch guide</a>, and used <a href="https://github.com/smith3d/Marlin/tree/bugfix-2.0.x-Smith3D">their fork of Marlin</a> which has some nice improvements.</p><p>I downgraded Cura to 4.6.2 for the time being, which resolved most of my print quality issues.</p><p>However, the core issue of potential print artifacts due to printing over USB still remain, and I&apos;m not willing to give up the convenience of Octoprint, so I investigated further.</p><p>Ideally, the code responsible for streaming to the printer should be run at a much higher priority to ensure it gets scheduled. It appears Octoprint&apos;s <a href="https://github.com/OctoPrint/OctoPrint/blob/3ab84ed7e4c3aaaf71fe0f184b465f25d689f929/src/octoprint/util/comm.py#L652-L662"><code>sending_thread</code> in <code>comm.py</code></a> is being run as a daemon thread, which I initially got excited cause it sounded like it would run it as a separate process (somehow?) which means we could just <code>snice</code> it to a higher priority, but that&apos;s not what it does.</p><p>My limited understanding of <code>comm.py</code> seems to indicate that Octoprint sends the next command to the printer once it&apos;s received an <code>ok</code> from the printer, which happens when Marlin commits the command into its ring buffer. The size of the ring buffer is defined by <code>BUFSIZE</code>, and it&apos;s usually set to <code>4</code> for most configurations.</p><p>This seems pretty small to me, but if Octoprint isn&apos;t filing the buffer reliably, then increasing <code>BUFSIZE</code> should decrease the likelihood but not mitigate it completely.</p><p>There is a <a href="https://github.com/OctoPrint/OctoPrint/pull/3209">WIP pull request</a> that enables parsing <code>ADVANCED_OK</code> to fill buffers accordingly, however it was last touched September 2019, so it may never get merged in.</p><h1 id="detecting-the-issue">Detecting the issue</h1><p>We should be able to discover the minimum speed that we can communicate reliably to the printer via a benchmark, and in theory, we should be able to look at a gcode file and determine if there are parts of the gcode that cannot be transmitted at the speed that it&apos;s meant to be parsed at.</p><p>There is also an <code>ADVANCED_OK</code> configuration option which will report the line number of the command being acked, as well as the remaining command buffer and motion planning buffers.</p><p>I&apos;m in the progress of writing an Octoprint plugin which should be able to track these buffers, as well as hopefully calculate a median latency of command to response to further diagnose this issue.</p><p>Ideally, there should be a way to detect when the command buffer is empty and report back to the host so users can be aware that there are issues happening. Initial research shows that <code>void GCodeQueue::advance()</code> is where the change should be made.</p><p>Next up: <a href="https://chen.do/adding-buffer-monitoring-to-marlin/">Adding buffer monitoring to Marlin</a>.</p>]]></content:encoded></item><item><title><![CDATA[Valve Index Base Station power management]]></title><description><![CDATA[<p>I managed to get a Valve Index a few weeks ago (which has been great), but the reliability of SteamVR could be a lot better. Especially with the base station power management feature, where it only puts the lasers on standby by default, which causes it to emit a high-pitch</p>]]></description><link>https://chen.do/valve-index-base-station-power-management/</link><guid isPermaLink="false">5f38abf0fdd7af006b19e8dc</guid><category><![CDATA[valve index]]></category><category><![CDATA[bluetooth]]></category><category><![CDATA[protip]]></category><dc:creator><![CDATA[chendo]]></dc:creator><pubDate>Sun, 16 Aug 2020 04:06:46 GMT</pubDate><content:encoded><![CDATA[<p>I managed to get a Valve Index a few weeks ago (which has been great), but the reliability of SteamVR could be a lot better. Especially with the base station power management feature, where it only puts the lasers on standby by default, which causes it to emit a high-pitch whine as the motors are still moving.</p><p>Turning on the proper standby power management feature only sometimes works, and often SteamVR won&apos;t successfully wake or put base stations on standby, which I&apos;ve tried to work around by restarting, forcing a discover and connection retry, all of which are extremely annoying to deal with.</p><p>There are tools on Github that allow management of these, namely <a href="https://github.com/nouser2013/lighthouse-v2-manager">https://github.com/nouser2013/lighthouse-v2-manager</a> (Python/Windows) and <a href="https://github.com/jeroen1602/lighthouse_pm">https://github.com/jeroen1602/lighthouse_pm</a> (Android, potentially works on iOS), however both seemed like more effort than it was worth.</p><p>Looking at the code in <code>lighthouse-v2-manager</code>, specifically <a href="https://github.com/nouser2013/lighthouse-v2-manager/blob/master/lighthouse-v2-manager.py#L146">https://github.com/nouser2013/lighthouse-v2-manager/blob/master/lighthouse-v2-manager.py#L146</a>, it reveals that it&apos;s a simple BLE characteristic write to turn them on and off. Why this appears to be such a difficult thing to do for SteamVR, I&apos;ll never know.</p><p>I attempted to hack something together in Node with <code>noble</code> as it seemed like the most mature BLE library for the languages I&apos;m familiar with, however I ran into some low-level looking errors so I gave up.</p><p>I used to have some <a href="https://www.ti.com/tool/CC2541DK-SENSOR">TI Sensortags</a> (very nifty devices) and I remembered their <a href="https://apps.apple.com/us/app/ti-sensortag/id552918064">iOS app</a> allowed BLE scanning and basic control.</p><p>Using the app, I was able to:</p><ul><li>See my base stations as indicated by <code>LHB-XXXXXXX</code></li><li>Tap on it to reveal the menu, and tap <code>Service Explorer</code></li><li>Tap the service indicated by UUID <code>00001523-1212-efde-1523-785feabcd124</code></li><li>Tap the characteristic as indicated by <code>00001525-1212-efde-1523-785feabcd124</code> (the first part of the UUID is <code>1525</code> rather than <code>1523</code>)</li><li>You can query its current value with <code>Read characteristic</code>. <code>0x00</code> means off, <code>0x01</code> means on</li><li>You can set the power state with <code>Write w/response characteristic</code>. Sending <code>0x00</code> will turn it off, and <code>0x01</code> will turn it on</li></ul><p>This is obviously not a great solution, but considering I don&apos;t have to get either of those tools working or build my own, this will have to do until SteamVR can write a byte to their own BLE devices.</p><p></p>]]></content:encoded></item></channel></rss>