<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>The Ryan D. Lewis Feed</title>
  <subtitle>Posts, projects, and other content from Ryan.</subtitle>
  <link href="https://ryandlewis.dev/feed.xml" rel="self"/>
  <link href="https://ryandlewis.dev"/>
  <updated>2025-01-19T16:23:15Z</updated>
  <id>https://ryandlewis.dev</id>
  <author>
    <name>Ryan D. Lewis</name>
    <email>public+feed@ryandlewis.dev</email>
  </author>
		  <entry>
			<title>🦑 The Modular Autonomous Discovery for Science (MADSci) Framework</title>
			<link href="https://ryandlewis.dev/projects/madsci/"/>
			<updated>2024-11-01T00:00:00Z</updated>
			<id>https://ryandlewis.dev/projects/madsci/</id>
			<content type="html">&lt;p&gt;Project Link: &lt;a href=&quot;https://github.com/AD-SDL/MADSci&quot;&gt;https://github.com/AD-SDL/MADSci&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Description: &lt;em&gt;A modular, autonomous, and scalable framework for scientific discovery and experimentation.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;about-madsci&quot; tabindex=&quot;-1&quot;&gt;About MADSci&lt;/h2&gt;
&lt;p&gt;MADSci is the spiritual successor to &lt;a href=&quot;https://ryandlewis.dev/projects/wei&quot;&gt;WEI&lt;/a&gt;, and is intended to provide a modular toolkit with which to assemble self driving labs from any combination of automated instruments, resources, and functionalities the user might need.&lt;/p&gt;
&lt;p&gt;It&#39;s still very much a work in progress (we&#39;re aiming for an Alpha release in the next couple weeks&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://ryandlewis.dev/projects/madsci/#1-note&quot; id=&quot;1-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup title=&quot;As of 2025-01-19&quot;&gt;1&lt;/sup&gt;&lt;/a&gt;), but I&#39;m excited to take all the lessons we&#39;ve learned from working on WEI and apply it to MADSci.&lt;/p&gt;
  &lt;footer role=&quot;doc-endnotes&quot; class=&quot;Footnotes&quot;&gt;
    &lt;h2 id=&quot;footnotes-label&quot; class=&quot;Footnotes__title&quot;&gt;Footnotes&lt;/h2&gt;
    &lt;ol class=&quot;Footnotes__list&quot;&gt;&lt;li id=&quot;1-note&quot; class=&quot;Footnotes__list-item&quot; role=&quot;doc-endnote&quot;&gt;As of 2025-01-19 &lt;a class=&quot;Footnotes__back-link&quot; href=&quot;https://ryandlewis.dev/projects/madsci/#1-ref&quot; aria-label=&quot;Back to reference 1&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
  &lt;/footer&gt;
</content>
		  </entry>
		  <entry>
			<title>🧪 The Workflow Execution Interface (WEI)</title>
			<link href="https://ryandlewis.dev/projects/wei/"/>
			<updated>2023-09-05T00:00:00Z</updated>
			<id>https://ryandlewis.dev/projects/wei/</id>
			<content type="html">&lt;p&gt;Project Link: &lt;a href=&quot;https://github.com/AD-SDL/WEI&quot;&gt;https://github.com/AD-SDL/WEI&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Description: &lt;em&gt;The Workcell Execution Interface (WEI) for Autonomous Discovery/Self Driving Laboratories (AD/SDLs)&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;about-wei&quot; tabindex=&quot;-1&quot;&gt;About WEI&lt;/h2&gt;
