Adventures in Milling

Jul 16, 2024

Last month I was fortunate enough to purchase a Dewalt 735x thickness planer. My goal with this addition to the shop was two-fold:

  1. Make it possible to mill my own lumber from rough stock
  2. Easily thickness the lumber I have

In all my previous woodworking projects, I've used either S4S (surfaced-four-sides) or S2S (surfaced-two-sides) lumber; that is, all (or most) of the milling work has already been done for you. Buying S4S material is the most expensive way to purchase lumber. The cheapest way to purchase it is in the rough; the boards have been cut by the sawmill, but none of the faces are clean, square, or parallel. It's up to you, the woodworker, to fix that.

As an introductory step into the world of milling, I purchased enough rough Sapele lumber to build two patio side-tables. Each table is a modified version of the California Casual Patio Table that I made over five years ago, which is now all but falling apart.

Since I don't have a jointer, I built myself a jointing sled for the planer. It's an incredibly simple jig: a sheet of MDF with a small cleat glued to one end. Just stabilize the lumber that you want to joint on the sled with shims, then send it through the planer, taking off just a hair at a time. Eventually, the entire surface will be planed and the face will be both flat and parallel to the sled's reference surface (which is also flat). The lumber can then be sent through the planer to get the opposite face parallel (and flat). Combine this with an edge-jointing jig, which I built a while back for my table saw, and I can now convert rough lumber to S4S!

As with most things, there has been a learning curve. I've first learned that milling is typically at least a two-day process. The first step is to mill the lumber to rough thickness (7/8", for example, if the final thickness is to be 3/4"), and then let it rest for a night or two. The next milling process will take it to final thickness, and remove any warping that occurred after the first round. Rushing the process will yield warped lumber, which is a problem I ran into with the first table. Happily, the warpage was minimal, so I was able to recover.

I've also learned that milling creates a ton of sawdust. I'm amazed at how much sawdust results from the thickness planer; bags and bags of it!

All in all, this has been an enjoyable area of woodworking to learn and explore. The results speak for themselves, I think; the lumber I've milled for these side tables has been among the best quality I've worked with so far. It's a joy to construct something and have it come together so neatly. I look forward to using these skills in upcoming projects.

Here's a terrific video from Rick Beato on why music is getting worse. He's a music producer (and has a terrific channel), so if anyone knows the ins and outs, it's him. Check it out.

Arctic Outpost AM 1270

Jun 13, 2024

I found a terrific radio station today on Radio Garden: Arctic Outpost AM 1270, "broadcasting from the top of the world." Based out of Longyearbyen, Norway, the radio station is truly way up North. They play 78's from 1902 to 1958, including this truly hilarious ditty I hadn't heard before:

Be sure to check out this station. It's commercial free and definitely worth a listen!

I've mentioned once before some of the packages I use in Sublime Text, but it's also useful to keep track of the customized settings I use in a few of them. This post will help me track that kind of thing.


I like WinMerge for doing visual diffs, so that's what I configure here.

    "cmd": ["C:\\Program Files (x86)\\WinMerge\\WinMergeU.exe", "$file1", "$file2"]


I like seeing my diagnostic counts in the status bar.

  "show_diagnostics_count_in_view_status": true,


