Nick Whyte Software Engineer in Sydney https://nickwhyte.com/ Wed, 19 Mar 2025 23:33:56 +0000 Wed, 19 Mar 2025 23:33:56 +0000 Jekyll v3.10.0 Ender 3 v2 Pen Plotter <p> <img class="img-responsive alignright" width="40%" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2585.jpeg" /> Last year I purchased myself an Ender 3 V2 3D Printer. It was a logical purchase, to compliment my home automation hobby, allowing me to design and print custom enclosures for miscellaneous ESPHome and Zigbee nodes. </p> <p>It turns out having a <a href="https://all3dp.com/2/corexy-3d-printer-is-it-worth-buying/">CoreXY</a> 3D printer allows you to do more than just 3D printing if you’re willing to get creative. For example, adding on a <a href="https://all3dp.com/2/ender-3-laser-engraver-upgrade-all-you-need-to-know/">laser engraver</a>.</p> <p>In my case, I was tasked with automating the process of drawing an embroidery pattern onto fabric using a heat/friction erasable pen for my partner, <a href="https://kateescott.com/">Kate</a>.</p> <p>Unsurprisingly, there are services available which can do this, albeit using machine washable dye rather than heat reactive dye in most cases. However, in almost every case these are cost prohibitive for hobby embroidery projects and also happen to incur a long turn around time from order to delivery.</p> <p>So as with most projects I undertake, rather than re-designing from scratch, I opted to start searching for existing projects. Surely someone had done this before! I managed to find a handful of models (e.g. <a href="https://www.printables.com/model/121808-ender-3-v2-pen-plotter-addon"> Ender 3 v2 Pen Plotter addon </a>), however most of these either required you to fully remove the hotend assembly or did not accomodate mounting a BL-Touch levelling probe.</p> <p>Fortunately I managed to find <a href="https://www.thingiverse.com/thing:4664467">this model</a> which both accommodated both of my requirements. It supported mounting of the BL-Touch probe without needing to remove the hotend assembly. I printed it and mounted it to the printer… job done… right? Unfortunately not. Due to the way it mounted it now meant that the hot-end carriage could no longer hit the x-axis limit switch. Not good!</p> <p>From here, <strong>the only logical solution was to design my own</strong>, which accommodated for my specific requirements. I’ve been getting more and more familiar with Fusion 360 since buying the printer, so was confident to get straight to prototyping a design.</p> <p>I started off by importing dimensionally accurate models of both the Ender 3 v2 print head assembly/x-carriage and the BL-Touch:</p> <p><img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/prototyping-models.png" /></p> <p>By doing so, I would be able to project the precise positions of mounting holes onto the sketches for component of my attachment, rather than measuring by hand with some amount of inaccuracies.</p> <p>Another requirement I came to realise was that it would be useful would be the ability to remove the plotter attachment when it is not in use. As such I arrived at my first design iteration:</p> <p><img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/prototyping-iteration-1.png" /></p> <p>The design was relatively simple. Two main parts; a mounting arm to hold the BL-Touch probe to the hotend assembly, and a three-piece arm to hold the pen, which is mounted to the mounting arm using the same screws from the BL-Touch probe. The three-piece arm was designed to allow a pen to be mounted with some spring tension, however in actual fact once it was printed there was too much friction to provide any reasonable amount of travel. Back to the drawing board.</p> <p>On to the next iteration, I re-thought the design. I reused the same mounting arm to the hotend assembly, but changed the mounting angle of the pen attachment. Instead, it would side-mount to the mounting arm using M3 bolts into heat-set inserts. I thought I would give this a try, without any tension mechanism, as an automatically leveled bed should suffice.</p> <p><img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/prototyping-iteration-2.png" /></p> <p>One printed, I came to realise that the side mounting again caused some interference with the x-axis limit switch, so I found that in order to solve this problem I needed to move the rear bolt closer to the front, and additionally provide a countersunk for the bolt head to sit inside.</p> <p><img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/prototyping-iteration-3.png" /></p> <p>After a handful more prints and some more tweaks to the height of mounting arm and hole sizes, I had a final design:</p> <p><img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/final-render.png" /></p> <p>Now, with the mounting bracket complete, it was time to move onto the software aspect of the project. Fortunately for me, this is a solved problem. <a href="https://urish.medium.com/how-to-turn-your-3d-printer-into-a-plotter-in-one-hour-d6fe14559f1a">Uri Shaked has written an excellent post</a> about using Inkscape with the Gcodetools extension to produce plotting toolpats from SVG files. Inkscape isn’t the nicest software to run on MacOS, so the experience is a little clunky (and crashy), but it does the job. However, I plan on exploring <a href="https://www.reddit.com/r/3Dprinting/comments/lla1is/comment/gnqo577/?utm_source=share&amp;utm_medium=web2x&amp;context=3">other tools</a>, like <a href="https://github.com/inkcut/inkcut">Inkcut</a>, <a href="http://jscut.org/">JScut</a>, <a href="https://hackage.haskell.org/package/juicy-gcode">juicy-gcode</a>, <a href="https://github.com/avwuff/SVG-to-GCode">SVG-toGcode</a> and <a href="https://www.gnu.org/software/hp2xx/">hp2xx</a> in due time.</p> <p>Gcodetools allows you to specify a custom header and footer for produced gcode files. I used this to provide a more ergonomic printing experience by ensuring the printer was homed, bed levelling mesh is enabled, and the print timer is initiated to allow for on-the-fly tweaking of the z-height.</p> <p>Header:</p> <figure class="highlight"><pre><code class="language-gcode" data-lang="gcode">M420 S1 ; Enable Jyers Mesh M75 ; Start print timer G28 ; home all axis</code></pre></figure> <p>Footer:</p> <figure class="highlight"><pre><code class="language-gcode" data-lang="gcode">G1 Z20.00 F600 ; Move print head up G1 X5 Y180 F9000 ; present print M84 X Y E ; disable motors M77 ; Stop print timer</code></pre></figure> <p>Now, with some G-code produced, it was time for a test. As a software engineer, there’s no better way to test the output capability of something than to <code class="language-plaintext highlighter-rouge">print("hello world")</code>, and so that is exactly what I did:</p> <p><img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2585.jpeg" /></p> <p>Great success! From here, I did some further tests on fabric and found it worked just fine, albeit a little faint since more pen pressure resulted in snagging the fabric, so for fabric prints 2 or 3 passes is generally necessary. Beyond just plotting SVGs I’ve started to explore generative art using mathematical functions (<a href="https://github.com/asharkinwater/spirograph-">like a spirograph</a>). Here are a few pictures from testing and a video:</p> <div class="row"> <div class="col-xs-6"> <p> <a target="_blank" href="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2573.jpeg"> <img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2573.jpeg" /> </a> </p> </div> <div class="col-xs-6"> <p> <a target="_blank" href="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2575.jpeg"> <img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2575.jpeg" /> </a> </p> </div> </div> <div class="row"> <div class="col-xs-6"> <p> <a target="_blank" href="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2602.jpeg"> <img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2602.jpeg" /> </a> </p> </div> <div class="col-xs-6"> <p> <a target="_blank" href="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2616.jpeg"> <img class="img-responsive center-block" src="/static/posts/2022-04-25-ender-3-v2-pen-plotter/IMG_2616.jpeg" /> </a> </p> </div> </div> <p></p> <div class="embed-responsive embed-responsive-16by9"> <iframe width="640" height="360" src="//www.youtube.com/embed/-DLPJjCscFY?rel=0" frameborder="0" allowfullscreen=""></iframe> </div> <p></p> <p>You can find the model for my Ender 3 V2 BL Touch Mount and Pen Plotter on <a href="https://www.thingiverse.com/thing:5363733">Thingiverse</a> and <a href="https://thangs.com/mythangs/file/62724">Thangs</a></p> Mon, 25 Apr 2022 00:00:00 +0000 https://nickwhyte.com/post/2022/ender-3-v2-pen-plotter/ https://nickwhyte.com/post/2022/ender-3-v2-pen-plotter/ Blog 100 Warm Tunas 2018 Prediction Analysis <p>Over the space of 6 weeks, <a href="https://100warmtunas.com">100 Warm Tunas</a> collected a large sum of data and chugged away at it to make some predictions about what the Hottest 100 of 2018 would look like.</p> <p>In summary,</p> <ul> <li>We collected <strong>6,234</strong> entries (13.6% decrease since 2017 🔻).</li> <li>We tallied <strong>58,463</strong> votes across these entries (12.9% decrease since 2017 🔻).</li> <li><strong>3.00%</strong> of votes were collected via Instagram direct message.</li> <li>Triple J counted <strong>2,758,584</strong> votes.</li> <li>Therefore, we collected a sample of <strong>2.12%</strong>.</li> <li>We <strong>successfully predicted #1</strong></li> <li>We predicted <strong>7 out of the top 10 songs</strong>.</li> <li>We predicted <strong>15 out of the top 20 songs</strong>.</li> <li>We predicted <strong>83 out of the 100 songs</strong> played in the countdown.</li> <li>Throughout December and January, <a href="https://100warmtunas.com">100warmtunas.com</a> was loaded over <strong>105,000</strong> times by over <strong>28,000 users</strong>.</li> </ul> <p>You can read the full technical prediction analysis over at the <a href="https://100warmtunas.com/news/2019/02/prediction-analysis-2018/">100 Warm Tunas news site</a>.</p> Mon, 04 Feb 2019 00:00:00 +0000 https://nickwhyte.com/post/2019/100-warm-tunas-2018-analysis/ https://nickwhyte.com/post/2019/100-warm-tunas-2018-analysis/ Blog Programming 100 Warm Tunas has a new home! <p>100 Warm Tunas has found a new home this year! This year’s results, along with the past 2 years can now be found at <a href="https://100warmtunas.com">100warmtunas.com</a>. The existing domain (<a href="https://100-warm-tunas.nickwhyte.com/">100-warm-tunas.nickwhyte.com</a>) will simply perform a <code class="language-plaintext highlighter-rouge">301</code> permanent redirect to the new domain, so all existing inbound links should be unaffected.</p> Thu, 01 Nov 2018 00:00:00 +0000 https://nickwhyte.com/post/2018/100-warm-tunas-2018-website/ https://nickwhyte.com/post/2018/100-warm-tunas-2018-website/ Blog Programming 100 Warm Tunas 2017 Prediction Analysis <div class="pull-right hidden-xs" style="font-size:100px; padding: 5px 5px 5px 20px;">&#128293;&#128175;</div> <div class="pull-right hidden-sm hidden-lg hidden-md" style="font-size:68px; padding: 5px 5px 5px 20px;">&#128293;&#128175;</div> <p>Over the space of 6 weeks, <a href="https://100-warm-tunas.nickwhyte.com/2017/">100 Warm Tunas</a> collected a large sum of data and chugged away at it to make some predictions about what the Hottest 100 of 2017 would look like. Along the way we encountered <a href="http://127.0.0.1:4000/post/2018/100-warm-tunas-2017-update/">a bug</a> in the collection process, however data was backfilled and showed that I had collected a sample size around the same as in <a href="https://100-warm-tunas.nickwhyte.com/2016/">2016</a>.</p> <h3 id="summary">Summary</h3> <ul> <li>100 Warm Tunas collected <strong>7,216 entries</strong> (7.3% less than 2016 🔻)</li> <li>100 Warm Tunas tallied <strong>67,085</strong> votes across these entries (2.6% more than 2017 🔺). This is due to improvements in 100 Warm Tunas’ counting and recognition process.</li> <li>Triple J counted <strong>2,386,133 votes</strong>.</li> <li>Therefore, 100 Warm Tunas, collected a sample of <strong>2.8%</strong>. Not bad! (The same as in 2016).</li> <li>Warm Tunas predicted <strong>8 out of the top 10 songs</strong> (Same as 2016) (Ignoring order)</li> <li>Warm Tunas predicted <strong>16 out of the top 20 songs</strong> (3 less than in 2016, where 19 out of 20 were predicted) (Ignoring order).</li> <li>Warm Tunas predicted <strong>83 out of the 100 songs</strong> played in the countdown. (1 less than in 2016) (Ignoring order)</li> </ul> <p>Overall, even though the sample size was reasonably consistent between 2016 and 2017, it is clear that the results collected in 2016 were more accurate.</p> <h3 id="technical-analysis">Technical Analysis</h3> <p>The results this year definitely show a more accurate 1st place prediction (predicting HUMBLE. to win), as opposed to last year where the top two positions were placed out of order, however looking at the data, it looks as though all other aspects of the prediction stayed almost the same.</p> <p>To start this analysis, lets take a look at the top 10 of the official countdown and match it up with their predicted places in Warm Tunas:</p> <div class="table-responsive"> <table> <thead> <tr> <th>Artist</th> <th>Title</th> <th>ABC Rank</th> <th>Tunas Rank</th> <th>Difference</th> </tr> </thead> <tbody> <tr> <td>Kendrick Lamar</td> <td>HUMBLE.</td> <td>1</td> <td>1</td> <td>0</td> </tr> <tr> <td>Gang Of Youths</td> <td>Let Me Down Easy</td> <td>2</td> <td>3</td> <td>1</td> </tr> <tr> <td>Angus &amp; Julia Stone</td> <td>Chateau</td> <td>3</td> <td>6</td> <td>3</td> </tr> <tr> <td>Methyl Ethel</td> <td>Ubu</td> <td>4</td> <td>4</td> <td>0</td> </tr> <tr> <td>Gang Of Youths</td> <td>The Deepest Sighs, The Frankest Shadows</td> <td>5</td> <td>2</td> <td>3</td> </tr> <tr> <td>Lorde</td> <td>Green Light</td> <td>6</td> <td>8</td> <td>2</td> </tr> <tr> <td>PNAU</td> <td>Go Bang</td> <td>7</td> <td>5</td> <td>2</td> </tr> <tr> <td>Thundamentals</td> <td>Sally {Ft. Mataya}</td> <td>8</td> <td>10</td> <td>2</td> </tr> <tr> <td>Vance Joy</td> <td>Lay It On Me</td> <td>9</td> <td>15</td> <td>6</td> </tr> <tr> <td>Gang Of Youths</td> <td>What Can I Do If The Fire Goes Out?</td> <td>10</td> <td>13</td> <td>3</td> </tr> <tr> <td>BROCKHAMPTON</td> <td>SWEET</td> <td>11</td> <td>7</td> <td>4</td> </tr> <tr> <td>Peking Duk &amp; AlunaGeorge</td> <td>Fake Magic</td> <td>12</td> <td>16</td> <td>4</td> </tr> <tr> <td>Khalid</td> <td>Young Dumb &amp; Broke</td> <td>13</td> <td>24</td> <td>11</td> </tr> <tr> <td>Lorde</td> <td>Homemade Dynamite</td> <td>14</td> <td>30</td> <td>16</td> </tr> <tr> <td>Vera Blue</td> <td>Regular Touch</td> <td>15</td> <td>11</td> <td>4</td> </tr> <tr> <td>Jungle Giants, The</td> <td>Feel The Way I Do</td> <td>16</td> <td>32</td> <td>16</td> </tr> <tr> <td>Baker Boy</td> <td>Marryuna {Ft. Yirrmal}</td> <td>17</td> <td>12</td> <td>5</td> </tr> <tr> <td>Ball Park Music</td> <td>Exactly How You Are</td> <td>18</td> <td>14</td> <td>4</td> </tr> <tr> <td>Killers, The</td> <td>The Man</td> <td>19</td> <td>19</td> <td>0</td> </tr> <tr> <td>Peking Duk</td> <td>Let You Down {Ft. Icona Pop}</td> <td>20</td> <td>38</td> <td>18</td> </tr> </tbody> </table> </div> <p>Lets pull apart this table and grab some statistics about how we did with our prediction:</p> <table> <thead> <tr> <th>Predicted</th> <th>Out Of Top N</th> <th>Percentage</th> </tr> </thead> <tbody> <tr> <td>8</td> <td>10</td> <td>80.0%</td> </tr> <tr> <td>16</td> <td>20</td> <td>80.0%</td> </tr> <tr> <td>22</td> <td>30</td> <td>73.3%</td> </tr> <tr> <td>33</td> <td>40</td> <td>82.5%</td> </tr> <tr> <td>42</td> <td>50</td> <td>84.0%</td> </tr> <tr> <td>50</td> <td>60</td> <td>83.3%</td> </tr> <tr> <td>62</td> <td>70</td> <td>88.6%</td> </tr> <tr> <td>68</td> <td>80</td> <td>85.0%</td> </tr> <tr> <td>78</td> <td>90</td> <td>86.7%</td> </tr> <tr> <td>83</td> <td>100</td> <td>83.0%</td> </tr> </tbody> </table> <p>So from the above data, it’s apparent that once again:</p> <ul> <li>The average error for the top ten ranks was <code class="language-plaintext highlighter-rouge">2.2</code> positions (an increase from 2016’s <code class="language-plaintext highlighter-rouge">1.9</code> positions)</li> <li>Warm Tunas predicted <strong>8 out of the top 10 songs</strong></li> <li>Warm Tunas predicted <strong>16 out of the top 20 songs</strong></li> <li>Warm Tunas predicted <strong>83 out of the 100 songs</strong> played in the countdown.</li> </ul> <p>That’s not a bad result at all!</p> <p>The average rank prediction error, grouped into divisions of 10 is provided below. It shows that it’s difficult to predict where songs will place once you leave the top 50:</p> <table> <thead> <tr> <th>ABC Position</th> <th>Warm Tunas Avg Error</th> </tr> </thead> <tbody> <tr> <td>1-10</td> <td>1.9000</td> </tr> <tr> <td>11-20</td> <td>8.2000</td> </tr> <tr> <td>21-30</td> <td>14.3000</td> </tr> <tr> <td>31-40</td> <td>12.5000</td> </tr> <tr> <td>41-50</td> <td>15.2000</td> </tr> <tr> <td>51-60</td> <td>24.7000</td> </tr> <tr> <td>61-70</td> <td>18.2000</td> </tr> <tr> <td>71-80</td> <td>29.9000</td> </tr> <tr> <td>81-90</td> <td>34.1000</td> </tr> <tr> <td>91-100</td> <td>29.5000</td> </tr> </tbody> </table> <p>To compare Warm Tuna’s predictions vs actual rankings, a scatter plot has been provided below. We can see as we get closer to rank 1, the 100 Warm Tunas prediction gets better and converges upon the actual rankings played out on the day.</p> <div class="embed-responsive embed-responsive-4by3"> <iframe width="640" height="480" frameborder="0" scrolling="no" src="//plot.ly/~nickw444/3.embed"></iframe> </div> <p>Fortunately this year around, 100 Warm Tunas was able to successfully predict the winner of the countdown. The reason this prediction was able to be made was because the sample collected clearly indicated <em>HUMBLE.</em> as an outlier. – an entire 5% higher than the next track, predicted to place 2nd.</p> <p>Anyway, that’s a wrap. See you later this year for 100 Warm Tunas 2018 edition!</p> Fri, 16 Feb 2018 00:00:00 +0000 https://nickwhyte.com/post/2018/100-warm-tunas-2017-analysis/ https://nickwhyte.com/post/2018/100-warm-tunas-2017-analysis/ Blog Programming 100 Warm Tunas 2017 Update &#128293;&#128175; <p>100 Warm Tunas has been happily chugging away for the last month or so. I’ve obtained a fair amount of media coverage too.</p> <p>A couple of days back, I posted the site to the <a href="https://reddit.com/r/triplej">triplej subreddit</a>. Someone replied to the post telling me my vote count was significantly less than what they had been counting by hand, which made me somewhat suspicious – was there a bug in my Instagram scraping library that I built?</p> <p>Well, after a bit of debugging early this morning, I found that there was indeed a bug. Not a bug with my scraping library, but rather a bug with how I was using the library:</p> <figure class="highlight"><pre><code class="language-patch" data-lang="patch"><span class="gd">- for page in ig.fetch_pages('triplej', per_page=10): </span><span class="gi">+ for page in ig.fetch_pages(hashtag, per_page=10): </span> for post in page.posts: if post.is_video: logger.info("Skipping {} because it's a video".format(post.shortcode))</code></pre></figure> <p>For those who are programmers, you’ll probably spot the issue here. For those who aren’t, the issue is that I have been using a hardcoded string to collect Instagram votes, when I thought I was collecting a handful of hashtags.</p> <p>This has now been rectified and I have kicked off a full re-scrape to back-fill the data.</p> Tue, 23 Jan 2018 00:00:00 +0000 https://nickwhyte.com/post/2018/100-warm-tunas-2017-update/ https://nickwhyte.com/post/2018/100-warm-tunas-2017-update/ Blog Programming 100 Warm Tunas 2017 <div class="pull-right hidden-xs" style="font-size:100px; padding: 5px 5px 5px 20px;">&#128293;&#128175;</div> <div class="pull-right hidden-sm hidden-lg hidden-md" style="font-size:68px; padding: 5px 5px 5px 20px;">&#128293;&#128175;</div> <p>Last year I predicted the top 3 in Triple J’s hottest 100 (ignoring order). This year I’m back at it once again <a href="https://100-warm-tunas.nickwhyte.com/2017/">with an updated webpage and a Spotify playlist</a>.</p> <p>Results are collected, optimised, and processed multiple times per day. Instagram images tagged with #hottest100 and a few others are included for counting.</p> <p>Happy voting!</p> <p>Feel free to check out <a href="https://100-warm-tunas.nickwhyte.com/2016/">the results from 2016</a>, <a href="https://nickwhyte.com/post/2017/100-warm-tunas-2016-analysis/">2016 results analysis</a>, and <a href="https://nickwhyte.com/post/2016/predicting-triple-j-hottest-100-2015/">the process taken in 2015</a>.</p> Sun, 17 Dec 2017 00:00:00 +0000 https://nickwhyte.com/post/2017/100-warm-tunas-2017/ https://nickwhyte.com/post/2017/100-warm-tunas-2017/ Blog Programming 100 Warm Tunas 2016 Prediction Analysis <p>It’s been a long time since the Hottest 100 of 2016 was aired. Unfortunately, I never really got around to publishing some analysis I performed on the <a href="https://100-warm-tunas.nickwhyte.com/2016/">prediction results</a>. Fortunately, I managed to find some time recently!</p> <p>Looking from afar, the results don’t look <em>fantastic</em> (when you compare them to <a href="https://nickwhyte.com/post/2016/predicting-triple-j-hottest-100-2015/">my results from 2015</a> at least). The prediction unfortunately predicted the top two places out of order, however did manage to predict the third place correctly.</p> <p>Lets take a look at the Top 10 of Triple J’s list and match it up with 100 Warm Tunas:</p> <p><a href="/static/posts/2017-12-17-100-warm-tunas-2016-analysis/triplej-rank-vs-tuna-rank.png"> <img src="/static/posts/2017-12-17-100-warm-tunas-2016-analysis/triplej-rank-vs-tuna-rank.png" class="img-responsive" alt="Triple J Rank vs Tuna Rank" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>Looking at this we see most predictions we can find some learnings:</p> <ul> <li>The average error for the top ten rank was <code class="language-plaintext highlighter-rouge">1.9</code> rank positions.</li> <li>If 100 Warm Tunas ignored rank and simply guessed the top ten, it would have predicted <strong>8 of the top 10 songs</strong>.</li> <li>If 100 Warm Tunas ignored rank and simply guessed the top 3 songs to win, it would have predicted <strong>all 3 songs</strong>. <em>Woo!</em></li> </ul> <p>Lets dive into a chart that shows error for all ranks:</p> <p><a href="/static/posts/2017-12-17-100-warm-tunas-2016-analysis/rank-error.png"> <img src="/static/posts/2017-12-17-100-warm-tunas-2016-analysis/rank-error.png" class="img-responsive" alt="Rank error per position" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>From this chart, we can deduce that the further away from position 1 we become, the higher the error. This information alone isn’t very useful. We can get a better understanding of error by finding the average for each ranking group:</p> <p><a href="/static/posts/2017-12-17-100-warm-tunas-2016-analysis/rank-error-avg.png"> <img src="/static/posts/2017-12-17-100-warm-tunas-2016-analysis/rank-error-avg.png" class="img-responsive" alt="Average Rank error per group" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>As we get closer to rank 1, the results become more and more accurate, however they are not perfect. This is more obvious if we use a scatter plot to compare Triple J ranks against Warm Tunas predictions:</p> <p><a href="/static/posts/2017-12-17-100-warm-tunas-2016-analysis/triplej-vs-tunas-scatter.png"> <img src="/static/posts/2017-12-17-100-warm-tunas-2016-analysis/triplej-vs-tunas-scatter.png" class="img-responsive" alt="Triple J vs Tunas Scatter Plot" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>It’s clear now that as we get closer to rank 1, the 100 Warm Tunas prediction gets better and converges upon the actual rankings played out on the day. However, unfortunately this year the difference between rank 1 and rank 2 was way too close to call - just <code class="language-plaintext highlighter-rouge">0.67%</code> of voting volume was separating the two. A difference that was not enough to provide an accurate prediction of the winner.</p> <p>Overall, whilst 100 Warm Tunas 2016 did get the two top positions out of order, it’s understandable as to why this happened. Hopefully <a href="https://100-warm-tunas.nickwhyte.com/2017/">this year</a> there is a greater difference between ranks, giving further ability to predict the winner in position #1.</p> Sun, 17 Dec 2017 00:00:00 +0000 https://nickwhyte.com/post/2017/100-warm-tunas-2016-analysis/ https://nickwhyte.com/post/2017/100-warm-tunas-2016-analysis/ Blog Programming Reverse Engineering a 433MHz Motorised Blind RF Protocol <p>I’ve been doing a fair bit of DIY home automation hacking lately across many different devices - mostly interested in adding DIY homekit integrations. A couple of months ago, my dad purchased a bulk order of <a href="http://www.raexmotor.com/index.php?m=content&amp;c=index&amp;a=show&amp;catid=9&amp;id=23">RAEX 433MHz RF motorised blinds</a> to install around the house, replacing our existing manual roller blinds.</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/IMG_8745_crop.jpg"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/IMG_8745_crop.jpg" class="img-responsive" alt="RAEX Motorised Blind" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p><small>Note: If you are based in Australia, you can purchase these in bulk or individually via <a href="https://www.raexaustralia.com?utm_source=nickwhyte.com&amp;utm_medium=post&amp;utm_content=433mhz-reversing">www.raexaustralia.com</a> (Full disclosure – my father runs the site).</small></p> <p>The blinds are a fantastic addition to the house, and allow me to be super lazy opening/closing my windows, however in order to control them you need to purchase the RAEX brand remotes. RAEX manufacture many different types of remotes, of which, I have access to two of the types, depicted below:</p> <div style="display: flex; align-items: center; margin:20px 0"> <div style="text-align: center;"> <a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/remote_r.jpg"><img width="70%" src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/remote_r.jpg" alt="R Type Remote" /></a> <p><code>R</code> Type Remote (YRL2016)</p> </div> <div style="text-align: center;"> <a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/remote_x.jpg"><img width="70%" src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/remote_x.jpg" alt="X Type Remote" /></a> <p><code>X</code> Type Remote (YR3144)</p> </div> </div> <p>Having a remote in every room of the house isn’t feasible, since many channels would be unused on these remotes and thus a waste of $$$ purchasing all the remotes. Instead, multiple rooms are programmed onto the same remote. Unfortunately due to this, remotes are highly contended for.</p> <p>An alternate solution to using the RAEX remotes is to use a piece of hardware called the <a href="http://www.ibroadlink.com/rm/">RM Pro</a>. This allows you to control the remotes via your smartphone using their app</p> <div style="display: flex; align-items: center; margin: 20px 0"> <div style="text-align: center;"> <a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/rmpro_home.jpg"><img width="70%" src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/rmpro_home.jpg" alt="RM Pro Home Screen" /></a> </div> <div style="text-align: center;"> <a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/rmpro_blind.jpg"><img width="70%" src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/rmpro_blind.jpg" alt="RM Pro Blind Control Screen" /></a> </div> </div> <p>The app is slow, buggy and for me, doesn’t fit well into the home-automation ecosystem. I want my roller blinds to be accessible via Apple Homekit.</p> <p>In order to control these blinds, I knew I’d need to either:</p> <ol> <li>Reverse engineer how the RM Pro App communicated with the RM Pro and piggy-back onto this</li> <li>Reverse engineer the RF protocol the remotes used to communicate with the blinds.</li> </ol> <p>I attempted option 1 for a little while, but ruled it out as I was unable to intercept the traffic used to communicate between the iPhone and the hub. Therefore, I began my adventure to reverse engineer the RF protocol.</p> <p>I purchased a 433MHz transmitter/receiver pair for Arduino on <a href="http://www.ebay.com.au/itm/433Mhz-RF-transmitter-receiver-link-kit-for-Arduino-Free-Postage-/302377132217?">Ebay</a>. In case that link stops working, try searching Ebay for <em>433Mhz RF transmitter receiver link kit for Arduino</em>.</p> <div class="text-center"> <a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/rf-transmitter-receiver.jpg"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/rf-transmitter-receiver.jpg" width="50%" alt="RF Transmitter / Receiver" /> </a> </div> <h3 id="initial-research">Initial Research</h3> <p>A handful of Google searches didn’t yield many results for finding a technical specification of the protocol RAEX were using.</p> <ul> <li>I could not find any technical specification of the protocol via FCC or patent lookup</li> <li>Emailed RM Pro to obtain technical specification; they did not understand my English.</li> <li>Emailed RAEX to obtain technical specification; they would not release without confidentiality agreement.</li> <li>I did find that <a href="http://www.rfxcom.com/">RFXTRX</a> was able to control the blind via their BlindsT4 mode, which appears to also work for <em>Outlook Motion Blinds</em>.</li> <li>After opening one of the remotes and identifying the micro-controllers in use, I was unable to find any documentation explaining a generic RF encoding scheme being used.</li> <li>It <em>may</em> have been possible to reverse engineer the firmware on a remote by taking an I2C <a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/remote-pcb.jpg">dump of the ROM chip</a>. It seems similar remotes <a href="http://travisgoodspeed.blogspot.co.uk/2010/07/reversing-rf-clicker.html">allow dumping at any point after boot</a></li> </ul> <h3 id="capturing-the-data">Capturing the data</h3> <p>Once my package had arrived I hooked up the receiver to an Arduino and began searching for an Arduino sketch that could capture the data being transmitted. I tried <a href="https://www.liwen.id.au/arduino-rf-codes/">many things</a> that all failed, however eventually <a href="https://github.com/nickw444/homekit/blob/master/blindkit/sketches/receive_manchester.ino">found one</a> that appeared to capture the data.</p> <p>Once I captured what I deemed to be enough data, I began <a href="https://github.com/nickw444/homekit/blob/master/blindkit/research/initial-captures-analysis.txt">analysing it</a>. It was really difficult to make any sense of this data, and I didn’t even know if what had been captured was correct.</p> <p>I did <a href="http://mightydevices.com/?p=300">some</a> <a href="http://rayshobby.net/?p=3381">further</a> <a href="http://rayshobby.net/interface-with-remote-power-sockets-final-version/">reading</a> and read a few RF reverse engineering write-ups. A lot of them experimented with the idea of using Audacity to capture the signal via the receiver plugged into the microphone port of the computer. I thought, why not, and began working on this.</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/capturing-rig.jpg"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/capturing-rig.jpg" class="img-responsive" alt="The RF capturing setup" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/audacity1.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/audacity1.png" class="img-responsive" alt="Audacity capture" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>This captures <em>a lot</em> of data. I captured 4 different <code class="language-plaintext highlighter-rouge">R</code> type remotes, along with 2 different <code class="language-plaintext highlighter-rouge">X</code> type remotes, and to make things even more fun, 8 different devices pairings from the Broadlink RM Pro (<code class="language-plaintext highlighter-rouge">B</code> type).</p> <p>From this, I was able to determine a few things</p> <ol> <li>The transmissions did not have a rolling code. Therefore, I could simply replay captured signals and make the blind do the exact same thing each time. This would be the worst-case scenario if I could not reverse engineer the protocol.</li> <li>The transmissions were repeated at least 3 times (changed depending on the remote type being used)</li> </ol> <p>Zooming into the waveform, we can see the different parts of a captured transmission. This example below is the capture of Remote 1, Channel 1, for the <strong>pairing</strong> action:</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/r1c1pairing1.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/r1c1pairing1.png" class="img-responsive" alt="R1, CH1 PAIR capture" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>Zooming in:</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/r1c1pairing2.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/r1c1pairing2.png" class="img-responsive" alt="Zoomed R1, CH1 PAIR capture" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>In the zoomed image you can see that the transmission begins with a oscillating <code class="language-plaintext highlighter-rouge">0101</code> AGC pattern, followed by a further double width preamble pattern, followed by a longer header pattern, and then by data.</p> <p>This preamble, header and data is repeated 3 times for R type remotes (The AGC pattern is only sent once at the beginning of transmission). This can be seen in the first image.</p> <p>Looking at this data won’t be too useful. I need a way to turn it digital and analyse the bits and determine some patterns between different remotes, channels and actions.</p> <h3 id="decoding-the-waveform">Decoding the waveform.</h3> <p>We need to determine how the waveform is encoded. It’s very common for these kinds of hardware applications to use one of the following:</p> <ul> <li><a href="http://www.erg.abdn.ac.uk/users/gorry/course/phy-pages/man.html">Manchester Encoding</a>,</li> <li><a href="http://tinkerman.cat/decoding-433mhz-rf-data-from-wireless-switches/">Tri-State/Tri-bit Encoding</a>, <a href="http://tinkerman.cat/decoding-433mhz-rf-data-from-wireless-switches-the-data/">Additional info</a></li> <li>PWM Encoding</li> <li>Raw? high long = <code class="language-plaintext highlighter-rouge">11</code>, high short = <code class="language-plaintext highlighter-rouge">1</code>, low long = <code class="language-plaintext highlighter-rouge">00</code>, low short = <code class="language-plaintext highlighter-rouge">0</code>?</li> </ul> <p>By doing some <a href="https://www.youtube.com/watch?v=i_TLLACZuRk">research</a>, I was able to determine that the encoding used was most likely manchester encoding. Let’s keep this in mind for later.</p> <h3 id="digitising-the-data">Digitising the data</h3> <p>I began processing the data as the raw scheme outlined above (even though I believed it was manchester). The reason for this is that if it happened to not be manchester, I could try decode it again with another scheme. (Also writing out raw by hand was easier than doing manchester decoding in my head).</p> <p>I wrote out each capture into a <a href="https://docs.google.com/spreadsheets/d/1oP6-OY93fNaIKRSyX8hcdRp30glidLyhRHn7Lt-4DNo/edit?usp=sharing">Google Sheets spreadsheet</a>. It took about 5 minutes to write out each action for each channel, and there were 6 channels per remote. I began to think this would take a while to actually get enough data to analyse. (Considering I had 160 captures to digitise)</p> <p>I stopped once I collected all actions from 8 different channels across 2 remotes. This gave me 32 captures to play with. From this much data, I was able to infer a few things about the raw bits:</p> <ul> <li>Some bits changed per channel</li> <li>Some bits changed per remote.</li> <li>Some bits changed seemingly randomly for each channel/remote/action combination. <ul> <li>Could this be some sort of checksum?</li> </ul> </li> </ul> <p>I still needed more data, but I had way too many captures to decode by hand. In order to get anywhere with this, I needed a script to process WAV files I captured via Audacity. I <a href="https://github.com/nickw444/homekit/blob/master/blindkit/rf-process/process_waveform.py">wrote a script</a> that detected headers and extracted data as its raw encoding equivalent (as I had been doing by hand). This script produced output in JSON so I could add additional metadata and cross-check the captures with the waveform:</p> <figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"filename"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Users/nickw/Dropbox/RF_Blinds/Export_Audio2/tracks2/R1_CH1.wav"</span><span class="p">,</span><span class="w"> </span><span class="nl">"captures"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="s2">"01100101100110011001100101101001011010010110011010011010101010101010101010011001101010101010101010101010101"</span><span class="p">,</span><span class="w"> </span><span class="nl">"header_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">15751</span><span class="p">,</span><span class="w"> </span><span class="nl">"preamble_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">15071</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="s2">"01100101100110011001100101101001011010010110011010100110101010101001101010011001101010101010101010101010101"</span><span class="p">,</span><span class="w"> </span><span class="nl">"header_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">46307</span><span class="p">,</span><span class="w"> </span><span class="nl">"preamble_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">45628</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="s2">"01100101100110011001100101101001011010010110011010010110101010101010011010011001101010101010101010101010101"</span><span class="p">,</span><span class="w"> </span><span class="nl">"header_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">73514</span><span class="p">,</span><span class="w"> </span><span class="nl">"preamble_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">72836</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="s2">"01100101100110011001100101101001011010010110011010101010101010100101010101101001011010101010101010101010101"</span><span class="p">,</span><span class="w"> </span><span class="nl">"header_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">103575</span><span class="p">,</span><span class="w"> </span><span class="nl">"preamble_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">102895</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span></code></pre></figure> <p>Once verified, I tabulated this data and inserted it into my spreadsheet for further processing. Unfortunately there was too many bits per capture to keep myself sane:</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-raw-captures.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-raw-captures.png" class="img-responsive" alt="Raw captures inside a spreadsheet" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>I decided it would be best if I decoded this as manchester. To do this, I <a href="https://github.com/nickw444/homekit/blob/master/blindkit/rf-process/tabulate.py">wrote a script</a> that processes the raw capture data into manchester (or other encoding types). Migrating this data into my spreadsheet, it begins to make a lot more sense.</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-manchester-1.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-manchester-1.png" class="img-responsive" alt="Manchester captures inside a spreadsheet" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>Looking at this data we can immediately see some relationship between the bits and their purpose:</p> <ul> <li>6 bits for channel (<code class="language-plaintext highlighter-rouge">C</code>)</li> <li>2 bits for action (<code class="language-plaintext highlighter-rouge">A</code>)</li> <li>6 bits for some checksum, appears to be a function of action and channel. <code class="language-plaintext highlighter-rouge">F(A, C)</code> <ul> <li>Changes when action changes</li> <li>Changes when channel changes.</li> <li>Cannot be certain it changes across remotes, since no channels are equal.</li> </ul> </li> <li>1 bit appears to be a function of Action <code class="language-plaintext highlighter-rouge">F(A)</code></li> <li>1 bit appears to be a function of <code class="language-plaintext highlighter-rouge">F(A)</code>, thus, <code class="language-plaintext highlighter-rouge">G(F(A))</code>. It changes depending on <code class="language-plaintext highlighter-rouge">F(A)</code>’s value, sometimes 1-1 mapping, sometimes inverse mapping.</li> </ul> <p>After some further investigation, I determined that for the same remote and channel, for each different action, the <code class="language-plaintext highlighter-rouge">F(A, C)</code> increased by 1. (if you consider the bits to be big-endian.).</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-manchester-2.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-manchester-2.png" class="img-responsive" alt="Encoded value increasing per different action" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>Looking a bit more into this, I also determined that for adjacent channels, the bits associated with <code class="language-plaintext highlighter-rouge">C</code> (Channel) count upwards/backwards (X type remotes count upwards, R type remotes count backward). Additionally <code class="language-plaintext highlighter-rouge">F(C)</code> also increases/decreases together. Pay attention to the <code class="language-plaintext highlighter-rouge">C</code> column.</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-manchester-3.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-manchester-3.png" class="img-responsive" alt="Encoded value increasing with adjacent channels" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>From this, I can confirm a relationship between <code class="language-plaintext highlighter-rouge">F(A, C)</code> and <code class="language-plaintext highlighter-rouge">C</code>, such that <code class="language-plaintext highlighter-rouge">F(A, C) = F(PAIR, C0) == F(PAIR, C1) ± 1</code>. After this discovery, I also determine that there’s another mathematical relationship between <code class="language-plaintext highlighter-rouge">F(A, C)</code> and <code class="language-plaintext highlighter-rouge">A</code> (Action).</p> <h3 id="making-more-data">Making More Data</h3> <p>From the information we’ve now gathered, it seems plausible that we can create new remotes by changing 6 bits of channel data, and mutating the checksum accordingly, following the mathematical relationship we found above. This means we can generate 64 channels from a single seed channel. This many channels is enough to control all the blinds in the house, however I really wanted to fully decode the checksum field and in turn, be able to generate an (almost) infinite amount of remotes.</p> <p>I wrote a <a href="https://github.com/nickw444/homekit/blob/2cac1db865a8a74d784d8d8f56c7e17caa654d96/blindkit/remote-gen/generate_cmd.go">tool</a> to output all channels for a seed capture:</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">./remote-gen generate 01000110110100100001010110111111111010101 ...</code></pre></figure> <p>My reasoning behind generating more data was that maybe we could determine how the checksum is formed if we can view different remotes on the same channel. I.e. <code class="language-plaintext highlighter-rouge">R0CH0</code>, <code class="language-plaintext highlighter-rouge">R1CH0</code>, <code class="language-plaintext highlighter-rouge">X1CH0</code>, etc…</p> <p>Essentially what I wanted to do was solve the following equation’s function <code class="language-plaintext highlighter-rouge">G</code>:</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">F(ACTION_PAIR, CH0) == G(F(ACTION_PAIR, CH0))</code></pre></figure> <p>However, looking at all Channel 0’s PAIR captures, the checksum still appeared to be totally jumbled/random:</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-jumbled-checksum.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-jumbled-checksum.png" class="img-responsive" alt="Identical channels / action jumbled checksums" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>Whilst looking at this data, however, another pattern stands out. <code class="language-plaintext highlighter-rouge">G(F(A))</code> sits an entire byte offset (8 bits) away from <code class="language-plaintext highlighter-rouge">F(A)</code>. Additionally the first 2 bits of <code class="language-plaintext highlighter-rouge">F(A, C)</code> sit at the byte boundary and also align with <code class="language-plaintext highlighter-rouge">A</code> (Action). As Action increases, so does <code class="language-plaintext highlighter-rouge">F(A, C)</code>. Lets line up all the bits at their byte boundaries and see what prevails:</p> <div style="margin: 20px 0;"> <a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-byte-boundaries-1.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-byte-boundaries-1.png" class="img-responsive" alt="Identified Boundaries" /> </a> <div style="font-size:14px; margin-top: 5px; text-align: center;">Colours denoting byte boundaries</div> </div> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <div style="margin: 20px 0;"> <a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-byte-boundaries-2.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-byte-boundaries-2.png" class="img-responsive" alt="Aligned byte boundaries" /> </a> <div style="font-size:14px; margin-top: 5px; text-align: center;">Aligned boundaries</div> </div> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>From here, we need to determine some function that produces the known checksum based on the first 4 bytes. Initially I try to do XOR across the bytes:</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-checksum-xor.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-checksum-xor.png" class="img-responsive" alt="Attempt to find checksum function via XOR" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>Not so successful. The output appears random and XOR’ing the output with the checksum does not produce a constant key. Therefore, I deduce the checksum isn’t produced via XOR. How about mathematical addition? We’ve already seen some addition/subtraction relationship above.</p> <p><a href="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-checksum-addition.png"> <img src="/static/posts/2017-07-15-reversing-433mhz-raex-motorised-rf-blinds/findings-checksum-addition.png" class="img-responsive" alt="Attempt to find checksum function via addition" /> </a></p> <!-- { %include linked-img.html src="" alt="" caption="" % } --> <p>This appeared to be more promising - there was a constant difference between channels for identical type remotes. Could this constant be different across different type remotes because my generation program had a bug? Were we not wrapping the correct number of bits or using the wrong byte boundaries when mutating the channel or checksum?</p> <p><em>It turns out that this was the reason</em> 😑.</p> <h3 id="solving-the-checksum">Solving the Checksum</h3> <p>Looking at the original captures, and performing the same modulo additions, we determine the checksum is computed by adding the leading 4 bytes and adding 3. I can’t determine why a <code class="language-plaintext highlighter-rouge">3</code> is used here, other than RAEX wanting to make decoding their checksum more difficult or to ensure a correct transmission pattern.</p> <p>I refactored my application to handle the boundaries we had just identified:</p> <figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">type</span> <span class="n">RemoteCode</span> <span class="k">struct</span> <span class="p">{</span> <span class="n">LeadingBit</span> <span class="kt">uint</span> <span class="c">// Single bit</span> <span class="n">Channel</span> <span class="kt">uint8</span> <span class="n">Remote</span> <span class="kt">uint16</span> <span class="n">Action</span> <span class="kt">uint8</span> <span class="n">Checksum</span> <span class="kt">uint8</span> <span class="p">}</span></code></pre></figure> <p>Looking at the data like this began to make more sense. It turns out that <code class="language-plaintext highlighter-rouge">F(A)</code> wasn’t a function of <code class="language-plaintext highlighter-rouge">A</code> (Action), it was actually part of the action data being transmitted:</p> <figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">type</span> <span class="n">BlindAction</span> <span class="k">struct</span> <span class="p">{</span> <span class="n">Name</span> <span class="kt">string</span> <span class="n">Value</span> <span class="kt">uint8</span> <span class="p">}</span> <span class="k">var</span> <span class="n">validActions</span> <span class="o">=</span> <span class="p">[]</span><span class="n">BlindAction</span><span class="p">{</span> <span class="n">BlindAction</span><span class="p">{</span><span class="n">Value</span><span class="o">:</span> <span class="m">127</span><span class="p">,</span> <span class="n">Name</span><span class="o">:</span> <span class="s">"PAIR"</span><span class="p">},</span> <span class="n">BlindAction</span><span class="p">{</span><span class="n">Value</span><span class="o">:</span> <span class="m">252</span><span class="p">,</span> <span class="n">Name</span><span class="o">:</span> <span class="s">"DOWN"</span><span class="p">},</span> <span class="n">BlindAction</span><span class="p">{</span><span class="n">Value</span><span class="o">:</span> <span class="m">253</span><span class="p">,</span> <span class="n">Name</span><span class="o">:</span> <span class="s">"STOP"</span><span class="p">},</span> <span class="n">BlindAction</span><span class="p">{</span><span class="n">Value</span><span class="o">:</span> <span class="m">254</span><span class="p">,</span> <span class="n">Name</span><span class="o">:</span> <span class="s">"UP"</span><span class="p">},</span> <span class="p">}</span></code></pre></figure> <p>Additionally, the fact there is a split between channel and remote probably isn’t necessary. Instead this could just be an arbitrary 24 bit integer, however it is easier to work with splitting it up as an 8 bit int and a 16 bit int. Based on this, I can deduce that the protocol has room for 2^24 remotes (~16.7 million)! That’s a lot of blinds!</p> <p>I formally write out the checksum function:</p> <figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">func</span> <span class="p">(</span><span class="n">r</span> <span class="o">*</span><span class="n">RemoteCode</span><span class="p">)</span> <span class="n">GuessChecksum</span><span class="p">()</span> <span class="kt">uint8</span> <span class="p">{</span> <span class="k">return</span> <span class="n">r</span><span class="o">.</span><span class="n">Channel</span> <span class="o">+</span> <span class="n">r</span><span class="o">.</span><span class="n">Remote</span><span class="o">.</span><span class="n">GetHigh</span><span class="p">()</span> <span class="o">+</span> <span class="n">r</span><span class="o">.</span><span class="n">Remote</span><span class="o">.</span><span class="n">GetLow</span><span class="p">()</span> <span class="o">+</span> <span class="n">r</span><span class="o">.</span><span class="n">Action</span><span class="o">.</span><span class="n">Value</span> <span class="o">+</span> <span class="m">3</span> <span class="p">}</span></code></pre></figure> <h3 id="additional-tooling">Additional Tooling</h3> <p>My <a href="https://github.com/nickw444/homekit/tree/master/blindkit/remote-gen"><code class="language-plaintext highlighter-rouge">remote-gen</code></a> program was good for the purpose of generating codes using a seed remote (although, incorrect due to wrapping issues), however it now needed some additional functionality.</p> <p>I needed a way to extract information from the captures and verify that all their checksums align with our rule-set for generating checksums. I wrote an info command:</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">./remote-gen info 00010001110001001101010111011111101010100 --validate Channel: 196 Remote: 54673 Action: STOP Checksum: 42 Guessed Checksum: 42</code></pre></figure> <p>Running with <code class="language-plaintext highlighter-rouge">--validate</code> exits with an error if the guessed checksum != checksum. Running this across all of our captures proved that our checksum function was correct.</p> <p>Another piece of functionality the tool needed was the ability to generate arbitrary codes to create our own <em>remotes</em>:</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">./remote-gen create --channel=196 --remote=54654 --verbose 00010001101111110101010111111111010011001 Action: PAIR 00010001101111110101010110011111101101000 Action: DOWN 00010001101111110101010111011111111101000 Action: STOP 00010001101111110101010110111111100011000 Action: UP</code></pre></figure> <p>I now can generate any remote I deem necessary using this tool.</p> <h3 id="wrapping-up">Wrapping Up</h3> <p>There you have it, that’s how I reverse engineered an unknown protocol. I plan to follow up this post with some additional home-automation oriented blog posts in the future.</p> <p>From here I’m going to need to build my transmitter to transmit my new, generated codes and build an interface into homekit for this via my <a href="https://github.com/nickw444/homekit/tree/master/bridges/mainbridge">homebridge</a> program.</p> <p>You can view all the work related to this project in the <a href="https://github.com/nickw444/homekit/tree/master/blindkit">nickw444/homekit/blindkit</a> repo.</p> <p>As mentioned above, if you are based in Australia, you can purchase these blinds and associated accessories in bulk or individually via <a href="https://www.raexaustralia.com?utm_source=nickwhyte.com&amp;utm_medium=post&amp;utm_content=433mhz-reversing">www.raexaustralia.com</a> (Full disclosure – my father runs the site)</p> Sat, 15 Jul 2017 00:00:00 +0000 https://nickwhyte.com/post/2017/reversing-433mhz-raex-motorised-rf-blinds/ https://nickwhyte.com/post/2017/reversing-433mhz-raex-motorised-rf-blinds/ Programming Electronics 100 Warm Tunas 2016 <div class="pull-right hidden-xs" style="font-size:100px; padding: 5px 5px 5px 20px;">&#128293;&#128175;</div> <div class="pull-right hidden-sm hidden-lg hidden-md" style="font-size:68px; padding: 5px 5px 5px 20px;">&#128293;&#128175;</div> <p>Last year I predicted the top 3 results in order in Triple J’s hottest 100. This year I’m back at it again, however, <a href="https://nickwhyte.com/100-warm-tunas-2016/">now with a webpage and a Spotify playlist</a>.</p> <p>Results are collected, optimised, and processed multiple times per day. Instagram images tagged with #hottest100 and a few others are included for counting.</p> <p>Happy voting!</p> <p>You can read about the <a href="https://nickwhyte.com/post/2016/predicting-triple-j-hottest-100-2015/">process last year here</a>. However, vote collection is a fair bit more accurate this year.</p> <hr /> <h3 id="press">Press:</h3> <ul> <li><a href="http://inthemix.junkee.com/computer-science-nerd-predicted-triple-js-hottest-100-results/149015">A computer science nerd has predicted triple j’s Hottest 100 results</a> (inthemix)</li> <li><a href="http://junkee.com/computer-science-grad-may-figured-predict-hottest-100/92990">A Computer Science Grad May Have Figured Out How To Predict The Hottest 100</a> (Junkee)</li> <li><a href="http://stoneyroads.com/2017/01/this-dudes-hottest-100-prediction-method-is-pretty-damn-good">This Dude’s Hottest 100 Prediction Method Is Pretty Damn Good</a> (Stoney Roads)</li> <li><a href="http://www.tonedeaf.com.au/495885/biggest-hottest-100-upsets.htm">We Could Be Looking At One Of The Biggest Hottest 100 Upsets In Years</a> (Tonedeaf)</li> <li><a href="http://chattr.com.au/2017/01/07/hottest-100-2016-results/">Has the Triple J Hottest 100 2016 Result Been Predicted?</a> (Chattr)</li> <li><a href="http://www.oddschecker.com.au/tips/tv-and-specials/20170112-oddschecking-the-triple-j-hottest-100">Oddschecking The Triple J Hottest 100</a> (Oddschecker)</li> <li><a href="http://thenewdaily.com.au/entertainment/ /2017/01/15/hottest-100-2016-countdown-prediction/">Triple J Hottest 100: Computer geek predicts the 2016 countdown</a> (The New Daily)</li> <li><a href="http://themusic.com.au/news/all/2017/01/21/amy-shark-opens-up-on-the-pressure-of-being-a-hottest-100-favourite/">Amy Shark Opens Up On The Pressure Of Being A Hottest 100 Favourite</a> (The Music)</li> <li><a href="http://www.standard.net.au/story/4410130/who-will-win-triple-js-hottest-100-for-2016/">Who will win triple j’s Hottest 100 for 2016?</a> (The Standard)</li> <li><a href="https://www.theguardian.com/music/2017/jan/25/triple-j-hottest-100-crib-notes-up-and-coming-artists-set-to-sweep-the-countdown">Triple J Hottest 100 crib notes: up-and-coming artists set to sweep the countdown</a> (The Guardian)</li> <li><a href="https://studentedge.com.au/article/do-we-already-know-the-hottest-100-winner">Do We Already Know The “Hottest 100” Winner?</a> (Student Edge)</li> <li><a href="http://musicfeeds.com.au/news/hottest-100-experts-predict-top-2016-countdown">Hottest 100 Experts On Who They Predict Will Top This Year’s Countdown</a> (Music Feeds)</li> <li><a href="http://www.dailytelegraph.com.au/subscribe/news/1/index.html?sourceCode=DTWEB_WRE170_a&amp;mode=premium&amp;dest=http://www.dailytelegraph.com.au/entertainment/music/could-amy-shark-or-flume-reach-no-1-on-this-years-hottest-100/news-story/d5499b0a01de8d91c34227868d287539&amp;memtype=anonymous">Could Amy Shark Or Flume Reach No 1 On This Years Hottest 100</a> (Daily Telegraph)</li> </ul> <h3 id="other-mentions">Other Mentions:</h3> <ul> <li><a href="http://www.eigenmagic.com/2017/01/25/triplej-hottest-100-predictions-2016-edition/">TripleJ Hottest 100 Predictions 2016 Edition</a></li> <li><a href="https://beancounters100.com/blog/">The Bean Counters – 100</a></li> <li><a href="http://www.hottest100comp.com/">Hottest 100 Comp</a></li> </ul> <hr /> <p><small><strong>Edit:</strong> Woohoo, the Spotify playlist now has just over 1200 followers, and the website has had over 30,000 hits! That’s massive, thanks everyone!</small></p> Wed, 04 Jan 2017 00:00:00 +0000 https://nickwhyte.com/post/2017/100-warm-tunas-2016/ https://nickwhyte.com/post/2017/100-warm-tunas-2016/ Blog Programming Understanding and Tweaking some GPX data <p>As a casual bike rider, I enjoy tracking my rides with <a href="https://www.strava.com">Strava</a> so I can take a look at how my ride went and how well I performed throughout.</p> <p>However, very rarely the Strava tracking application randomly crashes, or gets killed by iOS on my phone, during the ride. This means that the data was never recorded between the point at which the app died and the point when I became aware the app had died.</p> <p>If we plot this type of failure, it looks something like this:</p> <p class="text-center"> <img style="max-width: 450px; width:100%" src="/static/posts/2016-10-29-playing-with-gpx/playing-with-gpx.png" alt="Map with missing data" /> </p> <p>Fortunately in this case, there wasn’t too much missing data. However, I was still determined to learn about the GPX format and see if I could patch up the GPX file programatically.</p> <p>In the specific case of the above map, I was riding north west, and at a point Strava crashed. Between this point and when I pulled out my phone to check my progress, no points were plotted. Google maps interprets this lack of data as a straight line between to the 2 points (as per GPX specification).</p> <p>If we crack open the GPX file and take a look, we can see exactly what this looks like:</p> <figure class="highlight"><pre><code class="language-xml" data-lang="xml">... <span class="nt">&lt;trkpt</span> <span class="na">lat=</span><span class="s">"-33.9014420"</span> <span class="na">lon=</span><span class="s">"151.1066810"</span><span class="nt">&gt;</span> <span class="nt">&lt;ele&gt;</span>6.6<span class="nt">&lt;/ele&gt;</span> <span class="nt">&lt;time&gt;</span>2016-10-28T23:38:50Z<span class="nt">&lt;/time&gt;</span> <span class="nt">&lt;/trkpt&gt;</span> <span class="nt">&lt;trkpt</span> <span class="na">lat=</span><span class="s">"-33.8802920"</span> <span class="na">lon=</span><span class="s">"151.0702190"</span><span class="nt">&gt;</span> <span class="nt">&lt;ele&gt;</span>20.9<span class="nt">&lt;/ele&gt;</span> <span class="nt">&lt;time&gt;</span>2016-10-28T23:51:14Z<span class="nt">&lt;/time&gt;</span> <span class="nt">&lt;/trkpt&gt;</span> ...</code></pre></figure> <p>In it’s simplest form, a GPX file is an XML document that contains a sequence of GPS points (with associated metadata like elevation, and other depending on the tracker). This makes it reasonably simple for us to get our hands dirty and begin fixing the data set.</p> <p>In order to add the missing data back into the GPX file, we need 3 things:</p> <ul> <li>The last coordinate recorded before the app crashed</li> <li>The coordinate when the app was revived</li> <li>A list of points of the track we want to use for our data points.</li> </ul> <p>Fortunately, I was able to obtain a list of coordinates for the missing data since I travelled the same path on the return journey (As can be seen on the map above).</p> <p>The other 2 app state points of interest are reasonably easy to find - just find 2 data points that have a (reasonably) large time distance between them.</p> <p>In order to process the data, I used a python library called <code class="language-plaintext highlighter-rouge">gpxpy</code> which provided some good utilities for reading and processing a GPX file.</p> <p>With this library, I was able to find the crash point, the revival point, and the list of the points of the track. With this data, I interpolated the start/end times of the crash points onto the track data, and spliced it back into the dataset.</p> <p>After exporting the data set, we achieve a map that looks like:</p> <p class="text-center"> <img style="max-width: 450px; width:100%" src="/static/posts/2016-10-29-playing-with-gpx/playing-with-gpx-fixed-map.png" alt="Map with resolved data" /> </p> <p>Quite clearly, this has a few limitations, for example, the calculated velocity through all of the data points is simply an average. However, this did provide me with an improved dataset which I could re-upload to Strava.</p> <p>You can find all the source for this script <a href="https://github.com/nickw444/strava-fixer">on my github</a></p> Sat, 29 Oct 2016 00:00:00 +0000 https://nickwhyte.com/post/2016/playing-with-gpx/ https://nickwhyte.com/post/2016/playing-with-gpx/ Blog Programming