&lt;p&gt;When I returned to ANL in the fall of 2023, I started full time as a roboticist and software developer in the &lt;a href=&quot;https://rpl.cels.anl.gov/&quot;&gt;Rapid Prototyping Lab&lt;/a&gt;. In the time I had been away at UMich earning my MS in Robotics, the lab had been busy, primarily working on this project: the Workflow Execution Interface, or WEI.&lt;/p&gt;
&lt;p&gt;The principal purpose of WEI is to act as an orchestrator and central planner for the various instruments we were automating as part of the Self Driving Labs effort. In the year after starting, I spent much of my time working on improving the functionality of WEI: adding better state management, dockerizing and packaging the software, adding better helpers for easily automating instruments, supporting administrative actions like pausing and safety stops, and various minor improvements.&lt;/p&gt;
&lt;p&gt;As of the beginning of 2025, we&#39;ve started working on a successor project to WEI, the &lt;a href=&quot;https://ryandlewis.dev/projects/madsci&quot;&gt;Modular Autonomous Discovery for Science (MADSci) Framework&lt;/a&gt;, to leverage everything we learned building and improving WEI while creating a more scalable and approachable toolkit for lab automation and autonomous discovery.&lt;/p&gt;
</content>
		  </entry>
		  <entry>
			<title>How to Call a Service from a ROS2 Launch File</title>
			<link href="https://ryandlewis.dev/posts/callserviceinros2launch/"/>
			<updated>2022-09-03T00:00:00Z</updated>
			<id>https://ryandlewis.dev/posts/callserviceinros2launch/</id>
			<content type="html">&lt;p&gt;This one is pretty straight forward, but took me a non-trivial amount of searching to find for myself. To call a ros2 service from a ros2 launch file, add the following to your launch file (see &lt;a href=&quot;https://docs.ros.org/en/humble/Tutorials/Intermediate/Launch/Creating-Launch-Files.html&quot;&gt;the official docs&lt;/a&gt; for more on launch files):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from launch.substitutions import FindExecutable
from launch.actions import ExecuteProcess

...

ld.add_action(
    ExecuteProcess(
        cmd=[[
            FindExecutable(name=&#39;ros2&#39;),
            &amp;quot; service call &amp;quot;,
            &amp;quot;/namespace/service_to_call &amp;quot;,
            &amp;quot;example_msgs/srv/ExampleMsg &amp;quot;,
            &#39;&amp;quot;{param_1: True, param_2: 0.0}&amp;quot;&#39;,
        ]],
        shell=True
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ld&lt;/code&gt; here is a variable containing an instance of &lt;code&gt;LaunchDescription&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/namespace/service_to_call&lt;/code&gt; is replaced with the service you&#39;re looking to call (don&#39;t forget any appropriate namespaces) and can be found with &lt;code&gt;ros2 service list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;example_msgs/srv/ExampleMsg&lt;/code&gt; is the message type used by that service, which you can get with &lt;code&gt;ros2 service info /namespace/service_to_call&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;quot;{param_1: True, param_2: 0.0}&amp;quot;&lt;/code&gt; is the dictionary defining the message data. To find the parameters you need to set, you may need to consult the &lt;code&gt;.srv&lt;/code&gt; file or documentation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don&#39;t forget to include the &lt;code&gt;shell=True&lt;/code&gt; argument, as the command will fail with a confusing &amp;quot;File not found&amp;quot; error without it.&lt;/p&gt;
</content>
		  </entry>
		  <entry>
			<title>The Bandoleers Session 027</title>
			<link href="https://ryandlewis.dev/posts/ttrpg/thebandoleers027/"/>
			<updated>2022-06-25T00:00:00Z</updated>
			<id>https://ryandlewis.dev/posts/ttrpg/thebandoleers027/</id>
			<content type="html">&lt;h2 id=&quot;dm&#39;s-log%3A-supplemental...session-027&quot; tabindex=&quot;-1&quot;&gt;DM&#39;s Log: Supplemental...Session 027&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Date: 2022-06-01&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;party%3A-the-bandoleers&quot; tabindex=&quot;-1&quot;&gt;Party: The Bandoleers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Kilomir, Minotaur Wild-Magic Barbarian&lt;/li&gt;
&lt;li&gt;Faegwyn Suithrasas, High Elf School of Evocation Wizard&lt;/li&gt;
&lt;li&gt;Ollie, Tiefling Way of the Long Death Monk&lt;/li&gt;
&lt;li&gt;Mick Nichols, Rock Gnome Assassin Rogue
&lt;ul&gt;
&lt;li&gt;Accompanied by his trusty sidekick/mount, a Giant Duck named Lumpy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Melisende, Cleric of Aelys and temporary Retainer accompanying the party&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;session-027-recap&quot; tabindex=&quot;-1&quot;&gt;Session 027 Recap&lt;/h3&gt;
&lt;p&gt;We pick up at the beginning of the session exactly where we left off: immediately after the party fended off 12 hungry zombies and their Wight leader in the middle of the night. Though mostly unscathed, the party opted to finish their rest, and slept in.&lt;/p&gt;
&lt;p&gt;Before resuming their journey, they searched the Zombie&#39;s corpses&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers027/#1-note&quot; id=&quot;1-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup title=&quot;Though they forgot the Wight, who had all the interesting stuff on him.&quot;&gt;1&lt;/sup&gt;&lt;/a&gt;, before deciding to hide them far enough off the side of the road that they wouldn&#39;t be seen. They then set off to make the final day&#39;s trek to the keep. Along the way, they came across an abandoned camp, complete with weapons and armor, presumably from the zombified guards.&lt;/p&gt;
&lt;p&gt;Arriving at their destination at dusk, they prudently opted to make camp a safe distance away rather than approaching the keep at night. In the morning, Mick rode Lumpy up to survey their destination. He discovered the keep was dark, silent, and utterly absent of any sign of life, with the gate standing wide open and seemingly unprotected. Mick returned to the group with this news, and they concocted a plan: hide the wagon, approach on foot, sneak around back, and have Lumpy fly a rope attached to a grappling hook up to the top of the keep.&lt;/p&gt;
&lt;p&gt;And so, a few good stealth rolls later, the party found themselves on the roof of the castle, inching down a dark staircase into the top floor of the keep. They poked around in a large dining area, noting the old food on the central table and investigating a couple statues around the perimeter of the room that Faegwyn determined were probably former Knights of the Westerwatch. Meanwhile, Mick got to work picking a locked door on one side of the room, and Kilomir kept a lookout.&lt;/p&gt;
&lt;p&gt;Unfortunately, a very low perception roll meant that Kilomir was keeping an eye out for threats coming from &lt;em&gt;outside&lt;/em&gt; the room, and failed to notice the 5 &lt;a href=&quot;https://www.dndbeyond.com/monsters/1680917-boneless&quot;&gt;Boneless&lt;/a&gt; (undead remains lacking their skeletons) slithering out from beneath the large dining table in the center of the room.&lt;/p&gt;
&lt;p&gt;The ambush led to a short but pitched battle, with the party successfully defeating the Boneless, though not before Mick was engulfed by one of the &amp;quot;Undead Murder Blankets&amp;quot; and nearly smothered to death. But Kilomir&#39;s &lt;em&gt;Longsword of the Forgotten Paladin&lt;/em&gt;, with its extra Radiant Damage against Undead, Faegwyn&#39;s punishing Scorching Rays, and Ollie&#39;s onslaught of fists made quick work of their foes. The Party emerged from the fight a little battered, a little bruised, but ready to press deeper into the Westerwatch.&lt;/p&gt;
&lt;h2 id=&quot;dm&#39;s-post-mortem&quot; tabindex=&quot;-1&quot;&gt;DM&#39;s Post Mortem&lt;/h2&gt;
&lt;p&gt;Okay, so two things stuck out to me about this session.&lt;/p&gt;
&lt;p&gt;First, if you give one of your players a flying mount, assume they will use it for unconventional approaches to entering dungeons. I had a whole thing planned with a closing portcullis trap, which I&#39;ve been itching to try out and was going to use with the entrance to the keep, and it was completely defanged just by them deciding to do an aerial insertion. Plus, now they&#39;re going through my encounter design backwards. Ah well.&lt;/p&gt;
&lt;p&gt;Second, if you have a campaign that will heavily feature a certain type of enemy, don&#39;t give the low-level players a weapon that does &lt;strong&gt;2d10 magical bonus damage&lt;/strong&gt; against enemies of that type, &lt;em&gt;especially&lt;/em&gt; if those enemies often have weaknesses to said type. Or at least, equally distribute that among the party. Otherwise you&#39;ll find yourself in a situation where one character consistently outpaces the rest of the party in ways that are hard to balance around. Obvious in retrospect, but I don&#39;t think I really considered how much extra damage 2d10 is on &lt;em&gt;every attack&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;That&#39;s all for this post! Life&#39;s been a bit hectic recently (that&#39;s why this post is almost a month late), so I&#39;ve got a small backlog of sessions to post about, so stay tuned for those!&lt;/p&gt;
  &lt;footer role=&quot;doc-endnotes&quot; class=&quot;Footnotes&quot;&gt;
    &lt;h2 id=&quot;footnotes-label&quot; class=&quot;Footnotes__title&quot;&gt;Footnotes&lt;/h2&gt;
    &lt;ol class=&quot;Footnotes__list&quot;&gt;&lt;li id=&quot;1-note&quot; class=&quot;Footnotes__list-item&quot; role=&quot;doc-endnote&quot;&gt;Though they forgot the Wight, who had all the interesting stuff on him. &lt;a class=&quot;Footnotes__back-link&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers027/#1-ref&quot; aria-label=&quot;Back to reference 1&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
  &lt;/footer&gt;
</content>
		  </entry>
		  <entry>
			<title>The Bandoleers Session 026</title>
			<link href="https://ryandlewis.dev/posts/ttrpg/thebandoleers026/"/>
			<updated>2022-05-18T00:00:00Z</updated>
			<id>https://ryandlewis.dev/posts/ttrpg/thebandoleers026/</id>
			<content type="html">&lt;h2 id=&quot;dm&#39;s-log%3A-supplemental...session-026&quot; tabindex=&quot;-1&quot;&gt;DM&#39;s Log: Supplemental...Session 026&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Date: 2022-05-12&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;party%3A-the-bandoleers&quot; tabindex=&quot;-1&quot;&gt;Party: The Bandoleers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Kilomir, Minotaur Wild-Magic Barbarian
&lt;ul&gt;
&lt;li&gt;The muscle of the group&lt;/li&gt;
&lt;li&gt;Trying to learn how to craft Dragonborn Plate Armor&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Faegwyn Suithrasas, High Elf School of Evocation Wizard
&lt;ul&gt;
&lt;li&gt;Resident grumpy old man&lt;/li&gt;
&lt;li&gt;Trying, with varying degrees of success, to expand the Wagonline business that he inherited after the unfortunate death of the previous proprietor at the vicious claws of a flock of cockatrice&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ollie, Tiefling Way of the Long Death Monk
&lt;ul&gt;
&lt;li&gt;Mandatory party Edgelord&lt;/li&gt;
&lt;li&gt;Has the unique ability to see and speak with the Gods of Corithynn&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mick Nichols, Rock Gnome Assassin Rogue
&lt;ul&gt;
&lt;li&gt;Accompanied by his trusty sidekick/mount, a Giant Duck named Lumpy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;session-026-recap&quot; tabindex=&quot;-1&quot;&gt;Session 026 Recap&lt;/h3&gt;
&lt;p&gt;In this session, The Bandoleers began their 3-day journey to the Westerwatch, a small keep nestled in the mountains west of the city of Dmittlock &lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#1-note&quot; id=&quot;1-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup title=&quot;The city the party has been calling home for the majority of the campaign.&quot;&gt;1&lt;/sup&gt;&lt;/a&gt; that recently stopped sending reports. They were joined by a cleric, Melisende, of the local deity Aelys.&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#2-note&quot; id=&quot;2-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup title=&quot;They had requested Melisende&#39;s presence as part of their deal to look into the problem (plague outbreaks have been cropping up in the region, so the party wanted to have a healer on hand).&quot;&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The route to the Westerwatch is along a fairly well-traveled and &amp;quot;safe&amp;quot; road, and so their first day of travel passed without incident. As nightfall approached, they reached a small hamlet, where they decided to try and find a place to stay. And so they made the acquaintance of an old one-legged Rock Gnome named Igden Umbodoben, who was sitting on his porch and enjoying the fresh air.&lt;/p&gt;
&lt;p&gt;After some pleasantries, Igden graciously agreed to put the party up in his barn for the evening for a crisp 5 silver. He also answered some questions about their route and destination (including telling them that a patrol of 12 towns guard had passed through on their way to the keep recently, and not yet returned).&lt;/p&gt;
&lt;p&gt;The party&#39;s resident Gnome, Mick, also had a private sidebar with Igden, connecting over the fact that they were both veterans of a recent civil war in their home city, Quarriton. There was an awkward verbal dance where they both tried to figure out which side the other fought on, until eventually Igden asked directly and it was revealed they were both Partisans.&lt;/p&gt;
&lt;p&gt;After an evening&#39;s rest safely in the barn, the second day of the journey brought them out of the dense forest surrounding Dmittlock. Now they found themselves journeying across grasslands and sloping hills which I described to the players as having &amp;quot;Big Iowa Energy&amp;quot;. And, just like driving across Iowa, this day of travel passed without anything of note occurring.&lt;/p&gt;
&lt;p&gt;With no civilization in sight this time at the end of the day, the party decided to take their wagon off the side of the road (&amp;quot;31 feet&amp;quot;, per Kilomir&#39;s player&#39;s request). At this point, the party has learned to keep a watch, and so Ollie was dutifully patrolling around the wagon when he heard ominous moaning coming from down the road.&lt;/p&gt;
&lt;p&gt;Peering out into the darkness with his Tiefling Dark Vision, Ollie was able to detect a small group of figures shambling up the road. After waiting a beat to see what they did and realizing that they were getting closer and closer, he decided to wake the others. Unfortunately, his attempt to do so quietly was foiled when he rolled in the single digits on a Stealth check, and as a result was given away by a &lt;em&gt;very&lt;/em&gt; squeaky hinge on the wagon&#39;s door.&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#3-note&quot; id=&quot;3-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup title=&quot;At least this had the benefit of waking the rest of the party.&quot;&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As soon as the figures heard Ollie&#39;s racket, they froze. The party gathered and readied themselves, and, as the unidentified group began moving again, Faegwyn&#39;s sharp elvish eyes (and high perception roll) immediately identified the shuffling gait of wretched Undead. With a cry to alert the others, initiative was rolled as the Zombies charged in!&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#4-note&quot; id=&quot;4-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup title=&quot;&#39;Charge&#39; is relative, zombie&#39;s speed being what it is.&quot;&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The battle was chaotic but not necessarily difficult for the party. Ollie, Mick, and Kilomir charged to the fore, while Melisende and Faegwyn stayed back by the wagon. As it transpired, the &lt;em&gt;12 zombies&lt;/em&gt; were joined by a Wight, who hung back and attempted (mostly unsuccessfully) to pepper Faegwyn with his longbow. The frontliners were able to clean up the Zombies without too much damage (Kilomir and Ollie came out entirely unscathed), and then chased down and dispatched the Wight when it attempted to flee.&lt;/p&gt;
&lt;p&gt;And that&#39;s where we&#39;ll pick up next session!&lt;/p&gt;
&lt;h2 id=&quot;dm&#39;s-post-mortem&quot; tabindex=&quot;-1&quot;&gt;DM&#39;s Post Mortem&lt;/h2&gt;
&lt;p&gt;Alright, so this session was...interesting...to try and run, because I made the mistake of trying to do it while sick.&lt;a class=&quot;Footnotes__ref&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#5-note&quot; id=&quot;5-ref&quot; aria-describedby=&quot;footnotes-label&quot; role=&quot;doc-noteref&quot;&gt;&lt;sup title=&quot;Some sort of respiratory bug. Tests say not COVID, but who can say at this point. Either way, we play over discord, so no players were at risk of IRL plague from the session.&quot;&gt;5&lt;/sup&gt;&lt;/a&gt; This meant that the session 1.) was shorter than I&#39;d hoped, 2.) took me longer to prepare than usual, and 3.) was probably not my best work. I was determined to run, regardless of how miserable I felt, because scheduling is hard and it had already been awhile since our previous session and I didn&#39;t want to postpone again. But by the end of the night I was practically a zombie myself, and as soon as the Wight&#39;s HP hit 0 I was out.&lt;/p&gt;
&lt;p&gt;But I did still get the opportunity to try some stuff I&#39;ve been wanting to work in to the campaign, so let&#39;s talk about those things.&lt;/p&gt;
&lt;h3 id=&quot;on-melisende-and-retainers&quot; tabindex=&quot;-1&quot;&gt;On Melisende and Retainers&lt;/h3&gt;
&lt;p&gt;There are many ways to run an NPC who&#39;s tagging along with a party of PCs.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Roll up a PC and run them yourself (often frowned upon)&lt;/li&gt;
&lt;li&gt;Make a stat block and run them as another NPC (more work for the DM, action economy in shambles)&lt;/li&gt;
&lt;li&gt;The Companion Rules from &lt;em&gt;Tasha&#39;s Cauldron of Everything&lt;/em&gt; (haven&#39;t tried them yet)&lt;/li&gt;
&lt;li&gt;MCDM&#39;s Retainers &amp;lt;--This is the one I went with&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&#39;ve had MCDM&#39;s &lt;em&gt;&lt;a href=&quot;https://shop.mcdmproductions.com/products/strongholds-followers-hardcover-pdf&quot;&gt;Strongholds and Followers&lt;/a&gt;&lt;/em&gt; for awhile now, but hadn&#39;t gotten a chance to use the Retainer system introduced in it just yet. This system provides an easy way to give your PCs loyal companions which the DM roleplays, but the player runs in combat. The system is suppose to enable competent, helpful, and easy to run companions who don&#39;t steal the show from the main characters: the PCs.&lt;/p&gt;
&lt;p&gt;As it just so happens, MCDM just recently released the kickstarter for their next book, &lt;em&gt;&lt;a href=&quot;https://www.kickstarter.com/projects/mattcolville/mcdm-monster-book&quot;&gt;Flee, Mortals!&lt;/a&gt;&lt;/em&gt;, and with it a &lt;a href=&quot;https://files.mcdmproductions.com/FleeMortals/FleeMortalsPreview.pdf&quot;&gt;26-page PDF preview&lt;/a&gt;. And that preview included some updated Retainer rules (most notably replacing a somewhat clunky health level system from the original), so I decided to give that a shot. I used a &amp;quot;Healer&amp;quot; Cleric retainer from the S&amp;amp;F book, updated with the new rules, made a little &lt;a href=&quot;https://ryandlewis.dev/assets/PDFs/Melisende.pdf&quot;&gt;PDF handout&lt;/a&gt; of her stats to give to the player for reference, and that was that.&lt;/p&gt;
&lt;p&gt;I gave combat control of her to Kilomir&#39;s player, who seemed to be able to pick up on what he could do with her with little issue and only a small amount of guidance from me as the DM. The addition of a Retainer didn&#39;t seem to slow the game down much, if at all, or terribly unbalance the encounter, which are really all you can ask for. Overall, very happy with the system so far!&lt;/p&gt;
&lt;h3 id=&quot;minions&quot; tabindex=&quot;-1&quot;&gt;Minions&lt;/h3&gt;
&lt;p&gt;From the same preview PDF, MCDM also introduced rules for Minions, in the style of 4th edition D&amp;amp;D, but revamped and revised for 5e. This was another thing I really wanted to try, and since I already had a zombie encounter planned and the PDF included a sample zombie minion, it seemed meant to be! These rules allow you to inundate your PCs with swarms of low-health enemies that are weak individually but dangerous in hordes.&lt;/p&gt;
&lt;p&gt;Overall, I found the system worked pretty well, and definitely was faster than running 12 individual zombies with their own action, movement and bonus action. It has a nice system for bundling up multiple attacking minions into a single attack roll per target. The overkill mechanics are also nice.&lt;/p&gt;
&lt;p&gt;I suspect the most challenging aspect of using these guys properly will ultimately be balancing encounters. This combat encounter wasn&#39;t meant to be brutally difficult, more just ominously foreshadowing, but I was a little surprised at how easily the party dispatched the horde. Part of that was because I made the mistake of starting the zombies too far away, giving the PCs ample time to hit them at range and thin the herd. But the other part was that I suspect the CR math overestimated the threat involved. But nobody has ever accused 5e&#39;s CR system of being infallible.&lt;/p&gt;
&lt;p&gt;I did find the mechanics around spell saves unintuitive enough that I had to stop and look them up, which didn&#39;t feel great, but I understand the design decisions behind it and think the way it currently works is probably for the best. I imagine it&#39;ll be better once I can remember it off the cuff.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot; tabindex=&quot;-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Maybe not my best session, but the players seemed to have fun and I got to try out some fun MCDM goodies I&#39;ve been meaning to find an excuse to use. And hey, at least I can blame any problems that came up during the session on illness.&lt;/p&gt;
  &lt;footer role=&quot;doc-endnotes&quot; class=&quot;Footnotes&quot;&gt;
    &lt;h2 id=&quot;footnotes-label&quot; class=&quot;Footnotes__title&quot;&gt;Footnotes&lt;/h2&gt;
    &lt;ol class=&quot;Footnotes__list&quot;&gt;&lt;li id=&quot;1-note&quot; class=&quot;Footnotes__list-item&quot; role=&quot;doc-endnote&quot;&gt;The city the party has been calling home for the majority of the campaign. &lt;a class=&quot;Footnotes__back-link&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#1-ref&quot; aria-label=&quot;Back to reference 1&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/li&gt;
&lt;li id=&quot;2-note&quot; class=&quot;Footnotes__list-item&quot; role=&quot;doc-endnote&quot;&gt;They had requested Melisende&#39;s presence as part of their deal to look into the problem (plague outbreaks have been cropping up in the region, so the party wanted to have a healer on hand). &lt;a class=&quot;Footnotes__back-link&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#2-ref&quot; aria-label=&quot;Back to reference 2&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/li&gt;
&lt;li id=&quot;3-note&quot; class=&quot;Footnotes__list-item&quot; role=&quot;doc-endnote&quot;&gt;At least this had the benefit of waking the rest of the party. &lt;a class=&quot;Footnotes__back-link&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#3-ref&quot; aria-label=&quot;Back to reference 3&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/li&gt;
&lt;li id=&quot;4-note&quot; class=&quot;Footnotes__list-item&quot; role=&quot;doc-endnote&quot;&gt;&#39;Charge&#39; is relative, zombie&#39;s speed being what it is. &lt;a class=&quot;Footnotes__back-link&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#4-ref&quot; aria-label=&quot;Back to reference 4&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/li&gt;
&lt;li id=&quot;5-note&quot; class=&quot;Footnotes__list-item&quot; role=&quot;doc-endnote&quot;&gt;Some sort of respiratory bug. Tests say not COVID, but who can say at this point. Either way, we play over discord, so no players were at risk of IRL plague from the session. &lt;a class=&quot;Footnotes__back-link&quot; href=&quot;https://ryandlewis.dev/posts/ttrpg/thebandoleers026/#5-ref&quot; aria-label=&quot;Back to reference 5&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
  &lt;/footer&gt;
</content>
		  </entry>
		  <entry>
			<title>Introducing DM&#39;s Log: Supplemental</title>
			<link href="https://ryandlewis.dev/posts/ttrpg/introducingdmslog/"/>
			<updated>2022-05-05T00:00:00Z</updated>
			<id>https://ryandlewis.dev/posts/ttrpg/introducingdmslog/</id>
			<content type="html">&lt;blockquote&gt;
&lt;h2 id=&quot;dm&#39;s-log%3A-supplemental&quot; tabindex=&quot;-1&quot;&gt;DM&#39;s Log: Supplemental&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Date: 2022-05-05.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Session: N/A&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At last, I&#39;ve discovered it! The nerdiest possible thing I could spend my time doing: writing about my Dungeons and Dragons games on the internet.&lt;/p&gt;
&lt;p&gt;This will be the first in a series of logs recounting my experiences as an aggressively mediocre Dungeon Master attempting to run half decent games in between work and grad school.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;why%3F&quot; tabindex=&quot;-1&quot;&gt;Why?&lt;/h2&gt;
&lt;p&gt;A little experiment I&#39;m trying. I want to practice writing more often (fun writing, not just code, emails, project reports, and academic papers). And I figure, what better way to do it than cataloging the shenanigans of my semi-regular D&amp;amp;D games?&lt;/p&gt;
&lt;p&gt;I&#39;m also hoping to include a little bit of reflection on each game, how I thought it went, and how I think I can improve. Hopefully, that will be useful for other DM&#39;s as well, but if nothing else it should help me.&lt;/p&gt;
&lt;h2 id=&quot;what-games%3F&quot; tabindex=&quot;-1&quot;&gt;What Games?&lt;/h2&gt;
&lt;p&gt;Currently, I&#39;m DMing one game with plans to start a fairly ambitious project this summer. I may also write about some of the games I play in, if the mood strikes me, though I just wrapped up one game and the other I regularly play in is on indefinite hold.&lt;/p&gt;
&lt;h3 id=&quot;the-bandoleers&quot; tabindex=&quot;-1&quot;&gt;The Bandoleers&lt;/h3&gt;
&lt;p&gt;A scrappy group of adventurers grappling with a mysterious outbreak of undead and plague, the many dangers of open road and claustrophobic dungeon, and the struggles of operating a small business; all set in the homebrew world of Corithynn.&lt;/p&gt;
&lt;p&gt;Starring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kilomir, Minotaur Wild-Magic Barbarian
&lt;ul&gt;
&lt;li&gt;The muscle of the group&lt;/li&gt;
&lt;li&gt;Trying to learn how to craft Dragonborn Plate Armor&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Faegwyn Suithrasas, High Elf School of Evocation Wizard
&lt;ul&gt;
&lt;li&gt;Resident grumpy old man&lt;/li&gt;
&lt;li&gt;Trying, with varying degrees of success, to expand the Wagonline business that he inherited after the unfortunate death of the previous proprietor at the vicious claws of a flock of cockatrice&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ollie, Tiefling Way of the Long Death Monk
&lt;ul&gt;
&lt;li&gt;Mandatory party Edgelord&lt;/li&gt;
&lt;li&gt;Has the unique ability to see and speak with the Gods of Corithynn&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mick Nichols, Rock Gnome Assassin Rogue
&lt;ul&gt;
&lt;li&gt;Accompanied by his trusty sidekick/mount, a Giant Duck named Lumpy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the next session (Session 026), the Bandoleers are heading out from the relative safety of their current port of call, the Drunkard&#39;s City Dmittlock, and making a 3-day trek to an old fort called the Westerwatch that has stopped sending its regular reports.&lt;br /&gt;
With one patrol of the City Watch already sent to investigate and seemingly lost, they&#39;re expecting trouble.&lt;br /&gt;
They got this particular gig by striking a deal with the head of the Privy Council of Dmittlock to figure out what&#39;s going on in exchange for a hefty chunk of change and the opportunity to further some of their party goals.&lt;/p&gt;
&lt;h3 id=&quot;empyrea-lost&quot; tabindex=&quot;-1&quot;&gt;Empyrea Lost&lt;/h3&gt;
&lt;p&gt;This is my secret summer project. More details to come!&lt;/p&gt;
</content>
		  </entry>
		  <entry>
			<title>How to Install and Use Docker in WSL2</title>
			<link href="https://ryandlewis.dev/posts/howtowsldocker/"/>
			<updated>2022-03-06T00:00:00Z</updated>
			<id>https://ryandlewis.dev/posts/howtowsldocker/</id>
			<content type="html">&lt;p&gt;&lt;em&gt;Edit: It&#39;s come to my attention that, since I figured out this workaround back when WSL2 and thus Docker&#39;s WSL2 backend were new, Docker Desktop for Windows has added support for using Docker from within your WSL2 distro. This obviates the need to install Docker within a WSL2 distro in most cases. But if you find yourself in a position where you can&#39;t or don&#39;t want to use the Docker Desktop support, read on.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Say you want to run a Linux environment on a Windows machine, and in that environment one of the things you want to do is make use of docker containers. Here&#39;s the quick and dirty way to get that set up:&lt;/p&gt;
&lt;h2 id=&quot;install-wsl&quot; tabindex=&quot;-1&quot;&gt;Install WSL&lt;/h2&gt;
&lt;p&gt;Nowadays, this should be as simple as &lt;code&gt;Win+X&lt;/code&gt;, selecting &lt;code&gt;&amp;lt;Command Prompt/Powershell/Windows Terminal&amp;gt; (Admin)&lt;/code&gt;, and running &lt;code&gt;wsl --install&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If that doesn&#39;t work, or you want to fiddle/customize/use a non-default distro, check out &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/wsl/install&quot;&gt;Microsoft&#39;s guide here&lt;/a&gt;. Make sure you install a WSL2 distro.&lt;/p&gt;
&lt;p&gt;For the rest of this, I&#39;m assuming you&#39;ve installed the default Ubuntu Distro, steps might be slightly different for other distros.&lt;/p&gt;
&lt;h2 id=&quot;verify-and-setup-wsl&quot; tabindex=&quot;-1&quot;&gt;Verify and Setup WSL&lt;/h2&gt;
&lt;p&gt;Make sure that the distro you just installed is a WSL2 distro, as you can&#39;t run docker in WSL1.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Set the default version to 2
wsl --set-default-version 2
# Check that the distro you installed is version 2
wsl -l -v
# Upgrade a v1 distro to v2
wsl --set-version &amp;lt;distro-name&amp;gt; 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&#39;re having trouble upgrading the distro, see &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/wsl/install#ways-to-run-multiple-linux-distributions-with-wsl&quot;&gt;here for help&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, open the &amp;quot;Ubuntu&amp;quot; application that you just got installed, and set your username and password. Do package updates, install whatever tools and packages you want, and just generally make yourself at home.&lt;/p&gt;
&lt;h2 id=&quot;install-docker&quot; tabindex=&quot;-1&quot;&gt;Install Docker&lt;/h2&gt;
&lt;p&gt;Follow &lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository&quot;&gt;these setup instructions&lt;/a&gt; (if you chose to install a distro other than Ubuntu, find the appropriate install guide on the left of that page).&lt;/p&gt;
&lt;p&gt;Stop before running &lt;code&gt;sudo docker run hello-world&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;configuring-docker-on-wsl2&quot; tabindex=&quot;-1&quot;&gt;Configuring Docker on WSL2&lt;/h2&gt;
&lt;h3 id=&quot;using-docker-without-invoking-root&quot; tabindex=&quot;-1&quot;&gt;Using Docker Without Invoking Root&lt;/h3&gt;
&lt;p&gt;Don&#39;t want to have to run docker commands with &lt;code&gt;sudo&lt;/code&gt; all the time? &lt;a href=&quot;https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user&quot;&gt;Follow this guide to add yourself to the docker group&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;starting-the-docker-daemon&quot; tabindex=&quot;-1&quot;&gt;Starting the Docker Daemon&lt;/h3&gt;
&lt;p&gt;One hiccup with docker in WSL2 is that it doesn&#39;t automatically start the Docker service. The simple but annoying solution is to run &lt;code&gt;sudo service docker start&lt;/code&gt; whenever you want to use Docker.&lt;/p&gt;
&lt;p&gt;If you don&#39;t want to have to remember and invoke that command every time, you can add the following to your &amp;quot;~/.profile&amp;quot;, or your shell configuration file like &amp;quot;~/.bashrc&amp;quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if [ -n &amp;quot;`service docker status | grep not`&amp;quot; ]; then
    sudo /usr/sbin/service docker start
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now this has the annoying side effect of making you have to type out your sudo password whenever you start WSL2 for the first time.&lt;/p&gt;
&lt;p&gt;To fix this, you can run &lt;code&gt;sudo visudo -f /etc/sudoers.d/passwordless_docker_start&lt;/code&gt;, add the following to the file (replacing &lt;code&gt;username&lt;/code&gt; with your Linux username), save and close. (&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/how-to-edit-the-sudoers-file&quot;&gt;See here to learn more about the intricacies and nuances of sudoer files&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;username        ALL = (root) NOPASSWD: /usr/sbin/service docker start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, the docker service automatically starts in WSL2 without requiring authentication, and you can use it more or less exactly like you would use Docker on a regular Linux install.&lt;/p&gt;
&lt;p&gt;Cover Photo by &lt;a href=&quot;https://www.pexels.com/@tomfisk?utm_content=attributionCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=pexels&quot;&gt;Tom Fisk&lt;/a&gt; from &lt;a href=&quot;https://www.pexels.com/photo/aerial-view-photography-of-container-van-lot-1427107/?utm_content=attributionCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=pexels&quot;&gt;Pexels&lt;/a&gt;.&lt;/p&gt;
</content>
		  </entry>
		  <entry>
			<title>How to Communicate with an Arduino from Docker</title>
			<link href="https://ryandlewis.dev/posts/howtoarduinodocker/"/>
			<updated>2021-03-14T00:00:00Z</updated>
			<id>https://ryandlewis.dev/posts/howtoarduinodocker/</id>
			<content type="html">&lt;p&gt;Something I learned this weekend. Let&#39;s say you have an Arduino, and you want to communicate with it via serial from a Linux device, like a Jetson Nano. But not from that device&#39;s host operating system. No, that&#39;d be too easy.&lt;/p&gt;
&lt;p&gt;Instead, you want to talk with this Arduino from an application running in an unprivileged Docker container. How would you do that?&lt;/p&gt;
&lt;h2 id=&quot;how-to-bring-an-arduino-inside-docker&quot; tabindex=&quot;-1&quot;&gt;How To Bring an Arduino inside Docker&lt;/h2&gt;
&lt;p&gt;It turns out it&#39;s actually quite simple! When connected to a Linux device via a USB cable, most Arduino&#39;s show up as a device in the form &lt;code&gt;/dev/ttyAMCx&lt;/code&gt; where x is replaced with an integer counter, starting from 0. So the first Arduino you connect is &lt;code&gt;/dev/ttyAMC0&lt;/code&gt;, the second is &lt;code&gt;/dev/ttyAMC1&lt;/code&gt; and so on.&lt;/p&gt;
&lt;p&gt;To access that from Docker, all you need is the docker &lt;code&gt;--device&lt;/code&gt; flag. Include it in the docker run command as &lt;code&gt;docker run &amp;lt;other options&amp;gt; --device=&amp;quot;/dev/ttyACM0&amp;quot; &amp;lt;more options, image name, etc&amp;gt;&lt;/code&gt; and you&#39;re all set. You can access the Arduino in the container at &lt;code&gt;/dev/ttyACMx&lt;/code&gt; just like you would on the host, say with the &lt;code&gt;pyserial&lt;/code&gt; package in Python.&lt;/p&gt;
&lt;h2 id=&quot;words-of-warning&quot; tabindex=&quot;-1&quot;&gt;Words of Warning&lt;/h2&gt;
&lt;p&gt;Couple quick notes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You need to have the device connected before you launch your docker container with that flag. Otherwise, you&#39;ll get an error.&lt;/li&gt;
&lt;li&gt;You may run into issues if the device is disconnected and reconnected while the container is running.&lt;/li&gt;
&lt;li&gt;Serial over wires (for instance, with jumper cables connecting pins on the arduino to GPIO on a Jetson or Raspberry Pi) rather than via the USB port will probably have a different device file handle, but will function the same way (pass the device file handle into the container with &lt;code&gt;--devices&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
</content>
		  </entry>
		  <entry>
			<title>⌨ MCK142Pro.ahk</title>
			<link href="https://ryandlewis.dev/projects/mck142pro/"/>
			<updated>2021-02-28T00:00:00Z</updated>
			<id>https://ryandlewis.dev/projects/mck142pro/</id>
			<content type="html">&lt;p&gt;Project Link: &lt;a href=&quot;https://gist.github.com/LuckierDodge/2ed678e035306d7f3cd935a40b3b0028&quot;&gt;https://gist.github.com/LuckierDodge/2ed678e035306d7f3cd935a40b3b0028&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Description: &lt;em&gt;An AutoHotKey script that I use to extend the functionality of an MCK142Pro programmable mechanical keyboard.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I wrote a whole &lt;a href=&quot;https://ryandlewis.dev/posts/mck142pro&quot;&gt;post&lt;/a&gt; about this project! In short, I took a thrift store mechanical keyboard from the 90s and figured out how to make it a budget Stream Deck for the 2020s.&lt;/p&gt;
</content>
		  </entry>
		  <entry>
			<title>Extending an Old-school Programmable Keyboard with AutoHotKey</title>
			<link href="https://ryandlewis.dev/posts/mck142pro/"/>
			<updated>2021-02-23T00:00:00Z</updated>
			<id>https://ryandlewis.dev/posts/mck142pro/</id>
			<content type="html">&lt;h2 id=&quot;so-it-begins...&quot; tabindex=&quot;-1&quot;&gt;So it Begins...&lt;/h2&gt;
&lt;p&gt;Our tale begins in December of 2020 with a message from my friend, roommate, and a notorious thrifter/dumpster diver extraordinaire, Jorge (name changed to protect the innocent) asking if I was interested in an old mechanical keyboard from the thrift store, still in the original packaging.&lt;br /&gt;
The message came with a picture of the front of the box, from which little information could be gleaned except the words &amp;quot;TWENTY-FOUR PROGRAMMABLE KEYS&amp;quot;.&lt;br /&gt;
Like any good computer nerd, I said &amp;quot;Absolutely!&amp;quot;.&lt;br /&gt;
So he picked up two, one for himself and one for me, for a crisp $3 USD each.&lt;/p&gt;
&lt;p&gt;When he returned with his loot, I quickly set to figuring out what, exactly, we had on our hands.&lt;br /&gt;
These were brand-new in the box keyboards, and the box said &lt;em&gt;MCK-142 Pro&lt;/em&gt;...and not much else.&lt;br /&gt;
Not even a manufacturer.&lt;br /&gt;
But a name&#39;s as good a place to start as any, and I turned to the internet and my search engine of choice to look for clues.&lt;/p&gt;
&lt;p&gt;And that&#39;s where I found &lt;a href=&quot;http://www.mck142.com/&quot;&gt;http://www.mck142.com/&lt;/a&gt;, a lovely little time capsule/memorial devoted to this keyboard specifically.&lt;br /&gt;
I encourage you to click through and appreciate this website for a moment (it doesn&#39;t take too long to read in its entirety, and it&#39;s such a lovingly crafted little webpage).&lt;/p&gt;
&lt;h2 id=&quot;getting-to-know-the-mck-142-pro&quot; tabindex=&quot;-1&quot;&gt;Getting to Know the MCK-142 Pro&lt;/h2&gt;
&lt;p&gt;For those of you who didn&#39;t click through, I&#39;ll give you a quick rundown of this keyboard&#39;s features&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Satisfyingly Clicky Mechanical Switches&lt;/li&gt;
&lt;li&gt;Two sets of Function Keys: the standard top row and a &amp;quot;Left-handed&amp;quot; column on the left side of the main keyboard&lt;/li&gt;
&lt;li&gt;8-directional arrow keys (that&#39;s right, this thing can move the cursor diagonally with a single keypress)&lt;/li&gt;
&lt;li&gt;Coiled PS2 cable&lt;/li&gt;
&lt;li&gt;A Fast Repeat Key. According to the manual, this bumps the keyboard from a default of 10 Characters Per Second (CPS) when a key is held, to a crisp 20 CPS with the Fast Repeat Key.&lt;/li&gt;
&lt;li&gt;24 Programmable Keys&lt;/li&gt;
&lt;li&gt;8K (as in, Kilobytes) of Onboard CMOS SRAM Memory, maintained by a set of 4(!) AA batteries (not included, thankfully. Otherwise they&#39;d be a corroded mess by now)&lt;/li&gt;
&lt;li&gt;A floppy disk with software for programming the aformentioned Programmable Keys&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of this functionality has been rendered obsolete by the passage of time, of course.&lt;br /&gt;
The &amp;quot;Fast Repeat&amp;quot; key, which would allow the user to more quickly enter the same key when holding it down, is superseded by the host operating system&#39;s settings on key repeats and associated delays these days.&lt;br /&gt;
The floppy disk with included software for programming may still be usable on some IBM PC&#39;s from the 90s, but I wasn&#39;t about to try and find out.&lt;/p&gt;
&lt;p&gt;Notably, it does not feature a Windows Key, because it wasn&#39;t really intended for use with Windows NT-based computers, but &lt;a href=&quot;https://github.com/microsoft/PowerToys&quot;&gt;PowerToys&#39;&lt;/a&gt; key remapping utility solves that (in my case, I remapped the Caps Lock key, a key which I ONLY press accidentally).&lt;/p&gt;
&lt;p&gt;The keyboard itself very much reminds me of the old Gateway family PC we had when I was growing up, with the same solid square body, weighty heft, and lightly textured beige ABS with gray accents.&lt;br /&gt;
I&#39;ve never had too much nostalgia for that era of computing (I mostly remember that PC for being slow, noisy, and prone to crashes), but it&#39;s well-built, if nothing else.&lt;/p&gt;
&lt;div class=&quot;image&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https://ryandlewis.dev/assets/images/mck142pro/keyboard.jpg#responsiveimage&quot; alt=&quot;The MCK142Pro Keyboard and Manual, sitting on a desk.&quot; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;programming-the-programmable-keyboard&quot; tabindex=&quot;-1&quot;&gt;Programming the Programmable Keyboard&lt;/h2&gt;
&lt;p&gt;Now, I had to figure out how to program this beauty.&lt;br /&gt;
At first, I was concerned that, without the software working (aka, without an IBM PC or some sort of emulator), I&#39;d be unable to actually program the keyboard.&lt;br /&gt;
Not the end of the world, but disappointing to relinquish such unique functionality.&lt;/p&gt;
&lt;p&gt;Thankfully, a detailed reading of the manual revealed that this was not the case!&lt;br /&gt;
In addition to the 24 Programmable Keys, the keyboard has a &amp;quot;Select&amp;quot; key and two LEDs: &amp;quot;Menu&amp;quot; and &amp;quot;Prog&amp;quot;.&lt;br /&gt;
As it turns out, that key could be used to program each of the 24 keys, with the following steps (taken word for word out of the manual):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Press the SELECT Key twice within a second, the PROG LED indicator in YELLOW will turn on.&lt;/li&gt;
&lt;li&gt;Press one PF Key, e.g. any one of PF1 to PF24, to start data entry.&lt;/li&gt;
&lt;li&gt;Type strings or commands you want to save in any one of PF keys by using keyboard typewriter keys. The keystrokes of every PF key is limited within 320 keystrokes.&lt;/li&gt;
&lt;li&gt;Press the SELECT Key once to save the data to the chosen PF Key, the PROG LED indicator in YELLOW will flash once.&lt;/li&gt;
&lt;li&gt;Repeat the action from 2. to 4. for defining others or redefining any PF keys.&lt;/li&gt;
&lt;li&gt;Press the SELECT Key one more time to complete data entry, the PROG LED indicator in YELLOW will turn off.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, for instance, if I wanted to map the &lt;code&gt;PF1&lt;/code&gt; key to something like &lt;code&gt;Ctrl+Shift+V&lt;/code&gt;, I could do that by:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pressing &lt;code&gt;SELECT&lt;/code&gt; twice, pressing &lt;code&gt;PF1&lt;/code&gt;, typing &lt;code&gt;Ctrl+Shift+V&lt;/code&gt;, pressing the &lt;code&gt;SELECT&lt;/code&gt; key, and then pressing the &lt;code&gt;SELECT&lt;/code&gt; key again to exit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, the manual did forget to mention something: how to actually use the &lt;code&gt;PF&lt;/code&gt; keys.&lt;br /&gt;
After programming one of the keys, I found that pressing it didn&#39;t actually do anything.&lt;br /&gt;
Perplexing.&lt;/p&gt;
&lt;p&gt;With the quest teetering on the edge of disaster, I set out to find a solution.&lt;br /&gt;
After some experimentation, I discovered that I needed to press the &lt;code&gt;SELECT&lt;/code&gt; key once to activate the &lt;code&gt;PF&lt;/code&gt; keys. When I did that, the Menu LED lit up green and the &lt;code&gt;PF&lt;/code&gt; keys would work.&lt;br /&gt;
Without it, no dice.&lt;br /&gt;
Moreover, this needed to be done anytime the computer was restarted or the keyboard was disconnected.&lt;br /&gt;
Easy enough, but strangely, not mentioned anywhere in the manual.&lt;br /&gt;
I can&#39;t help but think the software might automatically handle that functionality, but without it the keys have to be manually toggled.&lt;/p&gt;
&lt;h2 id=&quot;extending-with-autohotkey&quot; tabindex=&quot;-1&quot;&gt;Extending with AutoHotKey&lt;/h2&gt;
&lt;p&gt;Now at this point, I can program the keys to do essentially anything I can do on my keyboard (with 320 keystrokes, at least).&lt;br /&gt;
This includes both entering hotkeys and typing text (or a mix of both), which is neat, but I wanted to push it further.&lt;br /&gt;
After all, I could already use hotkeys by...pressing the hotkey.&lt;/p&gt;
&lt;p&gt;Naturally, the first thing I did once I got programming working was program it to type &lt;a href=&quot;https://knowyourmeme.com/memes/the-tragedy-of-darth-plagueis-the-wise&quot;&gt;The Tragedy of Darth Plagueis the Wise&lt;/a&gt; at the press of a button.&lt;br /&gt;
There are rules, after all.&lt;/p&gt;
&lt;p&gt;But how to make this actually useful?&lt;br /&gt;
The use cases suggested by the keyboard were things like passwords, email signatures, and other text you have to repeat often.&lt;br /&gt;
But I&#39;ve found that most of these scenarios have been solved by password managers, built-in email signatures, and other software approaches.&lt;br /&gt;
Plus programming passwords or other sensitive information into a keyboard really sets my security spider-senses atingling.&lt;/p&gt;
&lt;p&gt;What I really wanted to do with this keyboard, was configure it such that I could essentially trigger an arbitrary action on my computer with each button.&lt;br /&gt;
And I wanted to be able to easily change what each &lt;code&gt;PF&lt;/code&gt; key would do (typing out the whole Tragedy of Darth Plagueis takes awhile, after all).&lt;/p&gt;
&lt;h3 id=&quot;enter-autohotkey&quot; tabindex=&quot;-1&quot;&gt;Enter AutoHotKey&lt;/h3&gt;
&lt;p&gt;To do that, I turned to &lt;a href=&quot;https://www.autohotkey.com/&quot;&gt;AutoHotKey&lt;/a&gt;, the self-proclaimed &amp;quot;ultimate automation scripting language for Windows&amp;quot;.&lt;br /&gt;
It&#39;s a powerful tool, capable of a great many things, but for our purposes, it provides us with a way to create custom hot keys that can trigger a wide range of things: run one or more programs, manage which windows are open and active, type text, make decisions based on whats happening on the computer, and more.&lt;br /&gt;
Moreover, these hotkeys and outcomes are laid out in a script, which can be quickly modified and restarted to change the effect of a given hotkey.&lt;/p&gt;
&lt;p&gt;One wrinkle quickly presents itself, however.&lt;br /&gt;
AutoHotKey works by intercepting values from the keyboard.&lt;br /&gt;
For instance, if I type &lt;code&gt;Ctrl+Shift+J&lt;/code&gt;, and I have an AutoHotKey script with that hotkey programmed as a trigger, the corresponding actions will be run when &lt;code&gt;Ctrl+Shift+J&lt;/code&gt; is detected.&lt;br /&gt;
But the &lt;code&gt;PF&lt;/code&gt; keys aren&#39;t really keys, at least, not from the Operating System&#39;s perspective.&lt;br /&gt;
When a &lt;code&gt;PF&lt;/code&gt; key is pressed, the only keys the computer sees are whatever strokes the key was programmed to type.&lt;br /&gt;
So I can&#39;t have an AutoHotKey script listen for the &lt;code&gt;PF&lt;/code&gt; keys directly, I have to have it listen for a special hot key which I program into each &lt;code&gt;PF&lt;/code&gt; key.&lt;/p&gt;
&lt;p&gt;Since I don&#39;t want the AutoHotKey hooks to trigger when I press something other than a &lt;code&gt;PF&lt;/code&gt; key, these can&#39;t be hotkeys I need to use regularly.&lt;br /&gt;
And I don&#39;t want the AutoHotKey functionality to mask actual functionality, either.&lt;/p&gt;
&lt;p&gt;So I set out to find a set of 24 hotkeys not used anywhere else on my computer.&lt;br /&gt;
And I almost succeeded.&lt;br /&gt;
The closest I got was with the pattern &lt;code&gt;Ctrl+Alt+Shift+Fx&lt;/code&gt;, where &lt;code&gt;Fx&lt;/code&gt; corresponds to &lt;code&gt;PFx&lt;/code&gt;, i.e. &lt;code&gt;PF1:F1&lt;/code&gt;, &lt;code&gt;PF2:F2&lt;/code&gt;, etc.&lt;br /&gt;
Now, you&#39;ll quickly realize there&#39;s a problem here: I have 24 programmable keys, but only 12 function keys.&lt;br /&gt;
Not to fear, as it turns out AutoHotKey can differentiate between left and right modifier keys! So I set the first 12 &lt;code&gt;PF&lt;/code&gt; keys to use the left control key, and the last 12 to use the right control key.&lt;/p&gt;
&lt;p&gt;Unfortunately, exactly one of these shortcuts (the &lt;code&gt;F7&lt;/code&gt; combo, I think) is actually already mapped to some obscure function of Nvidia&#39;s GeForce Experience software.&lt;br /&gt;
But since I had never used that functionality or key combo before in my life, I decided that was okay.&lt;br /&gt;
This specific pattern of hotkeys may not work for everyone, but it worked for my purposes.&lt;/p&gt;
&lt;p&gt;Notably, this approach confers another advantage: even without this particular keyboard, I can still use my AutoHotKey setup, because they just correspond to (awkward, inconvenient) hotkeys.&lt;br /&gt;
The Programmable keyboard provides a simple, clean interface from which I can trigger whatever functionality I want with the press of a single button now.&lt;/p&gt;
&lt;h3 id=&quot;the-autohotkey-script&quot; tabindex=&quot;-1&quot;&gt;The AutoHotKey Script&lt;/h3&gt;
&lt;p&gt;So what does the script look like, and what am I actually doing with it?&lt;br /&gt;
&lt;a href=&quot;https://gist.github.com/LuckierDodge/2ed678e035306d7f3cd935a40b3b0028&quot;&gt;Here&#39;s a Gist&lt;/a&gt; of the full version, but I&#39;ll break it down below.&lt;/p&gt;
&lt;p&gt;We start with some opening configuration, which are pretty much the defaults for a new AutoHotKey script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn  ; Enable warnings to assist with detecting common errors.
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, we add 24 of the following, slightly modified to match each &lt;code&gt;PF&lt;/code&gt; key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; PF1
&amp;lt;^!+F1::
Run, wt
return
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line is a comment identifying the programmable key I&#39;m creating a hotkey for (AutoHotKey uses semicolons for its comment character).&lt;br /&gt;
The second is declaring the hotkey, broken up as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;&lt;/code&gt; indicates that we want the &amp;quot;left&amp;quot; version of the next modifier key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;^&lt;/code&gt; corresponds to the control key in AutoHotKey parlance.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; corresponds to the alt key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt; corresponds to the shift key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F1&lt;/code&gt; corresponds to the first function key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;::&lt;/code&gt; indicates the end of our hotkey, with everything after it corresponding to what should happen after the hotkey is detected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This brings us to the third line &lt;code&gt;Run, wt&lt;/code&gt; which, as the name implies, runs a program.&lt;br /&gt;
In this case, its running the &lt;code&gt;wt&lt;/code&gt; shortcut, which opens the Windows Terminal, but you could replace that with any shortcut name or path to an application.&lt;br /&gt;
Basically, it works the same as the Windows Run dialog accessed with &lt;code&gt;Win+r&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But we&#39;re not limited to running just a single command. Consider,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; PF2
&amp;lt;^!+F2::
Run, outlook
Run, &amp;quot;C:&#92;Users&#92;Ryan Lewis&#92;AppData&#92;Local&#92;Microsoft&#92;Teams&#92;Update.exe&amp;quot; --processStart &amp;quot;Teams.exe&amp;quot;
Run, &amp;quot;C:&#92;Users&#92;Ryan Lewis&#92;AppData&#92;Local&#92;slack&#92;slack.exe&amp;quot;
Run, &amp;quot;C:&#92;Users&#92;Ryan Lewis&#92;AppData&#92;Local&#92;Discord&#92;Update.exe&amp;quot; --processStart Discord.exe
return
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which opens all of my various communications applications. I just switch to an empty virtual desktop, press &lt;code&gt;PF2&lt;/code&gt;, and it opens Outlook, Teams, Slack, and Discord. With &lt;a href=&quot;https://github.com/microsoft/PowerToys&quot;&gt;PowerToys&#39;&lt;/a&gt; Fancy Zones, it even nicely positions them in the same grid pattern every time.&lt;/p&gt;
&lt;p&gt;Last but not least, my proudest creation so far:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; PF3
&amp;lt;^!+F3::
WinHTTP := ComObjCreate(&amp;quot;WinHTTP.WinHttpRequest.5.1&amp;quot;)
WinHTTP.Open(&amp;quot;GET&amp;quot;, &amp;quot;https://wttr.in/12345?nFATQ&amp;quot;)
WinHttp.Send()
response := WinHTTP.ResponseText
Gui, New,, Weather Report
Gui, Color, 111111
Gui, Font, s14 cWhite, Cascadia Mono
Gui, Add, Text,, % response
Gui, Show
return
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which sends an HTTP request to the wonderful &lt;a href=&quot;https://wttr.in/&quot;&gt;wttr.in&lt;/a&gt; weather API, and displays the results in a helpful dialog.&lt;br /&gt;
I won&#39;t bore you with the details of how it works, but I think it&#39;s a simple example of some of the powerful potential here: anything I can hook up to an API endpoint, I can run with the press of a button.&lt;/p&gt;
&lt;div class=&quot;image&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https://ryandlewis.dev/assets/images/mck142pro/weather-dialog.png#responsiveimage&quot; alt=&quot;The Weather popup that I see when I press PF3, complete with adorable ASCII art.&quot; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;denouement&quot; tabindex=&quot;-1&quot;&gt;Denouement&lt;/h2&gt;
&lt;p&gt;To summarize, I took an old programmable mechanical keyboard, combined it with the power of AutoHotKey, and found a needlessly complicated way to create a budget &lt;a href=&quot;https://www.elgato.com/en/gaming/stream-deck&quot;&gt;Stream Deck&lt;/a&gt;.&lt;br /&gt;
Compared to the $150 price tag for one of those, $3 and some elbow grease doesn&#39;t seem half bad.&lt;br /&gt;
And, more importantly, it was pretty fun to figure out!&lt;/p&gt;
&lt;p&gt;I&#39;m not done with this project just yet: I hope to find some more useful shortcuts to map, as well as integrating into my homelab/self hosting.&lt;br /&gt;
Once I get Home Assistant setup, for instance, it would be fun to hook up buttons to some automations.&lt;br /&gt;
Of course, I&#39;ll update the Gist as I go.&lt;/p&gt;
&lt;p&gt;Got any suggestions, questions, or thoughts? What would you map these buttons to? Let me know! Otherwise, support open source projects, and be nice to each other out there!&lt;/p&gt;
</content>
		  </entry></feed>
