tag:blogger.com,1999:blog-31445698768656816732024-03-13T08:48:05.094-07:00Mark's Project BlogProjects I've undertaken, mostly home automation and small electronics.Mark T. Tomczakhttp://www.blogger.com/profile/00635435261857863001noreply@blogger.comBlogger77125tag:blogger.com,1999:blog-3144569876865681673.post-27028167590615628972022-09-05T06:00:00.019-07:002022-09-05T06:00:00.237-07:00Transitioning to a new blog<p> tl;dr: This blog will be following its sibling blog and transitioning to my personal site, <a href="http://blog.fixermark.com">blog.fixermark.com</a>.</p><h2 style="text-align: left;">Changing to another blog engine</h2><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW5nO46xT0VQEiNC3wXnY-EexCCc20zpSe51zPzbRQ8J_TjXbnjP-FEDZ435wJtDqhaI3M8V48szihbaL-12FWdRBkD3e0GXblp6KBFDn7OyRoDuJvx5nNU1dTHATBxDc62vItsV6el6dBBzQLbHBOUCjP67nXv_Kw7ojoStbDfPL-GxqKDi28Yr_o/s550/fish.jpg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="550" data-original-width="550" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW5nO46xT0VQEiNC3wXnY-EexCCc20zpSe51zPzbRQ8J_TjXbnjP-FEDZ435wJtDqhaI3M8V48szihbaL-12FWdRBkD3e0GXblp6KBFDn7OyRoDuJvx5nNU1dTHATBxDc62vItsV6el6dBBzQLbHBOUCjP67nXv_Kw7ojoStbDfPL-GxqKDi28Yr_o/s320/fish.jpg" width="320" /></a></div><br /><div><br /></div><div>Effective today, new posts will be showing up at <a href="http://blog.fixermark.com">blog.fixermark.com</a> instead of here. Blogger has been an excellent home for several years, but I've decided to shift my writing to my own server to simplify several things. Some details follow.</div><div><br /></div><h3 style="text-align: left;">Why?</h3><div>Several reasons, but the main ones are control and convenience. I've set up a pretty clean blog-authoring architecture behind the scenes using <a href="https://gohugo.io/">Hugo</a>, and the most effort-intensive part next to "actually writing a blog post" is "reformatting the Hugo output to match to Blogger's requirements." Self-hosting just makes it easier to maintain the blog.</div><div><br /></div><div>(... See that break in the background color there? The place where light cream gradient discontinuously jumps into flat orange? That's just a bug in the style of the theme Google provided. Can I fix it? Maybe. How? Don't know. On Hugo, I just set the background to a back-and-forth gradient and I'm happy).</div><div><br /></div><h3 style="text-align: left;">Do you hate Google now?</h3><div>Definitely not, in fact, I'm giving up several convenient features by going to self-hosting:</div><div><ul style="text-align: left;"><li>The analytics here are top-notch, and the analytics I get on my own blog are far, far lower-resolution. I've chosen not to attach Google Analytics to my new blog as it brings concern for some readers, so such lack of resolution is to be expected.</li><li>Google is very, <i>ahem, </i>generous with indexing blog posts on its own service into its index. In contrast, my personal blog has been off Blogger for several months now and has yet to show up in any search indices.</li><li>Blogger's web interface works great. My new blog requires editing text files and uploading the results to a server.</li><li>The new blog is totally statically-generated, so comments and timed publishing have to be done by me manually now. In contrast, this post went up automatically at the time I set it to.</li></ul><div>All of that said: the driving factor to leaving is still UI convenience. Blogger's UI hasn't really evolved in ages, and for technical writing in particular it has some pretty severe friction points that I will not miss.</div></div><div><br /></div><h3 style="text-align: left;">What of all the content that's already here?</h3><div>Nothing will be deleted from this blog, because that would mess up way too many permalinks (and I'm not planning to put the effort in to clone all the articles here to the new site). You should be able to find everything here indefinitely.</div><div><br /></div><div>So that's that then. Thanks for being my home for awhile Blogger; you did good work. But I'm hoping people enjoy the new blog, where I've already put up an article about building a plugin for the FIRST Robotics "Shuffleboard" dashboard program. Hope to see you there!</div>Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-8987363237816194102022-07-04T06:00:00.001-07:002022-07-04T06:00:00.210-07:00Where's Mark been?<p> </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOc8rbC7Wl0CB4ZR30MaRPpR6rk_li0Uqbg8iWw832o_MAaIKPZA7bWdia-U_x_As8D8-wdEN5Vm3aMEqAfQd6jP5JKGsXLWixuzFcNBo-9k3EcIcEuGVTS8hAHNS6MTI3j5wdRwBXTXdfGVYnCN0ungBYG4NJXUAwjudphUDIxVCW5LfDsxLZfMbJ/s586/loading.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="328" data-original-width="586" height="179" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOc8rbC7Wl0CB4ZR30MaRPpR6rk_li0Uqbg8iWw832o_MAaIKPZA7bWdia-U_x_As8D8-wdEN5Vm3aMEqAfQd6jP5JKGsXLWixuzFcNBo-9k3EcIcEuGVTS8hAHNS6MTI3j5wdRwBXTXdfGVYnCN0ungBYG4NJXUAwjudphUDIxVCW5LfDsxLZfMbJ/s320/loading.png" width="320" /></a></div><br /><p></p><p>So what happened to the rest of that drive train story?</p><p>Well, it's complicated. No literally. I dove into researching what I wanted to write next, and found it to be way more complicated than I thought. I'm digesting a bunch of control theory that has been too hard to fit into one blog post (for the record, mostly from <a href="https://file.tavsys.net/control/controls-engineering-in-frc.pdf" target="_blank">this source</a>). Once I can break off a piece worth sharing, I'll do so.</p><p>Wish me luck!</p>Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-25106747336079908802022-05-02T06:00:00.001-07:002022-05-02T06:00:00.215-07:00Simulating a differential drivetrain, part 1 - If it's stupid but it works... <p>There’s a lot going on in a drivetrain.</p>
<p>On the surface, they seem pretty simple: “Rotary power comes from
somewhere. It’s moved through mechanical links. The links connect to
wheels. Wheels make thing go.” But there’s a lot of subtle complexity that
underpins all of that. Drivetrains can actually be one of the more complicated
things to simulated on a robot.</p>
<p>In a FIRST competition, every robot has some method of getting around, so it’s
worth the time to invest in learning how to simulat them and what is involved in
simulating them. Over the next few weeks of posts, we’re going to dig deep on
this topic. I’m going to start from a simple simulation, then move onto a more
complicated one, and then finally onto one using built-in WPILib classes that
tries to capture many, many details of the drivetrain. Along the way, we’ll
compare the approaches and the results we get and see what the tradeoffs are.</p>
<p>Full disclosure: I’m as exploring as I am explaining here, dear reader. At the
moment, I have only implemented one of these approaches before, so I don’t
really know what we’ll discover. It’ll be exciting to see where we end up.</p>
<p>Before we start in with our first model, let’s talk a bit about what a differential
drive is and what we’ll be comparing.</p>
<h1 id="whats-a-differential-drive">What’s a differential drive?</h1><div style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinMUv4-GE6bsLMYvm2LyZAxjMNQGL9EHp5ndIvrGnbhc5tj-XIHghpHR6ENSPdxvzNOuWOfSCLlkbVbdeu3gNKRygI125utAcriNFsPRY6O0aJJuCe6N6LOvQwJoeeFsTpykV-FMY-4SSclznhwnNaY3U8-tN6LD-q6CUsWGJhE42PENAgnsHEUlp2/s1024/diffdrive.jpg" imageanchor="1"><img alt="[A picture of a robot with a differential drive train, courtesy WPILib docs" border="0" data-original-height="1024" data-original-width="768" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinMUv4-GE6bsLMYvm2LyZAxjMNQGL9EHp5ndIvrGnbhc5tj-XIHghpHR6ENSPdxvzNOuWOfSCLlkbVbdeu3gNKRygI125utAcriNFsPRY6O0aJJuCe6N6LOvQwJoeeFsTpykV-FMY-4SSclznhwnNaY3U8-tN6LD-q6CUsWGJhE42PENAgnsHEUlp2/w240-h320/diffdrive.jpg" width="240" /></a></div><div><br /></div>
<p>A differential drive (“diff drive”) is a drivetrain where the motors and wheels
are divided into two independently controlled sets: left and right. By varying
the power to each half, you can get the robot to move forward and backward or
rotate (generally, for convenience, we want to build the robot so that point is
in the middle of the chassis when the left and right trains are driven with equal
but opposite inputs).</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3QeTQUH6iM1Vr5KW_23eYmaZLeIBOUWYSlAfI8mHOyhntLEptqhh6o8IG45lZPpOZ2zeseaKqVurOnrAHNtzpJhzSOJ8smnb0-E8GGkxp44R88OL-kWKJ6s83hvpH2QAzO_trxB6H_24jURZUbk6Qd668FjTUCjBth_99aQuqO9SNQE0K6flNZTHZ/s408/diffdiagram.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Diagram of the motions a diff drive can make: forward, backward, turn left, turn right" border="0" data-original-height="183" data-original-width="408" height="144" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3QeTQUH6iM1Vr5KW_23eYmaZLeIBOUWYSlAfI8mHOyhntLEptqhh6o8IG45lZPpOZ2zeseaKqVurOnrAHNtzpJhzSOJ8smnb0-E8GGkxp44R88OL-kWKJ6s83hvpH2QAzO_trxB6H_24jURZUbk6Qd668FjTUCjBth_99aQuqO9SNQE0K6flNZTHZ/w320-h144/diffdiagram.png" width="320" /></a></div>
<p>Since there’s no way for a diff drive to “sideslip”, we say diff drive is a
<b>non-holonomic</b> system (which is a fancy way of saying there are paths through
space an air-hockey puck could take but the robot can’t; it can’t move in an
arbitrary direction while continuously spinning). But since the robot can turn
on a dime, it should still be able to get everywhere (as opposed to a robot
using car-style steering, which has some parking spots it can’t reach).</p>
<p>The simplest model of a differential drive is just two big wheels controlled by
two motors, so that’s what we’re going to use. We’ll assume our wheels have a
diameter of 4" (so about 0.1016 meters) so the robot goes about <code>π * 0.1016 ≈ 0.3192</code> meters per revolution if both wheels are driven at the same speed.</p>
<h1 id="the-comparison">The comparison</h1>
<p>For each simulation, we’ll compare the following:</p>
<ul>
<li>Driving straight for three rotations (about one meter total, more
precisely 95.76cm)</li>
<li>Driving in a circle for three rotations (one motor full forward, one full
back). Assuming a track width (distance between wheels) of 50 centimeters
(about 19.6 inches), three wheel rotations are a distance
along an arc of 31.92 centimeters in a circle with diameter 50 centimeters. Since
the circumference of a circle is <code>π * diameter</code>, we can find the angle by
<pre tabindex="0"><code> length of arc / total circumference
= 31.92 / π * 50
≈ 31.92 / 157.07
≈ 20.32% of the circle
</code></pre>… which is about <code>(2π * 20%) radians = 2/5π radians = 72 degrees</code>.</li>
<li>driving in a lazy arc (one motor full power, one motor half power) for three
rotations. This one, I’m not going to lay out the math to predict what will
happen yet (future post); we’ll just see what it does.</li>
</ul>
<p>We’ll consider for each kind of simulator</p>
<ul>
<li>What results it gives us for our inputs</li>
<li>How we set it up</li>
<li>Velocity curves, acceleration curves, and final positions</li>
</ul>
<p>And we’ll discuss some pros and cons.</p>
<p>Having established what we’re doing here, let’s start off with a very simple model.</p>
<h1 id="the-simple-interpolator">The simple interpolator</h1>
<p>So at first glance, the question of simulating a diff drive doesn’t feel too complicated. We know that</p>
<ul>
<li>If both motors go full-forward, the robot goes max-speed forward</li>
<li>If both motors go full-backward, the robot goes max-speed backward</li>
<li>If one motor is full forward and one full backward, the robot turns on a dime at max speed</li>
</ul>
<p>The interpolation simulator just assumes that any motion the robot could be
doing between these extremes is a simple linear interpolation of these
motions. We establish a top speed for the wheel based on motor input (something
simple, like “The robot goes at most 1 meter / second forward and rotates twice
a second at max speed”). Then we split the motion in question into linear motion and
turning motion.</p>
<ul>
<li>To figure out linear, we take the average of the sum of the left and right motor power and multiply that by top linear speed</li>
<li>To figure out rotation, we take the average of the <em>difference</em> of the left
and right motor (i.e. <code>(left - right) / 2)</code>) and multiply that by the top rotational speed</li>
</ul>
<p>Then we update the robot’s speed and then update the robot’s position and heading based on that speed. Updating the position
involves changing simulated encoder values, and updating the rotation involves changing a simulated gyro value.</p>
<p>There’s an implementation of this approach split between
<a href="https://github.com/fixermark/team2051-simulator-demo/blob/interpolation-simulator-observer/src/main/java/frc/robot/PoseEstimator.java">PoseEstimator.java</a>
and
<a href="https://github.com/fixermark/team2051-simulator-demo/blob/interpolation-simulator-observer/src/main/java/frc/robot/SimpleSimulatedChassis.java">SimpleSimulatedChassis.java</a>
in this simulator demo.</p>
<h1 id="comparison-and-results">Comparison and results</h1>
<p>The end result <em>feels</em> okay; when hand-controlled; it’s not obviously broken. Here’s what it looks like on the tests of motion:</p>
<p><strong>Note</strong>: one spot of trouble I got into testing this: you can change the refresh rate for graphs in Shuffleboard. If you set it too high, Shuffleboard becomes unresponseive and cannot edit the preference again. The fix is to delete the prefs from Java’s user prefs store (in my case, it’s stored in the system prefs in <code>~/.java/userPrefs/edu/api/first/shuffleboard/plugin/base/prefs.xml</code>.</p>
<h2 id="linear-motion">Linear motion</h2>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-i0Jz3vk5BmfLA33XRPZhMWcuDcIE3q0RfJ2HsJxZCe5ccZD1bALybOhI4sAQCWjQBwNQZMV_Q7oxMu_F3waFzPR6JFnJmAR5sB87hZ4upvj90bguHQFmydSycIxzA2T2AEFweJv5ZsU7FCsldvwdcr0RqEMCW0pCVX1s-g1zpMrfnSeJw-csEUBn/s861/linear-drive-graph.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Graph of the drive operation" border="0" data-original-height="861" data-original-width="430" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-i0Jz3vk5BmfLA33XRPZhMWcuDcIE3q0RfJ2HsJxZCe5ccZD1bALybOhI4sAQCWjQBwNQZMV_Q7oxMu_F3waFzPR6JFnJmAR5sB87hZ4upvj90bguHQFmydSycIxzA2T2AEFweJv5ZsU7FCsldvwdcr0RqEMCW0pCVX1s-g1zpMrfnSeJw-csEUBn/w200-h400/linear-drive-graph.png" width="200" /></a></div><br /><p><br /></p>
<p>The “Drive” command (which causes the robot to drive forward until its encoder passes three rotations)
causes the robot to drive forward and then stop; no problem. It gets pretty close to spot-on for the final value; the
overage is likely due to the fact that the simulation happens in steps of 0.02 seconds; if the robot passes the target
encoder rotation in one of those time steps, it can’t stop on a dime with simple on-off control and so stops a bit
after the target.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3UO6sREs2N9RquPKdcfRaMP0xnGwdi_A3RD9fhO6hylS-QvPcKSun40fSuIa7xK1XknQpx-hFLnJuDAaoaPmDrzyBlHBWFcDx9sAihpOih3Y216ZTqSoZaTykGFbwBEQhYC68xKCacvmc6WTBobqfxwOKhKVQBhqn5aRrdUhuiDkCVk7Vq4Mk_cQQ/s444/turn-graph.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Graph of the turn operation" border="0" data-original-height="444" data-original-width="437" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3UO6sREs2N9RquPKdcfRaMP0xnGwdi_A3RD9fhO6hylS-QvPcKSun40fSuIa7xK1XknQpx-hFLnJuDAaoaPmDrzyBlHBWFcDx9sAihpOih3Y216ZTqSoZaTykGFbwBEQhYC68xKCacvmc6WTBobqfxwOKhKVQBhqn5aRrdUhuiDkCVk7Vq4Mk_cQQ/w315-h320/turn-graph.png" width="315" /></a></div><br /><div style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_CTNLxKpQ_6lvCRFHAS7xgDDYdeMx08csZL3NFuYLNSwHatlAaNK8ZvNpYOwm0-Ml5hDc_c3nqxxSJWXgY0YQwYR-w50fnER9_NImm830HL264i5hgMBVO8ceOYmlDLcyK4qxErMSSERj-V0y_Nc5m-yJrqeWEobtwolLhdLiXOE4kv4W3tDFAjxE/s434/turn-number.png" imageanchor="1"><img alt="Final number of turn operation: 85.314" border="0" data-original-height="434" data-original-width="426" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_CTNLxKpQ_6lvCRFHAS7xgDDYdeMx08csZL3NFuYLNSwHatlAaNK8ZvNpYOwm0-Ml5hDc_c3nqxxSJWXgY0YQwYR-w50fnER9_NImm830HL264i5hgMBVO8ceOYmlDLcyK4qxErMSSERj-V0y_Nc5m-yJrqeWEobtwolLhdLiXOE4kv4W3tDFAjxE/w314-h320/turn-number.png" width="314" /></a></div><br /><p><br /></p>
<p>The “Turn” command (which attempts to cause the robot to turn precisely 72 degrees) is similarly accurate. It also overshoots because
turning can’t stop in the middle of a time step.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgKXlonuRgse2qHHtUm8DCLUDXqgsCFQsiGuad-nk66vOuGwRSLVtL2Ek7n2UD0_OGc9lPZCvl8YlzgAq7n8UVOWhvBZfOk4Flu9qlm3IQVGqOQg8UB2Mnw3arYsbGuykfLSyH2eUChfWfGzIO1tyaF_cuCzJRS0Qjqqf_yW2SZF-4YTSGlavjOgnS/s855/angular-accel.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Angular velocity and acceleration" border="0" data-original-height="443" data-original-width="855" height="208" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgKXlonuRgse2qHHtUm8DCLUDXqgsCFQsiGuad-nk66vOuGwRSLVtL2Ek7n2UD0_OGc9lPZCvl8YlzgAq7n8UVOWhvBZfOk4Flu9qlm3IQVGqOQg8UB2Mnw3arYsbGuykfLSyH2eUChfWfGzIO1tyaF_cuCzJRS0Qjqqf_yW2SZF-4YTSGlavjOgnS/w400-h208/angular-accel.png" width="400" /></a></div>
<p>Worth noting is the acceleration and velocity result. Because the robot
accelerates instantaneously, they are very spiky. This is unrealistic motion;
real robots don’t immediately stair-step from zero velocity to some high
velocity; they have to overcome inertia and static friction (and similarly,
once moving, inertia will keep them moving and motors have to fight that to stop
the machine). So our simulation isn’t particularly realistic.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8n4Byvt7Y4KrCNLkaGzDJiOl8uC6CgOpw60lSYFrz7GD-Fo1CcQ1Dzwq4F9eLH3rD7WaXAqs1WMD8bX1CVNx2vkMvfnBCSejn0CnHMbR6VmMpN5xMOt6VFEuFWU9rHrcByHkDEPmJ09HWI2knpFXBuqOlk5IE5VgrifAXmSREGkPjCCy4RXFTnG_l/s877/lazy-turn.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Graph of lazy turn position indciators" border="0" data-original-height="872" data-original-width="877" height="398" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8n4Byvt7Y4KrCNLkaGzDJiOl8uC6CgOpw60lSYFrz7GD-Fo1CcQ1Dzwq4F9eLH3rD7WaXAqs1WMD8bX1CVNx2vkMvfnBCSejn0CnHMbR6VmMpN5xMOt6VFEuFWU9rHrcByHkDEPmJ09HWI2knpFXBuqOlk5IE5VgrifAXmSREGkPjCCy4RXFTnG_l/w400-h398/lazy-turn.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjk50GcqvLm3mws9SIkWz68Emm-ADUfNmEqM1lmC-p0qao_1RPTg2B8zY8bfcNKqmDJ_NZsLLI2rLgbX82l57rk5cfbUx09H8aPR2C5TX7j694hkiNjHqZaZeHaLy1rh8lDnBUELjBk4M5dhEHei5PNcWuXH5MMR-az08n2Sf4wnOh8syjfSI7m8t1C/s867/lazy-turn-number.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Final lazy turn numbers" border="0" data-original-height="867" data-original-width="861" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjk50GcqvLm3mws9SIkWz68Emm-ADUfNmEqM1lmC-p0qao_1RPTg2B8zY8bfcNKqmDJ_NZsLLI2rLgbX82l57rk5cfbUx09H8aPR2C5TX7j694hkiNjHqZaZeHaLy1rh8lDnBUELjBk4M5dhEHei5PNcWuXH5MMR-az08n2Sf4wnOh8syjfSI7m8t1C/w398-h400/lazy-turn-number.png" width="398" /></a></div>
<p>Finally, we take a look at our lazy arc, which drives with the right motor full
power and the left half power until we pass 72 degrees. The robot does trace a
little arc, and we find our final position to be about a quarter-meter out in
the x and y position with a heading of 76 degrees, closer to the 72-degree
target than in raw turn (this makes sense; the rate of turn is lower on the
lazy-turn mode).</p>
<h1 id="analysis">Analysis</h1>
<p>While this is good enough for practicing, it does not reflect how an actual robot
behaves. For one thing, the motors cut on and off immediately, which doesn’t
really reflect how motors work in the real world (see the discussion of the
<a href="https://fixermark.blogspot.com/2022/04/simulating-flywheel.html">flywheel</a> for
details). For another, the robot’s path through space looks like a bunch of
small line segments, since the facing is only allowed to update between steps
and the robot always moves straight along its current facing. If the steps are
small enough, this might be fine, but it doesn’t reflect well the curves that
the robot actually travels in space.</p>
<p>Next week’s simulation approach will try to follow those curves.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-70316012604016650332022-04-25T06:00:00.001-07:002022-04-25T06:00:00.213-07:00Simulating a flywheel part 2: friction and time-step updates <p>On a previous post, I discussed creating a simulator for a simple flywheel in
the FIRST Robotics WPILib framework in Java. We last discussed the high-level
framework and simulating a motor; here, we’ll go into simulating friction and
how we put it all together.</p>
<h1 id="friction-is-deceptively-complicated">Friction is deceptively complicated</h1>
<p>On the surface, friction is extremely simple. As is taught in high school
physics, it’s the force that resists motion proportional to the normal force of
the surface pushing against gravity. Which is to say, it’s just
<code>-K<sub>d</sub> * m * g</code>, for <code>g</code> the gravitational constant (acceleration) and
<code>m</code> the mass of the object, and <code>K<sub>d</sub></code> the dynamic friction
coefficient. Since the flywheel’s axle is pushed down by gravity on its socket,
the friction force becomes a counter-torque opposing spin. So, no problem. We
calculate the counter-torque, subtract it from the motor’s forward torque, and
update every step. Punch that in and… Oh dear.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj2Qwv0sBKxL475l3b42JI7Jf7t5GkpyomD8EKmQRhxbRbWwG_VQ9Waio78kMaVxXjBsFYFDQSg7aTJZney2IQH-avdOQKX2ONgOFv1OLvrZHUM4v90QFa4vyLzSYgcJFHsqiTySm1qsAZHZPbIE45erLH-nWaDXV7ONn-r_S_szBAydbM8ad9Xbk4V/s442/oops-graph.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Graph of the flywheel velocity when motor on. When motor is off, the flywheel oscillates back and forth rapidly" border="0" data-original-height="429" data-original-width="442" height="622" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj2Qwv0sBKxL475l3b42JI7Jf7t5GkpyomD8EKmQRhxbRbWwG_VQ9Waio78kMaVxXjBsFYFDQSg7aTJZney2IQH-avdOQKX2ONgOFv1OLvrZHUM4v90QFa4vyLzSYgcJFHsqiTySm1qsAZHZPbIE45erLH-nWaDXV7ONn-r_S_szBAydbM8ad9Xbk4V/w640-h622/oops-graph.png" width="640" /></a></div><br /><p><br /></p>
<p>The problem is that friction is a continuous effect: the force opposing motion
applies until the object gets very close to zero velocity (relative to the
surface it’s rubbing against). At that point, the molecules of the two objects
“gum up” with each other and the object stops moving. But the simulator is
a discrete time-step simulation: every time step, the forces acting on the
flywheel are summed up, velocity is changed, and then it’s assumed the flywheel
velocity stays the same until the next time step. So the velocity “wiggles”
back and forth around zero without ever reaching zero and the flywheel never
actually stops.</p>
<p>There are several solutions of various levels of complexity for handling
this aspect of friction, but for our purposes we can use a fairly simple one:</p>
<ul>
<li>Calculate the new angular velocity from the torques and counter-torques.</li>
<li>Check to see if the sign of the velocity has flipped. If it has, we know the
velocity crossed through the zero boundary.</li>
<li>If we did cross zero, check to see if the driven torque exceeds the counter torque.</li>
<li>If it doesn’t, the motor seizes; set velocity to zero.</li>
</ul>
<p>Code as follows:</p>
<pre tabindex="0"><code>if (Math.signum(newAngularVelocityRps) != Math.signum(flywheelAngularVelocityRps)
&& flywheelAngularVelocityRps != 0
&& newAngularVelocityRps != 0) {
// Crossed zero boundary; if torque is too low to overcome friction, motor seizes
if (Math.abs(advantagedTorque) < Math.abs(counterTorque)) {
newAngularVelocityRps = 0;
}
}
</code></pre><p>One other aspect of friction to account for: Static friction generally exceeds
dynamic friction, and applies if the flywheel is starting from zero velocity. We
account for that by selecting the right friction coefficient based on whether
the motor is moving and comparing the torque to the counter-torque from the friction;
if driven torque does not exceed counter-torque, the motor doesn’t start.</p>
<pre tabindex="0"><code>var frictionCoefficient = m_motorSpeedRpm == 0 ?
m_flywheelStaticFrictionConstant.getDouble(1.0) :
m_flywheelDynamicFrictionConstant.getDouble(1.0);
// ...
var totalTorque = advantagedTorque - counterTorque;
if (m_motorSpeedRpm == 0 && Math.abs(advantagedTorque) < Math.abs(counterTorque)) {
// stalled motor does not overcome static friction
totalTorque = 0;
}
</code></pre><h1 id="putting-it-all-together">Putting it all together</h1>
<p>Having accounted for friction, we now only need to take the total torques computed and apply them
to how the velocity and encoder count changes over time.</p>
<p>To start, the total torque changes the angular velocity. Similar to the famous <code>F = ma</code> equation,
<code>torque = moment of inertia * angular acceleration</code>. So we divide our torque by the moment of inertia
to get an acceleration, and add it (times the time delta step) to the velocity:</p>
<pre tabindex="0"><code>var angularAccelerationRadsPerSec = totalTorque / m_flywheelMomentOfInertiaKgMSquared.getDouble(1.0);
var flywheelAngularVelocityRps = Units.rotationsPerMinuteToRadiansPerSecond(m_motorSpeedRpm) / gearRatio;
// Update angular velocity, parceling the change by the delta-T
var newAngularVelocityRps = flywheelAngularVelocityRps + angularAccelerationRadsPerSec * TIMESTEP_SECS;
</code></pre><p>Then, we just need to update the velocity and position for the encoder, and update the RPM graph:</p>
<pre tabindex="0"><code>m_motorSpeedRpm = Units.radiansPerSecondToRotationsPerMinute(newAngularVelocityRps * gearRatio);
// multiply by timestep and ratio of rotations to radians to get new flywheel position
var flywheelSpeedRotationsPerSec = newAngularVelocityRps / (2 * Math.PI);
var flywheelDelta = flywheelSpeedRotationsPerSec * TIMESTEP_SECS;
// encoder values are relative to flywheel, not motor
m_encoder.setDistance(m_encoder.getDistance() + flywheelDelta);
m_encoder.setRate(newAngularVelocityRps);
m_flywheelSpeedRpm.setDouble(flywheelSpeedRotationsPerSec * 60);
</code></pre><p>And we’re done! We have a self-updating flywheel simulation that we can experiment with.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-3083086446188330952022-04-18T06:00:00.002-07:002022-04-18T13:30:23.517-07:00Simulating a Flywheel part 1: overview and simulating a motor <p>Well, the off-season of FIRST Robotics is upon us. A time when young people’s
minds turn away from robots and towards other things. Like simulating robots!</p>
<p>To give the team something to play with on the software side, I’m putting
together a couple of simulators of robot subsystems to experiment with. The
first one is a simple flywheel connected to one motor and encoder.</p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLiPc8TmFH7FdKwxVZoBDkEbh_FYfRWZvuZhw5tFDKWsjf8wlQW3ecAGkSrqHvzUfA6ORk2ksF432vvrtoozPf6M4JL3E75EFkfePI5D5Dn2oazuTVC8xov2jFTKy1xehXxbOjsxNKRr5xdWP8D16rPJlclLnh72dF6koCrYcxezp8t981wmz7wKZm/s897/interface.png" style="margin-left: auto; margin-right: auto;"><img alt="The interface for the simulator in Shuffleboard" border="0" data-original-height="897" data-original-width="714" height="640" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLiPc8TmFH7FdKwxVZoBDkEbh_FYfRWZvuZhw5tFDKWsjf8wlQW3ecAGkSrqHvzUfA6ORk2ksF432vvrtoozPf6M4JL3E75EFkfePI5D5Dn2oazuTVC8xov2jFTKy1xehXxbOjsxNKRr5xdWP8D16rPJlclLnh72dF6koCrYcxezp8t981wmz7wKZm/w510-h640/interface.png" width="510" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Interface for the simulator, showing the graph of flywheel RPM over time</td></tr></tbody></table>
<p>I’m going to break the discussion of how I did this into two parts. Here, I’ll go over
the top-level architecture of the program and how it tries to simulate, and talk about
the equations to simulate a DC electric motor. In a follow-up post, I’ll go over
the physics of friction simulation.</p>
<p>Full disclosure: I’m a rank amateur at this, putting together half-remembered
high school physics, a barely-used robotics minor, and some hazy memories of
graphs from my first years playing FIRST Robotics. Definitely room for
improvement, and I welcome feedback on this.</p>
<p>You can get the simulator from the <a href="https://github.com/fixermark/frc-component-simulators-wpilib-java/tree/main/flywheel-drive">GitHub repsitory</a>.</p>
<h1 id="overview">Overview</h1>
<h2 id="program-structure">Program structure</h2>
<p>The program is a simple <code>TimedRobot</code> operating at the default frequency of 50
times a second. It includes one automatically-running <code>Flywheel</code> subsystem that
lets a motor be turned on and off (at max power or 0 power), which is controlled
by a single joystick button.</p>
<p>In addition to <code>Flywheel</code>, a <code>SimulatedFlywheel</code> class is constructed if the
robot is operating in simulation mode. This class takes in the motor from the
<code>Flywheel</code> and a simulation wrapper around the encoder, allowing us to
explicitly set the encoder’s position and velocity values.</p>
<h2 id="the-simulation">The simulation</h2>
<p>At a high level of abstraction, the torque on a motor-driven flywheel at any
moment is simple:</p>
<div style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlO_oTVHqrYclQ54fdnMB9KNDvLVFhnMpmvcdktd8xkdEYrcUWfy_ndakQkGGllqa9nm7JDVwA6n61j1at71U4Pa7zDABuDF447zhSLouc39cj9jm5E-LRBmNcOl0WcdbhrKV00ltJAB8RMOXXooLQTGwM6L_He4zvBYCcjOdichM7dhd6B7_ElGCp/s365/total-torque-equation.png"><img alt="Total torque = motor torque - friction torque" border="0" data-original-height="17" data-original-width="365" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlO_oTVHqrYclQ54fdnMB9KNDvLVFhnMpmvcdktd8xkdEYrcUWfy_ndakQkGGllqa9nm7JDVwA6n61j1at71U4Pa7zDABuDF447zhSLouc39cj9jm5E-LRBmNcOl0WcdbhrKV00ltJAB8RMOXXooLQTGwM6L_He4zvBYCcjOdichM7dhd6B7_ElGCp/s16000/total-torque-equation.png" /></a></div><div><br /></div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjl17wTpnnJTeTNgnHLq9dhPzhTtC-74cxnX6rHhWyejAFQ371OeQDlL7Alxog6nSWfBpk1gVFTxNsB6ixhX28FqqX1Lmzyx18TJI4kbNL14Ay2LdBC9iLTLwZO1pWqK8ZoU4YL3bRZymuEuBbBHO9LAmbByy87Wd-WGgz55I-p72Z2Jr98sqUmWV9w/s326/opposed-forces.png" style="margin-left: auto; margin-right: auto;"><img alt="Spinning flywheel, with forward force and opposed force" border="0" data-original-height="278" data-original-width="326" height="341" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjl17wTpnnJTeTNgnHLq9dhPzhTtC-74cxnX6rHhWyejAFQ371OeQDlL7Alxog6nSWfBpk1gVFTxNsB6ixhX28FqqX1Lmzyx18TJI4kbNL14Ay2LdBC9iLTLwZO1pWqK8ZoU4YL3bRZymuEuBbBHO9LAmbByy87Wd-WGgz55I-p72Z2Jr98sqUmWV9w/w400-h341/opposed-forces.png" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Opposed forces acting on a spinning flywheel</td></tr></tbody></table><figure><figcaption>
</figcaption>
</figure>
<p>Once we have calculated this torque, we can update the flywheel’s angular
velocity by adding <code>total torque / moment of inertia</code> to the flywheel’s previous
velocity. Then we update the flywheel’s position as well (accounting, in both
cases, for the step of time over which this change is occurring, in our case
20ms between updates).</p>
<p>That’s the high-level approach; let’s drill down on the details.</p>
<h1 id="simulating-motors">Simulating motors</h1>
<p>A DC motor is a relatively simple system, but it has a bit of complexity making simulating one interesting.</p>
<p>The basic principle by which a DC motor operates is that half of the motor uses electromagnets, while the other
half uses permanent magnets. Varying the current through the motor’s electromagnets creates moving magnetic
fields that the permanent magnets then “chase,” turning electricity into motion. The highest torque such a motor
can give is when it’s “stalled” (i.e. not moving) and the voltage across it is maximized. At this configuration,
the motor is pushing with its “stall torque” (<em>note</em>: It’s generally a bad idea to leave a motor stalled like this;
many DC motors aren’t designed to run current through with no motion, and doing so for long periods of time can
heat up and damage components).</p>
<p>So the total torque a motor can give is equal to some constant times the current through the motor:</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2F2xTvLvIG4qCtwfQBJxKw7NRGnlcC0zMNtObYtneH0NFii-R8CBy7-st4-CScAfoKqnBuWBiJ5Mi0KTfThTP1b5ptowaLahvH5JuVPbrE0Qr95JrqYv1M6BRNYZ-u4huH0C6gRCfYSeqhZA_l_mjedLlwb-qPvN8NU9kpvfLB4iS81wugp1NPu8k/s178/motor-torque-equation.png" style="margin-left: 1em; margin-right: 1em;"><img alt="motor torque = I * k_M" border="0" data-original-height="17" data-original-width="178" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2F2xTvLvIG4qCtwfQBJxKw7NRGnlcC0zMNtObYtneH0NFii-R8CBy7-st4-CScAfoKqnBuWBiJ5Mi0KTfThTP1b5ptowaLahvH5JuVPbrE0Qr95JrqYv1M6BRNYZ-u4huH0C6gRCfYSeqhZA_l_mjedLlwb-qPvN8NU9kpvfLB4iS81wugp1NPu8k/s16000/motor-torque-equation.png" /></a></div>
<p>Where I is current through the motor and k<sub>M</sub> is a torque
constant. <em>Note</em>: equations for motor behavior are mostly sourced from
<a href="https://pages.mtu.edu/~wjendres/ProductRealization1Course/DC_Motor_Calculations.pdf">here</a>. We
can quickly calculate k<sub>M</sub> as just the stall torque divided by the
current, but for the purposes of the simulation i find it easier to just consider a relationship between
stall torque and voltage by subsituting current through <code>V = IR</code>, then considering the motor with 12 volts
across it and finding the value of a constant <code>q</code> which is resistance divided by K<sub>M</sub>:</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjB18TY_AkjIb-RW-wUet6Sz5w4VKSr6BoyEOvzl814HtjCTk06E97JM8G_H5V9NJ64l8WPtVxI9PPvAwEXxXplq-A31BSJwev6BkWyCVCb80jdeE_y1Sel8sTAKCvx_gfuKn8gFHGCAcTbQkndGJNv7z1vwbNr5ut36X9G5XmD2UOiBTA8KvrXma9r/s182/q-equation.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="q = 12 / motor stall torque" border="0" data-original-height="41" data-original-width="182" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjB18TY_AkjIb-RW-wUet6Sz5w4VKSr6BoyEOvzl814HtjCTk06E97JM8G_H5V9NJ64l8WPtVxI9PPvAwEXxXplq-A31BSJwev6BkWyCVCb80jdeE_y1Sel8sTAKCvx_gfuKn8gFHGCAcTbQkndGJNv7z1vwbNr5ut36X9G5XmD2UOiBTA8KvrXma9r/s16000/q-equation.png" /></a></div>
<p>What’s interesting about electric motors is that when I say “some loops of wire acting as electromagnets near some
permanent magnets,” I’m also describing a type of generator. And indeed, a spinning electric motor tries to induce
a current in the direction opposite the spin! This opposing force, known as “back-EMF” (EMF: electromotive force),
creates a counter-voltage to the voltage across the motor, resulting in a drop in current through the circuit.
The physics of why this happens can be a little confusing, but there’s (I think) an elegant way to remember it:
the fact that motors have back-EMF sort of falls naturally out of conservation of energy once we remember
that motors with a voltage across them don’t spin up to infinite speed. Since a motor spinning at top speed
only needs a little bit of energy to overcome resistance and maintain that speed, and since energy lost
across a component of a circuit is the power (<code>P=IV</code>), we know that when the motor is spinning near top speed,
P across the motor must be small to keep it spinning, so current and/or voltage diminished.</p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJn45HbSYdzP8D3KwtCA3Oy2TPc5aHTHORu2A2qFvna1pRXAZwIhg0r60j7iXKWsPwB9b4l8gV9QakVrYQk3FjorElStF39bJ9z4FUXa-UG-6eLd7sbVKsLVlpG4ioyUAlWcTQgZ-tPoABPAB8YDvGu04pEJAdwnSjydCH7N--_D2d3p7erK3EmtG7/s1280/generator-motor.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img alt="Diagram of an electric generator and an electric motor" border="0" data-original-height="520" data-original-width="1280" height="260" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJn45HbSYdzP8D3KwtCA3Oy2TPc5aHTHORu2A2qFvna1pRXAZwIhg0r60j7iXKWsPwB9b4l8gV9QakVrYQk3FjorElStF39bJ9z4FUXa-UG-6eLd7sbVKsLVlpG4ioyUAlWcTQgZ-tPoABPAB8YDvGu04pEJAdwnSjydCH7N--_D2d3p7erK3EmtG7/w640-h260/generator-motor.png" width="640" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">An electric motor and electric generator differ mostly in where the energy comes from. (Images sourced from Wikimedia)</td></tr></tbody></table><figure><figcaption>
</figcaption>
</figure>
<p>The back-EMF on the motor is proportional to the motor’s rotational speed and some back-EMF constant k<sub>e</sub>.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8xt2P9QjMmvyJNSYEVzW2-_17quWchFGe3yzvtwbFbmsn-LkZsQ99-Bv4Pw38AkN96cklqQjons65gzkY9R6ShEIoqPyRCbNXw2EB5GCKsNIdyPmAgljWXxLpCl_vp5MkIv3QNbB8PKj91jA8HUWMzduLVUPv71_daH7bpOok1XSyspz_19h-aXD4/s85/back-em-equation.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Diagram of an electric generator and an electric motor" border="0" data-original-height="16" data-original-width="85" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8xt2P9QjMmvyJNSYEVzW2-_17quWchFGe3yzvtwbFbmsn-LkZsQ99-Bv4Pw38AkN96cklqQjons65gzkY9R6ShEIoqPyRCbNXw2EB5GCKsNIdyPmAgljWXxLpCl_vp5MkIv3QNbB8PKj91jA8HUWMzduLVUPv71_daH7bpOok1XSyspz_19h-aXD4/s16000/back-em-equation.png" /></a></div>
<p>A motor allowed to spin up to full speed under no load and with maximum voltage across it will spin at a
“free speed” which varies from motor to motor. At free speed, the back-EMF is approximately equal to the
forward voltage, so we can set the voltage and speed in the above equation and re-arrange to solve for the back-EMF constant:</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQGNTrjH3_UPscmtFxegSmQT0yp5M3BInNpB0yS8m9WuNZSHZf3loI_yWMcs-t5ySuPjWIhIByyDZ1ZRen_QHTIEaFRE2TpdWSbRyQ0Wir1rAE45iAl5NlUEAUxh6sJvKdW0gRAsWcYULK0VwTBcC0uWs6mO-qNV13AnaOD94anmFObAZOXQBpDKik/s127/back-emf-constant-equation.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="back-EMF constant = voltage / free speed" border="0" data-original-height="41" data-original-width="127" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQGNTrjH3_UPscmtFxegSmQT0yp5M3BInNpB0yS8m9WuNZSHZf3loI_yWMcs-t5ySuPjWIhIByyDZ1ZRen_QHTIEaFRE2TpdWSbRyQ0Wir1rAE45iAl5NlUEAUxh6sJvKdW0gRAsWcYULK0VwTBcC0uWs6mO-qNV13AnaOD94anmFObAZOXQBpDKik/s16000/back-emf-constant-equation.png" /></a></div>
<p>We’re closing in on simulating the torque. The remaining ingredient is the full torque equation.</p>
<p>To approach the final torque equation, we start by considering that the total voltage across the motor is
the <code>V=IR</code> relationship through the motor plus the back-EMF value.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4CgemDyGgFsyvyFs7K1Hg4NC23EtrkqtPKa0mTKEbmp2HkMFkNbY8yxcykqUsBDv5rUVpribRDPbl3JbSR2rIPtXHLo3rnJsMbsLVkdGa6ZnI6Vi5-KuRasQkRg-ZJcJ8spT5AEdPz4Of0kj9gynH5RUYDeGFJOt5YsdmL2UVZ1lLumZsR9pAoDGS/s248/voltage-equation.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="voltage = (current * resistance) + (motor speed * back-EMF constant" border="0" data-original-height="20" data-original-width="248" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4CgemDyGgFsyvyFs7K1Hg4NC23EtrkqtPKa0mTKEbmp2HkMFkNbY8yxcykqUsBDv5rUVpribRDPbl3JbSR2rIPtXHLo3rnJsMbsLVkdGa6ZnI6Vi5-KuRasQkRg-ZJcJ8spT5AEdPz4Of0kj9gynH5RUYDeGFJOt5YsdmL2UVZ1lLumZsR9pAoDGS/s16000/voltage-equation.png" /></a></div>
<p>Substituting in the relationship between torque and current, we find</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiP51LysQDke7PjXbwjHsAa9KzMHDfDdu9B3wSNSjrsEX82XQCvVUcF1D2Aj5pqArmiUtmUEsGgzFKUGbOltX0wbSI_tP-TghDxrKcuSOftpkLcNqu-LY7-0C5i_vJrY2wyEUYOSugEmZsSxihp52XxNYtGO-i4cx7Oj8wiCEKRLCuKAQXsabMjaUXI/s303/voltage-equation-with-torque.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="voltage = (motor torque * resistance / motor torque constant) + (motor speed * back-EMF constant)" border="0" data-original-height="40" data-original-width="303" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiP51LysQDke7PjXbwjHsAa9KzMHDfDdu9B3wSNSjrsEX82XQCvVUcF1D2Aj5pqArmiUtmUEsGgzFKUGbOltX0wbSI_tP-TghDxrKcuSOftpkLcNqu-LY7-0C5i_vJrY2wyEUYOSugEmZsSxihp52XxNYtGO-i4cx7Oj8wiCEKRLCuKAQXsabMjaUXI/s16000/voltage-equation-with-torque.png" /></a></div>
<p>Now we have voltage, motor speed, and motor torque related. Doing the algebra to make torque the dependent variable and
substituting our <code>q </code>constant for resistance divided by k<sub>M</sub>, we get:</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjM245ivzvqXyZgk6QvXoPqW_5U0c79MkveGqER95GR-_jYc8LYJAjkQclF7oY7BoI8Har8up8iJjstYpHaQk_kqW69tCznmdGtngf1hmrJYuyg68X-CW7hN5B0f-oTm0ik8P9zfKwEiFxGLpUdmSQMgre2Ifj85kF70Ba-J1r7vhYqNDGUWnSpHAu4/s301/final-torque-equation.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="motor torque = motor voltage - motor speed * back-EMF constant / q" border="0" data-original-height="42" data-original-width="301" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjM245ivzvqXyZgk6QvXoPqW_5U0c79MkveGqER95GR-_jYc8LYJAjkQclF7oY7BoI8Har8up8iJjstYpHaQk_kqW69tCznmdGtngf1hmrJYuyg68X-CW7hN5B0f-oTm0ik8P9zfKwEiFxGLpUdmSQMgre2Ifj85kF70Ba-J1r7vhYqNDGUWnSpHAu4/s16000/final-torque-equation.png" /></a></div>
<p>That gives us the motor torque in almost all circumstances, save one: what happens when the voltage is set to zero?</p>
<h2 id="brake-or-coast">Brake or coast?</h2>
<p>It’s a subtle bit of architecture, but an important one: as we’ve established, a motor acts like a generator. So what
happens when the motor should be applying no power?</p>
<p>Motor controllers used in the FIRST Robotics competition are generally
configurable to “brake” or “coast” mode. In “brake” mode, the controller
short-circuits the motor to itself, allowing the back-EMF to try and drive the
motor in reverse. This will resist motion in the motor and drag it to a halt
much quicker. The mathematics of this is, honestly, something I don’t know.</p>
<p>“Coast” mode is much easier to model: in “coast” mode, the controller opens the
circuit. With no ability for electricity to flow from one end of the motor to
the other, the motor basically contributes no torque and the flywheel will spin
down normally. So we special-case the zero-voltage condition to eliminate the
motor torque, simulating “coast mode.”</p>
<h1 id="next-steps">Next steps</h1>
<p>This gives us half the flywheel simulation. Next time, we’ll talk about
the friction counter-torque (and how annoying friction subtleties are!) and
tie it all together with the acceleration and velocity-update logic.</p><br />Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-23570261929617580402022-04-11T06:30:00.004-07:002022-04-11T06:30:00.215-07:00Piping Linux audio to a file <p>I recently had to pipe some audio from my browser to a file. This may not be the most elegant way, but I found it works.</p>
<h1 id="using-simplescreenrecorder">Using SimpleScreenRecorder</h1>
<p>For capturing audio and video, I use <a href="https://www.maartenbaert.be/simplescreenrecorder/">SimpleScreenRecoder</a>. It’s a pretty no-frills recording program that is intended for (among other things) streaming content from a machine. While it supports both audio and video, I only care about the audio portion for this project.</p>
<p>To minimize bandwidth wasted recordin video, I set “Record a fixed rectangle” and set a 2x2 rectangle at 0,0. I then enabled “Record audio” with the <code>PulseAudio</code> backend and a Source of <code>Monitor of sof-hda-dsp Speaker + Headphones</code>. That output to my computer’s main speakers.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjcPc9c9yuWm7WNfs6ALG3DnkKDukCqlIBw5n7tPfMJgHEjjtb9Ac1tEg2EkW0ogzLETNAEKeFir0yUECOds7V-KDYUKRZc3JM93kbreTzhgCuEnjJCAEnNxeEx_npTKFZ_SxUpk2J7sxiHvEe3JyGuCznrLZGt8kGPJU89nVcw8NRGsBF5Xkl-MS3L=s951" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Image of the SimpleScreenRecorder UI, showing the configuration" border="0" data-original-height="551" data-original-width="951" height="370" src="https://blogger.googleusercontent.com/img/a/AVvXsEjcPc9c9yuWm7WNfs6ALG3DnkKDukCqlIBw5n7tPfMJgHEjjtb9Ac1tEg2EkW0ogzLETNAEKeFir0yUECOds7V-KDYUKRZc3JM93kbreTzhgCuEnjJCAEnNxeEx_npTKFZ_SxUpk2J7sxiHvEe3JyGuCznrLZGt8kGPJU89nVcw8NRGsBF5Xkl-MS3L=w640-h370" width="640" /></a></div>
<p>On the next screen, I set up an Output profile declaring the destination, use of MP4 as the container, an H.264 video codec with constant rate factor of 23, superfast preset, and “Allow frame skipping”, and an Audio codec of MP3, Bit-rate 128 kbit/s). </p><p>Once set up, I could just start recording.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-25015873956094481552022-04-04T06:30:00.001-07:002022-04-04T06:30:00.214-07:00Resetting my monitor in Linux <p>Recently, I had an Ubuntu laptop I use regularly develop a weird quirk: sometimes when I plugged it into USB-C, neither the monitor nor power would connect. I discovered I could fix this by simply running <code>lspci</code> on the laptop, which somehow forced both to come back.</p>
<p>As this made no sense, I dug down a bit more. I can’t say I found an answer, but I did find some more things worth questioning.</p>
<h1 id="pci-devices">PCI devices</h1>
<p>On my laptop, devices are exposed at <code>/sys/bus/pci/devices</code>. Searching around a bit, I found that device <code>/sys/bus/cpi/devices/0000:3b:00.0</code> was reporting itself as <code>USB controller: Intel Corporation JHL6540 Thunderbolt 3 USB Controller (C step) [Alpine Ridge 4C 2016] (rev 02)</code>. Navigating into that subdirectory, I found I could <code>sudo -s</code>, then <code>echo 1 > reset</code>, which fixed the system in apparently the same way as running <code>lspci</code>.</p>
<h1 id="what-the-hell">What the hell?</h1>
<p>At this point, I don’t really know. Sometimes the Linux architecture just puzzles me. I don’t know that <code>lspci</code> is running a reset on devices, but perhaps when it tries to poll details on the device, the act of polling itself trips a fault in the driver (becuase the driver realizes it can’t communicate with the device) and forces a reset to heal itself? I haven’t found any code confirming or falsifying this hypothesis; it’s still an open question for me.</p>
<h1 id="on-not-giving-up">On not giving up</h1>
<p>Owning a Linux machine is like this. When things go wrong, the only person who’s responsible for fixing it is the owner. This is, ultimately, true of all self-owned computers, of course.</p>
<p>When I was younger, the ecosystem of Linux users was too hard to find to make this task easy; certainly not as easy as it was for Windows ownership, when everyone on the block had one of those machines. But the nice thing about living in the Internet era is Linux use bloomed, and at the same time users can find each other much more easily. I learned about the existence of the PCI list poking around on various sites.</p>
<p>There’s much more to learn, but it’s nice that resources are far easier to find now.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-91219075424346391292022-03-28T06:30:00.029-07:002022-03-28T06:30:00.205-07:00Visualizing access logs from my blog server<p>One of the things I’ve missed, having moved my blog off of Blogger, is the metrics. I don’t use the metrics for much, but there’s a nonzero serotonin hit to knowing that my content is read by someone. It’d be nice to be able to restore at least that piece of the Blogger feature-set.</p>
<p>Fortunately, I have access logs and a log analyzer.</p>
<p>I’ve settled on <a href="https://goaccess.io/">goaccess</a> for my log analysis; it’s pretty straightforward, takes HTTP access logs as input, and presents the data visually (including on the command line). It’s installable on my local machine via the package manager (<code>sudo apt-get install goaccess</code>), so no problems there.</p>
<p>The steps are pretty straightforward:</p>
<ul>
<li>Get the logs</li>
<li>Dump them into goaccess</li>
</ul>
<p>The script to do that is short and sweet:</p>
<div class="highlight"><pre style="-moz-tab-size: 4; -o-tab-size: 4; background-color: #272822; color: #f8f8f2; tab-size: 4;" tabindex="0"><code class="language-shell" data-lang="shell"><span style="color: #75715e;">#!/bin/bash
</span><span style="color: #75715e;"></span>
SERVER<span style="color: #f92672;">=</span>fixermark.com
LOGPATH<span style="color: #f92672;">=</span>logs/personal-blog.fixermark.com/http
DESTINATION<span style="color: #f92672;">=</span>logfiles
mkdir -p <span style="color: #e6db74;">"</span>$DESTINATION<span style="color: #e6db74;">"</span>
scp $SERVER:$LOGPATH/access.log* $DESTINATION
pushd $DESTINATION
<span style="color: #75715e;"># this is redundant because it's a symlink on the server to the most recent logfile</span>
rm access.log.0
<span style="color: #75715e;"># gunzip will confirm replacing files</span>
yes | gunzip -f *.gz
popd
goaccess $DESTINATION/access.log*
</code></pre></div><p>I run that, and I’m presented with a nice terminal interface for viewing the logs.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEj62Je5uyOYx0htc6-iKLanwXYWsixXdpbDs1nwUaZpbLUAZO2OAA-3HLsjrp9O-t5whwC_kmeLY_xzIOcnRakwoUX5vWJhUJHZb5BDnsaOKnSLma7Fd2H5CjojV___ky18awMnqoCL4u4pmuvdF9KE6NhUgEVHHHvFvlrLu1To4TkI8BvExDL5NvQi=s1894" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="Terminal interface, showing categories for Requested Files, static requests visitor hostnames and IPs" border="0" data-original-height="805" data-original-width="1894" height="272" src="https://blogger.googleusercontent.com/img/a/AVvXsEj62Je5uyOYx0htc6-iKLanwXYWsixXdpbDs1nwUaZpbLUAZO2OAA-3HLsjrp9O-t5whwC_kmeLY_xzIOcnRakwoUX5vWJhUJHZb5BDnsaOKnSLma7Fd2H5CjojV___ky18awMnqoCL4u4pmuvdF9KE6NhUgEVHHHvFvlrLu1To4TkI8BvExDL5NvQi=w640-h272" width="640" /></a></div>
<p>This is a good start!</p>
<h1 id="filtering">Filtering</h1>
<p>goaccess doesn’t support any filtering directly, but access logs are relatively simple to filter with command-line tools, and goaccess <em>does</em> support receiving its logs from the command line. Here’s a simple script to drop the logs related to various static content pieces:</p>
<div class="highlight"><pre style="-moz-tab-size: 4; -o-tab-size: 4; background-color: #272822; color: #f8f8f2; tab-size: 4;" tabindex="0"><code class="language-shell" data-lang="shell">cat $DESTINATION/access.log* | <span style="color: #ae81ff;">\
</span><span style="color: #ae81ff;"></span> grep -v /lib/ | <span style="color: #ae81ff;">\
</span><span style="color: #ae81ff;"></span> grep -v /css
grep -v /images/ | <span style="color: #ae81ff;">\
</span><span style="color: #ae81ff;"></span> grep -v /js/ | <span style="color: #ae81ff;">\
</span><span style="color: #ae81ff;"></span> goaccess --log-format<span style="color: #f92672;">=</span>COMBINED -
</code></pre></div><p>Adding this to the fetch script, the logs are now honed in on just posts.</p>
<h1 id="to-do-next">To do next</h1>
<p>Only a couple things I'd like to improve in this flow:</p><p></p><ul style="text-align: left;"><li>scrubbing logs—after 21 days, I’d like to substitute the IP addresses with <code>0.0.0.0</code> to increase user anonymity.</li><li>Run these server-side (or have <code>goaccess</code> pull them remotely, if possible) so logs aren’t living on more machines than strictly necessary.</li></ul><p></p>Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-3738617592108723002022-03-21T01:30:00.006-07:002022-03-21T01:30:00.225-07:00Self Hosted Hugo Comments: data rendered with partials <p>In an earlier post, I added self-hosted static comments via shortcodes in Hugo. This approach had some benefits, but I didn’t like how it required modifying every blog page to support comments, even if no comments were present.</p>
<p>Hugo has a system of <a href="https://gohugo.io/templates/partials/">partials</a> and <a href="https://gohugo.io/templates/">templates</a> to allow for similar pages to have the same layout. We can take advantage of these to handle comments on every blog page. This will pull the comments out of the main flow of the blog posts; we could move them into the <a href="https://gohugo.io/content-management/front-matter/">front matter</a> of the pages, but insted I’m going to knock out another con of the previous approach and consolidate all comments into one data file.</p>
<h1 id="the-method">The method</h1>
<p>We have a few steps to go through here:</p>
<ul>
<li>Consoolidate comments into a data file</li>
<li>Build <code>comments.html</code> and <code>comment.html</code> as partials</li>
<li>Build a new <code>blogpost</code> template to use the comments partial</li>
<li>Use cascading front-matter to shift all the blog posts to the new template</li>
</ul>
<h2 id="consolidate-comments-into-a-data-file">Consolidate comments into a data file</h2>
<p>To make it easy to work with comments as a separate construct from posts, we’ll shift all of them into a new file at <code>data/comments.yaml</code>. Hugo automatically parses files in the <code>data</code> directory and makes their content available for the site builder as <code>site.data.<name of file></code>.</p>
<p>I’m using yaml because it splits the difference a bit: easy to use, but allows for multi-line strings without a lot of hassle (and it place nicely with my emacs config). here’s a snippet of the resulting yaml file.</p>
<div class="highlight"><pre style="-moz-tab-size: 4; -o-tab-size: 4; background-color: #272822; color: #f8f8f2; tab-size: 4;" tabindex="0"><code class="language-yaml" data-lang="yaml"><span style="color: #e6db74;">"/posts/2021/this-is-year-of-linux-on-desktop/"</span>:
- <span style="color: #f92672;">id</span>: <span style="color: #ae81ff;">1</span>
<span style="color: #f92672;">username</span>: <span style="color: #e6db74;">"Anon 1"</span>
<span style="color: #f92672;">date</span>: <span style="color: #e6db74;">2021-10-21T16:54:40.122Z</span>
<span style="color: #f92672;">comment</span>: <span style="color: #ae81ff;">You have working audio on your GNU/Linux laptop? Must be nice.</span>
<span style="color: #f92672;">replies</span>:
- <span style="color: #f92672;">id</span>: <span style="color: #ae81ff;">2</span>
<span style="color: #f92672;">username</span>: <span style="color: #ae81ff;">Mark T. Tomczak</span>
<span style="color: #f92672;">date</span>: <span style="color: #e6db74;">2021-10-21T17:56:00.084Z</span>
<span style="color: #f92672;">comment</span>: <span style="color: #ae81ff;">I used to, but I changed my window manager and now I'm not so sure. :-p</span>
<span style="color: #e6db74;">"/posts/2021/marks-gallery-of-facebook-infractions-3/"</span>:
- <span style="color: #f92672;">id</span>: <span style="color: #ae81ff;">1</span>
<span style="color: #f92672;">username</span>: <span style="color: #ae81ff;">Anon-2</span>
<span style="color: #f92672;">date</span>: <span style="color: #e6db74;">2021-06-14T15:34:10.877Z</span>
<span style="color: #f92672;">comment</span>: |<span style="color: #e6db74;">
</span><span style="color: #e6db74;"> My vote is for "kill the filibuster." This is a failure of the algorithm to differentiate actual calls for violence from figurative language. I wonder if you could post a comment about "Killing the Lights" when discussing what you might do before a movie or bedtime.
</span><span style="color: #e6db74;">
</span><span style="color: #e6db74;"> Reminds me of when I tried to sell a dart board on FB Marketplace, and I included a photo of the darts themselves. I had my post removed for trying to sell weapons.
</span><span style="color: #e6db74;">
</span><span style="color: #e6db74;"> There is an ENTIRE CATEGORY devoted to "Darts Equipment." Oh, Zuck...</span>
</code></pre></div><p>Worth noting:</p>
<ul>
<li>The top-level object is a dictionary mapping post paths to a list of the top-level comments in the posts</li>
<li>comment IDs are unique within the post (they’re used to build URLs to email replies in)</li>
<li>We preserved the tree structure from the previous short-code solution, but since the replies are now a separate field from the comment text body, we’ll be able ot use Markdown on the comment without mangling replies.</li>
</ul>
<h2 id="build-the-commentshtml-partial">Build the comments.html partial</h2>
<p>The partial at <code>layouts/partials/comments.html</code> finds the comments for the current page. If they exist, it stitches them in.</p>
<pre tabindex="0"><code class="language-hugo" data-lang="hugo">
{{ with site.Data.comments }}
{{ $comments := index . $.Page.RelPermalink }}
<div class="comments">
<h1 class="comments">Comments</h1>
<div class="comments-menu">
<ul>
<li>
<a href="mailto:blog+personal-comment@fixermark.com?body=Your Name:%0d%0aIcon:%0d%0aComment:&subject=Comment on {{ $.Page.Permalink }}">
Add comment
</a>
</li>
<li>
<a href="/how-to-comment">How to comment</a>
</li>
</ul>
</div>
<div class="comments">
{{ with $comments }}
{{ $sorted := sort $comments "date" "desc"}}
{{ range $sorted }}
{{ partial "comment.html" (dict "comment" . "permalink" $.Page.Permalink) }}
{{ end }}
{{ else }}
<div class="no-comments"><i>This article has no comments</i></div>
{{ end }}
</div>
</div>
{{ end }}
</code></pre><p>Once we fetch the list of comments, we check for any comment list with a key matching this page. If we find any, we sort them by date and render them (<code>{{ range $sorted}}</code>). This partial also renders a header for the comments section and a link to add a comment to the post.</p>
<p>Partials receive only the state given to them by their invoking template. When we render individual comments with the <code>comment.html</code> partial, we only give it the two pieces of information it needs (in the form of a new dictionary): the comment data as <code>comment</code> and the link to this page as <code>permalink</code>. The link is used to build replies to comments.</p>
<h2 id="build-the-commenthtml-partial">Build the comment.html partial</h2>
<p>Rendering individual comments is delegated to a second partial.</p>
<pre tabindex="0"><code class="language-hugo" data-lang="hugo"><div class="comment">
<div class="user-info">
{{ if .comment.usericon }}
<img src="{{.usericon}}">
{{ end }}
<span class="username">{{ .comment.username }}</span> <span class="post-time">{{ time.Format "2006-01-02 15:04 Z" .comment.date }}</span>
</div>
<div class="comments-menu"><a href="mailto:blog+personal-comment@fixermark.com?body=Your Name:%0d%0aIcon:%0d%0aComment:&subject=Comment on {{ .permalink }}?comment-id={{ .comment.id }}">Reply</a></div>
<div class="comment-body">
{{ .comment.comment | markdownify }}
</div>
{{ with .comment.replies }}
<div class="replies">
{{ $sorted := sort . "date" "desc" }}
{{ range $sorted }}
{{ partial "comment.html" (dict "comment" . "permalink" $.permalink) }}
{{ end }}
</div>
{{ end }}
</div>
</code></pre><p>We pretty up the time representation of the comment using <code>time.Format</code> and run the body of the comment through <code>markdownify</code> to convert any special characters. We also add a reply link taking advantage of the comment ID.</p>
<p>Note that to render replies to this comment, this partial re-invokes itself passing the reply as the <code>comment</code>. This sort of recursion is fine in Hugo as long as it’s not infinite (the nature of the tree data structure this function is running on makes such infinite recursion impossible).</p>
<h2 id="build-a-new-blogpost-template-to-use-the-comments-partial">Build a new <code>blogpost</code> template to use the comments partial</h2>
<p>Hugo allows pages to specify their type, which determines which of several templates Hugo will use to render the content of the page. Now that we have comments, I copied the <code>single.html</code> template from the theme I’m using into <code>layouts/blogpost/single.md</code> and replaced its invocation of a comment partial with my own:</p>
<pre tabindex="0"><code class="language-hugo" data-lang="hugo">{{ partial "comments.html" . }}
</code></pre><p>At this top level, I give the partial everything the template has for convenience.</p>
<h2 id="use-cascading-front-matter-to-shift-all-the-blog-posts-to-the-new-template">Use cascading front-matter to shift all the blog posts to the new template</h2>
<p>Hugo uses a speciall-named <code>_index</code> file to allow for application of front-matter to every page in a subdirectory of the site. Using that, it’s straightforward to shift all my blog posts to the new tepmlate. I add the file <code>content/posts/index.md</code>:</p>
<pre tabindex="0"><code class="language-hugo" data-lang="hugo">---
cascade:
type: blogpost
---
</code></pre><p>Now, every page under <code>posts/</code> has its <code>type</code> set by default.</p>
<p>Putting that all together (and adding a bit of CSS to clean the formatting), we now have comments on every page <em>without</em> changes to every page. Very happy with the result!</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgkokKW27hm3zQ8pNGlZDR-JVceI7pU5Dn19QMdpYoN0jHpo4YD5lZu3txy2j8LhSc2wNlhDbBsJnqEb2dk2eJ6akFYHNgVH63dpzRiIgNGIQ9VUzW7LBlJD6_sVGk49LVxjcHyoo0zdrgUynWizmo_NBi3fqNHV60rILmsxLuCAdSa_jrJtnAd97Ni=s771" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="A couple of comments underneath an article" border="0" data-original-height="277" data-original-width="771" src="https://blogger.googleusercontent.com/img/a/AVvXsEgkokKW27hm3zQ8pNGlZDR-JVceI7pU5Dn19QMdpYoN0jHpo4YD5lZu3txy2j8LhSc2wNlhDbBsJnqEb2dk2eJ6akFYHNgVH63dpzRiIgNGIQ9VUzW7LBlJD6_sVGk49LVxjcHyoo0zdrgUynWizmo_NBi3fqNHV60rILmsxLuCAdSa_jrJtnAd97Ni=s16000" /></a></div><br /><p><br /></p>
<h1 id="pros-and-cons">Pros and cons</h1>
<h2 id="pros">Pros</h2>
<dl>
<dt>Thread flow still clear</dt>
<dd>Replies are nested under their comments. I’m glad I didn’t have to lose this from the inline solution</dd>
<dt>Comment text is just mark<em>down</em></dt>
<dd>I’m much happier with markdown as the comment body text than markup; easier to read, and modestly harder for end-users to find a way to accidentally break the whole sight flow</dd>
</dl>
<h2 id="cons">Cons</h2>
<dl>
<dt>Comments no longer live on their articles</dt>
<dd>I’m considering this a pro in the overall assessment. It’d be nice if comments lived right next to their articles, but with comments consolidated in one file it’s much easier to manage them as an entity (including scrubbing one if a user asks to have it removed; I only have to purge one file through all archives).</dd>
</dl>
<h1 id="final-thoughts">Final thoughts</h1>
<p>I’m really finding Hugo very straightforward to use. It’s nice to have my toolchain more tightly integrated and this level of control over both content and presentation.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-61481203530071841652022-03-14T01:30:00.008-07:002022-03-14T01:30:00.205-07:00Self Hosted Hugo Comments: embedded in page with shortcodes <p>Having chosen to self-host my Hugo comments as part of the static page content, there are a couple of ways to do it. In this article, I explore embedding them in the page using shortcodes.</p>
<h1 id="the-method">The method</h1>
<p>Comments in my blog are represented by two shortcodes.</p>
<h2 id="commenthtml">comment.html</h2>
<p>The first shortcode collects comment data in a semi-structured way and emits it as HTML. Here’s the whole thing.</p>
<p><code>/layouts/shortcodes/comment.html</code></p>
<pre tabindex="0"><code class="language-hugo" data-lang="hugo">{{- $postid := default (.Get 0) (.Get "id") -}}
{{- $username := default (.Get 1) (.Get "username") -}}
{{- $usericon := default (.Get 2) (.Get "usericon") -}}
{{- $postdate := default (.Get 3) (.Get "date") -}}
<div class="comment">
<div class="user-info">
{{ if $usericon }}
<img src="{{$usericon}}">
{{ end }}
{{ $username }} {{ $postdate }}
</div>
<div class="comments-menu"><a href="mailto:blog+personal-comment@fixermark.com?body=Your Name:%0d%0aIcon:%0d%0aComment:&subject=Comment on {{ $.Page.Permalink }}?comment-id={{ $postid }}">Reply</a></div>
<div class="comment-body">
{{ .Inner }}
</div>
</div>
</code></pre><p>This code is triggered as, for example,</p>
<pre tabindex="0"><code class="language-hugo" data-lang="hugo">\{\{< comment id="100" username="Phil P" date="2021-10-21T16:54:40.122Z" >}}
You have working audio on your GNU/Linux laptop? Must be nice.
\{\{< comment id="101" username="Mark T. Tomczak" date="2021-10-21T17:56:00.084Z" >}}
I used to, but I changed my window manager and now I'm not so sure. :-p
\{\{< / comment >}}
\{\{< / comment >}}
</code></pre><p>Note that the approach supports nesting; a comment emitted into the <code>Inner</code> material of another comment is just copied along with the other content, so we just roll the comment tree up as we go.</p>
<p>Also worth noting in the definition of the shortcode is the <code>mailto</code> link. The link is constructed such that it auto-populates the body and the subject for easy adherence to the commenting policy; uesrs get started with a template email that will get me the right information to add their comment. Right now, this process is manual, but I’ve attempted to wire it up so it can be easily automated in the future.</p>
<h2 id="commentshtml">comments.html</h2>
<p>A wrapper shortcode serves as an envelope for all the comments on the page and provides some placeholder text if there are no comments.</p>
<p><code>/layouts/shortcodes/comments.html</code></p>
<pre tabindex="0"><code class="language-hugo" data-lang="hugo"><div class="comments">
<h1 class="comments">Comments</h1>
<div class="comments-menu">
<ul>
<li>
<a href="mailto:blog+personal-comment@fixermark.com?body=Your Name:%0d%0aIcon:%0d%0aComment:&subject=Comment on \{\{ $.Page.Permalink }}">
Add comment
</a>
</li>
<li>
<a href="/how-to-comment">How to comment</a>
</li>
</ul>
</div>
\{\{ if (not .Inner) }}
<i>This article has no comments</i>
\{\{ else }}
\{\{ .Inner }}
\{\{ end }}
</div>
</code></pre><p>This code injects the HTML to show a <code>Comments</code> section in the page; it constructs a <code>mailto</code> link like the Reply button does (exercise for the reader: both of those links can stand to be further consolidated into their own shortcode, since they’re so similar). It also provides a simple placeholder text if there are not yet any comments.</p>
<p>Overall, I’m not at all unhappy with the result! (<em>Editor’s note:</em> I haven’t done a CSS pass on this yet, this just shows structure).</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEiRmNa_Z8CohG6IXRZjC7OVDYE-L-RVypsiXfwveErYw2sF1Zy7pgCHbiRP4pUOIjinZzZSIvqsbfTytg3qx6nJshfbOyHXm0nFYrQSgAJwXoK2kgpDOQ5uak7TP3uh1IXXONIU_GlJwEU536ugGyk1ZeTImV7Wqw_qodOqpAIEtNnoyp2z2KUv_h1o=s520" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="A couple of comments embedded in the blog post" border="0" data-original-height="225" data-original-width="520" height="173" src="https://blogger.googleusercontent.com/img/a/AVvXsEiRmNa_Z8CohG6IXRZjC7OVDYE-L-RVypsiXfwveErYw2sF1Zy7pgCHbiRP4pUOIjinZzZSIvqsbfTytg3qx6nJshfbOyHXm0nFYrQSgAJwXoK2kgpDOQ5uak7TP3uh1IXXONIU_GlJwEU536ugGyk1ZeTImV7Wqw_qodOqpAIEtNnoyp2z2KUv_h1o=w400-h173" width="400" /></a></div><h1 id="pros-and-cons">Pros and cons</h1>
<p>Overall, I’m not unhappy with this approach, but it has some tradeoffs.</p>
<h2 id="pros">Pros</h2>
<dl>
<dt>Thread flow is very clear</dt>
<dd>The fact replies are nested means it’s easy to see the flow of the conversation by reading the markdown itself.</dd>
<dt>Comment text is just markup</dt>
<dd>The comment text is just inline HTML, so relatively easy-to-read. It also supports everything I could possibly want to support (abuse of this—after all, it’s user-supplied content directly injected into the page—is moderated by the fact that the comments are hand-stitched into the page by me).</dd>
</dl>
<h2 id="cons">Cons</h2>
<dl>
<dt>Mixes content and presentation</dt>
<dd>To support this approach, we need the <code>\{\{< comments >}}</code> shortcode on every page. Even though Hugo supports template pages (archetypes), that’s a maintenance burden. Ideally, stitching comments into the page should be the job of the layout itself, while the comments would be metadata for the page.</dd>
<dt>Markup, not markdown</dt>
<dd>Because shortcodes output HTML, I can’t use Markdown for the body of the comments; trying to pass a comment with replies through the Markdown parser mangles the HTML emitted by the nested <code>\{\{< comment >}}</code> shortcodes. Markdown is easier to work with and further constrains the styling in a nice way, which I enjoy.</dd>
<dt>Lack of consolidation</dt>
<dd>There’s benefit to having comments consolidated in one place for management. For example, adding a new comment with this approach requires mapping from the Subject line of an email to the relevant URL (and comment ID). If comments were consolidated in one place, adding new comments would be a simple append operation.</dd>
</dl>
<h1 id="whats-next">What’s next?</h1>
<p>This approach is working for now, but I’m going to pursue moving the comments into page front-matter (or even a central data file that is read once and built into a scannable structure).</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-31398783839898829322022-03-07T00:30:00.012-08:002022-03-07T00:30:00.215-08:00I'm switching my personal blog to self-hosting via Hugo <div class="content" itemprop="articleBody">
<p>After putting a bit of thought into it, I’ve decided to start the process of switching my <a href="http://alsofixermark.blogspot.com">personal blog</a> to self-hosting on <a href="http://gohugo.io">Hugo</a> instead of hosting through Blogger. This blog is staying where it is, but I’ve been playing with the Hugo framework for awhile and am finding I really enjoy it. Expect to see some posts about my experiences with it in here from time-to-time.</p><div style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhym5H2XrYkOv27_rqGceAZB6_82epXWBtui6ZEhxsnlxNS59STq7iC3YKEypMcCCzOvydo8u1kO96gRPncw39QbG55v0eHG6g6bmg-F3wwd3C0Ek0Kxrvby_9RvbUkZAYTY_NGYkUkRtQ_tLKzcMQte9mZmp0tgSc3GssvXAHwYeGrlVKTmWoKZ9Zh=s681" imageanchor="1"><img alt="An image of the blog, showing new header style and list of entries" border="0" data-original-height="358" data-original-width="681" height="210" src="https://blogger.googleusercontent.com/img/a/AVvXsEhym5H2XrYkOv27_rqGceAZB6_82epXWBtui6ZEhxsnlxNS59STq7iC3YKEypMcCCzOvydo8u1kO96gRPncw39QbG55v0eHG6g6bmg-F3wwd3C0Ek0Kxrvby_9RvbUkZAYTY_NGYkUkRtQ_tLKzcMQte9mZmp0tgSc3GssvXAHwYeGrlVKTmWoKZ9Zh=w400-h210" width="400" /></a></div>
<h1 id="why-switch">Why switch?</h1>
<p>Even with the benefit of <a href="http://takeout.google.com">Google Takeout</a>, moving blog infrastructure is time-consuming. So why have I bothered? A few reasons, in no particular order:</p>
<h2 id="tooling-and-control">Tooling and control</h2>
<p>Blogger’s UI hasn’t been updated in approximately ten years. It’s an acceptable WYSIWYG editor, but the resulting under-the-hood HTML is opinionated and has some weird formatting decisions. The editor also doesn’t support many keyboard accelerators, and it’s incredibly frustrating to have to break flow typing to go push a button to change style. In practice, I’ve been side-stepping the UI completely for weeks by writing blog posts in <a href="https://daringfireball.net/projects/markdown/dingus">Markdown</a> and copying-and-pasting the resulting HMTL directly into the raw editor view in Blogger. And I’m ultimately at the mercy of Blogger’s opinion of how layout should be done; some of my images overflow the content space, and I can either shrink them or leave that as-is.</p>
<p>Hugo lets me cut out the middle-man in that process; it renders directly from Markdown to HTML and the rendering can be reconfigured. The renderer is extensible via <a href="https://gohugo.io/content-management/shortcodes/">shortcodes</a> that tap into the Go infrastructure under the hood. I’m doing most of my blogging in emacs now, and it feels great. In the future, I should be able to automate the flow of adding a post, recompiling, uploading to my server, and publishing updates.</p>
<h2 id="privacy-tracking-and-censorship">Privacy, tracking, and censorship</h2>
<p>Of all the reasons, this is the least significant one, but it bears mentioning: I think enough of my potential readers have come to care about the information-harvesting capacity of Google that I’d like to do them a solid and move off of a Google-hosted service. I’ll lose some of my analytics, and I have to support my own comments, but I think those are going to be exciting enough challenges to justify the cost. The recent <a href="https://www.wired.com/story/google-analytics-europe-austria-privacy-shield/">rulings</a> regarding Google Analytics and the GDPR seem to have some people backing towards the exits on that infrastructure anyway.</p>
<p>Google also has a bad habit (or good habit, if your goal is to combat spam and bad actors; I’m still enough of a company man that I see it their way too) of deciding you’ve violated their terms of service and blowing <em>all</em> your Google services out of the water as a result. I’ve already soft-firewalled the personal blog behind ownership by my non-primary account, but since it’s the “spicier” one, I run the risk that I’ll trip over Google’s constantly-evolving TOS some day, they’ll decide my primary and non-primary accounts are the same actor, and I’ll lose both. Moving the entire thing off to another host decreases the odds of that outcome.</p>
<h1 id="things-ill-miss">Things I’ll miss</h1>
<p>Not too many, it turns out.</p>
<h2 id="embedding-images">Embedding images</h2>
<p>One thing the WYSIWYG editor actually does do quite nicely is image embedding. The flow in Hugo isn’t as clean; I have to copy the image into a folder alongside the blog post and then reference the filename in the Markdown. That having been said, I strongly suspect I’ll be able to automate that process with a couple of emacs macros to turn it into fetching an arbitrary file, copying it into the right location, and adding the reference.</p>
<p>It turns out, embedding video is more straightforward in Hugo; there’s a shortcode for linking to YouTube (assuming I don’t just self-host the video content).</p>
<h1 id="will-the-old-content-be-going-away">Will the old content be going away?</h1>
<p>No. I don’t plan to add more posts to the personal blog, but I’ll be keeping it up so that hyperlinks don’t break. This will, in practice, fork comments, but I’ve decided I don’t care too much about that issue (comment load is low enough that it’s a non-issue).</p>
<h1 id="how-will-you-do-comments-on-the-new-blog">How will you do comments on the new blog?</h1>
<p>Great question! As a static site generator, Hugo’s engine doesn’t handle comments natively. Hugo integrates with a variety of comment engines, but after putting some thought into it and reading <a href="https://cnx.srht.site/blog/reply">what others have done</a>, I decided to just have people email me comments and I’ll embed them into the site. This doesn’t completely detach me from Google (my email service is GMail), but it gives users some confidence that I’m not dumping their information <em>directly</em> into the hopper.</p>
<h1 id="will-i-be-moving-this-blog">Will I be moving this blog?</h1>
<p>It’s possible, but I think unlikely in the near future. This one is heavier (including more images and more posts), and will take quite a bit more time. But if my experience with the personal blog goes well, we’ll see.</p>
<h1 id="where-is-the-new-blog">Where is the new blog?</h1>
<p>The new blog is at <a href="http://personal-blog.fixermark.com">http://personal-blog.fixermark.com</a>. You can also subscribe to the <a href="http://personal-blog.fixermark.com/index.xml">Atom RSS feed</a>.</p>
</div>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com6tag:blogger.com,1999:blog-3144569876865681673.post-47542397265961373032022-02-28T06:30:00.013-08:002022-02-28T06:30:00.249-08:00Animating pulsing hexagons<p>Now that we can draw some hexagons, I'd like to animate them cross-fading occasionally from one color to another. This process will involve a few steps; we need to set up a rudimentary animation framework, and from there we'll take advantage of it to control the color of specific hexes and update them as they cross-fade back and forth.</p>
<h1>The framework</h1>
<p>The animation framework is pretty simple:
* A <code>HexAnimate</code> abstract object to track animation state and update the canvas
* A <code>PulseHex</code> concrete object that cross-fades a hex from one color to another and back
* A list of currently-running animations
* An update loop
* A system for spawning new animations when the previous one is done</p>
<h2>HexAnimate</h2>
<p>A <code>HexAnimate</code> object is just an object that knows how to <code>animate</code> itself:</p>
<pre><code>public abstract animate(timestamp: number, ctx: CanvasRenderingContext2D): boolean;
</code></pre>
<p>The <code>timestamp</code> is passed in so every animating element is sync'd to the same time, <code>ctx</code> is where to render to. Finally, the whole <code>animate</code> method returns <code>true</code> if it should be scheduled again next animation frame or <code>false</code> if it is done animating and can be discarded.</p>
<h2>PulseHex</h2>
<p>The <code>PulseHex</code> is a non-generalized animation to cross-fade one hex's color from a start point to a "peak" point and back over time.</p>
<pre><code>export class PulseHex extends HexAnimate {
constructor(
private xIndex: number,
private yIndex: number,
private startTime: number,
private peakTime: number,
private endTime: number,
private startColor: Color,
private endColor: Color) {
super();
}
public animate(timestamp: number, ctx: CanvasRenderingContext2D): boolean {
if (timestamp > this.endTime) {
// Render one last time to clear all the way to the initial color
ctx.save();
HEX_TEMPLATE.offsetToHex(ctx, this.xIndex, this.yIndex);
HEX_TEMPLATE.render(ctx, colorToCode(this.startColor));
ctx.restore();
return false;
}
const color = timestamp < this.peakTime ?
interpolate(this.startColor, this.endColor, (timestamp - this.startTime) / (this.peakTime - this.startTime)) :
interpolate(this.startColor, this.endColor, (this.endTime - timestamp) / (this.endTime - this.peakTime));
let stringRep = colorToCode(color);
ctx.save();
HEX_TEMPLATE.offsetToHex(ctx, this.xIndex, this.yIndex);
HEX_TEMPLATE.render(ctx, stringRep);
ctx.restore();
return true;
}
}
</code></pre>
<p>The constructor takes in the X and Y index of what hex to animate, a start, peak, and end timestamp, and what color we should start (and end) at and the color we should reach at peak time.</p>
<p>The animation logic splits into a terminator and a continuation: the first condition checks to see if we're past end time, and if we are we draw one last hexagon at start color to reset the hex to initial conditions. If we're not, we pick a color by interpolating between start and end (if before peak time) or end and start (if after peak time). The interpolation function is a very simple <code>a + t(b-a)</code> interpolation of each of the red, green, and blue components; not the fanciest (or necessarily the best-looking from a human perception standpoint), but it's a reasonable starting point. Once color is chosen, we draw the relevant hexagon.</p>
<h2>List of currently-running animations</h2>
<pre><code>let animations: HexAnimate[] = [];
</code></pre>
<p>Nothing fancy here; literally an array of animation objects. Worth noting is that this allows more than one to be running at a time; we don't take advantage of that right now, but we will!</p>
<h2>Update loop</h2>
<pre><code>const timestamp = Date.now();
// ...
animations = animations.filter((animation) => animation.animate(timestamp, context));
updateBackground(canvas);
setTimeout(updateRender, 1000 / FPS);
</code></pre>
<p>The update loop is very simple: we ask every animation to run, and if it returns <code>false</code> we drop it out of the list of animations for next loop. Once they've run, we push the new canvas to the background and schedule ourselves to run again.</p>
<h2>A system for spawning new animations when the previous one is done</h2>
<pre><code>if (animations.length === 0) {
animations.push(new PulseHex(
Math.floor(Math.random() * (CANVAS_HEXES_ACROSS - 2)) + 1,
Math.floor(Math.random() * (CANVAS_HEXES_DOWN - 2)) + 1,
timestamp,
timestamp + 1000,
timestamp + 3000,
codeToColor(DEFAULT_HEX_COLOR)!,
{
r: Math.random() * 256,
g: Math.random() * 256,
b: Math.random() * 256,
}
));
}
</code></pre>
<p>When our animation queue is empty, we construct a new one. This approach will guarantee us exactly one running at all times. We cheat a bit on the logic of the X and Y coordinates to inset from the edges so that we don't reveal the repetition seam of the background image.</p>
<p>With all those pieces put together, I'm pretty happy with the end result!</p><div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen="" class="BLOG_video_class" height="266" src="https://www.youtube.com/embed/vcwWbm667_I" width="320" youtube-src-id="vcwWbm667_I"></iframe></div><br /><p>The full code is available <a href="https://github.com/fixermark/typescript-node-testbench/releases/tag/experiment-animating-pulsing-hexagons">on GitHub</a>.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-85298802553389591782022-02-21T06:30:00.001-08:002022-02-21T06:30:00.209-08:00XMonad: giving a window affinity to live on a given workspace<p>I've been working on a project (<a href="FIRST Robotics competition robot">http://usfirst.org</a>) that combines vscode with a simulator to run Java locally. One small annoyance is that every time the program runs, the simulator opens a new window on the same desktop, but it's intended to be run full-screen. Since I'm running XMonad, I have to manually <code>window-shift-number</code> to send the simulator off to another workspace. That's the sort of annoyance I run my own window manager to deal with.</p>
<p>So let's deal with it.</p>
<p>First, we have to discriminate the simulator window from others. No major issue there; the <code>xprop</code> command-line tool lets us get details on the window. Just run and click.</p>
<pre><code>> xprop
WM_STATE(WM_STATE):
window state: Normal
icon window: 0x0
_NET_WM_ICON_NAME(UTF8_STRING) = "Robot Simulation"
_NET_WM_NAME(UTF8_STRING) = "Robot Simulation"
WM_LOCALE_NAME(STRING) = "en_US.UTF-8"
WM_CLIENT_MACHINE(STRING) = "rambler"
WM_ICON_NAME(STRING) = "Robot Simulation"
WM_NAME(STRING) = "Robot Simulation"
XdndAware(ATOM) = BITMAP
WM_CLASS(STRING) = "Robot Simulation", "Robot Simulation"
WM_NORMAL_HINTS(WM_SIZE_HINTS):
program specified location: 0, 0
window gravity: Static
WM_HINTS(WM_HINTS):
Initial state is Normal State.
_NET_WM_PID(CARDINAL) = 14024
WM_PROTOCOLS(ATOM): protocols WM_DELETE_WINDOW, _NET_WM_PING
</code></pre>
<p>The relevant detail here is <code>WM_CLASS</code>, which is the window class and appears to be the nice, stable string <code>"Robot Simulation"</code>. Great!</p>
<p>XMonad includes <code>manageHook</code> rules that let us hook into the rules for window config. Documentation lives <a href="https://hackage.haskell.org/package/xmonad-0.17.0/docs/XMonad-ManageHook.html">here</a>; there's a lot, but the most relevant ones are that we can get the class of the window (<code>className</code>) and we can direct the window to go to a different workspace (<code>doShift</code>). The stanza is a simple piece of XMonad line noise:</p>
<pre><code>windowAffinities = className =? "Robot Simulation" --> doShift "9"
</code></pre>
<p>... then just modify the top of my <code>xmonad</code> declaration to include that logic.</p>
<pre><code>main = do
xmproc <- spawnPipe "xmobar"
xmonad $ docks defaultConfig
{ manageHook = windowAffinities <+> manageDocks <+> manageHook defaultConfig
</code></pre>
<p>(Note: if you want more than one, the <code>composeAll [ hooks ]</code> grouping will do so. Just make sure the brackets [] get on their own lines, or Haskell will complain "<code>parse error (possibly incorrect indentation or mismatched brackets)</code>".</p>
<p>Added that stanza, a quick Windows-Q to reload the XMonad rules, and the next time the simulator opened, it opened on workspace 9. Just a little less typing to do what I mean.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-44895299732692491992022-02-14T06:30:00.006-08:002022-02-14T06:30:00.221-08:00Tilted hexes with CSS<p>Now that we can render and animate hexagons, can we tilt them?</p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgyd_Ig7UMSVfo8PslazwGnEXaVABuqLXYYevhb0cnYbDFrW3v-pancv0R4XZ8eKyC8_QjDpSRKyMGyowbO-4FM97vJDKss8UQXBBGjUNcfodqSIh5txOHC66ziU_k8T6myGIh886hblHmhRh0eJmBq7dPSVein2zyZ2Y-NWz23CKGXEil3vNN3nZMq=s765" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img alt="A tesselation of pale hexagons tilted at twenty degrees" border="0" data-original-height="765" data-original-width="688" height="400" src="https://blogger.googleusercontent.com/img/a/AVvXsEgyd_Ig7UMSVfo8PslazwGnEXaVABuqLXYYevhb0cnYbDFrW3v-pancv0R4XZ8eKyC8_QjDpSRKyMGyowbO-4FM97vJDKss8UQXBBGjUNcfodqSIh5txOHC66ziU_k8T6myGIh886hblHmhRh0eJmBq7dPSVein2zyZ2Y-NWz23CKGXEil3vNN3nZMq=w360-h400" width="360" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Spoiler: yes</td></tr></tbody></table>
<p>Directly in the rendering logic, this is tricky. I'll have to add a rotation to the canvas (easy) and then find the height and width of a rectangle overlapping the canvas at which tesselation occurs so I can tile the background (much less easy). Fortunately, CSS transforms can make this much easier—not perfectly easy, but not bad.</p>
<p>The big challenge doing something like this with CSS is that CSS conflates rendering logic and layout logic: every object visible on the page is a DOM element, and every DOM element impacts the page layout. In an ideal world, I could just set a rendering rule for the background and let layout operate separately (the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/image">element</a> <code>background-image</code> type supported by Firefox gets close to allowing this). Instead, the approach is a bit more complex:</p>
<ul>
<li>Leave the background of <code>body</code> unchanged</li>
<li>As children of <code>body</code>, have two <code>div</code>s: one to serve as background container, one to hold content</li>
<li>The <code>background-container</code> div addresses the layout concern: it is set to fill 100% of the window and uses <code>overflow: clip</code> to clip off any excess content that tries to overflow the window</li>
<li>As child of <code>background-container</code>, a <code>background</code> div is set larger than the window, is set to repeat its image, has the rotation applied, and has a <code>z-index</code> set to be behind other content</li>
<li>The <code>content</code> div is set to also take up the whole window and has a <code>z-index</code> set to be in front of the background.</li>
</ul>
<p>Without the <code>background-container</code>, the background forces the layout algorithm to try and fit the entire tilted element, which introduces unwanted scrolling.</p>
<p>It's not the prettiest CSS but it gets the job done.</p>
<pre><code>.background-container {
position: absolute;
left: 0px;
top: 0px;
right: 0px;
bottom: 0px;
overflow: clip;
}
#background {
position: absolute;
left: -100%;
top: -100%;
right: -100%;
bottom: -100%;
background-repeat: repeat;
transform: rotate(-20deg);
z-index: 0;
}
.content {
position: absolute;
left: 0px;
top: 0px;
right: 0px;
bottom: 0px;
z-index: 1;
}
</code></pre>
<p>And the result is pretty satisfying!</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-20565878386339826702022-02-07T06:30:00.001-08:002022-02-07T06:30:00.240-08:00Animating a web page background<p>Having confirmed that I can dynamically generate a web page background by rendering an image to canvas (and then setting that canvas's data URL as the <code>background</code> CSS value), the next question becomes: can I animate it?</p>
<p>The cheapest way to do that is with <code>setTimeout</code>. This isn't ideal (it'll block on the UI thread), but does it work at all?</p>
<p>The answer is yes!</p><div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen="" class="BLOG_video_class" height="266" src="https://www.youtube.com/embed/QwZ-kmFK0DA" width="320" youtube-src-id="QwZ-kmFK0DA"></iframe></div><br /><div class="separator" style="clear: both; text-align: center;"><br /></div><p><img /></p>
<p>The code for the demo is relatively straightforward.</p>
<pre><code>const SEGMENT = 100 / 6;
const HEX_HEIGHT_WIDTH_RATIO = 1.1547005;
const MAX_VALUE=0xdd;
const FPS = 24;
let curTime = 0;
let currentValue = 0;
let ascending = true;
function renderBg(color: string) {
const canvas = document.createElement('canvas');
canvas.width = 100;
const HEIGHT = SEGMENT * 4 / HEX_HEIGHT_WIDTH_RATIO;
canvas.height = HEIGHT;
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
ctx.fillStyle = '#' + color;
ctx.fillRect(0,0,100,HEIGHT);
ctx.strokeStyle= "white";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, HEIGHT / 2);
ctx.lineTo(SEGMENT, HEIGHT / 2);
ctx.lineTo(2 * SEGMENT, 0);
ctx.lineTo(4 * SEGMENT, 0);
ctx.lineTo(5 * SEGMENT, HEIGHT / 2);
ctx.lineTo(100, HEIGHT / 2);
ctx.moveTo(SEGMENT, HEIGHT / 2);
ctx.lineTo(2 * SEGMENT, HEIGHT);
ctx.lineTo(4 * SEGMENT, HEIGHT);
ctx.lineTo(5 * SEGMENT, HEIGHT / 2);
ctx.stroke();
const body = document.getElementsByTagName('body').item(0);
if (!body) {
return;
}
body.style.background=`url(${canvas.toDataURL()})`;
}
function updateRender() {
let increment = (MAX_VALUE / 2.0 / FPS);
if(!ascending) {
increment = -increment;
}
currentValue += increment;
if (currentValue > MAX_VALUE || currentValue < 0) {
currentValue = Math.min(MAX_VALUE, Math.max(0, currentValue));
ascending = !ascending;
}
let stringRep = Math.floor(currentValue).toString(16);
while (stringRep.length < 2) {
stringRep = '0' + stringRep;
}
renderBg(`EEEE${stringRep}`);
setTimeout(updateRender, 1000/FPS);
}
setTimeout(updateRender, 1000/FPS);
</code></pre>
<p>The approach is pretty straigtforward: run a function 24 times a second to cycle through colors and redraw the background, then output the updated background data-URL to the body's <code>background</code> CSS property.</p>
<p>Performance isn't perfect (GPU process eats about 24% of my CPU when the tab is foreground), but the UI thread doesn't appear to be badly blocked. I'll have to test it on mobile next to see how much it harms performance.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-25721011570757395832022-01-31T19:10:00.002-08:002022-01-31T19:10:00.238-08:00Dynamic web page background with the Paint API<p>To spruce up the look of Swătch, I'm investigating animating a background for the game. This turns out to be a bit less trivial than I'd hoped; there are a couple of options, but none of them at this time are great. In this post, I'll describe some options I considered and which I settled on.</p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjDK9kFgS0QCZIlXgyRmnDncXG0tSdFLcgcFhZvFbaHNuAH6vXVxYHPLW2-AKI_JO4C3GxMT06_nI7G9OuGDj9iL7spIwTRLyezvYoet5ANyNQfMVgwXNRlJOGfCRzHL5fXwEy0MVNzlBFe1PGGYVvOXIRzHUWxFNQyd0wjkB_7vvd0ZmcsK9smSeX6=s458" style="margin-left: auto; margin-right: auto;"><img alt="A pattern of regular hexagons; egshell fill with white borders" border="0" data-original-height="306" data-original-width="458" height="268" src="https://blogger.googleusercontent.com/img/a/AVvXsEjDK9kFgS0QCZIlXgyRmnDncXG0tSdFLcgcFhZvFbaHNuAH6vXVxYHPLW2-AKI_JO4C3GxMT06_nI7G9OuGDj9iL7spIwTRLyezvYoet5ANyNQfMVgwXNRlJOGfCRzHL5fXwEy0MVNzlBFe1PGGYVvOXIRzHUWxFNQyd0wjkB_7vvd0ZmcsK9smSeX6=w400-h268" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">The bestagon</td></tr></tbody></table>
<h2>CSS Painting API</h2>
<p>A relatively new proposal is the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Painting_API">CSS Painting API</a>. This API lets you "hook" the CSS rendering pipeline via code similar to a web-worker. When CSS demands an image to render, it will delegate to your painter with a 2D render context, height and width of the space to draw, and any additional CSS properties you declare that your painter receives; you then render into the provided context (using JavaScript code) and CSS will use the result as an image.</p>
<p>This API is powerful, but hooking into it required some adjustment. To build a web worker, I had to modify my webpack configuration to emit two output files; webpack supports this via modifying the entrypoint to use <a href="https://webpack.js.org/concepts/entry-points/#object-syntax">object syntax</a> and then modifying the output rule to look like <code>[name].js</code> to template-in the object name from the entrypoints. But I then had to fight TypeScript because the Painting API isn't in the <code>dom</code> types yet; I fought with webpack trying to use .d.ts files (tricky; <a href="https://github.com/TypeStrong/ts-loader/issues/639">webpack hates this</a>) but finally gave up and wrote the paint worker in plain ol' JavaScript.</p>
<p><strong>Pros</strong>:</p>
<ul>
<li>Anything you can do in canvas rendering (kinda) you can do to the background of your object</li>
<li>Uses regular CSS, so tools like animations work</li>
</ul>
<p><strong>Cons</strong>:</p>
<ul>
<li>Fighting with TypeScript to use an API that isn't typed yet</li>
<li>Modify webpack to emit a web worker</li>
<li>(the big one) <strong>This API not supported by Firefox of Safari yet</strong></li>
</ul>
<p>Deciding I didn't want to limit my new feature to only Chrome users, I sought an alternative.</p>
<h2>toDataURL and canvas</h2>
<p>It's possible to have a bare canvas object emit its image buffer via a data URL. Once you've done that, you can set the background image of an element to that data URL, and you're all set. A bit inefficient, but it works. There's a <a href="https://filosophy.org/code/using-html5-canvas-to-make-a-generative-background/">great tutorial post here</a> describing the process.</p>
<p>Armed with this approach, it was relatively straightforward to build a small hexagon background on a 100 x (height of the hexagon) rectangle.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Supported in all modern browsers</li>
<li>Works with <code>background-repeat</code> CSS, so constructing and setting a small image automatically gives you tesselation</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Will this be fast enough to do simple animations? Not yet sure</li>
</ul>
<p>Armed with this approach, I put together a simple demo on top of my <a href="https://fixermark.blogspot.com/2022/01/setting-up-test-bench-for-typescript.html">testbench</a> to prove out the idea.</p>
<p><code>index.ts</code>:</p>
<pre><code>const SEGMENT = 100 / 6;
const HEX_HEIGHT_WIDTH_RATIO = 1.1547005;
function go() {
const canvas = document.createElement('canvas');
canvas.width = 100;
const HEIGHT = SEGMENT * 4 / HEX_HEIGHT_WIDTH_RATIO;
canvas.height = HEIGHT;
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
ctx.fillStyle = '#EEEEDD';
ctx.fillRect(0,0,100,HEIGHT);
ctx.strokeStyle= "white";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, HEIGHT / 2);
ctx.lineTo(SEGMENT, HEIGHT / 2);
ctx.lineTo(2 * SEGMENT, 0);
ctx.lineTo(4 * SEGMENT, 0);
ctx.lineTo(5 * SEGMENT, HEIGHT / 2);
ctx.lineTo(100, HEIGHT / 2);
ctx.moveTo(SEGMENT, HEIGHT / 2);
ctx.lineTo(2 * SEGMENT, HEIGHT);
ctx.lineTo(4 * SEGMENT, HEIGHT);
ctx.lineTo(5 * SEGMENT, HEIGHT / 2);
ctx.stroke();
const body = document.getElementsByTagName('body').item(0);
if (!body) {
return;
}
body.style.background=`url(${canvas.toDataURL()})`;
}
go();
</code></pre>
<p>The result generated the pattern at the top of this post; I'm not at all unhappy with it!</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-69974675355635771122022-01-24T06:30:00.002-08:002022-01-24T06:30:00.425-08:00Setting up a test bench for TypeScript compiled client code with Node.js (why are things like this?)<p>My first computer was an Apple ][c owned by my parents. In that environment, booting the machine with no disk in the drive would load a working development environment in the form of an Applesoft BASIC REPL.</p>
<p>Sometimes, when I spend an hour configuring a development environment for testing a web application, I miss those days.</p>
<p>I've uploaded a bare-bones <a href="https://github.com/fixermark/typescript-node-testbench">testbench</a> to GitHub for playing with TypeScript code compiled to run in a browser client. To do so, I set up the following dependencies:</p>
<ul>
<li>typescript</li>
<li>webpack (collects JavaScript into one file)</li>
<li>ts-loader (lets webpack drive TypeScript compilation)</li>
<li>webpack-cli (command-line tools to drive webpack)</li>
<li>webpack-dev-server (server to watch system changes)</li>
</ul>
<p>Once these were installed, I had to set up with a <code>tsconfig.json</code> file for TypeScript and a <code>webpack.config.js</code> file for webpack. It took a little while to pull all the pieces together, but the nice thing is that now they're set up and I don't have to do it again.</p>
<h2>Why are things like this?</h2>
<p>When I look back on how programming was when I started vs. now, I get nostalgic from time-to-time about how things were simpler. We just turned the computer on and coded it! What happened?</p>
<h3>More flexibility means more complexity</h3>
<p>The computer I coded on when I was six did exactly one thing at a time. Meanwhile, as I type this, I have my testbench open in another window, documentation for <code>npm</code> in a second window, my blog statistics open in a third window... Modern computers are simply more flexible, and there does exist a strong correlation between flexibility and complexity.</p>
<p>In a real sense, the art of software engineering isn't creating features, it's denying options. The most featureful computer is a machine exposing a REPL. You can do anything it can do! You just have to write the code! But we've built an entire global industry around paring down the set of possible things a computer <em>could</em> do to the subset people <em>actually want</em> to do (and in so doing, surfacing those features behind a few buttons at the cost of pushing every other feature of the machine deeper into a tree of options). An Apple ][c with no disk in it "knew" I wanted to program something in BASIC when I turned it on; a modern computer has no "idea" what the user wants.</p>
<p>Since building a tiny web server with TypeScript and Node.js is as likely to be what I want to do as checking my bank account, there is necessarily more complexity to make it happen.</p>
<h3>Embedded development isn't like desktop development</h3>
<p>Since I'm working on "just some browser stuff," it's easy to fall into the trap of thinking that what I'm doing is simple. Browsers are easy to code; pop open one text file and start writing.</p>
<p>That's true... If you want to code in HTML and JavaScript. But add TypeScript to the mix and, oops, I'm not doing "native" coding anymore. Now I need an infrastructure to deal with the impedance mismatch between the fact that the browser only understands JavaScript and HTML and the fact that the only context I'm willing to code in bare JavaScript anymore is if a child's life is on the line (and even then, it'd better be a child I like).</p>
<p>Looked at in this way, writing web comtent in TypeScript isn't even just compiled; it's more like embedded development: we are writing code in a language not directly supported by the embedding target (the browser) and we need to update the target's view of the code when it changes (recompile and put the data where the browser can see it). In that sense, writing web content with TypeScript looks a lot more like writing code against a microcontroller (albeit a fancy microcontroller run in a virtual machine on my desktop) than writing a BASIC program. Some complexity is to be expected. 100% of the dependencies I installed were to support automatic recompilation and hosting of changes to the code without having to stop and re-start a server.</p>
<h3>The real problem is discoverability</h3>
<p>At the end of the day, none of these problems would be problems if I already knew what I was doing. This testbench isn't the first one people have written (nor will it be the last). But currently, a search on GitHub for <code>typescript node testbench</code> only returns my code repository!</p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjlHKs9gKEP5G1EGsx1UjM9P4aDkq5WjAWBYumTqx-OUbaa3Ui0eFn5qbLKrCW09TMsHoUGRZD3Rj_b6Geukj25YwKrc1iMwPLYAFvfCfJea9YinRQu6w0U5-K6vWsZKWuWxT4viA-bnXnbN6n7YFfDlglatIlLv5XXTupv80H6Kc8wjAscc7ovx7Hv=s931" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img alt="A view of the GitHub search interface, showing typescript-node-testbench as the only search result for "typescript node testbench"" border="0" data-original-height="415" data-original-width="931" height="178" src="https://blogger.googleusercontent.com/img/a/AVvXsEjlHKs9gKEP5G1EGsx1UjM9P4aDkq5WjAWBYumTqx-OUbaa3Ui0eFn5qbLKrCW09TMsHoUGRZD3Rj_b6Geukj25YwKrc1iMwPLYAFvfCfJea9YinRQu6w0U5-K6vWsZKWuWxT4viA-bnXnbN6n7YFfDlglatIlLv5XXTupv80H6Kc8wjAscc7ovx7Hv=w400-h178" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">This can't be true.</td></tr></tbody></table><p>I can hope that someone sees this blog post and that the existence of my project somehow gets into StackOverflow or Google search results, but ultimately, this is the real complexity of modern programming: when you want to do something relatively simple, how do you get started? In the '80s, I could start by powering on the computer. How do I start now? <code>typescript node getting started</code> returns several more results, but none quite what I'd want (most of them add more complexity than I need; many are descriptions of how to solve the problem, not workable starter code).</p>
<p>This problem isn't trivial to solve; computing moves fast these days. Several of the technologies I'm using for this project didn't exist ten years ago. But putting starter frameworks in a place people can find them (and perhaps, even, creating a language where people can describe what they want) still feels like a problem in need of a better solution.</p>
<p>We've made incredibly flexible machines, and our ability to talk about them hasn't caught up to the inherent complexity of that flexibility.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-22116685372846432892022-01-17T06:30:00.005-08:002022-01-17T06:30:00.424-08:00I improved boardgame.io (a tiny bit)<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhSJyhrKJLh6dLCnWyu9wnjyw1T0QuQfkLm9QdrQFFtEkSBAiqg0kc3i4UeXHuajMw3nnIsDzNURsbf60584aIrZ4Q5083OyuN16kbdg1BQhNaZTSDelKshn3_HOdsLRzjOyy_P7RefKZvdSAostEF59mh3rThES4O5MA8jxlocAQ-tT_y1sNHf4LMi=s465" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="The boardgame.io logo" border="0" data-original-height="101" data-original-width="465" src="https://blogger.googleusercontent.com/img/a/AVvXsEhSJyhrKJLh6dLCnWyu9wnjyw1T0QuQfkLm9QdrQFFtEkSBAiqg0kc3i4UeXHuajMw3nnIsDzNURsbf60584aIrZ4Q5083OyuN16kbdg1BQhNaZTSDelKshn3_HOdsLRzjOyy_P7RefKZvdSAostEF59mh3rThES4O5MA8jxlocAQ-tT_y1sNHf4LMi=s16000" /></a></div><p>I've been using the <a href="https://boardgame.io/">boardgame.io</a> framework to create <a href="http://fixermark.blogspot.com/search/label/swatch">a multiplayer game</a> as of late. It's been going very well, but I did trip over a few quirks of the library. One in particular, I was able to chase down and submit a patch to tweak, which was graciously accepted by the framework maintainers! I thought I'd record just a bit of how that went.</p>
<h2>The issue</h2>
<p>The library includes a lobby management system that lets the game code host multiple simultaneous games on the same server. There is a convenient <a href="https://boardgame.io/documentation/#/api/Lobby?id=clients">Lobby</a> React component that exposes this feature to the client and lets a player choose their name and create / join games. One issue with the component is that it polls the server every two seconds to get the list of games, and this polling continues even when the player is playing a game. It's unnecessary traffic; anecdotally, having the UI open for a half hour consumed about 1MB of data.</p>
<p>I set about adjusting this. After forking the project and downloading a copy of the <a href="https://github.com/boardgameio/boardgame.io">source</a> from GitHub, I let it build and ran the unit tests to confirm I had a good development environment. One convenient thing about Node.js projects is that they're very consistent in this regard; <code>npm</code> is a fairly mature package management solution, and a well-constructed Node package describes both how to build a package for production and how to build an environment to develop it, so you generally have what you need to do testing after one <code>npm ci</code> run. The developers have also helpfully included contribution guidelines, which make it much easier to know how to develop changes to the framework.</p>
<p>The obvious place to start was the component itself, and after a bit of fishing I found the <a href="https://github.com/boardgameio/boardgame.io/blob/feb08a121b089c745e737cf4934d007e87b352e5/src/lobby/react.tsx">React component definition</a>, containing functions <code>_startRefreshInterval</code> and <code>_clearRefreshInterval</code>. Sure enough, these functions got called at component mount and unmount, but not when the component transitioned from listing games to playing a game (or from choosing a player's name to listing games; it would start polling before the list was displayed). I was able to make some tweaks and build out a unit test, and then I set about manual end-to-end testing. This proved very valuable; there were a couple corner cases in my initial attempt that manual testing caught before I pushed a bad patch up to the maintainers.</p>
<p>To make clear my intention, I filed an <a href="https://github.com/boardgameio/boardgame.io/issues/1043">issue</a> against the main project to give some context on why I was making a change. Even when I think the change is self-explanatory, I find filing an issue to be a good step when proposing a change to someone's GitHub project; it makes it clear why the change is happening and serves as a place for others to comment on the problem itself, not just the proposed fix. I like to follow the "What I did / expected / observed" pattern (where appropriate) to make clear why I think something needs to be changed.</p>
<p>As per the contribution guidelines, I pushed my changes to a new branch and created a <a href="https://github.com/boardgameio/boardgame.io/pull/1044">pull request</a> to propose the change. I expected to go a couple rounds with the maintainers on getting it just right (in particular, I didn't know how they'd feel about what I'd done with the unit tests), but to my surprise they were happy with it and accepted it on the first pass! The change is now in the latest minor revision, so hopefully others will benefit from fewer poll requests while their games are running.</p>
<p>I haven't done many pull requests on other people's GitHub projects, and this was the smoothest I've experienced. I credit part of it to a lot more practice in my day job on the flow of the process. Hopefully, this write-up can serve as a template for others to make changes they hope to see in code they use. Things that really worked in my favor here, I think:</p>
<ul>
<li>This project has a well-defined README, community standards, and contribution guidelines. Read all of those.</li>
<li>Node.js makes working on somoene else's code very, very simple. It's probably my favorite package management system nowadays, and that's really saying something; I'd pretty much written off package management as unsolvable, and <code>npm</code> is proving me wrong.</li>
<li>Full unit test coverage never hurts (nor does it hurt that the maintainer put a coverage-check infrastructure in!).</li>
<li>Detailed and followable end-to-end testing that checked on corner cases increased confidence in the change (and found some bugs). Do this. Think about corner cases.</li>
<li>If you take the time as a maintainer to make changes easy and are responsive to changes, you give submitters a positive experience and they're likely to come back!</li>
</ul>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-40181515845536376812022-01-10T06:30:00.016-08:002022-01-10T06:30:00.183-08:00Making a turn-based game: Heroku is unfairly good, yo<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEh1C6QQDTHYFk2XUkl4oSOYBlL60gMLu034Oz8AiPc28p8Q4OqxkGj5aCX9Ck3dqrWsFwBIFmV15FOqRpVaS0ABjbBRAIOAenq3VcQTRwKVOm5Js-w3LlzrTLPb1kaeJ1_WjgCfARnnP-mO7lUNgW-9Y9_ip5y0jJKrJE4kC47Mkx4nh308db7QBJjj=s454" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="The Heroku logo" border="0" data-original-height="454" data-original-width="328" height="400" src="https://blogger.googleusercontent.com/img/a/AVvXsEh1C6QQDTHYFk2XUkl4oSOYBlL60gMLu034Oz8AiPc28p8Q4OqxkGj5aCX9Ck3dqrWsFwBIFmV15FOqRpVaS0ABjbBRAIOAenq3VcQTRwKVOm5Js-w3LlzrTLPb1kaeJ1_WjgCfARnnP-mO7lUNgW-9Y9_ip5y0jJKrJE4kC47Mkx4nh308db7QBJjj=w289-h400" width="289" /></a></div><br /><p><br /></p><p>So over the course of several weeks (and <a href="http://fixermark.blogspot.com/search/label/swatch">several blog posts</a>), I've created a multiplayer turn-based color-guessing game hosted on DreamHost. I've been pretty happy with it, but there's still a couple annoying quirks in the situation:</p>
<ul>
<li>The server is HTTP only</li>
<li>Boardgame.io supports websockets, but attempting to use them fails
</li></ul>
<p>Both of these are related to my host: I have an old shared-hosting solution on DreamHost running Passenger Phusion, which makes it slightly trickier to support these features. It doesn't appear to e impossible: there is, for example, some <a href="https://discussion.dreamhost.com/t/websockets-on-shared-hosting/79014/2
">discussion</a> and a <a href="at https://github.com/benjaminkraus/testPassengerWebSockets
">code example</a> for setting up both SSL and websockets on DreamHost. But I think there's a simpler approach that I wanted to try first.</p>
<p>Friends, Heroku is unfairly good.</p>
<h2>Setting up Swătch on Heroku</h2>
<p>Heroku is a "Cloud Platform as a Service" (PaaS) that lets you very quickly host an app using one of several formats and frameworks, including NodeJS. Once configured, deploying changes is as easy as <code>git push</code> to a relevant target.</p>
<p>The boardgame.io documentation includes some <a href="https://boardgame.io/documentation/#/deployment?id=heroku">pretty clear directions</a> for setting up a Heroku instance to host a game... If you already know how Heroku works. This was my first Heroku app, so I took a few more steps to get there.</p>
<p>For starters, I set up two new apps on Heroku (<code>swatchgame-dev</code> and <code>swatchgame</code>) and arranged them in a pipeline so that I can get one-click promotion of staging candidates at <code>swatchgame-dev</code> to production at <code>swatchgame</code>. "Every project has a development environment... Some are privileged enough to have a production environment separate from the development environment." I then installed the Heroku CLI tools on my work machine (<code>sudo snap install --classic heroku</code>). That gave me the tooling to configure my git repo to work with Heroku (<code>heroku git:remote -a swatchgame-dev</code>, which set up a remote that would push to my Heroku app).</p>
<p>To prepare my app to work on Heroku, I only needed to do a couple of things:</p>
<ol>
<li>In <code>package.json</code>, clean up my <code>build</code> rule so it built both the client and the server code: <br /><code>"build": "npm run build-server && npm run build-client"</code></li>
<li>Add a declaration to <code>package.json</code> to let Heroku know what Node version is expected:<br />
<code>"engines": { "node": "12.22.7" }</code></li>
<li>Add a <code>Procfile</code> to the root of my project that told Heroku what to do to launch the project once it was installed. The file is a single line, <code>web: node build/app.js</code>, which tells Heroku to set up handling web connections by running the app.</li>
<li>Make the app read from a <code>PORT</code> environment variable to check what port to bind to. Heroku sets that variable to let Node.js apps know what port to listen on for incoming connections. This required only a few tweaks to the setup logic in the server's <code>app.ts</code> file</li>
</ol>
<p>The necessary code change was very simple:</p>
<pre><code>import 'process';
. . .
const MAYBE_PORT = Number(process.env.PORT);
const PORT = isNaN(MAYBE_PORT) ? 8000 : MAYBE_PORT;
server.run(PORT, () => {
console.log('server on');
});
</code></pre>
<p>Once all that was done, I simply had to push to Heroku. Heroku is watching for changes to only a couple of branches (<code>master</code> or <code>main</code>), so I just had to make sure my push matched the correct branch name (since I develop in a local <code>develop</code> branch):</p>
<p><code>git push heroku develop:main</code></p>
<p>I then visited <a href="https://swatchgame-dev.herokuapp.com/">https://swatchgame-dev.herokuapp.com/</a> and... It just worked! No further changes needed. Both HTTPS access and websockets are handled via Heroku's default configurations.</p>
<p>Seriously, Heroku made this ridiculously easy, and it's free to use for a project this small. I couldn't recommend anything else at this juncture.</p>
<p>The game is now playable at <a href="https://swatchgame.herokuapp.com/">https://swatchgame.herokuapp.com/</a>... Try it out!</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com3tag:blogger.com,1999:blog-3144569876865681673.post-4109269928139045612022-01-03T06:30:00.003-08:002022-01-03T09:59:28.864-08:00Making a turn-based game: Don't prematurely optimize, and different types of turns, and code available on GitHub :)<p>One of the things I wanted to add to Swătch was the ability to play different types of games. In addition to the initial game of "choose the shade of a color based on its name," I wanted to have the following:</p>
<ul>
<li>Given a shade and several options, pick the correct name</li>
<li>"Trick" mode: each player makes up a name for a displayed color shade. Then, they're shown the names provided by other players in addition to the correct name. Choose which name was the correct name (players also get points for tricking the other players into choosing their name).</li>
</ul>
<p>Supporting this atop boardgame.io proved extremely possible, with just a bit of work.</p>
<p>But first, a digression...</p>
<h2>Don't prematurely optimize!</h2>
<p>To support the "choose name based on shade" mode, I needed to extend the JSON file listing the set of all colors to include references to several nearby colors in the dataset. There are several clever approaches to this, but I chose the simplest one: for every color, find the distance to every other color and assign the N nearest as nearby colors. This is an O(n<sup>2</sup>) algorithm; pretty inefficient.</p>
<p>But since it was easy to code, I ran it anyway. On a 1,500 line input, it took about 0.1 seconds.</p>
<p>Friends, computers are fast nowadays. For heavy-lifting computation, don't burn time on being clever when you can use the sledgehammer of modern clock speeds.</p>
<h2>Different types of rounds</h2>
<p>Boardgame.io provides a fairly robust <a href="https://boardgame.io/documentation/#/turn-order">framework for distinct game turns</a>, as well as describing <a href="https://boardgame.io/documentation/#/stages">what moves are valid in different stages</a> of play. The framework also supports <a href="https://boardgame.io/documentation/#/secret-state">hidden state</a> (both global and player-specific) for each player. Originally, I constructed the game with just a single type of turn, the guess-the-shade-from-the-name turn. To support multiple different types of turns, I made the following changes:</p>
<ol>
<li>Added a <code>Round</code> interface, which described a round of play by providing the following methods:
<ul><li><code>initState</code>: Prep the public and private state for the round (set to initial, empty values)</li>
<li><code>onBegin</code>: Begin the round, choose hidden values (such as what color to guess), set up player state.</li>
<li><code>scoreRound</code>: Calculate and update scores and assign "best" (and possibly "second best") answers for later display.</li>
<li><code>buildPreviousRound</code>: Every round, upon conclusion, bundles its results into a <code>previousRound</code> data structure used to describe how the round went to the players. This is the method that turns the round state into that structure.</li>
<li><code>moves</code>: A map from the valid moves in this round to the code to execute them</li>
<li><code>getPlayerState</code>: Accessor for the per-player state for this round.</li>
<li><code>getPublicState</code>: Accessor for the public state for this round</li>
<li><code>getLastRoundState</code>: Accessor for the previous-round state for this round</li></ul></li>
<li>With the interface in place, I moved around the existing <code>Context</code> state to carry different types of context depending on the current round (and reworked the client-side code to pull from those new contexts and show a specific view based on what the current round was).</li>
<li>Now, I could refactor the existing turn logic into a <code>GuessShadeRound</code> implementation and generalize the boardgame.io-provided <code>onBegin</code> and <code>onEnd</code> methods to select a round to play (with only one option currently available) and score the round when everyone had taken a turn. At this point, the refactor could be tested.</li>
<li>I built out a <code>Rounds.ts</code> file to consolidate the separate round implementations.</li>
<li>With all of that out of the way, I could write three more Rounds:
<ul><li><code>GuessNameRound</code>, which shows a shade and asks players to select the right name for the shade.</li>
<li>A pair of sibling rounds: <code>MakeUpNameRound</code> and <code>GuessMadeUpNameRound</code>. These are special-cased: instead of the turn ending when <code>MakeUpNameRound</code> completes, it transitions directly into the <code>GuessMadeUpNameRound</code> and calls that round's <code>initState</code> to prep up the round for it.</li></ul></li>
</ol>
<p>This is all tough to follow from description alone, which is why I've made all the code available on <a href="https://github.com/fixermark/swatch">GitHub</a> to see. Feel free to run it locally and give it a try!</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-9176134950769300452021-12-27T06:30:00.008-08:002021-12-28T10:03:47.751-08:00Making a turn-based game: A game server on DreamHost<p>The next step in setting up our turn-based game is prepping up our server to do double-duty: it needs to both vend static HTML content and handle dynamic client-server traffic to run the game logic. To that end, we need to extend the initial configuration to support both.</p>
<h2>Koa server</h2>
<p>Boardgame.io integrates with the <a href="https://www.npmjs.com/package/koa">koa framework</a> to serve HTML. It's a relatively straightforward web server that supports a static server (<code>koa-static</code>); the server is even instrumented with the npm debug library and can give debug logging output if the <code>DEBUG=koa-static</code> env var is set.</p>
<p>Armed with this knowledge, I can improve the <code>server.js</code> file to vend static content.</p>
<p><code></code></p><pre><code>const frontendJsPath = path.resolve(__dirname, '..');
const frontendPublicPath = path.resolve(__dirname, '../../public');
server.app.use(async (ctx, next) => {
console.log(`requesting ${ctx.request.path}`);
await next();
});
server.app.use(serve((frontendJsPath)));
server.app.use(serve((frontendPublicPath)));
</code></pre><p></p>
<p>Now we're making progress, but the client code doesn't work because the browser can't import npm modules. It's time to bundle up the client-side code into something a regular JS browser can handle.</p>
<h2>Webpack</h2>
<p>Being already familiar with <a href="https://webpack.js.org/">webpack</a>, I chose to grab that as my weapon of choice for generating browser-compatible code. First step is to get it into the project:</p>
<p><code></code></p><pre><code>npm install --save-dev webpack
npm install --save-dev ts-loader
</code></pre><p></p>
<p>I set up the webpack config based on the <a href="https://webpack.js.org/guides/typescript">guide</a> and changed my build rule to <code>tsc && webpack</code>. My output is now <code>public/bundle.js</code> instead of build. </p>
<p>At this point, I realized I'd need two different configs for client and server code. I can do this with two <code>tsconfig.json</code> files and the <code>tsc -p</code> build option to select which one to build. With a little reorganizing of my code (client-specific in "client", server-specific in "server", and common in "game"), I can now generate two build artifacts: a server that can run in Node, and a client that the server vends to a browser that connects to it.</p>
<p>After uploading all that content (the build artifacts and the <code>node_modules</code> directory) and giving it a run, I had a successful instance of the game running from my server.</p>
<div style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEifqN2w2T7ovHVqFnzGvwZ0IpH9BximgQt_gTeIF6xdjikRHXgL7AY1z4dIH_gEasU1AVnD0tFv7AMmcgVH2_zFCuDifKZk9RHmL4xa9fDKwI-36auwJxFckyfDS2KT3vxjA59E42n013keTHxFU9bnI8W2tsSBkwXIB7GsWmW0nM1Fj6ERtaYW0Nbv=s913"><img alt="The Swatch user interface, showing a color to name" border="0" data-original-height="476" data-original-width="913" height="334" src="https://blogger.googleusercontent.com/img/a/AVvXsEifqN2w2T7ovHVqFnzGvwZ0IpH9BximgQt_gTeIF6xdjikRHXgL7AY1z4dIH_gEasU1AVnD0tFv7AMmcgVH2_zFCuDifKZk9RHmL4xa9fDKwI-36auwJxFckyfDS2KT3vxjA59E42n013keTHxFU9bnI8W2tsSBkwXIB7GsWmW0nM1Fj6ERtaYW0Nbv=w640-h334" width="640" /></a></div><br /><p><br /></p>
<p>Now to set up a lobby.</p>
<h2>Supporting a lobby</h2>
<p>Boardgame.io provides a rudimentary <a href="https://boardgame.io/documentation/#/api/Lobby">lobby server</a> that makes it easy to create and connect to games (just replace the top-level game object in your code with a Lobby client). Not quite documented is that lobby also assumes that HTML components that are class <code>hidden</code> will be hidden from user view (<code>display: none</code> css rule works fine). At this point, I also discovered that the default config rules for caching on my Dreamhost server didn't work well; every piece of GET content was served with a <code>Cache-Control: max-age=172800</code>. I added some cache-breaking to the <code>.htaccess</code> file:</p>
<p><code></code></p><pre><code>Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires 0
</code></pre><p></p>
<h2>Hiding colors from end-users</h2>
<p>One last bit of housekeeping at this stage: the color database is a JSON object that gets read into memory, but it should live only in the server memory; clients should not get a whole copy of it. To that end, I stored the colors in <code>private/colors.json</code> and used Node to fetch them (<code>require('fs')</code>, <code>require('path')</code>, dynamically load the file at boot time). I also had to get those files out of webpack's field of view so it didn't try (and fail) to bind <code>fs</code> and <code>path</code> into browser code. The following got added to <code>webpack.config.js</code>:</p>
<p><code></code></p><pre><code>resolve: {
fallback: {
'fs': false,
'path': false,
}
}
</code></pre><p></p>
<p>So in the common code, I can check if <code>fs.readFileSync</code> is defined, and if it is not I know I'm in the browser and can avoid trying to load colors.</p>
<h2>Give it a try!</h2>
<p>The game is up and available for 2 or more players at <a href="http://swatch.fixermark.com">http://swatch.fixermark.com</a>; feel free to try it and let me know what you think!</p><p>Next post, I'll talk a bit about re-architecting the game to support different kinds of rounds.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com5tag:blogger.com,1999:blog-3144569876865681673.post-15672477595039513992021-12-13T06:30:00.004-08:002021-12-13T06:30:00.187-08:00A Whirligig for a play<p>Taking a short break from talking about Boardgame.io for a project I took on this weekend.</p><p>My wife is in a play this holiday season, and one of the plot points is a kid struggling with a science fair project getting a little help from the main character. I went ahead and did some prop work for the before-and-after on the project, and I'm pretty happy with how it turned out.</p><h3 style="text-align: left;">Preparation</h3><div>Amanda sourced several STEM kits from <a href="https://amzn.to/3oKKuL4">Sntieecr</a> for raw materials so we could make the prop dynamic. They were pretty good, but they don't include any structural elements. I had a few yard stakes in the basement that I figured had enough bulk to serve. When it comes to woodworking, I'm absolutely amateur; I don't even have a jigsaw or a workbench. But I was able to jury-rig a bench with random stuff in the basement, and with some grunting with a saw I was able to segment a few yard stakes into 8" and 10" lengths.</div><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjpyamMTwIlg94lULFoa2dV8kudbSumAa6bzavPcOW76g4RlxmHB5-6W8oRq1TRJJF2wO0oUSnobQAKLLbRqSaAvXnY95jokKmRgia0L0wWTgcVgAb3RXVrzBUSeyVsU0O91DzPNng1OfhOpBbhRCszcuJHic3Ln3OKyR2lMuJGqd--dqiZ2wA6_-6R=s4032" style="margin-left: auto; margin-right: auto;"><img alt="A jury-rigged workbench: two folding chairs supporting a single shelf from an old Borders bookshelf. On the shelf, my drill kit, saw, the wood, and some clamps." border="0" data-original-height="3024" data-original-width="4032" height="300" src="https://blogger.googleusercontent.com/img/a/AVvXsEjpyamMTwIlg94lULFoa2dV8kudbSumAa6bzavPcOW76g4RlxmHB5-6W8oRq1TRJJF2wO0oUSnobQAKLLbRqSaAvXnY95jokKmRgia0L0wWTgcVgAb3RXVrzBUSeyVsU0O91DzPNng1OfhOpBbhRCszcuJHic3Ln3OKyR2lMuJGqd--dqiZ2wA6_-6R=w400-h300" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Literal basement-tier work</td></tr></tbody></table><br />I figured I could make a sufficiently-sturdy shape with a 3-4-5 right triangle and a cross span to keep the whole thing from tipping over. Yes, my understanding of mechanical stability pretty much tops out at this.<div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEi9BSvS8uAFzxKey9aIf0L1ylgPDIDUVdFsY7vH1KFHUW3R4HqgJ5JJ3TtWV1P-3H8GKf1WusgWvA-moy8pmyI9u0hglbPSlbXUG9FVe99j_Yti4QZocAtxzvXJFgaORs1hURcPTQ1MH307JAtQfH4E-C8mNXdDOyt8RyX_eexAQCcnqoSsN1u9q8S6=s807" style="margin-left: 1em; margin-right: 1em;"><img alt="Against the backdrop of a stack of three cut lawn spikes, "The plan": a rough sketch of a windmill." border="0" data-original-height="807" data-original-width="405" height="400" src="https://blogger.googleusercontent.com/img/a/AVvXsEi9BSvS8uAFzxKey9aIf0L1ylgPDIDUVdFsY7vH1KFHUW3R4HqgJ5JJ3TtWV1P-3H8GKf1WusgWvA-moy8pmyI9u0hglbPSlbXUG9FVe99j_Yti4QZocAtxzvXJFgaORs1hURcPTQ1MH307JAtQfH4E-C8mNXdDOyt8RyX_eexAQCcnqoSsN1u9q8S6=w201-h400" width="201" /></a></div><br /><div><br /></div><h3 style="text-align: left;">The work</h3><div>One thing I was concerned about was that without a jigsaw, cutting an angle into the top of the diagonal was going to be a real chore. Then I remembered that since I was using yard stakes, the original manufacturer had tapered the end for me. Thanks, industrial manufacturing!<div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgG4QPufJY2VV36UofrE22AZkdLRMKYrV2D3S1JmGLolRqbJ4NPeNKfk9G_XTPbJD7FbRQWcb9rahZ-2bPC_7O68lX3cDPBEQfJq3FP3vkekjRb1GfvTdfal0ypu0wejKUKXsoFWF_u6_0AQoayQBa3faK3QvR6J-IlP85tDrxv3uxX9bep6fBxlb-I=s4032" style="margin-left: 1em; margin-right: 1em;"><img alt="Two trusses, one including a diagonal brace, the second with the diagonal brace pulled away." border="0" data-original-height="4032" data-original-width="3024" height="400" src="https://blogger.googleusercontent.com/img/a/AVvXsEgG4QPufJY2VV36UofrE22AZkdLRMKYrV2D3S1JmGLolRqbJ4NPeNKfk9G_XTPbJD7FbRQWcb9rahZ-2bPC_7O68lX3cDPBEQfJq3FP3vkekjRb1GfvTdfal0ypu0wejKUKXsoFWF_u6_0AQoayQBa3faK3QvR6J-IlP85tDrxv3uxX9bep6fBxlb-I=w300-h400" width="300" /></a></div><br /><div><br /><div><br /></div><div>With a "before" and "after" truss finished, I was ready to put the STEM kit on them.</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgPP_fJfWdzKs6e3Q_loNxg5f1d-Q1ffICOuHOnd_s-cS8D2sqMqdv856D6jTrw36CqqBvEUiHC3F3BootjypZwErl0qBwzfDChotjUoUHqWy3kX12pMyz8OXg8YLVX94wNXJ93SQd8RwOzeg9JFtx1Jp033FWsa0MNqWFUZhskOqbgfP73gCIfZ-bi=s4032" style="margin-left: 1em; margin-right: 1em;"><img alt="On a large table, the two trusses and an open bag of small parts: motors, switches, plastic fan blades that fit to the motors, battery cases, and a large assortment of wires." border="0" data-original-height="3024" data-original-width="4032" height="300" src="https://blogger.googleusercontent.com/img/a/AVvXsEgPP_fJfWdzKs6e3Q_loNxg5f1d-Q1ffICOuHOnd_s-cS8D2sqMqdv856D6jTrw36CqqBvEUiHC3F3BootjypZwErl0qBwzfDChotjUoUHqWy3kX12pMyz8OXg8YLVX94wNXJ93SQd8RwOzeg9JFtx1Jp033FWsa0MNqWFUZhskOqbgfP73gCIfZ-bi=w400-h300" width="400" /></a></div><div><div><br /></div><div><br /></div><div>Here I hit a small snag: after affixing one of the light bulb sockets, it came apart when I screwed the light bulb in! It turns out the whole affair (socket threading, anode terminal, cathode terminal, and a paper insulator between) was held together with a single screw, and it had shipped loose; screwing the lightbulb in unscrewed te screw and dropped all the parts out of the socket. Once I figured out what had gone wrong, it was pretty straightforward to put together.</div><div><br /></div><div>The kit worked great once assembled, and I was excited to see it in action.</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><iframe allowfullscreen="" class="BLOG_video_class" height="266" src="https://www.youtube.com/embed/NDyGauVBcCA" width="320" youtube-src-id="NDyGauVBcCA"></iframe></div><br /><p style="text-align: left;">The last step was to bond the gadgets to a base board for easy transport, I was worried about a screw sunk into the stakes on the thin side splitting the stakes. So I used hot glue. A <i>lot</i> of hot glue.</p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEj5zIR09IniFnEavpJa4pVDEICuSrfbUsiGvEWEIhs8K0Pd4UZNnf6GFoZ5cTD18T0ewMOPVy7VlM_XU9A5OMY_TfsWSDAsEI-FhrUfSd84mzvcwzD1H_t7xGTlrhP1QTZIw0S9tfbiacpgzcLFjcT1U7akzQXFEkjgcbigZNEk3nwrhD4ocLPKnkYS=s4032" style="margin-left: auto; margin-right: auto;"><img alt="Close-up on the joint showing hot glue" border="0" data-original-height="3024" data-original-width="4032" height="300" src="https://blogger.googleusercontent.com/img/a/AVvXsEj5zIR09IniFnEavpJa4pVDEICuSrfbUsiGvEWEIhs8K0Pd4UZNnf6GFoZ5cTD18T0ewMOPVy7VlM_XU9A5OMY_TfsWSDAsEI-FhrUfSd84mzvcwzD1H_t7xGTlrhP1QTZIw0S9tfbiacpgzcLFjcT1U7akzQXFEkjgcbigZNEk3nwrhD4ocLPKnkYS=w400-h300" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">There are problems hot glue cannot solve.<br />Someday I'll meet one.<br /></td></tr></tbody></table><h3 style="text-align: left;">The result</h3></div></div></div><div>Overall, I think they turned out okay! I hope they're robust enough for handling during the show, but I think they'll be fun!</div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhyRjTSbD1VSVZR01792FUjritZBiuD6HZVG2NwYCzoZN3uhtgYF50pfPocdGF770pKY9FppeV5OF1d2kbSlvX54EZ4ks5Db53DTtiJdDVQHxFB2LL2CmOAQc5CK4kP8b98HHkKvfbGqOsJRHCoKJdw8aJn5He3yQmNqqDmmlWIsMuLFrsyeOocwxYq=s4032" style="margin-left: 1em; margin-right: 1em;"><img alt="The "before" whirligig, with several small parts laying around it indicating it's half-done." border="0" data-original-height="4032" data-original-width="3024" height="400" src="https://blogger.googleusercontent.com/img/a/AVvXsEhyRjTSbD1VSVZR01792FUjritZBiuD6HZVG2NwYCzoZN3uhtgYF50pfPocdGF770pKY9FppeV5OF1d2kbSlvX54EZ4ks5Db53DTtiJdDVQHxFB2LL2CmOAQc5CK4kP8b98HHkKvfbGqOsJRHCoKJdw8aJn5He3yQmNqqDmmlWIsMuLFrsyeOocwxYq=w300-h400" width="300" /></a></div><br /><div><br /></div><div style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEh_gk7vsZX8aJOEmMCMkCffElnwBdZd8E2YkrNpZJhIYXrL9bi6OeHMiEj3MfYj1vQsiT9pVwwBECbwBpBPBm3K7lxcQLNFH7pNbZWWR9ix0sPV5kP_vm5nekJk2I6IITzHdTEneGQGaQvRh9dkH52bnpujV-ycWslkb31bqXqjAZG3KsZcUAKKBctM=s4032" imageanchor="1"><img alt="The "after" whirligig, notably Christmas-themed." border="0" data-original-height="4032" data-original-width="3024" height="400" src="https://blogger.googleusercontent.com/img/a/AVvXsEh_gk7vsZX8aJOEmMCMkCffElnwBdZd8E2YkrNpZJhIYXrL9bi6OeHMiEj3MfYj1vQsiT9pVwwBECbwBpBPBm3K7lxcQLNFH7pNbZWWR9ix0sPV5kP_vm5nekJk2I6IITzHdTEneGQGaQvRh9dkH52bnpujV-ycWslkb31bqXqjAZG3KsZcUAKKBctM=w300-h400" width="300" /></a></div><br />Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-37592207146583359372021-12-06T06:30:00.006-08:002021-12-06T06:30:00.456-08:00Making a turn-based game: Getting started with boardgame.io<p>For my game, I went looking for a framework that would handle some of the boring parts and free me up to focus on the game mechanics and logic. It didn't actually take much hunting to stumble upon <a href="boardgame.io">boardgame.io</a>, which I was surprised to discover handled just about everything I wanted.</p>
<p>In this post, I'll talk a bit about my choice of engine and the steps I took to get started.</p>
<h2>Architecture</h2>
<p>The boardgame.io framework has several features I appreciate:</p>
<h3>Language and libraries</h3>
<ul>
<li>Designed to run on NodeJS, which I'm already familiar with</li>
<li>Includes React components</li>
<li>Includes TypeScript type bindings</li>
</ul>
<h3>Features</h3>
<ul>
<li>State synchronization between server and clients</li>
<li>Client-side prediction of next move on public state, to speed up the experience</li>
<li>Automated client information hiding</li>
<li>Game state machine library</li>
<li>Debugging UI</li>
<li>Integration with a couple of backing stores</li>
<li>Rudimentary multi-game server and client lobby</li>
</ul>
<p>The framework was designed with Heroku in mind, but I was able to get it working on a shared DreamHost server. For the remainder of this post, I'll go into that process (which involved a lot of dead-ends, but eventually worked!).</p>
<h2>Getting started with a boardgame.io project</h2>
<p>For this project, I set up a pretty standard Node project:</p>
<ul>
<li>installed <a href="https://github.com/nvm-sh/nvm">nvm</a> using the wget directions</li>
<li><code>nvm install 12.22.7</code> to sync with version required by DreamHost for compatibility with Passenger Phusion</li>
<li>configured the project to use <a href="https://www.digitalocean.com/community/tutorials/setting-up-a-node-project-with-typescript
">TypeScript</a> </li>
<li>Started setting up the boardgame.io <a href="https://boardgame.io/documentation/#/tutorial">tic-tac-toe tutorial</a></li>
</ul>
<p>By the end of this stage, I had the following files set up:</p>
<p>tsconfig.json
```
{
"compilerOptions": {
/* Language and Environment <em>/
"target": "es6", /</em> Set the JavaScript language version for emitted JavaScript and include compatible library declarations. <em>/
"lib": ["es2015"], /</em> Specify a set of bundled library declaration files that describe the target runtime environment. */</p>
<pre><code>/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "src", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
/* Emit */
"outDir": "build", /* Specify an output folder for all emitted files. */
/* Interop Constraints */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": false, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
</code></pre>
<p>}
}
```</p>
<p>tslint.json
</p><pre><code>
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"no-console": false
},
"rulesDirectory": []
}
</code></pre><p></p>
<p>One note about the boardgame.io tutorials: they'll mention using <code>parcel-bundler</code>, but it's deprecated for <code>parcel</code>.</p>
<p>Next on the list was to set up React.</p>
<ul>
<li><code>npm install react</code> (which gave me v17.0.2)</li>
<li><code>npm i --save-dev @types/react</code></li>
<li>add <code>"jsx": "react"</code> to <code>compilerOptions</code> in <code>tsconfig.json</code></li>
<li><code>npm install react-dom</code></li>
<li>add <code>dom</code> to <code>lib</code> in <code>compilerOptions</code></li>
<li>add <code>"include": ["./src/*"]</code> as a peer to <code>compilerOptions</code> (note: <code>src/*</code> is <em>not</em> the same and will result in failure to find modules)</li>
</ul>
<p>This gets us halfway to a working solution, but to serve from DreamHost, we'll need to have a JavaScript program that Node.JS can run and a program (likely one JS file) that can be vended to the client. Configuring that is the next step.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-73561841697927448842021-11-29T06:30:00.001-08:002021-11-29T06:30:00.236-08:00Making a turn-based game: setting up DreamHost for NodeJS<p>Recently, I've taken an idea I started prototyping on <a href="http://boargamearena.com">boardgamearena</a> and moved it to my personal server. it's been an exciting project so far, and I'm getting close to showing it off widely.</p>
<p>I'm going to do a few posts talking through the process of getting the game working. This one will go into getting DreamHost to run a Node server.</p>
<h2>Using boardgame.io with DreamHost</h2>
<p>DreamHost uses a shared <a href="https://www.phusionpassenger.com/">Phusion Passenger</a> instance to host web apps, which is compatible with NodeJS apps. Details of configuration are helpfully provided in their <a href="https://help.dreamhost.com/hc/en-us/articles/360029083351-Installing-a-custom-version-of-NVM-and-Node-js">documentation</a> and an associated <a href="https://help.dreamhost.com/hc/en-us/articles/360043547431-Node-js-example-scripts">example</a>. To start, I set up a <a href="https://help.dreamhost.com/hc/en-us/articles/215457827-How-do-I-add-a-subdomain-">new subdomain</a> and made sure to <a href="https://help.dreamhost.com/hc/en-us/articles/216635318-Enabling-Passenger-for-Node-js">enable Passenger</a> on it.</p>
<p>DreamHost expects the NodeJS app you want to serve to exist as an <code>app.js</code> file at the root of the domain directory. My first attempt was to set up one of the <a href="https://help.dreamhost.com/hc/en-us/articles/360043547431-Node-js-example-scripts">example scripts</a>, but it immediately crashed. Hand-running on port :8888 by executing <code>node app.js</code> in an SSH terminal succeeded. I followed some of the troubleshooting advice (<code>chmod 755 app.js</code>, then <code>mkdir tmp; touch tmp/restart.txt</code> to force Passenger to restart its instance of my server). No luck, so I modified the <code>.htaccess</code> file at the domain root to offer some more details:</p>
<p><code>
PassengerNodejs /home/myusername/.nvm/versions/node/v12.22.7/bin/node
PassengerFriendlyErrorPages on
</code></p>
<p>Adding <code>PassengerFriendlyErrorPages</code> helped here; turns out I was seeing</p>
<p><code>
*** ERROR ***: Cannot execute /usr/bin/node: No such file or directory (2)]
</code></p>
<p>At this point, I got a little stuck and filed a support ticket to DreamHost. It didn't take them more than an hour to respond, and the solution was straightforward: turns out I was the first person trying to use Node on this shared server, and the Passenger Phusion server itself needed a restart. Heh.</p>
<p>After the restart, I got a successful <code>Hello World</code> message. next step was to begin setting up my boardgame.io project on my local machine.</p>
<p>We'll go into that next week.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0tag:blogger.com,1999:blog-3144569876865681673.post-57975788298769684482021-11-22T06:30:00.004-08:002021-11-22T07:31:50.143-08:00When not to use classes in JavaScript<p>... all the time.</p>
<h2>Do you want to be more specific?</h2>
<p>Okay, yes, I probably should.</p>
<p>JavaScript is secretly a very simple language with some ill-thought-out features layered on top of it. The most famous one is probably the class architecture, which started as "duck-typed" (objects can have arbitrary fields assigned to them), prototype-based (an object is only a specific <em>kind</em> of object because it happens to have a previously-very-visible-but-now-hidden-the-visible-field-is-deprecated-forget-I-mentioned-it-<code>__proto__</code>-what-is-that-even "Prototype" property that adds fields beyond the ones living directly in the object), and mutable (an object can become a different type of object by calling <code>Object.setPrototypeOf(instance, newPrototype)</code>, which on modern browsers will stab your performance directly in the jibblies but will also change an object to another type of object). Instances of classes are very convenient—Who doesn't love getting an object and just calling <code>myObject.doSomeStuff(args)</code>?—and they can really help you organize your code.</p>
<p>Here's why you should use them less.</p>
<h3>They require special serialization and deserialization</h3>
<p>Since you're using JavaScript, you're probably in a web browser or talking to a web browser (<em>note</em>: if you are using JavaScript and not in those circumstances, I'm sending you a digital hug and I highly encourage you to explore the wide, beautiful world of programming languages that aren't a hacked-together shambling mass built atop a Netscape demo from 1995 with name and syntax specifically chosen to capitalize on the popularity of a totally-unrelated language to the Scheme that birthed it). This means that sooner or later, you're shipping data over the network to or from the server. If your data can be represented as 'POJOs' (plain ol' JavaScript objects), the entire process of converting the data to or from network format is a single pair of library calls in modern JavaScript runtimes, <code>JSON.stringify</code> and <code>JSON.parse</code>. You'll likely still need to validate the parsed data is the right format, since the Web is a hellworld full of active attackers (such as your own server code compiled to the wrong version), but you're 90% of the way there.</p>
<p>But if your objects are <em>class instances?</em> Oh dear. I'm sorry you did that to yourself. Don't forget to define a <code>toJSON</code> method to explicitly select what fields you want serialized, <em>and</em> keep it up-to-date as the class changes. If you don't, you'll get the object's "enumerable properties" (what those are is left as an exercise for the reader). And on the deserialization side, don't forget to specify a <code>reviver</code> function that takes pieces of your parsed JSON, pattern-matches them against the original instances, and uses the class constructor to change the object to an instance. Be careful: <code>reviver</code> is looking at sub-trees and you're at risk if some sub-trees with the same properties should be different classes; I recommend synchronizing the <code>toJSON</code> for those classes to add a 'tag' field that can be inspected to pop the POJO back into an object instance. And don't forget to synchronize all that serialization and deserialization logic with the server's representation of the data, or uh-oh!</p>
<p>... or you could do none of that, and just have the in-memory object representation track more closely to the on-wire model by not using classes.</p>
<h3>They add overhead</h3>
<p>Every instance of an object is carrying around some indication of what its prototype is. That's not a lot of data on a large object, but on a small object (such as an RGB "Color" specifier or a three-number 3D coordinate), the "what kind of thing am I" field is over 10% of the data stored in the object. And since JavaScript is so dynamic, there are very few optimizations the runtime can practically do to strip out that data. Unless your code frequently has to pass colors or 3D coordinates in the same information channel and doesn't know which (which we might consider a code smell), all those extra "Hey I'm a color here are my methods" tags are wasted space.</p>
<h3>They're a pain in the ass to unit test</h3>
<p>What you want to do in unit tests is confirm your functions manipulate state correctly. Because class use encourages state hiding, it makes it trickier to write unit tests... Do you want to test your private or protected methods? "No," says the purist, "you should test your public API." Okay, but the public API is relying internally on some couple dozen private functions, so now I'm writing big, jangly dependency-heavy tests to get around the fact that I can't just call <code>myInstance.privateMethod</code> directly and test its output. And if I'm using a mocking library, I'm now mocking stuff up in <code>MyClass.prototype</code>, and sometimes I'm working around private methods by adding a <code>makeTestable</code> public method of some kind... It's all a bit of a mess. JavaScript's class model has no escape hatches for letting test logic sneak a peek into the permission space of a class, and it's frustrating.</p>
<h3>Inheritance and <code>instanceof</code> are traps</h3>
<p>One reason people sometimes turn to classes is inheritance. The "Shape" that could be a "Circle" or "Rectangle" is the most classic example. Tricky thing is, it's also one of the more corner-case examples (are you doing GUI programming?); many inheritance hierarchies aren't nearly so clean, and JavaScript handles the classic "diamond of death" problem (where one class inherits from two other classes that inherit from the same base class) by... Not allowing multiple inheritance. Inheritance also somewhat clashes with the mutable-class "feature" of the language... You can change the type of an object by editing its prototype, but you can't edit the prototype chain to, for example, swap one instance's grandparent with some other class (if you try to do so by editing prototype relationships, congratulations... You've now mutated <em>every class in your runtime</em>).</p>
<p>When you have to dynamically determine if an unknown object is an instance of some class, you can use the built-in <code>instanceof</code> operator. This walks the prototype chain for the object to see if the specified class shows up anywhere as a parent. This works great until you get into anything complicated involving libraries and modules. Suddenly, you discover that your <code>ThreeDCoordinate</code> isn't a <code>ThreeDCoordinate</code> because it was built with <code>ThreeDLib</code> version 2.7, but you're using <a href="https://www.npmjs.com/">npm</a> and the code you're running right now is in a library you added which is depending on version 2.9 of <code>ThreeDLib</code>, and no, the 2.9 and 2.7 <code>ThreeDCoordinate</code> classes aren't the same class even though they are 100% the same code.</p>
<h2>So what should we do instead?</h2>
<p>JavaScript is actually perfectly capable of supporting a functional pattern riding atop POJO data. In this model, we rarely use classes; we just build objects by calling functions that instantiate them and manipulate those objects as "bags of data referenced by field." In fact, the language's "duck-typed" nature makes this simpler than in other languages: we don't have to be overly-cautious about type. I don't need my inputs to <code>getDistance(coord1, coord2)</code> to <em>really</em> be <code>ThreeDCoordinate</code> objects; if they have <code>x</code>,<code>y</code>, and <code>z</code> fields that are numbers, I can act on them.</p>
<p>With POJOs manipulated by functions, I don't need special handling for serialization, my objects are much smaller (and I can't modify a prototype chain so I can't incur the expense of doing a very slow operation in a modern browser), and I can get inheritance by either extending objects (taking one object and adding fields to it... Not great, because this also incurs browser overhead) or composing objects (making a new object that has a field containing the "parent" object).</p>
<p>There are some possible downsides to this approach. One is the lack of enforced discipline in only having some methods available on some objects means you'll have to be more careful with your code to keep your inputs straight (it's easier to pass the wrong object to the wrong handler function if you're not referencing the methods via myObject.method()). Another is functions divorced from the data they care about can tend to end up wordy; it's no longer <code>myCoordinate.translateX(value)</code>, it's <code>ThreeDCoordTranslateX(coord, value)</code>. The latter is actually a namespace problem, not a class problem; modern JavaScript offers some good tooling around namespacing functions (either via modules or the "poor man's module," a class full of static functions (one weird tip; <a href="https://google.github.io/styleguide/jsguide.html#es-module-exports">Google hates it</a>)).</p>
<p>One additional downside is the lack of private members. To be honest, while I find these conceptually useful I don't find I need the language itself enforcing discipline around them these days. My experience is that the question "how private is private" is wuzzier than I want it to be. The object model enforces it as "data only visible inside the methods of the class," and I find myself needing to "jail-break" that abstraction (for testing or "friend-class" reasons) too often. For functions, I can get privacy by scoping them to the module level. For data, if the API is sufficiently complex that private data matters, I put creation and maintenance of the object behind constructor and mutator functions and only change the data through those functions.</p>
<h2>A TypeScript plug</h2>
<p>It's definitely worth noting that I don't program much in plain JavaScript these days. The language enables a <em>lot</em> of shoot-your-own-leg-off opportunities regarding its near-total lack of static type enforcement.</p>
<p>To implement the approach I'm describing here, what I <em>really</em> do these days is build my types of objects as interfaces and use interface inheritance to indicate when one object can be treated as a subset of another object. I construct objects in functions declared to return a particular interface-conforming object and write functions that take in a particular interface-conforming object. The compiler will do the work at compile time to let me know if I'm trying to pass the wrong type to the wrong function. In the relatively rare cases that I'm handling multiple types of object on the same channel, I can use tag fields (or the structure of the data) and <a href="https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types">type guards</a> to turn mystery-typed objects into an understood type.</p>
<h2>The zeroth rule is there are no rules</h2>
<p>I've been hard on classes, but this walk has been a bit tongue-and-cheek. In reality, classes are an integral part of JavaScript, they aren't going anywhere, and it's okay to use them. I wanted to get us out of the mindset that they're the <strong>only</strong> way to solve problems, so I've asked you to imagine a world where we should never be using them.</p>
<p>I actually use them often, but I have some specific rules of thumb on when to use and when to avoid them:</p>
<h3>1. If you have a big type family and inheritance is cheaper than special-casing</h3>
<p>If you're dealing with a family of a dozen or more related types, where most of them share implementation but a few do have special handling needs (i.e. the traditional "shapes are circles and rectangles" problem), you may very well be better off using classes than an elaborate family of handler functions and special-case logic for switching on particular instances of the type family. In my experience, big bags of things mapping to tangible objects will fit this description.</p>
<h3>2. Don't use them to describe data on the wire</h3>
<p>It's hardly ever worth it to do the heavyweight serialization / deserialization of mapping data on the wire to class instances. If your data is going on the wire, keep it simple. Note that point 1 and point 2 come into conflict sometimes. There is no universal answer here; you'll be making tradeoffs one way or the other if you class-up your big type family that also serializes onto the wire. At least if you go that road, you can make <code>implement toJSON on every class and a reviver that understands the whole class family</code> part of the process.</p>
<h3>3. Don't use <code>instanceof</code></h3>
<p>At this point in my career, I consider <code>instanceof</code> harmful; it actively conflicts with the ability to use different versions of a library in the same codebase, and fails in silent and confusing ways. It also bakes knowledge of the class hierarchy into possibly-unrelated code. Try not to do it.</p>
Mark T. Tomczakhttp://www.blogger.com/profile/08932288233834247685noreply@blogger.com0