<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>justinmklam | blog</title><link>https://justinmklam.com/</link><description>Recent content on justinmklam | blog</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 15 Feb 2026 10:59:22 -0800</lastBuildDate><atom:link href="https://justinmklam.com/index.xml" rel="self" type="application/rss+xml"/><item><title>A Beginner’s Guide to Split Keyboards</title><link>https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/</link><pubDate>Sun, 15 Feb 2026 10:59:22 -0800</pubDate><guid>https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/</guid><description>&lt;p>So you’ve heard of split keyboards and want to buy one, but don’t know where to start? You’ve come to the right place! There are many offerings these days which can be overwhelming, so this guide aims to provide a high level overview of the landscape so you can figure out which path you want to take.&lt;/p></description><content:encoded>&lt;p>So you’ve heard of split keyboards and want to buy one, but don’t know where to start? You’ve come to the right place! There are many offerings these days which can be overwhelming, so this guide aims to provide a high level overview of the landscape so you can figure out which path you want to take.&lt;/p>
&lt;blockquote>
&lt;p>The intent of this guide is not to tell you which specific keyboard you should buy, but rather to equip you with the knowledge to find one that works for yourself.&lt;/p>
&lt;p>The content in this post is mainly a consolidation of content that exists in &lt;a href="https://www.reddit.com/r/ErgoMechKeyboards/">r/ErgoMechKeyboards&lt;/a>, but organized in a way that is hopefully helpful for beginners.&lt;/p>
&lt;p>Also, it should be noted that regardless of how ergonomic your setup is, it&amp;rsquo;s important to take frequent breaks from being in front of a computer when possible, and to get up and move around. If you’re suffering from acute muscular pain (e.g. from keyboard use, or other), consider also seeing a physiotherapist or other healthcare practitioner for professional help.&lt;/p>&lt;/blockquote>

&lt;h1 id="introduction" class="anchor">
 &lt;a href="#introduction">
 Introduction
 &lt;/a>
&lt;/h1>

&lt;h2 id="why-use-a-split-keyboard" class="anchor">
 &lt;a href="#why-use-a-split-keyboard">
 Why Use a Split Keyboard?
 &lt;/a>
&lt;/h2>
&lt;p>There&amp;rsquo;s already many other resources that talk about the benefits of a split keyboard, but the gist is that by separating the left and right halves of the keys, your hands can rest in a more &lt;strong>natural position&lt;/strong> instead of being forced together like on a traditional keyboard. This reduces outward wrist bending (aka &lt;strong>ulnar deviation&lt;/strong>), shoulder tension, etc. The goal isn’t to magically fix posture overnight, but to remove some of the physical constraints imposed by a standard keyboard so your body can settle into something more relaxed during long sessions at the computer.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/why-split_hu_bf6e7f5d3e3abc17.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/why-split_hu_bf6e7f5d3e3abc17.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Exaggerated depiction of how a split keyboard can help promote a more neutral, comfortable typing position. (Source: &lt;a href=https://candid.technology/keyboards-and-wrist-rest-a-writers-guide-to-comfortable-typing/>Candid Technology&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="types-of-keyboards" class="anchor">
 &lt;a href="#types-of-keyboards">
 Types of Keyboards
 &lt;/a>
&lt;/h2>
&lt;p>There are three main categories of keyboards, which define the overall positions of the keys.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Row-staggered&lt;/strong>: Keys are aligned in rows, like a traditional keyboard. This is what you&amp;rsquo;re already used to.&lt;/li>
&lt;li>&lt;strong>Column-staggered&lt;/strong>: Keys are aligned in columns. This better matches finger anatomy and is very common in ergonomic splits.&lt;/li>
&lt;li>&lt;strong>Ortholinear&lt;/strong>: Uniform grid, no stagger. Visually pleasing, but less common in ergonomic splits.&lt;/li>
&lt;/ul>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/layout-comparison_hu_c4a14e6dbfe8009f.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/layout-comparison_hu_c4a14e6dbfe8009f.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Comparison of keyboard layouts.&lt;/p>
&lt;/div>

For people interested in ergonomics, most transition from row staggered to column staggered. Ortholinear was popular for a time and still has a cult following (e.g. with the Planck), but is not as popular for ergonomic keyboards. Majority of the split keyboards shown in this post will be &lt;strong>column staggered&lt;/strong>.&lt;/p>
&lt;p>A common question is whether learning to type on a non-row staggered keyboard will impact your ability to type on a traditional keyboard. If you switch between the two somewhat regularly, the answer is no - the brain appears to be great at compartmentalizing muscle memory for different devices! In my first month or so of learning to type on a column stagger keyboard, it took me a few minutes to get used to a regular keyboard again in that session. But now, I can use either a traditional or split without issue or “ramp up” time.&lt;/p>

&lt;h2 id="the-big-question-how-much-time-do-you-have" class="anchor">
 &lt;a href="#the-big-question-how-much-time-do-you-have">
 The Big Question: How Much Time Do You Have?
 &lt;/a>
&lt;/h2>
&lt;p>Despite the promised land of ergonomic benefits being a mere credit card purchase away, it is not without its dark side. There is unfortunately an &lt;strong>inevitable learning curve,&lt;/strong> and it can be a steep one.&lt;/p>
&lt;p>Re-training years, if not decades, of muscle memory for typing on a traditional keyboard is tough work, and can have its frustrating moments. It’s common to take at least a week or two of lots of typing practice to get accustomed to the new layout. For me, it took about a month of heavy use to be comfortable typing at a productive level for work, and that’s in a profession that involves a significant amount of daily typing (i.e. software development).&lt;/p>
&lt;p>If you’re practicing with something like &lt;a href="https://monkeytype.com/">monkeytype&lt;/a>, the default mode of typing lowercase words is only half the battle. Getting familiar with a keyboard involves other things, like:&lt;/p>
&lt;ul>
&lt;li>Using numbers, punctuation, and symbols&lt;/li>
&lt;li>Selecting/manipulating text (arrow keys) with shift or by word, home/end&lt;/li>
&lt;li>Application-specific hot keys (e.g. browser fwd/back, tab cycling, closing tabs/Windows, window management, Excel motions, etc)&lt;/li>
&lt;/ul>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/monkeytype_hu_37faf530d1df45c6.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/monkeytype_hu_37faf530d1df45c6.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The typing progression with my first split keyboard, over the course of 3 months.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>If at this point you’re questioning the time investment required for this whole endeavour, fear not! &lt;strong>There’s a keyboard for everyone,&lt;/strong> and not all require this steep learning curve.&lt;/p>

&lt;h1 id="path-1-i-want-a-split-but-dont-have-time-to-learn" class="anchor">
 &lt;a href="#path-1-i-want-a-split-but-dont-have-time-to-learn">
 Path 1: “I want a split, but don&amp;rsquo;t have time to learn”
 &lt;/a>
&lt;/h1>
&lt;p>These keyboards keep a row-staggered layout, so you can keep that well-trained muscle memory. You still get the biggest ergonomic win of splitting your hands apart, but without a massive cognitive load of retraining your fingers and brain for typing on a radically new layout.&lt;/p>
&lt;p>Some examples of traditional keyboards with a slight split in them:&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/logitech-sculpt_hu_92608fd377944579.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/logitech-sculpt_hu_92608fd377944579.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Logitech ERGO K860, which is similar to the previously popular, but now discontinued Microsoft Sculpt. (Source: &lt;a href=https://www.logitech.com/en-ca/shop/p/k860-split-ergonomic>Logitech&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/keychron-alice-q10_hu_7a62d412cf4f7396.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/keychron-alice-q10_hu_7a62d412cf4f7396.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Keychron Q10 in what&amp;rsquo;s called an Alice Layout. (Source: &lt;a href=https://www.keychron.com/collections/alice-layout-keyboards/products/keychron-q10-alice-layout-qmk-custom-mechanical-keyboard>Keychron&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Going one step further, getting a keyboard with independent halves will provide the most flexibility, since this allows you to freely position the halves (e.g. shoulder width apart), or even set them up with &lt;a href="#tenting">tenting&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/kinesis-freestyle_hu_319fb6e7b9ee3c57.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/kinesis-freestyle_hu_319fb6e7b9ee3c57.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Kinesis Freestyle 2, with optional tenting accessories. (Source: &lt;a href=https://kinesis-ergo.com/shop/freestyle2-for-pc-us/>Kinesis&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/ultimate-hacking-keyboard_hu_2df8fdef20c8700b.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/ultimate-hacking-keyboard_hu_2df8fdef20c8700b.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">UHK 60 and 80 variants, which come with optional thumb modules for mousing. (Source: &lt;a href=https://uhk.io/>Ultimate Hacking Keyboard&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>These keyboards may not be perfect for &amp;ldquo;power&amp;rdquo; users, but they tick enough boxes for the majority of people. If you do want to use more advanced features like layers and combos, you can use software like &lt;a href="https://karabiner-elements.pqrs.org/">Karabiner Elements&lt;/a> (macOS), &lt;a href="https://github.com/rvaiya/keyd">keyd&lt;/a> (Linux), or &lt;a href="https://github.com/jtroo/kanata">Kanata&lt;/a> (cross-platform) to add this functionality, similar to how I modified a regular keyboard&amp;rsquo;s keymap &lt;a href="https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/">here&lt;/a>.&lt;/p>
&lt;p>If you&amp;rsquo;re in this group, you can stop reading here! You can also find other similar keyboards using the (basic) search term &amp;ldquo;ergonomic keyboard&amp;rdquo; to see what&amp;rsquo;s currently available on the market.&lt;/p>

&lt;h1 id="path-2-im-willing-to-learn-for-better-ergonomics" class="anchor">
 &lt;a href="#path-2-im-willing-to-learn-for-better-ergonomics">
 Path 2: “I’m willing to learn for better ergonomics”
 &lt;/a>
&lt;/h1>
&lt;p>If you do have the time and motivation, then welcome to the rabbit hole of features, geometries, and customizability!&lt;/p>
&lt;p>Most keyboards in this category will have columnar stagger and multiple thumb keys (in place of a single spacebar), which are the main drivers for relearning how to type. The benefit of thumb keys is they let the thumbs handle high-frequency actions so the pinkies aren’t overworked reaching for shift and modifiers. However, keep in mind that it&amp;rsquo;s possible for &lt;a href="https://getreuer.info/posts/keyboards/thumb-ergo/index.html">thumbs to get overuse injuries&lt;/a> as well!&lt;/p>

&lt;h2 id="features" class="anchor">
 &lt;a href="#features">
 Features
 &lt;/a>
&lt;/h2>

&lt;h3 id="number-of-keys" class="anchor">
 &lt;a href="#number-of-keys">
 Number of Keys
 &lt;/a>
&lt;/h3>
&lt;p>When shopping for split keyboards, you’ll notice they can have a wide range of number of keys. A standard US keyboard has 104 keys (or 105 keys for those outside the US), but many have half that or even less!&lt;/p>
&lt;p>The rationale for having fewer keys is to keep your fingers as close to home row as possible, minimizing the amount of hand movement while typing. The tradeoff to fewer keys is complexity - instead of just reaching over to press a number, multiple keys will need to be pressed to achieve the same result.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/layers-corne_hu_747bf0eabc1ae8a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/layers-corne_hu_747bf0eabc1ae8a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Holding the left thumb key activates the second layer, which contains symbols and numbers.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>There’s &lt;a href="https://getreuer.info/posts/keyboards/40-percent-ergo/index.html">some discussion&lt;/a> of whether smaller layouts are actually better for ergonomics, but some find enjoyment in seeing how few keys one actually needs. This is ultimately a personal choice, but if you’re just starting out, it’s always better to have more keys and not use them, than to not have them and want them. You can also always try smaller layouts on a larger keyboard, like I did when &lt;a href="https://justinmklam.com/posts/2025/07/36-key-layout/">experimenting with a 36 key layout&lt;/a>.&lt;/p>
&lt;p>Here’s a few examples of keyboards showing the range of key counts:&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/kinesis-advantage360_hu_6c7d1c3932bf1cc6.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/kinesis-advantage360_hu_6c7d1c3932bf1cc6.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Kinesis Advantage360, a &amp;ldquo;maximalist&amp;rdquo; split keyboard with 76 keys. Omits the F-keys at the top, but offers a hefty 6-key thumb cluster on each side. (Source: &lt;a href=https://kinesis-ergo.com/shop/adv360pro/>Kinesis&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/lily58_hu_a366f0bf1f3aab37.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/lily58_hu_a366f0bf1f3aab37.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Lily58, &amp;ldquo;full&amp;rdquo; keyboard with a number row, outer columns, and 4 thumb keys. (Source: &lt;a href=https://github.com/kata0510/Lily58>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/corne_hu_25cb2e98be27c7b9.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/corne_hu_25cb2e98be27c7b9.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">The popular Corne, with 42 keys. Commonly denoted as 6x3+3 (6 columns x 3 rows + 3 thumb keys). Drops the number row. (Source: &lt;a href=https://keebd.com/en-ca/products/corne-cherry-v3-rgb-keyboard-kit>Keebd&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/chocofi_hu_61b425fa4ca41f64.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/chocofi_hu_61b425fa4ca41f64.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Chocofi, 36 keys (5x3+3). No number row or outer columns, so keys like shift, tab, quotation are no longer accessible on the main layer via a single key press. (Source: &lt;a href=https://shop.beekeeb.com/products/presoldered-chocofi-split-keyboard>beekeeb&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/sweep_hu_1b102080d78e6e19.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/sweep_hu_1b102080d78e6e19.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Sweep, 34 keys (5x3+2). With only 2 thumb keys, this is about as small as your can go while having all the alpha keys on the main layer. (Source: &lt;a href=https://github.com/davidphilipbarr/Sweep>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Smaller keyboards do exist, but these layouts require extensive use of layers and/or combos since the main layer is no longer large enough to contain all alpha keys as well as basic function keys like layer switch, space, backspace, etc. The &lt;a href="https://github.com/PJE66/hummingbird">Hummingbird&lt;/a> is a common keyboard/layout in this form factor, where Z/X/Q/J keys are relegated to combos to make space for other keys.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/tern_hu_98b065c65342ebf.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/tern_hu_98b065c65342ebf.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">A tiny 30 key keyboard. Questionable utility, but undeniable aesthetics! (Source: &lt;a href=https://github.com/rschenk/tern>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="geometric-considerations" class="anchor">
 &lt;a href="#geometric-considerations">
 Geometric Considerations
 &lt;/a>
&lt;/h3>
&lt;p>Aside from the number of physical keys, there are also many options of how the keys are laid out. Keyboards can have different amounts of column stagger, as well as &lt;strong>splay&lt;/strong> - which is when the columns are at an angle instead of parallel with each other.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/hillside52_hu_3ef06aacee99a5eb.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/hillside52_hu_3ef06aacee99a5eb.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Hillside 52 with splay in the outer 3 columns. (Source: &lt;a href=https://github.com/mmccoyd/hillside>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>You can also find keyboards that are &lt;strong>sculpted&lt;/strong> in 3D like the Glove80 or Charybdis, or you can even design your own &lt;a href="https://ryanis.cool/dactyl/#manuform">Dactyl&lt;/a>. Some find great comfort in these, however they come at a higher cost due to manufacturing complexities, unless you &lt;a href="https://justinmklam.com/posts/2025/08/handwired-skeletyl/">handwire your own&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/moergo-glove80_hu_64c6dcde373fcf36.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/moergo-glove80_hu_64c6dcde373fcf36.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Glove80 sculpted keyboard. (Source: &lt;a href=https://www.moergo.com/collections/glove80-keyboards>Moergo&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Recently, people started designing &lt;strong>sculpted keycaps&lt;/strong> like the &lt;a href="https://github.com/braindefender/KLP-Lame-Keycaps">KLP Lamé&lt;/a> which brings the benefits of sculpted shapes without needing a special keyboard.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/keycaps-klp-lame_hu_c3949f1f916ade7b.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/keycaps-klp-lame_hu_c3949f1f916ade7b.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">KLP Lamé sculpted keycaps on a Ferris Sweep. (Source: &lt;a href=https://github.com/braindefender/KLP-Lame-Keycaps/>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>You can also find &lt;strong>unibody/monoblock&lt;/strong> keyboards, which still have a split-like layout but are connected as a single unit. Some prefer the constraints and reproducibility of having the halves always being in the same position, since independent halves can shift around throughout the day. Unibody keyboards are also generally easier to take and move around, like if you’re working on your lap on the couch or on an airplane tray.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/reviung_hu_90541614c09d7a7f.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/reviung_hu_90541614c09d7a7f.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Reviung41 unibody with low profile switches and a Pimoroni trackball. (Source: &lt;a href=https://holykeebs.com/products/trackball-reviung41-low-profile>Holy Keebs&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The pursuit of ergonomics goes quite deep, where there&amp;rsquo;s also a league of non-keyboard-looking keyboards that use innovative methods for input. The &lt;a href="https://svalboard.com/">Svalboard&lt;/a>, for example, uses multiple feather-light paddles for each finger instead of regular switches. This keyboard was inspired by the &lt;a href="https://en.wikipedia.org/wiki/DataHand">DataHand&lt;/a>, an alternative keyboard designed to be operated without requiring any finger travel. Similarly, the &lt;a href="https://www.charachorder.com/">Charachorder&lt;/a> uses joystick-like switches for each finger and makes heavy use of combos, although this makes it closer to &lt;a href="https://studysteno.com/moo/mod/page/view.php?id=150">stenography&lt;/a> in philosophy.&lt;/p>
&lt;p>In reality, this is near the bottom of the rabbit hole. Most users never go anywhere near this level of ergonomic experimentation, but it’s where the most interesting (and potentially &lt;a href="https://svalboard.com/pages/reviews">game changing&lt;/a>) ideas tend to emerge!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/svalboard_hu_895528b82e79e2e1.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/svalboard_hu_895528b82e79e2e1.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">&amp;ldquo;Banish pain with the most adaptable ergonomic keyboard and mousing instrument ever made.&amp;rdquo; (Source: &lt;a href=https://svalboard.com/>Svalboard&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/charachorder_hu_d1e7ca51b3a90b73.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/charachorder_hu_d1e7ca51b3a90b73.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">&amp;ldquo;Less finger travel = quicker reactions, faster typing, &amp;amp; less strain. Leave 1D typewriting in the past and experience a new dimension built for the digital age.&amp;rdquo; (Source: &lt;a href=https://www.charachorder.com>Charachorder&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="tenting" class="anchor">
 &lt;a href="#tenting">
 Tenting
 &lt;/a>
&lt;/h3>
&lt;p>Many people find it more comfortable to have their keyboard slightly raised in the middle, which allows the wrist to maintain a more neutral pronation. The ideal tenting angle is dependent on both the user and the keyboard, where at a given angle, a wider keyboard with more keys will feel different than a smaller keyboard with fewer keys.&lt;/p>
&lt;p>Some keyboards come with integrated tenting, but if not, there are many DIY solutions to add it on after. For small angles, foldable laptop stand feet/risers work great.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/totem_hu_5a302c1710885afb.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/totem_hu_5a302c1710885afb.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Totem with sculpted keycaps and adjustable laptop stand feet. (Source: &lt;a href=https://github.com/GEIGEIGEIST/TOTEM>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>For larger angles, adhesive-backed Magsafe rings and mobile phone stands allow for more adjustability. The stands can be pricey, but there are also &lt;a href="https://kbd.news/Split-keyboard-Magsafe-tenting-stand-2732.html">cheaper, 3D printed options&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/keebio-magsafe-tenting_hu_cc9880dd13f422cf.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/keebio-magsafe-tenting_hu_cc9880dd13f422cf.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Tenting with Magsafe ring and phone stands. (Source: &lt;a href=https://keeb.io/products/magnetic-magsafe-tenting-stand-kit-for-split-keyboard-r2>Keebio&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>For extreme angles, ball mount clamps (like those for cameras) can be mounted to a desk (or even chairs)&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/ben-vallack-tenting_hu_1f1c061171d485c6.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/ben-vallack-tenting_hu_1f1c061171d485c6.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Tenting solution with ball joint clamps. (Source: &lt;a href=https://www.youtube.com/watch?v=mT3TToFqqEU>Ben Vallack, YouTube&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="pointing-devices-and-encoders" class="anchor">
 &lt;a href="#pointing-devices-and-encoders">
 Pointing Devices and Encoders
 &lt;/a>
&lt;/h3>
&lt;p>With custom keyboards these days, there are many options that include an integrated touchpad, trackball, or even trackpoint to minimize (or remove) the need to move your hand to use the mouse.&lt;/p>
&lt;p>Some keyboards also have encoders for easy access to things like volume control or scrolling, which can be controlled via firmware.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/toucan_hu_8e347f9b3eccd1a5.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/toucan_hu_8e347f9b3eccd1a5.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Toucan 42 and 36, with an integrated Cirque 40mm Glide touchpad. (Source: &lt;a href=https://shop.beekeeb.com/products/toucan-36>beekeeb&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/charybdis_hu_67b771c0d33e10fa.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/charybdis_hu_67b771c0d33e10fa.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Charybdis, with a thumb trackball. (Source: &lt;a href=https://bastardkb.com/charybdis/>Bastard Keyboards&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/cocot46plus_hu_9a63e71e0e8c74af.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/cocot46plus_hu_9a63e71e0e8c74af.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">cocot46plus with a finger trackball and encoder. (Source: &lt;a href=https://github.com/aki27kbd/cocot46plus>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/tps42_hu_b75b63bbf3d22d45.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/tps42_hu_b75b63bbf3d22d45.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">TPS42 with an integrated trackpoint. (Source: &lt;a href=https://github.com/crehmann/TPS42/wiki>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Custom keyboards also supports &lt;a href="https://docs.qmk.fm/features/mouse_keys">mouse keys&lt;/a> to emulate mouse movements with your keyboard. It can feel a bit limiting in functionality, but is better than nothing and can be helpful in a pinch.&lt;/p>

&lt;h3 id="wired-vs-wireless" class="anchor">
 &lt;a href="#wired-vs-wireless">
 Wired vs Wireless
 &lt;/a>
&lt;/h3>
&lt;p>Historically, split keyboards were only wired due to both &lt;a href="https://www.reddit.com/r/olkb/comments/s95y98/comment/htkwvkp/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button">technical and licensing limitations&lt;/a> with &lt;a href="https://qmk.fm/">QMK&lt;/a>, the custom firmware that powers many keyboards. A permissive, wireless, Bluetooth-first alternative called &lt;a href="https://zmk.dev/">ZMK&lt;/a> began development in &lt;a href="https://kbd.news/ZMK-A-History-2222.html">2020&lt;/a>, and now many keyboards are offered in both wired and wireless variants.&lt;/p>
&lt;p>One thing to note - battery management has some quirks for fully wireless split keyboards. The central side needs to do double duty since it needs to communicate with both the host computer and the peripheral side, and as a result its battery discharges significantly faster. Additionally, the more features your keyboard has (display, pointing device, etc.), the shorter the battery life will be.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/zmk-power-profiler_hu_ee62923988e3d812.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/zmk-power-profiler_hu_ee62923988e3d812.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">A common setup of a nice!nano and a 110mAh battery yields an estimated ~2 weeks for the central side, and a whopping ~3 months for the peripheral side. (Source: &lt;a href=https://zmk.dev/power-profiler>ZMK Power Profiler&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>A popular workaround is to use a ZMK dongle, which makes both sides of a wireless keyboard act as peripherals to maximize battery life, although there are still &lt;a href="https://docs.slicemk.com/firmware/zmk/wireless/dongle/">some limitations&lt;/a> to this approach.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/zmk-dongle_hu_e5fc1deb20fc52a4.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/zmk-dongle_hu_e5fc1deb20fc52a4.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">One example of the many available ZMK dongle screens. (Source: &lt;a href=https://github.com/janpfischer/zmk-dongle-screen>ZMK Dongle Screen, Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Some companies like ZSA still &lt;a href="https://www.zsa.io/wireless">prefer wired&lt;/a>, and if your keyboard is going to stay on your desk, I would tend to agree.&lt;/p>

&lt;h3 id="high-vs-low-profile-switches" class="anchor">
 &lt;a href="#high-vs-low-profile-switches">
 High vs Low Profile Switches
 &lt;/a>
&lt;/h3>
&lt;p>There are a ton of online resources that already go into the comparison of these, but the short of it is that low profile mechanical switches are relatively new and are becoming more common in keyboard designs. Lower profile means a shorter keyboard height, alleviating the need to use wrist/palm wrests.&lt;/p>
&lt;p>At the time of writing, high profile (MX) switches still have many more options and generally feel and sound better, and finding keycaps for low profile switches (especially Choc v2) can be challenging, but availability is expected to improve over time as they become more widespread.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/switch-comparison_hu_3a3b2fdb6a858f9c.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/switch-comparison_hu_3a3b2fdb6a858f9c.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Comparison of MX (high) and low profile switches. (Source: &lt;a href=https://kineticlabs.com/blog/will-switches-work-on-hotswap>Kinetic Labs&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Keyboards often have hotswap sockets to allow installing different switches without needing to solder. Some boards like the &lt;a href="https://github.com/tompi/cheapino">cheapino&lt;/a> have the option to directly solder the switches (instead of using sockets) to keep costs low.&lt;/p>
&lt;p>You might notice that many keyboards consist of &lt;strong>blank keycaps&lt;/strong> instead of ones with legends. This is common because with the use of layers and keys having multiple functions, legends don&amp;rsquo;t end up being that useful for indicating what a key will actually do.&lt;/p>
&lt;p>For both switches and keycaps, again it comes down to personal preference.&lt;/p>

&lt;h2 id="keymap-customization" class="anchor">
 &lt;a href="#keymap-customization">
 Keymap Customization
 &lt;/a>
&lt;/h2>
&lt;p>A core part of split keyboards is customizing the keymap to suit your own needs. Although keymaps are defined in firmware, many popular keyboards will have pre-compiled firmware with VIA/Vial support (for QMK, or ZMK Studio for ZMK) to support editing keymaps through a GUI.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/vial_hu_9f7e1564fe34a9b4.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/vial_hu_9f7e1564fe34a9b4.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Using Vial to customize a keyboard layout. (Source: &lt;a href=https://jawick.com/blog/vial-waterfowl-guide/>Jawick&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Basic &amp;ldquo;programmable&amp;rdquo; features consist of the following:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://docs.qmk.fm/mod_tap">&lt;strong>Mod-Tap&lt;/strong>&lt;/a>: One key when tapped, another key when held. (E.g. on a traditional keyboard, many programmers remap &lt;code>CAPSLOCK&lt;/code> to &lt;code>ESC&lt;/code> when tapped, &lt;code>CTRL&lt;/code> when held).&lt;/li>
&lt;li>&lt;a href="https://docs.qmk.fm/features/combo">&lt;strong>Combos&lt;/strong>&lt;/a> : Press two keys to output another. (E.g. &lt;code>J+K&lt;/code> to &lt;code>ESC&lt;/code>, or &lt;code>F+G&lt;/code> to a symbol like &lt;code>-&lt;/code>)&lt;/li>
&lt;/ul>
&lt;p>At a minimum, you&amp;rsquo;ll need to get used to using mod-taps on the thumb keys for keys like space, backspace, etc. and to activate the different layers.&lt;/p>
&lt;p>Beyond the surface lies more advanced features, where the sky&amp;rsquo;s the limit to what fancy things can be done:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://docs.qmk.fm/one_shot_keys">&lt;strong>One shot keys&lt;/strong>&lt;/a>: For modifiers only, allows you to use a modifier without needing to hold it down. Tap a modifiers key to activate it, then tap another key send the key code.&lt;/li>
&lt;li>&lt;a href="https://precondition.github.io/home-row-mods">&lt;strong>Home row mods:&lt;/strong>&lt;/a> Giving home row keys (&lt;code>ASDF&lt;/code> + &lt;code>JKL;&lt;/code>) double duty to also act as modifier keys (&lt;code>SHIFT&lt;/code>, &lt;code>CTRL&lt;/code>, &lt;code>ALT&lt;/code>, &lt;code>GUI&lt;/code>) when held&lt;/li>
&lt;li>&lt;a href="https://github.com/callum-oakley/qmk_firmware/tree/master/users/callum">&lt;strong>Callum style mods&lt;/strong>&lt;/a>: Similar to home row mods, but combined with one shot keys to remove the dependency on timing&lt;/li>
&lt;/ul>
&lt;p>If you don’t know where to start with creating your own keymap, you can draw inspiration from existing ones on &lt;a href="https://keymapdb.com/">KeymapDB&lt;/a>. For 36 key keyboards, the &lt;a href="https://github.com/manna-harbour/miryoku">Miryoku&lt;/a> keymap is a good place to start.&lt;/p>

&lt;h3 id="what-about-using-a-non-qwerty-layout" class="anchor">
 &lt;a href="#what-about-using-a-non-qwerty-layout">
 What About Using a Non-QWERTY Layout?
 &lt;/a>
&lt;/h3>
&lt;p>Some choose to also change to an alternative layout like Dvorak, Colemak or other, since QWERTY is not designed with ergonomics in mind. However, learning a new layout requires even more time, and I personally haven’t gone down this route since the benefits of a column stagger and split keyboard are sufficient enough for me (and I reckon most people as well).&lt;/p>
&lt;p>However, if you&amp;rsquo;re curious to learn more, there are many other layouts aside from the popular Dvorak layout, which each have their own advantages depending on the metrics important to you (e.g. &lt;a href="https://getreuer.info/posts/keyboards/glossary/index.html#same-finger-bigram-sfb">same-finger bigrams (SFBs)&lt;/a>, &lt;a href="https://getreuer.info/posts/keyboards/glossary/index.html#lateral-stretch-bigram-lsb">lateral stretch bigrams (LSBs)&lt;/a>, &lt;a href="https://getreuer.info/posts/keyboards/glossary/index.html#scissor">scissors&lt;/a>, &lt;a href="https://getreuer.info/posts/keyboards/glossary/index.html#redirect">redirects&lt;/a>, &lt;a href="https://getreuer.info/posts/keyboards/glossary/index.html#roll">rolls&lt;/a>, etc.). More information can be found in the &lt;a href="https://layouts.wiki/">https://layouts.wiki&lt;/a> or &lt;a href="https://www.reddit.com/r/KeyboardLayouts/?screen_view_count=4">r/KeyboardLayouts&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/keymap-alt-layout-compraison_hu_19a5fba8a90b7f8e.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/keymap-alt-layout-compraison_hu_19a5fba8a90b7f8e.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Comparison table of different metrics. (Source: &lt;a href=https://getreuer.info/posts/keyboards/alt-layouts/index.html>Pascal Getreuer&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="how-do-i-choose" class="anchor">
 &lt;a href="#how-do-i-choose">
 How Do I Choose?
 &lt;/a>
&lt;/h2>
&lt;p>With the overwhelming number of keyboards one can acquire, how does one choose one for themselves?&lt;/p>
&lt;p>The first question is to ask yourself how many keys you think you need, which starts with whether you need a number row, arrow keys, home/end, etc. Generally, the more keys you have, the faster the learning curve will be.&lt;/p>
&lt;p>Once you narrow down to the number of keys, next is the form factor. Aside from watching review videos on YouTube, there are also some great web tools to help since this will also depend on your own hand geometry.&lt;/p>
&lt;blockquote>
&lt;p>Personally, I found the amount of column stagger doesn&amp;rsquo;t make a huge difference (except for maybe the pinky column), whereas the thumb cluster matters a bit more since an overly tucked or stretched thumb is more noticeably uncomfortable.&lt;/p>&lt;/blockquote>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/yal-tools-ergo-keyboards_hu_3b0d966f8e1c6c09.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/yal-tools-ergo-keyboards_hu_3b0d966f8e1c6c09.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Searchable collection of ergonomic keyboards. (Source: &lt;a href=https://yal-tools.github.io/ergo-keyboards/>YAL Tools&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/splitkbcompare_hu_58c5d99d4de8cdb3.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/splitkbcompare_hu_58c5d99d4de8cdb3.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Web app to compare the physical sizes of common keyboards. (Source: &lt;a href=https://jhelvy.shinyapps.io/splitkbcompare/>SplitKB Compare&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>To get a better feel of your potential keyboard(s), you can use some low-fidelity prototypes to test the form factor. Printing it on paper is the easiest way&amp;hellip;





&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/paper-trial_hu_f112fd7d294675ae.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/paper-trial_hu_f112fd7d294675ae.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Printing a layout on paper to test hand positions. (Source: &lt;a href=https://getreuer.info/posts/keyboards/thumb-ergo/index.html>Pascal Getreuer&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Or you can take it a step further and cut out the template on to cardboard, then install switches. If you want, you could even handwire it to make it a usable, functional keyboard, which may be worth the effort since actually using a keyboard can identify many more things (e.g. discomforts) than just randomly pressing dummy keys.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/cardboard-proto-1_hu_6a3159b30a863302.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/cardboard-proto-1_hu_6a3159b30a863302.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Cutting out the keyboard template on to an old shoe box.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/cardboard-proto-2_hu_ffde50928258b6bb.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/cardboard-proto-2_hu_ffde50928258b6bb.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Switches and keycaps installed on to the cardboard prototype.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="to-buy-or-diy" class="anchor">
 &lt;a href="#to-buy-or-diy">
 To Buy or DIY?
 &lt;/a>
&lt;/h2>

&lt;h3 id="buy" class="anchor">
 &lt;a href="#buy">
 Buy
 &lt;/a>
&lt;/h3>
&lt;p>Gone are the days of needing to spend &amp;gt;$500 on an ergonomic keyboard, and now you can find a keyboard for every budget! Generally, the smaller the keyboard, the cheaper it will be since fewer keys = fewer switches/keycaps + smaller PCB.&lt;/p>
&lt;p>In the &lt;strong>upper budget range&lt;/strong>, companies like &lt;a href="https://kinesis-ergo.com/">Kinesis&lt;/a>, &lt;a href="https://www.zsa.io">ZSA&lt;/a>, and &lt;a href="https://dygma.com/">Dygma&lt;/a> make specialized, closed-source keyboards but offer premium devices and support.
There are also more boutique options like &lt;a href="https://www.moergo.com/">MoErgo&lt;/a> and &lt;a href="https://bastardkb.com/">Bastard Keyboards&lt;/a> that offer high quality products. These keyboards will usually have their own custom software to program them, which usually easier to use than the open-source software versions.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/zsa-voyager-product_hu_cbab5e75b18a7552.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/zsa-voyager-product_hu_cbab5e75b18a7552.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Premium keyboard with a premium website. (Source: &lt;a href=https://www.zsa.io/voyager/buy>ZSA Voyager&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>In the &lt;strong>middle range&lt;/strong>, you can find many shops like &lt;a href="https://beekeeb.com/">beekeeb&lt;/a>, &lt;a href="https://holykeebs.com/">holykeebs&lt;/a>, &lt;a href="https://splitkb.com/">splitkb&lt;/a>, or any of the other vendors listed on the &lt;a href="https://www.reddit.com/r/ErgoMechKeyboards/wiki/resources">r/ErgoMechKeyboards wiki&lt;/a>. They typically sell both pre-assembled boards and kits that require soldering, but are offered at a more affordable price point. Many of the keyboards offered at these vendors are based on or variants of open-sourced designs.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/shop-beekeeb_hu_36da2f3af56e6e16.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/shop-beekeeb_hu_36da2f3af56e6e16.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Beekeeb (Source: &lt;a href=https://shop.beekeeb.com>beekeeb&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Although you do get a &lt;em>much&lt;/em> nicer keyboard in the above price ranges, cheaper options exist on sites like Amazon and Aliexpress for those on &lt;strong>low budgets&lt;/strong>. The experience or physical product may not be as refined (e.g. 3D printed cases are often used), but it&amp;rsquo;ll still be functional and at least let you try it out without breaking the bank. This is where I started on &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard/">my journey with using a Corne keyboard&lt;/a>.&lt;/p>
&lt;blockquote>
&lt;p>When buying cheap keyboards, there is a &lt;a href="https://www.reddit.com/r/ErgoMechKeyboards/comments/1idz2rn/why_you_should_always_reflash_new_keyboards_my_50/">security risk&lt;/a> to them. If possible, you should reflash the firmware to remove any risk of malicious behaviour.&lt;/p>&lt;/blockquote>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/shop-aliexpress_hu_e1e2472c29935690.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/shop-aliexpress_hu_e1e2472c29935690.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Aliexpress with many affordable options for split keyboards. (Source: &lt;a href=https://www.aliexpress.com>Aliexpress&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="diy" class="anchor">
 &lt;a href="#diy">
 DIY
 &lt;/a>
&lt;/h3>
&lt;p>Many keyboard designs are open-sourced, which means you can take the design files (usually hosted on Github, packaged as &lt;a href="https://en.wikipedia.org/wiki/Gerber_format">gerber files&lt;/a>) and get them manufactured somewhere like &lt;a href="https://www.pcbway.com/">PCBWay&lt;/a>. Getting a PCB made with them is pretty painless: you upload your gerber files, pick your specs (defaults are usually fine), and the board typically arrives within a week or two, even to North America. The quality is solid, and their instant quote tool makes it easy to see costs upfront before committing.&lt;/p>
&lt;p>This is a great option if you find a design that isn&amp;rsquo;t available in any of the options above. But in terms of cost, by the time you factor in the PCBs, switches, diodes, microcontrollers, fasteners, etc., not to mention the soldering equipment if you don&amp;rsquo;t already have that, buying something from Amazon or Aliexpress will likely be the cheapest if you&amp;rsquo;re starting from zero.&lt;/p>
&lt;p>However, if you&amp;rsquo;re okay with being even more hands on, and if you already have soldering equipment, &lt;a href="https://justinmklam.com/posts/2025/08/handwired-skeletyl/">handwiring a keyboard&lt;/a> with a 3D printed enclosure will be the cheapest (and most flexible) option. As long as your soldering is solid, there wouldn&amp;rsquo;t be any functional difference between this and a PCB!&lt;/p>

&lt;h2 id="popular-keyboards" class="anchor">
 &lt;a href="#popular-keyboards">
 Popular Keyboards
 &lt;/a>
&lt;/h2>
&lt;p>Still have trouble finding which keyboard you should get? Here’s a list of popular* keyboards for each budget (as of Feb 2026):&lt;/p>
&lt;blockquote>
&lt;p>*The keyboards listed below are from my personal perspective, based on what I see mentioned and recommended around Reddit. YMMV and I could have missed some - there are many other keyboards out there, so please do your own research!&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Keyboard&lt;/th>
 &lt;th>Budget&lt;/th>
 &lt;th># Keys&lt;/th>
 &lt;th>Open Sourced?&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Kinesis Advantage2/360&lt;/td>
 &lt;td>$$$&lt;/td>
 &lt;td>80 / 76&lt;/td>
 &lt;td>no&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ZSA Moonlander / Voyager&lt;/td>
 &lt;td>$$$&lt;/td>
 &lt;td>76 / 52&lt;/td>
 &lt;td>no&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dygma Defy&lt;/td>
 &lt;td>$$$&lt;/td>
 &lt;td>70&lt;/td>
 &lt;td>no&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Glove80 / Go60&lt;/td>
 &lt;td>$$$&lt;/td>
 &lt;td>80 / 60&lt;/td>
 &lt;td>no&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dactyl / Charybdis&lt;/td>
 &lt;td>$$$&lt;/td>
 &lt;td>58 / 42 / 36&lt;/td>
 &lt;td>yes&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Keyball&lt;/td>
 &lt;td>$$$&lt;/td>
 &lt;td>61 / 44 / 39&lt;/td>
 &lt;td>yes&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Elora / Kyria&lt;/td>
 &lt;td>$$&lt;/td>
 &lt;td>62 / 50&lt;/td>
 &lt;td>no&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sofle / Lily58&lt;/td>
 &lt;td>$$&lt;/td>
 &lt;td>58&lt;/td>
 &lt;td>yes&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Totem&lt;/td>
 &lt;td>$$&lt;/td>
 &lt;td>38&lt;/td>
 &lt;td>yes&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Silakka54&lt;/td>
 &lt;td>$&lt;/td>
 &lt;td>54&lt;/td>
 &lt;td>yes&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Corne&lt;/td>
 &lt;td>$&lt;/td>
 &lt;td>36 / 42&lt;/td>
 &lt;td>yes&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sweep&lt;/td>
 &lt;td>$&lt;/td>
 &lt;td>34&lt;/td>
 &lt;td>yes&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Open sourced means the design files are available for building yourself, as well as there being many variants around (at both reputable vendors and places like Aliexpress). For example, if you bought a Corne on Aliexpress and like the layout but want some premium features like wireless, low profile switches, and/or a sturdier case, you can shop around at different vendors to find boards with these features.&lt;/p>
&lt;p>A common starting keyboard is the 42 key Corne if you’re okay with fewer keys, or the Silakka54/Lily58/Sofle if you want more keys. From there, some stay with these to be their daily drivers, and others use them as a stepping stone to move on to other keyboards after learning more about their workflow preferences.&lt;/p>

&lt;h2 id="design-your-own" class="anchor">
 &lt;a href="#design-your-own">
 Design Your Own
 &lt;/a>
&lt;/h2>
&lt;p>If no existing keyboard sparks joy, there are many resources and tools to design your own. Tools like &lt;a href="https://flatfootfox.com/ergogen-introduction/">Ergogen&lt;/a> and &lt;a href="https://github.com/adamws/kle-ng">kle-ng&lt;/a> make it easy to create your own layout, which can be exported to &lt;a href="https://www.kicad.org/">Kicad&lt;/a> to finalize the PCB. Or if you want one fully customized to your hand shape, you can use &lt;a href="https://ryanis.cool/cosmos/">Cosmos&lt;/a> to create a sculpted one to be handwired.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/tool-ergogen_hu_61c27e165c5d04a4.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/tool-ergogen_hu_61c27e165c5d04a4.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Ergogen keyboard layout generator. (Source: &lt;a href=https://ergogen.ceoloide.com/>Ergogen&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/tool-cosmos_hu_80bc28bb1841b894.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2026/02/beginners-guide-split-keyboards/tool-cosmos_hu_80bc28bb1841b894.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Cosmos keyboard configurator. (Source: &lt;a href=https://ryanis.cool/cosmos/>Cosmos&lt;/a>)&lt;/p>
&lt;/div>

This is a topic in itself, but there are many existing tutorials and guides on YouTube and around the internet. For keyboards on Github, you can also poke around the design files and reference or adapt them to your needs, which is the great thing about open source!&lt;/p>

&lt;h1 id="closing-thoughts" class="anchor">
 &lt;a href="#closing-thoughts">
 Closing Thoughts
 &lt;/a>
&lt;/h1>
&lt;p>If you&amp;rsquo;ve made it this far, thanks for reading! Hope this was helpful in starting you off in this journey of finding your perfect keyboard.&lt;/p>
&lt;p>Happy typing!&lt;/p></content:encoded></item><item><title>Remapping a Standard Keyboard</title><link>https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/</link><pubDate>Sun, 30 Nov 2025 22:18:06 -0800</pubDate><guid>https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/</guid><description>&lt;p>As much as I enjoy using my ergonomic keyboards, sometimes I still need to use the built in keyboard on my laptop. Transitioning between the two drastically different layouts is no longer an issue, but I find myself having an itch of wondering whether it’s possible to improve the usability of a standard keyboard.&lt;/p></description><content:encoded>&lt;p>As much as I enjoy using my ergonomic keyboards, sometimes I still need to use the built in keyboard on my laptop. Transitioning between the two drastically different layouts is no longer an issue, but I find myself having an itch of wondering whether it’s possible to improve the usability of a standard keyboard.&lt;/p>
&lt;p>I personally find typing letters and numbers is actually not so bad; despite the row stagger causing my fingers to move around more than they would need to on a columnar stagger layout, it’s not the worst thing to deal with. The thing that bothers me more are the actions around text manipulation like selecting or deleting text by words or entire lines. The keys for these are at the peripheral of the keyboard, which causes me to move both my hands away from the home row whenever I need to do those operations, which is fairly often while typing.&lt;/p>
&lt;p>With a bit of layer remapping that I adapted from using small form factor keyboards, I was able to achieve a layout that keeps my hands relatively centered around home row. It’s not a far departure from a standard layout, so the learning curve is low compared to other, more drastic layout changes to bring improved ergonomics to common keyboards.&lt;/p>

&lt;h1 id="the-goal" class="anchor">
 &lt;a href="#the-goal">
 The Goal
 &lt;/a>
&lt;/h1>
&lt;p>To enable keeping my hands close to home row as often as possible, I need to move a handful of &lt;em>key&lt;/em> keys:&lt;/p>
&lt;ul>
&lt;li>Top left: &lt;code>Escape&lt;/code>&lt;/li>
&lt;li>Top right: &lt;code>Delete/Backspace&lt;/code>&lt;/li>
&lt;li>Bottom left: &lt;code>Shift&lt;/code>, &lt;code>Control&lt;/code>, &lt;code>Option/Alt&lt;/code>, &lt;code>Command&lt;/code>&lt;/li>
&lt;li>Bottom right: &lt;code>Left&lt;/code>, &lt;code>Right&lt;/code>, &lt;code>Up&lt;/code>, &lt;code>Down&lt;/code> Arrow Keys&lt;/li>
&lt;/ul>

&lt;h1 id="prior-art" class="anchor">
 &lt;a href="#prior-art">
 Prior Art
 &lt;/a>
&lt;/h1>
&lt;p>Other efforts exist like &lt;a href="https://drop.com/talk/138510/what-is-space-fn-and-why-you-should-give-it-a-try">SpaceFN&lt;/a> or &lt;a href="https://drop.com/talk/138510/what-is-space-fn-and-why-you-should-give-it-a-try">Home Row Mods&lt;/a> to improve the usability of standard keyboards, but the issue I found is that they all modify frequently used keys. I find these customizations work best on keys that aren&amp;rsquo;t as commonly used during regular typing. On my split keyboard, I started using bottom row mods (where the modifiers are on &lt;code>ZXCV&lt;/code> instead of &lt;code>ASDF&lt;/code>) which I find work much better, but unfortunately on a standard layout, the bottom row is shifted quite far right which makes using it with the left hand more awkward.&lt;/p>
&lt;p>After a bit of trial and error, I settled on a remapping that works for my workflow. I even went as far as replacing my daily split keyboard with the &lt;a href="https://capitaloneshopping.com/p/apple-magic-keyboard-us-english/TWRN969XDQ">Apple Magic Keyboard&lt;/a> for a few weeks, since my secondary hypothesis was whether it’s possible to make a standard keyboard ergonomic enough to avoid needing to buy and learn how to use a columnar-stagger split keyboard. Unsurprising spoiler alert: You can get close, but there are tradeoffs.&lt;/p>

&lt;h1 id="the-layers" class="anchor">
 &lt;a href="#the-layers">
 The Layers
 &lt;/a>
&lt;/h1>

&lt;h2 id="base" class="anchor">
 &lt;a href="#base">
 Base
 &lt;/a>
&lt;/h2>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/layer-main.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/layer-main.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Main layer with three modified keys.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>For the main base layer, only three infrequently used keys are changed:&lt;/p>
&lt;ul>
&lt;li>&lt;code>Tab&lt;/code> -&amp;gt; &lt;code>Tab&lt;/code> when pressed, &lt;code>Control&lt;/code> when held&lt;/li>
&lt;li>&lt;code>Capslock&lt;/code> -&amp;gt; Hold for &lt;code>Extend&lt;/code> layer&lt;/li>
&lt;li>&lt;code>RightAlt&lt;/code> -&amp;gt; Hold for &lt;code>Nav&lt;/code> layer&lt;/li>
&lt;/ul>
&lt;p>Additionally, I added a combo of &lt;code>J+K = Escape&lt;/code>, which is especially useful for Vim.&lt;/p>
&lt;p>&lt;code>Tab&lt;/code>&amp;rsquo;s default behaviour doesn&amp;rsquo;t change, but holding it unlocks a new behaviour of acting as &lt;code>Control&lt;/code>. &lt;code>Capslock&lt;/code> and &lt;code>Right Alt&lt;/code> are not used often (at least by me), so I opted to use those as the keys to activate the two new layers, &amp;ldquo;Extend&amp;rdquo; and &amp;ldquo;Nav&amp;rdquo;, which I&amp;rsquo;ll explain more in the following sections.&lt;/p>
&lt;p>I considered using &lt;code>LeftCommand&lt;/code> to toggle the layer as well, which would help with symmetry and also have both thumbs operating the layers, similar to how it works on typical split keyboards. But I use &lt;code>Command&lt;/code> often, and &lt;code>Capslock&lt;/code> is basically unused. I already had remapped it to be &amp;ldquo;escape when tapped, control when held&amp;rdquo; to improve ergonomics when using vim, since &lt;code>Capslock&lt;/code> is in a prime location for the pinky to press while the rest of the fingers are free to move along the home row.&lt;/p>

&lt;h2 id="extend-via-capslock" class="anchor">
 &lt;a href="#extend-via-capslock">
 Extend: via Capslock
 &lt;/a>
&lt;/h2>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/layer-extend.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/layer-extend.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">When the extend (previously capslock) key is pressed, it activates the other coloured keys.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>With this layer, I can easily manipulate text with my right fingers on the arrow keys, and left fingers on the modifiers. This lets my hands stay on home row, where typically to do similar actions, I&amp;rsquo;d need to move both hands to the bottom corners to press the modifiers and the arrow keys.&lt;/p>
&lt;p>For example, if I want to select a bunch of text, instead of moving my left hand to the bottom left of the keyboard to press &lt;code>Shift + Option&lt;/code>, and my right hand to the arrow keys, I can just press &lt;code>S+D&lt;/code> with my left hand, and one of &lt;code>H|J|K|L&lt;/code> with my right hand for the arrow key.&lt;/p>
&lt;p>In this layer, &lt;code>Spacebar&lt;/code> also becomes &lt;code>Delete/Backspace&lt;/code>, which avoids having to move my right hand to the top corner to delete text, as well as allowing my left hand to use &lt;code>Option&lt;/code> or &lt;code>Command&lt;/code> to easily delete by word or line.&lt;/p>
&lt;p>I also added a few keys for browser navigation - browser forward/back, and next/previous tab.&lt;/p>
&lt;p>If you don&amp;rsquo;t use macOS, you could consider remapping the &lt;code>Left Alt&lt;/code> key to activate the left thumb layer instead, which would keep things symmetrical where both thumbs are used to activate layers.&lt;/p>
&lt;p>One caveat is that putting &lt;code>Control&lt;/code> on the &lt;code>Tab&lt;/code> key is a little weird for vim motions. It&amp;rsquo;s not the most comfortable and takes a while to get used to, but I&amp;rsquo;m okay sacrificing a bit here to keep the arrow keys in an easily accessible place.&lt;/p>

&lt;h2 id="nav-via-right-alt" class="anchor">
 &lt;a href="#nav-via-right-alt">
 Nav: via Right Alt
 &lt;/a>
&lt;/h2>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/layer-nav.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/11/remapping-standard-keyboard/layer-nav.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>By activating this layer with the right thumb, the keys turn to mouse navigation (similar to QMK Mouse Keys) for those times when you just need to nudge your mouse a bit, e.g. to the next input box or adjacent window. Using a mouse or dedicated pointing device is still much more efficient, but it&amp;rsquo;s nice to have this option for other situations.&lt;/p>
&lt;p>I also added cut/copy/paste here. Since I use a mouse with my left hand, this regains my ability to execute these operations with my non-mousing hand.&lt;/p>
&lt;p>For moving mouse keys and navigating tabs. Also adding cut/copy/command so I can use it along with left mouse.&lt;/p>

&lt;h1 id="karabiner-config" class="anchor">
 &lt;a href="#karabiner-config">
 Karabiner Config
 &lt;/a>
&lt;/h1>
&lt;p>Making complex modifications directly with Karabiner isn&amp;rsquo;t the easiest, where it requires manually writing JSON, which can be error prone and finicky. There are other tools to make it easier to create configs, and I recently started using &lt;a href="https://github.com/al-ce/karaml">karaml&lt;/a>.&lt;/p>
&lt;p>The &lt;code>karaml_config.yaml&lt;/code> looks like this (non-exhaustive), which then gets translated to Karabiner JSON format:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">/base/&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">caps_lock&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/extend/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">j + k&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">escape&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tab&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">left_control, tab]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">/extend/&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Vim motions, (x) means to include any optional mods&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">(x) | h&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">left&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">(x) | j&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">down&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">(x) | k&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">up&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">(x) | l&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">right&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">space&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">backspace&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">semicolon&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://github.com/yqrashawn/GokuRakuJoudo">GokuRakuJoudo&lt;/a> is another popular option, but I personally found the syntax to be confusing and too complicated for my simple mind to comprehend.&lt;/p>
&lt;p>For those on Linux, keyd is quite simple to configure in a similar way. However, I haven&amp;rsquo;t quite figured out how to chord the combos in the extend layer (e.g. to execute &lt;code>Shift + Control + Left&lt;/code>), which works fine in Karabiner though. So please comment if you know how to fix it!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="k">[ids]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">*&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[main]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">capslock&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">overload(extend, esc)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">j+k&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">esc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">tab&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">overload(control, tab)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">rightalt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">overload(nav, backspace)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[extend]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">leftalt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">s&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">leftshift&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">d&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">leftcontrol&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">f&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">leftmeta&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">left&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">j&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">down&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">k&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">l&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">right&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">space&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">backspace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[nav]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">u&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">C-S-tab&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">p&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">C-tab&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">A-left&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">o&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">A-right&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h1 id="closing-thoughts" class="anchor">
 &lt;a href="#closing-thoughts">
 Closing Thoughts
 &lt;/a>
&lt;/h1>
&lt;p>All in all, this was a successful experiment in improving the usability of a regular keyboard, and I&amp;rsquo;ll definitely continue to iterate on this layout as needed. Use a dedicated split keyboard is still (probably) superior, sometimes you need to make do with what you have, and hopefully this helps anyone who&amp;rsquo;s on that journey.&lt;/p>
&lt;p>Some resources referenced in this post:&lt;/p>
&lt;ul>
&lt;li>Karabiner configurations (&lt;a href="https://gist.github.com/justinmklam/3e389a6e06820ffaec1c2dea8381357b">Github Gist&lt;/a>)&lt;/li>
&lt;li>Keyboard Layout Editor visualizations:
&lt;ul>
&lt;li>&lt;a href="https://www.keyboard-layout-editor.com/##@_backcolor=%23e0e0e0&amp;amp;radii=18px&amp;amp;css=%2F@import%20url(http%2F:%2F%2F%2F%2Ffonts.googleapis.com%2F%2Fcss%3Ffamily%2F=Varela+Round)%2F%3B%0A%0A.keylabel%20%7B%0A%20%20%20%20font-family%2F:%20'volkswagen%2F_serialregular'%2F%3B%0A%7D%0A%0A%2F%2F*%20Strangely,%20%22Volkswagen%20Serial%22%20doesn't%20have%20a%20tilde%20character%20*%2F%2F%0A.varela%20%7B%20%0A%20%20%20%20font-family%2F:%20'Varela%20Round'%2F%3B%20%0A%20%20%20%20display%2F:%20inline-block%2F%3B%20%0A%20%20%20%20font-size%2F:%20inherit%2F%3B%20%0A%20%20%20%20text-rendering%2F:%20auto%2F%3B%20%0A%20%20%20%20-webkit-font-smoothing%2F:%20antialiased%2F%3B%20%0A%20%20%20%20-moz-osx-font-smoothing%2F:%20grayscale%2F%3B%0A%20%20%20%20transform%2F:%20translate(0,%200)%2F%3B%0A%7D%0A.varela-tilde%2F:after%20%7B%20content%2F:%20%22%5C07e%22%2F%3B%20%7D%3B&amp;amp;@_t=%23666666&amp;amp;p=CHICKLET&amp;amp;f:2&amp;amp;w:1.5%3B&amp;amp;=%0Aesc&amp;amp;_fa@:0&amp;amp;:0&amp;amp;:0&amp;amp;:1%3B%3B&amp;amp;=%0A%0A%0AF1&amp;amp;=%0A%0A%0AF2&amp;amp;=%0A%0A%0AF3&amp;amp;=%0A%0A%0AF4&amp;amp;=%0A%0A%0AF5&amp;amp;=%0A%0A%0AF6%0A%0A%0A%0A%0A~&amp;amp;=%0A%0A%0AF7&amp;amp;=%0A%0A%0AF8&amp;amp;=%0A%0A%0AF9&amp;amp;=%0A%0A%0AF10&amp;amp;=%0A%0A%0AF11&amp;amp;=%0A%0A%0AF12&amp;amp;_t=%23000000&amp;amp;a:7&amp;amp;f:3%3B&amp;amp;=%3B&amp;amp;@_t=%23666666&amp;amp;a:5&amp;amp;f:5%3B&amp;amp;=%0A%60%0A%0A%0A%0A%0A~&amp;amp;=!%0A1&amp;amp;=%2F@%0A2&amp;amp;=%23%0A3&amp;amp;=$%0A4&amp;amp;=%25%0A5&amp;amp;=%5E%0A6&amp;amp;=%2F&amp;amp;%0A7&amp;amp;=*%0A8&amp;amp;=(%0A9&amp;amp;=)%0A0&amp;amp;=%2F_%0A-&amp;amp;=+%0A%2F=&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:1.5%3B&amp;amp;=%0A%0A%0Adelete%3B&amp;amp;@_w:1.5%3B&amp;amp;=%0Atab&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=Q&amp;amp;=W&amp;amp;=E&amp;amp;=R&amp;amp;=T&amp;amp;=Y&amp;amp;=U&amp;amp;=I&amp;amp;=O&amp;amp;=P&amp;amp;_a:5%3B&amp;amp;=%7B%0A%5B&amp;amp;=%7D%0A%5D&amp;amp;=%7C%0A%5C%3B&amp;amp;@_a:4&amp;amp;f:2&amp;amp;w:1.75%3B&amp;amp;=%0Acaps%20lock&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=A&amp;amp;=S&amp;amp;=D&amp;amp;=F&amp;amp;=G&amp;amp;=H&amp;amp;=J&amp;amp;=K&amp;amp;=L&amp;amp;_a:5%3B&amp;amp;=%2F:%0A%2F%3B&amp;amp;=%22%0A'&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:1.75%3B&amp;amp;=%0A%0A%0Aenter%3B&amp;amp;@_w:2.25%3B&amp;amp;=%0Ashift&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=Z&amp;amp;=X&amp;amp;=C&amp;amp;=V&amp;amp;=B&amp;amp;=N&amp;amp;=M&amp;amp;_a:5%3B&amp;amp;=%3C%0A,&amp;amp;=%3E%0A.&amp;amp;=%3F%0A%2F%2F&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:2.25%3B&amp;amp;=%0A%0A%0Ashift%3B&amp;amp;@=%0Afn&amp;amp;=%0Acontrol&amp;amp;=alt%0Aoption&amp;amp;_fa@:3&amp;amp;:0&amp;amp;:0%3B&amp;amp;w:1.25%3B&amp;amp;=%E2%8C%98%0Acommand&amp;amp;_a:7&amp;amp;w:5%3B&amp;amp;=&amp;amp;_a:4&amp;amp;fa@:3&amp;amp;:0&amp;amp;:3%3B&amp;amp;w:1.25%3B&amp;amp;=%0A%0A%E2%8C%98%0Acommand&amp;amp;_fa@:3&amp;amp;:0&amp;amp;:1%3B%3B&amp;amp;=%0A%0Aalt%0Aoption&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=%E2%86%90&amp;amp;_h:0.55%3B&amp;amp;=%E2%86%91&amp;amp;=%E2%86%92%3B&amp;amp;@_y:-0.5499999999999998&amp;amp;x:12.5&amp;amp;h:0.55%3B&amp;amp;=%E2%86%93">Unmodified&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.keyboard-layout-editor.com/##@_backcolor=%23e0e0e0&amp;amp;radii=18px&amp;amp;css=%2F@import%20url(http%2F:%2F%2F%2F%2Ffonts.googleapis.com%2F%2Fcss%3Ffamily%2F=Varela+Round)%2F%3B%0A%0A.keylabel%20%7B%0A%20%20%20%20font-family%2F:%20'volkswagen%2F_serialregular'%2F%3B%0A%7D%0A%0A%2F%2F*%20Strangely,%20%22Volkswagen%20Serial%22%20doesn't%20have%20a%20tilde%20character%20*%2F%2F%0A.varela%20%7B%20%0A%20%20%20%20font-family%2F:%20'Varela%20Round'%2F%3B%20%0A%20%20%20%20display%2F:%20inline-block%2F%3B%20%0A%20%20%20%20font-size%2F:%20inherit%2F%3B%20%0A%20%20%20%20text-rendering%2F:%20auto%2F%3B%20%0A%20%20%20%20-webkit-font-smoothing%2F:%20antialiased%2F%3B%20%0A%20%20%20%20-moz-osx-font-smoothing%2F:%20grayscale%2F%3B%0A%20%20%20%20transform%2F:%20translate(0,%200)%2F%3B%0A%7D%0A.varela-tilde%2F:after%20%7B%20content%2F:%20%22%5C07e%22%2F%3B%20%7D%3B&amp;amp;@_t=%23666666&amp;amp;p=CHICKLET&amp;amp;f:2&amp;amp;w:1.5%3B&amp;amp;=%0Aesc&amp;amp;_fa@:0&amp;amp;:0&amp;amp;:0&amp;amp;:1%3B%3B&amp;amp;=%0A%0A%0AF1&amp;amp;=%0A%0A%0AF2&amp;amp;=%0A%0A%0AF3&amp;amp;=%0A%0A%0AF4&amp;amp;=%0A%0A%0AF5&amp;amp;=%0A%0A%0AF6%0A%0A%0A%0A%0A~&amp;amp;=%0A%0A%0AF7&amp;amp;=%0A%0A%0AF8&amp;amp;=%0A%0A%0AF9&amp;amp;=%0A%0A%0AF10&amp;amp;=%0A%0A%0AF11&amp;amp;=%0A%0A%0AF12&amp;amp;_t=%23000000&amp;amp;a:7&amp;amp;f:3%3B&amp;amp;=%3B&amp;amp;@_t=%23666666&amp;amp;a:5&amp;amp;f:5%3B&amp;amp;=%0A%60%0A%0A%0A%0A%0A~&amp;amp;=!%0A1&amp;amp;=%2F@%0A2&amp;amp;=%23%0A3&amp;amp;=$%0A4&amp;amp;=%25%0A5&amp;amp;=%5E%0A6&amp;amp;=%2F&amp;amp;%0A7&amp;amp;=*%0A8&amp;amp;=(%0A9&amp;amp;=)%0A0&amp;amp;=%2F_%0A-&amp;amp;=+%0A%2F=&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:1.5%3B&amp;amp;=%0A%0A%0Adelete%3B&amp;amp;@_c=%23f5d793&amp;amp;w:1.5%3B&amp;amp;=%0A%5Bcontrol%2F_t%5D%0A%0A%0A%0A%0Atab&amp;amp;_c=%23cccccc&amp;amp;a:7&amp;amp;f:5%3B&amp;amp;=Q&amp;amp;=W&amp;amp;=E&amp;amp;=R&amp;amp;=T&amp;amp;=Y&amp;amp;=U&amp;amp;=I&amp;amp;=O&amp;amp;=P&amp;amp;_a:5%3B&amp;amp;=%7B%0A%5B&amp;amp;=%7D%0A%5D&amp;amp;=%7C%0A%5C%3B&amp;amp;@_c=%23f5d793&amp;amp;a:4&amp;amp;f:2&amp;amp;w:1.75%3B&amp;amp;=%0Aextend&amp;amp;_c=%23cccccc&amp;amp;a:7&amp;amp;f:5%3B&amp;amp;=A&amp;amp;=S&amp;amp;=D&amp;amp;=F&amp;amp;=G&amp;amp;=H&amp;amp;=J&amp;amp;=K&amp;amp;=L&amp;amp;_a:5%3B&amp;amp;=%2F:%0A%2F%3B&amp;amp;=%22%0A'&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:1.75%3B&amp;amp;=%0A%0A%0Aenter%3B&amp;amp;@_w:2.25%3B&amp;amp;=%0Ashift&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=Z&amp;amp;=X&amp;amp;=C&amp;amp;=V&amp;amp;=B&amp;amp;=N&amp;amp;=M&amp;amp;_a:5%3B&amp;amp;=%3C%0A,&amp;amp;=%3E%0A.&amp;amp;=%3F%0A%2F%2F&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:2.25%3B&amp;amp;=%0A%0A%0Ashift%3B&amp;amp;@=%0Afn&amp;amp;=%0Acontrol&amp;amp;=alt%0Aoption&amp;amp;_fa@:3%3B&amp;amp;w:1.25%3B&amp;amp;=%E2%8C%98%0Acommand&amp;amp;_a:7&amp;amp;w:5%3B&amp;amp;=&amp;amp;_c=%23f5d793&amp;amp;a:4&amp;amp;w:1.25%3B&amp;amp;=%0A%0A%0Anav&amp;amp;_c=%23cccccc&amp;amp;fa@:3&amp;amp;:0&amp;amp;:1%3B%3B&amp;amp;=%0A%0Aalt%0Aoption&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=%E2%86%90&amp;amp;_h:0.55%3B&amp;amp;=%E2%86%91&amp;amp;=%E2%86%92%3B&amp;amp;@_y:-0.5499999999999998&amp;amp;x:12.5&amp;amp;h:0.55%3B&amp;amp;=%E2%86%93">Main Layer&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.keyboard-layout-editor.com/##@_backcolor=%23e0e0e0&amp;amp;radii=18px&amp;amp;css=%2F@import%20url(http%2F:%2F%2F%2F%2Ffonts.googleapis.com%2F%2Fcss%3Ffamily%2F=Varela+Round)%2F%3B%0A%0A.keylabel%20%7B%0A%20%20%20%20font-family%2F:%20'volkswagen%2F_serialregular'%2F%3B%0A%7D%0A%0A%2F%2F*%20Strangely,%20%22Volkswagen%20Serial%22%20doesn't%20have%20a%20tilde%20character%20*%2F%2F%0A.varela%20%7B%20%0A%20%20%20%20font-family%2F:%20'Varela%20Round'%2F%3B%20%0A%20%20%20%20display%2F:%20inline-block%2F%3B%20%0A%20%20%20%20font-size%2F:%20inherit%2F%3B%20%0A%20%20%20%20text-rendering%2F:%20auto%2F%3B%20%0A%20%20%20%20-webkit-font-smoothing%2F:%20antialiased%2F%3B%20%0A%20%20%20%20-moz-osx-font-smoothing%2F:%20grayscale%2F%3B%0A%20%20%20%20transform%2F:%20translate(0,%200)%2F%3B%0A%7D%0A.varela-tilde%2F:after%20%7B%20content%2F:%20%22%5C07e%22%2F%3B%20%7D%3B&amp;amp;@_t=%23666666&amp;amp;p=CHICKLET&amp;amp;f:2&amp;amp;w:1.5%3B&amp;amp;=%0Aesc&amp;amp;_fa@:0&amp;amp;:0&amp;amp;:0&amp;amp;:1%3B%3B&amp;amp;=%0A%0A%0AF1&amp;amp;=%0A%0A%0AF2&amp;amp;=%0A%0A%0AF3&amp;amp;=%0A%0A%0AF4&amp;amp;=%0A%0A%0AF5&amp;amp;=%0A%0A%0AF6%0A%0A%0A%0A%0A~&amp;amp;=%0A%0A%0AF7&amp;amp;=%0A%0A%0AF8&amp;amp;=%0A%0A%0AF9&amp;amp;=%0A%0A%0AF10&amp;amp;=%0A%0A%0AF11&amp;amp;=%0A%0A%0AF12&amp;amp;_t=%23000000&amp;amp;a:7&amp;amp;f:3%3B&amp;amp;=%3B&amp;amp;@_t=%23666666&amp;amp;a:5&amp;amp;f:5%3B&amp;amp;=%0A%60%0A%0A%0A%0A%0A~&amp;amp;=!%0A1&amp;amp;=%2F@%0A2&amp;amp;=%23%0A3&amp;amp;=$%0A4&amp;amp;=%25%0A5&amp;amp;=%5E%0A6&amp;amp;=%2F&amp;amp;%0A7&amp;amp;=*%0A8&amp;amp;=(%0A9&amp;amp;=)%0A0&amp;amp;=%2F_%0A-&amp;amp;=+%0A%2F=&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:1.5%3B&amp;amp;=%0A%0A%0Adelete%3B&amp;amp;@_w:1.5%3B&amp;amp;=%0A%5Bcontrol%2F_t%5D%0A%0A%0A%0A%0Atab&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=Q&amp;amp;=W&amp;amp;=E&amp;amp;=R&amp;amp;=T&amp;amp;=Y&amp;amp;_c=%23a9a3cf&amp;amp;a:4&amp;amp;f:2&amp;amp;fa@:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:5%3B%3B&amp;amp;=%0Acommand%0A%0A%0A%0A%0A%0A%0A%0A%E2%86%90&amp;amp;_fa@:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:3%3B%3B&amp;amp;=%0Actrl+shift%0A%0A%0A%0A%0A%0A%0A%0Atab&amp;amp;_a:5&amp;amp;fa@:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:3%3B%3B&amp;amp;=%0Actrl%0A%0A%0A%0A%0Atab&amp;amp;_a:4&amp;amp;fa@:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:0&amp;amp;:3&amp;amp;:0&amp;amp;:0&amp;amp;:5%3B%3B&amp;amp;=%0Acommand%0A%0A%0A%0A%0A%0A%0A%0A%E2%86%92&amp;amp;_c=%23cccccc&amp;amp;a:5&amp;amp;f:5%3B&amp;amp;=%7B%0A%5B&amp;amp;=%7D%0A%5D&amp;amp;=%7C%0A%5C%3B&amp;amp;@_c=%23e26757&amp;amp;a:4&amp;amp;f:2&amp;amp;w:1.75%3B&amp;amp;=%0Aextend&amp;amp;_c=%23abc6dc&amp;amp;f:5&amp;amp;f2:2%3B&amp;amp;=%0Acontrol&amp;amp;=%0Ashift&amp;amp;=%0Aoption&amp;amp;_fa@:3&amp;amp;:2%3B%3B&amp;amp;=%E2%8C%98%0Acommand&amp;amp;_c=%23cccccc&amp;amp;a:7&amp;amp;f:5%3B&amp;amp;=G&amp;amp;_c=%23e1ba44&amp;amp;f:5%3B&amp;amp;=%E2%86%90&amp;amp;_f:5%3B&amp;amp;=%E2%86%93&amp;amp;_f:5%3B&amp;amp;=%E2%86%91&amp;amp;_f:5%3B&amp;amp;=%E2%86%92&amp;amp;_c=%23cccccc&amp;amp;a:5&amp;amp;f:5%3B&amp;amp;=%2F:%0A%2F%3B&amp;amp;_f:5%3B&amp;amp;=%22%0A'&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:1.75%3B&amp;amp;=%0A%0A%0Aenter%3B&amp;amp;@_w:2.25%3B&amp;amp;=%0Ashift&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=Z&amp;amp;=X&amp;amp;=C&amp;amp;=V&amp;amp;=B&amp;amp;=N&amp;amp;=M&amp;amp;_a:5%3B&amp;amp;=%3C%0A,&amp;amp;=%3E%0A.&amp;amp;=%3F%0A%2F%2F&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:2.25%3B&amp;amp;=%0A%0A%0Ashift%3B&amp;amp;@=%0Afn&amp;amp;=%0Acontrol&amp;amp;=alt%0Aoption&amp;amp;_fa@:3%3B&amp;amp;w:1.25%3B&amp;amp;=%E2%8C%98%0Acommand&amp;amp;_c=%23ffb07b&amp;amp;a:5&amp;amp;w:5%3B&amp;amp;=%0Adelete&amp;amp;_c=%23cccccc&amp;amp;a:4&amp;amp;fa@:3&amp;amp;:0&amp;amp;:3%3B&amp;amp;w:1.25%3B&amp;amp;=%0A%0A%E2%8C%98%0Acommand&amp;amp;_fa@:3&amp;amp;:0&amp;amp;:1%3B%3B&amp;amp;=%0A%0Aalt%0Aoption&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=%E2%86%90&amp;amp;_h:0.55%3B&amp;amp;=%E2%86%91&amp;amp;=%E2%86%92%3B&amp;amp;@_y:-0.5499999999999998&amp;amp;x:12.5&amp;amp;h:0.55%3B&amp;amp;=%E2%86%93">Extend Layer&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.keyboard-layout-editor.com/##@_backcolor=%23e0e0e0&amp;amp;radii=18px&amp;amp;css=%2F@import%20url(http%2F:%2F%2F%2F%2Ffonts.googleapis.com%2F%2Fcss%3Ffamily%2F=Varela+Round)%2F%3B%0A%0A.keylabel%20%7B%0A%20%20%20%20font-family%2F:%20'volkswagen%2F_serialregular'%2F%3B%0A%7D%0A%0A%2F%2F*%20Strangely,%20%22Volkswagen%20Serial%22%20doesn't%20have%20a%20tilde%20character%20*%2F%2F%0A.varela%20%7B%20%0A%20%20%20%20font-family%2F:%20'Varela%20Round'%2F%3B%20%0A%20%20%20%20display%2F:%20inline-block%2F%3B%20%0A%20%20%20%20font-size%2F:%20inherit%2F%3B%20%0A%20%20%20%20text-rendering%2F:%20auto%2F%3B%20%0A%20%20%20%20-webkit-font-smoothing%2F:%20antialiased%2F%3B%20%0A%20%20%20%20-moz-osx-font-smoothing%2F:%20grayscale%2F%3B%0A%20%20%20%20transform%2F:%20translate(0,%200)%2F%3B%0A%7D%0A.varela-tilde%2F:after%20%7B%20content%2F:%20%22%5C07e%22%2F%3B%20%7D%3B&amp;amp;@_t=%23666666&amp;amp;p=CHICKLET&amp;amp;f:2&amp;amp;w:1.5%3B&amp;amp;=%0Aesc&amp;amp;_fa@:0&amp;amp;:0&amp;amp;:0&amp;amp;:1%3B%3B&amp;amp;=%0A%0A%0AF1&amp;amp;=%0A%0A%0AF2&amp;amp;=%0A%0A%0AF3&amp;amp;=%0A%0A%0AF4&amp;amp;=%0A%0A%0AF5&amp;amp;=%0A%0A%0AF6%0A%0A%0A%0A%0A~&amp;amp;=%0A%0A%0AF7&amp;amp;=%0A%0A%0AF8&amp;amp;=%0A%0A%0AF9&amp;amp;=%0A%0A%0AF10&amp;amp;=%0A%0A%0AF11&amp;amp;=%0A%0A%0AF12&amp;amp;_t=%23000000&amp;amp;a:7&amp;amp;f:3%3B&amp;amp;=%3B&amp;amp;@_t=%23666666&amp;amp;a:5&amp;amp;f:5%3B&amp;amp;=%0A%60%0A%0A%0A%0A%0A~&amp;amp;=!%0A1&amp;amp;=%2F@%0A2&amp;amp;=%23%0A3&amp;amp;=$%0A4&amp;amp;=%25%0A5&amp;amp;=%5E%0A6&amp;amp;=%2F&amp;amp;%0A7&amp;amp;=*%0A8&amp;amp;=(%0A9&amp;amp;=)%0A0&amp;amp;=%2F_%0A-&amp;amp;=+%0A%2F=&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:1.5%3B&amp;amp;=%0A%0A%0Adelete%3B&amp;amp;@_w:1.5%3B&amp;amp;=%0A%5Bcontrol%2F_t%5D%0A%0A%0A%0A%0Atab&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=Q&amp;amp;=W&amp;amp;=E&amp;amp;=R&amp;amp;=T&amp;amp;=Y&amp;amp;=U&amp;amp;_c=%23ffb07b&amp;amp;f:4%3B&amp;amp;=cut&amp;amp;=copy&amp;amp;=paste&amp;amp;_c=%23cccccc&amp;amp;a:5&amp;amp;f:5%3B&amp;amp;=%7B%0A%5B&amp;amp;=%7D%0A%5D&amp;amp;=%7C%0A%5C%3B&amp;amp;@_a:4&amp;amp;f:2&amp;amp;w:1.75%3B&amp;amp;=%0Aextend&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=A&amp;amp;=S&amp;amp;_c=%23d290b4&amp;amp;a:5&amp;amp;f:2%3B&amp;amp;=mouse%0Aclick%0A%0A%0A%0A%0Aleft&amp;amp;=mouse%0Aclick%0A%0A%0A%0A%0Aright&amp;amp;_c=%23cccccc&amp;amp;a:7&amp;amp;f:5%3B&amp;amp;=G&amp;amp;_c=%2393c9b7&amp;amp;a:5&amp;amp;fa@:2&amp;amp;:0%3B%3B&amp;amp;=mouse%0A%0A%0A%0A%0A%0A%E2%86%90&amp;amp;=mouse%0A%0A%0A%0A%0A%0A%E2%86%93&amp;amp;=mouse%0A%0A%0A%0A%0A%0A%E2%86%91&amp;amp;=mouse%0A%0A%0A%0A%0A%0A%E2%86%92&amp;amp;_c=%23cccccc&amp;amp;f:5%3B&amp;amp;=%2F:%0A%2F%3B&amp;amp;_f:5%3B&amp;amp;=%22%0A'&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:1.75%3B&amp;amp;=%0A%0A%0Aenter%3B&amp;amp;@_w:2.25%3B&amp;amp;=%0Ashift&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=Z&amp;amp;=X&amp;amp;=C&amp;amp;=V&amp;amp;=B&amp;amp;=N&amp;amp;=M&amp;amp;_a:5%3B&amp;amp;=%3C%0A,&amp;amp;=%3E%0A.&amp;amp;=%3F%0A%2F%2F&amp;amp;_a:4&amp;amp;f:2&amp;amp;w:2.25%3B&amp;amp;=%0A%0A%0Ashift%3B&amp;amp;@=%0Afn&amp;amp;=%0Acontrol&amp;amp;=alt%0Aoption&amp;amp;_fa@:3%3B&amp;amp;w:1.25%3B&amp;amp;=%E2%8C%98%0Acommand&amp;amp;_a:7&amp;amp;w:5%3B&amp;amp;=&amp;amp;_c=%23e26757&amp;amp;a:4&amp;amp;w:1.25%3B&amp;amp;=%0A%0A%0Anav&amp;amp;_c=%23cccccc&amp;amp;fa@:3&amp;amp;:0&amp;amp;:1%3B%3B&amp;amp;=%0A%0Aalt%0Aoption&amp;amp;_a:7&amp;amp;f:5%3B&amp;amp;=%E2%86%90&amp;amp;_h:0.55%3B&amp;amp;=%E2%86%91&amp;amp;=%E2%86%92%3B&amp;amp;@_y:-0.5499999999999998&amp;amp;x:12.5&amp;amp;h:0.55%3B&amp;amp;=%E2%86%93">Nav Layer&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>Happy typing!&lt;/p></content:encoded></item><item><title>Replacing Disqus With Giscus and Github Discussions</title><link>https://justinmklam.com/posts/2025/08/replacing-disqus-with-giscus/</link><pubDate>Tue, 26 Aug 2025 21:08:26 -0700</pubDate><guid>https://justinmklam.com/posts/2025/08/replacing-disqus-with-giscus/</guid><description>&lt;p>As you might have noticed, I&amp;rsquo;ve put this website through a number of changes this year. Apart from changing the theme (sorry if you hate blue), I also switched my website metrics from Google Analytics to &lt;a href="https://umami.is/">Umami&lt;/a>, which is an open source, privacy focused analytics platform.&lt;/p>
&lt;p>Along the same vain, it was finally time to move on from Disqus.&lt;/p></description><content:encoded>&lt;p>As you might have noticed, I&amp;rsquo;ve put this website through a number of changes this year. Apart from changing the theme (sorry if you hate blue), I also switched my website metrics from Google Analytics to &lt;a href="https://umami.is/">Umami&lt;/a>, which is an open source, privacy focused analytics platform.&lt;/p>
&lt;p>Along the same vain, it was finally time to move on from Disqus.&lt;/p>
&lt;p>Disqus was the go-to commenting platform back in the 2010s, but over time they&amp;rsquo;ve become focused on profiting through &lt;a href="https://markosaric.com/remove-disqus/">selling user data and advertisements&lt;/a>, as well as just being &lt;a href="https://victorzhou.com/blog/replacing-disqus/">very bloated&lt;/a>. The final straw was adding advertisements to their free plan, and despite them not enabling it (yet) for small, non-commercial websites like this one, it&amp;rsquo;s likely only a matter of time before they go that direction.&lt;/p>

&lt;h1 id="prior-art" class="anchor">
 &lt;a href="#prior-art">
 Prior Art
 &lt;/a>
&lt;/h1>
&lt;p>There are a number of cloud and self-hosted Disqus alternatives, but I wanted something open source, simple, no-frills, and low maintenance. For this static site hosted on Github Pages, it seemed silly to self-host a comment platform like &lt;a href="https://github.com/isso-comments/isso">Isso&lt;/a> or &lt;a href="https://github.com/adtac/commento">Commento&lt;/a> just for the &amp;lt;100 comments I have.&lt;/p>
&lt;p>&lt;a href="https://github.com/utterance/utterances">Utterances&lt;/a> was previously a common choice which uses Github Issues, but I settled on &lt;a href="https://github.com/giscus/giscus">Giscus&lt;/a> since it uses the (more logical) Github Discussions component instead. It also seemed to be the go-to choice these days amongst bloggers, as well as being the most actively maintained of these tools.&lt;/p>
&lt;p>One caveat to using a Github-based tool is that users need to have a Github account to comment and react to a post. I mostly write about tech, so most of my users likely already have Github accounts, so this isn&amp;rsquo;t too much of an issue for me. It does add a friction point of not having easy login options like Google or other social media accounts, though I don&amp;rsquo;t get a ton of comments on my blog anyway, so again, something I can live with.&lt;/p>

&lt;h1 id="setup" class="anchor">
 &lt;a href="#setup">
 Setup
 &lt;/a>
&lt;/h1>

&lt;h2 id="configuring-giscus" class="anchor">
 &lt;a href="#configuring-giscus">
 Configuring Giscus
 &lt;/a>
&lt;/h2>
&lt;p>Setting up Giscus was quite straightforward, the instructions on &lt;a href="https://giscus.app/">their homepage&lt;/a> were easy to follow. Once the Giscus app was installed and Discussions enabled on my repo, I copied the generated snippet to my website, replacing where Disqus would go.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;https://giscus.app/client.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-repo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;[ENTER REPO HERE]&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-repo-id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;[ENTER REPO ID HERE]&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-category&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;[ENTER CATEGORY NAME HERE]&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-category-id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;[ENTER CATEGORY ID HERE]&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-mapping&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;pathname&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-strict&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-reactions-enabled&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-emit-metadata&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-input-position&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;bottom&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;preferred_color_scheme&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">data-lang&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;en&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">crossorigin&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;anonymous&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="na">async&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="counting-comments" class="anchor">
 &lt;a href="#counting-comments">
 Counting Comments
 &lt;/a>
&lt;/h2>
&lt;p>One of the nice things about Disqus was its &amp;ldquo;built-in&amp;rdquo; comment counter. This was enabled with a snippet like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- The placeholder to be replaced... --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">span&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;disqus-comment-count&amp;#34;&lt;/span> &lt;span class="na">data-disqus-url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ .Permalink }}#disqus_thread&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>0 comments&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- ... From this script --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">defer&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;dsq-count-scr&amp;#34;&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;//[SHORTNAME].disqus.com/count.js&amp;#34;&lt;/span> &lt;span class="na">async&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Giscus doesn&amp;rsquo;t have a built in option for that (see &lt;a href="https://github.com/orgs/giscus/discussions/113">this issue&lt;/a>, but I was able to achieve it fairly easily with a bit of Javascript. In my Hugo template:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- The placeholder to be replaced... --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">span&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;giscus-comment-count-{{ $context.File.UniqueID }}&amp;#34;&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;giscus-comment-count&amp;#34;&lt;/span> &lt;span class="na">data-url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $context.RelPermalink }}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $context.RelPermalink }}#comments&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>0 comments&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- ... From this script --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ $giscusJs := resources.Get &amp;#34;js/giscus-comments.js&amp;#34; }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ if $giscusJs }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ $giscusJs := $giscusJs | resources.Minify }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $giscusJs.RelPermalink }}&amp;#34;&lt;/span> &lt;span class="na">defer&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ end }}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And in &lt;code>giscus-comments.js&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Giscus Comment Count Manager
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Uses PostMessage API to get comment counts from Giscus iframes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">class&lt;/span> &lt;span class="nx">GiscusCommentManager&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">constructor&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setupMessageListener&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">setupMessageListener&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">origin&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s2">&amp;#34;https://giscus.app&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">typeof&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">giscus&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">giscusData&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">giscus&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">currentPath&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">location&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">pathname&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Check if this contains discussion data with comment count
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;discussion&amp;#34;&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="nx">giscusData&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">discussion&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">giscusData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">discussion&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">discussion&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="k">typeof&lt;/span> &lt;span class="nx">discussion&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">totalCommentCount&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s2">&amp;#34;number&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Calculate total count including both comments and replies
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">commentCount&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">discussion&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">totalCommentCount&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">replyCount&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">discussion&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">totalReplyCount&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">totalCount&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">commentCount&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">replyCount&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Update comment count displays on current page
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">updateCommentCountDisplays&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">currentPath&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">totalCount&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`Giscus: Page &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">currentPath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> has &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">commentCount&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> comments and &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">replyCount&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> replies (&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">totalCount&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> total)`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">discussion&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Discussion doesn&amp;#39;t exist (404), default to 0 comments
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">updateCommentCountDisplays&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">currentPath&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`Giscus: Page &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">currentPath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> has no discussion (defaulting to 0 comments)`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Handle error messages (e.g., when discussion doesn&amp;#39;t exist)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;error&amp;#34;&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="nx">giscusData&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">error&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">giscusData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">error&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Default to 0 comments on any error
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">updateCommentCountDisplays&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">currentPath&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">updateCommentCountDisplays&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">url&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">count&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Update comment count elements for this URL on the current page
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">elements&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`[data-url=&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">url&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">&amp;#34;]`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">elements&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">element&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">link&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">element&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;a&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">element&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">count&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">link&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">innerHTML&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;0 comments&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">count&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">link&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">innerHTML&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;1 comment&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">link&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">innerHTML&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">count&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> comments`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Initialize comment count manager
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">initialize&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// The PostMessage listener will automatically update counts when Giscus loads
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// No need for stored counts - fresh data every time
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Initialize the comment manager when DOM is loaded
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;DOMContentLoaded&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">giscusCommentManager&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GiscusCommentManager&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">giscusCommentManager&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">initialize&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One mildly annoying thing is that this counter can take a few seconds to update since the page needs to wait for the Giscus widget to fully load, but it&amp;rsquo;s a small (and acceptable) price to pay to avoid having user data being tracked and sold!&lt;/p>

&lt;h1 id="the-great-migration" class="anchor">
 &lt;a href="#the-great-migration">
 The Great Migration
 &lt;/a>
&lt;/h1>
&lt;p>Giscus unfortunately doesn&amp;rsquo;t have a built in way to migrate comments from Disqus to Github Discussions. There are quite a few blog posts about this topic, but they were in languages like Java, Ruby, .NET, none of which I have set up on my laptop. Some of them also did things like attempting to map the Disqus usernames to Github users, but I didn&amp;rsquo;t want to bother with that since the hit rate seems low anyway.&lt;/p>
&lt;p>Predominantly being a Python developer, I figured this would also be a good opportunity to see how far &lt;em>vibe coding&lt;/em> can take me through this problem and use it to create a script tailored to my heart&amp;rsquo;s desires.&lt;/p>

&lt;h2 id="exporting-from-disqus" class="anchor">
 &lt;a href="#exporting-from-disqus">
 Exporting from Disqus
 &lt;/a>
&lt;/h2>
&lt;p>Fortunately, Disqus allows &lt;a href="https://help.disqus.com/en/articles/1717164-comments-export">exporting comments&lt;/a> to XML. After requesting an export &lt;a href="http://disqus.com/admin/discussions/export/">here&lt;/a>, I shortly received an email with the contents.&lt;/p>

&lt;h2 id="converting-disqus-to-github-discussions" class="anchor">
 &lt;a href="#converting-disqus-to-github-discussions">
 Converting Disqus to Github Discussions
 &lt;/a>
&lt;/h2>
&lt;p>There were a few tricky parts with the conversion:&lt;/p>
&lt;ul>
&lt;li>Disqus has support for multiple thread levels, Github only has one level&lt;/li>
&lt;li>Export only has usernames of the commenters&lt;/li>
&lt;li>Github has a GraphQL rate limit of 5000 requests/hr&lt;/li>
&lt;li>Unless you create a separate Github account, all imported comments will show up under your Github user&lt;/li>
&lt;/ul>
&lt;p>&lt;del>The last point might be a deal breaker for some, but I wanted to keep this migration as simple as possible, so I dealt with it. The imported comments will have a header that shows who and when it would be posted, which is sufficient enough for my needs.&lt;/del> Update: I added support for posting via a Github App, which removes this issue!&lt;/p>
&lt;p>The full script is in &lt;a href="https://gist.github.com/justinmklam/59a6c72d98ffc0a67948e254286114ae">this gist&lt;/a>, and it was definitely way more lines than I thought it&amp;rsquo;d be. But it does have some nice features like:&lt;/p>
&lt;ul>
&lt;li>Having a &lt;code>--dry-run&lt;/code> flag to test parsing without hitting the Github API&lt;/li>
&lt;li>Idempotency by keeping track of published comments in a state file&lt;/li>
&lt;/ul>
&lt;p>The vibe coding did take quite a few back-and-forths to get right, but I&amp;rsquo;m overall impressed with the output and speed.&lt;/p>
&lt;details>
&lt;summary>Script Usage (click to expand):&lt;/summary>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">$ uv run disqus_to_giscus.py --help
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">usage: disqus_to_giscus.py &lt;span class="o">[&lt;/span>-h&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--repo-owner REPO_OWNER&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--repo-name REPO_NAME&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--category-name CATEGORY_NAME&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--dry-run&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--output OUTPUT&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--state-file STATE_FILE&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">[&lt;/span>--app-id APP_ID&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--private-key-path PRIVATE_KEY_PATH&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--installation-id INSTALLATION_ID&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> xml_file
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Migrate Disqus comments to GitHub Discussions
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">positional arguments:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> xml_file Path to Disqus XML &lt;span class="nb">export&lt;/span> file
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">options:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -h, --help show this &lt;span class="nb">help&lt;/span> message and &lt;span class="nb">exit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --repo-owner REPO_OWNER
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GitHub repository owner/organization name &lt;span class="o">(&lt;/span>required &lt;span class="k">for&lt;/span> real migration&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --repo-name REPO_NAME
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GitHub repository name &lt;span class="o">(&lt;/span>required &lt;span class="k">for&lt;/span> real migration&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --category-name CATEGORY_NAME
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GitHub Discussion category name &lt;span class="o">(&lt;/span>default: Announcements&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --dry-run Preview migration without posting to GitHub &lt;span class="o">(&lt;/span>recommended&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --output OUTPUT Output file &lt;span class="k">for&lt;/span> dry run preview &lt;span class="o">(&lt;/span>default: migration_preview.md&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --state-file STATE_FILE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> State file &lt;span class="k">for&lt;/span> tracking migration progress &lt;span class="o">(&lt;/span>default: migration_state.json&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">GitHub App Authentication:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Use GitHub App &lt;span class="k">for&lt;/span> authentication instead of personal access token
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --app-id APP_ID GitHub App ID &lt;span class="o">(&lt;/span>can also be &lt;span class="nb">set&lt;/span> via GITHUB_APP_ID environment variable&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --private-key-path PRIVATE_KEY_PATH
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Path to GitHub App private key file &lt;span class="o">(&lt;/span>can also be &lt;span class="nb">set&lt;/span> via GITHUB_APP_PRIVATE_KEY_PATH environment variable&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --installation-id INSTALLATION_ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GitHub App installation ID &lt;span class="o">(&lt;/span>can also be &lt;span class="nb">set&lt;/span> via GITHUB_APP_INSTALLATION_ID environment variable&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Environment Variables:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Authentication &lt;span class="o">(&lt;/span>choose one&lt;span class="o">)&lt;/span>:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GITHUB_TOKEN Personal access token &lt;span class="k">for&lt;/span> GitHub API
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OR &lt;span class="k">for&lt;/span> GitHub App authentication:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GITHUB_APP_ID GitHub App ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GITHUB_APP_PRIVATE_KEY_PATH Path to GitHub App private key file
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> GITHUB_APP_INSTALLATION_ID GitHub App installation ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">State Tracking:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> The script maintains a &lt;span class="nb">local&lt;/span> state file &lt;span class="o">(&lt;/span>migration_state.json by default&lt;span class="o">)&lt;/span> to track
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> which discussions and comments have been successfully created. This makes the script
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> idempotent - you can safely re-run it after failures and it will resume where it
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> left off without creating duplicates.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Authentication Methods:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1. Personal Access Token &lt;span class="o">(&lt;/span>original method&lt;span class="o">)&lt;/span>:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - Set GITHUB_TOKEN environment variable
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - Token needs &lt;span class="s1">&amp;#39;repo&amp;#39;&lt;/span> and &lt;span class="s1">&amp;#39;write:discussion&amp;#39;&lt;/span> scopes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 2. GitHub App &lt;span class="o">(&lt;/span>recommended &lt;span class="k">for&lt;/span> organizations&lt;span class="o">)&lt;/span>:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - Create a GitHub App with discussions:write permission
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - Install the app on your repository/organization
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - Provide --app-id, --private-key-path, --installation-id
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> - Or &lt;span class="nb">set&lt;/span> corresponding environment variables
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Examples:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Dry run (recommended first)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> python disqus_to_giscus.py export.xml --dry-run
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Real migration with personal access token&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">export&lt;/span> &lt;span class="nv">GITHUB_TOKEN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your_token_here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> python disqus_to_giscus.py export.xml --repo-owner myusername --repo-name myrepo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Real migration with GitHub App&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> python disqus_to_giscus.py export.xml --repo-owner myusername --repo-name myrepo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --app-id &lt;span class="m">123456&lt;/span> --private-key-path /path/to/private-key.pem --installation-id &lt;span class="m">789012&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># With custom category name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> python disqus_to_giscus.py export.xml --repo-owner myusername --repo-name myrepo --category-name &lt;span class="s2">&amp;#34;General&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Custom output file for dry run&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> python disqus_to_giscus.py export.xml --dry-run --output custom_preview.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Custom state file location&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> python disqus_to_giscus.py export.xml --repo-owner myusername --repo-name myrepo --state-file my_migration.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;p>Using the &lt;code>--dry-run&lt;/code> mode will export the parsed comments into a &lt;code>migration_preview.md&lt;/code> file, in case you want to edit any processing/formatting to suit your own needs.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">$ uv run disqus_to_giscus.py justinmklam-2025-08-26T15_34_53.793894-all.xml --dry-run
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">🚀 Starting Disqus to GitHub Discussions migration
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">📖 Parsing Disqus XML: justinmklam-2025-08-26T15_34_53.793894-all.xml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">✅ Parsed &lt;span class="m">277&lt;/span> threads with comments
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">🔍 Performing dry run - generating preview to migration_preview.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">📊 Existing state: &lt;span class="m">4&lt;/span> discussions, &lt;span class="m">28&lt;/span> comments already created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">✅ Dry run complete! Preview saved to migration_preview.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">📊 Summary: &lt;span class="m">8&lt;/span> new threads ready &lt;span class="k">for&lt;/span> migration, &lt;span class="m">4&lt;/span> already exist
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">🚫 Skipped &lt;span class="m">265&lt;/span> threads with no comments
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h3 id="option-1-posting-from-a-personal-account" class="anchor">
 &lt;a href="#option-1-posting-from-a-personal-account">
 Option 1: Posting from a Personal Account
 &lt;/a>
&lt;/h3>
&lt;p>If you want to avoid having to go through the extra steps of creating a Github App, and are okay with all the imported comments being under your account, then this method is for you. If not, move on to the next section!&lt;/p>
&lt;p>For actually running the migration to Github Discussions, be sure to create a new &lt;a href="https://github.com/settings/personal-access-tokens">personal access token&lt;/a> and set it as &lt;code>GITHUB_TOKEN&lt;/code> in your environment.&lt;/p>
&lt;p>I would recommend testing it out on a new repository first to make sure everything works and looks as expected. I did this with a new &lt;code>giscus-import-test&lt;/code> repo, and after verifying the results, I changed the repo to the final repo.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">$ uv run disqus_to_giscus.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> justinmklam-2025-08-26T15_34_53.793894-all.xml &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --repo-owner justinmklam &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --repo-name personal-blog &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --category-name &lt;span class="s2">&amp;#34;Blog Comments&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, all my comments are in this website&amp;rsquo;s repo under the Discussions tab &lt;a href="https://github.com/justinmklam/personal-blog/discussions">here&lt;/a>!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/replacing-disqus-with-giscus/discussions.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/replacing-disqus-with-giscus/discussions.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Code and user comments, all in one repo!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>As mentioned before, the comment threads all appear under my Github handle, which is not ideal but it&amp;rsquo;s an acceptable trade-off for a free, no-hassle commenting platform.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/replacing-disqus-with-giscus/comment-example.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/replacing-disqus-with-giscus/comment-example.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Example of an imported comment thread, but all under your own account.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="option-2-posting-from-a-github-app" class="anchor">
 &lt;a href="#option-2-posting-from-a-github-app">
 Option 2: Posting from a Github App
 &lt;/a>
&lt;/h3>
&lt;p>After doing the previous option, I did a bit more reading and found out that creating a Github App to do the posting isn&amp;rsquo;t actually that much more work, and the end result is much cleaner since the comments would then be posted under a bot account. This makes it much clearer that the comments are imported from another platform, and that it&amp;rsquo;s not just you having a conversation with yourself.&lt;/p>
&lt;p>I should probably have just started with this since I did have to delete all the previously created discussions, but it was pretty quick and the end result is much nicer.&lt;/p>
&lt;p>Steps to set up a Github App:&lt;/p>
&lt;ol>
&lt;li>Go to your Github &lt;a href="https://github.com/settings/apps">developer settings&lt;/a> and create a new app
&lt;ol>
&lt;li>Give it a name&lt;/li>
&lt;li>Set a homepage url (this can be anything)&lt;/li>
&lt;li>Disable webhooks&lt;/li>
&lt;li>Set repository permissions to have read/write access to Discussions&lt;/li>
&lt;li>Upload a custom logo if desired&lt;/li>
&lt;li>Generate a private key and download it (&lt;code>*.private-key.pem&lt;/code> file)&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>Install the app to your target repository&lt;/li>
&lt;li>Make note of the app&amp;rsquo;s:
&lt;ol>
&lt;li>App ID -&amp;gt; from the app&amp;rsquo;s about page&lt;/li>
&lt;li>Installation ID -&amp;gt; go to your &lt;a href="https://github.com/settings/installations">Applications&lt;/a>, click &amp;ldquo;Configure&amp;rdquo; for your app, then use the numeric id in the URL path&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ol>
&lt;p>Then run the same script, but with a few different options (and this time, &lt;code>GITHUB_TOKEN&lt;/code> isn&amp;rsquo;t required):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">$ uv run disqus_to_giscus.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> justinmklam-2025-08-26T15_34_53.793894-all.xml &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --repo-owner justinmklam &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --repo-name personal-blog &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --category-name &lt;span class="s2">&amp;#34;Blog Comments&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --app-id &lt;span class="m">12345&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --private-key-path giscus-comment-bot.2025-08-27.private-key.pem &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --installation-id &lt;span class="m">67890&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/replacing-disqus-with-giscus/image.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/replacing-disqus-with-giscus/image.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Example of an imported comment thread using a Github App. Nice!&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="closing-thoughts" class="anchor">
 &lt;a href="#closing-thoughts">
 Closing Thoughts
 &lt;/a>
&lt;/h1>
&lt;p>I was able to get the migration done in an evening, so I&amp;rsquo;d call that a success! Claude Code definitely did a lot of heavy lifting, but it was a neat experiment in using AI for menial, one-off automations that would normally be more painful and time consuming. And now, all my website code and comments are in &lt;a href="https://github.com/justinmklam/personal-blog">one repository&lt;/a>, free of advertisements and selling of user data.&lt;/p>
&lt;p>Hope this helps anyone else planning to migrate away from Disqus!&lt;/p></content:encoded></item><item><title>Handwiring a 36-Key Split, Sculpted Keyboard</title><link>https://justinmklam.com/posts/2025/08/handwired-skeletyl/</link><pubDate>Tue, 05 Aug 2025 14:35:20 -0700</pubDate><guid>https://justinmklam.com/posts/2025/08/handwired-skeletyl/</guid><description>&lt;p>As I continued the descent into my split keyboard addiction, I decided to dive off the deep end and handwire my own ergonomic keyboard.&lt;/p>
&lt;p>My &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard">Corne&lt;/a> was a good intro keyboard, but I eventually determined it had more keys than &lt;a href="https://justinmklam.com/posts/2025/07/36-key-layout">I actually needed&lt;/a>. I also grew intrigued towards keyboards with better ergonomics - more stagger and splay to improve alignment with ring and pinky fingers, as well as keyboards with sculpted, organic surfaces to better follow my hand’s natural shape.&lt;/p></description><content:encoded>&lt;p>As I continued the descent into my split keyboard addiction, I decided to dive off the deep end and handwire my own ergonomic keyboard.&lt;/p>
&lt;p>My &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard">Corne&lt;/a> was a good intro keyboard, but I eventually determined it had more keys than &lt;a href="https://justinmklam.com/posts/2025/07/36-key-layout">I actually needed&lt;/a>. I also grew intrigued towards keyboards with better ergonomics - more stagger and splay to improve alignment with ring and pinky fingers, as well as keyboards with sculpted, organic surfaces to better follow my hand’s natural shape.&lt;/p>
&lt;p>Other keyboards like the &lt;a href="https://github.com/Bastardkb/Charybdis">Charybdis&lt;/a> seemed to be quite popular, but I was searching for something smaller and less bulky looking. I also didn’t want to design my own using &lt;a href="https://ryanis.cool/cosmos/">Cosmos&lt;/a> just yet, since I knew it’d take a few iterations to get right which would be difficult without my own 3D printer.&lt;/p>
&lt;p>The &lt;a href="https://github.com/Bastardkb/Skeletyl">Skeletyl&lt;/a> spoke to me on an spiritual level, and although it lacked an integrated pointing device like a trackpoint, it checked enough boxes for me to be sold on that form factor.&lt;/p>
&lt;p>Since I had never used a keyboard like this before, I was hesitant to put too much money into it. Prebuilt options and kits were going for &amp;gt;$300 CAD, which was more than I was willing to empty my wallet for something that might just be a fad for me. Although I could have tried sourcing the PCBs myself, this keyboard was a perfect candidate to try handwiring due to its complex shape as well as keeping costs low since I had most of the equipment already.&lt;/p>
&lt;p>So with my soldering iron and wires in hand, I began my quest.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/DSCF1925_hu_488b124678cd7a7a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/DSCF1925_hu_488b124678cd7a7a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The completed Skeletyl in its natural habitat.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/DSCF1888_hu_ce142beae42ffe59.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/DSCF1888_hu_ce142beae42ffe59.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Close up of the left half. The sparkle pattern came through quite well!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/DSCF1900_hu_fad96c8e5c1b93f5.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/DSCF1900_hu_fad96c8e5c1b93f5.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Backside of the keyboard showing the USB-C and TRS ports.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="the-build" class="anchor">
 &lt;a href="#the-build">
 The Build
 &lt;/a>
&lt;/h1>

&lt;h2 id="bill-of-materials" class="anchor">
 &lt;a href="#bill-of-materials">
 Bill of Materials
 &lt;/a>
&lt;/h2>
&lt;p>My friend from &lt;a href="https://www.emberprototypes.com/">Ember Prototypes&lt;/a> was kind enough to print the case for me, but if you don&amp;rsquo;t have access to a 3D printer yourself or a friend who does, many local libraries have them and are usually very affordable. Otherwise, places like JLCPCB offer printing for not too much.&lt;/p>
&lt;p>Total cost for my build was &lt;strong>~$70 CAD&lt;/strong>, where $45 of it was from the keycaps and (relatively expensive) switches. Aside from the case, all parts were sourced from Amazon and Aliexpress.&lt;/p>
&lt;p>If you&amp;rsquo;re looking to build one yourself, based on the parts I found it can range from &lt;strong>$45-100 CAD&lt;/strong>. Getting the build to be &amp;lt;$40 would likely be possible if you hunt a bit harder for better deals, especially since switches and keycaps make up for a large portion of the overall cost. Alternatively, 3D printing your own keycaps is an option to further save some money.&lt;/p>
&lt;p>For my build, I chose the same TTC Frozen Silent v2 switches that I used on my &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard">Corne keyboard&lt;/a>, and I bought blank PBT in the XDA profile. I tried Cherry/OEM profile, but I wasn’t a fan of feel or look of the sharp corners.&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Part&lt;/th>
 &lt;th>Quantity&lt;/th>
 &lt;th>Cost (in CAD)&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>3D printed case (top + bottom)&lt;/td>
 &lt;td>2&lt;/td>
 &lt;td>$0-15&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>M4 heat-set threaded inserts&lt;/td>
 &lt;td>12&lt;/td>
 &lt;td>$2&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>M4 5mm fasteners&lt;/td>
 &lt;td>12&lt;/td>
 &lt;td>$3&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1N4148 diodes (through hole)&lt;/td>
 &lt;td>36&lt;/td>
 &lt;td>$1&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TRS/TRRS connectors&lt;/td>
 &lt;td>2&lt;/td>
 &lt;td>$3&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RP2040 Zero dev board&lt;/td>
 &lt;td>2&lt;/td>
 &lt;td>$12&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TRS cable&lt;/td>
 &lt;td>1&lt;/td>
 &lt;td>$4&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MX switches&lt;/td>
 &lt;td>36&lt;/td>
 &lt;td>$5-30&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MX-compatible keycaps&lt;/td>
 &lt;td>36&lt;/td>
 &lt;td>$15-30&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>

&lt;h2 id="schematic" class="anchor">
 &lt;a href="#schematic">
 Schematic
 &lt;/a>
&lt;/h2>
&lt;p>The wiring followed a basic row/column matrix, where the diodes were connected along the rows, and the remaining switch legs were wired directly together in columns.&lt;/p>
&lt;p>On the MCU, the pins required were:&lt;/p>
&lt;ul>
&lt;li>Switch matrix, 4 rows x 5 columns = &lt;strong>9 GPIO pins&lt;/strong>&lt;/li>
&lt;li>TRS connector (soft serial) = &lt;strong>1 GPIO pin, VCC, GND&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/rp2040.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/rp2040.png>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">RP2040 Pinout. (Source: &lt;a href=https://www.waveshare.com/rp2040-zero.htm>Waveshare&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>In order for both sides to communicate with each other, there are a few different ways that &lt;a href="https://docs.qmk.fm/features/split_keyboard">QMK supports&lt;/a>. I opted for the simplest of using soft serial so only three wires were required. For the &lt;a href="https://docs.qmk.fm/platformdev_rp2040">RP2040&lt;/a>, the soft serial pin (using the PIO driver) needed to be &lt;code>GP1&lt;/code>.&lt;/p>
&lt;p>For the matrix columns and rows, I chose the pins on the right side to make it a bit easier to assemble. Any valid pins can be used, as long as they&amp;rsquo;re mapped correctly in the QMK &lt;code>keyboard.json&lt;/code>, where mine looks something like this (full configuration &lt;a href="#qmk">below&lt;/a>).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">	&lt;span class="err">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;matrix_pins&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cols&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GP4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP8&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rows&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GP9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP10&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP11&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP12&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>&lt;span class="err">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;split&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="err">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;soft_serial_pin&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;GP1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;matrix_pins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;right&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cols&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GP8&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP4&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rows&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GP9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP10&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP11&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP12&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>&lt;span class="err">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="err">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="assembling-the-hardware" class="anchor">
 &lt;a href="#assembling-the-hardware">
 Assembling the Hardware
 &lt;/a>
&lt;/h2>
&lt;p>Despite having quite a bit of soldering experience, handwiring still took longer than expected - the first half took about 3-4h, and the second half took maybe 2-3h once I figured out the best way to do certain things. It seems like such a simple task of &lt;em>just&lt;/em> connecting wires together, but as with most things, it still takes time and effort. Though it did turn out to be a pleasant change of pace to my typical programming work, since the manual, dexterous, but not cognitively-challenging labour required a different type of patience that ended up being a bit meditative.&lt;/p>
&lt;p>The models for the case (without the additional tenting) were downloaded from &lt;a href="https://www.printables.com/model/744408-skeletyl-ergonomic-keyboard">Printables&lt;/a>, and was printed on a &lt;a href="https://ca.store.bambulab.com/products/p1s">Bambu Lab P1S&lt;/a> in the &lt;a href="https://ca.store.bambulab.com/products/pla-sparkle?id=43809130152176">Onyx Black PLA Sparkle&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4286_hu_265a568e83e4afb6.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4286_hu_265a568e83e4afb6.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Test fitting the switches and a few keycaps to get a sense of how the sculpted profile feels.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4271_hu_758237990929f41a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4271_hu_758237990929f41a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Using a soldering iron to install the heatset inserts.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4275_hu_1d952773df926c90.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4275_hu_1d952773df926c90.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Coiling the diode leg to make it easier to attach to the switch pin.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4318_hu_40673b339e2ed19c.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4318_hu_40673b339e2ed19c.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Diodes placed, marking the wire locations to splice.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>For splicing the wire, this turned out to be easier to do than expected. Joe Scotto’s way is to use bare copper wire and heat shrink the intersections, but I didn’t think it was actually that much faster since it still takes time to cut, place, and heat the heat shrink.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/stripping-wires_hu_5a57d93fa96519ff.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/stripping-wires_hu_5a57d93fa96519ff.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Using basic wire cutters and an x-acto knife to expose the wire.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4303_hu_292400c5e0d0bf29.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4303_hu_292400c5e0d0bf29.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Rows and columns all connected!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>One weird aspect of sculpted keyboards is how the thumb clusters are on a different plane than the rest of the keys. The wiring ended up looking a little funny, but I wanted to hide it reasonably well to prevent them peeking through the exposed sections of the case.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4304_hu_5a3f8a794fa539.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4304_hu_5a3f8a794fa539.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Close up of how I wired the thumb cluster keys.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>With the matrix all done, all that was left was to wire each row and column to the MCU. I chose to solder these blue wires at the ends of each row and column to keep things looking tidy. The MCU was oriented upside down so the reset buttons would still be easily accessible when fully assembled.&lt;/p>
&lt;p>For the TRS connector, I initially bought ones on breakout boards, but it turned out to be way too bulky so I had to desolder it. Was a bit of a pain, but it worked out in the end.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/mcu-wiring_hu_bf73de717af512fe.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/mcu-wiring_hu_bf73de717af512fe.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">RP2040-Zero all wired up.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Doing everything again for the other half, and it was complete!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4334_hu_164eae10ed19db48.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4334_hu_164eae10ed19db48.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Build complete!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>I initially thought to design a nice 3D printed bracket to hold the MCU and TRS connector, but I was impatient and opted for the hacky way of just using hot glue since I was likely only going to be building this once.&lt;/p>
&lt;p>Surprisingly, the glue was sturdy enough to hold the components in place even when plugging/unplugging the cables from them.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4371_hu_982d2583e4a67397.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/IMG_4371_hu_982d2583e4a67397.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Securing components with &amp;ldquo;structural&amp;rdquo; hot glue.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="compiling-the-firmware" class="anchor">
 &lt;a href="#compiling-the-firmware">
 Compiling the Firmware
 &lt;/a>
&lt;/h2>

&lt;h3 id="qmk" class="anchor">
 &lt;a href="#qmk">
 QMK
 &lt;/a>
&lt;/h3>
&lt;p>I followed the &lt;a href="https://docs.qmk.fm/newbs">QMK tutorial&lt;/a> and forked the repo, ran the new keyboard command, and then updated the config. With the RP2040, the bootloader and processor needed to just be RP2040 instead of the dev board, which was one of the default options.&lt;/p>
&lt;p>It took a while to figure out what the correct configuration was for the layout, but I eventually got it done with a bit of help from ChatGPT.&lt;/p>
&lt;p>For the firmware flashing, I just set the split enabled flag for each side and commented out the opposite side. Maybe next time I’ll use EEPROM or setting a pin high to avoid needing to build different firmware for each side.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#pragma once
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/* RP2040- and hardware-specific config */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET &lt;/span>&lt;span class="c1">// Activates the double-tap behavior
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="cp">#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET_TIMEOUT 500U
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#define PICO_XOSC_STARTUP_DELAY_MULTIPLIER 64
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#define SERIAL_PIO_USE_PIO1
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In &lt;code>rules.mk&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">SERIAL_DRIVER = vendor
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And the &lt;code>keyboard.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;manufacturer&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Justin Lam&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;keyboard_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;skeletyl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;maintainer&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Justin Lam&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bootloader&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rp2040&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;processor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;RP2040&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;diode_direction&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;COL2ROW&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;features&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bootmagic&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;extrakey&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mousekey&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;nkro&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;matrix_pins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cols&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GP4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP8&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rows&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GP9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP10&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP11&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP12&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;split&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;right&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;soft_serial_pin&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;GP1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transport&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;protocol&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;serial&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;matrix_pins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;right&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cols&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GP8&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP7&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP4&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rows&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GP9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP10&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP11&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;GP12&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;usb&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;device_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;pid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0x0254&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;vid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0xFEED&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;layouts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;LAYOUT_split_3x5_3&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;layout&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">9&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">9&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">9&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.125&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.25&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.5&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">4.5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.75&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">6.5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.75&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">7.5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.5&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">8.5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.25&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h3 id="vial" class="anchor">
 &lt;a href="#vial">
 Vial?
 &lt;/a>
&lt;/h3>
&lt;p>My previous keyboard came with &lt;a href="https://get.vial.today/">Vial&lt;/a>, and it was immensely helpful to be able to play around with different key layouts and tweaks without having to compile and flash for every small change. Since I had my 36 key layout dialled in (or so I thought, more on that later), I thought that having a relatively stable layout in QMK would be sufficient.&lt;/p>
&lt;p>And then I actually went through the process of compiling/flashing firmware, and I immediately wanted Vial back!&lt;/p>
&lt;p>The main reason was because the process to make changes on a split keyboard are:&lt;/p>
&lt;ol>
&lt;li>Update configuration&lt;/li>
&lt;li>Run command to compile firmware&lt;/li>
&lt;li>Unplug USB cable from keyboard&lt;/li>
&lt;li>Unplug TRS cable from both halves&lt;/li>
&lt;li>With one half, turn upside down and press reset on the MCU to enter bootloader mode&lt;/li>
&lt;li>Plug USB cable back in&lt;/li>
&lt;li>Open file explorer, move firmware file to bootloader&lt;/li>
&lt;li>Unplug USB cable to exit bootloader mode&lt;/li>
&lt;li>Repeat step 5-8 for other half (may not be necessary depending on type of change)&lt;/li>
&lt;li>Plug TRS cable back in to both halves&lt;/li>
&lt;li>Plug USB cable back in to keyboard&lt;/li>
&lt;li>Test updated configuration&lt;/li>
&lt;li>Repeat from step 1 as needed&lt;/li>
&lt;/ol>
&lt;p>Compared to the process with vial:&lt;/p>
&lt;ol>
&lt;li>Open &lt;a href="https://vial.rocks/">vial.rocks&lt;/a> in web browser&lt;/li>
&lt;li>Update configuration&lt;/li>
&lt;li>Test updated configuration&lt;/li>
&lt;li>Repeat from step 2 as needed&lt;/li>
&lt;/ol>
&lt;p>Way easier and faster right? Especially when fiddling with timing or mouse key configurations, it’s just a much better development cycle since vial enables immediate changes.&lt;/p>

&lt;h3 id="yes-vial" class="anchor">
 &lt;a href="#yes-vial">
 Yes, Vial
 &lt;/a>
&lt;/h3>
&lt;p>Anyway, setting up Vial was relatively straightforward. It involved creating a new config from the QMK one (following the porting guide &lt;a href="https://get.vial.today/docs/porting-to-via.html">here&lt;/a>, and adding an extra config file that describes the visual representation of the keyboard. This config maps what you see in the UI to the keys in QMK.&lt;/p>
&lt;p>It was mildly annoying since I initially forked the QMK repo, and Vial requires forking their repo instead, so I just had to copy files into the Vial repo to continue from there. But I guess I should have read all the docs first!&lt;/p>
&lt;p>All files listed below are saved under &lt;code>keymaps/vial&lt;/code>.&lt;/p>
&lt;p>In &lt;code>config.h&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#pragma once
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#define VIAL_KEYBOARD_UID {0x11, 0x6B, 0x9E, 0x21, 0xCB, 0x6C, 0xB7, 0x37}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#define VIAL_UNLOCK_COMBO_ROWS { 0, 2 }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#define VIAL_UNLOCK_COMBO_COLS { 0, 4 }
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In &lt;code>keymaps/vial/rules.mk&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">VIA_ENABLE = yes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">VIAL_ENABLE = yes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And &lt;code>vial.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;skeletyl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;vendorId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0xFEED&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;productId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0x0254&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;matrix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rows&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cols&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;layouts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;keymap&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span> &lt;span class="s2">&amp;#34;0,0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0,1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0,2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0,3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0,4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="s2">&amp;#34;4,0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;4,1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;4,2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;4,3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;4,4&amp;#34;&lt;/span> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span> &lt;span class="s2">&amp;#34;1,0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;1,1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;1,2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;1,3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;1,4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="s2">&amp;#34;5,0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;5,1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;5,2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;5,3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;5,4&amp;#34;&lt;/span> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span> &lt;span class="s2">&amp;#34;2,0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;2,1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;2,2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;2,3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;2,4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="s2">&amp;#34;6,0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;6,1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;6,2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;6,3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;6,4&amp;#34;&lt;/span> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">2.5&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="s2">&amp;#34;3,2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;3,3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;3,4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="s2">&amp;#34;7,0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;7,1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;7,2&amp;#34;&lt;/span> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After flashing, my keyboard then showed up under &lt;a href="https://vial.rocks">vial.rocks&lt;/a>!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/08/handwired-skeletyl/vial.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/08/handwired-skeletyl/vial.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Vial web user interface.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="required-adjustments" class="anchor">
 &lt;a href="#required-adjustments">
 Required Adjustments
 &lt;/a>
&lt;/h1>

&lt;h2 id="keyboard-layout" class="anchor">
 &lt;a href="#keyboard-layout">
 Keyboard Layout
 &lt;/a>
&lt;/h2>
&lt;p>I thought my &lt;a href="https://justinmklam.com/posts/2025/07/36-key-layout/">36 key layout&lt;/a> would be completely fine as is, but it was not. Since the thumb cluster was wider, the farthest thumb no longer felt as comfortable or easy to hit for me since it required my hand to stretch across a large distance, especially for combos like &lt;code>CMD+T&lt;/code>. For modifiers, I started to use bottom row mods in addition to the thumb clusters, which ended up working quite well.&lt;/p>

&lt;h2 id="desk-ergonomics" class="anchor">
 &lt;a href="#desk-ergonomics">
 Desk Ergonomics
 &lt;/a>
&lt;/h2>
&lt;p>Since the keyboard is taller than normal, I started experiencing wrist and shoulder fatigue after a few hours in the first week of using it. Wrist/palm rests also did not help. After some research, I eventually came across advice that changing the height of your desk and/or chair are needed to accommodate the keyboard’s height.&lt;/p>
&lt;p>Since I use a floor desk, my desk is the only thing that has height adjustability, so I lowered it 1/2” and found it was a big improvement to my posture. Maybe one day I’ll &lt;a href="https://www.reddit.com/r/ErgoMechKeyboards/comments/1micni6/split_keyboard_recessed_into_custom_cnc_desktop/">recess my keyboard into my desk&lt;/a>?&lt;/p>
&lt;p>I also found that having the two halves closer and slightly angled inwards to be more comfortable than strictly at shoulder width apart. I wonder if it’s something to do with the height of the keyboard and how my forearms aren’t completely parallel with the ground? Either way, the nice thing about having a split is the ability to easily move them around to find the best position. Or as physiotherapists like to say, “the best position is your next position”!&lt;/p>

&lt;h1 id="final-thoughts" class="anchor">
 &lt;a href="#final-thoughts">
 Final Thoughts
 &lt;/a>
&lt;/h1>
&lt;p>One interesting thing about sculpted keyboards is that if the rubber feet are a bit dusty, the keyboard would have a tendency to shift inwards while typing. This is not an issue with flat keyboards since the direction of force is vertically down, whereas here, the force vectors are also horizontal. This can be mitigated by making sure the rubber feet are clean and have good grip, or adding small weights to the case (e.g. with motorcycle wheel balancing weights).&lt;/p>
&lt;p>I also noticed how my hands wanted to rest in home row (e.g. the lowest sloped area of the keyboard). In contrast to a typical flat keyboard, where my right hand would sometimes rest one key over on &lt;code>HJKL&lt;/code> (because of vim and vim-like keybindings on many apps I use), it didn’t matter as much since my hands could just shift laterally without significantly changing my wrist angle/position. It required some adjustment to train my right hand to really stay on home row and use my forefinger to span the two columns &lt;code>YHB&lt;/code> and &lt;code>UJN&lt;/code> instead of just one, which I found particularly difficult since reaching over with my forefinger often led to bumping into the tallest thumb key.&lt;/p>
&lt;p>The curvature for the ring and pinky fingers were quite comfortable for me, and being able to press the top outermost keys with my pinkies was a nice change.&lt;/p>
&lt;p>The thumb clusters could also be a little lower and shifted slightly inward, since the outer key is hard to reach for my (somewhat) smaller hands. But this is getting into specific hand morphology, so YMMV.&lt;/p>
&lt;p>All in all, I was quite pleased with how it turned out, and I’ve been daily driving this keyboard for a few weeks now. Unfortunately, building my first keyboard from scratch has opened the Pandora’s box of the desire to build even more keyboards, so time will tell if my journey ends here or continues on…&lt;/p></content:encoded></item><item><title>Optimizing My Symbol Layer: A Data Driven Approach</title><link>https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/</link><pubDate>Mon, 28 Jul 2025 10:37:03 -0700</pubDate><guid>https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/</guid><description>&lt;p>When first designing my layout on my &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard">Corne keyboard&lt;/a>, I was mostly focused on the macro level of what layers keys should go on as well as the ease of common workflows like selecting text or switching workspaces. I put &lt;em>some&lt;/em> thought into the placement of each symbol, but after growing quite comfortable with my keyboard and having a nagging feeling that something about the placement of my symbols felt off, it was finally a good opportunity to revisit the micro details of my symbols layer.&lt;/p></description><content:encoded>&lt;p>When first designing my layout on my &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard">Corne keyboard&lt;/a>, I was mostly focused on the macro level of what layers keys should go on as well as the ease of common workflows like selecting text or switching workspaces. I put &lt;em>some&lt;/em> thought into the placement of each symbol, but after growing quite comfortable with my keyboard and having a nagging feeling that something about the placement of my symbols felt off, it was finally a good opportunity to revisit the micro details of my symbols layer.&lt;/p>

&lt;h1 id="why-go-through-the-hassle" class="anchor">
 &lt;a href="#why-go-through-the-hassle">
 Why Go Through the Hassle?
 &lt;/a>
&lt;/h1>
&lt;p>You might be wondering - why not just copy the symbol positions of a standard keyboard along the top row? While this works, it’s not the most effective, or at least not in my experience. With the numbers across home row and the corresponding symbols along the top row, I found it kind of difficult to remember where the second half of the numbers were (e.g. was &lt;code>7&lt;/code> on my middle or ring finger?). Switching the numbers to be a numpad layout was much easier to remember for me, since it leveraged the same muscle memory as in standard layouts as well as allowing for single-handed entry.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/standard-layout.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/standard-layout.png>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Standard layout of a QWERTY keyboard. (Source: &lt;a href=https://www.daskeyboard.com/blog/qwerty-vs-dvorak-vs-colemak-keyboard-layouts/>Das Keyboard&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/markstos-layout.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/markstos-layout.png>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">A popular layout for 3x6 keyboards. Numbers and symbols run left to right to mimic a standard QWERTY layout. (Source: &lt;a href=https://mark.stosberg.com/markstos-corne-3x5-1-keyboard-layout/>Mark Stosberg&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Although I could have used the numeric position as the symbols&amp;rsquo; position (e.g. &lt;code>!&lt;/code> at &lt;code>1&lt;/code>, &lt;code>@&lt;/code> at &lt;code>2&lt;/code>, etc.), I would still need to train myself to commit the new positions to memory. But if I&amp;rsquo;m doing that anyway, I may as well create a personalized, brand new layout for symbols to memorize!&lt;/p>
&lt;p>I initially just put them in order spanning the rows on the left half, which worked okay but I figured it would be better to put my most used symbols on the home row for ease of use.&lt;/p>
&lt;p>The pic below is from another blog I came across during my research, which showed their grid of relative effort for each key placement. I generally agree with most of it, except I find the bottom pinky position actually quite easy, likely because of my habit of using my pinky to hold down the &lt;code>SHIFT&lt;/code> key (in lieu of using &lt;code>CAPSLOCK&lt;/code>), and/or because my pinkies are strong from &lt;a href="https://www.youtube.com/shorts/SjlCFlNW8UI">rock climbing&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/effort.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/effort.png>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Relative effort of each key position; lower is better. (Source: &lt;a href=https://www.jonashietala.se/blog/2021/06/03/the-t-34-keyboard-layout/>Jonas Hietala&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>I mainly use Python at work, so these symbol usages will be biased towards that. Every language will have its own set of common symbols (as explained in depth by &lt;a href="https://getreuer.info/posts/keyboards/symbol-layer/index.html">Pascal Getreuer&lt;/a>, so keep that in mind if you decide to design your own layout.&lt;/p>

&lt;h1 id="getting-nerdy-with-it" class="anchor">
 &lt;a href="#getting-nerdy-with-it">
 Getting Nerdy With It
 &lt;/a>
&lt;/h1>

&lt;h2 id="making-a-keylogger" class="anchor">
 &lt;a href="#making-a-keylogger">
 Making a Keylogger
 &lt;/a>
&lt;/h2>
&lt;p>To start, I created a simple python script to log all my keystrokes. I left it running in the background for a week or so to gather enough data, since my day to day involves both coding and writing documentation. The script only saves to a persistent file when it&amp;rsquo;s terminated, but this didn’t seem to cause any data loss issues for me.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pynput&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">keyboard&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Path to the JSON file where stats will be stored&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">STATS_FILE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;key_stats.json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Load existing stats or initialize a new dictionary&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">load_stats&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">STATS_FILE&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">exists&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">STATS_FILE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;r&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Save stats to the JSON file&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">save_stats&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">stats&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">STATS_FILE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dump&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">stats&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">indent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Update key stats&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">update_stats&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">stats&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">key_str&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">char&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nb">hasattr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;char&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">key&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">char&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">lower&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">AttributeError&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">key_str&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">key_str&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">stats&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stats&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key_str&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stats&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key_str&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Key press handler&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">on_press&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">global&lt;/span> &lt;span class="n">stats&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">update_stats&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">stats&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">save_stats&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">stats&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Initialize stats&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">stats&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">load_stats&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Start listening to key presses&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Keylogger started. Press &amp;#39;Ctrl + C&amp;#39; to stop.&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">keyboard&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Listener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">on_press&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">on_press&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">listener&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">listener&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">KeyboardInterrupt&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Keylogger stopped.&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">save_stats&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">stats&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The output data file looked something like this, which made it easy to visualize afterwards.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.cmd&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1259&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;k&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5877&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.shift&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3153&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;l&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1953&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1803&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;e&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3930&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1119&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="err">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I manually split up the json file into alpha, symbols, and misc files to graph the keys in logical groups. I considered building this into the script, but decided not to since it didn&amp;rsquo;t seem worth the effort for this one-time data collection and analysis.&lt;/p>

&lt;h2 id="patterns-pain-points-and-priorities" class="anchor">
 &lt;a href="#patterns-pain-points-and-priorities">
 Patterns, Pain Points, and Priorities
 &lt;/a>
&lt;/h2>

&lt;h3 id="alphanumeric-keys" class="anchor">
 &lt;a href="#alphanumeric-keys">
 Alphanumeric Keys
 &lt;/a>
&lt;/h3>
&lt;p>&lt;code>J&lt;/code> and &lt;code>K&lt;/code> were the most used, most likely because of vim. Yes, they aren’t the most efficient motions, but they do add up in usage due to many one off line moves (in comparison to larger motions like &lt;code>CTRL+D&lt;/code>, &lt;code>CTRL+U&lt;/code>, &lt;code>CTRL+[&lt;/code>, &lt;code>CTRL+]&lt;/code>, etc.).&lt;/p>
&lt;p>Nothing too surprising here, except maybe how infrequently &lt;code>X&lt;/code>, &lt;code>Q&lt;/code>, and especially &lt;code>Z&lt;/code> were not used by a relatively wide margin.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/keystats-alphanumeric.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/keystats-alphanumeric.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Alphanumeric keys&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="symbol-keys" class="anchor">
 &lt;a href="#symbol-keys">
 Symbol Keys
 &lt;/a>
&lt;/h3>
&lt;p>As mentioned before, I predominantly use Python (and vim) at work, so these symbols will reflect that (e.g. high usage of &lt;code>:&lt;/code> for vim commands, low usage of &lt;code>;&lt;/code> since Python doesn&amp;rsquo;t use semicolons at the end of each line like C/C++ do). I’m actually surprised that square brackets are my least commonly used bracket types since it&amp;rsquo;s used for array indices and dictionaries, but I guess it&amp;rsquo;s not as frequent as I thought.&lt;/p>
&lt;p>It&amp;rsquo;s no surprise that the symbols &lt;code>@|$%&amp;amp;^&lt;/code> are the least used, so the placement of those symbols will be fine in less-optimal positions.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/keystats-symbols.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/keystats-symbols.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Symbol keys&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="miscellaneous-keys" class="anchor">
 &lt;a href="#miscellaneous-keys">
 Miscellaneous Keys
 &lt;/a>
&lt;/h3>
&lt;p>Since I use macOS, &lt;code>CMD&lt;/code> and &lt;code>ALT&lt;/code> are the most used modifiers (e.g. &lt;code>CMD+C&lt;/code> to copy, &lt;code>ALT+LeftArrow&lt;/code> to navigate text by word). &lt;code>CTRL&lt;/code> would likely be lower if it weren’t for my vim keybindings, although on Linux or Windows, I&amp;rsquo;d imagine it&amp;rsquo;d take the place of &lt;code>CMD&lt;/code> (e.g. since on those platforms, copying is &lt;code>CTRL+C&lt;/code>).&lt;/p>
&lt;p>Interestingly, the arrow keys are fairly equal in usage. I also wonder if it&amp;rsquo;s common for &lt;code>SPACE&lt;/code> and &lt;code>BACKSPACE&lt;/code> to be so close in usage together - maybe I make a ton of typing mistakes?&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/keystats-misc.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/keystats-misc.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Miscellaneous keys&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="tweaking-for-ease-of-use-and-comfort" class="anchor">
 &lt;a href="#tweaking-for-ease-of-use-and-comfort">
 Tweaking for Ease of Use and Comfort
 &lt;/a>
&lt;/h1>
&lt;p>With this data, I can now put the symbols in places that make a bit more sense!&lt;/p>
&lt;p>I kept &lt;code>!&lt;/code> and &lt;code>@&lt;/code> in the same positions out of habit, and it didn&amp;rsquo;t really make sense to move those. Home row now contains &lt;code>#'&amp;quot;-=&lt;/code>, and I opted to put &lt;code>_&lt;/code> where &lt;code>SHIFT&lt;/code> would be since I find it quite comfortable to hit that bottom corner key with my pinky while typing variable names (e.g. &lt;code>some_variable_like_this&lt;/code>), which leverages the same &amp;ldquo;hold shift while typing for all caps&amp;rdquo; muscle memory. I also decided to add &lt;code>'&lt;/code> and &lt;code>&amp;quot;&lt;/code> as separate keys to make those easier to type, especially with &lt;code>&amp;quot;&lt;/code> no longer requiring an additional &lt;code>SHIFT&lt;/code> key to hit.&lt;/p>
&lt;p>&lt;code>$%^&amp;amp;&lt;/code> are now on the bottom row since they don&amp;rsquo;t see as frequent usage.&lt;/p>
&lt;p>Keen readers might notice that the parentheses, brackets, and braces are nowhere to be found. They weren&amp;rsquo;t forgotten, but rather promoted to my main layer as combos! See my main keymap &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard/#main-layer">here&lt;/a> for details. I did switch the position of &lt;code>[]&lt;/code> and &lt;code>()&lt;/code> so the latter is on home row instead, based on it being more frequently used from my data above.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/symbols-old.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/symbols-old.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Before&amp;hellip;&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/symbols-new.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/optimizing-symbols-layer/symbols-new.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">&amp;hellip; And after. Note that no changes were made on the right half.&lt;/p>
&lt;/div>

After a few weeks of using this layout, I can say for sure that it&amp;rsquo;s quite an improvement. Common symbols are easier to type, and as a result, it was also easier to memorize these new symbol placements.&lt;/p>
&lt;p>Admittedly, I still find myself using my alpha-layer combo of &lt;code>L+;&lt;/code> to type the quotation more often, so time will tell if I decide to replace those two home row positions with something else. Otherwise, I&amp;rsquo;m quite happy with the result of this exercise.&lt;/p>
&lt;p>Hooray for data driven decisions!&lt;/p></content:encoded></item><item><title>Transitioning to a 36-Key Layout</title><link>https://justinmklam.com/posts/2025/07/36-key-layout/</link><pubDate>Wed, 16 Jul 2025 09:50:41 -0700</pubDate><guid>https://justinmklam.com/posts/2025/07/36-key-layout/</guid><description>&lt;p>Having used my &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard">Corne keyboard&lt;/a> for some time now, I kept seeing 34/36 key layouts on &lt;a href="https://www.reddit.com/r/ErgoMechKeyboards/">r/ErgoMechKeyboards&lt;/a> with a growing amount of envy. I mean, the minimalist aesthetics of the &lt;a href="https://github.com/davidphilipbarr/Sweep">Ferris Sweep&lt;/a> are just so enticing! Staring down at my whopping 42 keys, I found myself not actually using the outer columns that often and wondered if I too could join the ranks of those using even fewer keys…&lt;/p>
&lt;p>So naturally, I removed the extraneous keys, covered the openings with electrical tape, and began the pursuit of trimming my layout with the goal of (arguably futile) minimalism and &amp;ldquo;efficiency&amp;rdquo;.&lt;/p></description><content:encoded>&lt;p>Having used my &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard">Corne keyboard&lt;/a> for some time now, I kept seeing 34/36 key layouts on &lt;a href="https://www.reddit.com/r/ErgoMechKeyboards/">r/ErgoMechKeyboards&lt;/a> with a growing amount of envy. I mean, the minimalist aesthetics of the &lt;a href="https://github.com/davidphilipbarr/Sweep">Ferris Sweep&lt;/a> are just so enticing! Staring down at my whopping 42 keys, I found myself not actually using the outer columns that often and wondered if I too could join the ranks of those using even fewer keys…&lt;/p>
&lt;p>So naturally, I removed the extraneous keys, covered the openings with electrical tape, and began the pursuit of trimming my layout with the goal of (arguably futile) minimalism and &amp;ldquo;efficiency&amp;rdquo;.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/36-key-layout/sweep.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/36-key-layout/sweep.png>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">The aesthetic allure of having only 34 keys. (Source: &lt;a href=https://github.com/davidphilipbarr/Sweep/assets/27895007/97e13cdc-b84b-4545-8e09-139a4bb935e5>Github&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="why-" class="anchor">
 &lt;a href="#why-">
 Why &amp;hellip;
 &lt;/a>
&lt;/h1>

&lt;h2 id="-fewer-keys" class="anchor">
 &lt;a href="#-fewer-keys">
 &amp;hellip; Fewer Keys?
 &lt;/a>
&lt;/h2>
&lt;p>Citing ergonomics would be the knee-jerk reaction, where with this smaller layout, each key is at most 1u key away from a finger. There&amp;rsquo;s some debate about whether fewer keys is actually better, since you end up trading off more hand movements for more combos (as well as cognitive load), as described in more detail in &lt;a href="https://getreuer.info/posts/keyboards/40-percent-ergo/index.html">this blog post&lt;/a>. I personally found smaller layouts to be more comfortable to use since the reduced wrist and finger stretching induced noticeably less strain on those body parts, which far outweighed the need for layers, combos, and memorizing a new layout.&lt;/p>
&lt;p>Granted, there&amp;rsquo;s probably a sweet spot of reduced keys that is greater than ~30-40% to gain ergonomic benefits without the need to learn a radically new layout. Take the &lt;a href="https://github.com/Squalius-cephalus/silakka54?tab=readme-ov-file">Silakka54&lt;/a> as an example, which contains a number row and outer columns, so it’s a minimal departure from the layout of a standard keyboard.&lt;/p>
&lt;p>Although for me, it&amp;rsquo;s honestly fun to dive into creating a personalized layout that suits my own computing needs, and the aesthetics of having such a minimal keyboard are hard to beat (through my rose-tinted glasses anyway).&lt;/p>

&lt;h2 id="-36-and-not-34-keys" class="anchor">
 &lt;a href="#-36-and-not-34-keys">
 &amp;hellip; 36 and Not 34 Keys?
 &lt;/a>
&lt;/h2>
&lt;p>It should be noted that going from 42 to 36 keys was relatively straightforward, since I was able to place all the required modifiers across the thumb clusters while still having layer keys.&lt;/p>
&lt;p>However, going down to 34 keys (thus removing two of the thumb keys) makes the reduction much more challenging, since relocating the modifiers to no longer be on dedicated keys requires a bit more creativity. Mod taps become a necessity (e.g. through the use of home or bottom row mods), or something like &lt;a href="https://keymapdb.com/keymaps/callum_oakley/">Callum mods&lt;/a> which uses one shot modifiers on a separate layer. This was something I wasn&amp;rsquo;t quite ready to do yet, so I decided on a &amp;ldquo;sane&amp;rdquo; reduction to 36 keys.&lt;/p>

&lt;h1 id="relocating-the-outer-keys" class="anchor">
 &lt;a href="#relocating-the-outer-keys">
 Relocating the Outer Keys
 &lt;/a>
&lt;/h1>
&lt;p>The main challenge in reducing a &lt;a href="https://justinmklam.com/posts/2025/05/corne-keyboard/#keymap">42-key layout&lt;/a> to 36 keys were figuring out where to put the outer column keys, which consisted of &lt;code>TAB&lt;/code>, &lt;code>ESC&lt;/code>, &lt;code>SHIFT&lt;/code>, &lt;code>BACKSPACE&lt;/code>, &lt;code>'&lt;/code>, and &lt;code>ENTER&lt;/code>. For the keys on the inner column, they didn’t see much use anyway since I just mapped them superfluously to &lt;code>VOL+&lt;/code>, &lt;code>VOL-&lt;/code>, &lt;code>RGUI&lt;/code>, and &lt;code>RALT&lt;/code>, so losing those wasn’t a big deal.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-42.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-42.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Previous 42-key layout.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="tab-enter" class="anchor">
 &lt;a href="#tab-enter">
 Tab, Enter
 &lt;/a>
&lt;/h2>
&lt;p>I added these keys to the remaining thumb cluster keys that weren&amp;rsquo;t using tap modifiers (e.g. &lt;code>LGUI&lt;/code> and &lt;code>RSFT&lt;/code>). No major issues or adjustments here, since it was fairly similar to hitting &lt;code>SPACE&lt;/code> with a thumb. &lt;code>ENTER&lt;/code> was placed on the opposite side of &lt;code>LGUI&lt;/code> so that combo could still be easily used for some desktop and web apps (e.g. to submit a comment in GitHub or Jira).&lt;/p>

&lt;h2 id="backspace" class="anchor">
 &lt;a href="#backspace">
 Backspace
 &lt;/a>
&lt;/h2>
&lt;p>Out of habit, I had this in the top right corner, but I eventually got used to using backspace with my thumb so it became redundant. I thought I would miss the &lt;code>LALT+BSPC&lt;/code> combo for easily deleting words (on macOS), but I found that hitting &lt;code>LALT&lt;/code> with my left thumb and &lt;code>BSPC&lt;/code> with my right thumb was equally as convenient.&lt;/p>

&lt;h2 id="escape" class="anchor">
 &lt;a href="#escape">
 Escape
 &lt;/a>
&lt;/h2>
&lt;p>After adding the &lt;code>J+K&lt;/code> combo to be &lt;code>ESC&lt;/code>, I quickly became accustomed to it since it was just so convenient! Again, this outer position became redundant, and was an artifact of remapping &lt;code>CAPSLOCK&lt;/code> to &amp;ldquo;&lt;code>CTRL&lt;/code> when held, &lt;code>ESC&lt;/code> when tapped&amp;rdquo;.&lt;/p>

&lt;h2 id="apostrophe-quotation-mark" class="anchor">
 &lt;a href="#apostrophe-quotation-mark">
 Apostrophe, Quotation Mark
 &lt;/a>
&lt;/h2>
&lt;p>Not having the apostrophe and quotation mark in the normal position was definitely a bit jarring. I initially tried putting it with the rest of the symbols in my symbols layer, but it just didn&amp;rsquo;t feel natural. Things clicked once I created a combo on &lt;code>L+;&lt;/code>, and I&amp;rsquo;ve been using that since.&lt;/p>

&lt;h2 id="shift" class="anchor">
 &lt;a href="#shift">
 Shift
 &lt;/a>
&lt;/h2>
&lt;p>I previously tried home row mods, but couldn&amp;rsquo;t get used to it without causing accidental misfires. I also tried using shift on one of the thumb keys, but I found that it was difficult to keep shift held down when typing all caps. Yes, I could just use capslock, or better yet capsword, but the latter was not available on my keyboard&amp;rsquo;s version of vial (since it was purchased as a prebuilt without access to the firmware), and I&amp;rsquo;m not a fan of toggling capslock on/off while typing.&lt;/p>
&lt;p>I wanted to keep the shift on the pinky, so I changed &lt;code>Z&lt;/code> to be &amp;ldquo;&lt;code>SHIFT&lt;/code> when held, &lt;code>Z&lt;/code> when pressed&amp;rdquo; (aka &lt;code>SHIFT_T[Z]&lt;/code>). This worked decently until I started typing quickly, which then resulted in shift not being registered (e.g. Instead of &amp;ldquo;La&amp;rdquo;, it would output &amp;ldquo;zla&amp;rdquo;).&lt;/p>
&lt;p>This was mildly frustrating, so I started playing around with one shot modifiers. I mapped it to the &lt;code>S+D&lt;/code> combo, and it actually flowed quite well into my typing habits. I still occasionally had misfires of an extra shifted letter when typing quickly (e.g. &amp;ldquo;LA&amp;rdquo; instead of &amp;ldquo;La&amp;rdquo;), but this happened far less often then with &lt;code>SHIFT_T[Z]&lt;/code>.&lt;/p>
&lt;p>However, for holding down shift while typing as well as for combos (e.g. selecting text), I still use &lt;code>SHIFT_T[Z]&lt;/code> for that, which works without issue. Optionally, I can hit &lt;code>S+D&lt;/code> twice in succession to enable or disable holding the shift key (so effectively capslock). Some may find it burdensome to having too many different options to remember, but I like how they serve different use cases: shifting while typing is a different rhythm and hand position than shifting while navigating, doing motions, etc.&lt;/p>
&lt;p>With &lt;code>SHIFT_T[Z]&lt;/code>, I figured I might as well try adding the rest of the modifiers (&lt;code>ALT&lt;/code>, &lt;code>CTRL&lt;/code>, &lt;code>GUI&lt;/code>) as tap mods, so home row mods but on the bottom row. This actually worked surprisingly well, and removed many of the misfire issues I experienced with home row mods without having to fiddle around with &lt;a href="https://precondition.github.io/home-row-mods#finding-the-sweet-spot">tapping term&lt;/a> or enabling &lt;a href="https://docs.qmk.fm/tap_hold#chordal-hold">chordal hold&lt;/a> or &lt;a href="https://docs.qmk.fm/tap_hold#flow-tap">flow tap&lt;/a> (which again, my version of vial did not support). Ironically, this made me realize that transitioning to 34 keys would not be as out of reach as I initially thought, but I shelved that idea to be revisited another time.&lt;/p>

&lt;h1 id="revised-keymap" class="anchor">
 &lt;a href="#revised-keymap">
 Revised Keymap
 &lt;/a>
&lt;/h1>
&lt;p>The diagrams below show my “final” 36 key layout.&lt;/p>
&lt;p>The main changes were on the main layer, although I did rearrange the function keys on the last layer since I could no longer span &lt;code>F1&lt;/code> to &lt;code>F12&lt;/code> along the top row (which didn&amp;rsquo;t make much sense to do anyways). The left half RGB controls were replaced with the function keys. In retrospect, I could have matched the order of the function keys to mimic the numpad (or even be in the same position as them), but I&amp;rsquo;d rather have the media keys on the right side (since I use the mouse on my left hand). I rarely use the function keys anyway, so they&amp;rsquo;re just there in the odd time that I need it.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-letters.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-letters.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Layer 1: Letters&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-symbols.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-symbols.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Layer 2: Symbols&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-nav.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-nav.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Layer 3: Navigation&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-misc.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/36-key-layout/keymap-misc.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Layer 4: Miscellaneous&lt;/p>
&lt;/div>

I&amp;rsquo;ve been using this layout with success for a few weeks now, and it&amp;rsquo;s been quite good. Now to start shopping for an actual 36 key keyboard&amp;hellip;&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/07/36-key-layout/ipad_hu_20f206e827ed479c.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/07/36-key-layout/ipad_hu_20f206e827ed479c.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The pursuit of minimalist computing continues&amp;hellip;&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>Six Months with a Corne Keyboard</title><link>https://justinmklam.com/posts/2025/05/corne-keyboard/</link><pubDate>Tue, 27 May 2025 15:57:51 -0700</pubDate><guid>https://justinmklam.com/posts/2025/05/corne-keyboard/</guid><description>&lt;p>It all started when I first saw a &lt;a href="https://www.daskeyboard.com/blog/what-is-tkl-keyboard/">tenkeyless keyboard&lt;/a>. Compared to a standard 104-key keyboard, having only 87 keys (an entire 17 keys fewer!) seemed like the next best thing to &lt;a href="https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/">sliced bread&lt;/a>. Since I wasn&amp;rsquo;t a big user of the numpad, it seemed like a no brainer to reduce desk space, bring my mousing hand a few inches closer, and just feel like I was one with &lt;a href="https://www.reddit.com/r/pcmasterrace/">r/PCMasterRace&lt;/a>. Oh how naive I was.&lt;/p></description><content:encoded>&lt;p>It all started when I first saw a &lt;a href="https://www.daskeyboard.com/blog/what-is-tkl-keyboard/">tenkeyless keyboard&lt;/a>. Compared to a standard 104-key keyboard, having only 87 keys (an entire 17 keys fewer!) seemed like the next best thing to &lt;a href="https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/">sliced bread&lt;/a>. Since I wasn&amp;rsquo;t a big user of the numpad, it seemed like a no brainer to reduce desk space, bring my mousing hand a few inches closer, and just feel like I was one with &lt;a href="https://www.reddit.com/r/pcmasterrace/">r/PCMasterRace&lt;/a>. Oh how naive I was.&lt;/p>
&lt;p>Little did I know that it was simply the beginning of a long, arduous journey in search of elusive perfection. Perhaps what I was looking for is actually from within? Or, perhaps I just needed to buy another keyboard. I went with the latter, and ended up with a minimalistic-ly cute 46-key keyboard called the &lt;a href="https://github.com/foostan/crkbd">Corne&lt;/a>.&lt;/p>

&lt;h1 id="backstory" class="anchor">
 &lt;a href="#backstory">
 Backstory
 &lt;/a>
&lt;/h1>
&lt;p>The table below shows most of the keyboards I&amp;rsquo;ve tried over the years. I never got too deep into the world of mechanical keyboards since my pockets were (fortunately?) not as deep as my heart wanted them to be.&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Date&lt;/th>
 &lt;th>Keyboard&lt;/th>
 &lt;th>Switches&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>July 2016&lt;/td>
 &lt;td>CM Storm TKL&lt;/td>
 &lt;td>MX Cherry Blue&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Aug 2016&lt;/td>
 &lt;td>Lenovo TrackPoint Keyboard&lt;/td>
 &lt;td>Membrane&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sep 2017&lt;/td>
 &lt;td>Poker 61&lt;/td>
 &lt;td>MX Cherry Black&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Nov 2017&lt;/td>
 &lt;td>Havit Low Profile TKL&lt;/td>
 &lt;td>Kaihl Blue&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mar 2019&lt;/td>
 &lt;td>Tada68&lt;/td>
 &lt;td>Gateron Silent Red&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apr 2021&lt;/td>
 &lt;td>Apple Magic Keyboard&lt;/td>
 &lt;td>🦋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Jan 2025&lt;/td>
 &lt;td>Corne&lt;/td>
 &lt;td>TTC Frozen V2 Silent&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>I stuck with the TKL format for a while, but was always intrigued by smaller layouts. I tried the Poker, but I found it cumbersome not having arrow keys easily accessible, since it made things like selecting text by word require some nasty finger gymnastics (e.g. &lt;code>CTRL+SHIFT+[LAYER+LEFT]&lt;/code>). A few boards later and after some ridicule from a coworker for having tried more keyboards than he&amp;rsquo;s used in his lifetime, the Tada68 became my daily driver.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/TADA68_hu_2ef91f84bdc93131.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/TADA68_hu_2ef91f84bdc93131.jpg>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Tada, what a nice compact layout! Is this the end game? (Source: &lt;a href=https://drop.com/buy/tada68-mechanical-keyboard>Drop&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>I was previously using Linux computers for personal and work, but when I started my new job in 2021, I was given a MacBook and was quickly frustrated with the difference in layouts. I then purchased a Magic Keyboard so the layout would be the same whether I was at my desk or on the go, and eventually came to appreciate the shallow keys since it allowed me to type quite quickly.&lt;/p>
&lt;p>However, a part of me kept wondering if something was missing from my life. Obviously it was another keyboard that was needed, but this time I looked towards ergonomic, mechanical keyboards.&lt;/p>
&lt;p>Previously, I had tried the &lt;a href="https://www.pcmag.com/reviews/microsoft-sculpt-ergonomic-desktop">Microsoft Sculpt&lt;/a> and the &lt;a href="https://kinesis-ergo.com/shop/freestyle2-for-pc-us">Kinesis Freestyle&lt;/a>, but they never really clicked for me. I wanted something that spoke to me on a spiritual level. Perusing &lt;a href="https://www.reddit.com/r/ErgoMechKeyboards/">r/ErgoMechKeyboards&lt;/a> was just what I needed, and the simplistic-yet-still-practical layout of the Corne caught my eye. I bit the bullet with (a relatively inexpensive) prebuilt one on Aliexpress for ~$80 CAD, and a few weeks later it arrived at my doorstep, ready to fill my emotional void.&lt;/p>
&lt;p>For reference, here are some of the other keyboards I considered, but ultimately settled on the Corne due to a combination of features, aesthetics, cost, and ease of purchase.&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/kata0510/Lily58">Lily58&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/josefadamcik/SofleKeyboard">Sofle&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/GEIGEIGEIST/TOTEM">Totem&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/davidphilipbarr/Sweep">Ferris Sweep&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/nicinabox/lets-split-guide">Let&amp;rsquo;s Split&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://keeb.io/products/iris-keyboard-split-ergonomic-keyboard">Iris&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.zsa.io/voyager">Ergodox Voyager&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.zsa.io/moonlander">Ergodox Moonlander&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://naya.tech/">Naya Create&lt;/a> &amp;lt;- Way out of budget but it sure looks nice!&lt;/li>
&lt;/ul>

&lt;h1 id="getting-used-to-the-corne" class="anchor">
 &lt;a href="#getting-used-to-the-corne">
 Getting Used to the Corne
 &lt;/a>
&lt;/h1>

&lt;h2 id="learning-curve" class="anchor">
 &lt;a href="#learning-curve">
 Learning Curve
 &lt;/a>
&lt;/h2>
&lt;p>It felt like I was in elementary school again, playing intro typing games like home row and &lt;a href="https://en.wikipedia.org/wiki/Math_Blaster!">Math Blasters&lt;/a> in the computer lab, trying to make sense of this piece of hardware that turns thoughts into words on a screen.&lt;/p>
&lt;p>I can usually type around 100-110 wpm, but the first few hours with this layout was definitely an adjustment. Started off with a paltry 30 wpm, steadily increased back up to my normal typing speed. With some practice, I was starting to get the hang of the columnar stagger.&lt;/p>
&lt;p>After about 1-2 weeks, I was typing steadily enough to use it for work, albeit still nowhere close to my usual flow. At the 3-4 week mark, I became comfortable with the layout and started to get a better understanding of what worked and didn’t work in my layout.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/pic-monkeytype.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/pic-monkeytype.png>&lt;/a>
 
 
 
 
 
 
 
 
 &lt;p class="caption">Steadily increasing WPM over the span of 3 months. (Source: &lt;a href=https://monkeytype.com/>MonkeyType&lt;/a>)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="ergonomics" class="anchor">
 &lt;a href="#ergonomics">
 Ergonomics
 &lt;/a>
&lt;/h2>
&lt;p>Many of the keyboards I saw online were tented, so it seemed like the norm to try it out. Since mine didn’t come with a tenting kit or mechanism, I used a book to achieve the same angling to see how it felt.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/tenting_hu_3cedaa48a6fc58a4.jpeg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/tenting_hu_3cedaa48a6fc58a4.jpeg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Book-based tenting in action.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>It was ok. I didn’t find it to be a drastic improvement over being flat. I also didn&amp;rsquo;t like how it left my wrists floating above the surface. Perhaps I needed to take the ergonomics more seriously and try a little harder&amp;hellip;&lt;/p>
&lt;p>So I went deeper. Bought some ball mount clamps and went full ergo, or at least tried to. Unfortunately it didn&amp;rsquo;t really work with my set up. It was too high for my elbows and, although it was comfortable for my wrists, it felt like my arms and shoulders couldn’t really relax. It also made using the mouse even more inconvenient, but that was a secondary problem.&lt;/p>
&lt;p>I think a set up like this would do better on a standing desk, or if you have a better, more flexible way of mounting them. I found the clamped position to not be optimal, and it also pushed me farther from my desk and monitor.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/pic-ergo_hu_574c18859f747d08.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/pic-ergo_hu_574c18859f747d08.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Went a little too hard on the ergo-front.&lt;/p>
&lt;/div>

Ultimately, I went back to just a flat configuration. I found that having the sides separated was more important than the angle, at least for me.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/desk_hu_b0d6a2e4905897be.jpeg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/desk_hu_b0d6a2e4905897be.jpeg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">My humble floor desk.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="switches" class="anchor">
 &lt;a href="#switches">
 Switches
 &lt;/a>
&lt;/h2>
&lt;p>The keyboard came with some Leopold switches, which actually felt decent but were loud enough to warrant my wife granting me more budget to get something quieter.&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Name&lt;/th>
 &lt;th>Actuation Force&lt;/th>
 &lt;th>Comment&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Leopold Grayborg&lt;/td>
 &lt;td>40gf&lt;/td>
 &lt;td>Felt ok, but very loud.&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Akko Silent Fairy&lt;/td>
 &lt;td>50gf&lt;/td>
 &lt;td>Mushy and scratchy. Would not recommend.&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TTC Frozen v2 Silent&lt;/td>
 &lt;td>39gf&lt;/td>
 &lt;td>Impressed with how silent they are while feeling crisp and smooth.&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>After trying a handful of different ones, I settled on the TTC Frozen v2 switches. They might be even better if lubed, but they passed both mine and my wife&amp;rsquo;s sound test, so they were a keeper! A nice bonus is that due to their clear design, the keyboard lighting shines brightly and diffusely around the keycaps.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/switches_hu_1c67f27a6ed064f6.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/switches_hu_1c67f27a6ed064f6.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Switches galore!&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="keymap" class="anchor">
 &lt;a href="#keymap">
 Keymap
 &lt;/a>
&lt;/h1>
&lt;p>I tried to keep a handful of things in mind when designing the layout of my keyboard:&lt;/p>
&lt;ul>
&lt;li>Certain keys/combos should still be usable with one hand, e.g:
&lt;ul>
&lt;li>Using arrow keys&lt;/li>
&lt;li>Copy/cut/paste&lt;/li>
&lt;li>Cycling through windows/tabs&lt;/li>
&lt;li>Taking screencaps via selected area&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Certain mouse + keyboard workflows still need to work, e.g:
&lt;ul>
&lt;li>Holding &lt;code>ALT&lt;/code> and clicking to open a link in my IDE&lt;/li>
&lt;li>Horizontal scrolling by holding &lt;code>SHIFT&lt;/code> with the opposite hand&lt;/li>
&lt;li>Panning certain diagraming tools by holding &lt;code>GUI&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Frequently used keys/combos should be easy and comfortable to use, e.g:
&lt;ul>
&lt;li>Selecting text by words/home/end&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Optimized for writing and software development&lt;/li>
&lt;li>Layout should be somewhat mnemonic to ease the learning curve&lt;/li>
&lt;/ul>
&lt;p>Through using this keyboard as my daily driver for almost half a year and occasional layout tweaks, I&amp;rsquo;ve come up with the mapping described below. It&amp;rsquo;s far from perfect, but enables a pretty smooth workflow for how I use the computer.&lt;/p>
&lt;blockquote>
&lt;p>Note: This keyboard uses &lt;a href="https://get.vial.today/">VIAL&lt;/a>, which is QMK based but includes a web interface to update the layout on the fly instead of having to flash new firmware every time. Makes it easy to fiddle around and try new configurations without much friction.&lt;/p>
&lt;p>Keyboard graphics were generated with &lt;a href="https://github.com/caksoylar/keymap-drawer-web">Keymap Drawer&lt;/a>, which was fed the QMK layout from &lt;a href="https://config.qmk.fm/#/crkbd/rev4_0/standard/LAYOUT_split_3x6_3_ex2">QMK Configurator&lt;/a>. Source files for both tools can be found in &lt;a href="https://gist.github.com/justinmklam/7385760c62a32232a9a72b08c342e36e">this gist&lt;/a>.&lt;/p>&lt;/blockquote>

&lt;h2 id="main-layer" class="anchor">
 &lt;a href="#main-layer">
 Main Layer
 &lt;/a>
&lt;/h2>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/keymap-letters.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/keymap-letters.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Main layer consists of letters, modifiers, and some symbols.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The main alpha layer is fairly straightforward, since it follows a standard QWERTY layout.&lt;/p>
&lt;p>Things that worked well:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Key combos&lt;/strong>: They&amp;rsquo;re a nice way to bring more keys to the main layer without having to switch layers. Open/closing brackets being on opposite sides was kind of neat; it was easy to remember and convenient for programming.&lt;/li>
&lt;li>&lt;strong>Thumb mapping combos:&lt;/strong> Key mappings like &lt;code>SPACE|L2&lt;/code> and &lt;code>BSPC|LTCL&lt;/code> were nice because they provide double duty on optimally positioned thumb keys.&lt;/li>
&lt;li>&lt;strong>Thumb layer/modifier combos&lt;/strong> Having the layer and modifier keys right next to each other (&lt;code>L1&lt;/code> and &lt;code>LGUI&lt;/code>, &lt;code>L2&lt;/code> and &lt;code>LCTL&lt;/code>) meant that it was possible to use the thumb on both simultaneously to make certain key combos much easier, e.g. &lt;code>LTCL+[L2+H]&lt;/code> to be the equivalent of &lt;code>LCTL+LEFT&lt;/code>, which switches virtual desktops on macOS.&lt;/li>
&lt;li>&lt;strong>Breaking symmetry with the right layer key:&lt;/strong> I mapped the L2 layer key on the leftmost key instead of center (like how it is on the left side), since I found my right index finger to always hover over &lt;code>H&lt;/code> instead of the typical home row position of &lt;code>J&lt;/code>. This is likely because on the right side, the second last column contains symbols instead of more frequently used letters. I didn&amp;rsquo;t like having my thumb curled under my index too much, especially since it made using arrow keys a bit awkward.&lt;/li>
&lt;/ul>
&lt;p>Things that didn&amp;rsquo;t work that well:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Optimizing for mouse + keyboard use:&lt;/strong> Since I mouse on my left hand, doing things like horizontal scrolling (shift + mouse) and panning (&lt;code>GUI&lt;/code> + mouse) was initially hard until I duplicated some keys on the right side (&lt;code>RGUI&lt;/code>, &lt;code>RALT&lt;/code>, &lt;code>RSFT&lt;/code>).&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://docs.qmk.fm/mod_tap">Mod tap&lt;/a> on letter keys:&lt;/strong> Tried &lt;a href="https://precondition.github.io/home-row-mods">home row mods&lt;/a> for a bit, but didn&amp;rsquo;t like how the timing was quite finicky and it would sometimes cause delays in keypresses, which was quite frustrating. With the presence of modifier keys on thumb clusters, it didn&amp;rsquo;t seem like a necessary feature to implement.&lt;/li>
&lt;/ul>

&lt;h2 id="symbols-layer" class="anchor">
 &lt;a href="#symbols-layer">
 Symbols Layer
 &lt;/a>
&lt;/h2>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/keymap-symbols.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/keymap-symbols.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Symbols and numbers.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>I initially set it up so numbers went from left to right along the middle row, then the corresponding symbols above it on the top row. It worked ok, but I found it hard to remember where the higher numbers were (6-0). I switched to numpad on the right side, and found it to be much better. Typing numbers (and things like numeric versions) is quite a bit faster, since it can all be done with the same hand.&lt;/p>
&lt;p>There&amp;rsquo;s duplication of the hyphen which might not be optimal, but I find it handy to be able to use the left one while typing (e.g. hyphenated variable names), and the right one for number-related tasks (e.g. in spreadsheets).&lt;/p>
&lt;p>The random &lt;code>LCTL+Q&lt;/code> is to lock my laptop with the hotkey &lt;code>GUI+CTRL+Q&lt;/code> on macOS. I initially had it be one macro but found it was too easy to accidentally hit, so made it explicit but still be able to do it with one hand.&lt;/p>
&lt;p>Things that worked well:&lt;/p>
&lt;ul>
&lt;li>Numpad was easier to remember than having them all in the top row&lt;/li>
&lt;li>Hyphen and underscore are typed often, didn&amp;rsquo;t like them as combos since didn&amp;rsquo;t feel as precise/fast&lt;/li>
&lt;li>Like having # and * above/below each other for vim, since they&amp;rsquo;re used for cycling forward/back on the currently selected text.&lt;/li>
&lt;/ul>
&lt;p>Things that didn&amp;rsquo;t:&lt;/p>
&lt;ul>
&lt;li>Less frequently used symbols are still hard to remember, e.g. &lt;code>$%^&amp;amp;&lt;/code>&lt;/li>
&lt;/ul>

&lt;h2 id="navigation-layer" class="anchor">
 &lt;a href="#navigation-layer">
 Navigation Layer
 &lt;/a>
&lt;/h2>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/keymap-nav.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/keymap-nav.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Arrow keys, browser, and mouse navigation.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Things that worked well:&lt;/p>
&lt;ul>
&lt;li>Arrow keys in &lt;code>hjkl&lt;/code> are also very convenient, but needed layer toggle on the same side (whereas for numpad, it&amp;rsquo;s opposite).&lt;/li>
&lt;li>Arrow key selection is really handy and fast since I can use both hands for the combo - right hand for the arrows (while hand is in home row), and left hand for the modifiers (e.g. &lt;code>SHIFT&lt;/code> plus &lt;code>ALT&lt;/code> for selecting by word, or &lt;code>CMD&lt;/code> for selecting home/end).&lt;/li>
&lt;li>Navigation for browser fwd/back and tab next/prev is a pleasure to use.&lt;/li>
&lt;/ul>
&lt;p>Things that didn&amp;rsquo;t:&lt;/p>
&lt;ul>
&lt;li>Don&amp;rsquo;t really use the mouse wheel keys on the left cluster.&lt;/li>
&lt;li>Sometimes use mouse control, but it&amp;rsquo;s kind of hard to be precise. Also doesn&amp;rsquo;t easily work with click/drag. But it&amp;rsquo;s useful for things like refocusing a window/input that the mouse is nearby.&lt;/li>
&lt;/ul>
&lt;p>For right handed users, you probably would want to move all the browser nav hotkeys (top row on the right side) to the left, so your mouse can still be used while your hand is on those keys.&lt;/p>

&lt;h2 id="miscellaneous-layer" class="anchor">
 &lt;a href="#miscellaneous-layer">
 Miscellaneous Layer
 &lt;/a>
&lt;/h2>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/corne-keyboard/keymap-misc.svg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/corne-keyboard/keymap-misc.svg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Layer with everything else.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>I used to use this layer mainly for the volume and media keys, but I recently mapped the common ones (volume control, media play/pause) to the extra inner keys on the first two layers, which are more convenient and allow for single hand control.&lt;/p>
&lt;p>Other than that, I don&amp;rsquo;t use this layer very much, aside from the occasional &lt;code>F2&lt;/code> when I want to rename all instances of a variable in my IDE. They&amp;rsquo;re here just in case I need them. I&amp;rsquo;ve also seen the function keys mapped to a single side, so I may try that in the future if I end up needing to use these function keys more often.&lt;/p>

&lt;h1 id="using-regular-keyboards" class="anchor">
 &lt;a href="#using-regular-keyboards">
 Using Regular Keyboards?
 &lt;/a>
&lt;/h1>
&lt;p>For the first three months, I exclusively used my Corne keyboard. When I tried using a regular keyboard after that, it definitely felt jarring and took a few minutes to recalibrate my typing. Now, with regularly switching between my Corne and a regular keyboard, I no longer suffer from that initial transition period, and can now type on either at full proficiency. Thank goodness for muscle memory, which fortunately keeps the different layouts well-compartmentalized!&lt;/p>
&lt;blockquote>
&lt;p>For the record, I did try using my Corne on top of my laptop while I&amp;rsquo;m sitting on the couch with it, but I found it cumbersome to move it from my desk, as well as it being precariously placed on top of my laptop. Even though there are solutions for keeping the keyboard in place and even disabling the laptop&amp;rsquo;s internal keyboard, I found it just not as nice or easy to use.&lt;/p>&lt;/blockquote>
&lt;p>However, I did look into software like &lt;a href="https://karabiner-elements.pqrs.org/">Karabiner Elements&lt;/a> (for macOS) and &lt;a href="https://github.com/rvaiya/keyd">keyd&lt;/a> (for Linux) to bring some of the niceties over from my Corne. Main things were:&lt;/p>
&lt;ul>
&lt;li>Mapping &lt;code>j+k&lt;/code> to &lt;code>ESC&lt;/code>&lt;/li>
&lt;li>Overloading &lt;code>CAPSLOCK&lt;/code> to &lt;code>ESC&lt;/code> if tapped, or &lt;code>CTRL&lt;/code> if held&lt;/li>
&lt;li>Having a navigation layer to turn &lt;code>hjkl&lt;/code> into arrow keys&lt;/li>
&lt;/ul>
&lt;p>Here’s the keyd config I use for my Linux laptop:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="k">[ids]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">*&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[main]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">capslock&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">overload(control, esc)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">j+k&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">esc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">=&lt;/span>&lt;span class="s">+backspace = C-backspace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">meta&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">layer(nav)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[nav]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">left&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">j&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">down&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">k&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">l&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">right&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h1 id="closing-thoughts" class="anchor">
 &lt;a href="#closing-thoughts">
 Closing Thoughts
 &lt;/a>
&lt;/h1>
&lt;p>Overall, I&amp;rsquo;ve been quite happy with this keyboard and the functionality that it enables. Having a wireless option would be nice for making it easier to use with a laptop on the go, but the relatively short battery life is a deal breaker for me, since it&amp;rsquo;ll just become another thing to regularly charge.&lt;/p>
&lt;p>A lower profile, choc-style keyboard might be next on my list, but I actually haven&amp;rsquo;t suffered from any wrist issues that have previously given me issues with MX-height keyboards (without using a wrist rest).&lt;/p>
&lt;p>What&amp;rsquo;s currently on my mind is figuring out a better pointing/mousing workflow. Haven&amp;rsquo;t decided whether that would look something like a keyboard with &lt;a href="https://github.com/Bastardkb/Charybdis">an integrated trackball&lt;/a>, or if I should dive into a fully keyboard-driven workflow with something like &lt;a href="https://mouseless.click/">Mouseless&lt;/a>.&lt;/p>
&lt;p>Or, perhaps this isn&amp;rsquo;t the end of my keyboard journey, and I should go even deeper and get something like the &lt;a href="https://www.charachorder.com/en-ca/products/cc2">CharaChorder&lt;/a>.&lt;/p>
&lt;p>&amp;hellip;Or perhaps I&amp;rsquo;ll just stick with this one for now :)&lt;/p></content:encoded></item><item><title>Hugo: Some Random Tips and Tricks</title><link>https://justinmklam.com/posts/2025/05/hugo-tips-and-tricks/</link><pubDate>Fri, 23 May 2025 08:47:00 -0700</pubDate><guid>https://justinmklam.com/posts/2025/05/hugo-tips-and-tricks/</guid><description>&lt;p>My intro to Hugo was back in 2016, a mere 3 years after its &lt;a href="https://en.wikipedia.org/wiki/Hugo_(software)">inception&lt;/a>, when I was in my early days of learning web development. Quite a bit has changed since then, both in what Hugo is capable of and my understanding of how to actually code. I&amp;rsquo;m far from being a frontend dev, but I&amp;rsquo;ve gathered a collection of snippets that I&amp;rsquo;ve stumbled upon along the way in bringing this website up to more modern standards.&lt;/p></description><content:encoded>&lt;p>My intro to Hugo was back in 2016, a mere 3 years after its &lt;a href="https://en.wikipedia.org/wiki/Hugo_(software)">inception&lt;/a>, when I was in my early days of learning web development. Quite a bit has changed since then, both in what Hugo is capable of and my understanding of how to actually code. I&amp;rsquo;m far from being a frontend dev, but I&amp;rsquo;ve gathered a collection of snippets that I&amp;rsquo;ve stumbled upon along the way in bringing this website up to more modern standards.&lt;/p>

&lt;h1 id="creating-new-posts" class="anchor">
 &lt;a href="#creating-new-posts">
 Creating New Posts
 &lt;/a>
&lt;/h1>
&lt;p>To create a new post, typically you would do something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">hugo new posts/2025/05/my-new-post/index.md
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>But manually typing date paths is cumbersome, so instead you can use a target like this in your Makefile:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">new:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	@if &lt;span class="o">[&lt;/span> -z &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>title&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Please provide &amp;#39;title&amp;#39;&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	hugo new posts/&lt;span class="nv">$$&lt;/span>&lt;span class="o">(&lt;/span>date +%Y/%m&lt;span class="o">)&lt;/span>/&lt;span class="k">$(&lt;/span>title&lt;span class="k">)&lt;/span>/index.md
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Which can then be used like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">make new &lt;span class="nv">title&lt;/span>&lt;span class="o">=&lt;/span>my-new-post
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h1 id="markdown-render-hooks" class="anchor">
 &lt;a href="#markdown-render-hooks">
 Markdown Render Hooks
 &lt;/a>
&lt;/h1>
&lt;p>Render hooks can be used to override markdown to HTML conversions to give full flexibility in how components are presented while staying being able to write markdown. As useful as &lt;a href="https://gohugo.io/content-management/shortcodes/">shortcodes&lt;/a> are, they typically aren&amp;rsquo;t compatible with normal markdown editors/viewers, which adds a bit of friction when you need to have two windows open (one for editing, and the other for previewing).&lt;/p>
&lt;p>Full docs are &lt;a href="https://gohugo.io/render-hooks/introduction/">here&lt;/a>, but the components below are the ones I found that I actually wanted to change.&lt;/p>

&lt;h2 id="headings" class="anchor">
 &lt;a href="#headings">
 Headings
 &lt;/a>
&lt;/h2>
&lt;p>Many websites these days have heading anchors that readers can click on, making it easy to reference or share direct sections of a page. Although Hugo doesn’t have this behaviour by default, a simple template can be used to achieve this.&lt;/p>
&lt;p>Create the following file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">layouts/_default/_markup/render-heading.html
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With content that looks something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h&lt;/span>&lt;span class="err">{{&lt;/span> &lt;span class="err">.&lt;/span>&lt;span class="na">Level&lt;/span> &lt;span class="err">}}&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ .Anchor | safeURL }}&amp;#34;&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;anchor&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;#{{ .Anchor | safeURL }}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>{{ .Text | safeHTML }}&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&amp;lt;&lt;/span>/h{{ .Level }}&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And voila, all your headings in your rendered posts are now anchored!&lt;/p>

&lt;h2 id="images" class="anchor">
 &lt;a href="#images">
 Images
 &lt;/a>
&lt;/h2>
&lt;p>I wanted to customise how images and captions were displayed, as well as being able to use markdown syntax for both images and videos. This template does that, which conditionally renders the two media types differently.&lt;/p>
&lt;p>I also wanted to show a build warning when an image can&amp;rsquo;t be found (e.g. image was moved or renamed without updating the link), so &lt;code>warnf&lt;/code> does that.&lt;/p>
&lt;p>In this file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">layouts/_default/_markup/render-image.html
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Contents look like so:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- This renders markdown image notation into pics or vids with captions --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ $dest := .Destination | safeURL }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;row img-captioned&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- Video --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ if or (hasSuffix $dest &amp;#34;.mp4&amp;#34;) (hasSuffix $dest &amp;#34;.webm&amp;#34;) }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">video&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;img-content&amp;#34;&lt;/span> &lt;span class="na">autoplay&lt;/span> &lt;span class="na">loop&lt;/span> &lt;span class="na">muted&lt;/span> &lt;span class="na">controls&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">source&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $dest }}&amp;#34;&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;video/mp4&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">video&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ else }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- Image --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ $image := .PageInner.Resources.Get $dest }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ if $image }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ $image = partial &amp;#34;resize-image.html&amp;#34; (dict &amp;#34;image&amp;#34; $image) }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="na">image&lt;/span>&lt;span class="err">.&lt;/span>&lt;span class="na">RelPermalink&lt;/span> &lt;span class="err">}}&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;&lt;/span>&lt;span class="nt">img&lt;/span> &lt;span class="na">loading&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;lazy&amp;#34;&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;img-content&amp;#34;&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="na">image&lt;/span>&lt;span class="err">.&lt;/span>&lt;span class="na">RelPermalink&lt;/span> &lt;span class="err">}}&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ else }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ warnf &amp;#34;Image not found for %s: %s&amp;#34; $.PageInner.RelPermalink $dest }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ end }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ end }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;caption&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>{{ .Text | safeHTML }}{{ with .Title }} (Source: {{ . | safeHTML }}){{ end }}&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note: There&amp;rsquo;s a &lt;code>resize-image.html&lt;/code> partial that&amp;rsquo;s referenced, more on that next!&lt;/p>

&lt;h1 id="pipes" class="anchor">
 &lt;a href="#pipes">
 Pipes
 &lt;/a>
&lt;/h1>

&lt;h2 id="compiling-and-minifying-css" class="anchor">
 &lt;a href="#compiling-and-minifying-css">
 Compiling and Minifying CSS
 &lt;/a>
&lt;/h2>
&lt;p>If you&amp;rsquo;re using SASS/SCSS in your stylesheets, Hugo can compile them for you with &lt;code>toCSS&lt;/code> without having it part of a separate build step. You can also use &lt;code>resources.Concat&lt;/code> to concatenate multiple files into a single one, and then optionally use &lt;code>resources.Minify&lt;/code> to minify them, which will work on any CSS, JS, JSON, HTML, SVG, or XML file.&lt;/p>
&lt;p>An example that combines all of these operations:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">{{ $custom := resources.Get &amp;#34;css/custom.scss&amp;#34; | toCSS }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ $main := resources.Get &amp;#34;css/main.scss&amp;#34; | toCSS }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ $styles := slice $main $custom | resources.Concat &amp;#34;/css/styles.css&amp;#34; | resources.Minify }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $styles.Permalink }}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="processing-images" class="anchor">
 &lt;a href="#processing-images">
 Processing Images
 &lt;/a>
&lt;/h2>
&lt;p>Before &lt;a href="https://gohugo.io/content-management/page-bundles/">page bundles&lt;/a>, images were typically stored under your &lt;code>/static&lt;/code> directory. But since its introduction in &lt;a href="https://github.com/gohugoio/hugo/releases/tag/v0.32">v0.32&lt;/a>, it opens up the ability to process images during the build. For example, if you want to resize and convert all your images that are displayed on your website to &lt;code>jpg&lt;/code> or &lt;code>webp&lt;/code>, you can use the partial below.&lt;/p>
&lt;blockquote>
&lt;p>Fun fact: Partials can also be used as functions, not just for rendering snippets of HTML.&lt;/p>&lt;/blockquote>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span>&lt;span class="cm">/*
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> Usage:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> {{ partial &amp;#34;resize-image.html&amp;#34; (dict &amp;#34;image&amp;#34; $image) }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> Returns: resized and processed image resource.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm">*/&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;!&lt;/span>&lt;span class="o">--&lt;/span> &lt;span class="nx">Constants&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">maxSizePx&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">1200&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">imageType&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="s">&amp;#34;jpg&amp;#34;&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;!&lt;/span>&lt;span class="o">--&lt;/span> &lt;span class="nx">Input&lt;/span> &lt;span class="nx">args&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">image&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image_ext&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nf">lower&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Ext&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">image&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">)&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;!&lt;/span>&lt;span class="o">--&lt;/span> &lt;span class="nx">do&lt;/span> &lt;span class="nx">nothing&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="nx">these&lt;/span> &lt;span class="nx">filetypes&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">eq&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image_ext&lt;/span> &lt;span class="s">&amp;#34;.gif&amp;#34;&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">eq&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image_ext&lt;/span> &lt;span class="s">&amp;#34;.png&amp;#34;&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;!&lt;/span>&lt;span class="o">--&lt;/span> &lt;span class="nx">Resize&lt;/span> &lt;span class="nx">landscape&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">gt&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Width&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">maxSizePx&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">resizeStrLandscape&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">printf&lt;/span> &lt;span class="s">&amp;#34;%dx %s&amp;#34;&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">maxSizePx&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">imageType&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Resize&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">resizeStrLandscape&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;!&lt;/span>&lt;span class="o">--&lt;/span> &lt;span class="nx">Resize&lt;/span> &lt;span class="nx">portrait&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nx">gt&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Height&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">maxSizePx&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">resizeStrPortrait&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">printf&lt;/span> &lt;span class="s">&amp;#34;x%d %s&amp;#34;&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">maxSizePx&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">imageType&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Resize&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">resizeStrPortrait&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;!&lt;/span>&lt;span class="o">--&lt;/span> &lt;span class="nx">No&lt;/span> &lt;span class="nx">resize&lt;/span> &lt;span class="nx">necessary&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">just&lt;/span> &lt;span class="nx">convert&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Process&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">imageType&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{{&lt;/span> &lt;span class="nx">end&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="nx">end&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="nx">image&lt;/span> &lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Usage of this partial would look something like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">{{ $image := .Resources.Get &amp;#34;posts/myimage.jpg&amp;#34; }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ $image = partial &amp;#34;resize-image.html&amp;#34; (dict &amp;#34;image&amp;#34; $image) }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">img&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $image.RelPermalink }}&amp;#34;&lt;/span>&lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h1 id="adaptive-syntax-highlighting" class="anchor">
 &lt;a href="#adaptive-syntax-highlighting">
 Adaptive Syntax Highlighting
 &lt;/a>
&lt;/h1>
&lt;p>Hugo has built in support for &lt;a href="https://gohugo.io/content-management/syntax-highlighting/">syntax highlighting using Chroma&lt;/a>, but the default configuration doesn&amp;rsquo;t allow for dynamically setting light/dark syntax highlighting. Fortunately there&amp;rsquo;s an easy workaround with a few configs!&lt;/p>
&lt;p>By default, your &lt;code>config.yaml&lt;/code> may look something like this, which tells the renderer to use the &lt;code>monokai&lt;/code> style everywhere:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">markup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">highlight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">style&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">monokai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Instead, we can set &lt;code>noClasses=false&lt;/code> to use an external stylesheet, which will allow us to dynamically set the theme depending if the current colour scheme is light or dark. Change the &lt;code>config.yaml&lt;/code> to this instead:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">markup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">highlight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">guessSyntax&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">noClasses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And then generate your desired stylesheets using the &lt;a href="https://gohugo.io/commands/hugo_gen_chromastyles/">Hugo CLI&lt;/a> (see &lt;a href="https://gohugo.io/quick-reference/syntax-highlighting-styles/">here&lt;/a> for the full list of supported themes):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Light mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">hugo gen chromastyles --style&lt;span class="o">=&lt;/span>monokailight &amp;gt; static/css/syntax.css
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Dark mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">hugo gen chromastyles --style&lt;span class="o">=&lt;/span>monokai &amp;gt; static/css/syntax-dark.css
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And then include the following in your header to use these stylesheets depending on the colour scheme:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/css/syntax.css&amp;#34;&lt;/span> &lt;span class="na">media&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;screen&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/css/syntax-dark.css&amp;#34;&lt;/span> &lt;span class="na">media&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;screen and (prefers-color-scheme: dark)&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>Depending on your other stylesheets, this may be all you need. However, if this still results in some wonky dark mode colours, you can make some manual edits to fix it. The exported chroma stylesheet contains empty classes, which normally would inherit the default text colour from your site, but since we&amp;rsquo;re defining two stylesheets, it may end up inheriting from the light theme instead.&lt;/p>
&lt;p>To resolve this, we&amp;rsquo;ll need to do a &lt;strong>search and replace&lt;/strong> of &lt;code>{ }&lt;/code> to &lt;code>{ color:inherit }&lt;/code> in the second stylesheet, to properly render the text colours based on the colour scheme. So change this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* Generated using: hugo gen chromastyles --style=monokai */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* Background */&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">bg&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mh">#f8f8f2&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mh">#272822&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* PreWrapper */&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">chroma&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mh">#f8f8f2&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mh">#272822&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* Other */&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">chroma&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">x&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* Generated using: hugo gen chromastyles --style=monokai */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* Background */&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">bg&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mh">#f8f8f2&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mh">#272822&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* PreWrapper */&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">chroma&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mh">#f8f8f2&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mh">#272822&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl">&lt;span class="c">/* Other */&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">chroma&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">x&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="kc">inherit&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h1 id="building-and-deploying" class="anchor">
 &lt;a href="#building-and-deploying">
 Building and Deploying
 &lt;/a>
&lt;/h1>
&lt;p>Since my website is hosted on GitHub Pages, I used to manually build and deploy the assets with a script that looked something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1"># Usage:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ./deploy.sh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ./deploy.sh &amp;#34;Your optional commit message&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># If a command fails then the deploy stops&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -e
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;\033[0;32mRebuilding website...\033[0m\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Build the project.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">hugo &lt;span class="c1"># if using a theme, replace with `hugo -t &amp;lt;YOURTHEME&amp;gt;`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Go To publish folder&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> docs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Add changes to git.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Commit changes.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">msg&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Rebuild site (&lt;/span>&lt;span class="k">$(&lt;/span>date&lt;span class="k">)&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$*&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">msg&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">msg&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">: &lt;/span>&lt;span class="nv">$*&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$msg&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s2">&amp;#34;\033[0;32mPushing updates to GitHub...\033[0m\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Push source and build repos.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>However, after learning about CI/CD and that it can be quite easy to set up these days, that manual deployment has now been replaced with a Github Action. It runs on commits to main, which then Github picks up on the &lt;code>gh-pages&lt;/code> branch.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Github Pages&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pull_request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">submodules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Fetch Hugo themes (true OR recursive)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fetch-depth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Fetch all history for .GitInfo and .Lastmod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Setup Hugo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">peaceiris/actions-hugo@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hugo-version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;0.147.1&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extended&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hugo --minify&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">peaceiris/actions-gh-pages@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github.ref == &amp;#39;refs/heads/main&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">github_token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.GITHUB_TOKEN }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">publish_dir&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./docs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h1 id="bonus-using-obsidian-as-a-local-cms" class="anchor">
 &lt;a href="#bonus-using-obsidian-as-a-local-cms">
 Bonus: Using Obsidian as a Local CMS
 &lt;/a>
&lt;/h1>
&lt;p>Since Hugo posts are just markdown files, I started using &lt;a href="https://obsidian.md/">Obsidian&lt;/a> to edit the files. Previously, I was writing content in my IDE, but something about it felt a bit off &amp;ndash; the virtual atmosphere that promoted writing code just didn’t hit the same for writing creatively.&lt;/p>
&lt;p>I wanted a simple &lt;a href="https://en.wikipedia.org/wiki/WYSIWYG">WYSIWYG&lt;/a> editor to write my posts in, and with Obsidian being more popular than ever these days, I incorporated it into my workflow and have been finding it quite a joy to use! Now, I can write content in a clean interface using markdown, and have the niceties of having the elements rendered similar to how it&amp;rsquo;d look on my website without needing to have a separate window open to show the preview.&lt;/p>

&lt;h2 id="settings" class="anchor">
 &lt;a href="#settings">
 Settings
 &lt;/a>
&lt;/h2>
&lt;p>Be sure to disable &lt;strong>&amp;ldquo;Use Wikilinks&amp;rdquo;&lt;/strong> so that markdown format is used for image links, and select &lt;strong>&amp;ldquo;Default location for new attachments&amp;rdquo;&lt;/strong> is set to the &lt;strong>&amp;ldquo;Same folder as current file&amp;rdquo;&lt;/strong>, which works well if page bundles are used.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/hugo-tips-and-tricks/settings.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/hugo-tips-and-tricks/settings.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Obsidian settings -&amp;gt; Files and links&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Note that the filename of the pasted image may not render correctly by Hugo, since it typically doesn&amp;rsquo;t play well with spaces in image filenames. To fix this, just rename the image in Obsidian, which will automatically update the link in your markdown document.&lt;/p>

&lt;h2 id="plugins" class="anchor">
 &lt;a href="#plugins">
 Plugins
 &lt;/a>
&lt;/h2>
&lt;p>Some days I want to stay within Obsidian and not have to open a separate terminal to commit my changes to Github, and the &lt;a href="https://github.com/Vinzent03/obsidian-git">Obsidian Git Plugin&lt;/a> makes that possible. After setting it up, you can do normal git operations straight from Obsidian for a truly seamless workflow.&lt;/p>
&lt;p>There&amp;rsquo;s a plethora of other &lt;a href="https://obsidian.md/plugins">community of plugins&lt;/a> which you can incorporate into your own writing workflow.&lt;/p>

&lt;h2 id="front-matter" class="anchor">
 &lt;a href="#front-matter">
 Front Matter
 &lt;/a>
&lt;/h2>
&lt;p>Hugo supports defining your posts&amp;rsquo; front matter in &lt;code>yaml&lt;/code> or &lt;code>toml&lt;/code>, but Obsidian only supports rendering &lt;code>yaml&lt;/code>. If you have a front matter that looks like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-md" data-lang="md">&lt;span class="line">&lt;span class="cl">---
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">title: New Post
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">date: 2025-05-23T14:34:05-07:00
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tagline:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">image: placeholder.jpeg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tags:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">draft: true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">layout: single
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">type: blog
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">aliases:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">---
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Lorem ipsum dolor sit amet...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It&amp;rsquo;ll be rendered in Obsidian like so, which is pretty nifty!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2025/05/hugo-tips-and-tricks/post.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2025/05/hugo-tips-and-tricks/post.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">A nicely rendered markdown document, a welcome upgrade to writing in an IDE.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="on-the-go" class="anchor">
 &lt;a href="#on-the-go">
 On the Go
 &lt;/a>
&lt;/h2>
&lt;p>Obsidian also has iOS and Android apps, but the git plugin isn&amp;rsquo;t the most stable, at least not on iOS. As an alternative, you can follow the steps described in this &lt;a href="https://forum.obsidian.md/t/setting-up-obsidian-git-on-ios-without-ish-or-working-copy/97800">Obsidian forum thread&lt;/a> on setting up either &lt;a href="https://ish.app/">iSH&lt;/a> (free) or &lt;a href="https://workingcopy.app/">Working Copy&lt;/a> (paid).&lt;/p>
&lt;p>With iSH, git can be slow at times (e.g. during &lt;code>git status&lt;/code> or &lt;code>git checkout&lt;/code>) since to make it work, it emulates an x86 architecture on ARM-based iOS devices, so the added overhead takes a toll on performance.&lt;/p>
&lt;p>But hey, at least works! Paired with automated deployments, it provides a relatively painless (and free!) way for me to update my website from my phone.&lt;/p></content:encoded></item><item><title>Using Google Cloud Pubsub for Batch Pipelines in Apache Beam</title><link>https://justinmklam.com/posts/2022/11/apache-beam-pubsub/</link><pubDate>Wed, 23 Nov 2022 12:31:53 -0800</pubDate><guid>https://justinmklam.com/posts/2022/11/apache-beam-pubsub/</guid><description>&lt;p>Google Cloud&amp;rsquo;s &lt;a href="https://cloud.google.com/pubsub/docs/overview">Pub/Sub&lt;/a> is a useful service that provides an asynchronous and scalable messaging platform that decouples services producing messages from those that receive and process those messages. When combined with &lt;a href="https://github.com/apache/beam">Apache Beam&lt;/a> (and/or &lt;a href="https://cloud.google.com/dataflow/docs/about-dataflow">Dataflow&lt;/a>, Google&amp;rsquo;s managed version of it), you can quickly develop powerful batch and streaming pipelines for data-parallel processing.&lt;/p></description><content:encoded>&lt;p>Google Cloud&amp;rsquo;s &lt;a href="https://cloud.google.com/pubsub/docs/overview">Pub/Sub&lt;/a> is a useful service that provides an asynchronous and scalable messaging platform that decouples services producing messages from those that receive and process those messages. When combined with &lt;a href="https://github.com/apache/beam">Apache Beam&lt;/a> (and/or &lt;a href="https://cloud.google.com/dataflow/docs/about-dataflow">Dataflow&lt;/a>, Google&amp;rsquo;s managed version of it), you can quickly develop powerful batch and streaming pipelines for data-parallel processing.&lt;/p>
&lt;p>However, I recently ran into one slight hiccup - although Apache Beam has a &lt;a href="https://beam.apache.org/releases/pydoc/2.4.0/apache_beam.io.gcp.pubsub.html#module-apache_beam.io.gcp.pubsub">built-in IO connector for pubsub&lt;/a>, it only supported streaming pipelines (at the time of development). Fortunately, after a bit of searching, someone else on &lt;a href="https://stackoverflow.com/a/67755184/7543727">Stack Overflow&lt;/a> figured out a workable solution:&lt;/p>
&lt;blockquote>
&lt;p>The trick is that if you call future.result() inside the process() method, you will block until that single message is successfully published, so instead collect a list of futures and then at the end of the bundle make sure they&amp;rsquo;re all either published or definitively timed out. Some quick testing with one of our internal pipelines suggested that this approach can publish 1.6M messages in ~200s.&lt;/p>&lt;/blockquote>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">apache_beam&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">beam&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">apache_beam.io.gcp.pubsub&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">PubsubMessage&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">google.cloud.pubsub_v1&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">PublisherClient&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">google.cloud.pubsub_v1.types&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BatchSettings&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">LimitExceededBehavior&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">PublishFlowControl&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">PublisherOptions&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">PublishClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PublisherClient&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> You have to override __reduce__ to make PublisherClient pickleable 😡 😤 🤬
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> Props to &amp;#39;Ankur&amp;#39; and &amp;#39;Benjamin&amp;#39; on SO for figuring this part out; god knows
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> I would not have...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">__reduce__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__class__&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">batch_settings&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">publisher_options&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">BatchPubsubWriter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">beam&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">DoFn&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> beam.io.gcp.pubsub does not yet support batch operations, so
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> we do this the hard way. it&amp;#39;s not as performant as the native
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> pubsubio but it does the job.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">topic&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">topic&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">topic&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">window&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">beam&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">window&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">GlobalWindow&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">setup&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">batch_settings&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">BatchSettings&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">max_bytes&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">1e6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># 1MB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># by default it is 10 ms, should be less than timeout used in future.result() to avoid timeout&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">max_latency&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">publisher_options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">PublisherOptions&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enable_message_ordering&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># better to be slow than to drop messages during a recovery...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">flow_control&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">PublishFlowControl&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">limit_exceeded_behavior&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">LimitExceededBehavior&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">BLOCK&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">publisher&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">PublishClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">batch_settings&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">publisher_options&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">start_bundle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">futures&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">process&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">element&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">PubsubMessage&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">window&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">beam&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">DoFn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">WindowParam&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">window&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">window&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">futures&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">publisher&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">publish&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">topic&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">topic&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">element&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">**&lt;/span>&lt;span class="n">element&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">attributes&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">finish_bundle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Iterate over the list of async publish results and block
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> until all of them have either succeeded or timed out. Yield
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> a WindowedValue of the success/fail counts.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">count&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">futures&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">fut&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">futures&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># future.result() blocks until success or timeout;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># we&amp;#39;ve set a max_latency of 60s upstairs in BatchSettings,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># so we should never spend much time waiting here.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fut&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">60&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">Exception&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">ex&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ex&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">res_count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;success&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">res&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">results&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">res&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">res_count&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;success&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># if it&amp;#39;s not a string, it&amp;#39;s an exception&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">msg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">res&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">msg&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">res_count&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">res_count&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">msg&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">res_count&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">msg&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Pubsub publish results: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">res_count&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">yield&lt;/span> &lt;span class="n">beam&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">utils&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">windowed_value&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">WindowedValue&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">res_count&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">timestamp&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">windows&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">window&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">teardown&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Published &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">count&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> messages&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Unfortunately, I eventually ran into an issue where the pubsub client was being overloaded when processing large amounts of data under specifying conditions. Explicitly batching the data mitigated the issue, instead of letting the (Dataflow) runner auto-magically determining the bundle sizes based on the input data.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">apache_beam&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">beam&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># This can probably be higher if needed, but works fine as it is&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">MIN_PUBSUB_BATCH_SIZE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">10000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Have tested up to 400k, but instability seems to start occurring at these bundle sizes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">MAX_PUBSUB_BATCH_SIZE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">200000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">beam&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Pipeline&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">options&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">pipeline_options&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">pipeline&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pipeline&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">|&lt;/span> &lt;span class="s2">&amp;#34;Batch Messages for Pubsub&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">beam&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transforms&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">util&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">BatchElements&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">min_batch_size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">MIN_PUBSUB_BATCH_SIZE&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">max_batch_size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">MAX_PUBSUB_BATCH_SIZE&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">|&lt;/span> &lt;span class="s2">&amp;#34;Publish Batches to PubSub&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">beam&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ParDo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BatchPubsubWriter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;projects/myproject/topics/my-topic&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Hopefully Apache Beam eventually adds official support for pubsub in batch pipelines, but until then, this seems to suffice for processing millions of rows of data per job.&lt;/p></content:encoded></item><item><title>Tips and Tricks with Terraform's null_resource</title><link>https://justinmklam.com/posts/2022/05/terraform-null-resource/</link><pubDate>Tue, 17 May 2022 20:21:37 -0800</pubDate><guid>https://justinmklam.com/posts/2022/05/terraform-null-resource/</guid><description>&lt;p>Terraform&amp;rsquo;s &lt;code>null_resource&lt;/code> resource can be useful when there aren&amp;rsquo;t any existing modules to satisfy your needs (with some caveats). &lt;a href="https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource">Hashicorp&amp;rsquo;s documentation&lt;/a> for it is a bit lacking, but fortunately there&amp;rsquo;s more information about the provisioners in their other docs &lt;a href="https://developer.hashicorp.com/terraform/language/resources/provisioners/syntax">here&lt;/a>. After using these resources in a handful of places across our infrastructure deployments, I&amp;rsquo;ve developed a small collection of tips I picked up over the past few months that I thought I&amp;rsquo;d share.&lt;/p></description><content:encoded>&lt;p>Terraform&amp;rsquo;s &lt;code>null_resource&lt;/code> resource can be useful when there aren&amp;rsquo;t any existing modules to satisfy your needs (with some caveats). &lt;a href="https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource">Hashicorp&amp;rsquo;s documentation&lt;/a> for it is a bit lacking, but fortunately there&amp;rsquo;s more information about the provisioners in their other docs &lt;a href="https://developer.hashicorp.com/terraform/language/resources/provisioners/syntax">here&lt;/a>. After using these resources in a handful of places across our infrastructure deployments, I&amp;rsquo;ve developed a small collection of tips I picked up over the past few months that I thought I&amp;rsquo;d share.&lt;/p>
&lt;p>Use &lt;code>timestamp()&lt;/code> as a trigger when you need the resource to run on every deployment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;null_resource&amp;#34; &amp;#34;always-run&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> triggers&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> timestamp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">timestamp&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">provisioner&lt;/span> &lt;span class="s2">&amp;#34;local-exec&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> command&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;echo foobar&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can also supply multiple &lt;code>provisioner&lt;/code> blocks, where one of them can be configured with &lt;code>when = destroy&lt;/code> to specify the action to take when the resource will be destroyed. Dynamic values need to be accessed using &lt;code>self.triggers.*&lt;/code> since Terraform isn&amp;rsquo;t able to resolve the values at runtime during resource destruction.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;null_resource&amp;#34; &amp;#34;include-cleanup&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> triggers&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">region&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"> # Executes on resource creation {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n"> command&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34; &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;echo&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;create:&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;${self.triggers.name}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;${self.triggers.region}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"> # Executes on resource destruction
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">provisioner&lt;/span> &lt;span class="s2">&amp;#34;local-exec&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> when&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">destroy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> command&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34; &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;echo&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;destroy:&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;${self.triggers.name}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;${self.triggers.region}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>local-exec&lt;/code> provisioner also allows for an &lt;a href="https://www.terraform.io/docs/language/resources/provisioners/local-exec.html#environment">environment&lt;/a> field, which can be used to easily pass values into commands or scripts:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;null_resource&amp;#34; &amp;#34;environment-values&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">provisioner&lt;/span> &lt;span class="s2">&amp;#34;local-exec&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> command&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;./some-script.sh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> environment&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> ENVIRONMENT&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">environment&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> JSON_DATA&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">jsonencode&lt;/span>&lt;span class="p">(&lt;/span>{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> &amp;#34;var1&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> &amp;#34;var2&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">region&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Aside from provisioning resources, &lt;code>null_resource&lt;/code> can also be used for debugging values (which I stumbled upon from &lt;a href="https://nexxai.dev/how-to-debug-terraform-variable-content-using-this-custom-module/">nexxai.dev&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;null_resource&amp;#34; &amp;#34;terraform-debug&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">provisioner&lt;/span> &lt;span class="s2">&amp;#34;local-exec&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> command&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;echo $VARIABLE1 &amp;gt;&amp;gt; debug.txt; echo $VARIABLE2 &amp;gt;&amp;gt; debug.txt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> environment&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> VARIABLE1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">jsonencode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">your_variable_name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> VARIABLE2&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">jsonencode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">piece_of_data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded></item><item><title>Deploying Google Cloud Functions with Terraform</title><link>https://justinmklam.com/posts/2022/03/deploy-cloud-functions-terraform/</link><pubDate>Tue, 08 Mar 2022 11:47:29 -0800</pubDate><guid>https://justinmklam.com/posts/2022/03/deploy-cloud-functions-terraform/</guid><description>&lt;p>Cloud Functions are an easy, performant, and potentially inexpensive way to build serverless backends. I recently went down the route of setting up continuous deployments for them, and thought I&amp;rsquo;d share my learnings with it.&lt;/p></description><content:encoded>&lt;p>Cloud Functions are an easy, performant, and potentially inexpensive way to build serverless backends. I recently went down the route of setting up continuous deployments for them, and thought I&amp;rsquo;d share my learnings with it.&lt;/p>
&lt;p>The easiest way to deploy a Cloud Function is using the &lt;code>gcloud&lt;/code> CLI like so:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">gcloud functions deploy &amp;lt;YOUR_FUNCTION_NAME&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --region&lt;span class="o">=&lt;/span>&amp;lt;YOUR_REGION&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --runtime&lt;span class="o">=&lt;/span>&amp;lt;YOUR_RUNTIME&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --source&lt;span class="o">=&lt;/span>&amp;lt;YOUR_SOURCE_LOCATION&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --entry-point&lt;span class="o">=&lt;/span>&amp;lt;YOUR_CODE_ENTRYPOINT&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &amp;lt;TRIGGER_FLAGS&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Things get a bit more complicated if you want to use Terraform for deployments, which has its own set of advantages. The main trick to getting it to work is in line 43 below, where a checksum is appended to the archive&amp;rsquo;s filename every time it&amp;rsquo;s uploaded to the storage bucket.&lt;/p>
&lt;p>The reason this is necessary is because the &lt;code>google_cloudfunctions_function&lt;/code> resource won&amp;rsquo;t be triggered for a redeployment on subsequent code changes - by having a checksum generated based on the source code, we ensure that Terraform knows to redeploy the Cloud Function whenever the underlying code changes.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">locals&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n"> project_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;my-project&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;us-west1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> component&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;my-component&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> cloud_function&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${local.component}-cf&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> description&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Some description for this cloud function&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> runtime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;python39&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="n"> entry_point&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;my_entry_point&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="n"> source_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;./src&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="n"> archive_filepath&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/path/to/file&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">}&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1"># Service account for the Cloud Function
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;google_service_account&amp;#34; &amp;#34;cloud_function_sa&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="n"> project&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">project_id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="n"> account_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">component&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="n"> display_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">component&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">}&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="c1"># Bucket to store the source code archives
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;google_storage_bucket&amp;#34; &amp;#34;function_archive&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${local.component}-cloud-function-archive&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="n"> location&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">region&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="n"> project&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">project_id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="n"> uniform_bucket_level_access&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">}&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">&lt;span class="c1"># Archive the source code as a zip file
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">data&lt;/span> &lt;span class="s2">&amp;#34;archive_file&amp;#34; &amp;#34;function_archive&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl">&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;zip&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl">&lt;span class="n"> source_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cloud_functions&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">source_dir&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl">&lt;span class="n"> output_path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.root}/${local.cloud_function.archive_filepath}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl">}&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl">&lt;span class="c1"># Upload the source code archive to the bucket. This will happen each time
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl">&lt;span class="c1"># the source code changes.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;google_storage_bucket_object&amp;#34; &amp;#34;archive&amp;#34;&lt;/span> {&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl">&lt;span class="c1"> # Append checksum so file changes trigger a cloud function deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">43&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">format&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">44&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;%s#%s&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">45&lt;/span>&lt;span class="cl"> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cloud_function&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">archive_filepath&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">46&lt;/span>&lt;span class="cl"> &lt;span class="k">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">archive_file&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">function_archive&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">output_md5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">47&lt;/span>&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">48&lt;/span>&lt;span class="cl">&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">google_storage_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">function_archive&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">49&lt;/span>&lt;span class="cl">&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">archive_file&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">function_archive&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">output_path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">50&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">51&lt;/span>&lt;span class="cl">&lt;span class="n"> content_disposition&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;attachment&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">52&lt;/span>&lt;span class="cl">&lt;span class="n"> content_encoding&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;gzip&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">53&lt;/span>&lt;span class="cl">&lt;span class="n"> content_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;application/zip&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">54&lt;/span>&lt;span class="cl">}&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">55&lt;/span>&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">56&lt;/span>&lt;span class="cl">&lt;span class="c1"># Cloud Function that uses the uploaded source code archive
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">57&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;google_cloudfunctions_function&amp;#34; &amp;#34;some_cloud_function&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">58&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cloud_function&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">59&lt;/span>&lt;span class="cl">&lt;span class="n"> description&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Some description for my cloud function&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">60&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">61&lt;/span>&lt;span class="cl">&lt;span class="n"> project&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">project_id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">62&lt;/span>&lt;span class="cl">&lt;span class="n"> region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">region&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">63&lt;/span>&lt;span class="cl">&lt;span class="n"> source_archive_bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">google_storage_bucket_object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">archive&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">bucket&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="ln">64&lt;/span>&lt;span class="cl">&lt;span class="n"> source_archive_object&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">google_storage_bucket_object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">archive&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">65&lt;/span>&lt;span class="cl">&lt;span class="n"> service_account_email&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">google_service_account&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cloud_function_sa&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">email&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">66&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">67&lt;/span>&lt;span class="cl">&lt;span class="n"> runtime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cloud_function&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">runtime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">68&lt;/span>&lt;span class="cl">&lt;span class="n"> entry_point&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cloud_function&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">entry_point&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">69&lt;/span>&lt;span class="cl">&lt;span class="n"> trigger_http&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">70&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded></item><item><title>Precision Sourdough: A Smart Lid for Your Starter</title><link>https://justinmklam.com/posts/2021/02/levain-monitor/</link><pubDate>Mon, 22 Feb 2021 16:36:18 +0000</pubDate><guid>https://justinmklam.com/posts/2021/02/levain-monitor/</guid><description>&lt;blockquote>
&lt;p>Featured on &lt;a href="https://hackaday.com/2021/03/04/smart-lid-spies-on-sourdough-starter-sends-data-wirelessly/">Hackaday&lt;/a>!&lt;/p>&lt;/blockquote>
&lt;p>A few years ago, I had the idea to &lt;a href="https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/">track my sourdough starter using computer vision&lt;/a>. It was neat to monitor it this way, but it was fairly impractical to do for each feeding since it required setting up a camera, downloading the images, and doing some manual image cropping before running it through my analysis script. The analysis was also only done after the fact, and what I really wanted was something that could tell me when the starter was ready to be used (or fed), or, if I missed the window of opportunity, how long ago it peaked.&lt;/p></description><content:encoded>&lt;blockquote>
&lt;p>Featured on &lt;a href="https://hackaday.com/2021/03/04/smart-lid-spies-on-sourdough-starter-sends-data-wirelessly/">Hackaday&lt;/a>!&lt;/p>&lt;/blockquote>
&lt;p>A few years ago, I had the idea to &lt;a href="https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/">track my sourdough starter using computer vision&lt;/a>. It was neat to monitor it this way, but it was fairly impractical to do for each feeding since it required setting up a camera, downloading the images, and doing some manual image cropping before running it through my analysis script. The analysis was also only done after the fact, and what I really wanted was something that could tell me when the starter was ready to be used (or fed), or, if I missed the window of opportunity, how long ago it peaked.&lt;/p>
&lt;p>Last year, I came across &lt;a href="https://www.reddit.com/r/Sourdough/comments/duhqmd/i_built_a_device_that_tracks_the_development_of/">this Reddit thread&lt;/a> and &lt;a href="https://www.twilio.com/blog/sourd-io-is-a-fitness-tracker-for-your-sourdough-starter">Christine Sunu&amp;rsquo;s sourd.io project&lt;/a>, where they both had distance sensors inside the lid to measure the height of the starter. I thought it was genius, and had to make one for myself! However, in addition to the live monitoring, I wanted to log the data for further analysis, so I also decided to make it internet-connected as a way to get the data off the device (since saving to an SD card would add hardware costs, as well as being less &amp;ldquo;sexy&amp;rdquo; in today&amp;rsquo;s world of everything having wifi connectivity).&lt;/p>
&lt;p>Interested in making your own? All the design files and code can be found on &lt;a href="https://github.com/justinmklam/iot-sourdough-starter-monitor">GitHub&lt;/a>!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/jar.gif>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/jar.gif>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Three modes of operation: Max rise and time, graph, stats for nerds.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/webapp.gif>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/webapp.gif>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Selecting, viewing, and downloading data for a given feeding session.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>&lt;em>Aside: In honesty, this project took longer than I originally expected, and I had spouts of project fatigue where I had zero motivation to work on this anymore. Eventually, a third (or maybe fourth, I lost count) wave of inspiration came to me, and I managed to finish the last remaining bits of this project. I&amp;rsquo;m glad I did though, because this turned out to be one of the more nifty gadgets I put together. I&amp;rsquo;m telling you this because people are often talking about side projects and hustles (especially software engineers), and I want to say that it&amp;rsquo;s ok to focus on your mental well-being and&lt;/em> &lt;a href="https://www.youtube.com/watch?v=9-XkF1so5rI">&lt;em>just be a potato sometimes&lt;/em>&lt;/a>&lt;em>.&lt;/em>&lt;/p>

&lt;h1 id="the-development" class="anchor">
 &lt;a href="#the-development">
 The Development
 &lt;/a>
&lt;/h1>
&lt;p>If you&amp;rsquo;re only interested in the resulting data that came out of this, you can skip to &lt;a href="#the-analysis">the end&lt;/a> where I visualize the growth, temperature, and humidity from a few weeks of worth of feedings. Otherwise, read on to learn about the development of this high specialized, mildly esoteric kitchen gadget!&lt;/p>

&lt;h2 id="hardware" class="anchor">
 &lt;a href="#hardware">
 Hardware
 &lt;/a>
&lt;/h2>

&lt;h3 id="electronics" class="anchor">
 &lt;a href="#electronics">
 Electronics
 &lt;/a>
&lt;/h3>
&lt;p>With the idea in mind, I bought the components off Digikey and AliExpress and hooked them up on a breadboard. The parts I used for this project:&lt;/p>
&lt;ul>
&lt;li>NodeMCU ESP8266&lt;/li>
&lt;li>VL6180X Time of flight distance sensor&lt;/li>
&lt;li>DHT22 Temperature and humidity sensor&lt;/li>
&lt;li>SSD1306 128x32 OLED display&lt;/li>
&lt;/ul>
&lt;p>I wrote some code to test that all components worked correctly, then went on to design a PCB to make it easier to integrate into the form factor of a jar lid.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/IMG_1513_hu_6f58c0f8a7a5019.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/IMG_1513_hu_6f58c0f8a7a5019.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Breadboard prototype with off-the-shelf modules.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>I&amp;rsquo;ve made PCBs using protoboards before (in my &lt;a href="https://justinmklam.com/posts/2017/05/sous-vide-controller/#the-controller">sous vide controller&lt;/a>), but it was extremely time consuming. Since I surpassed the learning curve of designing boards in KiCad, now I&amp;rsquo;d rather wait a few weeks for the boards to show up from overseas, especially since it&amp;rsquo;s so cheap ($2 for 5 boards, plus $14 shipping).&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/kicad.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/kicad.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">PCB layout and schematic.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/pcb.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/pcb.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Top of the PCB with the display (left), bottom with the distance and temperature/humidity sensors (right).&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>At this point, I was eager to try it out, so I cut a hole in a plastic yogurt lid and taped the assembled PCB on.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/IMG_1618_hu_980187d9707970ad.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/IMG_1618_hu_980187d9707970ad.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">It ain&amp;rsquo;t pretty, but it works.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>It worked well enough to test out the initial workflow, and I realized that I needed few crucial things to make the measurements actually useful:&lt;/p>
&lt;ul>
&lt;li>The height of the jar&lt;/li>
&lt;li>The starting height of the starter&lt;/li>
&lt;li>The height the starter has grown&lt;/li>
&lt;/ul>
&lt;p>With a bit of algebra and math, these values were now measured by the distance sensor, mounted at the bottom of the lid.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/diagram_hu_a9bc657475fe00ed.jpeg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/diagram_hu_a9bc657475fe00ed.jpeg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Algebra? More like alge-bread!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Some definitions:&lt;/p>
$$ d_1 = distance \\ to \\ jar $$&lt;p>
&lt;/p>
$$ d_2 = distance \\ to \\ starting \\ height $$&lt;p>
&lt;/p>
$$ d_3 = distance \\ to \\ current \\ height $$$$ h_1 = starting \\ height $$&lt;p>
&lt;/p>
$$ h_2 = current \\ height $$&lt;p>And the governing equations:&lt;/p>
$$ h_1 = d_1 - d_2 $$&lt;p>
&lt;/p>
$$ h_2 = d_2 - d_3 $$&lt;p>
&lt;/p>
$$ h_{rise percent} = h_2 / h_1 = (d_2 - d_3) / (d_1 - d_2) $$&lt;p>Resulting in our formula for calculating the rise percentage of the starter!&lt;/p>

&lt;h3 id="enclosure" class="anchor">
 &lt;a href="#enclosure">
 Enclosure
 &lt;/a>
&lt;/h3>
&lt;p>I designed the enclosure in Fusion 360 and printed it on my &lt;a href="https://justinmklam.com/posts/2017/03/mp-select-mini/">Monoprice Mini 3D Printer&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/fusion-360.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/fusion-360.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">3D printed enclosure, designed in Fusion 360.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>I wasn&amp;rsquo;t the proudest of this design, since I had to resort to using hot glue to attach the PCB to the enclosure, mainly because I forgot to leave enough hole clearance on the PCB . Ideally, the PCB would drop into the top half and assemble from the back (instead of the bottom half, as designed), but unfortunately I didn&amp;rsquo;t have this forethought. I was designing the PCB to have a minimal footprint to keep costs low, instead of making it easy to integrate with!&lt;/p>
&lt;p>Since I didn&amp;rsquo;t want to solder the modules directly on to the PCB, I used female headers to keep them removable. However, this gave the assembly quite a bit of height, which resulted in the button needing some type of extension. I didn&amp;rsquo;t want to make another Digikey order specifically for this button, so I decided to be resourceful and glued a machine screw to the button to make up for the missing height.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/IMG_4185_hu_296cf78fdcca36b6.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/IMG_4185_hu_296cf78fdcca36b6.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Yes, that&amp;rsquo;s a machine screw hot glued on to a switch&amp;hellip;&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Anyway, it assembled together without issues, and the only mistake you can see from the outside is the screw head (or let&amp;rsquo;s just say I wanted to go for an intentional steampunk look)!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/jar2_hu_8ca09687545a6900.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/jar2_hu_8ca09687545a6900.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">An active, healthy starter being carefully monitored.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="firmware" class="anchor">
 &lt;a href="#firmware">
 Firmware
 &lt;/a>
&lt;/h2>
&lt;p>I wanted to use a task-based architecture, since having everything in a single state machine can become convoluted and difficult to debug. With multitasking, the processor is only executing a single task at any given time, but it switches between each task rapidly to give the illusion of concurrency. I&amp;rsquo;ve used FreeRTOS in other projects, but wanted something more lightweight since I didn&amp;rsquo;t need all the bells and whistles that it provides.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=TaskExecution.gif>&lt;img class="img-responsive img-content" src=TaskExecution.gif />&lt;/a>
 &lt;p class="caption">Multitasking vs concurrency (Source: &lt;a href=https://www.freertos.org/implementation/a00004.html> FreeRTOS&lt;/a>) &lt;/p>
&lt;/div>

&lt;p>I eventually came across &lt;a href="https://github.com/arkhipenko/TaskScheduler">TaskScheduler&lt;/a>, a cooperative multitasking framework, which checked all the boxes I needed.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=TaskScheduler_html.png>&lt;img class="img-responsive img-content" src=TaskScheduler_html.png />&lt;/a>
 &lt;p class="caption">A lightweight implementation of cooperative multitasking by TaskScheduler. (Source: &lt;a href=https://github.com/arkhipenko/TaskScheduler> GitHub&lt;/a>) &lt;/p>
&lt;/div>

&lt;p>The code was divided up into the following tasks/files:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>measurements.cpp&lt;/strong> - Read the sensors and make the measurements available for other tasks&lt;/li>
&lt;li>&lt;strong>userinput.cpp&lt;/strong> - Handle the button presses from user input (short press, long press, double click)&lt;/li>
&lt;li>&lt;strong>display.cpp&lt;/strong> - Display information to the user&lt;/li>
&lt;li>&lt;strong>iot.cpp&lt;/strong> - Send the measured data to the cloud&lt;/li>
&lt;/ul>
&lt;p>The benefit of this architecture is that each file is less than 200 lines of code, and the clear separation of concerns made it easy to develop and debug.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/jar1_hu_5ecf20e2661dc593.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/jar1_hu_5ecf20e2661dc593.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The display shows how much it peaked, and how much time elapsed since it peaked.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>With the architecture in place, the firmware itself was fairly straightforward. The only &amp;ldquo;fancy&amp;rdquo; thing I needed to do was save the jar height to EEPROM so that it would be saved between sessions, even if it&amp;rsquo;s powered off. Interestingly, the ESP8266 doesn&amp;rsquo;t actually have genuine EEPROM memory, so it&amp;rsquo;s &lt;a href="https://www.arduino.cc/reference/en/libraries/esp_eeprom/">emulated by using a section of flash memory&lt;/a>.&lt;/p>

&lt;h2 id="cloud-connectivity" class="anchor">
 &lt;a href="#cloud-connectivity">
 Cloud Connectivity
 &lt;/a>
&lt;/h2>
&lt;p>One of the more time consuming parts of this project was actually getting AWS set up on the ESP8266. I encountered many library compatibility issues, which I should have expected since this chip was released 6 years ago (at the time of development)! If I didn&amp;rsquo;t have an ESP8266 in my box of parts that I wanted to use, I would have used the newer ESP32, which (hopefully) would have presented fewer issues.&lt;/p>
&lt;p>If you&amp;rsquo;re looking to do the same, let me save you some headache! You can check out the template I made on &lt;a href="https://github.com/justinmklam/aws-iot-esp266-demo">GitHub&lt;/a> to get set up.&lt;/p>
&lt;p>Once I got it publishing messages over MQTT to AWS, I set up the cloud infrastructure to receive and save the data. The data flow was:&lt;/p>
&lt;ol>
&lt;li>Device sends the data in a message over MQTT&lt;/li>
&lt;li>On a message receive event, an &lt;a href="https://aws.amazon.com/lambda/">AWS Lambda&lt;/a> function is triggered to parse the data from the message and passes it to a &lt;a href="https://aws.amazon.com/kinesis/data-firehose/">Kinesis Firehose&lt;/a> delivery stream&lt;/li>
&lt;li>Kinesis Firehose receives the data and saves it &lt;a href="https://aws.amazon.com/s3/">Amazon S3&lt;/a>&lt;/li>
&lt;/ol>
&lt;div class="row img-captioned">
 &lt;a href=aws-iot.png>&lt;img class="img-responsive img-content" src=aws-iot.png />&lt;/a>
 &lt;p class="caption">AWS architecture for basic IoT applications. (Source: &lt;a href=https://dzone.com/articles/design-practices-aws-iot-solutions-volansys> DZone&lt;/a>) &lt;/p>
&lt;/div>

&lt;p>I initially was going to use &lt;a href="https://aws.amazon.com/quicksight/">Amazon QuickSight&lt;/a> to visualize the data, but there were limitations with the refresh rate that were a deal breaker for me. Instead, I bit the bullet and created a custom dashboard using &lt;a href="https://flask.palletsprojects.com/">Flask&lt;/a> and HTML/CSS/Javascript, which queries data from S3 using &lt;a href="https://aws.amazon.com/athena/">Amazon Athena&lt;/a>. I was too lazy to figure out how to host the dashboard on AWS for free, so I opted to use &lt;a href="https://www.heroku.com/">Heroku&lt;/a> (as I&amp;rsquo;ve done previously for my &lt;a href="https://github.com/justinmklam/recipe-converter">recipe converter web app&lt;/a>).&lt;/p>
&lt;p>With cloud connectivity and dashboard complete, the sourdough monitor was now ready to be used!&lt;/p>

&lt;h1 id="the-analysis" class="anchor">
 &lt;a href="#the-analysis">
 The Analysis
 &lt;/a>
&lt;/h1>
&lt;p>The main purpose of the monitor was to help with my timing of when to use the starter. Since the data was being collected anyway, I figured I might as well do some analysis to see if there was anything significant.&lt;/p>

&lt;h2 id="overview" class="anchor">
 &lt;a href="#overview">
 Overview
 &lt;/a>
&lt;/h2>
&lt;p>After using the monitor for a few weeks, I had enough data to play with. The two main things I was curious about were:&lt;/p>
&lt;ol>
&lt;li>Whether the peak height changes over time (i.e. through repeated feedings)&lt;/li>
&lt;li>If it still grew as much if the starter was kept in the fridge and hadn&amp;rsquo;t been fed for a few days&lt;/li>
&lt;/ol>
&lt;p>Doing a quick kernel density plot to get a feel for the data, we see some clustering but no obvious trends.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/kde-plot.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/kde-plot.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Kernel density plot to show the distribution and clustering of data. No clear correlations are present&amp;hellip;&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The graph below shows the consistency of my feedings:&lt;/p>
&lt;ul>
&lt;li>5 subsequent feedings, starting on Jan 21&lt;/li>
&lt;li>3 subsequent feedings on Jan 30, Feb 3, and Feb 17&lt;/li>
&lt;li>2 subsequent feedings on Feb 12&lt;/li>
&lt;/ul>
&lt;p>&lt;em>Note: The horizontal axis is a bit off since some days had multiple feedings, the graph assumes each feeding is a separate day.&lt;/em>&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/max-rise-over-time.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/max-rise-over-time.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Feeding schedule over the past few weeks.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The graph below shows no correlation between how long it took for the peak rise to occur. It also didn&amp;rsquo;t seem to matter if the starter was kept in the fridge for a few days, which is great news (and maybe not news to those who know their starter well)!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/regression.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/regression.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">No statistical significance between the most relevant metrics.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="in-detail" class="anchor">
 &lt;a href="#in-detail">
 In Detail
 &lt;/a>
&lt;/h2>
&lt;p>Looking at the time series data, we see the progression of the rise height from the first to last feeding. Some observations:&lt;/p>
&lt;ul>
&lt;li>Peak rise height increased from ~100% to ~200% by the 6th subsequent feeding&lt;/li>
&lt;li>Slope of rise height was constant, even with the first feeding out of the fridge&lt;/li>
&lt;li>Peak rise height is achieved by ~5 hours, and sustains at this height for ~3-5 hours&lt;/li>
&lt;/ul>
&lt;p>&lt;em>Note: I used a temperature controlled proofing box, which is why the temperature and humidity are constant throughout the majority of the fermentation. The temperature was set to 24°C for most days, but interestingly the recorded temperature was typically around 30°C. More testing is required to see which sensor is correct&amp;hellip;&lt;/em>&lt;/p>
&lt;!-- ![TEXT](combined.png) -->
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/feeding.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/feeding.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Rise height, temperature, and humidity over time.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/pairplot.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/pairplot.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Pair plot of the time series. Not much to show, except that temperature and humidity are correlated (as expected).&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The rest of the feeding data can be found in the combined figure below. There are a few interesting things that occurred, but further experimentation would be required to draw any conclusions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Jan 30&lt;/strong>: All feedings were relatively similar, even the first one after being in the fridge for 5 days&lt;/li>
&lt;li>&lt;strong>Feb 3-5&lt;/strong>: The second feeding had a steeper slope; I think I adjusted the feeding ratio for this one from 1:2:2 to 1:3:3, but I can&amp;rsquo;t quite remember (or perhaps my wife was the one who fed it this time?). Also interesting that the temperature for the third feeding was not as consistent. Perhaps the proofing box wasn&amp;rsquo;t turned on?&lt;/li>
&lt;li>&lt;strong>Feb 12&lt;/strong>: Second feeding peaked 100% more than the first feeding. I think the feeding ratio was changed. The first feeding&amp;rsquo;s humidity was also really weird in this one.&lt;/li>
&lt;li>&lt;strong>Feb 17-18&lt;/strong>: First feeding took longer to peak than the subsequent two, which contradicts my observations from Jan 30&amp;hellip;&lt;/li>
&lt;/ul>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/all-combined.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/all-combined.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Visualization of the rest of the feeding data, for those interested.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>So basically, what the data is telling me is that there are many other variables at play, and I&amp;rsquo;ll need to record a few other metrics in order for the measured data to be actionable, such as:&lt;/p>
&lt;ul>
&lt;li>Feed ratio&lt;/li>
&lt;li>Start and end temperature of the starter (using a probe thermometer)&lt;/li>
&lt;li>Optional: Water hardness&lt;/li>
&lt;/ul>
&lt;p>Other experiments that I&amp;rsquo;d like to nerd out on:&lt;/p>
&lt;ul>
&lt;li>Compare feed ratios and/or flour blends&lt;/li>
&lt;li>Compare growth performance with and without a temperature controlled environment (i.e. proofing box)&lt;/li>
&lt;li>Ideal location if you don&amp;rsquo;t have a proofing box (i.e. Oven with light on? Microwave with warm water?)&lt;/li>
&lt;/ul>

&lt;h1 id="conclusion" class="anchor">
 &lt;a href="#conclusion">
 Conclusion
 &lt;/a>
&lt;/h1>
&lt;p>So after all this, the takeaway might actually be that timing the starter isn&amp;rsquo;t all that important, since it stays active at its peak for at least a few hours. And if you maintain a somewhat regular feeding schedule and have a relatively stable environment, then you can probably get a good feel for how long it takes your starter to grow (or you can set up a timelapse with your smartphone camera to see how it&amp;rsquo;s doing).&lt;/p>
&lt;p>This was still a fun way to nerd out with baking, since engineers like metrics, and there&amp;rsquo;s a lot to measure with sourdough bread. Although this gadget might seem superfluous to some, I enjoyed the precision and confidence it gave me in taking out the guesswork of how my starter is doing. There&amp;rsquo;s two types of bakers in the world: those who go by feel, and those have a 0.01 g resolution kitchen scale. Guess which one I am 😏&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/02/levain-monitor/bread_hu_7a958edbc76b0376.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/02/levain-monitor/bread_hu_7a958edbc76b0376.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">A darn nice crumb, if I do say so myself!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Hope you found this enlightening, and if you have any other ideas on what experiments to try out, leave a comment below! If you like my content, please consider &lt;a href="https://www.buymeacoffee.com/justinmklam">buying me a coffee&lt;/a>!&lt;/p>
&lt;p>Happy baking!&lt;/p></content:encoded></item><item><title>Building a Backyard Chicken Coop in the City</title><link>https://justinmklam.com/posts/2021/06/chicken-coop/</link><pubDate>Wed, 09 Sep 2020 03:41:54 +0000</pubDate><guid>https://justinmklam.com/posts/2021/06/chicken-coop/</guid><description>&lt;p>Back in 2018, we decided to raise chickens in our urban backyard, all in the name of being able to have fresh eggs every morning. Our city had bylaws on having chickens, which included:&lt;/p>
&lt;ul>
&lt;li>A maximum of 4 hens (no roosters), 4 months or older&lt;/li>
&lt;li>Ducks, turkeys, or other livestock are not allowed&lt;/li>
&lt;li>Eggs, meat, and manure cannot be used for commercial purposes&lt;/li>
&lt;li>Backyard slaughtering is not allowed&lt;/li>
&lt;/ul>
&lt;p>Since we were fortunate enough to have access to a backyard at the time, we decided to build a coop and start the lifestyle of having easy access to fresh eggs. Was it cost effective? Heck no, but it sure was a fun journey!&lt;/p></description><content:encoded>&lt;p>Back in 2018, we decided to raise chickens in our urban backyard, all in the name of being able to have fresh eggs every morning. Our city had bylaws on having chickens, which included:&lt;/p>
&lt;ul>
&lt;li>A maximum of 4 hens (no roosters), 4 months or older&lt;/li>
&lt;li>Ducks, turkeys, or other livestock are not allowed&lt;/li>
&lt;li>Eggs, meat, and manure cannot be used for commercial purposes&lt;/li>
&lt;li>Backyard slaughtering is not allowed&lt;/li>
&lt;/ul>
&lt;p>Since we were fortunate enough to have access to a backyard at the time, we decided to build a coop and start the lifestyle of having easy access to fresh eggs. Was it cost effective? Heck no, but it sure was a fun journey!&lt;/p>

&lt;h1 id="the-development" class="anchor">
 &lt;a href="#the-development">
 The Development
 &lt;/a>
&lt;/h1>

&lt;h2 id="the-coop" class="anchor">
 &lt;a href="#the-coop">
 The Coop
 &lt;/a>
&lt;/h2>
&lt;p>After doing some rough sketching, we settled on dimensions of our coop. We put the frame together to get a feel for how big this thing would actually end up being!&lt;/p>
&lt;p>Most coops are made out of an outdoor wood like cedar, but that would have blown our budget for something that we may not keep forever. The other thing is that in general, my engineering style is to see how inexpensive and minimal I can build something, rather than overbuild with an unlimited budget.&lt;/p>
&lt;blockquote>
&lt;p>Anyone can build a bridge that won&amp;rsquo;t fail, but it takes an engineer to build one that barely stands.&lt;/p>&lt;/blockquote>
&lt;p>As such, we built out the frame with the thinnest (and cheapest) construction wood at Home Depot (something like 1x2). With just the perimeter built, it was honestly quite wobbly&amp;hellip; but once the trusses were added, rigidity was achieved.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/build1_hu_26850c140bffd030.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/build1_hu_26850c140bffd030.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Rough frame of the coop (top) and part of the run (bottom)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/build2_hu_e516f1a03b8640ee.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/build2_hu_e516f1a03b8640ee.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Building out the frame, adding the angled roof and the nesting box.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The doors were a bit of a pain, since the opening wasn&amp;rsquo;t perfectly square. But after an adequate amount of measuring and shimming, we had the doors installed, operational, and smooth.&lt;/p>
&lt;p>For the paint, we went to the clearance section at Home Depot and found heavily reduced outdoor paint which suited our needs. This strategy isn&amp;rsquo;t always fruitful, but luckily for us, we found a decent colour as well as not actually needing that much paint.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src="coopdoor.mp4" type="video/mp4" />
 &lt;/video>
 
 
 
 
 &lt;p class="caption">Sturdy enough for me, sturdy enough for chickens.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/build4_hu_f0ecfba63a4dff94.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/build4_hu_f0ecfba63a4dff94.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Dry fitting the coop and building out the run.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/build5_hu_10df7e10878ea077.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/build5_hu_10df7e10878ea077.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">How the chickens will enter and leave the coop.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/coop_hu_c51392663d461a1a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/coop_hu_c51392663d461a1a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Fully assembled and ready for our new guests!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>At this point, we were finally ready to go get ourselves some chicken(s)!&lt;/p>

&lt;h2 id="the-auction" class="anchor">
 &lt;a href="#the-auction">
 The Auction
 &lt;/a>
&lt;/h2>
&lt;p>Turns out buying chickens is easier said than done. We eventually found a farm in the next city over that hosts farm animal auctions, which sounded like a great idea at the time. Upon arrival, potential buyers browse the inventory and make notes about what they would like to purchase. What we didn&amp;rsquo;t realize was that sellers were there to sell in bulk&amp;hellip; While we wanted to buy a couple hens of different breeds, sellers were putting chickens up for sale in multiples of five!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/auction1_hu_65d09b6cbfbf25e2.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/auction1_hu_65d09b6cbfbf25e2.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Browsing the inventory before the auction starts.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Having never been to an auction before, it was actually a bit stressful. The auctioneer speaks extremely quickly, and you (as the buyer) need to make quick decisions on how much you want to bid, and if someone else also bids, if you&amp;rsquo;re willing to outbid them (and how many times you want to repeat this cycle). The auctioneer also scolded a few people for waving their paddles by accident, since the slightest of movement is an indicator that the person holding the paddle wants to bid.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/auction2_hu_72b251988410c849.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/auction2_hu_72b251988410c849.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Bidding for our hens.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="the-chickens" class="anchor">
 &lt;a href="#the-chickens">
 The Chickens
 &lt;/a>
&lt;/h2>
&lt;p>Within the span of 24 hours, our chicken ownership went something like this:&lt;/p>
&lt;ol>
&lt;li>Bid on two nice, small chickens&lt;/li>
&lt;li>Find out that they&amp;rsquo;re sold as a pair because one of them is a rooster&lt;/li>
&lt;li>&lt;em>Panic&lt;/em> because we can&amp;rsquo;t bring back a rooster, so put them back up for auction&lt;/li>
&lt;li>Sell the chickens at a slight loss, end up buying 4 chickens&lt;/li>
&lt;li>Try to confirm with one of the farmers that all of them are hens, is told that they&amp;rsquo;re too young to tell the gender&lt;/li>
&lt;li>Take all the chickens home, find out the next morning at 6 am that one of them is a rooster&lt;/li>
&lt;li>&lt;em>Panic again&lt;/em>, find a local farm to trade our rooster (plus another hen since our coop was built for max 3, not 4)&lt;/li>
&lt;/ol>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/chickens5_hu_eb6e69c2fa284629.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/chickens5_hu_eb6e69c2fa284629.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Hens in their new home (some of them anyway&amp;hellip;).&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/rooster_hu_e840054e1cffa6f6.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/rooster_hu_e840054e1cffa6f6.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Turns out catching a chicken is easier said than done, but eventually we got him!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>With the chicken fiasco over, we had ourselves a stable flock.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/chickens1_hu_fadbf72ec0cc4ea5.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/chickens1_hu_fadbf72ec0cc4ea5.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Three happy hens roosting in their new home.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/chickens2_hu_33cd1b8e232d1867.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/chickens2_hu_33cd1b8e232d1867.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">From left to right: Tamago (Bantam hen), Peidan (Black Copper Maran), and Pecorino (Ameraucana).&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/feeding_hu_efb1ce43fb31cc21.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/feeding_hu_efb1ce43fb31cc21.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">This was mildly painful at first since they&amp;rsquo;d nip our flesh when going for the seeds.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>On the first few nights, a bit of training was required to tell the chickens to go inside the coop at dusk. To do this, we shined a flashlight from the inside (such that it&amp;rsquo;s visible from the outside), which eventually attracted their attention and they flew up into the opening.&lt;/p>
&lt;p>After a while, they no longer needed any coaxing and like clockwork, they&amp;rsquo;d return to the coop right right when the sun set for the day.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src="chicken-night.mp4" type="video/mp4" />
 &lt;/video>
 
 
 
 
 &lt;p class="caption">Hens entering the coop at sunset&amp;hellip;&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src="chicken-morning.mp4" type="video/mp4" />
 &lt;/video>
 
 
 
 
 &lt;p class="caption">&amp;hellip; And exiting at sunrise&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>That winter, we had snowfall that stuck (which is rare for the &amp;ldquo;wet&amp;rdquo; coast of Vancouver), but the chickens were fortunately ok. We had a heat lamp ready but they didn&amp;rsquo;t seem to be cold, and we kept the water warm to keep it from freezing overnight.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/winter1_hu_a8df8025c0e592a8.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/winter1_hu_a8df8025c0e592a8.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">When winter rolled around, we had to keep an eye on their water to make sure it didn&amp;rsquo;t freeze.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/winter2_hu_2e0fcfad922857.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/winter2_hu_2e0fcfad922857.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">At least they were still somewhat free run!&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="the-eggs" class="anchor">
 &lt;a href="#the-eggs">
 The Eggs
 &lt;/a>
&lt;/h2>
&lt;p>We had to &amp;ldquo;let the taps run for a bit&amp;rdquo; before our egg production hit full tilt, so to speak. The shells from the first few eggs were too soft (due to a lack of calcium?), but after a week or so, we finally started to get some proper eggs.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/eggerror1_hu_67adc54ec6a35811.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/eggerror1_hu_67adc54ec6a35811.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Soft shell crab is a delicacy, but soft shell eggs are not.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/eggerror2_hu_8c6ba019e0652240.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/eggerror2_hu_8c6ba019e0652240.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">This one almost made it, but the shell was still too thin. More calcium required!&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/eggs1_hu_69b0214a691ce224.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/eggs1_hu_69b0214a691ce224.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Waking up to this pair of eggs was egg-tremely egg-citing (sorry, couldn&amp;rsquo;t resist).&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/eggs3_hu_1622e988fa163a7b.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/eggs3_hu_1622e988fa163a7b.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Production eventually entered steady state.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/tamago-gakegohan_hu_e8cdff5f5d399ffa.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/tamago-gakegohan_hu_e8cdff5f5d399ffa.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Try doing this with store bought eggs!&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="the-farewell" class="anchor">
 &lt;a href="#the-farewell">
 The Farewell
 &lt;/a>
&lt;/h1>
&lt;p>Two fruitful years later, life events resulted in having to say goodbye to our beloved hens. We initially put an ad up on Facebook Marketplace, but that quickly got taken down since apparently they don&amp;rsquo;t allow livestock to be sold on their platform. Back to Craigslist we went, and eventually, we found a buyer. When they arrived, they came prepared to take it all away, albeit with equipment that was a little non-standard&amp;hellip;&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/goodbye1_hu_bcd10bd4913b4d81.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/goodbye1_hu_bcd10bd4913b4d81.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Half of the coop packed up into the buyer&amp;rsquo;s SUV&amp;hellip;&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/goodbye2_hu_f9346e445c214d11.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/goodbye2_hu_f9346e445c214d11.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">&amp;hellip; And the other half in another SUV.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>All that was left was a memory of the chickens on our &lt;a href="https://organicclimbing.com/collections/crash-pads">crashpad&lt;/a>, thanks to our good friends who designed and gifted it to us for &lt;a href="https://www.justinmklam.com/posts/2019/diy-wedding-ring/">our wedding&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2021/06/chicken-coop/crashpad_hu_3d68dbf9430b2f9f.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2021/06/chicken-coop/crashpad_hu_3d68dbf9430b2f9f.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Memories of our original hens, now forever imprinted on our bouldering crashpads.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>Nordic nRF52 Development with Visual Studio Code</title><link>https://justinmklam.com/posts/2019/04/vscode-nrf52/</link><pubDate>Thu, 25 Apr 2019 14:14:25 -0700</pubDate><guid>https://justinmklam.com/posts/2019/04/vscode-nrf52/</guid><description>&lt;p>A few years ago, I created a &lt;a href="https://www.justinmklam.com/posts/2017/10/vscode-debugger-setup/">tutorial&lt;/a> on setting up Visual Studio Code for development with the STM32. Since I&amp;rsquo;ve also been developing on the Nordic nRF52, I thought I&amp;rsquo;d share another tutorial to show how a project can be set up, flashed, and debugged using Visual Studio Code.&lt;/p></description><content:encoded>&lt;p>A few years ago, I created a &lt;a href="https://www.justinmklam.com/posts/2017/10/vscode-debugger-setup/">tutorial&lt;/a> on setting up Visual Studio Code for development with the STM32. Since I&amp;rsquo;ve also been developing on the Nordic nRF52, I thought I&amp;rsquo;d share another tutorial to show how a project can be set up, flashed, and debugged using Visual Studio Code.&lt;/p>
&lt;p>The template project discussed in this post can be found on &lt;a href="https://github.com/justinmklam/nrf52-blinky-demo">Github&lt;/a>.&lt;/p>

&lt;h1 id="instructions" class="anchor">
 &lt;a href="#instructions">
 Instructions
 &lt;/a>
&lt;/h1>
&lt;p>The Nordic toolchain is cross-platform, but the instructions below are specifically for Linux. However, they can easily be replicated in Windows as long as installation paths and environment variables are set correctly.&lt;/p>

&lt;h2 id="general-comments" class="anchor">
 &lt;a href="#general-comments">
 General Comments
 &lt;/a>
&lt;/h2>
&lt;p>When using any editor + terminal for nRF52 development, the things to remember are:&lt;/p>
&lt;ul>
&lt;li>GCC path is set in &lt;code>&amp;lt;sdk&amp;gt;/components/toolchain/gcc/Makefile.posix&lt;/code>&lt;/li>
&lt;li>Makefile is up to date with:
&lt;ul>
&lt;li>&lt;code>SDK_ROOT&lt;/code> is pointed to where &lt;code>&amp;lt;sdk&amp;gt;&lt;/code> is located&lt;/li>
&lt;li>Source and header files for new components&lt;/li>
&lt;li>Board/component configurations in &lt;code>sdk_config.h&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>With Visual Studio Code:&lt;/p>
&lt;ul>
&lt;li>In &lt;code>.vscode/c_cpp_properties.json&lt;/code>, update &lt;code>defines&lt;/code>, &lt;code>includePath&lt;/code>, and &lt;code>compilerPath&lt;/code> as required&lt;/li>
&lt;li>In &lt;code>.vscode/launch.json&lt;/code>, update &lt;code>executable&lt;/code> and &lt;code>armToolchainPath&lt;/code> as required&lt;/li>
&lt;li>In &lt;code>.vscode/tasks.json&lt;/code>, update the current working directory &lt;code>cwd&lt;/code> and &lt;code>command&lt;/code> as required (ie. changing to &lt;code>make flash -j8&lt;/code> to use 8 cores to build).&lt;/li>
&lt;/ul>

&lt;h2 id="installation" class="anchor">
 &lt;a href="#installation">
 Installation
 &lt;/a>
&lt;/h2>

&lt;h3 id="system-tools" class="anchor">
 &lt;a href="#system-tools">
 System Tools
 &lt;/a>
&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo apt install build-essential
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Required by java-based CMSIS Configuration Wizard&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt install default-jre
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h3 id="code-editor" class="anchor">
 &lt;a href="#code-editor">
 Code Editor
 &lt;/a>
&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://code.visualstudio.com/download">Visual Studio Code&lt;/a>
&lt;ul>
&lt;li>&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools">C/C++ Extension&lt;/a> by Microsoft&lt;/li>
&lt;li>&lt;a href="https://marketplace.visualstudio.com/items?itemName=marus25.cortex-debug">Cortex-Debug Extension&lt;/a> by marus25&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>

&lt;h3 id="nrf52-toolchain" class="anchor">
 &lt;a href="#nrf52-toolchain">
 nRF52 Toolchain
 &lt;/a>
&lt;/h3>

&lt;h4 id="download" class="anchor">
 &lt;a href="#download">
 Download
 &lt;/a>
&lt;/h4>
&lt;ol>
&lt;li>&lt;a href="https://www.nordicsemi.com/Software-and-Tools/Software/nRF5-SDK">nRF52 SDK&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF5-Command-Line-Tools">nRF52 Command Line Tools&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.segger.com/downloads/jlink">Segger J-Link Software Tools&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads">GNU-RM Embedded Toolchain for ARM&lt;/a>
&lt;ul>
&lt;li>It&amp;rsquo;s recommended to install the GCC version that matches the Nordic SDK version. Check the GCC version in &lt;code>&amp;lt;sdk&amp;gt;/components/toolchain/gcc/Makefile.posix&lt;/code> and download the appropriate version.&lt;/li>
&lt;li>For nRF5 SDK 15.3.0, the gcc version is &lt;code>gcc-arm-none-eabi-7-2018-q2-update&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>Optional:&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://www.segger.com/downloads/jlink/#Ozone">Segger Ozone Debugger&lt;/a>&lt;/li>
&lt;/ol>

&lt;h4 id="setup" class="anchor">
 &lt;a href="#setup">
 Setup
 &lt;/a>
&lt;/h4>
&lt;p>Run the commands below to extract the archives to the respective paths.&lt;/p>
&lt;ul>
&lt;li>nRF5_SDK to &lt;code>$HOME&lt;/code>&lt;/li>
&lt;li>nRF Command Line Tools to &lt;code>/opt/&lt;/code> and &lt;code>/usr/local/bin&lt;/code>&lt;/li>
&lt;li>gcc-arm-none-eabi to &lt;code>/usr/local/bin&lt;/code>&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Unpack SDK to home directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">unzip nRF5_SDK_15.3.0_59ac345.zip -d &lt;span class="nv">$HOME&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Unpack nRF command line tools and make accessible in terminal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tar -xvf nRF-Command-Line-Tools_9_8_1_Linux-x86_64.tar --directory /opt/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo ln -s /opt/nrfjprog/nrfjprog /usr/local/bin/nrfjprog
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install Segger&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt install ./JLink_Linux_V644f_x86_64.deb
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Unpack gcc toolchain to /usr/local/bin&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo tar -xjvf gcc-arm-none-eabi-8-2018-q4-major-linux.tar.bz2 --directory /usr/local/bin
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If optional tools are downloaded:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Segger Ozone&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt install ./Ozone_Linux_V262_x86_64.deb
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Check &lt;code>nrfjproj --version&lt;/code> that it&amp;rsquo;s been installed correctly.&lt;/p>

&lt;h3 id="nordic-sdk-setup" class="anchor">
 &lt;a href="#nordic-sdk-setup">
 Nordic SDK Setup
 &lt;/a>
&lt;/h3>
&lt;p>In the nRF52 SDK folder, update the values in &lt;code>components/toolchain/gcc/Makefile.posix&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>GNU_INSTALL_ROOT&lt;/code>&lt;/li>
&lt;li>&lt;code>GNU_VERSION&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>This is only required if using a different gcc version than specified. It&amp;rsquo;s recommended to use the same one as the SDK.&lt;/p>

&lt;h2 id="using-the-project" class="anchor">
 &lt;a href="#using-the-project">
 Using the Project
 &lt;/a>
&lt;/h2>

&lt;h3 id="setup-1" class="anchor">
 &lt;a href="#setup-1">
 Setup
 &lt;/a>
&lt;/h3>
&lt;p>In &lt;code>blinky/&lt;/code> directory, do a global search and replace to update the SDK root to wherever your nRF5_SDK directory is:&lt;/p>
&lt;ul>
&lt;li>From: &lt;code>SDK_ROOT := ../../../../../..&lt;/code>&lt;/li>
&lt;li>To: &lt;code>SDK_ROOT := $(HOME)/nRF5_SDK_15.3.0_59ac345&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>If required, update the following in &lt;code>.vscode/c_cpp_properties.json&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>Include paths&lt;/li>
&lt;li>Defines based on C/assembler flags in the Makefile&lt;/li>
&lt;/ul>

&lt;h3 id="configuring-sdk_configh" class="anchor">
 &lt;a href="#configuring-sdk_configh">
 Configuring &lt;code>sdk_config.h&lt;/code>
 &lt;/a>
&lt;/h3>
&lt;p>&lt;a href="https://sourceforge.net/projects/cmsisconfig/">CMSIS Configuration Wizard&lt;/a> is integrated with example makefiles. In order to open sdk_config.h in this tool, type:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">make sdk_config
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h3 id="build-and-flash" class="anchor">
 &lt;a href="#build-and-flash">
 Build and Flash
 &lt;/a>
&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> blinky/pca10056/mbr/armgcc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># To just build. Optional `-jN` flag, where N is number of cores to use&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">make
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># To build and flash&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">make flash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># If a SoftDevice is included in your project&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">make flash_softdevice
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Build tasks can also be added to &lt;code>.vscode/tasks.json&lt;/code>. Pressing &lt;code>CTRL+SHIFT+B&lt;/code> will execute the default build task (in this case, the default build task is &lt;code>make flash&lt;/code> for &lt;code>PCA10056&lt;/code>).&lt;/p>

&lt;h3 id="segger-rtt-log" class="anchor">
 &lt;a href="#segger-rtt-log">
 Segger RTT Log
 &lt;/a>
&lt;/h3>
&lt;p>To use the RTT Viewer equivalent on GNU/Linux, start by opening a terminal and starting &lt;code>JLinkExe&lt;/code>.&lt;/p>
&lt;p>Follow the steps below to connect the device (in this case, &lt;code>NRF52840_XXAA&lt;/code>). Press ENTER to accept the default value. The only option that needs to be changed (aside from board, if necessary) is the target interface (use &lt;code>SWD&lt;/code> instead of &lt;code>JTAG&lt;/code>).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">J-Link&amp;gt;connect
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Device&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Please specify target interface:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> J&lt;span class="o">)&lt;/span> JTAG &lt;span class="o">(&lt;/span>Default&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> S&lt;span class="o">)&lt;/span> SWD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> T&lt;span class="o">)&lt;/span> cJTAG
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">TIF&amp;gt;S
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Specify target interface speed &lt;span class="o">[&lt;/span>kHz&lt;span class="o">]&lt;/span>. &amp;lt;Default&amp;gt;: &lt;span class="m">4000&lt;/span> kHz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Speed&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Device &lt;span class="s2">&amp;#34;NRF52840_XXAA&amp;#34;&lt;/span> selected.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Alternatively, you can specify the configurations in the command line:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">JLinkExe -device NRF52832_XXAA -if SWD -speed &lt;span class="m">4000&lt;/span> -autoconnect &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In another terminal, start &lt;code>JLinkRTTClient&lt;/code>. RTT output should now start displaying. Output should be as follows:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: ************************************************************&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * SEGGER Microcontroller GmbH *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * Solutions for real time microcontroller applications *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: ************************************************************&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * (c) 2012 - 2016 SEGGER Microcontroller GmbH *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * www.segger.com Support: support@segger.com *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: ************************************************************&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * SEGGER J-Link RTT Client Compiled Apr 12 2019 17:30:19 *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: * *&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: ************************************************************&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: -----------------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###RTT Client: Connecting to J-Link RTT Server via localhost:19021 Connected.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SEGGER J-Link V6.44f - Real &lt;span class="nb">time&lt;/span> terminal output
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">J-Link OB-SAM3U128-V2-NordicSemi compiled Jan &lt;span class="m">7&lt;/span> &lt;span class="m">2019&lt;/span> 14:07:15 V1.0, &lt;span class="nv">SN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">683903307&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Process: JLinkExe
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;info&amp;gt; app: SPI example started.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;info&amp;gt; app: Transfer completed.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;info&amp;gt; app: Transfer completed.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;info&amp;gt; app: Transfer completed.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h3 id="debugging" class="anchor">
 &lt;a href="#debugging">
 Debugging
 &lt;/a>
&lt;/h3>

&lt;h4 id="visual-studio-code" class="anchor">
 &lt;a href="#visual-studio-code">
 Visual Studio Code
 &lt;/a>
&lt;/h4>
&lt;ol>
&lt;li>Open the debug pane (&lt;code>CTRL+SHIFT+D&lt;/code>) and select &lt;strong>Cortex-Debug&lt;/strong>.&lt;/li>
&lt;li>To create a new configuration, select &lt;strong>Add Configuration&lt;/strong> and choose &lt;strong>Cortex-Debug&lt;/strong>.&lt;/li>
&lt;li>If required, update the &lt;code>executable&lt;/code> and/or &lt;code>armToolchainPath&lt;/code> in &lt;code>.vscode/launch.json&lt;/code>.&lt;/li>
&lt;li>Hit &lt;code>F5&lt;/code> to start debugging.&lt;/li>
&lt;/ol>

&lt;h4 id="segger-o-zone" class="anchor">
 &lt;a href="#segger-o-zone">
 Segger O-zone
 &lt;/a>
&lt;/h4>
&lt;p>Using Segger Ozone provides rich insights on memory, assembly instructions, peripheral registers, etc.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2019/04/vscode-nrf52/segger-ozone.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2019/04/vscode-nrf52/segger-ozone.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Screencap of using Segger Ozone debugger.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>New project settings:&lt;/p>
&lt;ol>
&lt;li>Select &lt;strong>Create new project&lt;/strong>&lt;/li>
&lt;li>Choose target device
&lt;ul>
&lt;li>Select device: &lt;code>nRF52840_xxAA&lt;/code> (or other)&lt;/li>
&lt;li>Peripherals: (blank)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Connection settings
&lt;ul>
&lt;li>Target interface: SWD&lt;/li>
&lt;li>Target interface speed: 1 MHz&lt;/li>
&lt;li>Host interface: USB&lt;/li>
&lt;li>Serial no: (blank)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Program file
&lt;ul>
&lt;li>Select &lt;code>pca10056/mbr/armgcc/_build/nrf52840_xxaa.out&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>

&lt;h1 id="resources" class="anchor">
 &lt;a href="#resources">
 Resources
 &lt;/a>
&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://infocenter.nordicsemi.com/index.jsp">Nordic Information Center&lt;/a> - Official Documentation&lt;/li>
&lt;/ul>
&lt;p>Happy developing!&lt;/p></content:encoded></item><item><title>Monitoring the Fermentation of Sourdough Starter with Computer Vision</title><link>https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/</link><pubDate>Sun, 24 Jun 2018 17:50:22 -0700</pubDate><guid>https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/</guid><description>&lt;blockquote>
&lt;p>Featured on &lt;a href="https://justinmklam.com/files/MagPi72.pdf#page=12">MagPi Issue 72&lt;/a>, &lt;a href="https://hackaday.com/2018/06/27/raspberry-pi-tracks-starter-fermentation-for-optimized-sourdough/">Hackaday&lt;/a>, &lt;a href="https://news.ycombinator.com/item?id=17390066">Hacker News&lt;/a>, &lt;a href="https://blog.adafruit.com/2018/07/06/using-computer-vision-to-ferment-the-perfect-sourdough-yeast-piday-raspberrypi-raspberry_pi/">Adafruit&lt;/a>, &lt;a href="https://www.hackster.io/news/using-computer-vision-to-ferment-the-perfect-sourdough-yeast-70ac6d226acb">Hackster.io&lt;/a> and &lt;a href="https://www.theguardian.com/food/2019/mar/13/sourdough-is-silicon-valleys-latest-craze-could-i-beat-the-coders-and-bake-the-perfect-loaf">The Guardian&lt;/a>!&lt;/p>&lt;/blockquote>
&lt;p>Bread, the quintessence of life. People have survived for centuries off this staple consisting only of flour, water, salt, and yeast. Try consuming all these ingredients separately, and you&amp;rsquo;ll be in for a digestive surprise. However, mix them together and let time do its thing, and the result is the release of profound flavour, texture, and nutrients that were previously locked away.&lt;/p></description><content:encoded>&lt;blockquote>
&lt;p>Featured on &lt;a href="https://justinmklam.com/files/MagPi72.pdf#page=12">MagPi Issue 72&lt;/a>, &lt;a href="https://hackaday.com/2018/06/27/raspberry-pi-tracks-starter-fermentation-for-optimized-sourdough/">Hackaday&lt;/a>, &lt;a href="https://news.ycombinator.com/item?id=17390066">Hacker News&lt;/a>, &lt;a href="https://blog.adafruit.com/2018/07/06/using-computer-vision-to-ferment-the-perfect-sourdough-yeast-piday-raspberrypi-raspberry_pi/">Adafruit&lt;/a>, &lt;a href="https://www.hackster.io/news/using-computer-vision-to-ferment-the-perfect-sourdough-yeast-70ac6d226acb">Hackster.io&lt;/a> and &lt;a href="https://www.theguardian.com/food/2019/mar/13/sourdough-is-silicon-valleys-latest-craze-could-i-beat-the-coders-and-bake-the-perfect-loaf">The Guardian&lt;/a>!&lt;/p>&lt;/blockquote>
&lt;p>Bread, the quintessence of life. People have survived for centuries off this staple consisting only of flour, water, salt, and yeast. Try consuming all these ingredients separately, and you&amp;rsquo;ll be in for a digestive surprise. However, mix them together and let time do its thing, and the result is the release of profound flavour, texture, and nutrients that were previously locked away.&lt;/p>
&lt;p>Despite it being relatively easy to turn dough into something that looks and feels like bread, the challenge is in squeezing every possible ounce of flavour and texture (using only those four ingredients) to achieve the embodiment of a true loaf of bread. The secret to artisinal bread is &lt;strong>all in the fermentation.&lt;/strong> A baker&amp;rsquo;s skill is in their ability to manage and control the fermentation process, which is usually achieved through countless months and/or years of trial and error.&lt;/p>
&lt;p>&lt;strong>But what if there was a better way to understand what happens during the fermentation process?&lt;/strong>&lt;/p>
&lt;p>In this blog post, we dive into the world of wild yeast (commonly known as sourdough starter) by tracking its growth through timelapses, automated image analysis, and cool graph animations. Read on to find out more!&lt;/p>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-content" autoplay="autoplay" loop="loop" controls>
 &lt;source src=timelapse.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">Timelapse taken over ~10 hours at 5 minute intervals. Shown: Two sourdough starters with different feeding ratios.&lt;/p>
&lt;/div>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src=2018-05-29%20Levain%20Timelapse.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">Image analysis for tracking growth of the above timelapse.&lt;/p>
&lt;/div>


&lt;h1 id="the-backstory" class="anchor">
 &lt;a href="#the-backstory">
 The Backstory
 &lt;/a>
&lt;/h1>
&lt;p>Two key components in making artisinal bread are &lt;strong>time&lt;/strong> and &lt;strong>fermentation&lt;/strong>. If you can afford a long, slow rise, you will be rewarded with a texture like no other, and flavours that are both complex and subtle. Most bakeries unfortunately do not have this luxury (since it&amp;rsquo;s a business after all), so commercial instant dry yeast is used to expedite the fermentation such that it reaches the desired loaf volume at a reasonable schedule. With bread risen with instant dry yeast, it will definitely resemble visual qualities of bread, but the texture and taste will not be comparable to bread that uses wild yeast.&lt;/p>
&lt;p>The quest for achieving the perfect loaf is an arduous one. It can take upwards of 12 hours from the first mix to actually being able to bake the loaf, and environmental factors (ie. temperature and humidity) can alter the dough&amp;rsquo;s behaviour (ie. in how it absorbs water, or how fast/slow the fermentation occurs). With every loaf being slightly different, it can be challenging to identify what processes need adjusting to get one step closer to that perfect loaf.&lt;/p>
&lt;p>For context, a typical process for making an artisinal loaf is roughly described below:&lt;/p>
&lt;ol>
&lt;li>Discard 80-90% of the sourdough starter and feed it with flour and water.&lt;/li>
&lt;li>Wait a few hours for the starter to ferment and double or triple in size.&lt;/li>
&lt;li>At its peak, take the starter and mix it in with the rest of the bread ingredients.&lt;/li>
&lt;li>Gently fold the dough to tighten it up (which creates the gluten) a few times for the first 1-2 hours.&lt;/li>
&lt;li>Let rest. Flavours are developed during this bulk fermentation period.&lt;/li>
&lt;li>Shape the loaf and place it in a proofing basket.&lt;/li>
&lt;li>Let rest. The final volume is achieved during this second fermentation.&lt;/li>
&lt;li>Bake.&lt;/li>
&lt;li>Place on cooling rack when done, and listen to the song of the crackling crust while you wait to cut it open.&lt;/li>
&lt;/ol>
&lt;p>Sourdough starter is a living organism, and as such should be treated with kindness, respect, and most importantly, food. A well-fed starter is an active and happy one; if we can get a feel for how the starter behaves on a day-to-day basis during its feeding (ie. when the fermentation occurs), it may help clear up at least one of the variables in bread baking.&lt;/p>
&lt;p>One way to achieve this understanding is simply through trial and error. But we can do better. We can use (rather simple) &lt;strong>computer vision&lt;/strong>!&lt;/p>
&lt;p>The loaf&amp;rsquo;s holey texture (ie. the elusive open crumb) is largely determined by activeness of the starter, so this is where we shall put our initial efforts. &lt;strong>Specifically, we want to understand&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>When the starter reaches its maximum fermentation,&lt;/li>
&lt;li>How consistent the starter&amp;rsquo;s fermentation is, and&lt;/li>
&lt;li>What happens when the starter is neglected and how quickly it can come back to life.&lt;/li>
&lt;/ul>
&lt;p>To answer these questions, our plan of action is to:&lt;/p>
&lt;ol>
&lt;li>Take a timelapse of starter&lt;/li>
&lt;li>Write an image analysis script to locate the current height of the starter in each image&lt;/li>
&lt;li>Plot the height over time to get its growth characteristics&lt;/li>
&lt;/ol>
&lt;p>Enough with the walls of text. Let&amp;rsquo;s get on to the fun stuff!&lt;/p>

&lt;h1 id="the-development" class="anchor">
 &lt;a href="#the-development">
 The Development
 &lt;/a>
&lt;/h1>

&lt;h2 id="setting-up-the-timelapse" class="anchor">
 &lt;a href="#setting-up-the-timelapse">
 Setting Up the Timelapse
 &lt;/a>
&lt;/h2>
&lt;p>For ease and convenience, the timelapse was set up on a Raspberry Pi. I initially had the idea to eventually create a real-time analysis of the starter (ie. a Python script updating a locally hosted dashboard), but I figured post processing a timelapse was good enough for now to answer those questions. You can pretty much use any camera to take a timelapse.&lt;/p>

&lt;h3 id="loading-the-raspberry-pi-zero-headless" class="anchor">
 &lt;a href="#loading-the-raspberry-pi-zero-headless">
 Loading the Raspberry Pi Zero (Headless)
 &lt;/a>
&lt;/h3>
&lt;p>Setting up a Raspberry Pi is very easy these days:&lt;/p>
&lt;ol>
&lt;li>Download &lt;a href="https://www.raspberrypi.org/downloads/raspbian/">Raspbian&lt;/a>&lt;/li>
&lt;li>Flash it to an SD card with something like &lt;a href="https://etcher.io/">Etcher&lt;/a>&lt;/li>
&lt;/ol>
&lt;p>The only hiccup I ran into was setting up without any monitor or ethernet attached, so getting it connected to WiFi and retrieving its IP address was non-trivial. Luckily, other people had run into the same problem over at the &lt;a href="https://www.raspberrypi.org/forums/viewtopic.php?t=191252">Raspberry Pi forums&lt;/a>.
The solution:&lt;/p>
&lt;ol>
&lt;li>Create an empty file on the SD&amp;rsquo;s boot partition called &lt;code>ssh&lt;/code> to enable it.&lt;/li>
&lt;li>Create another file named &lt;code>wpa_supplicant.conf&lt;/code> with the following content:&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">country&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">US&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ctrl_interface&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">DIR&lt;/span>&lt;span class="o">=/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">wpa_supplicant&lt;/span> &lt;span class="n">GROUP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">netdev&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">update_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">network&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ssid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your_real_wifi_ssid&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">scan_ssid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">psk&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your_real_password&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">key_mgmt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">WPA&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">PSK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Connect the camera module and boot it up. You can use something like &lt;a href="https://angryip.org/download/#linux">Angry IP Scanner&lt;/a> to find its IP address, and with any luck you should be able to SSH into it from your own computer!&lt;/p>

&lt;h3 id="creating-the-scripts" class="anchor">
 &lt;a href="#creating-the-scripts">
 Creating the Scripts
 &lt;/a>
&lt;/h3>
&lt;p>Fortunately, the Pi comes loaded with &lt;code>raspistill&lt;/code>, a command line tool to capture images (see &lt;a href="https://www.raspberrypi.org/documentation/usage/camera/raspicam/raspistill.md">here&lt;/a> for documentation). All we need to do is to write a simple shell script to execute this command every N seconds to create a timelapse.&lt;/p>
&lt;p>&lt;strong>Note&lt;/strong>: The easiest option is to use the built in &lt;a href="https://www.raspberrypi.org/documentation/usage/camera/raspicam/timelapse.md">timelapse mode&lt;/a>. However, I decided to make my own script to have more control over the filenames and where the images get saved.&lt;/p>

&lt;h4 id="folder-structure" class="anchor">
 &lt;a href="#folder-structure">
 Folder Structure
 &lt;/a>
&lt;/h4>
&lt;p>I set up two scripts, one to take the timelapses, and another to start the timelapse as a background process. The &lt;code>run&lt;/code> script is in the home directory so I can execute the command right when I login through the command line.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"> home/pi/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── sourdough-monitor/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── imgs/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── timelapse.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── run_sourdough_monitor.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h4 id="scripts" class="anchor">
 &lt;a href="#scripts">
 Scripts
 &lt;/a>
&lt;/h4>
&lt;p>In &lt;code>timelapse.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1"># Calls &amp;#39;raspistill&amp;#39; every 300s (ie. 5 mins) and writes the images&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># to a new date/timestamped folder.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">TOTAL_DELAY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">300&lt;/span> &lt;span class="c1"># in seconds&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">CAM_DELAY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="c1"># need to have a nonzero delay for raspistill&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Must be 1.33 ratio&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">RES_W&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1440&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">RES_H&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1080&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Calculate the total delay time per cycle&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">SLEEP_DELAY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$((&lt;/span>&lt;span class="nv">$TOTAL_DELAY&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nv">$CAM_DELAY&lt;/span>&lt;span class="k">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">FOLDER_NAME&lt;/span>&lt;span class="o">=&lt;/span>imgs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir -p &lt;span class="nv">$FOLDER_NAME&lt;/span> &lt;span class="c1"># create image root folder if not exist&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">IDX&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span> &lt;span class="c1"># image index&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">function&lt;/span> cleanup&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Exiting.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">trap&lt;/span> cleanup INT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">while&lt;/span> true&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">DATE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>date +%Y-%m-%d_%H-%M-%S&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">FNAME&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">DATE&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">_(&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">IDX&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span> &lt;span class="c1"># image filename&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Create folder for current timelapse set&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> &lt;span class="nv">$IDX&lt;/span> -eq &lt;span class="m">0&lt;/span> &lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">FOLDER_NAME&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$FOLDER_NAME&lt;/span>/&lt;span class="nv">$DATE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> mkdir -p &lt;span class="nv">$FOLDER_NAME&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Created folder: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">FOLDER_NAME&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Take image&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> raspistill --nopreview -t &lt;span class="nv">$CAM_DELAY&lt;/span> -o ./&lt;span class="nv">$FOLDER_NAME&lt;/span>/&lt;span class="nv">$FNAME&lt;/span>.jpg -w &lt;span class="nv">$RES_W&lt;/span> -h &lt;span class="nv">$RES_H&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Captured: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">FNAME&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">IDX&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$((&lt;/span>IDX+1&lt;span class="k">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> sleep &lt;span class="nv">$SLEEP_DELAY&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And in &lt;code>run_sourdough_monitor.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1"># Convenience script to run the monitor from home directory and launch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># in background&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> sourdough-monitor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Start in background so ssh session can be closed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nohup ./timelapse.sh &lt;span class="p">&amp;amp;&lt;/span>&amp;gt; /dev/null &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Nothing fancy here, so let&amp;rsquo;s move along.&lt;/p>

&lt;h2 id="taking-the-timelapses" class="anchor">
 &lt;a href="#taking-the-timelapses">
 Taking the Timelapses
 &lt;/a>
&lt;/h2>
&lt;p>Sometimes having kludgy setups is the best way forward. Might not be as pretty, but when bread is on the menu then nothing else matters. All we&amp;rsquo;re here for is the data!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/IMG_20180527_173837_hu_b2d359962744b73f.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/IMG_20180527_173837_hu_b2d359962744b73f.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Sometimes random parts and a bit of tape are the best way forward.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/IMG_20180527_225557_hu_c82e2e0a4e729181.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/IMG_20180527_225557_hu_c82e2e0a4e729181.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The light was originally placed in front (as shown above), but a better method was to put the light behind the jars to maximize contrast and minimize glare.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>To start the timelapse, ssh into the Pi and execute the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ ./run_sourdough_monitor.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The next morning, you can ssh back in and kill the process with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ pkill timelapse
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Once you have it set up on the Pi, you can also control it with your phone and something like &lt;a href="https://play.google.com/store/apps/details?id=com.sonelli.juicessh&amp;amp;hl=en_CA">JuiceSSH&lt;/a> to feel super techy.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/Screenshot_JuiceSSH_20180623-124103.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/Screenshot_JuiceSSH_20180623-124103.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Starting the timelapse on Android, because why not?&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Now we have some data to analyze!&lt;/p>

&lt;h2 id="the-analysis" class="anchor">
 &lt;a href="#the-analysis">
 The Analysis
 &lt;/a>
&lt;/h2>
&lt;p>The computer vision part of this project was quite straightforward, thanks to scikit-learn. All we have to do is to:&lt;/p>
&lt;ol>
&lt;li>Apply a binary threshold to the image to get two distinct regions&lt;/li>
&lt;li>Find the location of the boundary line&lt;/li>
&lt;/ol>
&lt;p>Fortunately, these are very easy to do with this library! The two main required components:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="http://scikit-image.org/docs/dev/auto_examples/segmentation/plot_thresholding.html">skimage.filters&lt;/a> - For applying thresholds&lt;/li>
&lt;li>&lt;a href="http://scikit-image.org/docs/dev/auto_examples/segmentation/plot_regionprops.html">skimage.measure&lt;/a> - For measuring region properties&lt;/li>
&lt;/ul>
&lt;p>Everything discussed in this blog post is also available on my &lt;a href="https://github.com/justinmklam/sourdough-starter-monitor">Github&lt;/a>.&lt;/p>

&lt;h3 id="thresholding-the-image" class="anchor">
 &lt;a href="#thresholding-the-image">
 Thresholding the Image
 &lt;/a>
&lt;/h3>
&lt;p>There are many threshold algorithms to choose from, and fortunately scikit-learn comes with a handy function to try them all at once.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">matplotlib.pyplot&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">plt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">skimage.filters&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">try_all_threshold&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">img&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">io&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">imread&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;2018-05-31 Levain Timelapse/test.jpg&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">as_grey&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">img&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">img&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">650&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1100&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># crop image to zoom in to jar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">try_all_threshold&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">img&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">figsize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">verbose&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">show&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Running the above code yields the following image:&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/Threshold_Comparison_1.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/Threshold_Comparison_1.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The try_all_threshold() function is fast and convenient to see which will likely be the best for an image.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Discussing the different thresholding algorithms is beyond the scope of this post, but what we&amp;rsquo;re looking for is one that is able to separate the boundary between the clear glass jar and the opaque sourdough starter. From the image above, it looks like isodata, otsu, and yen provide the sharpest thresholded boundary.&lt;/p>
&lt;p>Given a hot tip from my coworker (thanks Andreas) that Otsu&amp;rsquo;s method is a good one to pick, we can dig deeper into it. From the docs:&lt;/p>
&lt;blockquote>
&lt;p>Otsu’s method calculates an “optimal” threshold (marked by a red line in the histogram below) by maximizing the variance between two classes of pixels, which are separated by the threshold. Equivalently, this threshold minimizes the intra-class variance. - &lt;a href="http://scikit-image.org/docs/dev/auto_examples/segmentation/plot_thresholding.html">SciKit Image Docs&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>The main takeaway with Otsu&amp;rsquo;s method is that it works best with a bimodal distribution. For our image, this means the histogram should be represented by two distinct peaks. Since our cropped image looks to have two visually different areas (ie. transparent glass at the top, flour/water mixture at the bottom), let&amp;rsquo;s confirm that this method will work.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hist&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">img&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ravel&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">bins&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">256&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">1.0&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">fc&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;k&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ec&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;k&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Histogram of Original Image&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">show&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Plotting the histogram yields the following result:&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/histogram.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/histogram.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The image is a bimodal histogram, so Otsu&amp;rsquo;s method should work well. (Please forgive my unlabelled axes.)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Great, the histogram shows just what we need! (Except for the high concentration of saturated pixels, but let&amp;rsquo;s just ignore that for now&amp;hellip;)&lt;/p>
&lt;p>Taking a closer look at the thresholded image:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">skimage.filters&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">threshold_otsu&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">thresh&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">threshold_otsu&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">img&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">nbins&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">binary_img&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">img&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">thresh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">axes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ncols&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">axes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ravel&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">imshow&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">img&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cmap&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">gray&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Original image&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">imshow&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">binary_img&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cmap&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">gray&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Result&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">show&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/Threshold%20Comparison.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/Threshold%20Comparison.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The result of Otsu&amp;rsquo;s thresholding method.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Those white blobs on the walls of the jar may still be detected as regions of interest, but applying a minimum thresholded area will help with false positives. Luckily, we have moderate control of the lighting and contrast of the object in question. If we&amp;rsquo;re lucky we won&amp;rsquo;t have to change much for the timelapses taken at different times of the day!&lt;/p>

&lt;h3 id="quantifying-the-image" class="anchor">
 &lt;a href="#quantifying-the-image">
 Quantifying the Image
 &lt;/a>
&lt;/h3>
&lt;p>With our binary image, we can now use skimage.measure to easily get quantified properties of the regions. Full list of properties can be found &lt;a href="http://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.regionprops">in the docs&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">height&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">fig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ax&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subplots&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">imshow&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">binary_img&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cmap&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">gray&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">label_img&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">label&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">binary_img&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">regions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">regionprops&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">label_img&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">props&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">regions&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">y0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">x0&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">props&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">centroid&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">minr&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">minc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">maxr&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">maxc&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">props&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bbox&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">bx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">minc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">maxc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">maxc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">minc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">minc&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">by&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">minr&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">minr&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">maxr&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">maxr&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">minr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">area&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">maxc&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">minc&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">maxr&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">minr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">area&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="n">min_area&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Plot the bounding box&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">bx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">by&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;-b&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">linewidth&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">2.5&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Plot the centroid&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ax&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">plot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;ro&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">height&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">minr&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">plt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">show&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The resultant image below shows how multiple areas are detected and classified. However, we can apply some RI(TM) (aka. &lt;em>real&lt;/em> intelligence) to only pick the region of the sourdough starter. There are many ways to do this, but the simplest was to simply set a minimum area requirement. As long as it&amp;rsquo;s high enough, it should ignore all the other regions.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/min-area.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/min-area.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Minimum area is required since other areas will register as a detected region.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Due to the camera perspective and curvature of the jar, the boundary between the two regions is not actually straight. This was solved by taking a much narrower cropped region, which you will see below in the animated timelapses.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/first-thresh.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/first-thresh.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">This may have been a bit easier with a square container&amp;hellip;&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Now that we have a working understanding of the components we need, let&amp;rsquo;s get scripting and collect a bunch of data! You can check out the full analysis on &lt;a href="https://github.com/justinmklam/sourdough-starter-monitor">Github&lt;/a>.&lt;/p>

&lt;h2 id="the-results" class="anchor">
 &lt;a href="#the-results">
 The Results
 &lt;/a>
&lt;/h2>
&lt;p>The timelapses below show the sourdough starter from different dates. The boundary tracking algorithm is reasonably well at detecting the correct height, but there are occasional outliers that cause the errors. However, the overall trend of the growth is still captured.&lt;/p>

&lt;h3 id="may-29-left-jar" class="anchor">
 &lt;a href="#may-29-left-jar">
 May 29, Left Jar
 &lt;/a>
&lt;/h3>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src=2018-05-29%20Levain%20Timelapse.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">The rise on the left jar had the cleanest growth trace.&lt;/p>
&lt;/div>


&lt;h3 id="may-29-right-jar" class="anchor">
 &lt;a href="#may-29-right-jar">
 May 29, Right Jar
 &lt;/a>
&lt;/h3>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src=2018-05-29%20Levain%20Timelapse,%20Right.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">A narrow crop area had to be used to prevent detection of uneven rise levels.&lt;/p>
&lt;/div>


&lt;h3 id="may-31-first-feeding" class="anchor">
 &lt;a href="#may-31-first-feeding">
 May 31, First Feeding
 &lt;/a>
&lt;/h3>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src=2018-05-31%20Levain%20Timelapse.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">Sunlight through the kitchen window created glare on the bottom left area on the jar, which affected the binary thresholding.&lt;/p>
&lt;/div>


&lt;h3 id="may-31-second-feeding" class="anchor">
 &lt;a href="#may-31-second-feeding">
 May 31, Second Feeding
 &lt;/a>
&lt;/h3>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src=2018-05-31%20Levain%20Timelapse%202.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">Having the jar farther away reduced the thresholding accuracy, but the overall trace was still acceptable.&lt;/p>
&lt;/div>


&lt;h3 id="june-10-out-of-fridge" class="anchor">
 &lt;a href="#june-10-out-of-fridge">
 June 10, Out of Fridge
 &lt;/a>
&lt;/h3>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src=2018-06-10%20Out%20of%20Fridge.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">A larger jar was needed for this one! The thresholding algorithm was surprisingly still able ot catch the peak to some extent, despite minimal contrast.&lt;/p>
&lt;/div>


&lt;h3 id="june-23-first-feeding" class="anchor">
 &lt;a href="#june-23-first-feeding">
 June 23, First Feeding
 &lt;/a>
&lt;/h3>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src=2018-06-23%20First%20Feeding.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">After some neglect, the starter only rose to ~50%...&lt;/p>
&lt;/div>


&lt;h3 id="june-23-refeeding" class="anchor">
 &lt;a href="#june-23-refeeding">
 June 23, Refeeding
 &lt;/a>
&lt;/h3>
&lt;div class="row img-captioned">
 &lt;video class="img-responsive img-span-row img-content" autoplay loop muted controls>
 &lt;source src=2018-06-23%20Refeeding.mp4 type="video/mp4" />
 &lt;/video>
 &lt;p class="caption">But after another feeding the next morning, it had enough activity to spring back ~80% growth!&lt;/p>
&lt;/div>


&lt;h2 id="the-discussion" class="anchor">
 &lt;a href="#the-discussion">
 The Discussion
 &lt;/a>
&lt;/h2>
&lt;p>The animations are cool to watch, but what can we interpret from it? Plotting all the growths (as shown below), we see that they seem more similar than different. The peaks hover around 60-80%, and the rate of growth coming up to the peak are similar.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/all-growths_1.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/06/sourdough-starter-monitor/all-growths_1.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">All the timelapses plotted to compare normalized growth. Horizontal dotted line indicates 50% mark.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="effect-of-regular-feeding" class="anchor">
 &lt;a href="#effect-of-regular-feeding">
 Effect of Regular Feeding
 &lt;/a>
&lt;/h3>
&lt;p>Taking only a select number of days, there is a clearer trend to be seen. The graph below shows how the growth magnitude and rate increases over frequent and regularly scheduled feedings.&lt;/p>
&lt;p>On May 29th, we began to feed our sourdough starter after many months of sporadic feeding. With the June 10th growth, the ramp up occured later but eventually rises to ~80%. The ideal growth target is to double or triple in size, but hopefully that time will come with even more regular feeding.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=Levain%20Growth%20Over%20Time%20%28Regular%20Feeding%29.png>&lt;img class="img-responsive img-content" src=Levain%20Growth%20Over%20Time%20%28Regular%20Feeding%29.png />&lt;/a>
 &lt;p class="caption">We can see that regularly feeding the sourdough starter greatly increases its rate and growth.&lt;/p>
&lt;/div>

&lt;p>&lt;strong>Note&lt;/strong>: The time delay of each subsequent feeding is not a result of regularity, but rather because the starter to food ratio was reduced over time to promote a more active fermentation. We eventually migrated to a 50g starter + 100g water + 100g flour (50 / 50 white and whole wheat). The starter has more potential for growth by beginning with less starter and more food, but as a result it may take a bit longer for the bacteria to vigorously start multiplying.&lt;/p>

&lt;h3 id="bringing-the-starter-back-to-life" class="anchor">
 &lt;a href="#bringing-the-starter-back-to-life">
 Bringing the Starter Back to Life
 &lt;/a>
&lt;/h3>
&lt;p>Previous to June 23, the starter was neglected for a week. After the first (overnight) feeding, it showed little signs of growth. However, feeding it again around noon and tracking its progress shows that the growth springs back up to ~80%, which was around the previous maximum from before.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=refeeding_1.png>&lt;img class="img-responsive img-content" src=refeeding_1.png />&lt;/a>
 &lt;p class="caption">What doesn&amp;#39;t kill you makes you stronger (or at least as strong as before). First feeding is shifted because I forgot to start the timelapse right away.&lt;/p>
&lt;/div>

&lt;p>Thus, a bit of neglect seems to be okay since the starter appears to be fairly resilient to starvation! (But if you&amp;rsquo;re going on vacation, please feed it more than you typically would so it can sustain itself in your absence.)&lt;/p>

&lt;h2 id="the-conclusion" class="anchor">
 &lt;a href="#the-conclusion">
 The Conclusion
 &lt;/a>
&lt;/h2>
&lt;p>So what can we take away from ~40 hours of watching yeast rise&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>? From all this, we learned:&lt;/p>
&lt;ul>
&lt;li>The rate of growth appears to be fairly consistent, even with poorly maintained starters&lt;/li>
&lt;li>A healthy, regularly fed starter reaches maximum fermentation growth after 5-6 hours.&lt;/li>
&lt;li>The peak has a ~1 hour window for when it should be used&lt;/li>
&lt;li>Regularly feeding a neglected starter will make it (and you) happy again&lt;/li>
&lt;/ul>
&lt;p>Even just taking a timelapse will give you a good feel for how your starter behaves, especially when you&amp;rsquo;re starting out with baking bread. If you leave it overnight and come back the next morning to what looks like minimal growth, a timelapse can tell you if it peaked 3 hours in without you noticing, or if it actually didn&amp;rsquo;t grow. Every starter is different, but if you can give it the attention it deserves, its fermentation will continually reward you with loaves like no other.&lt;/p>
&lt;p>Happy baking!&lt;/p>
&lt;hr>

&lt;h1 id="further-reading" class="anchor">
 &lt;a href="#further-reading">
 Further Reading
 &lt;/a>
&lt;/h1>
&lt;p>Check out the links below to level-up your bread game. (I may make profit from the Amazon (affiliate) links below, but not the other ones. Those just have good content that I support!)&lt;/p>
&lt;ul>
&lt;li>Alex French Guy Cooking&amp;rsquo;s Guide to Sourdough &lt;a href="https://www.youtube.com/watch?v=APEavQg8rMw&amp;amp;t=514s">(YouTube)&lt;/a>&lt;/li>
&lt;li>Trevor J. Wilson&amp;rsquo;s Open Crumb Mastery &lt;a href="http://www.breadwerx.com/open-crumb-mastery/">(Breadwerx&lt;/a>, &lt;a href="https://www.youtube.com/watch?v=QHiQ5X3NKEI&amp;amp;t=288s">YouTube)&lt;/a>&lt;/li>
&lt;li>Tartine Bread &lt;a target="_blank" href="https://www.amazon.com/gp/product/0811870413/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=0811870413&amp;linkCode=as2&amp;tag=justinmklam-20&amp;linkId=389ae22a493d37c70d3a6c4446a42721">(Amazon)&lt;/a>&lt;img src="//ir-na.amazon-adsystem.com/e/ir?t=justinmklam-20&amp;l=am2&amp;o=1&amp;a=0811870413" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />&lt;/li>
&lt;li>Flour Water Salt Yeast: The Fundamentals of Artisan Bread and Pizza &lt;a target="_blank" href="https://www.amazon.com/gp/product/160774273X/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=160774273X&amp;linkCode=as2&amp;tag=justinmklam-20&amp;linkId=9e6460fb3ee23e897b924d08e34709d9">(Amazon)&lt;/a>&lt;img src="//ir-na.amazon-adsystem.com/e/ir?t=justinmklam-20&amp;l=am2&amp;o=1&amp;a=160774273X" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />&lt;/li>
&lt;li>My Bread: The Revolutionary No-Work, No-Knead Method&lt;a target="_blank" href="https://www.amazon.com/gp/product/0393066304/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=0393066304&amp;linkCode=as2&amp;tag=justinmklam-20&amp;linkId=292f06c9eaad7c87742dbb35cc1d1233"> (Amazon)&lt;/a>&lt;img src="//ir-na.amazon-adsystem.com/e/ir?t=justinmklam-20&amp;l=am2&amp;o=1&amp;a=0393066304" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Disclaimer: justinmklam.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to Amazon.com.&lt;/p>&lt;/blockquote>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Which, for the record, is much more interesting than watching paint dry.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></content:encoded></item><item><title>From Prototype to Cloud: A Python Recipe Converter</title><link>https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/</link><pubDate>Fri, 06 Apr 2018 20:33:46 -0700</pubDate><guid>https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/</guid><description>&lt;p>In case the title wasn&amp;rsquo;t clear, this blog post is about developing a web application using the Python programming language using Jupyter Lab, Flask, and the Heroku platform. If you were looking for an article on python recipes, you can start off with this one on making a &lt;a href="https://mobile-cuisine.com/recipes/recipe-poached-burmese-python-curry/">poached Burmese python curry&lt;/a>.&lt;/p>

&lt;h1 id="the-backstory" class="anchor">
 &lt;a href="#the-backstory">
 The Backstory
 &lt;/a>
&lt;/h1>
&lt;p>The problem with online baking recipes is that the majority of them use volumetric units. As any civilized baker would know, Patricia&amp;rsquo;s 1 cup of flour may very well be different than Patrick&amp;rsquo;s 1 cup of flour. Maybe Patricia sifted her flour. Maybe Patrick&amp;rsquo;s organic flour is a finer texture. Maybe both Pats &lt;strong>should measure by mass instead of volume&lt;/strong> to avoid all this confusion in the first place.&lt;/p></description><content:encoded>&lt;p>In case the title wasn&amp;rsquo;t clear, this blog post is about developing a web application using the Python programming language using Jupyter Lab, Flask, and the Heroku platform. If you were looking for an article on python recipes, you can start off with this one on making a &lt;a href="https://mobile-cuisine.com/recipes/recipe-poached-burmese-python-curry/">poached Burmese python curry&lt;/a>.&lt;/p>

&lt;h1 id="the-backstory" class="anchor">
 &lt;a href="#the-backstory">
 The Backstory
 &lt;/a>
&lt;/h1>
&lt;p>The problem with online baking recipes is that the majority of them use volumetric units. As any civilized baker would know, Patricia&amp;rsquo;s 1 cup of flour may very well be different than Patrick&amp;rsquo;s 1 cup of flour. Maybe Patricia sifted her flour. Maybe Patrick&amp;rsquo;s organic flour is a finer texture. Maybe both Pats &lt;strong>should measure by mass instead of volume&lt;/strong> to avoid all this confusion in the first place.&lt;/p>
&lt;p>With this in mind, I was tired of constantly typing &amp;ldquo;1 tbsp baking soda in grams&amp;rdquo; and &amp;ldquo;3 1/4 cup flour in grams&amp;rdquo; into Google every time I came across a new recipe. There had to be a better way, and there was. As usual, Python was staring, patiently waiting for me to make eye contact with it.&lt;/p>
&lt;p>My main reqiurement for this application was to be cross-platform. I wanted to be able to carry these conversions out on my laptop, someone else&amp;rsquo;s desktop, or my smartphone. Although &lt;a href="https://reactjs.org/">React&lt;/a> is what all the cool kids talk about these days, I also wanted to develop something fast. Since Python is both excellent at string parsing and easy to work with, it seemed like an appropriate choice.&lt;/p>
&lt;p>After a bit of searching, I came across &lt;a href="http://flask.pocoo.org/">Flask&lt;/a> and &lt;a href="https://www.heroku.com/python">Heroku&lt;/a> which would allow me to use Python for the entire development process, from prototype to deployment. Super cool.&lt;/p>

&lt;h1 id="the-outline" class="anchor">
 &lt;a href="#the-outline">
 The Outline
 &lt;/a>
&lt;/h1>
&lt;p>This blog post will outline the development of a simple string-parsing application that converts common ingredients measured in cups, tablespoons, and teaspoons to grams.&lt;/p>
&lt;ul>
&lt;li>Prototype the recipe conversions in &lt;a href="https://blog.jupyter.org/jupyterlab-is-ready-for-users-5a6f039b8906">Jupyter Lab&amp;rsquo;s&lt;/a> interactive environment&lt;/li>
&lt;li>Migrate script to &lt;a href="https://code.visualstudio.com/">Visual Studio Code&lt;/a> to a command line interface&lt;/li>
&lt;li>Integrate the Python script with &lt;a href="http://flask.pocoo.org/">Flask&amp;rsquo;s web framework&lt;/a>&lt;/li>
&lt;li>Set up and deploy the app through the &lt;a href="https://www.heroku.com/python">Heroku cloud platform&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Interested in trying it out?&lt;/strong> It&amp;rsquo;s live at &lt;a href="http://recipe-converter-app.herokuapp.com/">recipe-converter-app.herokuapp.com&lt;/a>!&lt;/p>
&lt;hr>

&lt;h1 id="the-development" class="anchor">
 &lt;a href="#the-development">
 The Development
 &lt;/a>
&lt;/h1>

&lt;h2 id="prototyping-in-jupyter" class="anchor">
 &lt;a href="#prototyping-in-jupyter">
 Prototyping in Jupyter
 &lt;/a>
&lt;/h2>
&lt;p>Sure you can do all your development in a text editor and command line. There&amp;rsquo;s nothing wrong with putting print statements everywhere, or even having a Python console open to do scratchpad-type testing. But when a tool as flexible and great as Jupyter is available for everyone to use, why settle for anything else?&lt;/p>
&lt;blockquote>
&lt;p>The reason for Jupyter’s immense success is it excels in a form of programming called &lt;strong>&amp;ldquo;literate programming&amp;rdquo;&lt;/strong>. It emphasizes a prose first approach where exposition with human-friendly text is punctuated with code blocks. It excels at demonstration, research, and teaching objectives - &lt;a href="https://unidata.github.io/online-python-training/introduction.html">Unidata&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>One of its greatest selling points is the option to execute code in chunks and see what the output is at each step. For data science, pairing this with the ability to write markdown above or below the code block makes documentation and explanations a joy to work with.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=jupyter%20demo%20gif.gif>&lt;img class="img-responsive img-content" src=jupyter%20demo%20gif.gif />&lt;/a>
 &lt;p class="caption">The power of Jupyter&amp;#39;s interactive output makes prototyping almost effortless. (Source: &lt;a href=http://n-s-f.github.io/2017/03/25/r-to-python.html> Noam Finkelstein&lt;/a>) &lt;/p>
&lt;/div>

&lt;div class="row img-captioned">
 &lt;a href=jupyter%20lab.png>&lt;img class="img-responsive img-content" src=jupyter%20lab.png />&lt;/a>
 &lt;p class="caption">Jupyter Lab is the successor to Jupyter (previously iPython) Notebook. The tabbed interface is a godsend. (Source: &lt;a href=https://blog.jupyter.org/jupyterlab-is-ready-for-users-5a6f039b8906> Jupyter&lt;/a>) &lt;/p>
&lt;/div>


&lt;h3 id="on-to-the-prototype" class="anchor">
 &lt;a href="#on-to-the-prototype">
 On To The Prototype
 &lt;/a>
&lt;/h3>
&lt;p>The proposed use case is described below. Nothing complex happening, just a handful of string parsing operations and a conversion table lookup.&lt;/p>
&lt;ol>
&lt;li>Copy ingredient list to clipboard&lt;/li>
&lt;li>Run script&lt;/li>
&lt;li>Display converted ingredient list&lt;/li>
&lt;/ol>
&lt;p>In the run script method (ie. the meat and potatoes of the app):&lt;/p>
&lt;ol>
&lt;li>Store clipboard text to list data structure*&lt;/li>
&lt;li>Parse fractions to floats&lt;/li>
&lt;li>Unpack abbreviations (ie. tbsp = tablespoon)&lt;/li>
&lt;li>Find each original ingredient in conversion table and convert to grams (where possible)&lt;/li>
&lt;li>Repeat Step 4 for each provided ingredient&lt;/li>
&lt;/ol>
&lt;p>&lt;em>*Turns out it&amp;rsquo;s easy to get clipboard data from a locally running script, but not as straightforward when the script is being hosted on a browser. I guess the user will just have to paste the ingredients into a textbox instead.&lt;/em>&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/conversions.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/conversions.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Imperial volume to metric mass (gram) conversions in a csv file, ie. the wrong units to the right ones.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Checking out my &lt;a href="https://github.com/justinmklam/recipe-converter/blob/3a4c109a1eb7604cb01cc3f615b37e73e08273ca/.ipynb_checkpoints/Recipe%20Converter-checkpoint.ipynb">first working version&lt;/a>, you&amp;rsquo;ll see that it&amp;rsquo;s not the most efficient script. However, when proving out a concept, the priority is to get something working (ie. a minimum viable product) and optimize later. Since uncertainty of this working (or being worth it) is quite high, we don&amp;rsquo;t want to spend too much time making it perfect!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/converter-jupyter-demo.gif>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/converter-jupyter-demo.gif>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Testing out the prototype in Jupyter.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Comparing that with the &lt;a href="https://github.com/justinmklam/recipe-converter/blob/master/Recipe%20Converter%20PoC.ipynb">current version&lt;/a>, we can see that the code structure went from messy and repetitive to organized and modular. Cleaning up code is always mildly therapeutic.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/modularity-comparison.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/modularity-comparison.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Repetitive version on left, modular version on right.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Now that we&amp;rsquo;ve finished testing the prototype and are happy with how it works, we can move on to the next step in turning it into a browser-based application.&lt;/p>

&lt;h2 id="migrating-to-visual-studio-code" class="anchor">
 &lt;a href="#migrating-to-visual-studio-code">
 Migrating to Visual Studio Code
 &lt;/a>
&lt;/h2>
&lt;p>Fortunately, this is painless! In Jupyter, navigate to &lt;code>File &amp;gt; Export Notebook As... &amp;gt; Executable Script&lt;/code>, and that&amp;rsquo;s it. A Python script will download with all the cells in a single executable file.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/export-from-jupyter.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/export-from-jupyter.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Exporting from Jupyter Lab to the next phase of development: the command line.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/vs-code.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/vs-code.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">A bit of clean up later and the python script is ready to be imported as a module.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Feel free to check out the cleaned up file on &lt;a href="https://github.com/justinmklam/recipe-converter/blob/master/recipeConverter.py">GitHub&lt;/a>. That was easy.&lt;/p>

&lt;h2 id="integrating-with-flask" class="anchor">
 &lt;a href="#integrating-with-flask">
 Integrating with Flask
 &lt;/a>
&lt;/h2>

&lt;h3 id="like-the-thing-used-in-chemistry" class="anchor">
 &lt;a href="#like-the-thing-used-in-chemistry">
 Like the Thing Used in Chemistry?
 &lt;/a>
&lt;/h3>
&lt;p>No, this Flask is different! There are many options for web development in Python, with two heavy hitters being Flask and Django. They&amp;rsquo;re both great frameworks, but where Django is the more fully-featured framework, Flask follows the Unix philosophy of &amp;ldquo;do one thing and do it well&amp;rdquo;.&lt;/p>
&lt;blockquote>
&lt;p>Flask is a lightweight WSGI web application framework. It is designed to make getting started quick and easy, with the ability to scale up to complex applications. It began as a simple wrapper around Werkzeug and Jinja and has become one of the most popular Python web application frameworks.&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>Flask offers suggestions, but doesn&amp;rsquo;t enforce any dependencies or project layout. It is up to the developer to choose the tools and libraries they want to use. There are many extensions provided by the community that make adding new functionality easy. - &lt;a href="https://github.com/pallets/flask">Flask GitHub&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>For myself (and this app), I chose Flask because of its simplicity and upfront/visible functionality. The function routing, template rendering is called explicitly with functions, with little magic happening &amp;ldquo;behind the scenes&amp;rdquo;. As &lt;a href="https://stackoverflow.com/questions/12781655/flask-or-django-for-a-beginner">this answer from StackOverflow&lt;/a> puts it, &amp;ldquo;Django can be a little more mysterious for a beginner to figure out how everything fits together&amp;rdquo;.&lt;/p>
&lt;p>Recommended reading:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="http://flask.pocoo.org/docs/0.11/quickstart/">Flask Quickstart&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://flask.pocoo.org/docs/dev/cli/">Running Flask from the Command Line&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://pythonspot.com/flask-web-forms/">Flask Simple Web Form Tutorial&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://flask.pocoo.org/docs/dev/tutorial/#tutorial">Blogging Application Tutorial&lt;/a>&lt;/li>
&lt;/ul>

&lt;h3 id="running-with-flask" class="anchor">
 &lt;a href="#running-with-flask">
 Running With Flask
 &lt;/a>
&lt;/h3>
&lt;p>Now the fun really begins. We can start off by firing up a terminal and installing (and updating) the Flask microframework:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt; pip install -U Flask
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The simplest example of Flask is shown below (taken from &lt;a href="https://en.wikipedia.org/wiki/Flask_(web_framework)#Example">Wikipedia&lt;/a>). If you copy it below and execute the script, a blank webpage will render and display &amp;ldquo;Hello World!&amp;rdquo; in your browser.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">flask&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Flask&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Flask&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@app.route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">hello&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;Hello World!&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;__main__&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The main difference here is the &lt;code>route()&lt;/code> decorator. Modern web frameworks use routing techniques to help keep tracak of application URLs. This decorator is used to bind a Python function to a URL. In this case, the URL is &lt;code>/&lt;/code>, otherwise known as the index / home / default page. If we were to change it to &lt;code>/hello&lt;/code>, then we&amp;rsquo;d have to navigate to &lt;code>localhost:5000/hello&lt;/code> to see &amp;ldquo;Hello World!&amp;rdquo;.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/hello-flask.gif>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/hello-flask.gif>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Saying hello from localhost:5000.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Now moving on to our recipe converter, we need two parts to make this work:&lt;/p>
&lt;ul>
&lt;li>Flask python script for the backend&lt;/li>
&lt;li>HTML template for the frontend&lt;/li>
&lt;/ul>

&lt;h4 id="apppy-aka-the-back-end" class="anchor">
 &lt;a href="#apppy-aka-the-back-end">
 App.py (aka the Back End)
 &lt;/a>
&lt;/h4>
&lt;p>Looking at the code below, there isn&amp;rsquo;t much difference from the simple &amp;ldquo;hello world&amp;rdquo; application from above. One key change is the inclusion of &lt;code>methods = ['GET', 'POST']&lt;/code> in the &lt;code>route()&lt;/code> decorator. From the &lt;a href="http://flask.pocoo.org/docs/0.12/quickstart/">quickstart&lt;/a> guide:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>GET&lt;/strong> - The browser tells the server to just get the information stored on that page and send it.&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>POST&lt;/strong> - The browser tells the server that it wants to post some new information to that URL and that the server must ensure the data is stored and only stored once. This is how HTML forms usually transmit data to the server.&lt;/p>&lt;/blockquote>
&lt;p>Since we are going to have a few buttons on our webpage, we need to include these HTTP methods. With multiple buttons, we can do a simple value check to handle them differently.&lt;/p>
&lt;p>The second key change is returning &lt;code>render_template()&lt;/code> in the routed function call. Since we want to be able to format an HTML page, we can use a template (running on &lt;a href="http://jinja.pocoo.org/docs/2.10/">Jinja2&lt;/a>) to create one.&lt;/p>
&lt;p>We can also pass variables to the template. In this case, &lt;code>textarea&lt;/code> is where the user will enter the ingredient list. When the &amp;lsquo;Convert&amp;rsquo; button is clicked, the text will be parsed by &lt;code>parse_form_text()&lt;/code>. To display the parsed/converted recipe, we can use the &lt;code>flash()&lt;/code> method, which Flask provides as a feedback mechanism to the users (see &lt;a href="http://flask.pocoo.org/docs/0.12/patterns/flashing/">Message Flashing&lt;/a>).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">flask&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Flask&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">render_template&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">flash&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">recipeConverter&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">rc&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Stuff to initialize the Flask app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Flask&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">from_object&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;SECRET_KEY&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;7d441f27d441f28567d441f2b6176a&amp;#39;&lt;/span> &lt;span class="c1"># this can be anything&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># This decorator tells Flask to use this function as a webpage handler/renderer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@app.route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">methods&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;GET&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;POST&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">hello&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Default text to display in the HTML template&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">input_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Hello world!&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Normal page load calls &amp;#39;GET&amp;#39;. &amp;#39;POST&amp;#39; gets called when one of the buttons is pressed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">method&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;POST&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Check which button was pressed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;submit&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;Convert&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">input_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parse_form_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">input_text&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;submit&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;Clear&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">input_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Render the HTML template. input_text gets fed into the textarea variable in the template&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">render_template&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;form.html&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">textarea&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">input_text&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">parse_form_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># This is where the magic happens&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">recipe&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">rc&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RecipeConverter&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># split the text to get each line in a list&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">text2&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">text_converted&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">recipe&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parse_recipe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">text2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">line&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">text_converted&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">flash&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;__main__&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h4 id="formhtml-aka-the-front-end" class="anchor">
 &lt;a href="#formhtml-aka-the-front-end">
 Form.html (aka the Front End)
 &lt;/a>
&lt;/h4>
&lt;p>To accompany our Flask script, we need a usable interface. The interesting bits are encapsulated by &lt;code>{}&lt;/code>, and everything else is straight HTML and the &lt;a href="https://getbootstrap.com">Bootstrap library&lt;/a>.&lt;/p>
&lt;p>The &lt;code>{{textarea}}&lt;/code> is the input variable from &lt;code>render_template()&lt;/code> above. For the message flashing, we can loop through the message list using &lt;code>get_flashed_messages()&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;page-header&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Recipe Converter&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;lead&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Tired of reading recipes in cups, tablespoons, and teaspoons when you just want everything in grams? Paste your recipe in volumetric units in the textbox below, and hit &amp;#34;Convert&amp;#34;!&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Input column (left) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;col-md-6&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h2&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Input Recipe&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h2&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">form&lt;/span> &lt;span class="na">action&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;&amp;#34;&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;textform&amp;#34;&lt;/span> &lt;span class="na">method&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;post&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">textarea&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>{{textarea}}&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">textarea&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- Input text box --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">br&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;submit&amp;#34;&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;submit&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;Clear&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- &amp;#34;Clear&amp;#34; button --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;submit&amp;#34;&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;submit&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;Convert&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- &amp;#34;Convert&amp;#34; button --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">form&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Output column (right) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;col-md-6&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h2&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Output Recipe&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h2&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- Display each converted line from the flash method (from Flask) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% with messages = get_flashed_messages() %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% if messages %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% for message in messages %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {{ message }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">br&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% endfor %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% endif %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {% endwith %}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And we&amp;rsquo;re done! To run the Flask app, we can either execute the Python script by:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt; python app.py
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or, as the Flask documentation suggests, we can also use the &lt;code>flask run&lt;/code> command after setting the script as the &lt;code>FLASK_APP&lt;/code> environment variable.&lt;/p>
&lt;p>&lt;strong>Unix Bash (Linux, Mac, etc.):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">$ export FLASK_APP=app.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ flask run
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Windows Command Prompt:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">&amp;gt; set FLASK_APP=app.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;gt; flask run
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Windows PowerShell:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">&amp;gt; $env:FLASK_APP = &amp;#34;app.py&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;gt; flask run
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/converter-flask-demo.gif>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/converter-flask-demo.gif>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Running Flask in Visual Studio Code.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="deploying-on-heroku-cloud" class="anchor">
 &lt;a href="#deploying-on-heroku-cloud">
 Deploying on Heroku Cloud
 &lt;/a>
&lt;/h2>
&lt;p>First thing&amp;rsquo;s first: Create download &lt;a href="https://devcenter.heroku.com/articles/heroku-cli#download-and-install">Heroku&lt;/a> and create an account by running (after the installation completes):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">&amp;gt; heroku login
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Heroku should now be initialized on your local system!&lt;/p>

&lt;h3 id="setting-up-the-required-files" class="anchor">
 &lt;a href="#setting-up-the-required-files">
 Setting Up the Required Files
 &lt;/a>
&lt;/h3>
&lt;p>To deploy an application/dyno on Heroku, we need the following files:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>app.py&lt;/strong>
&lt;ul>
&lt;li>Flask Python script&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Procfile&lt;/strong>
&lt;ul>
&lt;li>Tells Heroku what commands are run by your application&amp;rsquo;s dynos&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>requirements.txt&lt;/strong>
&lt;ul>
&lt;li>Tells Heroku what modules it needs to install&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>runtime.txt&lt;/strong>
&lt;ul>
&lt;li>Tells Heroku which version of Python to use&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>The following sections will detail what goes into where.&lt;/p>

&lt;h4 id="procfile" class="anchor">
 &lt;a href="#procfile">
 Procfile
 &lt;/a>
&lt;/h4>
&lt;p>We&amp;rsquo;ll be using &lt;a href="http://gunicorn.org/">Gunicorn&lt;/a> to run our application on Heroku, which is its preferred HTTP server. So in &lt;code>Procfile&lt;/code>, add the following line:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">web: gunicorn app:app --log-file=-
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>However, Gunicorn is only available on Unix systems. If we want to test our Heroku app locally, we&amp;rsquo;ll need to specify another command to run the app. For this, we&amp;rsquo;ll put the following in another file named &lt;code>Procfile.windows&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">web: python app.py --log-file=-
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is functionally equivalent to running the Flask command from before, with the main difference is that this uses Heroku&amp;rsquo;s local hosting framework.&lt;/p>

&lt;h4 id="requirementstxt" class="anchor">
 &lt;a href="#requirementstxt">
 Requirements.txt
 &lt;/a>
&lt;/h4>
&lt;p>In &lt;code>Requirements.txt&lt;/code>, we need to add the dependencies our app has. When we deploy it to Heroku cloud, it will install these modules on the server. Add the following lines:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">gunicorn
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Flask
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h4 id="runtimetxt" class="anchor">
 &lt;a href="#runtimetxt">
 Runtime.txt
 &lt;/a>
&lt;/h4>
&lt;p>Finally, we need to specify that our app runs on Python in &lt;code>runtime.txt&lt;/code>. You can check your own version by running &lt;code>python -V&lt;/code> or &lt;code>python --version&lt;/code>.&lt;/p>
&lt;p>In &lt;code>Runtime.txt&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">python-3.6.4
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h3 id="testing-heroku-locally" class="anchor">
 &lt;a href="#testing-heroku-locally">
 Testing Heroku Locally
 &lt;/a>
&lt;/h3>
&lt;p>For a minimal application, our file structure should look like the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"> ├── app.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── Procfile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── requirements.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── runtime.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With our recipe converter app (and because we&amp;rsquo;re on Windows), we have the addition of two files shown below:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"> ├── app.py
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl"> ├── recipe-converter.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── Procfile
&lt;/span>&lt;/span>&lt;span class="line hl">&lt;span class="cl"> ├── Procfile.Windows
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── requirements.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── runtime.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you&amp;rsquo;re on a Unix system, you only need to run &lt;code>heroku local web&lt;/code> to start a local server. However, since gunicorn is a webserver that isn&amp;rsquo;t available on Windows (but is required for Heroku), we need to have a separate Procfile for testing on Windows. Thus, we need to add &lt;code>-f procfile.windows&lt;/code> to ensure Heroku uses the correct file. So to start Heroku locally on Windows, we can run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt; heroku local web -f procfile.windows
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With any luck, the app should compile and run just like it did with Flask!&lt;/p>

&lt;h3 id="pushing-to-heroku-app" class="anchor">
 &lt;a href="#pushing-to-heroku-app">
 Pushing to Heroku App
 &lt;/a>
&lt;/h3>
&lt;p>Heroku uses &lt;code>git&lt;/code> to receive files to host on &lt;code>herokuapp.com&lt;/code>. First, we&amp;rsquo;ll setup the Heroku application with &lt;code>heroku create&lt;/code> (where the second argument is the app name, otherwise a randomly generated one will be used).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt; heroku create recipe-converter-app
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The following commands will create a local git repository and queue all files to be pushed to Heroku&amp;rsquo;s servers.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt; git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;gt; git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;gt; git commit -m &amp;#39;First commit&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally, we can push and deploy the files to Heroku:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt; git push heroku master
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/heroku-deploy.gif>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/heroku-deploy.gif>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Heroku output on compile.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>&lt;strong>Troubleshooting&lt;/strong>: If it compiles but fails to load at &lt;code>*.herokuapp.com&lt;/code>, you can check the logs with the following command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;gt; heroku logs
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h3 id="published-to-the-cloud" class="anchor">
 &lt;a href="#published-to-the-cloud">
 Published to the Cloud!
 &lt;/a>
&lt;/h3>
&lt;p>After all that&amp;rsquo;s been said and done, the recipe converter app is now published on &lt;a href="http://recipe-converter-app.herokuapp.com/">herokuapp.com&lt;/a>! It&amp;rsquo;s a freemium service, so it&amp;rsquo;s worth checking out for professional development as well.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/converter-heroku-demo.gif>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/converter-heroku-demo.gif>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The final python app published through Heroku App.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Logging in to &lt;a href="https://dashboard.heroku.com/">dashboard.heroku.com&lt;/a> provides a nice dashboard for managing all your deployments, activity, and more.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/heroku-dashboard.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/04/python-flask-heroku-tutorial/heroku-dashboard.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Heroku dashboard to manage your dynos.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="closing-thoughts" class="anchor">
 &lt;a href="#closing-thoughts">
 Closing Thoughts
 &lt;/a>
&lt;/h1>
&lt;p>To recap our on our journey, we:&lt;/p>
&lt;ul>
&lt;li>Prototyped the recipe conversions in &lt;a href="https://blog.jupyter.org/jupyterlab-is-ready-for-users-5a6f039b8906">Jupyter Lab&amp;rsquo;s&lt;/a> interactive environment&lt;/li>
&lt;li>Migrated script to &lt;a href="https://code.visualstudio.com/">Visual Studio Code&lt;/a> to a command line interface&lt;/li>
&lt;li>Integrated the Python script with &lt;a href="http://flask.pocoo.org/">Flask&amp;rsquo;s web framework&lt;/a>&lt;/li>
&lt;li>Deployed the app through the &lt;a href="https://www.heroku.com/python">Heroku cloud platform&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Doing everything in Python has &lt;a href="https://www.probytes.net/blog/advantages-disadvantages-python/">its caveats&lt;/a>, but it still can be an excellent tool for quickly getting results. Although the deployed version is still (arguably) a prototype, the amount of legwork we had to accomplish to achieve this end product was relatively minimal!&lt;/p>
&lt;p>As long as we keep in mind that Python is not the solution to everything, we can use it where appropriate and/or when we are aware of its trade-offs.&lt;/p>
&lt;p>Thanks for reading, and I hope you&amp;rsquo;re motivated to either try developing your own app or to stop using volumetric units. If you had to choose one, my vote is on the latter.&lt;/p>
&lt;p>Happy cooking!&lt;/p></content:encoded></item><item><title>Synchronous vs Asynchronous Ping Sweep in C# Windows Form</title><link>https://justinmklam.com/posts/2018/02/ping-sweeper/</link><pubDate>Fri, 09 Feb 2018 17:02:25 -0800</pubDate><guid>https://justinmklam.com/posts/2018/02/ping-sweeper/</guid><description>&lt;p>As a mechatronics engineer (in training), sometimes I like to pretend that I also know how to program.&lt;/p>
&lt;p>In my most recent adventures to software land at &lt;a href="https://mistywest.com/">MistyWest&lt;/a>, I needed to write an application in C# that involved doing a ping sweep to find devices that were physically connected through ethernet. Since Google and Stack Overflow are my two best friends, I was able to find (what seemed to be) an off-the-net solution quite quickly.&lt;/p></description><content:encoded>&lt;p>As a mechatronics engineer (in training), sometimes I like to pretend that I also know how to program.&lt;/p>
&lt;p>In my most recent adventures to software land at &lt;a href="https://mistywest.com/">MistyWest&lt;/a>, I needed to write an application in C# that involved doing a ping sweep to find devices that were physically connected through ethernet. Since Google and Stack Overflow are my two best friends, I was able to find (what seemed to be) an off-the-net solution quite quickly.&lt;/p>
&lt;p>However, despite this being a relatively well known objective with well-known libraries to accomplish it, my journey to developing a solution was not as easy as I originally thought. The following post outlines the things I tried before arriving to a working solution, which hopefully is at least mildly interesting and/or educational. Also if you&amp;rsquo;re a software engineer reading this, please go easy on my code. Or not.&lt;/p>

&lt;h1 id="the-development" class="anchor">
 &lt;a href="#the-development">
 The Development
 &lt;/a>
&lt;/h1>

&lt;h2 id="first-attempt-some-ping-simple" class="anchor">
 &lt;a href="#first-attempt-some-ping-simple">
 First Attempt: Some-ping Simple
 &lt;/a>
&lt;/h2>
&lt;p>A quick Google search showed that pinging addresses is dead easy in C#. Using the &lt;code>System.Net.NetworkInformation&lt;/code> namespace, we can easily use the &lt;code>Ping.Send()&lt;/code> command to check if a remote address is alive.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c#" data-lang="c#">&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">System.Net.NetworkInformation&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">int&lt;/span> &lt;span class="n">timeout&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">10&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">//in ms&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Ping&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">Ping&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">PingReply&lt;/span> &lt;span class="n">rep&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;192.168.1.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">timeout&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">rep&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Status&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="n">IPStatus&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Success&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">//host is active&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is great if we only had a few addresses to ping, because unfortunately this method is unacceptably slow for a user waiting to see if any devices are found. Although &lt;code>Ping.Send&lt;/code> has an overload to accept a timeout interval, it appears that setting very low values doesn&amp;rsquo;t actually change much. From the &lt;a href="https://msdn.microsoft.com/en-us/library/ms144955.aspx">MSDN docs&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>When specifying very small numbers for timeout, the Ping reply can be received even if timeout milliseconds have elapsed.&lt;/p>&lt;/blockquote>
&lt;p>In practice, it seems that ~500ms is about the fastest threshold that can be set. Unfortunately, scanning 255 IP addresses this way will be excruciatingly slow (for both the developer and end user). And the search continues&amp;hellip;&lt;/p>

&lt;h2 id="second-attempt-jaime-lan-nister-pingslayer" class="anchor">
 &lt;a href="#second-attempt-jaime-lan-nister-pingslayer">
 Second Attempt: Jaime LAN-nister, Pingslayer
 &lt;/a>
&lt;/h2>
&lt;p>A few more Googles later and I had what seemed to be the golden solution.&lt;/p>
&lt;p>Thanks to &lt;a href="https://stackoverflow.com/a/4042887">Tim Coker&lt;/a>, I thought I was mostly done my application already (knowledge is half the battle, right?). His console application worked wonderfully and was written in C#, which was great news since I was developing the application using Windows Forms. Instead of using the synchronous &lt;code>Ping.Send()&lt;/code>, it harnessed the asynchronous &lt;code>Ping.SendAsync()&lt;/code> along with the &lt;code>CountdownEvent&lt;/code> class in &lt;code>System.Threading&lt;/code>. However, upon copying the code into Windows Forms, it didn&amp;rsquo;t seem to work. What gives?&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/* Source: https://stackoverflow.com/a/4042887
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * Original author: Tim Coker
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">System&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">System.Diagnostics&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">System.Threading&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="nn">System.Net.NetworkInformation&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">namespace&lt;/span> &lt;span class="nn">ConsoleApplication1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">class&lt;/span> &lt;span class="nc">Program&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">static&lt;/span> &lt;span class="n">CountdownEvent&lt;/span> &lt;span class="n">countdown&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">static&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">upCount&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">static&lt;/span> &lt;span class="kt">object&lt;/span> &lt;span class="n">lockObj&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="kt">object&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">const&lt;/span> &lt;span class="kt">bool&lt;/span> &lt;span class="n">resolveNames&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">static&lt;/span> &lt;span class="k">void&lt;/span> &lt;span class="n">Main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">[]&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">countdown&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">CountdownEvent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Stopwatch&lt;/span> &lt;span class="n">sw&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">Stopwatch&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Start&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">string&lt;/span> &lt;span class="n">ipBase&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;10.22.4.&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="m">255&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">++)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">string&lt;/span> &lt;span class="n">ip&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">ipBase&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Ping&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">Ping&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">PingCompleted&lt;/span> &lt;span class="p">+=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">PingCompletedEventHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">p_PingCompleted&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">countdown&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">AddCount&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">SendAsync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ip&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">100&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ip&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">countdown&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Signal&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">countdown&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Wait&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Stop&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">TimeSpan&lt;/span> &lt;span class="n">span&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">TimeSpan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ElapsedTicks&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">WriteLine&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Took {0} milliseconds. {1} hosts active.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ElapsedMilliseconds&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">upCount&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ReadLine&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">static&lt;/span> &lt;span class="k">void&lt;/span> &lt;span class="n">p_PingCompleted&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">object&lt;/span> &lt;span class="n">sender&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PingCompletedEventArgs&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">string&lt;/span> &lt;span class="n">ip&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">UserState&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Reply&lt;/span> &lt;span class="p">!=&lt;/span> &lt;span class="kc">null&lt;/span> &lt;span class="p">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Reply&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Status&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="n">IPStatus&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Success&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">WriteLine&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;{0} is up: ({1} ms)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ip&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Reply&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">RoundtripTime&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">lock&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lockObj&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">upCount&lt;/span>&lt;span class="p">++;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Reply&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">WriteLine&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Pinging {0} failed. (Null Reply object?)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ip&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">countdown&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Signal&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lesson learned: Console applications and Windows Forms are different beasts and deadlocks occur with the above code. According to &lt;a href="https://stackoverflow.com/a/7767632">Hans Passant from another Stack Overflow thread&lt;/a>, the additional UI thread is the culprit:&lt;/p>
&lt;blockquote>
&lt;p>Winforms has a synchronization provider whereas console apps do not. The problem is that the &lt;code>Ping&lt;/code> class makes a best effort to raise the &lt;code>PingCompleted&lt;/code> event on the same thread that calls &lt;code>SendAsync()&lt;/code>. So it tries to raise the event on the main thread, but that can&amp;rsquo;t work since the main thread is blocked with the &lt;code>countdown.Wait()&lt;/code> call. In a console app however, the &lt;code>PingCompleted&lt;/code> event will be raised on a &lt;code>ThreadPool&lt;/code> thread.&lt;/p>&lt;/blockquote>
&lt;p>Hm, turns out this problem wasn&amp;rsquo;t as easy as copying and pasting random code off the internet. Time for a bit of research!&lt;/p>

&lt;h3 id="digging-deeper-a-battle-of-asynchronous-pings" class="anchor">
 &lt;a href="#digging-deeper-a-battle-of-asynchronous-pings">
 Digging Deeper: A Battle of Asynchronous Pings
 &lt;/a>
&lt;/h3>
&lt;p>For some reason that can only be to confuse inexperienced programmers like myself, there are two different asynchronous ping methods in this class. First on the list:&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>Ping.SendAsync()&lt;/code>: &lt;strong>Asynchronously attempts&lt;/strong> to send an Internet Control Message Protocol (ICMP) echo message to a computer, and receive a corresponding ICMP echo reply message from that computer. - &lt;a href="https://msdn.microsoft.com/en-us/library/system.net.networkinformation.ping.sendasync(v=vs.110).aspx">MSDN&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>And the second:&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>Ping.SendPingAsync()&lt;/code>: Sends an Internet Control Message Protocol (ICMP) echo message to a computer, and receives a corresponding ICMP echo reply message from that computer &lt;strong>as an asynchronous operation&lt;/strong>. - &lt;a href="https://msdn.microsoft.com/en-us/library/system.net.networkinformation.ping.sendpingasync(v=vs.110).aspx">MSDN&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>Based on these method descriptions, it appears that &lt;code>SendAsync()&lt;/code> does not guarantee asynchronous operation. Since we just learned how threading in console applications vs windows forms is dealt with differently, this may be why it didn&amp;rsquo;t work as expected in the latter case. What we want are guaranteed asynchronous operations, so hopefully &lt;code>SendPingAsync()&lt;/code> should perform to its name.&lt;/p>
&lt;p>(One can only assume this second method was added after-the-fact when Microsoft realized that developers wanted a truly asynchronous ping&amp;hellip;)&lt;/p>
&lt;p>&lt;strong>The takeaway:&lt;/strong> Use &lt;code>SendPingAsync()&lt;/code> or bust.&lt;/p>

&lt;h3 id="digging-deeper-background-worker-vs-asyncawait" class="anchor">
 &lt;a href="#digging-deeper-background-worker-vs-asyncawait">
 Digging Deeper: Background Worker vs Async/Await
 &lt;/a>
&lt;/h3>
&lt;p>So we&amp;rsquo;ve selected our asynchronous ping method, but that still leaves us hanging over how we&amp;rsquo;re going to handle it in a background task. Before .NET 4.0 was released, &lt;code>BackgroundWorker&lt;/code> was the de-facto standard&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. However:&lt;/p>
&lt;blockquote>
&lt;p>The core problem that &lt;code>BackgroundWorker&lt;/code> originally solved was the need to &lt;em>execute synchronous code on a background thread&lt;/em>. If you&amp;rsquo;re using it for asynchronous or parallel work, you&amp;rsquo;re not using the right tool in the first place. - &lt;a href="http://blog.stephencleary.com/2013/05/taskrun-vs-backgroundworker-round-1.html">Stephen Cleary&lt;/a>&lt;/p>&lt;/blockquote>
&lt;p>After .NET 4.0, we have the following options:&lt;/p>
&lt;ul>
&lt;li>Tasks (Async Methods)&lt;/li>
&lt;li>Tasks (Task Parallel Library)&lt;/li>
&lt;li>Delegate.BeginInvoke&lt;/li>
&lt;li>ThreadPool.QueueUserWorkItem&lt;/li>
&lt;li>Threads&lt;/li>
&lt;/ul>
&lt;p>Discussing each of these methods is beyond the scope of this post, but you can read more on Cleary&amp;rsquo;s article on &lt;a href="http://blog.stephencleary.com/2010/08/various-implementations-of-asynchronous.html">various implementations of asynchronous background tasks&lt;/a>. In short, using &lt;code>Task&lt;/code>-returning asynchronous methods is the best overall method to use.&lt;/p>
&lt;p>Kind of.&lt;/p>
&lt;p>For my application, sending each ping to its own task using &lt;code>async/await&lt;/code> is logical, as I could then call &lt;code>Task.WhenAll()&lt;/code> to wait until all pings have been received back to the main thread.&lt;/p>
&lt;p>However, I could still use &lt;code>BackgroundWorker&lt;/code> for SFTP file transfer from the remote devices to a local directory (required in my final application, but not included in this ping sweep demo). Doing so would prevent the main UI thread from hanging while files are being transferred. Although &lt;code>async/await&lt;/code> may also be used for this, &lt;code>BackgroundWorker&lt;/code> seemed to be the more appropriate (and easier) implementation since each file is serially transferred from remote to local device. Additionally, it&amp;rsquo;s just drag and drop in WinForms!&lt;/p>
&lt;p>&lt;strong>The takeaway:&lt;/strong> Use &lt;code>async/await&lt;/code> for asynchronous ping sweep, and &lt;code>BackgroundWorker&lt;/code> for SFTP file transfers.&lt;/p>

&lt;h2 id="third-attempt-one-ping-to-rule-them-all-one-ping-to-find-them" class="anchor">
 &lt;a href="#third-attempt-one-ping-to-rule-them-all-one-ping-to-find-them">
 Third Attempt: One Ping to Rule Them All, One Ping to Find Them
 &lt;/a>
&lt;/h2>
&lt;!-- ![Simple Winform application to demonstrate the power of threads.](winform.png) -->
&lt;p>Finally, we have a working solution! Using &lt;code>async/await&lt;/code> with &lt;code>Tasks&lt;/code> in &lt;code>System.Threading.Tasks&lt;/code> yields promising results. See below for the implementation.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c#" data-lang="c#">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">BaseIP&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;192.168.1.&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">StartIP&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">StopIP&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">255&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">ip&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">timeout&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">100&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">nFound&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">static&lt;/span> &lt;span class="kt">object&lt;/span> &lt;span class="n">lockObj&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="kt">object&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Stopwatch&lt;/span> &lt;span class="n">stopWatch&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">Stopwatch&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">TimeSpan&lt;/span> &lt;span class="n">ts&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="kd">async&lt;/span> &lt;span class="k">void&lt;/span> &lt;span class="n">RunPingSweep_Async&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nFound&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">tasks&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">Task&lt;/span>&lt;span class="p">&amp;gt;();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stopWatch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Start&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">StartIP&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="p">&amp;lt;=&lt;/span> &lt;span class="n">StopIP&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">++)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ip&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">BaseIP&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NetworkInformation&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Ping&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NetworkInformation&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Ping&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">task&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">PingAndUpdateAsync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ip&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tasks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">task&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">Task&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">WhenAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tasks&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">ContinueWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stopWatch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Stop&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ts&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">stopWatch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Elapsed&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MessageBox&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Show&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">nFound&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="s">&amp;#34; devices found! Elapsed time: &amp;#34;&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">ts&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="s">&amp;#34;Asynchronous&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kd">async&lt;/span> &lt;span class="n">Task&lt;/span> &lt;span class="n">PingAndUpdateAsync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NetworkInformation&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Ping&lt;/span> &lt;span class="n">ping&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">ip&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">reply&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">ping&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">SendPingAsync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ip&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">timeout&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">reply&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Status&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NetworkInformation&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">IPStatus&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Success&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">lock&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lockObj&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nFound&lt;/span>&lt;span class="p">++;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/02/ping-sweeper/result-async.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/02/ping-sweeper/result-async.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Asynchronous pings are light years faster! Half a second and we&amp;rsquo;re rocking with all the pings we needed.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="verification-with-benchmark" class="anchor">
 &lt;a href="#verification-with-benchmark">
 Verification with Benchmark
 &lt;/a>
&lt;/h3>
&lt;p>To make sure our numbers add up, let&amp;rsquo;s compare it with a known tool that is much more mature than this C# application.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/02/ping-sweeper/angryip.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/02/ping-sweeper/angryip.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Verifying scanned result with Angry IP scanner because apparently nmap isn&amp;rsquo;t fully functional in Windows Subsystem for Linux yet.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Same number of hosts alive, but a little slower. However, Angry IP Scanner is more robust in its pings and thread handling, so the few extra seconds is likely put to good use. I found my application to be somewhat inconsistent in finding all the alive hosts, which may be mitigated with multiple ping packets to send (instead of just one).&lt;/p>

&lt;h3 id="comparing-with-synchronous-ping-sweep" class="anchor">
 &lt;a href="#comparing-with-synchronous-ping-sweep">
 Comparing with Synchronous Ping Sweep
 &lt;/a>
&lt;/h3>
&lt;p>Because pinging is such an interesting past-time, let&amp;rsquo;s see how the very first synchronous solution performs using &lt;code>Ping.Send()&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c#" data-lang="c#">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">BaseIP&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;192.168.1.&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">StartIP&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">StopIP&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">255&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">ip&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">timeout&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">100&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">nFound&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Stopwatch&lt;/span> &lt;span class="n">stopWatch&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">Stopwatch&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">TimeSpan&lt;/span> &lt;span class="n">ts&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="k">void&lt;/span> &lt;span class="n">RunPingSweep_Sync&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nFound&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stopWatch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Start&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NetworkInformation&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Ping&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NetworkInformation&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Ping&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">StartIP&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="p">&amp;lt;=&lt;/span> &lt;span class="n">StopIP&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">++)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ip&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">BaseIP&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NetworkInformation&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">PingReply&lt;/span> &lt;span class="n">rep&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ip&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">timeout&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">rep&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Status&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NetworkInformation&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">IPStatus&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Success&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">nFound&lt;/span>&lt;span class="p">++;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stopWatch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Stop&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ts&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">stopWatch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Elapsed&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MessageBox&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Show&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">nFound&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="s">&amp;#34; devices found! Elapsed time: &amp;#34;&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">ts&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToString&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="s">&amp;#34;Synchronous&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/02/ping-sweeper/result-sync.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/02/ping-sweeper/result-sync.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Result of 255 pings using a synchronous method. Nobody has 2 minutes to wait for a complete scan.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Wow. A full two minutes compared to less than one second. Life truly is better when you live asynchronously.&lt;/p>

&lt;h1 id="closing-thoughts" class="anchor">
 &lt;a href="#closing-thoughts">
 Closing Thoughts
 &lt;/a>
&lt;/h1>
&lt;p>This concludes another adventure through asynchronous methods in C#. It&amp;rsquo;s amazing how much information is available on the internet, and I truly would not be able to get this far without it. Google and Stack Overflow, what would I be without you?&lt;/p>
&lt;p>Thanks for reading, and I hope you learned a &lt;em>ping&lt;/em> or two about these methods.&lt;/p>
&lt;br>
&lt;p>Check out the full source code of this project on &lt;a href="https://github.com/justinmklam/ping-sweeper/blob/master/Ping%20Sweep%20Demo/Ping%20Sweep%20Demo/FormMain.cs">Github&lt;/a>!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>According to &lt;a href="http://blog.stephencleary.com/2010/08/various-implementations-of-asynchronous.html">Stephen Cleary&lt;/a>, one of &lt;a href="https://mvp.microsoft.com/en-us/PublicProfile/5000058?fullName=Stephen%20Cleary">Microsoft&amp;rsquo;s Most Valuable Professionals&lt;/a> and top answerer for async/await questions on Stack Overflow. I&amp;rsquo;d trust him if I were you.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></content:encoded></item><item><title>Measuring the Spectral Characteristics of a Light Therapy Lamp</title><link>https://justinmklam.com/posts/2018/01/sad-lamp/</link><pubDate>Fri, 12 Jan 2018 09:42:57 -0800</pubDate><guid>https://justinmklam.com/posts/2018/01/sad-lamp/</guid><description>&lt;p>&lt;strong>Disclaimer:&lt;/strong> &lt;em>Light therapy is one method of easing seasonal affective disorder (SAD); some people swear by it whereas others remain unaffected. This blog post does not intend to refute the effectiveness of light therapy, but rather to dig deeper into the technology behind these light therapy lamps to better educate fellow consumers.&lt;/em>&lt;/p>
&lt;p>Ah, the winter blues of Vancouver, BC. While some days bring bluebird skies and &lt;a href="https://media1.tenor.com/images/25e5bfdf6e824bfa330964b5e0e48855/tenor.gif?itemid=5287297">fresh pow&lt;/a> for skiing, other days are downright gloomy. Spending this past Christmas in Singapore meant warm, sunny, and +25°C weather, which is quite atypical of how most Canadians spend their winters.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Disclaimer:&lt;/strong> &lt;em>Light therapy is one method of easing seasonal affective disorder (SAD); some people swear by it whereas others remain unaffected. This blog post does not intend to refute the effectiveness of light therapy, but rather to dig deeper into the technology behind these light therapy lamps to better educate fellow consumers.&lt;/em>&lt;/p>
&lt;p>Ah, the winter blues of Vancouver, BC. While some days bring bluebird skies and &lt;a href="https://media1.tenor.com/images/25e5bfdf6e824bfa330964b5e0e48855/tenor.gif?itemid=5287297">fresh pow&lt;/a> for skiing, other days are downright gloomy. Spending this past Christmas in Singapore meant warm, sunny, and +25°C weather, which is quite atypical of how most Canadians spend their winters.&lt;/p>
&lt;p>However, coming back to the overcast, bone-chilling, gray, and wet Vancouver winter was a climate shock my body had yet to feel before. A week or so of jetlag-ridden sleep cycles and my circadian rhythm was back to normal, but I couldn&amp;rsquo;t scratch the itch of yearning for sun and truly despising this city&amp;rsquo;s gloomy winter weather.&lt;/p>
&lt;p>Thanks to Amazon Prime&amp;rsquo;s same/one day shipping, a solution to this problem was less than 24 hours away. Introducing my new light therapy lamp!&lt;/p>
&lt;!-- ![Verilux's compact light therapy lamp and its advertised benefits.](lamp-benefits.png) -->
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/01/sad-lamp/IMG_20180104_192702_hu_99ad361b0b87217f.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/01/sad-lamp/IMG_20180104_192702_hu_99ad361b0b87217f.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Verilux HappyLight Compact Light Therapy Lamp with the UV filter/diffuser installed.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/01/sad-lamp/IMG_20180103_193302_hu_5ff43131e83ad191.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/01/sad-lamp/IMG_20180103_193302_hu_5ff43131e83ad191.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Ultraviolet difuser/filter removed from the lamp. Wait a minute, is that just a CFL bulb?&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="the-suspicions" class="anchor">
 &lt;a href="#the-suspicions">
 The Suspicions
 &lt;/a>
&lt;/h1>
&lt;p>But wait just one moment; upon unboxing the lamp, I noticed something familiar. The light source behind the box looked strangely similar to a standard CFL bulb&amp;hellip; Had I just been duped into spending $70 for a plain old light bulb in a fancy plastic enclosure?&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/01/sad-lamp/bulb-comparison.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/01/sad-lamp/bulb-comparison.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Left: $14.99 Verilux Full Spectrum Bulb. Right: $1.50 Standard CFL bulb.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Fortunately for me, I work at &lt;a href="https://mistywest.com/">MistyWest&lt;/a> and we have a fancy spectrometer that will allow me to confirm/deny my suspicions. It&amp;rsquo;s used to measure the spectral power density at each wavelength of light (or in layman terms, it measures the intensity of each specific colour of light). The two main questions I was curious to answer:&lt;/p>
&lt;ol>
&lt;li>Are the spectral characteristics of this bulb any different than a regular CFL bulb?&lt;/li>
&lt;li>Is the filter/diffuser actually made of a special UV-blocking material?&lt;/li>
&lt;/ol>
&lt;p>But before we get into the details of spectral characteristics, let&amp;rsquo;s shed &lt;em>a bit of light&lt;/em> on the different lighting methods and how they came to be. (Stay with me; it&amp;rsquo;s worth it.)&lt;/p>

&lt;h1 id="the-research" class="anchor">
 &lt;a href="#the-research">
 The Research
 &lt;/a>
&lt;/h1>

&lt;h2 id="illumination-nation" class="anchor">
 &lt;a href="#illumination-nation">
 Illumination Nation
 &lt;/a>
&lt;/h2>

&lt;h3 id="overview-of-light-sources" class="anchor">
 &lt;a href="#overview-of-light-sources">
 Overview of Light Sources
 &lt;/a>
&lt;/h3>
&lt;p>Different sources of light can have significantly different spectral characteristics. The figure below shows how six different light sources vary greatly at each wavelength.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=spectral_responses2.png>&lt;img class="img-responsive img-content" src=spectral_responses2.png />&lt;/a>
 &lt;p class="caption">Typical spectral characteristics and corresponding colours of various lighting. (Source: &lt;a href=http://housecraft.ca/eco-friendly-lighting-colour-rendering-index-and-colour-temperature/> HouseCraft&lt;/a>) &lt;/p>
&lt;/div>

&lt;ul>
&lt;li>&lt;strong>Daylight&lt;/strong>
&lt;ul>
&lt;li>Rounded peak around 450 nm&lt;/li>
&lt;li>Broad spectrum light intensity&lt;/li>
&lt;li>5000 - 6500K&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Incandenscent&lt;/strong>
&lt;ul>
&lt;li>Increases from 400 nm to 700 - 800 nm&lt;/li>
&lt;li>Burning of tungsten filament inside a vacuum bulb produces a very warm glow (in addition to a great amount of heat!)&lt;/li>
&lt;li>2400 K&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Fluorescent&lt;/strong>
&lt;ul>
&lt;li>Distinct peaks around 420 nm, 490 nm, 550 nm, and 610 nm&lt;/li>
&lt;li>Peaks are due to the fluorescence of excited phosphor within the glass tube&lt;/li>
&lt;li>2700 - 6500 K&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;div class="row img-captioned">
 &lt;a href=image-cfl.png>&lt;img class="img-responsive img-content" src=image-cfl.png />&lt;/a>
 &lt;p class="caption">Electricity ionizes the mercury and argon gas, producing UV light which hits the phosphor coating and finally fluoresces white. (Source: &lt;a href=https://www.naturalblaze.com/wp-content/uploads/2017/10/image-cfl.png> Natural Blaze&lt;/a>) &lt;/p>
&lt;/div>

&lt;ul>
&lt;li>&lt;strong>Halogen&lt;/strong>
&lt;ul>
&lt;li>Rounded peak around 600 nm&lt;/li>
&lt;li>Also burns a tungsten filament, but the gas is at a higher pressure for brighter illumination (basically a more advanced incandescent bulb)&lt;/li>
&lt;li>2800 - 3400K&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Cool White LED&lt;/strong>
&lt;ul>
&lt;li>Sharp peak around 450 nm, rounded peak around 550 nm&lt;/li>
&lt;li>Gallium nitride on a sapphire substrate produces blue (first peak), and a phosphor coating produces yellow light through fluorescence (second peak)&lt;/li>
&lt;li>3500 - 4100 K&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Warm White LED&lt;/strong>
&lt;ul>
&lt;li>Shallow peak around 450nm, large peak around 550 nm&lt;/li>
&lt;li>Combining different phosphors change the fluorescent characteristics of the emitted light&lt;/li>
&lt;li>2700 K&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;!-- ![. [Source: Serendip Bryn Mawr]](incandescentbulb.jpg) -->
&lt;div class="row img-captioned">
 &lt;a href=LED-explanation0.jpg>&lt;img class="img-responsive img-content" src=LED-explanation0.jpg />&lt;/a>
 &lt;p class="caption">When an electron flows from anode to cathode across the band gap, it falls into a lower energy level and releases energy in the form of a photon (light). (Source: &lt;a href=http://www.lumenelectronicjewelry.com/2014/04/how-does-an-led-work-anyway/> Lumen Electronic Jewelry&lt;/a>) &lt;/p>
&lt;/div>


&lt;h3 id="a-walk-down-memory-lane" class="anchor">
 &lt;a href="#a-walk-down-memory-lane">
 A Walk Down Memory Lane
 &lt;/a>
&lt;/h3>
&lt;p>Now, slapping some dates to the evolution of lighting technologies (thanks &lt;a href="https://en.wikipedia.org/wiki/Timeline_of_lighting_technology#20th_century">Wikipedia&lt;/a>) gives us a pretty good understanding of how this all played out:&lt;/p>
&lt;ul>
&lt;li>&lt;em>1904&lt;/em> - Alexander Just and Franjo Hanaman invent the tungsten filament for &lt;strong>incandescent&lt;/strong> lightbulbs.&lt;/li>
&lt;li>&lt;em>1926&lt;/em> - Edmund Germer patents the modern &lt;strong>fluorescent&lt;/strong> lamp.&lt;/li>
&lt;li>&lt;em>1927&lt;/em> - Oleg Losev creates the first &lt;strong>LED&lt;/strong> (light-emitting diode).&lt;/li>
&lt;li>&lt;em>1953&lt;/em> - Elmer Fridrich invents the &lt;strong>halogen&lt;/strong> light bulb.&lt;/li>
&lt;li>&lt;em>1962&lt;/em> - Nick Holonyak Jr. develops the first practical visible-spectrum (&lt;strong>red&lt;/strong>) light-emitting diode.&lt;/li>
&lt;li>&lt;em>1981&lt;/em> - Philips sells their first &lt;strong>Compact Fluorescent&lt;/strong> Energy Saving Lamps, with integrated conventional ballast.&lt;/li>
&lt;li>&lt;em>1995&lt;/em> - Shuji Nakamura at Nichia labs invents the first practical &lt;strong>blue&lt;/strong> and with additional phosphor, white LED, starting an LED boom.&lt;/li>
&lt;/ul>
&lt;p>Through this brief overview of light sources, we can see how the different illumination methods developed from sending current through a tiny strand of wire making it glow bright, to using fluorescence as an energy efficient way to produce light (and also at different colour temperatures).&lt;/p>

&lt;h3 id="so-what" class="anchor">
 &lt;a href="#so-what">
 So What?
 &lt;/a>
&lt;/h3>
&lt;p>The takeaway through this stroll through history lane is this: even before taking the spectral measurement of this bulb, we already know that the likelihood of this light therapy bulb being different is quite low. The History of Light(TM) paints a clear path of what&amp;rsquo;s both economically and physically possible in creating a light source, so this mystical light therapy bulb is almost certainly just your everyday compact fluorescent bulb (with a GU10 base).&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=bulb%20types.jpg>&lt;img class="img-responsive img-content" src=bulb%20types.jpg />&lt;/a>
 &lt;p class="caption">Let&amp;#39;s mix and match different bases and shapes and label it as a proprietary bulb! The marketing team will love it. (Source: &lt;a href=http://www.tomic-arms.com/track-lighting-bulb-types/good-track-lighting-bulb-types-77-on-track-lighting-with-ceiling-fan-with-track-lighting-bulb-types/> Tomic Arms&lt;/a>) &lt;/p>
&lt;/div>

&lt;p>But since we&amp;rsquo;re already here, let&amp;rsquo;s measure the data anyway! It&amp;rsquo;s not everyday we get to learn about the physics we&amp;rsquo;re surrounded by, especially when it involves expensive &lt;del>toys&lt;/del> instruments.&lt;/p>

&lt;h2 id="lets-get-to-it-playing-with-the-spectrometer" class="anchor">
 &lt;a href="#lets-get-to-it-playing-with-the-spectrometer">
 Let&amp;rsquo;s Get To It: Playing with the Spectrometer
 &lt;/a>
&lt;/h2>
&lt;p>With our &lt;a href="https://oceanoptics.com/product/sts-vis-rad/">Ocean Optics visible light spectrometer&lt;/a> placed about 10 cm in front of the light therapy lamp and set with a 10 ms integration time, we can capture the spectral characteristics of the bulb. To recap, the two things we&amp;rsquo;re interested in are the following:&lt;/p>
&lt;ol>
&lt;li>Are the spectral characteristics of this bulb any different than a regular CFL bulb?&lt;/li>
&lt;li>Is the filter/diffuser actually made of a special UV-blocking material?&lt;/li>
&lt;/ol>

&lt;h3 id="to-cfl-or-to-not-cfl-that-is-the-question" class="anchor">
 &lt;a href="#to-cfl-or-to-not-cfl-that-is-the-question">
 To CFL or To Not CFL? That is the Question
 &lt;/a>
&lt;/h3>
&lt;p>Looking at the figure below, the trace in blue is the response with the frosted plastic in front of the bulb (ie. the lamp fully assembled). Surprise, surprise: the distinct peaks suggest that it is in fact a compact fluorescent bulb.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/01/sad-lamp/Verilux-Comparison.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/01/sad-lamp/Verilux-Comparison.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Spectral responses with and without the UV diffuser/filter.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>However, a typical fluorescent bulb has sharp peaks and a flat baseline. What&amp;rsquo;s up with the rounded peak around 450 nm?&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=spectral_responses_fluorescent.png>&lt;img class="img-responsive img-content" src=spectral_responses_fluorescent.png />&lt;/a>
 &lt;p class="caption">Standard fluorescent tube. (Source: &lt;a href=http://housecraft.ca/eco-friendly-lighting-colour-rendering-index-and-colour-temperature/> HouseCraft&lt;/a>) &lt;/p>
&lt;/div>

&lt;p>Digging a bit deeper, it appears that the additional peak is due to the different colour temperature. Based on the spectral data collected by &lt;a href="http://www.advancedaquarist.com/2010/8/aafeature">Advanced Aquarist&lt;/a> shown below, the rounded peak is likely from an additional/different phosphor inside the tube. This creates additional fluorescence to shift the visible colour temperature towards a more daylight-esque tone (by adding more blue content). On the other hand, the soft white bulb shows a flat spectral base, as expected from our previous graphs.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=cfl_comparison.jpg>&lt;img class="img-responsive img-content" src=cfl_comparison.jpg />&lt;/a>
 &lt;p class="caption">Spectral response of a daylight CFL bulb, 6500K (left) and soft white, 2700K (right). (Source: &lt;a href=http://www.advancedaquarist.com/2010/8/aafeature> Advanced Aquarist&lt;/a>) &lt;/p>
&lt;/div>

&lt;p>&lt;strong>The takeaway:&lt;/strong> CFL bulbs have similar spectral characteristics to daylight in terms of how our eyes interpret it. This particular light therapy lamp uses a standard 6500K CFL bulb, so you&amp;rsquo;re kind of paying for the marketing hype.&lt;/p>

&lt;h3 id="true-or-alternative-fact-uv-blocking-filter" class="anchor">
 &lt;a href="#true-or-alternative-fact-uv-blocking-filter">
 True or Alternative Fact: UV Blocking Filter
 &lt;/a>
&lt;/h3>
&lt;p>So now that we have our first question answered, let&amp;rsquo;s get on with the second. The frosted filter/diffuser that comes with this lamp is advertised to ensure it is a UV-free light source. Is this an actual optical filter that transmits light beyond the UV wavelengths (&amp;gt; 400 nm), or does it just a piece of plastic?&lt;/p>
&lt;p>Let&amp;rsquo;s look closely at the spectral characteristics. Zooming in to the 350 - 400 nm region and changing the y-axis to a log scale (to better visualize the data), we see that the peak around 365 nm is indeed attenuated by a factor of ~10. However, looking back at the original figure (above), we see that the entire spectral response is attenuated.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/01/sad-lamp/Verilux-Comparison-UV.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/01/sad-lamp/Verilux-Comparison-UV.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Detailed look at the UV exposure on a logarithmic scale.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>To put a number to this, we can numerically integrate the spectral power density at each wavelength and sum up the values to calculate irradiance (in units mW/cm&lt;sup>2&lt;/sup>). Over the entire measured spectrum of 350 - 800 nm, we see that the diffuser reduces the total brightness by ~48%. So instead of removing just the UV wavelengths, they diffused the entire spectrum until the UV exposure is low enough to meet the &amp;ldquo;no UV exposure&amp;rdquo; threshold.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/01/sad-lamp/chart.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/01/sad-lamp/chart.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Comparison of total irradiance with and without the diffuser.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Integrating the UV region between 350 - 400 nm shows that there&amp;rsquo;s still a bit of UV light that gets through. However, it&amp;rsquo;s of insignificant amount compared to the UV exposure from other sources (ie. sun, TV, computer screen, etc.) that they can say that there&amp;rsquo;s &amp;ldquo;effectively no UV radiation&amp;rdquo; from this lamp.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2018/01/sad-lamp/chart-%281%29.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2018/01/sad-lamp/chart-%281%29.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Comparison of irradiance in the UV range, with and without the diffuser.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>&lt;strong>The takeaway:&lt;/strong> UV is removed by attenuating the entire spectral response of the CFL bulb until the UV light is removed (for practical purposes). Yes, the frosted diffuser also makes the lamp more user friendly (as staring into a bare bulb is rather jarring), but now you know that you can simply replace it with another diffuse material should you ever lose that part.&lt;/p>

&lt;h1 id="the-light-at-the-end-of-the-tunnel" class="anchor">
 &lt;a href="#the-light-at-the-end-of-the-tunnel">
 The Light at the End of the Tunnel
 &lt;/a>
&lt;/h1>
&lt;p>I hope this journey through light and its spectral characteristics was both entertaining and educational. With any luck, you may have learned a thing or two (and gained an appreciation) about all the different light sources surrounding you everyday.&lt;/p>
&lt;p>Although you can build your own light therapy lamp for a fraction of its retail purchasing cost, that may not be a viable (or convenient) option for everyone. Additionally, light therapy is apparently more dependent on the brightness output rather than the actual wavelengths; as long as it&amp;rsquo;s bright and white(-ish) then it should work for light therapy!&lt;/p>
&lt;p>Whatever you choose to do with this information is up to you, so light up your own path to mitigating your SAD-ness :)&lt;/p>
&lt;br>
&lt;p>&lt;em>&lt;strong>Acknowledgements:&lt;/strong>&lt;/em> &lt;em>Thanks to &lt;a href="https://mistywest.com/">MistyWest&lt;/a> for letting me use their spectrometer to measure this data!&lt;/em>&lt;/p></content:encoded></item><item><title>Debugger Setup with GDB + OpenOCD in Visual Studio Code</title><link>https://justinmklam.com/posts/2017/10/vscode-debugger-setup/</link><pubDate>Sun, 29 Oct 2017 14:24:52 -0700</pubDate><guid>https://justinmklam.com/posts/2017/10/vscode-debugger-setup/</guid><description>&lt;p>&lt;a href="https://code.visualstudio.com/">Visual Studio Code&amp;rsquo;s&lt;/a> combination of functionality, customizability, and aesthetics makes it one of my favourite code editors. As such, I was set on making it work with embedded development since I was getting started with the STM32 line of microcontrollers. I was following the steps outlined in &lt;a href="https://leanpub.com/mastering-stm32">Mastering STM32&lt;/a> by Carmine Noviello (which is an excellent resource) until it said to use Eclipse, because life&amp;rsquo;s too short to use software with unnecessary bloat. Enter VS Code and someone wanting to use the latest and greatest in code editors.&lt;/p></description><content:encoded>&lt;p>&lt;a href="https://code.visualstudio.com/">Visual Studio Code&amp;rsquo;s&lt;/a> combination of functionality, customizability, and aesthetics makes it one of my favourite code editors. As such, I was set on making it work with embedded development since I was getting started with the STM32 line of microcontrollers. I was following the steps outlined in &lt;a href="https://leanpub.com/mastering-stm32">Mastering STM32&lt;/a> by Carmine Noviello (which is an excellent resource) until it said to use Eclipse, because life&amp;rsquo;s too short to use software with unnecessary bloat. Enter VS Code and someone wanting to use the latest and greatest in code editors.&lt;/p>
&lt;p>The only thing that was keeping me away was the lack of out-of-the-box debugging compatibility. Fortunately, with VS Code&amp;rsquo;s debugging capability and my sunk-cost pain of figuring this out, getting this workflow going is fairly straightforward!&lt;/p>
&lt;p>&lt;strong>Prerequisites&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://launchpad.net/gcc-arm-embedded">GCC ARM Embedded Tools&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gnu-mcu-eclipse.github.io/windows-build-tools/">GCC ARM Build Tools&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://openocd.org/">OpenOCD&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://www.st.com/en/development-tools/stsw-link004.html">STM32 ST-LINK Utility&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Optional&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="http://platformio.org/">STM32CubeMX&lt;/a> - For setting up projects. Now making &lt;a href="https://hackaday.com/2017/07/15/stm32cubemx-makes-makefiles/">makefiles&lt;/a>!&lt;/li>
&lt;li>&lt;a href="http://platformio.org/">PlatformIO&lt;/a> - A one-stop shop for setting up common hardware kits.&lt;/li>
&lt;/ul>
&lt;p>&lt;em>Note: The following was set up on Windows 10 for STM32 Nucleo F303K8. The Nucleo F030R8 was also tested and confirmed working with this setup.&lt;/em>&lt;/p>

&lt;h1 id="configuring-vs-code" class="anchor">
 &lt;a href="#configuring-vs-code">
 Configuring VS Code
 &lt;/a>
&lt;/h1>
&lt;p>Open the Debug panel (&lt;code>CTRL + SHIFT + D&lt;/code>) and select &amp;ldquo;Add Configuration &amp;gt; GDB&amp;rdquo; through the top left dropdown arrow. Create a GDB configuration in launch.json and add the following. Note: Change the paths in &amp;ldquo;target&amp;rdquo;, &amp;ldquo;gdbpath&amp;rdquo;, and &amp;ldquo;autorun&amp;rdquo; to the correct locations.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;GDB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;gdb&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;request&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;launch&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cwd&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;${workspaceRoot}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;target&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;${workspaceRoot}/.pioenvs/nucleo_f303k8/firmware.elf&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;gdbpath&amp;#34;&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;C:/STM32Toolchain/gcc-arm/5.4 2016q3/bin/arm-none-eabi-gdb.exe&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;autorun&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 	&lt;span class="s2">&amp;#34;target remote localhost:3333&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 	&lt;span class="s2">&amp;#34;symbol-file ./.pioenvs/nucleo_f303k8/firmware.elf&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 	&lt;span class="s2">&amp;#34;monitor reset&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/10/vscode-debugger-setup/gdb.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/10/vscode-debugger-setup/gdb.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">GDB debugger entry added after successfuly entry in launch.json file.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="starting-a-debug-session" class="anchor">
 &lt;a href="#starting-a-debug-session">
 Starting a Debug Session
 &lt;/a>
&lt;/h1>
&lt;p>Before entering debug mode (&lt;code>F5&lt;/code>), you need to launch the OpenOCD server. Open Terminal in VS Code (&lt;code>CTRL + ` &lt;/code>) and type:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">openocd -f board\st_nucleo_f3.cfg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/10/vscode-debugger-setup/debugger.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/10/vscode-debugger-setup/debugger.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Adding breakpoints and stepping through code on an STM32 through VS Code!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Unfortunately you must always start the OpenOCD server before hitting &lt;code>F5&lt;/code>, but fortunately you can just hit &lt;code>Up&lt;/code> in the terminal to recall the last command.&lt;/p>

&lt;h1 id="troubleshooting" class="anchor">
 &lt;a href="#troubleshooting">
 Troubleshooting
 &lt;/a>
&lt;/h1>
&lt;p>When running OpenOCD, the following error might come up:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Error: libusb_open() failed with LIBUSB_ERROR_NOT_SUPPORTED
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Use Zadig to upgrade ST-Link Debug to the correct WinUSB driver. Enable through Options &amp;gt; List All&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/10/vscode-debugger-setup/zadig.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/10/vscode-debugger-setup/zadig.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Fixing faulty USB drivers on Windows 10.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Happy debugging!&lt;/p></content:encoded></item><item><title>Annealing 3D Printed Plastics: Sous Vide Style</title><link>https://justinmklam.com/posts/2017/06/sous-vide-pla/</link><pubDate>Tue, 13 Jun 2017 15:56:53 -0700</pubDate><guid>https://justinmklam.com/posts/2017/06/sous-vide-pla/</guid><description>&lt;blockquote>
&lt;p>Featured on &lt;a href="http://hackaday.com/2017/06/17/annealing-plastic-for-stronger-prints/">Hackaday&lt;/a>, &lt;a href="https://3dprinting.com/filament/engineer-reveals-easy-way-strengthen-pla-annealing-heat-bath/">3D Printing.com&lt;/a>, &lt;a href="https://all3dp.com/2/annealing-pla-prints-for-strength-easy-ways/">All3DP&lt;/a>, and &lt;a href="https://www.fictiv.com/blog/posts/june-hardware-roundup">Fictiv Blog&lt;/a>. Also cited in a few research papers&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;sup>, &lt;/sup>&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/p>&lt;/blockquote>
&lt;p>Yep, you heard it right. With all the craze surrounding cooking sous vide these days, it was only a matter of time before someone decided to venture using it outside of the culinary world. Turns out that someone also had a 3D printer, and &lt;em>you won&amp;rsquo;t believe what happened next!&lt;/em>&lt;/p>
&lt;p>Click-bait headlines aside, this post is quite lengthy and not everyone may have the patience to get through it all. Feel free to read the TL;DR below, or if you&amp;rsquo;re feeling inclined, follow along my adventure through the land of 3D printing, materials science, and modern cooking.&lt;/p></description><content:encoded>&lt;blockquote>
&lt;p>Featured on &lt;a href="http://hackaday.com/2017/06/17/annealing-plastic-for-stronger-prints/">Hackaday&lt;/a>, &lt;a href="https://3dprinting.com/filament/engineer-reveals-easy-way-strengthen-pla-annealing-heat-bath/">3D Printing.com&lt;/a>, &lt;a href="https://all3dp.com/2/annealing-pla-prints-for-strength-easy-ways/">All3DP&lt;/a>, and &lt;a href="https://www.fictiv.com/blog/posts/june-hardware-roundup">Fictiv Blog&lt;/a>. Also cited in a few research papers&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;sup>, &lt;/sup>&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/p>&lt;/blockquote>
&lt;p>Yep, you heard it right. With all the craze surrounding cooking sous vide these days, it was only a matter of time before someone decided to venture using it outside of the culinary world. Turns out that someone also had a 3D printer, and &lt;em>you won&amp;rsquo;t believe what happened next!&lt;/em>&lt;/p>
&lt;p>Click-bait headlines aside, this post is quite lengthy and not everyone may have the patience to get through it all. Feel free to read the TL;DR below, or if you&amp;rsquo;re feeling inclined, follow along my adventure through the land of 3D printing, materials science, and modern cooking.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=pla-carnage.gif>&lt;img class="img-responsive img-content" src=pla-carnage.gif />&lt;/a>
 &lt;p class="caption">Measuring maximum force before material failure, recorded on an iPhone 6 at 240 fps.&lt;/p>
&lt;/div>


&lt;h1 id="the-short-version" class="anchor">
 &lt;a href="#the-short-version">
 The Short Version
 &lt;/a>
&lt;/h1>
&lt;p>Heat treatment was carried out on 3D printed parts using a temperature controlled water bath (aka sous vide) instead of being baked in an oven.&lt;/p>
&lt;p>&lt;strong>What the goal was:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>To determine if extra strength can be squeezed out of PLA filament by annealing the parts and testing how much force can be applied before the test piece breaks in half.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>How it was tested:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Printed some small test blocks (7mm x 7mm x 30mm)&lt;/li>
&lt;li>Submerged them in water at 70°C for 30 mins&lt;/li>
&lt;li>Half of the blocks cooled to room temperature in air (70°C to 18°C in &amp;lt; 10 mins), and the other half were regulated to cooled much slower (70°C to 18°C in &amp;gt; 4 hours)&lt;/li>
&lt;li>Applied a point load on the test block&lt;/li>
&lt;li>Recorded the maximum load before failure&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>What the results suggest:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Using a temperature controlled water bath provides a more stable, uniform, and controllable heat source (so parts are less prone to warping from uneven heating)&lt;/li>
&lt;li>Annealing PLA yields an increase in mechanical strength under some circumstances&lt;/li>
&lt;li>Testing was fairly inconclusive due to the inconsistency/human error in rate of applied force&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>What the takeaways are:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Annealing printed parts by sous vide is a plausible method for annealing PLA&lt;/li>
&lt;li>Further testing is required to conclusively determine the balance between post-processing time and the resulting performance gains&lt;/li>
&lt;/ul>

&lt;h1 id="the-long-version" class="anchor">
 &lt;a href="#the-long-version">
 The Long Version
 &lt;/a>
&lt;/h1>
&lt;p>Let&amp;rsquo;s say we wanted to make a highly functional part using a 3D printer. Maybe you need a replacement gear or a weight-bearing mounting bracket, and 3D printing would be the easiest way to fabricate the part.&lt;/p>
&lt;p>You carefully select your slicer settings to optimize shell thickness, infill density, and layer height based on your application. You select a material that adequately suits your needs based on material strength or flexibility and environmental factors like UV exposure and heat resistance. You even take layer geometry into consideration to maximize strength in the loading direction.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=layer-orientation.jpg>&lt;img class="img-responsive img-content" src=layer-orientation.jpg />&lt;/a>
 &lt;p class="caption">Optimal layer orientation with respect to direction of primary load.&lt;/p>
&lt;/div>

&lt;p>With all settings configured and high hopes for success, you load up the filament and hit &amp;ldquo;PRINT&amp;rdquo;. Hours later and your part is complete and, giddy with excitement, you pop it off the printer bed and finally test the part.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=broken%20fence.jpg>&lt;img class="img-responsive img-content" src=broken%20fence.jpg />&lt;/a>
 &lt;p class="caption">Fence bracket repaired (left), and broken after a heavy windstorm (right).&lt;/p>
&lt;/div>

&lt;p>But it still fails. Tears ensue. Aspirations crumble. Is there any hope for the humanity of functional, home-made, plastic designs?&lt;/p>

&lt;h2 id="the-problem-with-printed-parts" class="anchor">
 &lt;a href="#the-problem-with-printed-parts">
 The Problem With Printed Parts
 &lt;/a>
&lt;/h2>
&lt;p>3D printing shines in the rapid creation of designs with moderately complex geometries. From trinkets to tool holders and enclosures to gear trains, 3D printing has found its way into a variety of purposes. However, at the end of the day, they&amp;rsquo;re only plastic parts having limited practicality in more demanding applications.&lt;/p>
&lt;p>The inherent property of these parts is that they&amp;rsquo;re built layer upon layer, with different areas being rapidly heated and cooled at different rates. This causes internal stresses and they end up acting like perforated lines that are prone to snapping apart.&lt;/p>
&lt;p>Thankfully there&amp;rsquo;s an entire industry dedicated to squeezing every ounce of performance out of material properties, so that will be an adequate starting point. Let&amp;rsquo;s begin!&lt;/p>

&lt;h2 id="the-solution-heat-treatment" class="anchor">
 &lt;a href="#the-solution-heat-treatment">
 The Solution: Heat Treatment
 &lt;/a>
&lt;/h2>
&lt;p>In metallurgy (the study of physical and chemical behaviour of metallic elements), annealing is a heat treatment process that alters the material&amp;rsquo;s physical (and sometimes chemical) properties&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>. For common metals such as copper, steel, silver, and brass, the process looks something like:&lt;/p>
&lt;ol>
&lt;li>Heat material until glowing&lt;/li>
&lt;li>Maintain at desired (recrystallization) temperature&lt;/li>
&lt;li>Slowly let cool to room temperature&lt;/li>
&lt;/ol>
&lt;!--In more scientific terms, these three stages of annealing are known as recovery, recrystallization, and grain growth. In recovery, the material is softened to relax its internal defects in the grain structure called _dislocations_, which normally cause internal stresses. In recrystallization, new strain-free grains grow in place of the dislocations. In grain growth, the microstructure coarsens-->

&lt;h3 id="the-importance-of-reducing-internal-stresses" class="anchor">
 &lt;a href="#the-importance-of-reducing-internal-stresses">
 The Importance of Reducing Internal Stresses
 &lt;/a>
&lt;/h3>
&lt;p>Typically with any material, internal defects are evident (notably on a microscopic scale) and create internal stresses which weaken its overall strength. When creating metal parts, the initial metal-forming processes create these defects and as a result, the metal will crack under stress along these stress-forming juncture lines called &amp;ldquo;grains&amp;rdquo;.&lt;/p>
&lt;p>To minimize the effect of these grains, annealing can be done to soften the material, relax the grain structures causing the internal stresses, and allow new, strain-free grains to form as replacements.&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=annealing_prints.jpg>&lt;img class="img-responsive img-content" src=annealing_prints.jpg />&lt;/a>
 &lt;p class="caption">Diagram showing the effect of heat treatment on the material&amp;#39;s microstructure. (Source: &lt;a href=https://www.3dsourced.com/rigid-ink/how-to-anneal-your-3d-prints-for-strength/> 3DSourced&lt;/a>) &lt;/p>
&lt;/div>

&lt;p>So to recap our newfound knowledge in maximizing material performance in metals:&lt;/p>
&lt;ol>
&lt;li>Internal stresses are bad&lt;/li>
&lt;li>Internal stresses are created when a material is pushed, squeezed, and formed into a part&lt;/li>
&lt;li>Internal stresses can be reduced by reheating, softening, and re-hardening the part&lt;/li>
&lt;/ol>

&lt;h3 id="how-does-this-relate-to-our-3d-printed-plastic" class="anchor">
 &lt;a href="#how-does-this-relate-to-our-3d-printed-plastic">
 How does this relate to our 3D printed plastic?
 &lt;/a>
&lt;/h3>
&lt;p>With 3D printed parts, these internal defects occur on a more macroscopic scale. Plastic is heated, pushed through the extruder nozzle, and quickly cooled to form a layer of a printed part. Since plastic is poor conductor of heat, it cools unevenly and result in a mishmash of internal defects and grains. When an entire part is fabricated with this method, there&amp;rsquo;s really no surprise that parts usually break fairly easily! Each printed layer forms a juncture line of non-ideal bonding, and within each layer yields internal stresses due to rapid and uneven cooling.&lt;/p>

&lt;h2 id="a-review-of-current-research" class="anchor">
 &lt;a href="#a-review-of-current-research">
 A Review of Current Research
 &lt;/a>
&lt;/h2>
&lt;p>Fortunately, a similar heat treatment process can be applied to plastics to remove these nasty stresses and allow internal harmony to coalesce. I came across a research paper by Lih-Sheng Turng and Yottha Srithep, which discusses the relationship of crystallinity (ie. the degree of structural order in a solid) and mechanical properties of injection molded polylactide, commonly known as PLA&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>&amp;hellip; Or, in plain English: they took a bunch of plastic sample pieces, performed some heat treatment on them, stuck them back in an oven to see if they still deform, and measured how much better the annealed samples hold up in the heat. Let&amp;rsquo;s dig in and see what they found!&lt;/p>

&lt;h3 id="the-nitty-gritty-of-crystallinity" class="anchor">
 &lt;a href="#the-nitty-gritty-of-crystallinity">
 The Nitty Gritty of Crystallinity
 &lt;/a>
&lt;/h3>
&lt;p>To go into a bit more detail, increasing a polymer&amp;rsquo;s crystallinity is good because it can lead to an increase in stiffness, strength, heat deflection temperature, and chemical resistance. However, this is difficult to do with PLA because of its low crystallization rate and its required slow cooling rate.&lt;/p>

&lt;h3 id="the-findings" class="anchor">
 &lt;a href="#the-findings">
 The Findings
 &lt;/a>
&lt;/h3>
&lt;p>The figure below shows the outcome of non-annealed (clear, first from bottom) and annealed (at varying temperatures and times) PLA test pieces. After heat treatment, they were placed in an oven to test their heat resistance. The annealed samples held up well in comparison with the non-annealed sample, suggesting that post-molding heat treatment results in better heat resistance and potentially mechanical performance as well.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=pla-annealing-paper.jpg>&lt;img class="img-responsive img-content" src=pla-annealing-paper.jpg />&lt;/a>
 &lt;p class="caption">The first sample (non-annealed) had the lowest degree of crystallinity and highest deformation at 65°C. (Source: Turng and Srithep, 2014)&lt;/p>
&lt;/div>

&lt;p>Looking at the graph below, we see that the PLA samples had a maximum crystallinity of about 49%. Maintaining the oven/annealing temperature at 80°C led to the fastest rate of crystallization, whereas 65°C had the slowest rate.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=crystallinity%20vs%20annealing%20time.JPG>&lt;img class="img-responsive img-content" src=crystallinity%20vs%20annealing%20time.JPG />&lt;/a>
 &lt;p class="caption">Degree of crystallinity versus annealing time. (Source: Turng and Srithep, 2014)&lt;/p>
&lt;/div>

&lt;p>This shows that maximum crystallinity can be achieved even at lower temperatures, as long as the material is given enough time to sufficiently undergo recrystallization.&lt;/p>

&lt;h3 id="the-takeaway" class="anchor">
 &lt;a href="#the-takeaway">
 The Takeaway
 &lt;/a>
&lt;/h3>
&lt;p>Unfortunately, this paper only tested the heat resistance of the annealed samples, as it would have been interesting to see them evaluate other mechanical properties such as tension/compression and getting a stress/strain curve out of it all. But this at least sheds some insight on performing heat treatment on PLA; if it improves heat resistance, then it should also improve other (potentially related) mechanical properties.&lt;/p>

&lt;h2 id="a-review-of-current-methods" class="anchor">
 &lt;a href="#a-review-of-current-methods">
 A Review of Current Methods
 &lt;/a>
&lt;/h2>
&lt;p>From a cursory search in the 3D printing community, annealing PLA seems to be a common, known method in squeezing a bit of extra mechanical performance out of printed parts. YouTubers Thomas Sanlader&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup> and Joe Mike Terranella&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup> have shown both quantitative and qualitative results in strength improvements by annealing.&lt;/p>

&lt;h3 id="the-oven-bake-method" class="anchor">
 &lt;a href="#the-oven-bake-method">
 The Oven Bake Method
 &lt;/a>
&lt;/h3>
&lt;p>Thomas&amp;rsquo; approach in testing oven-baked samples was nicely scientific, and warping was shown to be an issue since ovens aren&amp;rsquo;t great at providing even, uniform heating.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=screencap-thomas-sanlader.JPG>&lt;img class="img-responsive img-content" src=screencap-thomas-sanlader.JPG />&lt;/a>
 &lt;p class="caption">Annealing various 3D printed plastics in an oven. (Source: Thomas Sanlader, YouTube)&lt;/p>
&lt;/div>


&lt;h3 id="the-boiling-water-method" class="anchor">
 &lt;a href="#the-boiling-water-method">
 The Boiling Water Method
 &lt;/a>
&lt;/h3>
&lt;p>Joe&amp;rsquo;s approach with boiling PLA was a good proof of concept, but it was only qualitative, his parts were floating in the water, and most importantly, no data was collected (savage).&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=screencap-terranella.JPG>&lt;img class="img-responsive img-content" src=screencap-terranella.JPG />&lt;/a>
 &lt;p class="caption">Boiling PLA for 10 minutes for extra strength. (Source: Joe Mike Terranella, YouTube)&lt;/p>
&lt;/div>


&lt;h3 id="areas-of-improvement" class="anchor">
 &lt;a href="#areas-of-improvement">
 Areas of Improvement
 &lt;/a>
&lt;/h3>
&lt;p>Using water as a heat source is advantageous because it provides fairly uniform heating, but temperature control is fussy to maintain a specific temperature. Ovens are convenient since it provides a (moderately) temperature controlled chamber, but heat transfer from the heating element to the part is less than ideal and still leads to uneven heating.&lt;/p>
&lt;p>If only there was a way to combine the temperature control of an oven and uniform, stable heating of a water bath&amp;hellip;&lt;/p>

&lt;h2 id="annealing-pla-with-sous-vide" class="anchor">
 &lt;a href="#annealing-pla-with-sous-vide">
 Annealing PLA with&amp;hellip; Sous Vide?
 &lt;/a>
&lt;/h2>
&lt;p>Yes, that&amp;rsquo;s right. Sous vide is the ultimate hero of this story.&lt;/p>
&lt;p>A while back, I made a &lt;a href="https://justinmklam.com/projects/elec/sous-vide/">sous vide controller&lt;/a> to get in on the cooking fad. A few months later and the novelty wore off, but I still had a modular, capable temperature controller ready for its next task (coffee roasting comes to mind, but I digress). In comes my 3D printer, and the combination of cooking and tinkering lead to the idea of performing heat treatment with a kitchen gadget.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=IMG_20170318_130307.jpg>&lt;img class="img-responsive img-content" src=IMG_20170318_130307.jpg />&lt;/a>
 &lt;p class="caption">DIY sous vide controller hooked up to a kettle.&lt;/p>
&lt;/div>

&lt;p>Ladies and gentlemen, welcome to the meat and potatoes of this post.&lt;/p>
&lt;p>To recap, we&amp;rsquo;ve learned why annealing is desirable to reduce internal stresses (ie. increase crystallization), what previous research has identified, and what current heat treatment processes have already been tried. Although the presented information has helped in answering our preliminary questions, we still have unanswered ones that are left for us to uncover and test.&lt;/p>

&lt;h3 id="the-questions" class="anchor">
 &lt;a href="#the-questions">
 The Questions
 &lt;/a>
&lt;/h3>
&lt;ol>
&lt;li>Will annealing PLA in a temperature controlled water bath improve its mechanical properties?&lt;/li>
&lt;li>What effect does layer height have on annealed parts?&lt;/li>
&lt;li>Do we really need to cool the samples slowly, or can we get away with (quicker) cooling in room temperature?&lt;/li>
&lt;/ol>

&lt;h3 id="the-setup" class="anchor">
 &lt;a href="#the-setup">
 The Setup
 &lt;/a>
&lt;/h3>
&lt;p>We can learn from Joe&amp;rsquo;s mistakes by making sure our parts are submerged to reduce any risk of non-uniform heating. I put the samples in a Ziploc bag with a few Canadian pesos to keep them underwater. Two batches of 6 samples each were cooked sous vide, one set at 0.2625 mm layer height and the other at 0.175 mm.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=IMG_20170318_183551.jpg>&lt;img class="img-responsive img-content" src=IMG_20170318_183551.jpg />&lt;/a>
 &lt;p class="caption">Deprecated pennies were used to keep the samples submerged in the temperature controlled water bath.&lt;/p>
&lt;/div>

&lt;div class="row img-captioned">
 &lt;a href=IMG_20170318_165409.jpg>&lt;img class="img-responsive img-content" src=IMG_20170318_165409.jpg />&lt;/a>
 &lt;p class="caption">Test samples lined up for carnage.&lt;/p>
&lt;/div>


&lt;h3 id="designing-the-tests" class="anchor">
 &lt;a href="#designing-the-tests">
 Designing the Tests
 &lt;/a>
&lt;/h3>
&lt;p>Wait, hold the phone: we still have many other questions! What annealing temperature is going to be maintained? How long are the samples going to be annealed for? Why are the samples so small?&lt;/p>
&lt;p>All great questions, but unfortunately not all have great answers.&lt;/p>
&lt;p>&lt;strong>Q: What annealing temperature are we going to maintain?&lt;/strong>&lt;/p>
&lt;p>PLA melts around 180-220°C, and its glass transition temperature is between 60-65°C&lt;sup id="fnref:8">&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref">8&lt;/a>&lt;/sup>. We&amp;rsquo;re interested in the latter since that&amp;rsquo;s the temperature where recrystallization occurs. However, lower quality PLA requires higher temperatures due to more impurities in the material. To be safe, we&amp;rsquo;ll set the temperature to 70°C. According to the graph of crystallinity vs annealing time (a few page scrolls above), we&amp;rsquo;ll hit the maximum 48-49% crystallinity at around 6 hours.&lt;/p>
&lt;p>&lt;strong>Q: Wait, 6 hours of annealing time? Are we really going to wait that long?&lt;/strong>&lt;/p>
&lt;p>Ain&amp;rsquo;t nobody got time for that! I&amp;rsquo;m an impatient guy, and waiting for my 3D prints to finish is painful enough. If we want to be robust in our test methods, sure we can wait 1/4 of an entire day to extract a bit more performance out of a plastic part. But I&amp;rsquo;m also a practical guy, so I want to see how little time I can get away with to achieve a meaningful increase in strength. My threshold for this is about 30 minutes; anything longer and I would question if it&amp;rsquo;s worth it for everyday printing, so we&amp;rsquo;ll go with that.&lt;/p>
&lt;p>&lt;strong>Q: Sounds good. But why are the test samples so small? Other people seem to test with much larger parts.&lt;/strong>&lt;/p>
&lt;p>Since we&amp;rsquo;re applying (what we&amp;rsquo;ll assume to be) a point force, the sample doesn&amp;rsquo;t actually need to be that long. In terms of cross-sectional area, wall thickness has a much larger impact on a part&amp;rsquo;s strength than infill. Thus, these samples were designed to be hollow with a 2.0 mm wall thickness, which is actually a reasonable thickness for standard printed parts. Since those are the criteria that needs to be met, the sample just needs to be big enough to be able to test with (ie. long enough to span the gap). And going back to my impatience, 8 minutes is about the longest I want to wait for these samples since I&amp;rsquo;ll be printing multiple of these.&lt;/p>

&lt;h3 id="the-procedure" class="anchor">
 &lt;a href="#the-procedure">
 The Procedure
 &lt;/a>
&lt;/h3>
&lt;p>Now that that&amp;rsquo;s out of the way, we can finally start testing and breaking things!&lt;/p>

&lt;h4 id="annealing-the-samples" class="anchor">
 &lt;a href="#annealing-the-samples">
 Annealing the Samples
 &lt;/a>
&lt;/h4>
&lt;ol>
&lt;li>Print 9 rectangular prisms as the test samples&lt;/li>
&lt;li>Remove 3 samples as the control set (ie. unmodified and directly off the printer)&lt;/li>
&lt;li>Fill kettle with room temperature water&lt;/li>
&lt;li>Submerge the remaining 6 samples in water bath&lt;/li>
&lt;li>Set desired temperature of water bath&lt;/li>
&lt;li>Maintain temperature for 30 mins&lt;/li>
&lt;li>Remove 3 samples (1st sous vide set) and allow to air cool at room temperature&lt;/li>
&lt;li>Turn off heat and allow the remaining 3 samples (2nd sous vide set) to slowly cool with the water bath&lt;/li>
&lt;/ol>

&lt;h4 id="breaking-the-samples" class="anchor">
 &lt;a href="#breaking-the-samples">
 Breaking the Samples
 &lt;/a>
&lt;/h4>
&lt;ol>
&lt;li>Place test jig on top of bathroom scale&lt;/li>
&lt;li>Apply vertical point force on the sample&lt;/li>
&lt;li>Record maximum load before sample catastrophically explodes&lt;/li>
&lt;/ol>
&lt;div class="row img-captioned">
 &lt;a href=IMG_20170319_164047.jpg>&lt;img class="img-responsive img-content" src=IMG_20170319_164047.jpg />&lt;/a>
 &lt;p class="caption">Overview of test setup. Camera is used to capture the scale measurement at peak force.&lt;/p>
&lt;/div>


&lt;h3 id="the-results" class="anchor">
 &lt;a href="#the-results">
 The Results
 &lt;/a>
&lt;/h3>
&lt;p>To recap our hypotheses:&lt;/p>
&lt;ol>
&lt;li>Annealing PLA in a temperature controlled water bath will promote crystallinity (and thus lower internal stress) in comparison with using conventional ovens.&lt;/li>
&lt;li>Samples printed at a 0.175 mm layer height will have higher internal stress than those at 0.2625 mm.&lt;/li>
&lt;li>Samples cooled at room temperature in air will have higher internal stress than samples cooled with the water bath.&lt;/li>
&lt;/ol>
&lt;p>The figures below tell the story. However, the results were not 100% as expected! And that&amp;rsquo;s why we test our assumptions.&lt;/p>

&lt;h4 id="measuring-the-maximum-applied-load" class="anchor">
 &lt;a href="#measuring-the-maximum-applied-load">
 Measuring the Maximum Applied Load
 &lt;/a>
&lt;/h4>
&lt;p>For the 0.2625 mm layer height, there was virtually no change between the control and annealed (sous vide, slow/fast cooled) samples. On the other hand, the samples printed at 0.175 mm demonstrated ~53% increase in resistance to shear force when compared with the non-annealed samples.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=data-layer%20height.JPG>&lt;img class="img-responsive img-content" src=data-layer%20height.JPG />&lt;/a>
 &lt;p class="caption">Max applied force before failure at 0.2625 mm and 0.175 mm layer heights.&lt;/p>
&lt;/div>

&lt;p>Averaging the data paints a clearer picture. Interestingly, the sous vide samples when cooled quicker (ie. at room temperature instead of in the water bath) showed a slightly higher maximum force. Granted, three data points is hardly enough information to make any conclusions, but it provides some indication to the characteristic trend of these scenarios.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=data-layer%20height%20avg.JPG>&lt;img class="img-responsive img-content" src=data-layer%20height%20avg.JPG />&lt;/a>
 &lt;p class="caption">Averaged data with error bars showing standard deviations.&lt;/p>
&lt;/div>


&lt;h4 id="measuring-the-changes-in-physical-dimensions" class="anchor">
 &lt;a href="#measuring-the-changes-in-physical-dimensions">
 Measuring the Changes in Physical Dimensions
 &lt;/a>
&lt;/h4>
&lt;p>One of the hypothesized benefits of annealing with sous vide instead of in an oven is the uniform temperature control and reduced risk of warping. These parts were small and relatively thick, so warping wouldn&amp;rsquo;t normally be an issue anyway, but I measured the dimensional changes in the part from heat treatment anyway.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=data-dimensional%20change.JPG>&lt;img class="img-responsive img-content" src=data-dimensional%20change.JPG />&lt;/a>
 &lt;p class="caption">Quantifying the changes in each dimension after annealing.&lt;/p>
&lt;/div>

&lt;p>We see that the Z dimension (ie. the sample height and the longest dimension) increased the most, whereas both expansion and contraction occurred with the X and Y dimensions. However, these changes result in less than 2% dimensional change (and in most cases it was less than 1.5%), which is fairly acceptable for 3D printed parts.&lt;/p>

&lt;h3 id="the-discussion" class="anchor">
 &lt;a href="#the-discussion">
 The Discussion
 &lt;/a>
&lt;/h3>
&lt;p>So after all this, what does it all mean? Like it or not, it means we don&amp;rsquo;t have a conclusive answer (but we can at least still talk about it).&lt;/p>

&lt;h4 id="different-results-for-different-layer-heights" class="anchor">
 &lt;a href="#different-results-for-different-layer-heights">
 Different Results for Different Layer Heights
 &lt;/a>
&lt;/h4>
&lt;p>This was probably the most unexpected observation. Annealing didn&amp;rsquo;t seem to have an effect on the samples printed at 0.2625 mm, but it did at 0.175 mm. What gives?&lt;/p>
&lt;p>After doing a bit of Googling, other tests have shown that larger layer heights provide greater part strength&lt;sup id="fnref:9">&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref">9&lt;/a>&lt;/sup>. 3D Matter wrote a great article on how layer height, infill percentage, and infill pattern affects the maximum strength of a printed part.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=layerheightstressstrain.png>&lt;img class="img-responsive img-content" src=layerheightstressstrain.png />&lt;/a>
 &lt;p class="caption">Detailed results showing the stress-strain curves for samples printed at varying layer heights. (Source: 3D Matter)&lt;/p>
&lt;/div>

&lt;p>Their results: test samples printed at 0.3 mm had a maximum stress of about 36 MPa, whereas the samples at 0.1 mm topped out around 29 MPa. Backing up a bit, we can safely say that a part made of solid piece of plastic would be significantly more robust than two pieces of solid plastic bonded together. Since the material itself has imperfections, adding the bonding layer creates another source of imperfection and instability. Extrapolating this to a 3D printed part with hundreds of layers, we can guess that the increase in sources of imperfection will not bode well for the part&amp;rsquo;s own well being.&lt;/p>
&lt;p>So why did only the smaller layer height benefit from annealing? My guess is that the annealing helps massage these imperfections out, so more imperfections means more room for improvement. It&amp;rsquo;s possible that with the larger layer height, my test sample was too small and short to really benefit from the annealing. That, in addition to the many sources of error in my testing (which I&amp;rsquo;ll get to later), may be the reason for the lack of improvement in mechanical performance with the 0.2625 mm parts.&lt;/p>

&lt;h4 id="slow-cooling-vs-fast-cooling" class="anchor">
 &lt;a href="#slow-cooling-vs-fast-cooling">
 Slow Cooling vs Fast Cooling
 &lt;/a>
&lt;/h4>
&lt;p>For annealing to be effective, the material typically needs to be cooled uniformly back to room temperature. If it&amp;rsquo;s cooled too rapidly, different areas of the part may cool at different rates and cause either warping or internal stress to form. To slowly cool the test samples, they were left in the water bath (with heat turned off) such that the rate of part cooling matched that of the water bath. Since water has a high heat capacity of 4.181 J/g/K (and for comparison, solid aluminum is only 0.897 J/g/K)&lt;sup id="fnref:10">&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref">10&lt;/a>&lt;/sup>, it would reach room temperature at a significantly slower rate than if the parts were just left on the counter (a difference of about &amp;lt; 10 mins versus &amp;gt; 4 hours).&lt;/p>
&lt;p>The results suggest that faster cooling yielded a greater improvement in strength over the slow cooling, which goes against the above logic. However, since the test parts were small in size and having low thermal and physical masses, the benefits of slow cooling may not be evident. Additionally, the individual 0.175 mm results for the &amp;ldquo;SV, Slow Cool&amp;rdquo; set (in turquoise) had a fairly large spread, so more data should be collected to determine if it&amp;rsquo;s actually statistically significant from the 0.2625 mm data.&lt;/p>

&lt;h4 id="the-sources-of-error" class="anchor">
 &lt;a href="#the-sources-of-error">
 The Sources of Error
 &lt;/a>
&lt;/h4>
&lt;p>During the testing process of breaking the samples, it occurred to me that I was most likely applying the force at different rates. As I got into the rhythm of testing and got a feel for when the samples failed, I likely changed the applied force profile. I would have more confidence in the results if I applied force at the same speed for all trials, but alas human error got the best of me. This was probably the greatest source of error in the testing.&lt;/p>
&lt;p>The digital bathroom scale was also not ideal since I don&amp;rsquo;t know how quickly the measurement actually updates on the screen. Since the event of failure occurs so quickly, I may have easily missed the actual maximum force. This in tandem with the inconsistency in applied force means that I&amp;rsquo;m unfortunately unable to draw meaningful conclusions from the collected data.&lt;/p>
&lt;p>Another source of error includes part to part variation on the same printer, which would be reduced by collecting more data points (ie. more than a measly 3 tests per set). The test piece also may not have been perfectly centered between the two wooden supports, which may also affect the load distribution and thus required breaking force.&lt;/p>

&lt;h3 id="future-work" class="anchor">
 &lt;a href="#future-work">
 Future Work
 &lt;/a>
&lt;/h3>
&lt;p>The silver lining to all of this are the learnings from mistakes. If I were to carry out the tests again, I would change the following:&lt;/p>
&lt;ul>
&lt;li>Apply the point load at a consistent rate (maybe attach a stepper/DC motor to drive the force down into the sample)&lt;/li>
&lt;li>Use an analog instead of digital scale, or&lt;/li>
&lt;li>Set up a dedicated load sensor in addition to displacement measurement to quantify the stress-strain characteristics&lt;/li>
&lt;/ul>

&lt;h1 id="annealing-pla-and-you" class="anchor">
 &lt;a href="#annealing-pla-and-you">
 Annealing, PLA, and You
 &lt;/a>
&lt;/h1>
&lt;p>So does this mean annealing is worth the effort? Is sous vide really necessary over a regular oven? Was this just a waste 20 minutes reading an article with inconclusive results?&lt;/p>
&lt;p>Maybe, probably, and it depends.&lt;/p>
&lt;p>As discussed earlier in this post, annealing plastic has tangible benefits in increasing its mechanical strength. However, plastic (and especially 3D printed plastic) is not the end-all material for home projects. It can only go so far, and sometimes 3D printing may not be the most suitable manufacturing method. Yes, people have printed gearboxes and mechanical vises which are undoubtedly impressive, but sometimes it&amp;rsquo;s cheaper in material cost and/or time to look at alternative manufacturing methods (or even off-the-shelf components).&lt;/p>
&lt;p>Despite sous vide being home in the kitchen, I&amp;rsquo;m still adamant that it also has a place in heat treatment applications. The benefits of having an easily regulated, uniform temperature controlled environment is advantageous. However, limitations are evident in scaling as it may not be feasible to have a large tank of heated water in industrial settings when an oven may be cheaper and achieve similar results.&lt;/p>
&lt;p>As for this excessively long article, the least you can do is learn from my mistakes and apply the knowledge to your own future projects. Hopefully you found it interesting and enjoyable to follow along this technical deep dive, and maybe even learned a thing or two in the process.&lt;/p>
&lt;p>Until next time!&lt;/p>
&lt;br>
&lt;p style="margin-bottom:-15px">&lt;em>References&lt;/em>&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://threedmedprint.biomedcentral.com/articles/10.1186/s41205-020-00062-9">Identifying a commercially-available 3D printing process that minimizes model distortion after annealing and autoclaving and the effect of steam sterilization on mechanical strength&lt;/a>, April 2020&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>&lt;a href="https://www.emerald.com/insight/content/doi/10.1108/rpj-04-2021-0090/full/html">The effect of annealing on deformation and mechanical strength of tough PLA and its application in 3D printed prosthetic sockets&lt;/a>, August 2021&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>&lt;a href="https://en.wikipedia.org/wiki/Annealing_(metallurgy)">Annealing (metallurgy)&lt;/a>, Wikipedia.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>&lt;a href="https://www.3dsourced.com/rigid-ink/how-to-anneal-your-3d-prints-for-strength/">How to Anneal Your 3D Prints for Strength&lt;/a>, 3DSourced.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>&lt;a href="http://www.4spepro.org/pdf/005392/005392.pdf">Annealing conditions for injection-molded poly(lactic acid)&lt;/a>, Plastics Research Online.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>&lt;a href="https://www.youtube.com/watch?v=CZX8eHC7fws">Bake your PLA and have it outperform everything else!&lt;/a>, Thomas Sanladerer.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>&lt;a href="https://www.youtube.com/watch?v=WmTGU3r53VU">Annealing MakerGeeks Raptor PLA - The Boil Method&lt;/a>, Joe Mike Terranella.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:8">
&lt;p>&lt;a href="http://reprap.org/wiki/PLA">PLA&lt;/a>, RepRap Wiki.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:9">
&lt;p>&lt;a href="http://my3dmatter.com/influence-infill-layer-height-pattern/">What is the influence of infill %, layer height, and infill pattern on my 3D prints?&lt;/a>, 3D Matter.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:10">
&lt;p>&lt;a href="https://en.wikipedia.org/wiki/Heat_capacity">Heat capacity&lt;/a>, Wikipedia.&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></content:encoded></item><item><title>Battery Power Protection and Regulation PCB</title><link>https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/</link><pubDate>Tue, 09 May 2017 11:30:56 -0700</pubDate><guid>https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/</guid><description>&lt;p>&lt;strong>Objective:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Design a custom PCB to protect and regulate a 14.8V LiPo battery for use with an autonomous RC car&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>12V undervoltage and 10A overcurrent protection&lt;/li>
&lt;li>8.4V, 7.4V, and 5V regulated outputs&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Main Components:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.digikey.ca/product-detail/en/linear-technology/LT6109AHMS-2-PBF/LT6109AHMS-2-PBF-ND/3844948">LT6109&lt;/a> High Side Current Sense Amplifier&lt;/li>
&lt;li>&lt;a href="https://www.digikey.ca/product-detail/en/richtek-usa-inc/RT8288AZSP/1028-1148-1-ND/3078151">RT8288AZSP&lt;/a> Synchronous Step-Down Converter&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Skills:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Reading and understanding datasheets&lt;/li>
&lt;li>Schematic capture and PCB layout in Altium Designer&lt;/li>
&lt;li>Board bring-up&lt;/li>
&lt;li>Hardware debugging&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Achievement Unlocked:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>A mechanical engineer doing electrical engineering&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Acknowledgements:&lt;/strong>&lt;/p></description><content:encoded>&lt;p>&lt;strong>Objective:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Design a custom PCB to protect and regulate a 14.8V LiPo battery for use with an autonomous RC car&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>12V undervoltage and 10A overcurrent protection&lt;/li>
&lt;li>8.4V, 7.4V, and 5V regulated outputs&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Main Components:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.digikey.ca/product-detail/en/linear-technology/LT6109AHMS-2-PBF/LT6109AHMS-2-PBF-ND/3844948">LT6109&lt;/a> High Side Current Sense Amplifier&lt;/li>
&lt;li>&lt;a href="https://www.digikey.ca/product-detail/en/richtek-usa-inc/RT8288AZSP/1028-1148-1-ND/3078151">RT8288AZSP&lt;/a> Synchronous Step-Down Converter&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Skills:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Reading and understanding datasheets&lt;/li>
&lt;li>Schematic capture and PCB layout in Altium Designer&lt;/li>
&lt;li>Board bring-up&lt;/li>
&lt;li>Hardware debugging&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Achievement Unlocked:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>A mechanical engineer doing electrical engineering&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Acknowledgements:&lt;/strong>&lt;/p>
&lt;p>This project was completed under &lt;a href="https://mistywest.com/">MistyWest&lt;/a> with the guidance of Dave MacLeod, Div Gill, and Ryan Walker.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170524_162252_hu_6f6c19a021ac1975.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170524_162252_hu_6f6c19a021ac1975.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Completed PCB in all its glory, like a newly erected city skyline.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/Altium-schematic_hu_d945d7ec43cdf011.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/Altium-schematic_hu_d945d7ec43cdf011.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Schematic capture of the designed circuit.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/Altium-front-back_hu_fb65db9d294aa0df.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/Altium-front-back_hu_fb65db9d294aa0df.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">PCB layout of top overlay (right) and bottom overlay (left).&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/Altium-capture.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/Altium-capture.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">3D render of PCB in Altium Designer.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170519_154141-2_hu_e634526ea250e980.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170519_154141-2_hu_e634526ea250e980.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Boards ordered and received from OSH Park.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170523_164839_hu_e4335a283145d9b1.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170523_164839_hu_e4335a283145d9b1.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Stencil cut out and prepped for applying the solder paste.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170523_165130-2_hu_6074dd915937168b.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170523_165130-2_hu_6074dd915937168b.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Solder paste applied and components placed.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170523_174939_hu_a4ee099418df52c1.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170523_174939_hu_a4ee099418df52c1.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">PCB cooking in the reflow oven.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170525_123943_hu_ad94d5cd8f2f77ac.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/mistywest-mvs-pcb/IMG_20170525_123943_hu_ad94d5cd8f2f77ac.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">After much debugging, the board finally works! Red LED indicates protection circuit is active; Green LED indicates each regulated output is active. Multimeter shown is connected to the 8.4V output.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>Overhead Robotic Gantry for Tethered VR Headsets</title><link>https://justinmklam.com/posts/2017/04/vr-gantry/</link><pubDate>Tue, 04 Apr 2017 09:26:50 -0800</pubDate><guid>https://justinmklam.com/posts/2017/04/vr-gantry/</guid><description>&lt;h1 id="project-overview" class="anchor">
 &lt;a href="#project-overview">
 Project Overview
 &lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Objective:&lt;/strong> Create an autonomous gantry to follow the HTC Vive headset around, keeping its cable behind the user at all times.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> An extravagant party prop for an evening at CES 2017, hosted by MistyWest.&lt;/p>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>CoreXY planar gantry design&lt;/li>
&lt;li>System built with 8020 aluminum extrusions and laser cut acrylic components&lt;/li>
&lt;li>Stepper motor control through Teensy 3.2&lt;/li>
&lt;li>HTC Vive pose tracking through C++&lt;/li>
&lt;li>Patent pending&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Skills:&lt;/strong>&lt;/p></description><content:encoded>
&lt;h1 id="project-overview" class="anchor">
 &lt;a href="#project-overview">
 Project Overview
 &lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Objective:&lt;/strong> Create an autonomous gantry to follow the HTC Vive headset around, keeping its cable behind the user at all times.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> An extravagant party prop for an evening at CES 2017, hosted by MistyWest.&lt;/p>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>CoreXY planar gantry design&lt;/li>
&lt;li>System built with 8020 aluminum extrusions and laser cut acrylic components&lt;/li>
&lt;li>Stepper motor control through Teensy 3.2&lt;/li>
&lt;li>HTC Vive pose tracking through C++&lt;/li>
&lt;li>Patent pending&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Skills:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Mechanical design with rapid prototyping methods&lt;/li>
&lt;li>C++ software development&lt;/li>
&lt;li>Arduino-based firmware development&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Sources:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/MistyWestAdmin">MistyWest Github&lt;/a>&lt;/li>
&lt;li>Medium blog post titled &lt;a href="https://medium.com/@mistywest/tired-of-cables-in-virtual-reality-we-are-too-efeab5606bf0">&lt;em>Tired of cables in VR? We are too.&lt;/em>&lt;/a>&lt;/li>
&lt;li>CES Founders and Friends 2017 Photo Gallery by &lt;a href="https://natalialeva.shootproof.com/gallery/3918977/home">Natalia Leva&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Acknowledgements:&lt;/strong>&lt;/p>
&lt;p>This project was completed under &lt;a href="https://mistywest.com/">MistyWest&lt;/a> with the help of:&lt;/p>
&lt;ul>
&lt;li>Derek Disanjh - &lt;em>Project Supervisor&lt;/em>&lt;/li>
&lt;li>Div Gill - &lt;em>Firmware Advisor&lt;/em>&lt;/li>
&lt;li>Ryan Walker - &lt;em>Software Advisor&lt;/em>&lt;/li>
&lt;li>Denis Godin - &lt;em>Filming&lt;/em>&lt;/li>
&lt;li>Madison Reid - &lt;em>Video Editing&lt;/em>&lt;/li>
&lt;/ul>

&lt;div class="img-content video-container">
 &lt;iframe src=https://www.youtube.com/embed/zULBxDJVaHs>&lt;/iframe>
&lt;/div>
&lt;p class="caption">Demo video of the gantry in action.&lt;/p>

&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/04/vr-gantry/DSC1764_hu_edd64788d5b23a5b.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/04/vr-gantry/DSC1764_hu_edd64788d5b23a5b.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The gantry being used at a penthouse party hosted by MistyWest during CES 2017. (Photo by Natalia Leva)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="the-development-story" class="anchor">
 &lt;a href="#the-development-story">
 The Development Story
 &lt;/a>
&lt;/h1>
&lt;p>We wanted a solution to increase and maintain presence in virtual reality; having to worry about tripping over the cable takes away from it. What we needed was a way to mimic a cable sherpa, following our every movement to prevent tension in the cable and entanglement around our legs. Whether we were moving forward, backward, left, right, or turning around, we wanted a way to roam freely as if virtual reality was already wireless.&lt;/p>

&lt;h2 id="design-and-testing" class="anchor">
 &lt;a href="#design-and-testing">
 Design and Testing
 &lt;/a>
&lt;/h2>
&lt;p>We wanted to make this as quickly as possible, so I designed the rig using off-the-shelf parts and laser cut acrylic parts.&lt;/p>

&lt;div class="img-content video-container">
 &lt;iframe src=https://sketchfab.com/models/3be6275ae3c048098c2c777d7817ff26/embed?autostart&amp;#61;0&amp;amp;amp;preload&amp;#61;1>&lt;/iframe>
&lt;/div>
&lt;p class="caption">The gantry frame designed in SolidWorks 2017.&lt;/p>

&lt;p>Preliminary tests were less than stellar. I initially used relative position commands, but as the clip below shows, but it wasn&amp;rsquo;t responsive enough for practical use.&lt;/p>

&lt;div class="img-content video-container">
 &lt;iframe src=https://gfycat.com/ifr/AcademicDizzyJay>&lt;/iframe>
&lt;/div>
&lt;p class="caption">Initial testing of the tracking using relative position commands, almost as if it has a mind of its own.&lt;/p>

&lt;p>Changing the motor control to use absolute coordinates instead of relative showed promising results.&lt;/p>

&lt;div class="img-content video-container">
 &lt;iframe src=https://gfycat.com/ifr/SparsePassionateLaughingthrush>&lt;/iframe>
&lt;/div>
&lt;p class="caption">Revised tracking using absolute position commands for significantly improved precision.&lt;/p>

&lt;p>An overview of the software algorithm to parse the user&amp;rsquo;s current position and command the stepper motors is shown in the flowchart below.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/04/vr-gantry/SoftwareFlowchart.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/04/vr-gantry/SoftwareFlowchart.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Flowchart of the software algorithm.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="user-testing" class="anchor">
 &lt;a href="#user-testing">
 User Testing
 &lt;/a>
&lt;/h2>
&lt;p>The results were so promising, in fact, that we decided to strap it to the ceiling and do some user testing. The results were&amp;hellip; Loud due to the rattling and vibrations from the stepper motors. System responsiveness was slow, so more work needed to be done.&lt;/p>

&lt;div class="img-content video-container">
 &lt;iframe src=https://gfycat.com/ifr/FairPoisedArizonaalligatorlizard>&lt;/iframe>
&lt;/div>
&lt;p class="caption">First user test with the gantry attached to the lab ceiling (rotation tracking not yet implemented).&lt;/p>

&lt;p>Many revisions later, and the rig was finally working as expected!&lt;/p>

&lt;div class="img-content video-container">
 &lt;iframe src=https://gfycat.com/ifr/DesertedHospitableEwe>&lt;/iframe>
&lt;/div>
&lt;p class="caption">After many hardware, software, and firmware tweaks, the gantry finally became usable.&lt;/p>

&lt;!--![Achievement unlocked: Freedom of movement with wired VR.](vr-gantry.gif)-->

&lt;h2 id="making-it-portable" class="anchor">
 &lt;a href="#making-it-portable">
 Making it &amp;ldquo;Portable&amp;rdquo;
 &lt;/a>
&lt;/h2>
&lt;p>Since this was an internal project with &lt;a href="https://mistywest.com/">MistyWest&lt;/a>, the value was in showing this prototype around. The social media content was one thing, but physically bringing it around was thought to have additional &amp;ldquo;wow&amp;rdquo; factors. Since CES 2017 was coming up and MistyWest would be hosting a penthouse party during one of the evenings, it was time to make this thing free standing.&lt;/p>
&lt;p>Continuing the theme with 8020 extrusions, the external frame was assembled and tested.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/04/vr-gantry/IMG_20161212_141058_hu_43098d1553200754.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/04/vr-gantry/IMG_20161212_141058_hu_43098d1553200754.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Construction of the free standing frame in preparation for bringing the rig to CES.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/04/vr-gantry/IMG_20161216_161302_hu_77b19dfdab9d495a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/04/vr-gantry/IMG_20161216_161302_hu_77b19dfdab9d495a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Testing the free standing setup to gain confidence in its sturdiness.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/04/vr-gantry/IMG_20161222_114418_hu_a7a55229adf0d2e1.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/04/vr-gantry/IMG_20161222_114418_hu_a7a55229adf0d2e1.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Yes, it is in fact portable!&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="mission-complete" class="anchor">
 &lt;a href="#mission-complete">
 Mission Complete
 &lt;/a>
&lt;/h2>
&lt;p>On January 6, 2017, the rig was set up in Las Vegas and (after some headache and remote debugging) the rig came alive at the &lt;a href="https://mistywest.com/founders-friends-2017/">Founders and Friends 2017&lt;/a>. It was a huge hit amongst the tech enthusiasts and ran beautifully through the night.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/04/vr-gantry/DSC1873_hu_4644b4010771e7de.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/04/vr-gantry/DSC1873_hu_4644b4010771e7de.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">People showing interest in the high-tech marriage between robotics and virtual reality. (Photo by Natalia Leva)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/04/vr-gantry/DSC1658_hu_656caf8619f19232.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/04/vr-gantry/DSC1658_hu_656caf8619f19232.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Founders and Friends 2017. (Photo by Natalia Leva)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The rig is now retired and sleeps soundly in its storage container. On to the next project!&lt;/p></content:encoded></item><item><title>Hakko-Style Solder Fume Extractor</title><link>https://justinmklam.com/posts/2017/03/solder-fume-extractor/</link><pubDate>Sat, 25 Mar 2017 21:14:29 -0700</pubDate><guid>https://justinmklam.com/posts/2017/03/solder-fume-extractor/</guid><description>&lt;p>I&amp;rsquo;ve been slowly getting into the land of electrical pixies. So now, not only will I be inhaling sawdust, I can also fill them with leaded solder (because we all know that the performance of leaded solder takes higher priority than the safety of lead-free solder)! However, because I still value living, I figured I should get something to help get the solder fumes away from my lungs.&lt;/p>
&lt;p>At my current job with MistyWest, we have a nice, bench top solder fume extractor by Hakko. But I&amp;rsquo;m cheap, so heck no was I going to &lt;a href="https://www.digikey.ca/product-detail/en/american-hakko-products-inc/FA400-04/1691-1039-ND/6228795">pay over $100&lt;/a> for blowing away fumes when I can blow them away myself! Despite it being a nifty design with multiple configurations (shown in the images below), I would resist the temptation to lighten my wallet on this. Oh wait, I just bought a thing that I can use to make other things&amp;hellip;&lt;/p></description><content:encoded>&lt;p>I&amp;rsquo;ve been slowly getting into the land of electrical pixies. So now, not only will I be inhaling sawdust, I can also fill them with leaded solder (because we all know that the performance of leaded solder takes higher priority than the safety of lead-free solder)! However, because I still value living, I figured I should get something to help get the solder fumes away from my lungs.&lt;/p>
&lt;p>At my current job with MistyWest, we have a nice, bench top solder fume extractor by Hakko. But I&amp;rsquo;m cheap, so heck no was I going to &lt;a href="https://www.digikey.ca/product-detail/en/american-hakko-products-inc/FA400-04/1691-1039-ND/6228795">pay over $100&lt;/a> for blowing away fumes when I can blow them away myself! Despite it being a nifty design with multiple configurations (shown in the images below), I would resist the temptation to lighten my wallet on this. Oh wait, I just bought a thing that I can use to make other things&amp;hellip;&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/solder-fume-extractor/hakko-1.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/solder-fume-extractor/hakko-1.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Hakko solder fume extractor.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/solder-fume-extractor/hakko-2_hu_ff75c09529801382.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/solder-fume-extractor/hakko-2_hu_ff75c09529801382.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Look at all the lead I won&amp;rsquo;t be inhaling!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Taking the Hakko FA400-04 as a (heavily-inspired) design reference, I set off to make my own. Given my limited desk size and work area, I wanted to make it small and flexible. I had a 12V 40x40mm fan from eBay kicking around, which fortunately fit my needs quite well. See below for detailed images of the final product.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/solder-fume-extractor/1-LmEO3z2_hu_a9c3c9cc61ff3bc0.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/solder-fume-extractor/1-LmEO3z2_hu_a9c3c9cc61ff3bc0.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Image &lt;em>kind of&lt;/em> showing it working. Configuration 1: Upright&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/solder-fume-extractor/2-czUZR3J_hu_516671e3fe1f80e1.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/solder-fume-extractor/2-czUZR3J_hu_516671e3fe1f80e1.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Configuration 2: Low profile airflow&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/solder-fume-extractor/3-HPpDgDG_hu_59e07e8aa93be4b7.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/solder-fume-extractor/3-HPpDgDG_hu_59e07e8aa93be4b7.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Configuration 3: Third hand&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/solder-fume-extractor/4-nTKV4GM_hu_27b4e74c5d57db92.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/solder-fume-extractor/4-nTKV4GM_hu_27b4e74c5d57db92.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Carbon filters cut to size.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/solder-fume-extractor/5-4x2jB3f_hu_74d4d116691152f7.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/solder-fume-extractor/5-4x2jB3f_hu_74d4d116691152f7.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Pseudo-exploded view of assembly.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/solder-fume-extractor/6-bLHzYjo_hu_5723e2871dcb4742.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/solder-fume-extractor/6-bLHzYjo_hu_5723e2871dcb4742.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Uses M3 fasteners and a 40x40mm 12V fan.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>It may not be the greatest fume extractor out there, but at least it works. Maybe I&amp;rsquo;ll make another version which has a proper power connector and its own supply. Maybe I&amp;rsquo;ll also add some LED strips to help illuminate my work area. Maybe I&amp;rsquo;ll even add wifi to it, because you know, the Internet of Things and everybody&amp;rsquo;s doing it.&lt;/p>
&lt;p>Kidding.&lt;/p></content:encoded></item><item><title>Resources for 3D Printing with the MP Select Mini</title><link>https://justinmklam.com/posts/2017/03/3d-printing/</link><pubDate>Thu, 23 Mar 2017 19:38:27 -0700</pubDate><guid>https://justinmklam.com/posts/2017/03/3d-printing/</guid><description>&lt;p>What&amp;rsquo;s better than an inexpensive 3D printer? Free sources of information! The items below are a resource list for 3D printing with the Monoprice Select Mini, all thanks to a lively community of users around the interwebz.&lt;/p>

&lt;h1 id="general-info" class="anchor">
 &lt;a href="#general-info">
 General Info
 &lt;/a>
&lt;/h1>
&lt;ul>
&lt;li>Product Page:
&lt;ul>
&lt;li>&lt;a href="https://www.monoprice.com/product?p_id=15365">Monoprice Select Mini V1&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Official User Manual:
&lt;ul>
&lt;li>&lt;a href="15365_Manual_170509.pdf">MP Select Mini User Manual&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Reddit:
&lt;ul>
&lt;li>&lt;a href="https://www.reddit.com/r/MPSelectMiniOwners/">/r/MPSelectMiniOwners&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Facebook Group:
&lt;ul>
&lt;li>&lt;a href="https://www.facebook.com/groups/1717306548519045/">MP Mini User Group&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Unofficial Wiki:
&lt;ul>
&lt;li>&lt;a href="http://mpselectmini.com/start">MP Select Mini Wiki&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Community Knowledge Base:
&lt;ul>
&lt;li>&lt;a href="https://docs.google.com/document/d/1HJaLIcUD4oiIUYu6In7Bxf7WxAOiT3n48RvOe5pvSHk/edit">Google Doc&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>

&lt;h1 id="materials-and-accessories" class="anchor">
 &lt;a href="#materials-and-accessories">
 Materials and Accessories
 &lt;/a>
&lt;/h1>
&lt;ul>
&lt;li>Tested PLA Brands:
&lt;ul>
&lt;li>&lt;a href="https://www.amazon.ca/s/ref=bl_dp_s_web_3006902011?ie=UTF8&amp;amp;node=3006902011&amp;amp;field-brandtextbin=HATCHBOX">Hatchbox&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.amazon.ca/s/ref=bl_dp_s_web_667823011?ie=UTF8&amp;amp;node=667823011&amp;amp;field-brandtextbin=AMZ3D">AMZ3D&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Alternative Bed Surface:
&lt;ul>
&lt;li>&lt;a href="https://www.amazon.ca/gp/product/B0013HKZTA/ref=oh_aui_detailpage_o00_s00?ie=UTF8&amp;amp;psc=1">PEI Sheet&lt;/a>, see RepRap Wiki for &lt;a href="http://reprap.org/wiki/PEI_build_surface">details&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>

&lt;h1 id="initial-set-up" class="anchor">
 &lt;a href="#initial-set-up">
 Initial Set Up
 &lt;/a>
&lt;/h1>
&lt;ul>
&lt;li>CAD Modeling Software (free for hobbyists and enthusiasts):
&lt;ul>
&lt;li>&lt;a href="https://www.autodesk.com/products/fusion-360/overview">Autodesk Fusion 360&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Slicing Software:
&lt;ul>
&lt;li>&lt;a href="https://ultimaker.com/en/products/cura-software">Ultimaker Cura&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Onboard WiFi for wireless printing:
&lt;ul>
&lt;li>&lt;a href="http://mpselectmini.com/wifi/g-code_file">Connecting to WiFi through G-code&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Using Raspberry Pi and Octoprint for wireless printing:
&lt;ul>
&lt;li>&lt;a href="http://octoprint.org/download/">OctoPi&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>

&lt;h1 id="machine-settings" class="anchor">
 &lt;a href="#machine-settings">
 Machine Settings
 &lt;/a>
&lt;/h1>

&lt;h2 id="general" class="anchor">
 &lt;a href="#general">
 General
 &lt;/a>
&lt;/h2>
&lt;p>The machine settings below are based off the ones in the official user manual. However, the start and end gcode was taken from the community-driven Google Doc (see above) and slightly modified to include an initial nozzle wipe/primer and to remove the delay in turning the fans off.&lt;/p></description><content:encoded>&lt;p>What&amp;rsquo;s better than an inexpensive 3D printer? Free sources of information! The items below are a resource list for 3D printing with the Monoprice Select Mini, all thanks to a lively community of users around the interwebz.&lt;/p>

&lt;h1 id="general-info" class="anchor">
 &lt;a href="#general-info">
 General Info
 &lt;/a>
&lt;/h1>
&lt;ul>
&lt;li>Product Page:
&lt;ul>
&lt;li>&lt;a href="https://www.monoprice.com/product?p_id=15365">Monoprice Select Mini V1&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Official User Manual:
&lt;ul>
&lt;li>&lt;a href="15365_Manual_170509.pdf">MP Select Mini User Manual&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Reddit:
&lt;ul>
&lt;li>&lt;a href="https://www.reddit.com/r/MPSelectMiniOwners/">/r/MPSelectMiniOwners&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Facebook Group:
&lt;ul>
&lt;li>&lt;a href="https://www.facebook.com/groups/1717306548519045/">MP Mini User Group&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Unofficial Wiki:
&lt;ul>
&lt;li>&lt;a href="http://mpselectmini.com/start">MP Select Mini Wiki&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Community Knowledge Base:
&lt;ul>
&lt;li>&lt;a href="https://docs.google.com/document/d/1HJaLIcUD4oiIUYu6In7Bxf7WxAOiT3n48RvOe5pvSHk/edit">Google Doc&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>

&lt;h1 id="materials-and-accessories" class="anchor">
 &lt;a href="#materials-and-accessories">
 Materials and Accessories
 &lt;/a>
&lt;/h1>
&lt;ul>
&lt;li>Tested PLA Brands:
&lt;ul>
&lt;li>&lt;a href="https://www.amazon.ca/s/ref=bl_dp_s_web_3006902011?ie=UTF8&amp;amp;node=3006902011&amp;amp;field-brandtextbin=HATCHBOX">Hatchbox&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.amazon.ca/s/ref=bl_dp_s_web_667823011?ie=UTF8&amp;amp;node=667823011&amp;amp;field-brandtextbin=AMZ3D">AMZ3D&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Alternative Bed Surface:
&lt;ul>
&lt;li>&lt;a href="https://www.amazon.ca/gp/product/B0013HKZTA/ref=oh_aui_detailpage_o00_s00?ie=UTF8&amp;amp;psc=1">PEI Sheet&lt;/a>, see RepRap Wiki for &lt;a href="http://reprap.org/wiki/PEI_build_surface">details&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>

&lt;h1 id="initial-set-up" class="anchor">
 &lt;a href="#initial-set-up">
 Initial Set Up
 &lt;/a>
&lt;/h1>
&lt;ul>
&lt;li>CAD Modeling Software (free for hobbyists and enthusiasts):
&lt;ul>
&lt;li>&lt;a href="https://www.autodesk.com/products/fusion-360/overview">Autodesk Fusion 360&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Slicing Software:
&lt;ul>
&lt;li>&lt;a href="https://ultimaker.com/en/products/cura-software">Ultimaker Cura&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Onboard WiFi for wireless printing:
&lt;ul>
&lt;li>&lt;a href="http://mpselectmini.com/wifi/g-code_file">Connecting to WiFi through G-code&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Using Raspberry Pi and Octoprint for wireless printing:
&lt;ul>
&lt;li>&lt;a href="http://octoprint.org/download/">OctoPi&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>

&lt;h1 id="machine-settings" class="anchor">
 &lt;a href="#machine-settings">
 Machine Settings
 &lt;/a>
&lt;/h1>

&lt;h2 id="general" class="anchor">
 &lt;a href="#general">
 General
 &lt;/a>
&lt;/h2>
&lt;p>The machine settings below are based off the ones in the official user manual. However, the start and end gcode was taken from the community-driven Google Doc (see above) and slightly modified to include an initial nozzle wipe/primer and to remove the delay in turning the fans off.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/3d-printing/machine-settings.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/3d-printing/machine-settings.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Machine settings for MP Select Mini.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>For the lazy, you can simply copy and paste the code boxes below (ignore the End Gcode in the image above).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">;; Start Gcode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G21 ;metric values&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G90 ;absolute positioning&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">M82 ;set extruder to absolute mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">M107 ;start with the fan off&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G28 X0 Y0 ;move X/Y to min endstops&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G28 Z0 ;move Z to min endstops&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G1 Z15.0 F9000 ;move the platform down 15mm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G92 E0 ;zero the extruded length&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G1 F200 E3 ;extrude 3mm of feed stock&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G92 E0 ;zero the extruded length again&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G1 F9000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">;Put printing message on LCD screen&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">M117 Printing...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">;; End Gcode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">M104 S0 ; turn off hotend heater&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">M140 S0 ; turn off bed heater&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G91 ; Switch to use Relative Coordinates&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G1 E-2 F300 ; retract the filament a bit before lifting the nozzle to release some of the pressure&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G1 Z1 ; raise Z 1mm from current position&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G1 E-2 F300 ; retract filament even more&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G90 ; Switch back to using Absolute Coordinates&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G1 X20 ; move X axis close to tower but hopefully far enough to keep the fan from rattling&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G1 Y115 ; move bed forward for easier part removal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">M84 ; disable motors&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">G4 S600 ; keep fan running for 600 seconds to cool hotend and allow the fan to be turned off&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">M107 ; turn off fan&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="octoprint" class="anchor">
 &lt;a href="#octoprint">
 OctoPrint
 &lt;/a>
&lt;/h2>
&lt;p>To connect OctoPrint through Cura, go to &amp;ldquo;Settings &amp;gt; Printer &amp;gt; Manage Printers&amp;rdquo;, select your printer from the list, then click the &amp;ldquo;Connect OctoPrint&amp;rdquo; button on the right side of the menu.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/3d-printing/octopi-settings.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/3d-printing/octopi-settings.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Setting up OctoPrint through Cura.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>This way, you can send gcode files directly from Cura to Octoprint with a click of a button!&lt;/p>

&lt;h1 id="print-settings" class="anchor">
 &lt;a href="#print-settings">
 Print Settings
 &lt;/a>
&lt;/h1>
&lt;p>When fiddling with layer heights, it&amp;rsquo;s recommended to use the optimal values below:&lt;/p>
&lt;ul>
&lt;li>0.04375 (results may vary)*&lt;/li>
&lt;li>0.0875&lt;/li>
&lt;li>0.13125&lt;/li>
&lt;li>0.175&lt;/li>
&lt;li>0.21875&lt;/li>
&lt;li>0.2625&lt;/li>
&lt;li>0.30625&lt;/li>
&lt;/ul>
&lt;p>Numbers courtesy of Michael O&amp;rsquo;Brien on &lt;a href="https://hackaday.io/project/12696-monoprice-select-mini-electro-mechanical-upgrades">Hackaday&lt;/a>. Explanation:&lt;/p>
&lt;blockquote>
&lt;p>So that motor [Z-Axis] is a 7.5°, 48 step motor as I just listed. Since the motor is attached to a M4 rod, which has a 0.7 mm thread pitch, then in one revolution makes the Z-Axis travel up or down 0.7 mm. Since it took 48 steps to turn that rev, each step is 0.0014583&amp;hellip; mm. To avoid rounding errors, you can use multiple of 3 of this number, which is a nice and pretty 0.04375 mm. That is a nice and handy number that effectively represents the layer heights that mathematically work the best for layer heights for this printer.&lt;/p>&lt;/blockquote>
&lt;p>The settings below should serve as a decent starting point in dialing in your own print settings for PLA.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/3d-printing/pla-settings.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/3d-printing/pla-settings.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">PLA material settings.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="design-guidelines" class="anchor">
 &lt;a href="#design-guidelines">
 Design Guidelines
 &lt;/a>
&lt;/h1>
&lt;p>Want parts to fit together? 0.25mm is usually works well.





&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/3d-printing/tolerance.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/3d-printing/tolerance.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">For mating parts, a general guideline of 0.25mm is sufficient.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="troubleshooting" class="anchor">
 &lt;a href="#troubleshooting">
 Troubleshooting
 &lt;/a>
&lt;/h1>

&lt;h2 id="hole-features-on-first-layer-not-adhering" class="anchor">
 &lt;a href="#hole-features-on-first-layer-not-adhering">
 Hole Features on First Layer Not Adhering
 &lt;/a>
&lt;/h2>
&lt;p>Make the following setting modifications, in descending order of preference (I try to avoid using rafts to reduce print time and material waste).&lt;/p>
&lt;ul>
&lt;li>Slow down print speed to ~10mm/s&lt;/li>
&lt;li>Increase build plate temperature and/or use external adhesion methods (ie. glue/hairspray)&lt;/li>
&lt;li>Increase initial layer height such that more material sticks to the bed&lt;/li>
&lt;li>Disable fans for initial layer&lt;/li>
&lt;li>Use raft&lt;/li>
&lt;/ul>

&lt;h2 id="stringy-parts" class="anchor">
 &lt;a href="#stringy-parts">
 Stringy Parts
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>Fiddle with retraction distance and speed (start with 4.5mm and 40mm/s)&lt;/li>
&lt;/ul>
&lt;p>&lt;em>Last edited: February 10, 2018&lt;/em>&lt;/p></content:encoded></item><item><title>New Toy Tuesday: Monoprice Select Mini 3D Printer</title><link>https://justinmklam.com/posts/2017/03/mp-select-mini/</link><pubDate>Tue, 07 Mar 2017 15:56:53 -0700</pubDate><guid>https://justinmklam.com/posts/2017/03/mp-select-mini/</guid><description>&lt;p>I&amp;rsquo;ve been bit by the 3D printing bug.&lt;/p>
&lt;p>Like any tech-enthusiast, I have toyed with the idea of owning a 3D printer for quite some time. For my final year mechatronics design project, my partner and I designed an extravagent contraption to measure the yolk done-ness of a soft-boiled egg, which we called &lt;a href="http://justinmklam.com/projects/mecha/perfeggct/">The PerfEGGct&lt;/a>. We borrowed my roommate&amp;rsquo;s 3D printer to create the enclosure, and it turned out to be an indispensable tool during our prototyping process. At the time, reliable 3D printers were upwards of $500 which was just out of my threshold for nice-to-have gadgets.&lt;/p></description><content:encoded>&lt;p>I&amp;rsquo;ve been bit by the 3D printing bug.&lt;/p>
&lt;p>Like any tech-enthusiast, I have toyed with the idea of owning a 3D printer for quite some time. For my final year mechatronics design project, my partner and I designed an extravagent contraption to measure the yolk done-ness of a soft-boiled egg, which we called &lt;a href="http://justinmklam.com/projects/mecha/perfeggct/">The PerfEGGct&lt;/a>. We borrowed my roommate&amp;rsquo;s 3D printer to create the enclosure, and it turned out to be an indispensable tool during our prototyping process. At the time, reliable 3D printers were upwards of $500 which was just out of my threshold for nice-to-have gadgets.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_0407_hu_2bb03eb74241c790.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_0407_hu_2bb03eb74241c790.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Your everyday solution for perfect soft-boiled eggs: The PerfEGGct&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Fast forward to today, and we have the &lt;a href="https://www.monoprice.com/product?p_id=15365">Monoprice Select Mini 3D printer&lt;/a>. This bad boy is a mere $200 USD, putting it right into impulse buy territory. This price range of sub-$500 printers typically puts you with Chinese clones that are inconsistent, unreliable, and dangerous (no thermal regulation = setting fire to your house/rental suite). However, Monoprice is a reputable company in the electronics industry, putting ease to the reliability concern. More notably, just look at the thing: sheet metal enclosure and no assembly required. No muss, no fuss. At the time of writing, this is the best entry-level 3D printer due to its great price to performance ratio.&lt;/p>
&lt;p>Let&amp;rsquo;s get printing! See below for a collection of printed models off &lt;a href="https://www.thingiverse.com/">Thingiverse&lt;/a>.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170310_082445_hu_af892ba80037ba05.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170310_082445_hu_af892ba80037ba05.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Calibration cube to test dimensional accuracy. A little off of 20mm due to bulging at the corners.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170306_163656_hu_b192daf797237d15.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170306_163656_hu_b192daf797237d15.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Overhang test looking good.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170306_211119_hu_736652a2a8ab0ba6.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170306_211119_hu_736652a2a8ab0ba6.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Pebble charging stand.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170306_211720_hu_ad2ee737130a4c42.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170306_211720_hu_ad2ee737130a4c42.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Zoomed in on the base of the stand. Quality is quite impressive.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170306_215542_hu_997e7a7fb8cc1573.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170306_215542_hu_997e7a7fb8cc1573.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Hard drive cable organizer.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170308_230642_hu_72367cd84e634b02.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170308_230642_hu_72367cd84e634b02.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Raspberry Pi case.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170314_193727_hu_2c73743d04fc0010.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170314_193727_hu_2c73743d04fc0010.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Fancy desk organizer.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170317_170438_hu_2ac8c874da4ed09d.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170317_170438_hu_2ac8c874da4ed09d.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Fancy desk organizer: Nerdy style.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170314_212438_hu_5d519d5fe045d7ad.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170314_212438_hu_5d519d5fe045d7ad.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Extravagant business card dispenser&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170325_130430_hu_84cba6f95c26e5e3.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/03/mp-select-mini/IMG_20170325_130430_hu_84cba6f95c26e5e3.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Stylus holder for Surface Pro 4 pen.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Overall, I&amp;rsquo;m quite impressed with this little guy. Setting up was a breeze, and it printed well right out of the box. Fun times ahead with this 3D printer.&lt;/p></content:encoded></item><item><title>ESP8266 Sous Vide Controller</title><link>https://justinmklam.com/posts/2017/05/sous-vide-controller/</link><pubDate>Sun, 05 Feb 2017 12:28:27 -0800</pubDate><guid>https://justinmklam.com/posts/2017/05/sous-vide-controller/</guid><description>&lt;h1 id="project-summary" class="anchor">
 &lt;a href="#project-summary">
 Project Summary
 &lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Objective:&lt;/strong> Create a small, modular controller to regulate the temperature of a water bath.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> To get in on this cooking fad without dropping fat stacks of cash on an immersion circulator.&lt;/p>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Crisp 0.96&amp;quot; OLED display&lt;/li>
&lt;li>Pushbutton rotary encoder provides simple user interaction&lt;/li>
&lt;li>Removable temperature plug through standard 3-pos audio connector&lt;/li>
&lt;li>Temperature controlled outlet to be used with any heating element (ie. rice cooker, slow cooker, etc.)&lt;/li>
&lt;li>Always on outlet for water circulator&lt;/li>
&lt;li>Temperature logging over wifi&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Source:&lt;/strong> &lt;a href="https://github.com/justinmklam/sous-vide">Github&lt;/a>&lt;/p></description><content:encoded>
&lt;h1 id="project-summary" class="anchor">
 &lt;a href="#project-summary">
 Project Summary
 &lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Objective:&lt;/strong> Create a small, modular controller to regulate the temperature of a water bath.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> To get in on this cooking fad without dropping fat stacks of cash on an immersion circulator.&lt;/p>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Crisp 0.96&amp;quot; OLED display&lt;/li>
&lt;li>Pushbutton rotary encoder provides simple user interaction&lt;/li>
&lt;li>Removable temperature plug through standard 3-pos audio connector&lt;/li>
&lt;li>Temperature controlled outlet to be used with any heating element (ie. rice cooker, slow cooker, etc.)&lt;/li>
&lt;li>Always on outlet for water circulator&lt;/li>
&lt;li>Temperature logging over wifi&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Source:&lt;/strong> &lt;a href="https://github.com/justinmklam/sous-vide">Github&lt;/a>&lt;/p>
&lt;!--__Skills:__

+ Firmware programming
+ Enclosure design
+ PCB design-->
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8508_hu_c1ad61bc317722ca.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8508_hu_c1ad61bc317722ca.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Modular sous vide controller powered by everyone&amp;rsquo;s favourite WiFi chip, the EPS8266.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/ui-demo.gif>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/ui-demo.gif>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Push button toggles between three states: main monitoring screen, set cooking time, and set cooking temperature.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="the-long-version" class="anchor">
 &lt;a href="#the-long-version">
 The Long Version
 &lt;/a>
&lt;/h1>

&lt;h2 id="the-sous-vide-story" class="anchor">
 &lt;a href="#the-sous-vide-story">
 The Sous Vide Story
 &lt;/a>
&lt;/h2>
&lt;p>You may have heard people rant about this fancy new cooking method that&amp;rsquo;s all the rage right now, but of course you shrug it off as a fad. But here you are, intrigued enough to be reading this article about the very thing you previously rolled your eyes at. So what the heck is sous vide anyway? The peeps over at &lt;a href="https://www.chefsteps.com/activities/what-is-sous-vide">ChefSteps&lt;/a> have an answer:&lt;/p>
&lt;blockquote>
&lt;p>Imagine you’re cooking a steak. You probably know exactly the color and texture—the doneness, in other words—you’d like, right? With sous vide (say “sue veed”), you simply set a pot of water to the corresponding time and temperature, and you can get that perfect doneness you desire, every time.&lt;/p>&lt;/blockquote>
&lt;p>Simply put, you stick the steak in a ziploc bag, stick the bag in the temperature controlled water bath, and wait for the steak to reach thermal equilibrium. After an hour or so, the steak will be the same temperature of the water bath. Need to wait an extra hour before you cook? Not to worry, your steak will be safe since it&amp;rsquo;ll never exceed the temperature of the bath. With sous vide, say goodbye to overcooked food; physics just won&amp;rsquo;t let it happen.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/sousvideTechniques.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/sousvideTechniques.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Talk nerdy to me about sous vide. (Source: ChefSteps)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/what-is-sous-vide_hu_20d9d34a3a5268f2.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/what-is-sous-vide_hu_20d9d34a3a5268f2.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Once immersion circulators came to the consumer market, sous vide became accessible to home kitchens. (Source: Anova Culinary)&lt;/p>
&lt;/div>
&lt;/p>
&lt;!--![Why cooking sous vide is worth the effort. (Source: OBH Nordica)](Sous-Vide-ENG.jpg)-->

&lt;h2 id="the-controller" class="anchor">
 &lt;a href="#the-controller">
 The Controller
 &lt;/a>
&lt;/h2>
&lt;p>Wanting to get in on this craze without spending a fortune (especially if it turned out to be a novelty), I set off to make my own. If all a sous vide device does is temperature control of a heating element, then it wouldn&amp;rsquo;t be anything my recent mechatronics degree couldn&amp;rsquo;t handle.&lt;/p>
&lt;p>&lt;strong>Equipment:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Rice cooker to act as the heating element and water vessel&lt;/li>
&lt;li>&lt;a href="https://www.amazon.ca/gp/product/B00EWENKXO/ref=oh_aui_detailpage_o05_s00?ie=UTF8&amp;amp;psc=1">80 GPH Acquarium pump&lt;/a> to circulate the water&lt;/li>
&lt;li>&lt;a href="https://www.amazon.ca/gp/product/B00KLZQ0P8/ref=oh_aui_detailpage_o09_s00?ie=UTF8&amp;amp;psc=1">DS18B20&lt;/a> waterproof temperature sensor&lt;/li>
&lt;li>ESP8266 WiFi Chip&lt;/li>
&lt;/ul>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8608_hu_ee1f65f8d0e4db49.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8608_hu_ee1f65f8d0e4db49.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">DIY sous vide setup with home-made temperature controller.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8522_hu_cc6eee804bef696a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8522_hu_cc6eee804bef696a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Front face of the controller.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8526_hu_e1135fe694d02529.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8526_hu_e1135fe694d02529.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Backside of the controller. Note the switch and cord grip for that back-me-on-Kickstarter finish quality.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8454_hu_7243b436b7e6b716.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8454_hu_7243b436b7e6b716.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Little space was left unused to minimize the physical footprint of the circuit.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8432_hu_4bb9cd9b6e4117ef.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8432_hu_4bb9cd9b6e4117ef.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Reveal of perfboard craftsmanship using the gobs-of-solder trace method.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/iUJLGdd_hu_14d3796bcc015dff.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/iUJLGdd_hu_14d3796bcc015dff.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">I designed the panel in SolidWorks, printed out the layout, taped it to a store bought enclosure panel and used a Dremel to carve out the holes.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/DCfOE0B_hu_85369caed1f4b799.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/DCfOE0B_hu_85369caed1f4b799.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Dry fitting after a bit of filing to clean up the edges and corners.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8400_hu_1dd82d06ac4bd3d9.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8400_hu_1dd82d06ac4bd3d9.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Programming the ESP8266.&lt;/p>
&lt;/div>
&lt;/p>
&lt;!--![TEXT](IMG_8580.jpg)-->

&lt;h2 id="the-data" class="anchor">
 &lt;a href="#the-data">
 The Data
 &lt;/a>
&lt;/h2>
&lt;p>No project is complete without analyzing data! The main unknowns thus far:&lt;/p>
&lt;ol>
&lt;li>Rise time and stability comparison between various cooking vessels&lt;/li>
&lt;li>Temperature stability improvements with a water circulator&lt;/li>
&lt;li>Is a bang-bang controller sufficient or is PID required?&lt;/li>
&lt;/ol>
&lt;p>To assess these questions, I setup the ESP8266 to push live temperature data to an online web server. Through &lt;a href="http://mqtt.org/">MQTT&lt;/a>, I was able to send live temperature data from the controller to &lt;a href="https://io.adafruit.com/">Adafruit.IO&lt;/a>. This service (in open beta at the time of writing) allows dashboards and data feeds to be easily created for real-time monitoring of anything.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/adafruit-dashboard.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/adafruit-dashboard.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The ESP8266 logs temperature data through Adafruit servers. Data is displayed through their live dashboard feed.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="a-comparison-of-cookers" class="anchor">
 &lt;a href="#a-comparison-of-cookers">
 A Comparison of Cookers
 &lt;/a>
&lt;/h3>
&lt;p>Four trials were conducted with three different cooking vessels: a 3 cup rice cooker, 7 cup rice cooker, and a 14 cup (3.3 L) slow cooker. Two cups of water were used in each vessel for these tests. As shown through the graph below, the rice cookers get up to 55°C significantly than the &lt;em>(very)&lt;/em> slow cooker. Although it would be better in maintaining temperature due to its ceramic makeup, it took way too long to heat up for practical purposes. You can fill it with hot/boiling water to help it warm up, but it still wouldn&amp;rsquo;t be as fast or convenient than the rice cookers.&lt;/p>
&lt;p>Slow cooker, you are the weakest link. Goodbye.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/plot_benchmarks2.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/plot_benchmarks2.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Comparison of rice and slow cookers with circulated and uncirculated water baths.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Looking at the rice cookers in more detail, we see that the small rice cooker has a much lower overshoot than the large. This makes sense since the heating element is smaller, thus it retains less heat &amp;ldquo;momentum&amp;rdquo; upon shutoff. The addition of a water circulator significantly reduced the steady state temperature oscillation as well as the amplitude of overshoot. With the added benefit of greater temperature uniformity within the water bath than relying on natural convection to mix the water, using a circulator is proven to be a necessity.&lt;/p>
&lt;p>Moving forward, I decided to use the large rice cooker with a circulator. Although the small rice cooker reaches steady state more quickly, its small vessel size s unfortunately not practical for cooking anything but a feast for ants.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/plot_benchmarks.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/plot_benchmarks.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Comparison of the remaining cooking vessels.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8543_hu_e123f0ff7bd5bf56.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG_8543_hu_e123f0ff7bd5bf56.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Small aquarium pump used to circulate the water.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Time to actually cook with this thing! After trying out various foods, the data shows that the temperature stability is surprisingly stable given the simple on-off temperature control.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/plot_cook_times.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/plot_cook_times.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Log of cook times.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Quantifying the ripple with some simple analysis shows that the average standard deviation for these four cooking trials was ±0.4°C, where the steak had the highest deviation of ±0.6°C.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-no-highlight" data-lang="no-highlight">&lt;span class="line">&lt;span class="cl"> Trout Fillet at 48C for 59 mins
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Mean: 48.35°C (+1.15, -0.73)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Std: ±0.34°C
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Sirloin Steak at 53C for 84 mins
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Mean: 53.03°C (+1.78, -2.65)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Std: ±0.63°C
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Chicken Thighs at 70C for 99 mins
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Mean: 70.03°C (+0.59, -1.53)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Std: ±0.30°C
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Burger Patty at 60C for 31 mins
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Mean: 60.16°C (+0.72, -0.66)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Std: ±0.23°C
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --&amp;gt; AVERAGE STANDARD DEV: ±0.38°C
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Thus, using a simple temperature control implementation with a water circulator yields quite satisfying results.&lt;/p>

&lt;h2 id="the-verdict" class="anchor">
 &lt;a href="#the-verdict">
 The Verdict
 &lt;/a>
&lt;/h2>
&lt;p>They say a picture is worth a thousand words. The one below might not be worth quite that many, but that pink uniformity definitely speaks for itself.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG-20161231-WA0007_hu_70883dd95007304.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2017/05/sous-vide-controller/IMG-20161231-WA0007_hu_70883dd95007304.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">A uniformly cooked sirloin steak finished on a cast-iron pan.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>Tired of Cables in VR? We Are Too.</title><link>https://justinmklam.com/posts/2016/tired-of-cables/</link><pubDate>Tue, 15 Nov 2016 23:02:43 -0700</pubDate><guid>https://justinmklam.com/posts/2016/tired-of-cables/</guid><description>&lt;p>&lt;em>Originally posted on &lt;a href="https://medium.com/mistywest/tired-of-cables-in-virtual-reality-we-are-too-efeab5606bf0">Medium&lt;/a> under &lt;a href="https://mistywest.com/">MistyWest&lt;/a>.&lt;/em>&lt;/p>
&lt;p>Virtual reality pushes the envelope of bleeding edge technology, allowing us to explore and experience worlds beyond our mortal imaginations. It gives us immersion in another dimension, providing an unprecedented medium for communication and story telling. If a picture is worth a thousand words, then virtual reality must be worth millions. Except there’s one thing keeping it grounded to reality: cables.&lt;/p>
&lt;p>Since the first virtual reality headset that was hacked together in 2011, immersive head-mounted displays have progressed far and quickly. Oculus Rift, HTC Vive, and PSVR are some of the main contenders of virtual reality hardware, and between them lie stark differences in performance and usability. However, cables are the common denominator in these high performance virtual reality headsets. They are tethered to a host computer to provide the computationally intensive processing power required for high-fidelity content. Although mobile solutions are available such as Microsoft’s HoloLens, Samsung VR, Google Daydream, and even Google Cardboard, wired headsets will continue to be the vanguard of high performance virtual reality.&lt;/p></description><content:encoded>&lt;p>&lt;em>Originally posted on &lt;a href="https://medium.com/mistywest/tired-of-cables-in-virtual-reality-we-are-too-efeab5606bf0">Medium&lt;/a> under &lt;a href="https://mistywest.com/">MistyWest&lt;/a>.&lt;/em>&lt;/p>
&lt;p>Virtual reality pushes the envelope of bleeding edge technology, allowing us to explore and experience worlds beyond our mortal imaginations. It gives us immersion in another dimension, providing an unprecedented medium for communication and story telling. If a picture is worth a thousand words, then virtual reality must be worth millions. Except there’s one thing keeping it grounded to reality: cables.&lt;/p>
&lt;p>Since the first virtual reality headset that was hacked together in 2011, immersive head-mounted displays have progressed far and quickly. Oculus Rift, HTC Vive, and PSVR are some of the main contenders of virtual reality hardware, and between them lie stark differences in performance and usability. However, cables are the common denominator in these high performance virtual reality headsets. They are tethered to a host computer to provide the computationally intensive processing power required for high-fidelity content. Although mobile solutions are available such as Microsoft’s HoloLens, Samsung VR, Google Daydream, and even Google Cardboard, wired headsets will continue to be the vanguard of high performance virtual reality.&lt;/p>
&lt;p>Or so we thought.&lt;/p>
&lt;p>But before we begin, let’s backtrack to a mere two months ago. We found ourselves growing tired of worrying about tripping over cables while using the HTC Vive in room-scale experiences. This was (and still is) a common problem: our eyes tell us we have freedom of movement, but our hardware reminds us we don’t. Wouldn’t it be great if there was a way to break free from cables and improve the immersivity of virtual reality?&lt;/p>
&lt;p>Humans are hackers at heart and always strive to make things better. As a team of curious and hungry engineers, our answer was yes.&lt;/p>
&lt;p>We wanted a solution to increase and maintain presence in virtual reality; having to worry about tripping over the cable takes away from it. Some would argue that subconsciously stepping over the cable is a minor problem that one gets used to. However, despite its small inconvenience, it’s an inconvenience nonetheless which detracts from the otherwise immersive VR experience. Solutions do exist: at event demos, people hire cable sherpas to hold the wires behind the user and follow them around. Unfortunately, not everyone has access to their own personal sherpa.&lt;/p>
&lt;p>We have the ability to be transported to completely different worlds, yet we are tethered by cables that have existed longer than we can remember. We shouldn’t stop innovation for VR at just the headset; the entire experience needs to be unique and immersive.&lt;/p>
&lt;p>If this is such a common problem, what have people done to manage these notorious cables?&lt;/p>
&lt;p>A handful of HTC Vive owners have mounted retractable identification badge fobs or dog leashes to the ceiling to create an overhead holding point of the cable. However, certain player movements will result in tension between the mounting point and the headset. We tried this ourselves, and we found the cable tugging more distracting than having to step over the cable.&lt;/p>
&lt;p>&lt;div class="row img-captioned">
 &lt;a href=0-ybwUNzKuP1JnIQ6u.jpg>&lt;img class="img-responsive img-content" src=0-ybwUNzKuP1JnIQ6u.jpg />&lt;/a>
 &lt;p class="caption">Free standing cable boom. [SteelSeries Tech Blog]&lt;/p>
&lt;/div>

&lt;div class="row img-captioned">
 &lt;a href=1-hyLaVWqAzYS2YvWKKrLy7g.png>&lt;img class="img-responsive img-content" src=1-hyLaVWqAzYS2YvWKKrLy7g.png />&lt;/a>
 &lt;p class="caption">Ceiling mounted dog leashes. [YouTube]&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>What we needed was a way to mimic a cable sherpa, following our every movement to prevent tension in the cable and entanglement around our legs. Whether we were moving forward, backward, left, right, or turning around, we wanted a way to roam freely as if virtual reality was already wireless.&lt;/p>
&lt;p>Cue the engineers.&lt;/p>
&lt;p>Combining our expertise in rapid prototyping, software development, and the strong desire to make things better, we had ourselves a solution: the autonomous robotic gantry.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=1-L0-0M3ktUiBQcZHqOwzkyA.gif>&lt;img class="img-responsive img-content" src=1-L0-0M3ktUiBQcZHqOwzkyA.gif />&lt;/a>
 &lt;p class="caption">Achievement unlocked: Freedom of movement with wired VR.&lt;/p>
&lt;/div>

&lt;p>The autonomous robotic gantry is an overhead cable management system which allows the user to roam freely within the play area. Using aluminum extrusions and laser cut acrylic parts as the base materials, we designed and constructed a lightweight, planar motion system driven by two stepper motors through a a CoreXY timing belt configuration. Minimizing the number of moving components and weight was the key in optimizing acceleration and speed. By using off the shelf components and laser cut parts, we were able to design, build, and assemble the gantry in a short two weeks. Thanks to OpenVR, we were able to use the positional data to control and automate the overhead gantry. No muss, no fuss, no more tripping over cables.&lt;/p>
&lt;div class="row img-captioned">
 &lt;a href=1-xU58k_ZTrzgGpbr4mWfN_w.gif>&lt;img class="img-responsive img-content" src=1-xU58k_ZTrzgGpbr4mWfN_w.gif />&lt;/a>
 &lt;p class="caption">Cable-free gameplay of Space Pirate Trainer.&lt;/p>
&lt;/div>

&lt;p>Over-engineered? Maybe. Overkill? We think not. This was an exercise not in cost-effectiveness for the average user, but to explore how we can integrate existing technology with bleeding edge products in order to improve its efficacy and functionality. As the world has become a community of hackers, we don’t have to rest on our laurels and wait for companies to come up with solutions to our problems. The resources are available to us to come up with these solutions ourselves.&lt;/p>

&lt;div class="img-content video-container">
 &lt;iframe src=https://www.youtube.com/embed/zULBxDJVaHs>&lt;/iframe>
&lt;/div>
&lt;p class="caption">Full demo video of the robotic gantry.&lt;/p>

&lt;p>Fast forward to present day. Just last week, HTC announced a kit for wireless virtual reality. That’s right: the devs behind the HTC Vive have already been working on a consumer ready solution to cables. We didn’t anticipated this wireless movement for another 5–10 years since latency and frame rate is still an issue with current wired headsets. However, despite the rapid research and development to unlock the potential of true freedom of movement, wireless performance will always be succeeded by wired headsets.&lt;/p>
&lt;p>Is our robotic gantry dead on arrival? It was a fun, quick project to work on and we believe it still holds value. Potentially not as a product, but as an exercise to explore how we can use existing technology to improve upon new ones. In any event, we are excited to see the progression of wireless headsets and will continue to watch the rapid changes in industry that are happening before our very eyes.&lt;/p>
&lt;p>Virtual reality is still a nascent industry with large room for growth, and solutions for wired, wireless, and mobile media will continue to fill the stage. As new products emerge, older ones may fall. Regardless of what happens in the realm of virtual reality, we don’t want to just sit back and watch it happen. As mentioned before, we are all hackers at heart and continually strive to make things better. Whether it be virtual reality in wired, wireless, or mobile form, we are all capable of taking part in the forward progression of technology and will constantly seek opportunities to improve its performance and usability.&lt;/p></content:encoded></item><item><title>Wooden Phone Stand</title><link>https://justinmklam.com/posts/2016/phone-stand/</link><pubDate>Sun, 13 Nov 2016 21:35:28 -0700</pubDate><guid>https://justinmklam.com/posts/2016/phone-stand/</guid><description>&lt;p>Phone stands are something that seem a little unnecessary, but once you have one then you&amp;rsquo;ll wonder how your desk lived without one. I&amp;rsquo;m a firm believer in having a dedicated place for everything as a solution to keep things tidy, so making a phone stand for my desk at work just made sense.&lt;/p>
&lt;p>I started off on Google to look for existing designs I could imitate, but most fell short of not fitting my aesthetic needs. I wanted a balance between simplicity and form, without requiring too much effort to make. I eventually came across one design by &lt;a href="https://www.artfire.com/ext/shop/product_view/woodenlife/12640827/creative_wood_phone_stand_handmade/handmade/woodworking/other">Woodenlife on Artfire&lt;/a> and deemed it the winner for my copy-cat woodworking. I sketched out the side profile in SolidWorks, pasted it on to the wood block, and proceeded to cut it out on my bandsaw.&lt;/p></description><content:encoded>&lt;p>Phone stands are something that seem a little unnecessary, but once you have one then you&amp;rsquo;ll wonder how your desk lived without one. I&amp;rsquo;m a firm believer in having a dedicated place for everything as a solution to keep things tidy, so making a phone stand for my desk at work just made sense.&lt;/p>
&lt;p>I started off on Google to look for existing designs I could imitate, but most fell short of not fitting my aesthetic needs. I wanted a balance between simplicity and form, without requiring too much effort to make. I eventually came across one design by &lt;a href="https://www.artfire.com/ext/shop/product_view/woodenlife/12640827/creative_wood_phone_stand_handmade/handmade/woodworking/other">Woodenlife on Artfire&lt;/a> and deemed it the winner for my copy-cat woodworking. I sketched out the side profile in SolidWorks, pasted it on to the wood block, and proceeded to cut it out on my bandsaw.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/phone-stand/designs.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/phone-stand/designs.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Searching Google images for inspiration.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/phone-stand/IMG_41757cd0_365258_hu_9262b7ee2bac1b04.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/phone-stand/IMG_41757cd0_365258_hu_9262b7ee2bac1b04.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Design from Woodenlife on Artfire.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/phone-stand/solidworks.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/phone-stand/solidworks.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Cutting template created in SolidWorks.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/phone-stand/IMG_20161113_153119_hu_f9830f9254b3540a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/phone-stand/IMG_20161113_153119_hu_f9830f9254b3540a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Template pasted on to the wood stock.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/phone-stand/IMG_20161113_155138_hu_6defa99c5858f160.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/phone-stand/IMG_20161113_155138_hu_6defa99c5858f160.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Smoothing the curves with a mini plane.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/phone-stand/IMG_20161113_155954_hu_98297c8cbce2a914.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/phone-stand/IMG_20161113_155954_hu_98297c8cbce2a914.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Cleaning the cutout with a Dremel router attachment.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/phone-stand/IMG_8735_hu_46f5508e2ceddd48.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/phone-stand/IMG_8735_hu_46f5508e2ceddd48.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Stained, finished, and ready for display.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>For such a simple hunk of wood, I&amp;rsquo;m quite pleased with how it turned out.&lt;/p></content:encoded></item><item><title>Making Do With a Bargain Bin Bandsaw</title><link>https://justinmklam.com/posts/2016/bandsaw/</link><pubDate>Sat, 22 Oct 2016 20:58:10 -0700</pubDate><guid>https://justinmklam.com/posts/2016/bandsaw/</guid><description>It&amp;rsquo;s generally known that cheap tools are usually not worth the trouble, but the deal on this bandsaw was simply too hard to pass up. With only a missing table, it was merely a quick woodworking project away from restoring its full functionality.</description><content:encoded>&lt;p>I&amp;rsquo;ve always wanted a bandsaw. Being able to cut wood, plastics, etc. quickly and with flexibility has been a dream in my home shop. When I found an ad on Craigslist for a portable bandsaw for $50, I just couldn&amp;rsquo;t resist. The main caveat was that it was missing a table, but that wasn&amp;rsquo;t a big enough issue to turn me away from this killer deal.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/original_1_hu_a9807818597cf81a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/original_1_hu_a9807818597cf81a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Fresh off Craigslist in its table-naked glory.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Due to the lack of proper tools in my garage suitable to make a decent table, I needed to improvise with what I had. Enter the inverted jigsaw. Sure, I could have just used the jigsaw by itself, but it&amp;rsquo;s always easier to move the material instead of moving the tool. More importantly, this allowed me to easily cut smaller parts out.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161016_091555_hu_97928f5336d74627.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161016_091555_hu_97928f5336d74627.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">I needed something to cut parts for the table support. Thus, the inverted jigsaw was birthed.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Cutting the blade insert was much easier with the inverted jigsaw.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161022_170440_hu_1af45f9128790460.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161022_170440_hu_1af45f9128790460.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Cutting the table center out on the bandsaw stand-in.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161022_221814_hu_b76a31ff88c0472a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161022_221814_hu_b76a31ff88c0472a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Blade insert cut out&amp;hellip;&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161022_221837_hu_8c67193a9e783e98.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161022_221837_hu_8c67193a9e783e98.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">&amp;hellip; and seated nice and snug.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The first set of brackets I cut out weren&amp;rsquo;t great, but they did the job. However, it was good enough to get the bandsaw rolling and cut a second, revised set of brackets for better stability.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161016_091647_hu_6c85177bf08077e4.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161016_091647_hu_6c85177bf08077e4.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Attempt #1: First version of the table supports. It wasn&amp;rsquo;t extremely stable, but it was good enough to cut sturdier parts as replacements.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>The second set was much beefier and even included angle adjustment. Unfortunately it doesn&amp;rsquo;t work well since bandsaw tables typically use &lt;a href="https://woodgears.ca/bandsaw/trunions.html">trunion brackets&lt;/a> to keep the pivot point at the center of the blade. I didn&amp;rsquo;t envision myself using this bandsaw to cut angles (since the thing can&amp;rsquo;t even cut a damn straight 2x4&amp;hellip;, which I&amp;rsquo;ll get to later), so I didn&amp;rsquo;t bother with it this time around.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161022_162614_hu_2d6d618fffc83ef1.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161022_162614_hu_2d6d618fffc83ef1.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Cutting out the slotted curve.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161023_174353_hu_c783d5b3f33e2547.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161023_174353_hu_c783d5b3f33e2547.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Attempt #2: Completed table support with angle adjustment from the slotted hole. Also note the modified knob handle above to provide easier adjustment.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161023_174235_hu_8c319a0b2dee1012.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161023_174235_hu_8c319a0b2dee1012.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Bandsaw fully functional and ready to rip.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Below are a few woodworking projects I used the bandsaw for. Turns out you get what you pay for, with this bandsaw being no exception. It retails for &lt;a href="http://www.canadiantire.ca/en/pdp/mastercraft-120v-9-in-bandsaw-0556748p.html">$249 at Canadian Tire&lt;/a>, and many reviewers complained that cutting straight is virtually impossible without the blade bowing, likely due to the inadequately designed blade tensioner. It performs okay for cutting thin stock like plywood, but I wouldn&amp;rsquo;t rely on this for any mating joints.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161017_204329_hu_4b3b00f815b2419f.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161017_204329_hu_4b3b00f815b2419f.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">First bandsaw project was making a keyholder and to increase stoke for the winter ski season.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/bandsaw/IMG_20161021_222031_hu_3ebe869af69be139.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/bandsaw/IMG_20161021_222031_hu_3ebe869af69be139.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Also made a butcher&amp;rsquo;s block out of an old fireplace mantle.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Stay tuned for more woodworking projects!&lt;/p></content:encoded></item><item><title>Engineer's Diary</title><link>https://justinmklam.com/posts/2016/10/engineers-diary/</link><pubDate>Sat, 01 Oct 2016 11:34:47 -0700</pubDate><guid>https://justinmklam.com/posts/2016/10/engineers-diary/</guid><description>&lt;p>&lt;strong>Background:&lt;/strong> Between paper notebooks, post-it notes, OneNote, Evernote, and so many more, there is no shortage of ways to write things down. Each has its strengths and weaknesses, but none satisfied my requirements to act as a daily work log to record key events, thoughts, and milestones during my work day.&lt;/p>
&lt;p>My paper notebook is excellent for free-form thoughts, sketches, and calculations, but I would want to keep a separate notebook to keep track of these sequential events. We use OneNote at work, but where the infinite blank canvas is a strength in applications such as for research or brainstorming, I found it to be a weakness in record keeping since the document is too easy to edit and &amp;ldquo;fragile&amp;rdquo;.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Background:&lt;/strong> Between paper notebooks, post-it notes, OneNote, Evernote, and so many more, there is no shortage of ways to write things down. Each has its strengths and weaknesses, but none satisfied my requirements to act as a daily work log to record key events, thoughts, and milestones during my work day.&lt;/p>
&lt;p>My paper notebook is excellent for free-form thoughts, sketches, and calculations, but I would want to keep a separate notebook to keep track of these sequential events. We use OneNote at work, but where the infinite blank canvas is a strength in applications such as for research or brainstorming, I found it to be a weakness in record keeping since the document is too easy to edit and &amp;ldquo;fragile&amp;rdquo;.&lt;/p>
&lt;p>One day after learning about Git and SourceTree, I knew wanted to create a similar commit-style application to record activites and &amp;ldquo;freeze&amp;rdquo; them in time. Not web-based, internet connected, or cross platform; just a no frills desktop application that does one thing and one thing only.&lt;/p>
&lt;p>&lt;strong>Objective:&lt;/strong> Develop a simple application to log daily activities at work.&lt;/p>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Entries for each day are saved in a date-stamped text file&lt;/li>
&lt;li>Each commit is time-stamped in the text file&lt;/li>
&lt;li>Archive of log entries is accessible through the list in the left column&lt;/li>
&lt;li>Text files show up as read only in the application, but are editable and searchable through the Windows File Explorer&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Framework:&lt;/strong> C#&lt;/p>
&lt;p>&lt;strong>Source:&lt;/strong> &lt;a href="https://github.com/justinmklam/engineers-diary">Github&lt;/a>&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/10/engineers-diary/screencap2.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/10/engineers-diary/screencap2.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Screencap of the Engineer&amp;rsquo;s Diary. Write in the &amp;lsquo;Description&amp;rsquo; and &amp;lsquo;Project&amp;rsquo; text boxes, then press &amp;lsquo;Commit&amp;rsquo; when complete.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/10/engineers-diary/screencap3.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/10/engineers-diary/screencap3.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">After the &amp;lsquo;Commit&amp;rsquo; button is pressed, the entry is written or appended to a text file and displayed on screen.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>MECH 458: Capstone 3D Optical Well Imager</title><link>https://justinmklam.com/posts/2016/06/mech-458-capstone/</link><pubDate>Thu, 02 Jun 2016 12:57:32 -0700</pubDate><guid>https://justinmklam.com/posts/2016/06/mech-458-capstone/</guid><description>&lt;h1 id="introduction" class="anchor">
 &lt;a href="#introduction">
 Introduction
 &lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Objective&lt;/strong>: Develop an optical scanning system to be used for generating 3D models of well liners and casings with sub-millimeter accuracy.&lt;/p>&lt;/p>
&lt;p>&lt;strong>How&lt;/strong>: As a team of five mechatronic and mechanical engineering students, we designed and constructed an alpha prototype of the optical 3D well imager.&lt;/span>&lt;/p>&lt;/p>

&lt;h1 id="project-details" class="anchor">
 &lt;a href="#project-details">
 Project Details
 &lt;/a>
&lt;/h1>

&lt;h2 id="requirements-and-specifications" class="anchor">
 &lt;a href="#requirements-and-specifications">
 Requirements and Specifications
 &lt;/a>
&lt;/h2>

&lt;h3 id="optical" class="anchor">
 &lt;a href="#optical">
 Optical
 &lt;/a>
&lt;/h3>
&lt;ol>
 	&lt;li>Device must have a tolerance on distance measurement of 1 mm&lt;/li>
 	&lt;li>Device must have a camera circumferential resolution of 1.42°&lt;/li>
 	&lt;li>Device must have a 3D scan circumferential resolution of 1.42°&lt;/li>
 	&lt;li>Device must have an axial resolution of 1 mm&lt;/li>
&lt;/ol>

&lt;h3 id="mechanical" class="anchor">
 &lt;a href="#mechanical">
 Mechanical
 &lt;/a>
&lt;/h3>
&lt;ol>
 	&lt;li>Device must be submersible&lt;/li>
 	&lt;li>Device must fit in a 3.5” ID pipe&lt;/li>
 	&lt;li>Device must have a minimum operating velocity of 10 mm/s&lt;/li>
 	&lt;li>Device must be operable using the existing test apparatus&lt;/li>
&lt;/ol>

&lt;h2 id="design-overview" class="anchor">
 &lt;a href="#design-overview">
 Design Overview
 &lt;/a>
&lt;/h2>
&lt;p style="text-align: justify;">The final design of the prototype includes a camera module, laser module, and two conical mirrors. The basic functionality of the device is shown in the figure below. A red laser ring is generated by the laser module which is reflected by 90° on to the surface of the well casing. The camera’s vision is then reflected by 150° to capture the projected laser ring.&lt;/p></description><content:encoded>
&lt;h1 id="introduction" class="anchor">
 &lt;a href="#introduction">
 Introduction
 &lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Objective&lt;/strong>: Develop an optical scanning system to be used for generating 3D models of well liners and casings with sub-millimeter accuracy.&lt;/p>&lt;/p>
&lt;p>&lt;strong>How&lt;/strong>: As a team of five mechatronic and mechanical engineering students, we designed and constructed an alpha prototype of the optical 3D well imager.&lt;/span>&lt;/p>&lt;/p>

&lt;h1 id="project-details" class="anchor">
 &lt;a href="#project-details">
 Project Details
 &lt;/a>
&lt;/h1>

&lt;h2 id="requirements-and-specifications" class="anchor">
 &lt;a href="#requirements-and-specifications">
 Requirements and Specifications
 &lt;/a>
&lt;/h2>

&lt;h3 id="optical" class="anchor">
 &lt;a href="#optical">
 Optical
 &lt;/a>
&lt;/h3>
&lt;ol>
 	&lt;li>Device must have a tolerance on distance measurement of 1 mm&lt;/li>
 	&lt;li>Device must have a camera circumferential resolution of 1.42°&lt;/li>
 	&lt;li>Device must have a 3D scan circumferential resolution of 1.42°&lt;/li>
 	&lt;li>Device must have an axial resolution of 1 mm&lt;/li>
&lt;/ol>

&lt;h3 id="mechanical" class="anchor">
 &lt;a href="#mechanical">
 Mechanical
 &lt;/a>
&lt;/h3>
&lt;ol>
 	&lt;li>Device must be submersible&lt;/li>
 	&lt;li>Device must fit in a 3.5” ID pipe&lt;/li>
 	&lt;li>Device must have a minimum operating velocity of 10 mm/s&lt;/li>
 	&lt;li>Device must be operable using the existing test apparatus&lt;/li>
&lt;/ol>

&lt;h2 id="design-overview" class="anchor">
 &lt;a href="#design-overview">
 Design Overview
 &lt;/a>
&lt;/h2>
&lt;p style="text-align: justify;">The final design of the prototype includes a camera module, laser module, and two conical mirrors. The basic functionality of the device is shown in the figure below. A red laser ring is generated by the laser module which is reflected by 90° on to the surface of the well casing. The camera’s vision is then reflected by 150° to capture the projected laser ring.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-overview.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-overview.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Simplified diagram of scanning functionality.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="combine-modules" class="anchor">
 &lt;a href="#combine-modules">
 Combine Modules
 &lt;/a>
&lt;/h3>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/06/mech-458-capstone/Annotated-Combined-Modules_2-1.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/06/mech-458-capstone/Annotated-Combined-Modules_2-1.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Overview of the optical imaging device.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="camera-module" class="anchor">
 &lt;a href="#camera-module">
 Camera Module
 &lt;/a>
&lt;/h3>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/06/mech-458-capstone/Camera-Module.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/06/mech-458-capstone/Camera-Module.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Diagram of the camera module.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="laser-module" class="anchor">
 &lt;a href="#laser-module">
 Laser Module
 &lt;/a>
&lt;/h3>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/06/mech-458-capstone/Laser-Module.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/06/mech-458-capstone/Laser-Module.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Diagram of the laser module.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="system-evaluation" class="anchor">
 &lt;a href="#system-evaluation">
 System Evaluation
 &lt;/a>
&lt;/h2>
&lt;p style="text-align: justify;">Our prototype was successful in recreating a 3D model of the pipe internals during unsubmerged operation, as shown through the figures below. However, we were unable to test the device in submerged conditions due to time constraints of the project.&lt;/p>

&lt;h3 id="preliminary-testing-and-calibration" class="anchor">
 &lt;a href="#preliminary-testing-and-calibration">
 Preliminary Testing and Calibration
 &lt;/a>
&lt;/h3>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-evaluation_5.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-evaluation_5.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Setup of the calibration grid with a green laser ring (left); Captured image (right)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h3 id="deviceintegration" class="anchor">
 &lt;a href="#deviceintegration">
 Device Integration
 &lt;/a>
&lt;/h3>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-evaluation_2.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-evaluation_2.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Our device installed with the DarkVision lab equipment (left); Close up of the laser ring inside the well liner (right).&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="post-processing-andanalysis" class="anchor">
 &lt;a href="#post-processing-andanalysis">
 Post-Processing and Analysis
 &lt;/a>
&lt;/h2>
&lt;p style="text-align: justify;">Four different exposure times and traversing speeds were tested to determine which camera parameters would generate adequate images for post-processing and analysis. From our tests, we determined that the imaging parameters resulting in an effective frame rate of 0.13 fps at a forward jog speed of 7 mm/s showed the most promising image slices of the well liner. To increase the slice resolution of the scan, the frame rate was increased to 1.3 fps which yielded visually poorer images, but our processing algorithms were still able to generate a 3D model of the well liner. A hue saturation value filter was used to detect the outer edge of the raw image.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-evaluation_3.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-evaluation_3.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Captured laser ring by the camera through the conical mirror (left); Processed image (right)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-evaluation_4-1_hu_7cd430b54215bb5f.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/06/mech-458-capstone/System-evaluation_4-1_hu_7cd430b54215bb5f.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Generated 3D model from the image slices.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>New Workshop + Plywood Bench Stool</title><link>https://justinmklam.com/posts/2016/new-workshop/</link><pubDate>Wed, 01 Jun 2016 15:39:56 -0700</pubDate><guid>https://justinmklam.com/posts/2016/new-workshop/</guid><description>&lt;p>My family moved into a new house, meaning that we finally have an actual garage instead of a measly carport. But what this really means is that I get a chance to claim space to set up an actual workshop! First project was naturally a workbench, which I put together using 2x4&amp;rsquo;s and a 3/4&amp;quot; MDF board for the work surface (see above). Second project was a bench stool, as shown in the following images below. The builders left some 1/8&amp;quot; plywood in the yard, and I wanted to see how sturdy of a chair I can make with it given the inherent flimsiness of the thin sheet stock. By adding a reinforcing cross-brace at the base of the legs, I was able to significantly increase its sturdiness.&lt;/p></description><content:encoded>&lt;p>My family moved into a new house, meaning that we finally have an actual garage instead of a measly carport. But what this really means is that I get a chance to claim space to set up an actual workshop! First project was naturally a workbench, which I put together using 2x4&amp;rsquo;s and a 3/4&amp;quot; MDF board for the work surface (see above). Second project was a bench stool, as shown in the following images below. The builders left some 1/8&amp;quot; plywood in the yard, and I wanted to see how sturdy of a chair I can make with it given the inherent flimsiness of the thin sheet stock. By adding a reinforcing cross-brace at the base of the legs, I was able to significantly increase its sturdiness.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/new-workshop/IMG_20160602_170121_hu_98407d1193ce7c2e.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/new-workshop/IMG_20160602_170121_hu_98407d1193ce7c2e.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">I used a jigsaw to cut out each side, and fastened together with glue and nails (since screws would likely split the wood).&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/new-workshop/IMG_20160602_172723_hu_47224e915d31f717.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/new-workshop/IMG_20160602_172723_hu_47224e915d31f717.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Final panel being attached.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/new-workshop/IMG_20160603_122213_hu_d50beb1356b0a66f.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/new-workshop/IMG_20160603_122213_hu_d50beb1356b0a66f.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Blocks of wood being clamped, which will be used to attach the seat. note the cross-brace near the feet of the stool.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/new-workshop/IMG_20160602_210114_hu_861ec947a086eb38.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/new-workshop/IMG_20160602_210114_hu_861ec947a086eb38.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Gluing and clamping three sheets of the same plywood to get a more comfortable contour for sitting. This is a similar method to how skateboards/longboards are given their complex curvatures.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/new-workshop/IMG_20160604_113358_hu_aee28598ec738952.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/new-workshop/IMG_20160604_113358_hu_aee28598ec738952.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Stool sanded and clear coat applied.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>For a stool made with the cheapest possible wood, it actually turned out half-decent and it does what it needs to do!&lt;/p></content:encoded></item><item><title>Adjustable Portable Power Supply</title><link>https://justinmklam.com/posts/2016/05/power-supply/</link><pubDate>Wed, 25 May 2016 17:29:58 -0700</pubDate><guid>https://justinmklam.com/posts/2016/05/power-supply/</guid><description>&lt;p>&lt;strong>Objective:&lt;/strong> Build a cheap, portable, variable DC power supply.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> It was finally time to get my hands on a variable power supply for my electronics projects. Previous projects mainly involved Arduino, which was able to supply 5V and 3.3V with ease. However, the need for a supply with higher voltage, current, and flexibility eventually arose, resulting in the birth of this ghetto (but functional) power supply.&lt;/p>
&lt;p>&lt;strong>Limitations:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Only DC voltages available&lt;/li>
&lt;li>Max current is a function of input power and desired voltage (I=P/V)&lt;/li>
&lt;li>Current limiting feature is non-existent, so must be careful to not let the genie out of circuits&lt;/li>
&lt;/ul>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/05/power-supply/IMG_20160525_143029_hu_86f5fe2f91a1caaa.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/05/power-supply/IMG_20160525_143029_hu_86f5fe2f91a1caaa.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Internals of the power supply.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Objective:&lt;/strong> Build a cheap, portable, variable DC power supply.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> It was finally time to get my hands on a variable power supply for my electronics projects. Previous projects mainly involved Arduino, which was able to supply 5V and 3.3V with ease. However, the need for a supply with higher voltage, current, and flexibility eventually arose, resulting in the birth of this ghetto (but functional) power supply.&lt;/p>
&lt;p>&lt;strong>Limitations:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Only DC voltages available&lt;/li>
&lt;li>Max current is a function of input power and desired voltage (I=P/V)&lt;/li>
&lt;li>Current limiting feature is non-existent, so must be careful to not let the genie out of circuits&lt;/li>
&lt;/ul>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/05/power-supply/IMG_20160525_143029_hu_86f5fe2f91a1caaa.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/05/power-supply/IMG_20160525_143029_hu_86f5fe2f91a1caaa.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Internals of the power supply.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>MECH 423: Mechatronics Product Design</title><link>https://justinmklam.com/posts/2016/04/perfeggct/</link><pubDate>Fri, 22 Apr 2016 22:38:31 -0700</pubDate><guid>https://justinmklam.com/posts/2016/04/perfeggct/</guid><description>&lt;p>&lt;strong>Background:&lt;/strong> When a hard boiled egg is spun on a table, it rotates freely since the inside is completely solid. With a raw egg, the liquid yolk sloshes around and resists rotation. By using math and physics, we can analyze the rotational oscillation of an egg and determine the yolk consistency.&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;If it&amp;rsquo;s worth doing, it&amp;rsquo;s worth overdoing.&amp;rdquo; - Jaime Hyneman&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Objective:&lt;/strong> Design and build a device to determine how cooked a boiled egg is using non-invasive techniques.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Background:&lt;/strong> When a hard boiled egg is spun on a table, it rotates freely since the inside is completely solid. With a raw egg, the liquid yolk sloshes around and resists rotation. By using math and physics, we can analyze the rotational oscillation of an egg and determine the yolk consistency.&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;If it&amp;rsquo;s worth doing, it&amp;rsquo;s worth overdoing.&amp;rdquo; - Jaime Hyneman&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Objective:&lt;/strong> Design and build a device to determine how cooked a boiled egg is using non-invasive techniques.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> As part of our final year engineering course, we were tasked to create a mechatronics device of our choosing. This project was heavily inspired by &lt;a href="https://www.youtube.com/channel/UCckETVOT59aYw80B36aP9vw">Matthias Wandel&lt;/a> and his &lt;a href="https://www.youtube.com/watch?v=Cw9w1CZkTr0">boiled egg hardness tester&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Functional Requirements:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Measure egg mass from strain gauge&lt;/li>
&lt;li>Measure oscillation from accelerometer&lt;/li>
&lt;li>Control LCD screen&lt;/li>
&lt;li>Mechanical design and fabrication&lt;/li>
&lt;li>Integration and user interaction study&lt;/li>
&lt;li>C# interface&lt;/li>
&lt;li>MATLAB data analysis&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Limitations:&lt;/strong> This prototype was developed in 3 weeks during school and has approximately 80% repeatability.&lt;/p>
&lt;p>&lt;strong>Partner:&lt;/strong> &lt;a href="http://justin-liang.com">Justin Liang&lt;/a>&lt;/p>

&lt;div class="img-content video-container">
 &lt;iframe src=https://www.youtube.com/embed/54d9iQcqX7Q>&lt;/iframe>
&lt;/div>
&lt;p class="caption">Sixty second demo video of the device in action.&lt;/p>

&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/CAD_4.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/CAD_4.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">CAD model of electronics inside the enclosure (transparency shown for clarity)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/Fritzing-diagram_hu_24c6bd126fb5f4bb.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/Fritzing-diagram_hu_24c6bd126fb5f4bb.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">PCB diagram.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/DSC03408_hu_80566ec0e2d81df5.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/DSC03408_hu_80566ec0e2d81df5.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">PCB assembled.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/DSC03430_hu_953539d8921b8dca.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/DSC03430_hu_953539d8921b8dca.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Dry fitting of components inside handle enclosure.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/IMG_0461_annotated_hu_86740cb7f5727472.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/IMG_0461_annotated_hu_86740cb7f5727472.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Annotated diagram of the final device.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/Raw-signals.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/Raw-signals.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Example data from five egg-jiggling trials.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/storage_1.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/storage_1.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The PerfEggct stored in its natural habitat.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/IMG_0407_hu_2bb03eb74241c790.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/IMG_0407_hu_2bb03eb74241c790.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Kitchen gadgets aren&amp;rsquo;t complete without a fancy photoshoot!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2016/04/perfeggct/IMG_0424_hu_e159d61d7fb316a4.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2016/04/perfeggct/IMG_0424_hu_e159d61d7fb316a4.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Truly perf-egg-ct.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Interested in reading more about this project? Head over to &lt;a href="http://www.instructables.com/id/PerfEGGct-Engineering-the-Perfect-Soft-Boiled-Egg/">Instructables&lt;/a> for more details! And if you&amp;rsquo;re &lt;em>really&lt;/em> interested, feel free to read our glorious 35-page final report &lt;a href="https://justinmklam.com/files/MECH-423-Final-Project-Report.pdf">here&lt;/a>.&lt;/p></content:encoded></item><item><title>Boo-Boo with the Bamboo Bike</title><link>https://justinmklam.com/posts/2015/bamboo-bike-boo-boo/</link><pubDate>Fri, 18 Dec 2015 20:50:37 -0700</pubDate><guid>https://justinmklam.com/posts/2015/bamboo-bike-boo-boo/</guid><description>&lt;p>As you probably already know, I made a &lt;a href="https://justinmklam.com/projects/mech/bamboo-bike/">bamboo bike&lt;/a> which I&amp;rsquo;m very proud of. However, what you probably don&amp;rsquo;t know is that I made some mistakes in the process. The photo above shows the result of one of these progress speedbumps, and yes, I did indeed cry the day I had to cut those tubes out.&lt;/p>
&lt;p>I started this project in November 2013 but failed to finish due splitting bamboo. Progress was moving forward: I had the bamboo, carbon fibre, and epoxy ready for frame building, and the poles were in the process of being cut and shaped to size. However, winter was quickly approaching and the weather was becoming less and less ideal for carbon fibre layup. The epoxy I was using had a working temperature above 10°C, but the outside temperature was beginning to drop below that. I was able to keep it in its liquid state by using my mother&amp;rsquo;s hair dryer (don&amp;rsquo;t tell her though since she still doesn&amp;rsquo;t know&amp;hellip;), but the epoxy wasn&amp;rsquo;t the main issue. Apparently bamboo is a living thing and doesn&amp;rsquo;t like drastic changes in temperature. Turns out doing the layup in colder temperatures, then bringing the frame inside to dry in warm, livable temperatures wasn&amp;rsquo;t good for the bamboo. Moisture is the devil here, and causes the wood to expand and contract based on the current environmental conditions.&lt;/p></description><content:encoded>&lt;p>As you probably already know, I made a &lt;a href="https://justinmklam.com/projects/mech/bamboo-bike/">bamboo bike&lt;/a> which I&amp;rsquo;m very proud of. However, what you probably don&amp;rsquo;t know is that I made some mistakes in the process. The photo above shows the result of one of these progress speedbumps, and yes, I did indeed cry the day I had to cut those tubes out.&lt;/p>
&lt;p>I started this project in November 2013 but failed to finish due splitting bamboo. Progress was moving forward: I had the bamboo, carbon fibre, and epoxy ready for frame building, and the poles were in the process of being cut and shaped to size. However, winter was quickly approaching and the weather was becoming less and less ideal for carbon fibre layup. The epoxy I was using had a working temperature above 10°C, but the outside temperature was beginning to drop below that. I was able to keep it in its liquid state by using my mother&amp;rsquo;s hair dryer (don&amp;rsquo;t tell her though since she still doesn&amp;rsquo;t know&amp;hellip;), but the epoxy wasn&amp;rsquo;t the main issue. Apparently bamboo is a living thing and doesn&amp;rsquo;t like drastic changes in temperature. Turns out doing the layup in colder temperatures, then bringing the frame inside to dry in warm, livable temperatures wasn&amp;rsquo;t good for the bamboo. Moisture is the devil here, and causes the wood to expand and contract based on the current environmental conditions.&lt;/p>
&lt;p>A week of thermal cycling like so, and I had myself some cracked bamboo. I was heart-broken. It was the end of December at this point, so I decided not to continue with this project until the weather warmed up.&lt;/p>
&lt;p>Once spring came around, I set off to Chilliwack to get myself much higher quality materials from &lt;a href="http://www.bambooworld.com/">Bamboo World&lt;/a>. I was significantly more meticulous in heat treatment this time around, conducting an oven bake as well as direct heat treatment to remove as much moisture as possible and to cure the bamboo.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/bamboo-bike-boo-boo/2013-09-15-14.21.10-1024x768_hu_d4f4ef3fa63f7ee4.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/bamboo-bike-boo-boo/2013-09-15-14.21.10-1024x768_hu_d4f4ef3fa63f7ee4.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Bamboo poles after heat treatment in the oven, preparing for the next heat treatment.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>I weighed my options in what to do about the frame, whether to start from scratch or try to salvage it. Being both optimistic and lazy, I chose the latter. I figured I should be able to cut out the compromised tubes and simply replace it with new ones. The joints would be a bit bulkier from the leftover carbon fibre, but that would merely be an aesthetic issue. To replace the entire front triangle, I had to be strategic in the order of extraction and replacement to preserve the steerer tube geometry. Luckily I didn&amp;rsquo;t mess that up, so it became a matter of starting off where I left off. Seven steps back, 3 steps forward!&lt;/p>
&lt;p>Eventually I managed to finish the bike, and everything else worked out in the end&amp;hellip;&lt;/p>
&lt;p>&amp;hellip; Except not. Turns out bamboo &lt;strong>really&lt;/strong> doesn&amp;rsquo;t like moisture. Two winters later and this happened:&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/bamboo-bike-boo-boo/IMG_20151201_093746_hu_97b8299a3f630157.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/bamboo-bike-boo-boo/IMG_20151201_093746_hu_97b8299a3f630157.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Another split down the seatpost :(&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Despite all my efforts in heat treatment, wood sealing, and surface finishing, the bamboo still managed to crack. Initially I was only going to ride this bike in dry weather, but come on, it&amp;rsquo;s Vancouver aka the &amp;ldquo;Wet Coast&amp;rdquo;. From the photo above, all the road grime clearly didn&amp;rsquo;t do the frame any favours. The dirt/sand slowly withered the clear coat away (5 layers of it, by the way) and moisture slowly made its way into the bamboo.&lt;/p>
&lt;p>But to heck with it, I&amp;rsquo;ll keep riding it to the ground and see how long it lasts.&lt;/p>
&lt;br>
&lt;p>TL;DR Wood doesn&amp;rsquo;t like moisture, and if you don&amp;rsquo;t listen then you&amp;rsquo;re going to have a bad time.&lt;/p>
&lt;br>
&lt;p>&lt;em>Update: As of May 2017 and no more riding in the rain, the bamboo managed to dry and the split sealed up. The ride is much less spongy/flexy than before, so I think it will now officially become a fair-weather bike.&lt;/em>&lt;/p></content:encoded></item><item><title>The Making of Project Haikuza: Part 2</title><link>https://justinmklam.com/posts/2015/making-haikuza-ii/</link><pubDate>Thu, 06 Aug 2015 23:49:59 -0700</pubDate><guid>https://justinmklam.com/posts/2015/making-haikuza-ii/</guid><description>&lt;p>&lt;em>The format of this series is an outline of my thought process during the development of &lt;a href="https://justinmklam.com/projects/software/haikuza/">@thehaikuza&lt;/a>.&lt;/em>&lt;/p>
&lt;blockquote style="text-align:center">
Poetry is hard
&lt;br>To write when algorithms
&lt;br>Are extremely dumb.
&lt;/blockquote>
&lt;p>I dont want my haiku generator to be a vegetarian chef. There&amp;rsquo;s nothing wrong with always making word salad, but eventually it&amp;rsquo;ll have to learn to make fancier things. A poetic risotto would be nice from time to time.&lt;/p>
&lt;p>Leaving @thehaikuza to make complete gibberish wasnt what I had intended. I envisioned my algorithm to be able to reconstruct bad haikus, but definitely not as crappy as the ones it actually made. My idea of bad had more to do with this xkcd comic:&lt;/p></description><content:encoded>&lt;p>&lt;em>The format of this series is an outline of my thought process during the development of &lt;a href="https://justinmklam.com/projects/software/haikuza/">@thehaikuza&lt;/a>.&lt;/em>&lt;/p>
&lt;blockquote style="text-align:center">
Poetry is hard
&lt;br>To write when algorithms
&lt;br>Are extremely dumb.
&lt;/blockquote>
&lt;p>I dont want my haiku generator to be a vegetarian chef. There&amp;rsquo;s nothing wrong with always making word salad, but eventually it&amp;rsquo;ll have to learn to make fancier things. A poetic risotto would be nice from time to time.&lt;/p>
&lt;p>Leaving @thehaikuza to make complete gibberish wasnt what I had intended. I envisioned my algorithm to be able to reconstruct bad haikus, but definitely not as crappy as the ones it actually made. My idea of bad had more to do with this xkcd comic:&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/making-haikuza-ii/ios_keyboard.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/making-haikuza-ii/ios_keyboard.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">iOS Keyboard Predictions (Source: xkcd.com/1427)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>In any event, I needed to try something a little more sophisticated than shoving words into slots where they didn&amp;rsquo;t really fit.&lt;/p>

&lt;h1 id="the-lesser-known-cousin-of-2-chainz" class="anchor">
 &lt;a href="#the-lesser-known-cousin-of-2-chainz">
 The Lesser Known Cousin of 2 Chainz
 &lt;/a>
&lt;/h1>
&lt;p>Every time I Googled a new topic I didnt know about, five more topics were thrown on my plate. Notable keywords that popped up:&lt;/p>
&lt;ul>
&lt;li>Computational linguistics&lt;/li>
&lt;li>Neural networks&lt;/li>
&lt;li>Sentiment classification&lt;/li>
&lt;li>Bayesian inference&lt;/li>
&lt;li>Tree kernels for semantic role labeling&lt;/li>
&lt;li>Markov chains&lt;/li>
&lt;/ul>
&lt;p>By no means am I suggesting that I understood all of those search terms after reading about them. In fact, I still don&amp;rsquo;t and am constantly trying to wrap my head around those polysyllabic words. (Fun fact: anything above three syllables starts to scare most people.) The takeaway is the last item on the list, which coincidentally is the one I did manage to (somewhat) understand: the Markov chain.&lt;/p>

&lt;h2 id="a-primer-for-markov-chains" class="anchor">
 &lt;a href="#a-primer-for-markov-chains">
 A Primer for Markov Chains
 &lt;/a>
&lt;/h2>
&lt;p>In technical terms, a Markov chain is any random process that undergoes transitions from one state to another. It&amp;rsquo;s also a memoryless process, meaning that it only cares about its current state and not the states it has previously occupied.&lt;/p>
&lt;p>In less technical terms, a Markov chain is like your tokens position during a game of Monopoly. The next property that your token lands on is random (ie. its a random state transition), and every dice roll is independent of the previous roll (ie. its a memoryless process). Probability determines what the next dice roll is: theres a higher chance of rolling a 7 since there are 6 possible combinations (6/36 = 16.7% probability), whereas theres only one way to roll a 2 or 12 (1/36 = 2.8% probability).&lt;/p>
&lt;p>The marvelous aspect of Markov chains is that they can work with any item, not just numbers! Lets say you wanted to form sentences using Markov chains. Given a corpus of phrases (like the entire works of Shakespeare), a flowchart like the one below can be formed and used to create new phrases. Interestingly enough, this is the basis of prediction engines used on smartphone keyboards! It will learn from your phrasing habits and try to guess what your next word will be, based on how frequently youve typed similar words or phrases (just like the above xkcd comic).&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/making-haikuza-ii/chain.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/making-haikuza-ii/chain.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Visualization of words forming a Markov chain. (Source: Andrew Cholakian&amp;rsquo;s Blog)&lt;/p>
&lt;/div>
&lt;/p>

&lt;h2 id="harnessing-its-raw-indisputable-power" class="anchor">
 &lt;a href="#harnessing-its-raw-indisputable-power">
 Harnessing its Raw, Indisputable Power
 &lt;/a>
&lt;/h2>
&lt;p>Using Markov chains would allow me to use song lyrics as a training ground for creating alternate phrases. This would consequently form a probability-based flowchart like the one above, allowing me to generate new sentences by walking through each word state and letting probability determine my next word.&lt;/p>
&lt;p>To understand how to turn this into a programmable scenario, let&amp;rsquo;s take a look at an example. Given a phrase like:&lt;/p>
&lt;blockquote>
&lt;p>Mo butter, mo better, mo slipperier&lt;/p>&lt;/blockquote>
&lt;p>A Markov chain algorithm will take triplets of the phrase, use the first two words as the dictionary word, and the third word as the definition. If you&amp;rsquo;re familiar with using dictionaries in Python, the former is known as keys and the latter as values. This would result in the above phrase being translated to:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">(&amp;#39;mo&amp;#39;, &amp;#39;butter&amp;#39;) : [&amp;#39;mo&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;butter&amp;#39;, &amp;#39;mo&amp;#39;) : [&amp;#39;better&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;mo&amp;#39;, &amp;#39;better&amp;#39;) : [&amp;#39;mo&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;better&amp;#39;, &amp;#39;mo&amp;#39;) : [&amp;#39;slipperier&amp;#39;]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Okay, but this isn&amp;rsquo;t very captivating because there&amp;rsquo;s a 1:1 ratio of keys to values, which means that there will never be any mixing and matching of words since the probability of the next word is always 1. Alternatively, if we have a sentence such as:&lt;/p>
&lt;blockquote>
&lt;p>Living in a land of butter is like living in a paradise with flying unicorns&lt;/p>&lt;/blockquote>
&lt;p>Then the resulting dictionary will look like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">(&amp;#39;Living&amp;#39;, &amp;#39;in&amp;#39;) : [&amp;#39;a&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;in&amp;#39;, &amp;#39;a&amp;#39;) : [&amp;#39;land&amp;#39;, &amp;#39;paradise&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;a&amp;#39;, &amp;#39;land&amp;#39;) : [&amp;#39;of&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;land&amp;#39;, &amp;#39;of&amp;#39;) : [&amp;#39;butter&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;of&amp;#39;, &amp;#39;butter&amp;#39;) : [&amp;#39;is&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;butter&amp;#39;, &amp;#39;is&amp;#39;) : [&amp;#39;like&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;is&amp;#39;, &amp;#39;like&amp;#39;) : [&amp;#39;living&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;like&amp;#39;, &amp;#39;living&amp;#39;) : [&amp;#39;in&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;living&amp;#39;, &amp;#39;in&amp;#39;) : [&amp;#39;a&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;a&amp;#39;, &amp;#39;paradise&amp;#39;) : [&amp;#39;with&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;paradise&amp;#39;, &amp;#39;with&amp;#39;) : [&amp;#39;flying&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(&amp;#39;with&amp;#39;, &amp;#39;flying&amp;#39;) : [&amp;#39;unicorns&amp;#39;]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The interesting part of this phrase (other than the wildly imaginative scenario) is the second line item, where the key &lt;em>(in, a)&lt;/em> has two possible values, either land or paradise. So if I&amp;rsquo;m wandering around a word-based flowchart and come across the pair of words in a, my next word can either be land or paradise. Now, this was just for a 15 word phrase, so imagine the magnitude of choices if lyrics for an entire song was used (where more repetitions are prevalent), or all lyrics from an artists full discography, or even a body of text with a +1,000,000 word count meant for this purpose! More phrases and more words result in more possible combinations, which is excellent news for attempting to create pseudo-random sentences.&lt;/p>
&lt;p>The code below for the Markov chain algorithm was adapted from Agiliqs Blog:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">random&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">Markov&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">object&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">string&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cache&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">word_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">database&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cache&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">triples&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34; Generates triples from the given data string.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">yield&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">database&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">w1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">w2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">w3&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">triples&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">w1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">w2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">key&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">w3&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">w3&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">generate_markov_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">25&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">seed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">randint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">word_size&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">seed_word&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">next_word&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">seed&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">words&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">seed&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">w1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">w2&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">seed_word&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">next_word&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gen_words&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">xrange&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gen_words&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">w1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">w1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">w2&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">w2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">choice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="p">[(&lt;/span>&lt;span class="n">w1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">w2&lt;/span>&lt;span class="p">)])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gen_words&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">w2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">gen_words&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="teaching-markov-the-art-of-haikus" class="anchor">
 &lt;a href="#teaching-markov-the-art-of-haikus">
 Teaching Markov the Art of Haikus
 &lt;/a>
&lt;/h2>
&lt;p>Time to make some progress in making some beautifully robotic poetry! The plan for @thehaikuza V0.2 is to create a Markov chain dictionary using song lyrics and create randomized phrases from the resulting keys and values. Although this implementation still doesn&amp;rsquo;t involve a proper grammar model, it&amp;rsquo;s an improvement from the previous method because of its more structured approach. By simply mixing and matching phrases that once made sense before, there&amp;rsquo;s a much higher probability that the resulting phrase will also make some level of sense.&lt;/p>
&lt;p>The success rate should now go from laughable to respectfully laughable! Progress is progress.&lt;/p>
&lt;p>&lt;em>Check out the full repo on &lt;a href="https://github.com/justinmklam/project-haikuza">Github&lt;/a>!&lt;/em>&lt;/p></content:encoded></item><item><title>The Making of Project Haikuza: Part 1</title><link>https://justinmklam.com/posts/2015/making-haikuza-i/</link><pubDate>Sun, 12 Jul 2015 23:49:52 -0700</pubDate><guid>https://justinmklam.com/posts/2015/making-haikuza-i/</guid><description>&lt;p>&lt;em>The format of this series is an outline of my thought process during the development of &lt;a href="https://justinmklam.com/projects/software/haikuza/">@thehaikuza&lt;/a>.&lt;/em>&lt;/p>
&lt;blockquote style="text-align:center">
Haikus are simple
&lt;br>Even children can write them
&lt;br>maybe programs too?
&lt;/blockquote>
&lt;p>Nothing is cooler than algorithmic poetry. Except for maybe Carl Sagan. I heard he was a pretty cool guy.&lt;/p>
&lt;p>I was listening to the radio while driving home one Sunday evening, and an ad came up for a university that was submitting computer-generated poetry to a literature competition. I&amp;rsquo;ve recently been fascinated by the many intricacies of the English language, so the thought of somehow teaching a computer how to construct proper phrases seemed like an elusive task. But hey, if Google is also working on natural language processing, then how hard can it be?&lt;/p></description><content:encoded>&lt;p>&lt;em>The format of this series is an outline of my thought process during the development of &lt;a href="https://justinmklam.com/projects/software/haikuza/">@thehaikuza&lt;/a>.&lt;/em>&lt;/p>
&lt;blockquote style="text-align:center">
Haikus are simple
&lt;br>Even children can write them
&lt;br>maybe programs too?
&lt;/blockquote>
&lt;p>Nothing is cooler than algorithmic poetry. Except for maybe Carl Sagan. I heard he was a pretty cool guy.&lt;/p>
&lt;p>I was listening to the radio while driving home one Sunday evening, and an ad came up for a university that was submitting computer-generated poetry to a literature competition. I&amp;rsquo;ve recently been fascinated by the many intricacies of the English language, so the thought of somehow teaching a computer how to construct proper phrases seemed like an elusive task. But hey, if Google is also working on natural language processing, then how hard can it be?&lt;/p>
&lt;p>The idea of computer generated text is often a humourous one. Spamming your smartphone&amp;rsquo;s predictive keyboard is usually enough justification to avoid trying to computationally bang out the works of Shakespeare. As laughable as the monkeys at a typewriter idea is, maybe weve become smart enough to at least get close to this literary pipe dream&amp;hellip;&lt;/p>

&lt;h1 id="prequel-the-prudence-of-pivoting" class="anchor">
 &lt;a href="#prequel-the-prudence-of-pivoting">
 Prequel: The Prudence of Pivoting
 &lt;/a>
&lt;/h1>
&lt;p>Needless to say, it can take a few tries to end up at a decent idea, and @thehaikuza was no exception. After hearing about the poetry contest, I wanted to develop something related to computational linguistics, but I was partial to run-of-the-mill poetry. High school English classes had given me a negative bias for this type of literary art, so naturally I was in search for a different end product.&lt;/p>
&lt;p>Rap songs were the first to come to mind. After all, songs are simply poems accompanied by musical harmonies and melodies, so I wasn&amp;rsquo;t too far off from the realm of poetic computations. However, I eventually realized that writing rap songs from scratch (not to mention by an algorithm) would have mountains of difficulty, especially since my knowledge of natural language processing was currently at ground zero. I decided to scale back and start off small with the most simple form of poetry known to (probably) every North American student: the haiku.&lt;/p>
&lt;p>Haikus are great because they are short in length and only have one simple rule: don&amp;rsquo;t break the 5/7/5 syllable count per line. One of my coworkers suggested using Twitter as my platform for haiku generation, which would prove to be much easier than opening another can of worms of trying to develop my own web app. Since there&amp;rsquo;s already a Twitter API library for Python, and with each average haiku being under the threshold of 140 characters, my decision was made before I even had to think about it.&lt;/p>
&lt;p>With the advent of using Twitter, I had a few more options I could play with. My first idea was to make haikus based on the trending topics on Twitter. This would give my Twitter-bot the dynamic quality that&amp;rsquo;s inherent in the fast-paced nature of the internet, rather than just creating stale haikus about nature. It could retrieve the trending topics, gather all the relevant tweets, and then use the collected phrases to formulate a relevant haiku. In theory, this seemed like a great idea; in practice, not so much! After some prototyping, I quickly learned that the broken English, frequent spelling mistakes, and chaotic nature of &amp;lt; 140 character messages wouldnt serve as an adequate corpus for phrase generation.&lt;/p>
&lt;p>Okay, well forget about that idea. But I still wanted a venue to stay relevant on the internet. I figured songs can also be relevant, especially if I would be grabbing ones that are currently playing on the radio. Adding an interactive element to @thehaikuza by making it able to respond to song requests would just be icing on the cake. Thus, @thehaikuza was born!&lt;/p>
&lt;p>Or so I thought. Turned out it was still an embryo.&lt;/p>

&lt;h1 id="makin-bacon-haikus-v01" class="anchor">
 &lt;a href="#makin-bacon-haikus-v01">
 Makin Bacon Haikus V0.1
 &lt;/a>
&lt;/h1>
&lt;p>The first research topic on my ever-growing list was to figure out what makes a sensible sentence. In elementary school, we&amp;rsquo;re taught that the most basic phrase consists of a subject and verb. In the sentence below, for example, the boy is the subject and is sweating is the action.&lt;/p>
&lt;blockquote style="text-align:center">
The boy is sweating.
&lt;/blockquote>
&lt;p>Okay, this evidently isnt new knowledge to anyone. But what if we break down the phrase into types of words? Referencing a universal &lt;strong>part of speech (POS)&lt;/strong> tagging system, we get:&lt;/p>
&lt;blockquote style="text-align:center">
[The, determinant] [boy, noun] [is, verb] [sweating, verb]
&lt;/blockquote>
&lt;p>Interesting. Maybe I can create a Mad Lib-based language model that can learn the sentence structures of existing haikus, then use an external wordset to substitute the corresponding words. For poetry, and notably haikus, grammar isnt as important as a passive aggressive letter to your not-so-favourite mobile carrier, so I might be able to get away with this rudimentary language model. (Spoiler alert: I couldnt.)&lt;/p>

&lt;h2 id="haiku-training" class="anchor">
 &lt;a href="#haiku-training">
 Haiku Training
 &lt;/a>
&lt;/h2>
&lt;p>The plan was to create a pseudo-smart script (of sorts) that did the following:&lt;/p>
&lt;ol>
&lt;li>Import a collection of human-written haikus&lt;/li>
&lt;li>Extract the POS tags for each line&lt;/li>
&lt;li>Construct a database of 5 and 7 syllable POS phrases&lt;/li>
&lt;/ol>
&lt;p>The implementation looked something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">nltk&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">LearnHaiku&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">object&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fname&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">haikus&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fname&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pos_five&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pos_seven&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">log&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">analyze&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@staticmethod&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fname&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">haikus&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fname&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">inputfile&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">line&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">inputfile&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">haikus&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">haikus&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">analyze&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">line5s&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">line7s&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">errors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">is_line7s&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">numrows&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">haikus&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">numrows&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Only parse if not a blank line&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">haikus&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tags&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">nltk&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pos_tag&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">haikus&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39; &amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">tagset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;universal&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">is_line7s&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">line7s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">tags&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">is_line7s&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="mi">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">line5s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">tags&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">UnicodeDecodeError&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">errors&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">log_str&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Couldn&amp;#39;t parse line(s) &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span> &lt;span class="o">%&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;, &amp;#39;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">errors&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">line5s&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">line7s&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">log_str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;__main__&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hk&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">LearnHaiku&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;[REF] Haiku Training Examples V0.1.txt&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span> &lt;span class="n">hk&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pos_five&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span> &lt;span class="n">hk&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pos_seven&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>From this, I could then randomly generate haiku POS templates that I would be able to use with an external set of words. From the (rather awfully juvenile) example haikus below:&lt;/p>
&lt;blockquote style="text-align:center">
An old silent pond
&lt;br>A frog jumps into the pond,
&lt;br>splash! Silence again.
&lt;br>
&lt;br>The autumn moonlight
&lt;br>a large worm digs silently
&lt;br>into the chestnut.
&lt;br>
&lt;br>Lightning flashes bright
&lt;br>what I thought were white faces
&lt;br>are plumes of plain grass.
&lt;/blockquote>
&lt;p>The POS tag retrieval for the phrases were:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Five Syllable Lines:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> [u&amp;#39;DET&amp;#39;, u&amp;#39;ADJ&amp;#39;, u&amp;#39;NOUN&amp;#39;, u&amp;#39;NOUN&amp;#39;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> [u&amp;#39;NOUN&amp;#39;, u&amp;#39;NOUN&amp;#39;, u&amp;#39;NOUN&amp;#39;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> [u&amp;#39;DET&amp;#39;, u&amp;#39;NOUN&amp;#39;, u&amp;#39;NOUN&amp;#39;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> [u&amp;#39;NOUN&amp;#39;, u&amp;#39;VERB&amp;#39;, u&amp;#39;NOUN&amp;#39;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> [u&amp;#39;VERB&amp;#39;, u&amp;#39;ADV&amp;#39;, u&amp;#39;ADP&amp;#39;, u&amp;#39;NOUN&amp;#39;, u&amp;#39;NOUN&amp;#39;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Seven Syllable Lines:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> [u&amp;#39;DET&amp;#39;, u&amp;#39;NOUN&amp;#39;, u&amp;#39;VERB&amp;#39;, u&amp;#39;ADP&amp;#39;, u&amp;#39;DET&amp;#39;, u&amp;#39;ADJ&amp;#39;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> [u&amp;#39;DET&amp;#39;, u&amp;#39;ADJ&amp;#39;, u&amp;#39;NOUN&amp;#39;, u&amp;#39;VERB&amp;#39;, u&amp;#39;NOUN&amp;#39;, u&amp;#39;NOUN&amp;#39;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> [u&amp;#39;PRON&amp;#39;, u&amp;#39;PRON&amp;#39;, u&amp;#39;VERB&amp;#39;, u&amp;#39;NOUN&amp;#39;, u&amp;#39;NOUN&amp;#39;]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Almost there to our first haiku!&lt;/p>

&lt;h2 id="first-pass-haiku-generation" class="anchor">
 &lt;a href="#first-pass-haiku-generation">
 First Pass Haiku Generation
 &lt;/a>
&lt;/h2>
&lt;p>I wrote another simple script to take in song lyrics, apply a POS tag for each of the lyrics, and create a word list sorted by the corresponding tags. I could then plug-and-chug words from the generated list into an appropriate slot in the haiku template.&lt;/p>
&lt;p>For the first test, I thought it was only appropriate to use lyrics from Drake&amp;rsquo;s classic &lt;em>Started From The Bottom&lt;/em>. The output:&lt;/p>
&lt;blockquote style="text-align:center">
Story screaming real
&lt;br>Out uncle own stay worry
&lt;br>Through bottom lips heard
&lt;br>
&lt;br>When that standing as
&lt;br>Poppin coming we begging
&lt;br>From started keep if
&lt;br>
&lt;br>Fucking ball standing
&lt;br>Niggas explaining guys crowd
&lt;br>Say girl chain rumors
&lt;/blockquote>
&lt;p>Oh dear lord, what a mellifluous balance of repulsive vocabulary and lackadaisical grammar. Those were definitely some of the worst haikus I&amp;rsquo;ve read&amp;hellip; Maybe if I try using &lt;em>Blank Space&lt;/em> by Taylor Swift:&lt;/p>
&lt;blockquote style="text-align:center">
Half bottom boy flames
&lt;br>Feel what bottom you darling
&lt;br>crying was from is crying
&lt;br>
&lt;br>Explaining queen lights
&lt;br>Stays leave could cherry stay
&lt;br>Town cause lets dress the
&lt;br>
&lt;br>Crystal blank starts: flames
&lt;br>thats explaining funny hand
&lt;br>Cherry at god im
&lt;/blockquote>
&lt;p>I lied. Pretty sure those were worse.&lt;/p>
&lt;p>Obviously, the generated haikus were complete word salad. With absolutely no concept of context or proper grammar, @thehaikuza V0.1 was light years away from reliably generating poems that made at least some remote level of sense. There was definitely more to constructing phrases than stuffing in random words into corresponding POS slots, so I needed another approach.&lt;/p>
&lt;p>Hm, I keep hearing about this Markov Chain&amp;hellip; I wonder if he&amp;rsquo;s related to 2 Chainz?&lt;/p>
&lt;p>&lt;em>Click here to read &lt;a href="https://justinmklam.com/posts/2015/making-haikuza-ii/">The Making of Project Haikuza: Part 2&lt;/a>!&lt;/em>&lt;/p></content:encoded></item><item><title>Project Haikuza</title><link>https://justinmklam.com/posts/2015/07/haikuza/</link><pubDate>Fri, 10 Jul 2015 11:42:26 -0700</pubDate><guid>https://justinmklam.com/posts/2015/07/haikuza/</guid><description>&lt;p>&lt;strong>Objective:&lt;/strong> Develop an algorithm to generate haikus using song lyrics.&lt;/p>&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> Because computational linguistics are cool.&lt;/p>&lt;/p>
&lt;p>&lt;strong>Project:&lt;/strong> &lt;a href="https://twitter.com/thehaikuza">twitter.com/thehaikuza&lt;/a>&lt;/p>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Scrapes &lt;a href="http://www.vancouver.virginradio.ca/broadcasthistory.aspx">Virgin Radio&amp;rsquo;s broadcast history&lt;/a> to find recently played songs&lt;/li>
&lt;li>Creates a song-based haiku queue in &lt;a href="https://docs.google.com/spreadsheets/d/1HazfuywY_MrmQ49fxSpHOMA8QXBUYVhEDx1e4qhjbqU/edit?usp=sharing">Google Sheets&lt;/a>&lt;/li>
&lt;li>Generates a haiku using the queue as a reference and posts it on Twitter&lt;/li>
&lt;li>Checks for new tweets every 5 minutes and generates a relevant haiku, if requested&lt;/li>
&lt;li>Finds all song lyrics from &lt;a href="http://lyrics.wikia.com/Lyrics_Wiki">Lyrics Wikia&lt;/a>&lt;/li>
&lt;li>Runs on a Raspberry Pi&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Challenges:&lt;/strong>&lt;/p></description><content:encoded>&lt;p>&lt;strong>Objective:&lt;/strong> Develop an algorithm to generate haikus using song lyrics.&lt;/p>&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> Because computational linguistics are cool.&lt;/p>&lt;/p>
&lt;p>&lt;strong>Project:&lt;/strong> &lt;a href="https://twitter.com/thehaikuza">twitter.com/thehaikuza&lt;/a>&lt;/p>
&lt;p>&lt;strong>Features:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Scrapes &lt;a href="http://www.vancouver.virginradio.ca/broadcasthistory.aspx">Virgin Radio&amp;rsquo;s broadcast history&lt;/a> to find recently played songs&lt;/li>
&lt;li>Creates a song-based haiku queue in &lt;a href="https://docs.google.com/spreadsheets/d/1HazfuywY_MrmQ49fxSpHOMA8QXBUYVhEDx1e4qhjbqU/edit?usp=sharing">Google Sheets&lt;/a>&lt;/li>
&lt;li>Generates a haiku using the queue as a reference and posts it on Twitter&lt;/li>
&lt;li>Checks for new tweets every 5 minutes and generates a relevant haiku, if requested&lt;/li>
&lt;li>Finds all song lyrics from &lt;a href="http://lyrics.wikia.com/Lyrics_Wiki">Lyrics Wikia&lt;/a>&lt;/li>
&lt;li>Runs on a Raspberry Pi&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Challenges:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Formulaically counting syllables&lt;/li>
&lt;li>Developing a context-free language model&lt;/li>
&lt;li>Creating phrases that actually make sense&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Framework:&lt;/strong> Python 2.7.9&lt;/p>
&lt;p>&lt;strong>Source:&lt;/strong> &lt;a href="https://github.com/justinmklam/project-haikuza">Github&lt;/a>&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/07/haikuza/IMG_20150803_180047-1024x768_hu_c758603cb0286235.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/07/haikuza/IMG_20150803_180047-1024x768_hu_c758603cb0286235.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Raspberry Pi 1 Model B running @thehaikuza, 24/7!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Interested about the development of @thehaikuza? Follow it on &lt;a href="https://justinmklam.com/posts/2015/making-haikuza-i">The Making of Project Haikuza: Part 1&lt;/a>!&lt;/p></content:encoded></item><item><title>Places To Be: Howe Sound Crest Trail</title><link>https://justinmklam.com/posts/2015/howe-sound-trail/</link><pubDate>Wed, 08 Jul 2015 23:14:15 -0700</pubDate><guid>https://justinmklam.com/posts/2015/howe-sound-trail/</guid><description>&lt;p>&lt;em>Originally posted on &lt;a href="https://www.ubyssey.ca/sports/places-howe-sound-crest-trail/">The Ubyssey&lt;/a>.&lt;/em>&lt;/p>
&lt;p>Oh boy, some beginner trip this turned out to be.&lt;/p>
&lt;p>As a new member of the VOC, it quickly became apparent to me how &amp;ldquo;beginner&amp;rdquo; can take on drastically different meanings. In retrospect, I should have read the wiki page on &amp;ldquo;The 5 Definitions of Beginner Friendly&amp;rdquo; before agreeing to this little excursion, but a bit of Type II fun (not fun during, but makes for a great story later) never killed anyone, right? To be fair, Mike, the trip organizer, did specify this was a &amp;ldquo;beginner plus&amp;rdquo; trip, but my willingness (or call it ignorance) got the better of me.&lt;/p></description><content:encoded>&lt;p>&lt;em>Originally posted on &lt;a href="https://www.ubyssey.ca/sports/places-howe-sound-crest-trail/">The Ubyssey&lt;/a>.&lt;/em>&lt;/p>
&lt;p>Oh boy, some beginner trip this turned out to be.&lt;/p>
&lt;p>As a new member of the VOC, it quickly became apparent to me how &amp;ldquo;beginner&amp;rdquo; can take on drastically different meanings. In retrospect, I should have read the wiki page on &amp;ldquo;The 5 Definitions of Beginner Friendly&amp;rdquo; before agreeing to this little excursion, but a bit of Type II fun (not fun during, but makes for a great story later) never killed anyone, right? To be fair, Mike, the trip organizer, did specify this was a &amp;ldquo;beginner plus&amp;rdquo; trip, but my willingness (or call it ignorance) got the better of me.&lt;/p>
&lt;p>The item on this weekend&amp;rsquo;s menu was the Howe Sound Crest Trail, featuring a 29 km walk-in-the-park and a healthy serving of root gardens, rock faces and boulder fields, garnished with majestic summits along the way. We split into two groups at the pre-trip meeting. Team Slog would travel north to south and Team Carrywater would travel in the opposite direction. Team Slog would be starting from sea level with a climb of 1500 metres and Team Carrywater would have no water refill spots during the first day. Team Slog was the more popular choice at a ratio of 3 cars to 2; obviously elevation was preferable to thirstiness.&lt;/p>
&lt;p>Team Carrywater arrived at the Cypress trail head around 8 a.m. With our packs on our backs and boots on the roots, we began our adventure. The first viewpoint was St. Mark&amp;rsquo;s Summit, about 5.5 km from the trailhead. It was a relatively easy-going trail even with the constant elevation gain and handful of log hurdles. We enjoyed the pleasant view of Howe Sound while loading up on some snacks. Based on the presence of touristy folks, I concluded we still had quite a ways to go. The most difficult part for me was trying to figure out how to stay balanced while carrying a 60-pound backpack; being top heavy was definitely something I wasn&amp;rsquo;t used to. Not to worry, I still had a few kilometers to get used to it before the real fun began.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/howe-sound-trail/IMG_5801_hu_6fe4b1c10bd73e89.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/howe-sound-trail/IMG_5801_hu_6fe4b1c10bd73e89.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">St. Mark&amp;rsquo;s Summit&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>As we continued northward toward Mt. Unnecessary the trail began to narrow and evolve into a slightly trickier, but manageable, beast. The forested area began to thin out, and a couple forehead sweat-wipes later we were hiking out along the mountain ridge. While I was too busy focusing on my footing and trying not to tip over like an inverted pendulum, I hadn&amp;rsquo;t even noticed how spectacular the view was. Hiking through lush greenery was one thing, but traversing a rocky slab surrounded by an expansive view of the mountains and sea was on a whole different level. We approached a fairly steep climb along the ridge, but with the dry conditions and help of our trusty hiking boots, we didn&amp;rsquo;t leave anyone at the bottom. A few more sweat-wipes later and we were soaking in the glorious view of Howe Sound. A herd of mildly ominous-looking clouds peered from the east, covering portions of the Lions in the distance. The Weather Network reported a dry, sunny weekend, so why should we have anything to be concerned about? It&amp;rsquo;s not like they&amp;rsquo;ve been wrong about the weather in the past&amp;hellip;. Did I remember to bring a rain jacket?&lt;/p>
&lt;p>Continuing on from Mt. Unnecessary to the Lions was mostly harmless, aside from losing sight of the next trail marker and the rest of my group, accompanied by the reassuring sound of complete silence in the wilderness. Fortunately a pair of hikers (who were ironically lost themselves) coming from the opposite direction helped locate the next trail marker. Team Carrywater somehow managed to regroup along the trail and we collectively made it to the next summit. We enjoyed the gorgeous view while consuming our much deserved lunches.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/howe-sound-trail/IMG_5824.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/howe-sound-trail/IMG_5824.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>There was some discussion about scaling up the West Lion, but glancing over to what looked like the future death of me, I took the train back to Nopeville and happily continued eating my PB&amp;amp;J pudding. The more adventurous members of Team Carrywater quickly finished their meals and began their ascent up the West Lion. The three of us who held back watched from a comfortable distance, as if we were filming a National Geographic series on the spider man-like abilities of mountain goats. Shortly after sub-team Sketchy Scramblers were out of sight, sub-team You Only Live Once continued along the the trail toward our destination for the evening.&lt;/p>
&lt;p>The terrain was magnificently diverse, ranging from switchbacks through flowery meadows to White Walker-esque boulder fields. Step by step, I began to fully understand why this was a &amp;ldquo;beginner plus&amp;rdquo; trip. Having decent cardio was the only thing keeping me alive. We arrived at James Peak to a panoramic view of our days work. Seeing the mountains from this perspective was something else, and infinitely better than the view from the city that I&amp;rsquo;m accustomed to.&lt;/p>
&lt;p>Many sweat-wipes and bushwhacks later, we finally arrived at Magnesia Meadows, our tent site for the night. Some of us relaxed while others took a side trip up Mt. Harvey. We met up with Team Slog and shared the evening with the rest of the VOC party. As the night grew older, we gathered on a slab of rock and bundled up in sleeping bag burritos to watch the sunset and stars.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/howe-sound-trail/IMG_5898_hu_6e2603030754bcb7.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/howe-sound-trail/IMG_5898_hu_6e2603030754bcb7.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>As beautiful as the evening was, the fact that it was the summer solstice literally made it the worst day in the year to be stargazing. The extended sunset sure was splendid though!&lt;/p>
&lt;p>The morning came and we parted ways with Team Slog after breakfast. Team Carrywater marched through the meadows until reaching the base of Mt. Brunswick. We dropped our packs and hiked up the surprisingly steep trail. Have I mentioned how pleasant it was to walk up a waterfall of loose rocks? Again, 100 per cent beginner plus but the scramble was totally worth it, especially the 360-degree view.&lt;/p>
&lt;p>Going down a loose, steep trail was a challenge. I considered trying to use gravity and momentum to my advantage, but common sense suggested otherwise (with good reason). We eventually all made it down back to our packs where we enjoyed the remains of our lunch and continued on the latter half of our trip.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/howe-sound-trail/IMG_5913_hu_24e174e3ed3004b3.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/howe-sound-trail/IMG_5913_hu_24e174e3ed3004b3.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Woo hoo, it was all downhill from here! Literally, not figuratively.&lt;/p>
&lt;p>As a group, the hardest decision was which of the three beautiful lakes to swim in. We skipped the first lake with some hesitation, but the second was just as marvellous. The lake was refreshingly cold and the weightlessness of water was a treat to our tired bodies. We relaxed on the stranded logs and dried off in the sun before returning to our journey.&lt;/p>
&lt;p>The trail was much more leisurely than the previous day, and we all made it to Porteau Cove in one piece. We finished early so we ended our trip at Elwood&amp;rsquo;s and enjoyed their Sunday special, a $5.50 meal of steak and fries.&lt;/p></content:encoded></item><item><title>Co-op: Air Quality Calibration Chamber</title><link>https://justinmklam.com/posts/2015/mistywest-coop/</link><pubDate>Tue, 23 Jun 2015 21:30:17 -0700</pubDate><guid>https://justinmklam.com/posts/2015/mistywest-coop/</guid><description>&lt;p>&lt;strong>Background:&lt;/strong> Welcome to TZOA (pronounced &amp;lsquo;zoa&amp;rsquo;) - the world&amp;rsquo;s most advanced environment tracker. TZOA uses internal sensors to measure your air quality, temperature, humidity, atmospheric pressure, ambient light, and UV (sun) exposure, all in one wearable device.&lt;/p>
&lt;p>&lt;strong>Objective:&lt;/strong> Design and build a purified test chamber with zero air particulate to calibrate the TZOA devices against the DusTrak DRX 8533. An aerosolized cloud of ultrafine (0.3 - 20 μm) test dust was used to create the particle events.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Background:&lt;/strong> Welcome to TZOA (pronounced &amp;lsquo;zoa&amp;rsquo;) - the world&amp;rsquo;s most advanced environment tracker. TZOA uses internal sensors to measure your air quality, temperature, humidity, atmospheric pressure, ambient light, and UV (sun) exposure, all in one wearable device.&lt;/p>
&lt;p>&lt;strong>Objective:&lt;/strong> Design and build a purified test chamber with zero air particulate to calibrate the TZOA devices against the DusTrak DRX 8533. An aerosolized cloud of ultrafine (0.3 - 20 μm) test dust was used to create the particle events.&lt;/p>
&lt;p>&lt;strong>Source:&lt;/strong> &lt;a href="https://www.indiegogo.com/projects/tzoa-wearable-air-quality-tracker#/">Indiegogo Campaign Page&lt;/a>&lt;/p>
&lt;p>&lt;strong>Client:&lt;/strong> &lt;a href="https://www.tzoa.com/">TZOA&lt;/a>&lt;/p>
&lt;p>&lt;strong>Acknowledgements:&lt;/strong> This project was completed under MistyWest with the guidance of Taylor Cooper.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/mistywest-coop/2015_Tzoa_EnviroTracker_%285%29_hu_cbd3d00f937b554c.JPG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/mistywest-coop/2015_Tzoa_EnviroTracker_%285%29_hu_cbd3d00f937b554c.JPG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The TZOA paired with a smartphone to demonstrate the environment tracking dashboard.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/mistywest-coop/2015_Tzoa_EnviroTracker_%283%29_hu_39d76e30cf51374a.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/mistywest-coop/2015_Tzoa_EnviroTracker_%283%29_hu_39d76e30cf51374a.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Close up of the device in the test environment.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/mistywest-coop/2015_Tzoa_EnviroTracker_%282%29_hu_c6bd1456a764a495.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/mistywest-coop/2015_Tzoa_EnviroTracker_%282%29_hu_c6bd1456a764a495.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Overview of the test equipment inside the test chamber.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/mistywest-coop/IMG_20150501_155402_hu_6945c59c706491ef.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/mistywest-coop/IMG_20150501_155402_hu_6945c59c706491ef.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The Dylos air quality monitor showing zero particulate measurements. A HEPA filter was used to provide the filtration.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/mistywest-coop/TZOA_data_hu_97721efa435d96fb.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/mistywest-coop/TZOA_data_hu_97721efa435d96fb.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Correlation data between the TZOA vs DustTrak devices.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>External Ballistics Simulation</title><link>https://justinmklam.com/posts/2015/02/external-ballistics/</link><pubDate>Thu, 19 Feb 2015 11:38:35 -0700</pubDate><guid>https://justinmklam.com/posts/2015/02/external-ballistics/</guid><description>&lt;p>&lt;strong>Background:&lt;/strong> including (but not limited to) drag, gravity, air density, altitude, rotation of the bullet, and rotation of the Earth.&lt;/p>
&lt;p>One solution is to connect a computer to a manual targeting system (ie. a scope) and estimate the corrected target location by accounting for these external factors.  The user may then line the manual targeting system up with the corrected target location, hoping to the high heavens that the target will be hit upon releasing the projectile.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Background:&lt;/strong> including (but not limited to) drag, gravity, air density, altitude, rotation of the bullet, and rotation of the Earth.&lt;/p>
&lt;p>One solution is to connect a computer to a manual targeting system (ie. a scope) and estimate the corrected target location by accounting for these external factors.  The user may then line the manual targeting system up with the corrected target location, hoping to the high heavens that the target will be hit upon releasing the projectile.&lt;/p>&lt;/p>
&lt;p>&lt;strong>Objective:&lt;/strong> Determine the required release angle given the muzzle velocity and target distance, accounting for external factors.&lt;/p>&lt;/p>
&lt;p>&lt;strong>Limitations:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Only the corrected vertical distance is calculated&lt;/li>
&lt;li>Values have not yet been validated for accuracy&lt;/li>
&lt;li>Purely mathematical simulations lack the accuracy provided by simulations backed by empirical data&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Framework:&lt;/strong> Python(x,y) 2.7.9.0&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_0-deg-Scope-Angle_all_missed.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_0-deg-Scope-Angle_all_missed.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Visualization of release angles between 0 and 90° given the same muzzle velocity.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-simulation-flowchart.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-simulation-flowchart.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Flowchart of the algorithm to determine the required release angle.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_0-deg-Scope-Angle_all.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_0-deg-Scope-Angle_all.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Visualization of the algorithm on the release angle increment. The magenta trajectory is 5° higher than the red trajectory; the green is 2.5° higher than the magenta, and so on.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_0-deg-Scope-Angle-exaggerated.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_0-deg-Scope-Angle-exaggerated.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Predicted trajectory on flat ground.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_18-deg-Scope-Angle.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_18-deg-Scope-Angle.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Predicted trajectory when target is uphill from user.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_-9-deg-Scope-Angle.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2015/02/external-ballistics/Ballistics-Simulation_-9-deg-Scope-Angle.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Predicted trajectory when target is downhill from user.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>MECH 328: TrailRider Design Project</title><link>https://justinmklam.com/posts/2014/11/mech-328-trailrider/</link><pubDate>Mon, 24 Nov 2014 12:37:55 -0700</pubDate><guid>https://justinmklam.com/posts/2014/11/mech-328-trailrider/</guid><description>&lt;p>&lt;strong>Background:&lt;/strong> The TrailRider™ is a specialized device to provide accessibility to the wilderness for those with limited mobility.  It is intended for a seated rider propelled and balanced by assistants.&lt;/p>
&lt;p>&lt;strong>Objective:&lt;/strong> Expand the range of both riders and assistants who can ride safely use the Black Diamond TrailRider™.&lt;/p>
&lt;p>&lt;strong>How:&lt;/strong> As a team of seven mechanical engineering undergrads, we explored a range of areas for improvement and developed a new design to incorporate these features.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Background:&lt;/strong> The TrailRider™ is a specialized device to provide accessibility to the wilderness for those with limited mobility.  It is intended for a seated rider propelled and balanced by assistants.&lt;/p>
&lt;p>&lt;strong>Objective:&lt;/strong> Expand the range of both riders and assistants who can ride safely use the Black Diamond TrailRider™.&lt;/p>
&lt;p>&lt;strong>How:&lt;/strong> As a team of seven mechanical engineering undergrads, we explored a range of areas for improvement and developed a new design to incorporate these features.&lt;/p>
&lt;p>&lt;strong>Existing Problems:&lt;/strong>&lt;/p>
&lt;ul style="text-align: left;">
	&lt;li>Device relies entirely on assistants for balance, which may be physically demanding for some assistants&lt;/li>
	&lt;li>Riders generally find the current device too passive&lt;/li>
	&lt;li>Single-wheel design provides difficulty in rolling over abrupt trail features&lt;/li>
	&lt;li>Single-wheel provides limited cushioning over rough terrain&lt;/li>
	&lt;li>Device is difficult to transport and store when space is limited&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Our Improvements:&lt;/strong>&lt;/p>
&lt;ul>
	&lt;li style="text-align: left;">Incorporation of a 255 W electric power assist feature&lt;/li>
	&lt;li style="text-align: left;">Implementation of lever-propulsion handles for rider engagement&lt;/li>
	&lt;li style="text-align: left;">Integrated frame and suspension design for increased shock absorption&lt;/li>
	&lt;li style="text-align: left;">Increased portability&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Client:&lt;/strong> British Columbia Mobility Opportunities Society (BCMOS)&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Original-TR-in-use.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Original-TR-in-use.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Original TrailRider in use. (Image courtesy of BCMOS)&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Original-TR-CAD.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Original-TR-CAD.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">How the original TrailRider folds for transportation.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/11/mech-328-trailrider/TrailRider-Overview.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/11/mech-328-trailrider/TrailRider-Overview.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Our TrailRider in the folded configuration, 56% smaller than the current design.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Kickstand-diagram.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Kickstand-diagram.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Diagram showing the how the kickstand is integrated with the assistant&amp;rsquo;s push handles.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Side-view_annotated-large.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Side-view_annotated-large.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Overview of our redesigned TrailRider.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Suspension_annotated-large.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/11/mech-328-trailrider/Suspension_annotated-large.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Close up of the integrated frame and suspension system.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>Co-op: Drill Cover User Study</title><link>https://justinmklam.com/posts/2014/coop-drill-cover/</link><pubDate>Sat, 27 Sep 2014 12:39:34 -0700</pubDate><guid>https://justinmklam.com/posts/2014/coop-drill-cover/</guid><description>Drill availability is a major limitation in developing counties, where surgeons turn to using manual hand-crank drills to perform surgeries. A common hardware drill equipped with a waterproof, sterilizable fabric cover provides a cost-effective solution in the operating room.</description><content:encoded>&lt;p>&lt;strong>Background:&lt;/strong> Drill availability is a major limitation in developing countries, where surgeons turn to using manual hand-crank drills to perform surgeries.  A common hardware drill equipped with a waterproof, sterilizable fabric cover provides a cost-effective solution in the operating room.&lt;/p>
&lt;p>&lt;strong>Objective:&lt;/strong> Develop, conduct, and analyze a study to quantify the drilling performance of the drill cover solution and compare it against a commercial surgical drill and a manual hand-crank drill.&lt;/p>
&lt;p>&lt;strong>Abstract&lt;/strong>: &lt;a href="http://canjsurg.ca/wp-content/uploads/2015/10/58-4-S157.pdf">Surgical Device Innovation for Low-Resource Settings: An Alternative for Bone Drilling&lt;/a>&lt;/p>
&lt;p>&lt;strong>Company&lt;/strong>: &lt;a href="http://arbutusmedical.ca">Arbutus Medical&lt;/a>&lt;/p>

&lt;h1 id="the-drill-cover-solution" class="anchor">
 &lt;a href="#the-drill-cover-solution">
 The Drill Cover Solution
 &lt;/a>
&lt;/h1>
&lt;p>From the Arbutus Medical &lt;a href="http://arbutusmedical.ca/human-health/products/drill-cover-hex">product page&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>The DrillCover Hex is a sterilizable and reusable fully-sealed barrier that transforms a hardware drill into surgical grade drill. Our kit includes a drill and cover to provide the precision, sterility, and adaptability needed in low-resource settings.&lt;/p>&lt;/blockquote>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/coop-drill-cover/drillcover.PNG>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/coop-drill-cover/drillcover.PNG>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Detailed view of the drill cover.&lt;/p>
&lt;/div>
&lt;/p>

&lt;h1 id="the-user-study" class="anchor">
 &lt;a href="#the-user-study">
 The User Study
 &lt;/a>
&lt;/h1>
&lt;p>Owing to limited access to surgical power drills, orthopedic surgeons in low-resource settings commonly use manual drills or nonsterile hardware drills. We propose the use of a drill cover solution: a sterilizable fabric bag plus sealed surgical chuck adaptor to permit the safe use of hardware drills for orthopedic surgery.
The purpose of this study was to compare the drilling performance of covered hardware drills with manual drills and standard surgical power drills.
&lt;br>&lt;br>&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/coop-drill-cover/DC-Study_2.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/coop-drill-cover/DC-Study_2.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">From left to right: commercial surgical drill, drill cover solution, manual hand-crank drill.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/coop-drill-cover/DC-Study_3.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/coop-drill-cover/DC-Study_3.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Operation of the test apparatus.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/coop-drill-cover/DC-Study_4.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/coop-drill-cover/DC-Study_4.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Method to easily measure the plunge depth through the surrogate bone. The platform moves down as the drill bit pushes against it. The calipers measure the resulting depth.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/coop-drill-cover/DC-Study_1.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/coop-drill-cover/DC-Study_1.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Stamping method to consistently mark targets for the drilling tests&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/coop-drill-cover/Hole-quality-comparison.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/coop-drill-cover/Hole-quality-comparison.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Comparison of hole quality. A well drilled hole face (left) compared to two holes drilled off axis (right).&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/coop-drill-cover/Bone-model-render-1024x520_hu_d515674c5792fdea.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/coop-drill-cover/Bone-model-render-1024x520_hu_d515674c5792fdea.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Test apparatus, modeled in SolidWorks.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>&lt;em>Product photos by Masashi Karasawa&lt;/em>&lt;/p></content:encoded></item><item><title>Bowling With car2go</title><link>https://justinmklam.com/posts/2014/08/car2go/</link><pubDate>Fri, 29 Aug 2014 12:37:42 -0700</pubDate><guid>https://justinmklam.com/posts/2014/08/car2go/</guid><description>&lt;p>&lt;strong>Objective&lt;/strong>: Design a roof-mounted bowling rig for a car2go marketing event.&lt;/p>
&lt;p>&lt;strong>Constraints&lt;/strong>: Use a non-permanent method to securely attach the apparatus to the roof.&lt;/p>
&lt;p>&lt;strong>Venue&lt;/strong>: 2014 Vancouver Fringe Festival&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/car2go/20140907_165320_hu_dabc73c1ddcfd7a7.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/car2go/20140907_165320_hu_dabc73c1ddcfd7a7.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Bowling rig set up and ready to roll.&lt;/p>
&lt;/div>
&lt;/p>
&lt;div style='position:relative;padding-bottom:505px'>&lt;iframe src='https://gfycat.com/ifr/VastFoolishJaeger' frameborder='0' scrolling='no' width='100%' height='500px' style='position:absolute;top:0;left:0;' allowfullscreen>&lt;/iframe>&lt;/div>
&lt;p class="caption">Car2go bowling in action.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Objective&lt;/strong>: Design a roof-mounted bowling rig for a car2go marketing event.&lt;/p>
&lt;p>&lt;strong>Constraints&lt;/strong>: Use a non-permanent method to securely attach the apparatus to the roof.&lt;/p>
&lt;p>&lt;strong>Venue&lt;/strong>: 2014 Vancouver Fringe Festival&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/car2go/20140907_165320_hu_dabc73c1ddcfd7a7.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/car2go/20140907_165320_hu_dabc73c1ddcfd7a7.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Bowling rig set up and ready to roll.&lt;/p>
&lt;/div>
&lt;/p>
&lt;div style='position:relative;padding-bottom:505px'>&lt;iframe src='https://gfycat.com/ifr/VastFoolishJaeger' frameborder='0' scrolling='no' width='100%' height='500px' style='position:absolute;top:0;left:0;' allowfullscreen>&lt;/iframe>&lt;/div>
&lt;p class="caption">Car2go bowling in action.&lt;/p></content:encoded></item><item><title>Award Winning Bamboo Bike</title><link>https://justinmklam.com/posts/2014/08/bamboo-bike/</link><pubDate>Mon, 18 Aug 2014 22:12:57 -0700</pubDate><guid>https://justinmklam.com/posts/2014/08/bamboo-bike/</guid><description>&lt;p>&lt;strong>Objective&lt;/strong>: Exceed the Vancourite-hipster threshold by building a fixed-gear bicycle using bamboo and carbon fibre.&lt;/p>
&lt;p>&lt;strong>Challenges&lt;/strong>:&lt;/p>
&lt;ul>
	&lt;li style="text-align: left;">Constructing a frame jig to allow freedom around joints for carbon-fibre layup&lt;/li>
	&lt;li style="text-align: left;">Accounting for the non-uniformity of bamboo poles at the frame lugs&lt;/li>
	&lt;li style="text-align: left;">Maintaining compatibility with standard bicycle components&lt;/li>
	&lt;li style="text-align: left;">Ensuring the frame is treated for typical"Wet" Coast weather&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Awards:&lt;/strong>&lt;/p>
&lt;ul>
	&lt;li>First Prize - &lt;a href="http://www.instructables.com/contest/handtoolsonly/" target="_blank">Hand Tools Only Instructables Contest&lt;/a>&lt;/li>
	&lt;li>First Prize - &lt;a href="http://www.instructables.com/contest/teachit/" target="_blank">Teach It! Instructables Contest, Sponsored by Dremel&lt;/a>&lt;/li>
&lt;/ul>
&lt;br>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/Bike-frame-geometry-1024x567.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/Bike-frame-geometry-1024x567.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Track bicycle custom geometry.&lt;/p></description><content:encoded>&lt;p>&lt;strong>Objective&lt;/strong>: Exceed the Vancourite-hipster threshold by building a fixed-gear bicycle using bamboo and carbon fibre.&lt;/p>
&lt;p>&lt;strong>Challenges&lt;/strong>:&lt;/p>
&lt;ul>
	&lt;li style="text-align: left;">Constructing a frame jig to allow freedom around joints for carbon-fibre layup&lt;/li>
	&lt;li style="text-align: left;">Accounting for the non-uniformity of bamboo poles at the frame lugs&lt;/li>
	&lt;li style="text-align: left;">Maintaining compatibility with standard bicycle components&lt;/li>
	&lt;li style="text-align: left;">Ensuring the frame is treated for typical"Wet" Coast weather&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Awards:&lt;/strong>&lt;/p>
&lt;ul>
	&lt;li>First Prize - &lt;a href="http://www.instructables.com/contest/handtoolsonly/" target="_blank">Hand Tools Only Instructables Contest&lt;/a>&lt;/li>
	&lt;li>First Prize - &lt;a href="http://www.instructables.com/contest/teachit/" target="_blank">Teach It! Instructables Contest, Sponsored by Dremel&lt;/a>&lt;/li>
&lt;/ul>
&lt;br>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/Bike-frame-geometry-1024x567.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/Bike-frame-geometry-1024x567.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Track bicycle custom geometry.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/Drawing-Bike-frame-dimensions-1024x680.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/Drawing-Bike-frame-dimensions-1024x680.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Frame dimensions with tubes shown to determine the required cut lengths of bamboo.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/Bamboo-bike-in-jig-1-1024x572_hu_e028e16d4787738b.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/Bamboo-bike-in-jig-1-1024x572_hu_e028e16d4787738b.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Frame jig modeled in SolidWorks.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/2013-09-15-14.21.10-1024x768_hu_d4f4ef3fa63f7ee4.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/2013-09-15-14.21.10-1024x768_hu_d4f4ef3fa63f7ee4.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Bamboo poles after heat treatment in the oven, preparing for the next heat treatment.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/05-IMG_3962_hu_b29cb5c7fe5c404d.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/05-IMG_3962_hu_b29cb5c7fe5c404d.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Dry fitting of bamboo poles after mitering.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/07-IMG_3968_hu_e1cbd953452d85.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/07-IMG_3968_hu_e1cbd953452d85.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Bottom bracket junction with mitered bamboo.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/11-IMG_3985-1024x768_hu_7425ab9373b2124d.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/11-IMG_3985-1024x768_hu_7425ab9373b2124d.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Slotted bamboo to fit the dropouts.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/IMG_20140817_143412-1024x768_hu_dbc64b6780962a59.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/IMG_20140817_143412-1024x768_hu_dbc64b6780962a59.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Frame wrapped, sanded, and ready for finishing details.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/IMG_20140817_153701-768x1024_hu_c9c5c5220a131421.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/IMG_20140817_153701-768x1024_hu_c9c5c5220a131421.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Applying the final touches to the bottom bracket.&lt;/p>
&lt;/div>






&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/08/bamboo-bike/IMG_5364-1024x768_hu_c6285274bde9eb99.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/08/bamboo-bike/IMG_5364-1024x768_hu_c6285274bde9eb99.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Aaaand we&amp;rsquo;re done!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>For a detailed step-by-step guide, head over to my &lt;a href="http://www.instructables.com/id/Building-a-Carbon-Fibre-Bamboo-Bicycle-From-Scratc/" target="_blank">Instructable&lt;/a>.&lt;/p></content:encoded></item><item><title>MATLAB Photo Editing Script</title><link>https://justinmklam.com/posts/2014/01/photo-editing/</link><pubDate>Sun, 12 Jan 2014 11:45:14 -0700</pubDate><guid>https://justinmklam.com/posts/2014/01/photo-editing/</guid><description>&lt;p>&lt;strong>Objective:&lt;/strong> Develop a script to find and remove any differences in a series of photos.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> &lt;a href="http://toomanyadapters.com/how-to-remove-people-travel-photos-photoshop/">How to Remove People From Your Travel Using Photoshop&lt;/a>&lt;/p>
&lt;p>&lt;strong>Framework:&lt;/strong> MATLAB&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/01/photo-editing/Image-test_all.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/01/photo-editing/Image-test_all.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Script takes any number of images (preferably taken on a tripod) and combines them into one cleaned-up image.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/01/photo-editing/Photo-editing-script-algorithm.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/01/photo-editing/Photo-editing-script-algorithm.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Flowchart of the implemented algorithm.&lt;/p>
&lt;/div>
&lt;/p></description><content:encoded>&lt;p>&lt;strong>Objective:&lt;/strong> Develop a script to find and remove any differences in a series of photos.&lt;/p>
&lt;p>&lt;strong>Motivation:&lt;/strong> &lt;a href="http://toomanyadapters.com/how-to-remove-people-travel-photos-photoshop/">How to Remove People From Your Travel Using Photoshop&lt;/a>&lt;/p>
&lt;p>&lt;strong>Framework:&lt;/strong> MATLAB&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/01/photo-editing/Image-test_all.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/01/photo-editing/Image-test_all.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Script takes any number of images (preferably taken on a tripod) and combines them into one cleaned-up image.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2014/01/photo-editing/Photo-editing-script-algorithm.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2014/01/photo-editing/Photo-editing-script-algorithm.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Flowchart of the implemented algorithm.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>Co-op: The Age Effect on Bicycle Helmets</title><link>https://justinmklam.com/posts/2013/coop-mea/</link><pubDate>Sat, 28 Dec 2013 12:37:10 -0700</pubDate><guid>https://justinmklam.com/posts/2013/coop-mea/</guid><description>Foam liners in bicycle helmets absorb energy during impacts. Manufacturers recommend replacing a helmet every 2 to 10 years to maintain optimal head protection. To investigate this recommendation, we quantified the material and impact attenuation properties of helmets to determine if they changed with age.</description><content:encoded>&lt;p>&lt;strong>Objective&lt;/strong>: Determine if age has an effect on the mechanical properties of bicycle helmets.&lt;/p>
&lt;p>&lt;strong>Tasks:&lt;/strong>&lt;/p>
&lt;ul>
	&lt;li>Conducted 1630 helmet impact attenuation tests to measure the effect of age and use on bicycle helmet foam&lt;/li>
	&lt;li>Developed scripts in MATLAB to autonomously post-process the impact data and organize information according to existing helmet databases&lt;/li>
	&lt;li>Wrote the Methods section of the research report to be submitted to the Accident Prevention &amp;amp; Analysis Journal&lt;/li>
	&lt;li>Assisted in protocol development of the helmet and foam core impact attenuation tests&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Company&lt;/strong>: &lt;a href="http://www.meaforensic.com/" target="_blank">MEA Forensic Engineers &amp;amp; Scientists&lt;/a>&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2013/coop-mea/Drop-Tower-annotated-685x1024_hu_812e236c638c9112.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2013/coop-mea/Drop-Tower-annotated-685x1024_hu_812e236c638c9112.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Annotated diagram of the helmet drop tower.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>MECH 223: Hovercraft Competition</title><link>https://justinmklam.com/posts/2013/mech-223-hovercraft/</link><pubDate>Sat, 20 Apr 2013 12:22:15 -0700</pubDate><guid>https://justinmklam.com/posts/2013/mech-223-hovercraft/</guid><description>Teams were given 3 weeks to research, conceptualize, test, and refine their hovercrafts to compete in a class-wide battle of speed, cargo capacity, and maneuverability.</description><content:encoded>&lt;p>For our final MECH2 design project at UBC, we were given three weeks to research, conceptualize, test, and build a functional hovercraft. The end goal was a class-wide competition of speed, cargo capacity, maneuverability, and driving skill. Budget was limited since costs came out of our own pockets, so we set out to build an inexpensive hovercraft using parts from the hardware and dollar store.&lt;/p>
&lt;p>Our result: 5th place out of 30 teams. Not too shabby for a hovercraft made with foam and garbage bags!&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2013/mech-223-hovercraft/Lift-tests.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2013/mech-223-hovercraft/Lift-tests.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Various fan and chassis designs to provide lift.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2013/mech-223-hovercraft/Prototypes.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2013/mech-223-hovercraft/Prototypes.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Intermediate prototypes prior to our final design.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2013/mech-223-hovercraft/Final-Picture1.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2013/mech-223-hovercraft/Final-Picture1.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Close up of our rooster-themed hovercraft.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2013/mech-223-hovercraft/902707_10200373195283748_1412416824_o-1024x683_hu_29e813aa76c29ff2.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2013/mech-223-hovercraft/902707_10200373195283748_1412416824_o-1024x683_hu_29e813aa76c29ff2.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Mech2 Hovercraft competition showcase at the Robson Skating Rink in Vancouver, BC.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2013/mech-223-hovercraft/Recommendations1-1024x544.png>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2013/mech-223-hovercraft/Recommendations1-1024x544.png>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Diagram outlines our well-implemented design features and recommendations for future revisions.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item><item><title>Men in Massage: Research Paper</title><link>https://justinmklam.com/posts/2011/12/men-in-massage-therapy/</link><pubDate>Thu, 01 Dec 2011 17:51:38 -0700</pubDate><guid>https://justinmklam.com/posts/2011/12/men-in-massage-therapy/</guid><description>&lt;p>&lt;em>Originally written as a research paper for ENG 112 at UBC. Republished here for archival purposes.&lt;/em>&lt;/p>

&lt;h1 id="research-proposal" class="anchor">
 &lt;a href="#research-proposal">
 Research Proposal
 &lt;/a>
&lt;/h1>
&lt;p>According to Tom Delph-Janiurek, a “mis-performance of voice” may be necessary in order to gain authority in specific interactional spaces by creating a display of control and assertiveness (277). He proposes that the vocal performances in teaching spaces adapt to their social roles of authority by opposing the hegemonic heterosexuality in voices and perform vocal features of “drag” without giving up their gendered identities, contributing to and disrupting the “heterosexing of [teaching] space” (277). In spite of his conclusion regarding voice as “drag”, he fails to thoroughly assess the aspects outside of vocal performances that also relate to the projections of gendered bodies such as embodiment. A performed gender role relies on the physical aspects of body language and gesture in addition to voice in order to create a complete portrayal of a sexualized identity.&lt;/p></description><content:encoded>&lt;p>&lt;em>Originally written as a research paper for ENG 112 at UBC. Republished here for archival purposes.&lt;/em>&lt;/p>

&lt;h1 id="research-proposal" class="anchor">
 &lt;a href="#research-proposal">
 Research Proposal
 &lt;/a>
&lt;/h1>
&lt;p>According to Tom Delph-Janiurek, a “mis-performance of voice” may be necessary in order to gain authority in specific interactional spaces by creating a display of control and assertiveness (277). He proposes that the vocal performances in teaching spaces adapt to their social roles of authority by opposing the hegemonic heterosexuality in voices and perform vocal features of “drag” without giving up their gendered identities, contributing to and disrupting the “heterosexing of [teaching] space” (277). In spite of his conclusion regarding voice as “drag”, he fails to thoroughly assess the aspects outside of vocal performances that also relate to the projections of gendered bodies such as embodiment. A performed gender role relies on the physical aspects of body language and gesture in addition to voice in order to create a complete portrayal of a sexualized identity.&lt;/p>
&lt;p>The greatest panic of heterosexual men may be the perceived inability to conform to the hegemonic image of masculinity, especially in a female dominated environment (Delph-Janiurek 265). As a result, men may not be willing to enter a setting of close relations and intimacy such as massage therapy possibly by reason of homophobia, alongside the belief that it is solely a woman’s profession (van Meter, 2006). Female dominance is evident in massage therapy because of the nurturing and caring qualities that are typical of hegemonic femininity and this particular profession (Dobson, 2005). However, not all men share this same necessity of adhering to an idealized, unachievable masculinity, as there is still a presence of men (albeit a scarcity of them) in female-dominated work environments (van Meter, 2006). In consequence, men are forced to adopt effeminate qualities to gain authority in the interactional space of massage therapy. Thus, how are men’s gendered identities performed when they cross the boundary of conventional gender roles and enter a female-dominated work setting?&lt;/p>
&lt;hr>
&lt;h1 class="text-center" style="margin-top: 25px;">Men In Massage&lt;/h1>
&lt;p style="font-size: 17px; font-style: italic; color: gray; text-align: center; margin-bottom: 15px;">
The Performance of Hegemonic Femininity by Male Bodies in the Female-Dominated Environment of Massage Therapy
&lt;/p>

&lt;h2 id="introduction" class="anchor">
 &lt;a href="#introduction">
 Introduction
 &lt;/a>
&lt;/h2>
&lt;p>Studies have shown how sexualized bodies adopt characteristics of other gendered identities to better perform their occupational roles. According to Tom Delph-Janiurek (1999), a “mis-performance of voice” may be necessary in order to gain authority in specific interactional spaces by creating a display of control and assertiveness (277). He proposes that the vocal performances in university teaching spaces are adapted to their social roles of authority by performing vocal features of “drag” without sacrificing their gendered identities (277). However, he fails to thoroughly assess the aspects outside of vocal performances that also relate to the projections of gendered bodies such as embodiment. As previous evidence suggests, performed gender roles rely on the physical aspects of body language and gesture in addition to voice in order to create a complete portrayal of a gendered identity.&lt;/p>

&lt;h2 id="body" class="anchor">
 &lt;a href="#body">
 Body
 &lt;/a>
&lt;/h2>
&lt;p>Delph-Janiurek proposes that the greatest panic of heterosexual men may be the perceived inability to conform to the hegemonic image of masculinity, especially within a female-dominated environment (265). As a result, van Meter theorizes men may not be willing to enter a setting of close relations and intimacy such as massage therapy possibly by reason of homophobia, alongside the belief it is solely a woman’s profession (48). Moreover, Dobson illustrates how female dominance is evident in massage therapy because of the nurturing and caring qualities that are typical of hegemonic femininity and this particular profession (165). McCarthy notes how “men… are constructed by gender ideologies that discourage their participation in… caretaking activities that involve body intimacy” (12). However, van Meter suggests not all men share this same necessity of adhering to an idealized, unachievable masculinity, as there is still a presence of men (albeit a scarcity of them) in female-dominated work environments (48). Thus, men are forced to perform femininity in order to gain authority in the interactional space of massage therapy.&lt;/p>
&lt;p>According to Sullivan, people are drawn to massage as a form of health care therapy because of the psychosocial processes relating to contact, comfort, connection, and caring (182). He also claims the “culture of care” affiliated with massage therapy provides clients with “benefits such as relaxation, feelings of well-being, improved circulation, and reduction in anxiety and pain” (181). A consumer survey conducted in 2007 by the American Massage Therapy Association (AMTA) found 25 percent of men, relative to 43 percent of women, had a massage within the past five years; 30 percent of the total recipients had massage therapy for medical reasons, whereas 22 percent did so in search for relaxation and 13 percent for indulgence. These statistics suggest men and women mainly seek massage therapy for (medical) maintenance of the body.&lt;/p>
&lt;p>The profession of massage therapy is recognized as women’s work because of its associations with qualities of mothering. According to AMTA, approximately 82-84 percent of the 258 000 total therapists in the United States are female. Both Dobson and McCarthy agree the nature of intimacy in massage is inherently related to the stereotypical characteristics of nurturing and caring perceived in hegemonic femininity. They emphasize how massage is a sensual and intimate profession, where these “natural properties” of femininity parallel the nurturing and caring mothers typically display to their children, relative to the infrequent physical interaction children receive from their fathers (171; 180). Dobson speculates that the similarities between the natures of work in massage therapy and being the caretakers of the family are intrinsic precursors to the societal idea of massage being a profession of femininity, where women are typically located in the “domestic sphere of work” in a heteronormative society (9). However, she also suggests women are drawn to massage because of its self-employability and flexible scheduling, which ultimately permit women to achieve a balance between work and family (9).&lt;/p>
&lt;p>The preference for female massage therapists is determined by the construction of gender roles in a heteronormative society. Through van Meter’s interviews with licensed therapists, she found their clients tended to have the preference of female therapists over male ones when scheduling appointments, some to the extent where clients would rather wait for the next available female therapist (when no others were available) and decline the offer for a male therapist, even after reassurance by the receptionist concerning the proficiency and experience of the male therapist (48). Notably, for women having had experience(s) with violent men, Klein reveals the preference for female therapists may develop from their fear of being sexually assaulted, as female therapists provide the safety and comfort male therapists typically may not be able to contribute to (13). Dobson continues by suggesting the difficulty for male massage therapists to reach out to female clients is a result of the conventions of a heteronormative society implying “all heterosexual men must want sex with women” (180).&lt;/p>
&lt;p>Male clients who choose to embrace massage therapy will request female therapists to pronounce their masculinity in this setting of femininity (Klein, 186). Socially constructed homophobia functions as a constituent in determining the masculinity of men in massage therapy; Klein suggests homophobia has been internalized to the degree where the notion of being massaged by another male is perceived as unconventional and irrational in society (13). Furthermore, van Meter illustrates how the sexualisation of touch heightens the difficulty for male therapists in gaining male clients as Dave Murdock, a nationally certified massage therapist, notes, “Your potential male clients are afraid you might be gay and your potential female clients are afraid that you’re not” (48). However, as noted by Nevels through an interview with Pete Whitridge, a licensed massage therapist and former assistant director of the Florida School of Massage in Gainesville, there is minimal gender difference once the client has overcome the initial barrier of sexualisation and enters the massage room and gets on the table (108).&lt;/p>
&lt;p>Massage therapy is not openly accepted in a heteronormative society because of its implied associations with sexuality and intimacy. Nevels identifies men as being hesitant in accepting massage as a form of health care therapy due to the perception that having massage means being “pampered” (which relates to indulgence and luxury), rather than having the body being medically “maintained”, ultimately leading to the disruption of the “hegemonic normative prescriptions of gender” (104). Moreover, she believes the conventional performances of male gendered bodies are illustrated through the omission of femininity and therefore, massage, because therapy in this form does not recognize their masculine needs (104). To Connell, hegemonic masculinity forces men to disregard qualities of femininity in body language and speech (cited in Delph-Janiurek, 265). Similarly, Pascoe suggests any qualities of effeminacy in the behaviour of men results in being labelled as non-heterosexual, consequently “powerfully undermin[ing] a man’s masculinity and social value in a misogynist, heteronormative culture” because of the constant judgement by society (cited in Klein, 186).&lt;/p>
&lt;p>Males interpret touch differently than females as a result of the physical interactions they, as children, have with their parents. According to Klein, young children must have physical interaction as a form of “social nutrition” in order to grow and develop in society, despite human sexuality being restrained throughout childhood (49, 50). Kiely’s study of fathers in families illustrates how fathers rarely (if at all) have physical interaction with their children: 59 percent of respondents were never hugged by their fathers whereas 61 percent were occasionally hugged by their mothers (cited in McCarthy, 180). McCarthy proposes this evidence leads to the belief that touch is solely for women in a heteronormative society, as patriarchal ideologies insist men have minimal physical interactions (of intimacy) with each other (180). Thus, while women interpret touch as a sign of friendliness, men interpret it as being intrusive (180).&lt;/p>
&lt;p>The impartiality of a male’s performance in massage therapy deprives them of their own embodied experience of healing from massage (van der Riet, 153). Seidler emphasizes “learning to be a man means learning to be impersonal” by reason of the “hegemonic scripts of conventional gender roles” (cited in van der Riet, 153). Chodorow follows by stating men prefer not to be handled or touched because masculinity is threatened by intimacy, which subsequently gives rise to, as McCarthy describes, the implications of a reduced status as a gendered body (cited in van der Riet, 151; 180). Van der Riet’s study of a students’ massage workshop finds the intimacy and sexual implications of massage therapy cause males to feel uncomfortable, which is shown through a lack of concentration by the male students in practice (e.g. constantly looking around, not focusing on the massage), and most notably, their body language of awkward movements, rigidity and tense facial expressions throughout the duration of delivering their massage (151).&lt;/p>
&lt;p>The sexual implications of massage force therapists to continuously desexualize the massage atmosphere in order to establish a professional relationship with the client. Van der Riet states massage is perceived as “socially fragile” because of the evident risks in embarrassing the client through physical and/or emotional invasion of the client’s body and private space from “unpleasant” procedures (151). Klein emphasizes it is especially inherent for male therapists to discourage the socially constructed “sexual misrepresentations of massage” by avoiding terminology associated with sexual connotations (i.e. “massage table” vs. “massage bed;” “disrobe” vs. “take your clothes off”) and by presenting themselves professionally through their clothing and personal grooming (78). Furthermore, it is essential for male therapists to respect the personal boundaries and comfort zone of the client. However, Dobson suggests this becomes difficult when the therapist is sexually attracted to the client and is forced to manage his “sexual responses to ‘beautiful women’ or ‘pheromones’” (171). In order to create a “non-sexual interaction” with the client, Klein speculates desexualisation is achieved through the therapist partitioning the body (by draping the areas not being worked on) and avoiding “sensitive” areas (inner thighs, abdomen, sides of women’s torso, buttocks) of the client (81). Moreover, van der Riet proposes therapists should be emotionally detached from the massage by setting emotional boundaries between the client and themselves (153).&lt;/p>

&lt;h2 id="discussion" class="anchor">
 &lt;a href="#discussion">
 Discussion
 &lt;/a>
&lt;/h2>
&lt;p>From analysing past studies on massage therapy, it is evident the difficulty for males to become effective therapists lies in the construction of heteronormativity in society. Male therapists are faced with the issue of being judged in society through their choice of profession, thus deterring other men from entering massage therapy. As previously stated by van der Riet, the continuous effort by men to prove their masculinity causes male student therapists to perform poorly, where the display of mothering qualities of nurturing and caring are necessary in order to enhance the atmosphere of massage for the client (153). Thus, males entering massage therapy should disregard the socially constructed pressures to conform to the hegemonic image of masculinity in order to gain authority as a proficient therapist. Moreover, as Nevels illustrates, it is recommended for male therapists to cohere to the characteristics of hegemonic femininity by embracing the caretaking qualities evident in the “culture of massage,” possibly allowing them to attract a client base of both males and females (104). Male therapists are still able to thrive as a male gendered body despite massage being a female-dominated profession by desexualizing the environment and relationship between the therapist and client, consequently producing a healthy atmosphere for healing and maintaining the body for their clients receiving massage. Additionally, the preference for female therapists may be refuted once male and female clients allow themselves to disregard what a heteronormative society forces them to believe concerning the sexual misrepresentations of massage therapy.&lt;/p>

&lt;h2 id="conclusion" class="anchor">
 &lt;a href="#conclusion">
 Conclusion
 &lt;/a>
&lt;/h2>
&lt;p>In spite of the adequate evidence to suggest the performance of femininity is necessary for male therapists to thrive in massage therapy, this does not directly translate into the acceptance of massage as a suitable form of health care therapy for males and females without facing repercussions by a heteronormative society. Although male therapists are able to stylise their sexualized identities to present themselves in a more attractive light for both male and female clients, it is ultimately the decision of clients, with respect to their perception of massage therapy, whether or not to embrace massage as their desired form of medical healing. While this paper explains how male therapists perform their gendered body in a female-dominated setting to gain authority, it fails to answer how massage therapy may be displayed without the negative connotations of sexuality that hinders its growth as an effective method of healing. Future research should include further evidence concerning why sexual interaction is frowned upon in society, possibly achieved through studying the influence of mass media on parents and their children, as the notions of non-sexual interactions are primarily induced during childhood. Regardless, it is fundamentally necessary for masculine bodies to adopt effeminate qualities in massage therapy where authority is predetermined by female gendered identities.&lt;/p>

&lt;h2 id="works-cited" class="anchor">
 &lt;a href="#works-cited">
 Works Cited
 &lt;/a>
&lt;/h2>
&lt;em>
Dobson, Marnie. "Professionalizing Touch: Gender, Sexuality, the Law and Massage Work." University of California, Irvine, 2005. United States -- California: ProQuest Dissertations &amp; Theses (PQDT). Web. 9 Nov. 2011.
&lt;p>Klein, Noa. &amp;ldquo;Loving Touch: Therapeutic Massage, the Socialization of the Body, and the Healing of US Culture.&amp;rdquo; University of California, Santa Barbara, 2010. United States &amp;ndash; California: ProQuest Dissertations &amp;amp; Theses (PQDT). Web. 9 Nov. 2011.&lt;/p>
&lt;p>McCarthy, Michael. “Skin and touch as intermediates of body experience with reference to gender, culture and clinical experience.” Journal of Bodywork and Movement Therapies 2.3 (1998): 174. Science Direct Journals. Web. 4 Nov. 2011.&lt;/p>
&lt;p>Nevels, Amanda. “Getting him through the door: How to market massage therapy to males.” Massage Therapy Journal 47.1 (2008): 104. Health Reference Center Academic. Web. 4 Nov. 2011.&lt;/p>
&lt;p>Sullivan, S John. “The culture of massage therapy: Valued elements and the role of comfort, contact, connection and caring.” Complementary Therapies in Medicine 17.4 (2009): 181. Science Direct Journals. Web. 4 Nov. 2011.&lt;/p>
&lt;p>van der Riet, Pamela. “Massage and sexuality in nursing.” Nursing Inquiry 2.3 (1995): 149-156. Wiley Online. Web 4 Nov. 2011.&lt;/p>
&lt;p>van Meter, Ryan. “A touchy subject: is there gender bias in massage therapy?” Massage Therapy Journal 45.4 (2006): 48. Health Reference Center Academic. Web. 4 Nov. 2011.
&lt;/em>&lt;/p></content:encoded></item><item><title>LEGO Headphones</title><link>https://justinmklam.com/posts/2010/07/lego-headphones/</link><pubDate>Sat, 10 Jul 2010 12:37:00 -0700</pubDate><guid>https://justinmklam.com/posts/2010/07/lego-headphones/</guid><description>&lt;p>&lt;strong>Background:&lt;/strong> I decided to combine the two things that I love most: music and LEGO. This whole project took me about a month and a half, very on and off. I&amp;rsquo;m quite pleased with how the headphones turned out in the end, as they are sturdy enough to be tossed around in a bag. My favourite part is that I can take it apart any time to make repairs that may be needed on the go. No tools necessary!&lt;/p></description><content:encoded>&lt;p>&lt;strong>Background:&lt;/strong> I decided to combine the two things that I love most: music and LEGO. This whole project took me about a month and a half, very on and off. I&amp;rsquo;m quite pleased with how the headphones turned out in the end, as they are sturdy enough to be tossed around in a bag. My favourite part is that I can take it apart any time to make repairs that may be needed on the go. No tools necessary!&lt;/p>
&lt;p>&lt;strong>Objective:&lt;/strong> Use LEGO to create the external assembly of a pair of headphones.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2010/07/lego-headphones/P1080061-e1424407288767_hu_606b7d7a998bb748.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2010/07/lego-headphones/P1080061-e1424407288767_hu_606b7d7a998bb748.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The world&amp;rsquo;s finest LEGO headphones in all its glory.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2010/07/lego-headphones/P1080050_hu_ad92b69c8af632f9.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2010/07/lego-headphones/P1080050_hu_ad92b69c8af632f9.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">Easily deconstructed, staying true to the LEGO roots.&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>Visit &lt;a href="http://www.instructables.com/id/LEGO-Headphones/">Instructables&lt;/a> to learn more about the project!&lt;/p></content:encoded></item><item><title>A Minty-Fresh Power Pack</title><link>https://justinmklam.com/posts/2009/altoids/</link><pubDate>Thu, 16 Apr 2009 22:52:25 -0700</pubDate><guid>https://justinmklam.com/posts/2009/altoids/</guid><description>Before the ubiquity of powerbanks, there was nothing on the market to provide longer battery life for portable electronics. Connecting a female USB connector to four rechargable AA batteries opened up a new world of extended iPod playback.</description><content:encoded>&lt;p>Before the ubiquity of powerbanks, there was nothing on the market to provide longer battery life for portable electronics. Connecting a female USB connector to four rechargable AA batteries opened up a new world of extended iPod playback. I built a simple, rechargable battery pack to keep the tunes playing on long road trips.&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2009/altoids/Altoids-charger_2_hu_48c14e84c1dbf47e.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2009/altoids/Altoids-charger_2_hu_48c14e84c1dbf47e.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">The iPod is alive!&lt;/p>
&lt;/div>
&lt;/p>
&lt;p>




&lt;div class="row img-captioned">
 
 
 
 
 
 
 
 &lt;a href=https://justinmklam.com/posts/2009/altoids/Altoids-charger_3_hu_4cd18e09ccb58bfd.jpg>&lt;img class="img-responsive img-content" src=https://justinmklam.com/posts/2009/altoids/Altoids-charger_3_hu_4cd18e09ccb58bfd.jpg>&lt;/a>
 
 
 
 
 
 &lt;p class="caption">President&amp;rsquo;s Choice rechargable batteries and some janky wiring is all it took.&lt;/p>
&lt;/div>
&lt;/p></content:encoded></item></channel></rss>