To stay current, I use a custom installation path, which I can keep up to date with the latest release.

    "initializationOptions": {
        "globalSettings": {
            "logLevel": "info",
            "path": ["C:\\Python\\Python31203\\Scripts\\ruff.exe"],
            "interpreter": ["C:\\Python\\Python31203\\python.exe"]


Just the vanilla parser for me, thanks.

    "enabled_parsers": ["markdown"]


This config adds a few extra locations to ignore when looking for TODOs:

    "exclude_folders": [


Apr 17, 2024

For years now, I've been using ack to search for stuff via the command line (it's so much better than grep!). One of its drawbacks, however, is its reliance on having an installed Perl interpreter. In fact, ack is the only reason I have Perl installed these days. Also, like seemingly so many other projects in recent times, ack development appears to have stopped altogether. Several months ago, I stumbled upon ripgrep, a Rust-powered tool that is orders of magnitude faster than ack and friends.

I haven't been using it for long, but it looks like a worthy replacement for my previous tool of choice. For posterity, here's how I have my global configuration set up; the default colors leave a bit to be desired, especially when using a dark terminal theme, and sorting is disabled by default:

# Change the default colors

# Sort by default (slows things down a bit, but awfully handy)

I placed the file (named .ripgreprc) in my home directory, and then referenced it by setting up an environment variable (RIPGREP_CONFIG_PATH). Lots of stuff appears to be moving to Rust for performance, and I'm thoroughly impressed with how fast this searching tool is. Be sure to check it out.

Now that I am fully managing this website's back-end, I've had to educate myself on configuring nginx. Here are a few of the things I've learned since starting down this path:

Using try_files

At first, I couldn't figure out the magic for having nginx serve a static file if it was actually present on the server, but fall back to my Django application if it wasn't. The answer is to use try_files, a handy built-in resource for this exact use case. I found this via this helpful entry in the Pitfalls and Common Mistakes article (which itself is a treasure trove of information). The specific entry ended up looking like this:

server {
    location / {
        try_files $uri @proxy_to_app;

    location @proxy_to_app {
        # Typical proxy-pass stuff here

Shortcutting Known Bad Paths

Don't let your Django app handle paths you know are bad; let nginx offload that processing work for you! I was seeing plenty of accesses against .php files in my server access logs, so I wrote a quick block to reject them:

server {
    location ~* \.php$ {
        return 404;

Enabling Strict-Transport Security

Until recently, I didn't even know that HTTP Strict Transport Security was a thing, but it turns out to be pretty easy to implement. Once you have your SSL stuff ironed out and working, you simply need to enable the appropriate header:

add_header Strict-Transport-Security "max-age=5184000; includeSubDomains";

Blue LEDs

Mar 20, 2024

I really enjoyed this video detailing the surprising history of blue LEDs (and why they were so difficult to produce).

A 2018 article on how Joel Spolsky broke IT recruiting randomly popped up in my news feed today. I particularly enjoyed the section that discussed IT recruiting, though the bits relating to StackOverflow haven't aged well; that site is beyond having jumped the shark.

I'm not a people manager, but I have assisted in hiring a number of folks over the years. Some of them turned out to be great developers, while others turned out to be terrible. It's so hard to vet folks based on a handful of interviews. Back when in-person interviews were more of a thing, it was easier. Today's online-only world makes that nearly impossible, especially given that some candidates are no longer local.

Having an actual apprenticeship program would make the process so much easier. New candidates could come aboard for a trial period; 6 or 12 months. Financially, it would be treated like a contract position. During this period they could then be evaluated over time:

  • Is the candidate reliable?
  • Are their development skills what they said they were?
  • Can they communicate effectively?
  • Do they complete deliverables on time?

After the apprenticeship is up, they'd become a "journeyman" and a full-time employee. A program of this sort would greatly reduce the ridiculous speed bump some employers put in the way of hiring developers. It would also make it really easy to get rid of the downright awful developers who occasionally creep in.

This really needs to become a thing.

New Backend

Mar 18, 2024

This website is now running on a DreamCompute instance. One perk of this is that I can run the software stack of my choosing, rather than having to rely on whatever the shared stack is. One drawback, however, is that I'm now in total control; if there's a problem, I have to solve it.

Migrating to this has been fairly rough so far, but I got there in the end. I'll likely have another post in the near future detailing some of the headaches I ran into. There are still a ton of loose ends to tie up, but at least this site is back on the air. Let me know if you spot something broken. Note that I will likely pull down my photos subdomain, as I don't really keep it up to date, and it's not worth migrating in my opinion.

Online Radio Scanners

Mar 15, 2024

What a world we live in.

A few moments prior to my writing this, I heard fire engine sirens outside my house. Looking out my office window and through our neighbor's trees, I could see multiple units responding to a house across the nearest main street. I wondered to myself: "Could I determine what problem they were responding to?" A quick google for "Raleigh fire scanner" returned several hits, one of which was OpenMHz. I pulled up the site, saw that I could filter on "EMS Fire Alerts Only," and selected that option.

Clicking backwards through the playlist, I found the corresponding entry: a gas leak! Cross referencing the reported address, I verified that the house was indeed near our neighborhood. Success!

It's amazing what is possible through the internet. As an aside, this website appears to be a really neat way to listen to what's going on in the area. I can listen to the airport (there was a reported ash-can fire this morning), Raleigh police, or a general overview of all channels (a multiple-vehicle crash occurred a few minutes ago down near Fuquay Varina). Other filters are also available; pretty neat!

Django Web Hosting?

Feb 2, 2024

The web host I've used since the inception of this site will be dropping support for Python (and by extension Django) based applications at the end of March. As such, I'm wondering what alternatives exist. Is anyone familiar with a host that supports Python / Django applications? I'd appreciate any suggestions.

Python dict to tuple

Feb 1, 2024

I had no idea Python could do this, but Ruff taught me something new this morning. Suppose I want to transform a dict into a set of tuples. You can do it in a single line, with no comprehension required!

mydict = {
    'a': 123,
    'b': 456,
    'c': 789,
myset = set(mydict.items())
# myset is now: {('a', 123), ('b', 456), ('c', 789)}

I love stumbling upon little hidden gems like this!

High Quality Work

Dec 31, 2023

The Essential Craftsman channel on YouTube occasionally has great philosophical videos (I've featured a few here on the site before). Today's video on "The Mindset of Doing High Quality Work" is worth a watch. In the new year, one of my professional goals will be to improve the quality of my work. In recent times, I've become much more lax in testing the code that I write. This has led to bone-headed bugs that ended up needing a hot-fix, often something that I could have caught had I spent some time testing.

One of the comments in this video really resonated with me: "Strive for perfection, settle for excellence." I think that will be my mantra in 2024 for my professional work.

Classical Meets Rock

Oct 26, 2023

Alice Cooper's My God, a track from the Lace and Whiskey album, is unlike any rock and roll track you've heard. I stumbled upon it while listening to the album for the first time this week, and it's been stuck in my head ever since. Its lyrics are intriguing, especially given that this was recorded during an increasingly troubling time in Cooper's life. Good stuff.

Pretty cool video on the complexity of one of the greatest songs in Sesame Street. It's amazing just how intricate the musicianship is in this!


Sep 18, 2023

The latest Smarter Every Day video is engineering on an entirely different level. The math, software, and other challenges this company face are tremendously complex. It's amazing that people get to do this for a living! I bet we hear a lot more about this technology in the future; this seems like a promising area of development and manufacturing.

Here's another thing that linting taught me recently: Python's str.startswith() method, and str.endswith() as well, takes a tuple as the first parameter! This makes checking for multiple options really simple:

# Verbose way of writing it
if (mystring.startswith('c.') or mystring.startswith('m.') or mystring.startswith('s.')):

# Easier way
if (mystring.startswith(('c.', 'm.', 's.'))):

I didn't realize the language allowed this!

As I mentioned previously, I'm now using Ruff to lint my Python projects. Several linter warnings continually crop up in my code, which I find interesting, so I thought I'd highlight a few of them (there are plenty that I'm leaving out; I apparently write fairly crude code by these linters' standards).

This is a common recurrence in places where I'm setting up a dict for something:

mydict = {
    'posts': some_queryset.all(),
    'today':  # Missing a trailing comma

This pops up on occasion for me, though not terribly often. I apparently easily forget about the not operator.

return False if self.errors else True

# The above is a little more legible if we use:
return not self.errors

I was surprised that this occurred so often in my code. Removing these cases flattens the code somewhat, which is a new practice I'm trying to engrain into my programming habits.

if (is_ajax_request(request)):
    return HttpResponseForbidden('Forbidden')
else:  # This isn't needed
    return redirect(reverse('home'))

# The above looks better as:
if (is_ajax_request(request)):
    return HttpResponseForbidden('Forbidden')

return redirect(reverse('home'))

This warning taught me something I didn't know about f-strings; namely that explicit conversion flags are available. Also that the conversions I was making were mostly not necessary in the first place.

error = f"Part not owned by {str(self.part_owner)}!"

# Better:
error = f"Part not owned by {self.part_owner!s}!."

# Best:
error = f"Part not owned by {self.part_owner}!."

Again, I was surprised by how often I do this. Base types can (and often are) subclassed, so it's better to use isinstance() than type is.

if (type(loader) is list):
    return error_response(loader)

# Better:
if (isinstance(loader, list)):
    return error_response(loader)

Running the linting frameworks has taught me a fair amount about my programming habits, and has also informed me about various aspects of the language. I recommend running linters if you don't already, and I highly recommend Ruff!

Linting With Ruff

Sep 8, 2023

I enjoy using linting frameworks for the code I write, primarily employing flake8 for my Python code, which is about 90% of what I write these days. Recently, however, I saw news on Ruff, a new linting framework written in Rust that is orders of magnitude faster. It's so fast that the entire CPython repository, which contains over 1200 files, can be linted from scratch in only 0.29 seconds. Several testimonial quotes in Ruff's README attest to this blazing speed:

Ruff is so fast that sometimes I add an intentional bug in the code just to confirm it's actually running and checking the code. - Sebastián Ramírez, creator of FastAPI

Just switched my first project to Ruff. Only one downside so far: it's so fast I couldn't believe it was working till I intentionally introduced some errors. - Timothy Crosley, creator of isort

Another benefit on top of its speed is the near-parity it brings with Flake8, which is nice. There are still a number of formatting rules from the pycodestyle package that haven't been implemented, which is an annoyance, but there's an active issue tracking the progress on that front.

To top it all off, Ruff includes rules from dozens of Flake8 plugins, most of which I've never run. Enabling all of them in my projects has been humbling, to say the least, but I'm learning a ton of improved practices from doing so. I don't always agree with some of the rules, and have disabled a number of rule sets that annoy me, but it's been an interesting learning process.

In the coming days I'll be writing about a few of the sloppy practices that this framework has pointed out in my code, so stay tuned.

My wife made these the other night for a dinner with neighbors and they turned out terrific. In fact, they're likely better than the snickerdoodle recipe that I posted several years ago! The cookies in this recipe end up way softer, and are just as flavorful. Highly recommended! Note that this recipe was taken from an external website, which I'm transcribing here for both simplicity and posterity.

Makes 36 cookies in about 20 minutes.


  • 2-3/4 cups all-purpose flour
  • 2 tsp cream of tartar
  • 1 tsp baking soda
  • 1/2 tsp salt
  • 1 cup unsalted butter, just softened
  • 1-1/2 cups sugar
  • 2 eggs
  • 1 tsp vanilla extract


  • 1/3 cup sugar
  • 2 tbsp cinnamon


  1. Preheat oven to 350 degrees F.
  2. In a large bowl, mix together flour, cream of tartar, baking soda, and salt together. Set aside.
  3. In a stand mixer, cream together butter (barely softened) and sugar. Add eggs and vanilla and blend well.
  4. Add dry ingredients to wet ingredients and mix well.
  5. In a small bowl, combine remaining ⅓ cup sugar and 2 tablespoons cinnamon.
  6. Use a small cookie scoop to scoop out dough, roll into a ball and then roll into the cinnamon sugar mixture- twice.
  7. Place 2 inches apart on an ungreased cookie sheet.
  8. Bake for 8 – 10 minutes. Let sit on the cookie sheet for a few additional minutes before removing to a wire rack to cool.