I’ve been experimenting with woodworking joinery that can be cut flat (without vertical workholding) on a CNC. There are lots of examples of these joints online, and I don’t have any new ones to share, but I did model and CAM a bunch of them to compare relative glue surface area and internal voids. Maybe that information is helpful to somebody.

Overview

For these tests, I modeled two 90-degree boards, 8 inches wide by ¾" thick. These joints are primarily practical and non-decorative, and I’ve tried to keep the same or similar proportions for tongues, except the finger joints. Most lapped/hidden joints can be mitered with a chamfer bit for an additional toolchange, but this wont affect glue and void volume significantly.

The machining time is very subjective, but I’m estimating based on consumer grade (read: not stiff) CNC routers, using ¼" and ⅛" endmills. Times assume that boards are already planed to thickness and cut to length, and use a roughing pass followed by a lighter finishing pass.

For the roughing pass, I’m simulating with a 2-flute endmill, 3mm stepdown, 0.14mm feed per tooth at 18,000 RPM. For the ⅛" tool, where needed, I’m using 2mm stepdown, 0.1mm feed per tooth at 20,000 RPM.

For alignment, I’m counting the number of directions that the joint can’t move when assembled, where higher is (probably) better. A butt joint is held in place in one direction. An open-sided rabbet joint is held in place in two directions. A dovetail joint is held in place in five directions.

Butt joint

Image

The most basic joint is a simple butt joint and supplies the baseline for gluable area. In this case, 6 square inches. It provides no help in assembly, since the boards can slide past each other or slide sideways.

Glue surface area:
6 in²
CAD effort:
None/minimal
Machining time:
0
Tool changes:
0
Void volume:
0
Alignment directions:
1

Mitered joint

Image

It’s easier to cut this on a table saw, but could be cut in multiple passes with a chamfering tool on a CNC as well. It has no voids, and like the butt joint, does not help with assembly.

Glue surface area:
8.49 in²
CAD effort:
Minimal
Machining time:
0
Tool changes:
0
Void volume:
0
Alignment directions:
1

Stepped “miter” joint

Image

This joint is constructed with repeated, successively deeper steps along the joint edge. It has no voids, improves the glue surface area, and helps a little with assembly depending on step size.

Glue surface area:
11.4 in²
CAD effort:
Medium
Machining time:
5m56s
Tool changes:
0
Void volume:
0
Alignment directions:
2

Rabbet joint

Image

This joint is another one that can easily be cut on the table saw or router table. It provides a little more glue surface, and a little help with assembly.

Glue surface area:
9 in²
CAD effort:
Minimal
Machining time:
2m11s
Tool changes:
0
Void volume:
0
Alignment directions:
2

Tongue housing joint

Image

This joint too can be cut on a table saw with a dado stack, or on a router table, but is also easy with a CNC. It does not prevent sliding but does otherwise lock the pieces together, and provides twice the glue surface of a butt joint. To prevent sliding and improve alignment, the tongue and groove can stop before the board edge and appropriately dogboned.

Glue surface area:
12 in²
CAD effort:
Minimal
Machining time:
2m26s
Tool changes:
0
Void volume:
0
Alignment directions:
3

Hammer tenon

Image

The hammer tenon can be cut flat and provides axial and radial assembly help. It’s a bit more complicated to design, and requires adding dogbones, which creates some void spaces in the joint. Alignment and surface contact can be improved by using a smaller tool to reduce the dogbone size.

Glue surface area:
6.43 in²
CAD effort:
High
Machining time:
6m13s
Tool changes:
0
Void volume:
0.367 in³
Alignment directions:
5

Finger tenons

Image

This design uses thin tenons about the width of the router bit, so a single dogbone suffices on each slot.

Glue surface area:
16.31 in²
CAD effort:
Medium
Machining time:
4m19s
Tool changes:
0
Void volume:
0.53 in³
Alignment directions:
3

Blind finger tenons

This is functionally the same as the finger tenon design but retains ⅛" of wood on the outside of the corner to hide the joint, though the dogbones are still visible from the inside. This also increases the glue area and reduces the internal voids a little.

Glue surface area:
17.02 in²
CAD effort:
Medium
Machining time:
3m21s
Tool changes:
0
Void volume:
0.45 in³
Alignment directions:
4

Mortise and through tenon

Image

This uses half-thickness tenons that pass through the opposite board, and requires dogbone relief cuts on both the mortises and tenons. Glue surface area is not great, but it provides good assembly support.

Glue surface area:
7.81 in²
CAD effort:
Medium
Machining time:
8m55s
Tool changes:
0
Void volume:
0.27 in³
Alignment directions:
3

Mortise and hidden tenon

The tenons for this joint stop ⅛" of an inch before exiting the opposite board, and uses parallel dogbones to hide the joint from both the inside and outside of the corner, and also increases the glue surface. These dogbones allow the joint to slide a bit though.

Glue surface area:
10.29 in²
CAD effort:
Medium
Machining time:
8m59s
Tool changes:
0
Void volume:
0.27 in³
Alignment directions:
3

Box joints

The following box joints are all similar but use different methods for relieving the cutter radius in inside corners, resulting in differences in void space, gluing area, CAD effort and manufacturing time.

Basic box joint

Image

This box joint use basic ¼" dogbones for inside corners. It provides good assembly support, but lower glue surface area due to the large dogbones cutouts.

Glue surface area:
6.44 in²
CAD effort:
Low
Machining time:
4m47s
Tool changes:
0
Void volume:
0.09 in³
Alignment directions:
4

Box joint with smaller dogbones

Image

By using a smaller tool for the dogbones, the glue surface can be increased, and the voids decreased at the cost of machining time and one tool change.

Glue surface area:
8.14 in²
CAD effort:
Low
Machining time:
5m24s
Tool changes:
1
Void volume:
0.02 in³
Alignment directions:
4

Box joint with smaller perpendicular dogbones

Image

This joint uses dogbones that are perpendicular to the ends of the tenons, which will hide them from the inside of the joint (but still visible on the outside). This increases the overall void volume, and decreases glue surface below that of the original butt joint.

Glue surface area:
8.66 in²
CAD effort:
Low
Machining time:
6m14s
Tool changes:
1
Void volume:
0.06 in³
Alignment directions:
4

Box joint with rabbets

Instead of dogbones, this design uses rabbets at the corners of the tenons. It takes more work in CAD, as the box tenons need to be created in addition to the rabbets.

Credit goes to mosquitomade for this method.

Glue surface area:
7.56 in²
CAD effort:
High
Machining time:
5m43s
Tool changes:
0
Void volume:
0.13 in³
Alignment directions:
4

Box joint with small rabbets

With a tool change, the same design can reduce the rabbet size to moderately increase glue surface, and decrease void volume.

Glue surface area:
8.69 in²
CAD effort:
High
Machining time:
5m22s
Tool changes:
1
Void volume:
0.03 in³
Alignment directions:
4

Box joint with rabbets and hidden tenons

By adding ⅛" of material on the outside corner of both boards, the joint can be hidden from the inside out, and the glue area can be increased significantly.

Glue surface area:
11.46 in²
CAD effort:
High
Machining time:
5m01s
Tool changes:
0
Void volume:
0.11 in³
Alignment directions:
4

Box joint with small rabbets and hidden tenons

This joint uses a smaller ⅛" tool for inside corners and the rabbets to increase glue surface and decrease void volume, at the cost of manufacturing time and a tool change

Glue surface area:
12.62 in²
CAD effort:
High
Machining time:
4m49s
Tool changes:
1
Void volume:
0.03 in³
Alignment directions:
4

Box joint with half rounded tenons

This joint uses contoured, half rounded tenons to compensate for the inside radius on the opposite side. It requires significantly more machining time since the tenon edges must be 3d surfaced, but has good glue contact area and low void volume.

Glue surface area:
8.89 in²
CAD effort:
Moderate
Machining time:
7m37s
Tool changes:
0
Void volume:
0.04 in³
Alignment directions:
4

Box joint with smaller rounding on tenons

Image

This joint uses the same rounded tenons but uses a ⅛" tool to reduce the void area and tighten the inside corners.

Glue surface area:
9.54 in²
CAD effort:
Moderate
Machining time:
6m41s
Tool changes:
0
Void volume:
0.01 in³
Alignment directions:
4

Summary

For my needs, I’ll probably be using a mix of the tongue housing joint for quick, easy cadding and glue area, and using the rabbeted box joint or with hidden tenons if I want more self alignment.

Name Glue Surface Cad effort Tool Changes Machining time Void volume Align
Butt 6 in² None 0 0 0 1
Mitered 8.49 in² Min 0 0 0 1
Stepped 11.4 in² Med 0 5m56s 0 2
Rabbet 9 in² Min 0 2m11s 0 2
Tongue 12 in² Min 0 2m26s 0 3
Hammer 6.43 in² High 0 6m13s 0.38 in³ 5
Finger 16.31 in² Med 0 4m19s 0.53 in³ 3
Blind finger 17.02 in² Med 0 3m21s 0.45 in³ 4
Mortise 7.81 in² Med 0 8m55s 0.27 in³ 3
Blind mortise 10.29 in² Med 0 8m59s 0.27 in³ 3
Box 6.44 in² Low 0 4m47s 0.09 in³ 4
Box small dogbone 8.14 in² Low 1 5m24s 0.02 in³ 4
Perp dogbone 8.66 in² Low 1 6m14s 0.06 in³ 4
Rabbet box 7.56 in² High 0 5m43s 0.13 in³ 4
Small rabbet box 8.69 in² High 1 5m22s 0.03 in³ 4
Hidden rabbet box 11.46 in² High 0 5m01s 0.11 in³ 4
Small hidden rabbet box 12.62 in² High 1 4m49s 0.03 in³ 4
Rounded box 8.89 in² Med 0 7m37s 0.04 in³ 4
Small rounded box 9.54 in² Med 1 6m41s 0.01 in³ 4

Karakeep setup with Dokku

I’m in the process of setting up karakeep (formerly Hoarder) to replace Pinry, and am once again installing it on a server running dokku.

This is convenient for my own projects using common service dependencies, but is proving increasingly complicated with newer projects.

Anyway, here’s what I needed to do to get karakeep running in dokku:

# Create apps
dokku apps:create headless-shell
dokku apps:create karakeep

# Create a network to share between apps
dokku network:create karakeep-services

# Attach karakeep and chrome to the same network
dokku network:set headless-shell attach-post-create karakeep-services
dokku network:set karakeep attach-post-create karakeep-services

# Create and link karakeep service
dokku meilisearch:create kara-search
dokku meilisearch:link kara-search karakeep

# Create headless browser app manually
dokku config:set headless-shell DOKKU_DOCKERFILE_START_CMD=" --disable-gpu --disable-crash-reporter --no-crashpad --disable-dev-shm-usage --hide-scrollbars"
# disable headless-shell web proxy
dokku proxy:disable headless-shell
# Pull the headless shell image
dokku git:from-image headless-shell chromedp/headless-shell:137.0.7117.2

# Create storage for karakeep
dokku storage:mount karakeep /path/to/local/storage:/data
# karakeep uses its own init process, so disable dokku's
dokku scheduler-docker-local:set karakeep init-process false
# set port mapping manually (or https:443:3000 depending on setup)
dokku ports:add karakeep http:80:3000
# Set config values
dokku config:set karakeep DATA_DIR=/data NEXTAUTH_SECRET=<secret> BROWSER_WEB_URL=http://headless-shell.web:9222

# Use dokku config:get karakeep MEILSEARCH_URL to show the linked API key and URL
dokku config:set karakeep MEILI_ADDR=http://dokku-meilisearch-kara-search:7700
dokku config:set karakeep MEILI_MASTER_KEY=<api key from MEILSEARCH_URL above>

# Set the logout URL
dokku config:set karakeep NEXTAUTH_URL=http://$(dokku domains:report karakeep --domains-app-vhosts)
# Pull the karakeep image
dokku git:from-image karakeep ghcr.io/karakeep-app/karakeep:0.23.2

CNC touchplate update

I’m currently working on a number of updates to my CNC machine, including upgrading to a spindle, but the post about it was getting long, so I’m breaking it into smaller chunks.

The first is this update to the touch plate, as I’ve had a couple of issues with the current one.

The first is that the magnetic probe is kind of a pain to make, as it required soldering directly to a magnet, while moving quickly enough to minimize loss of magnetism.

The second and biggest one is that, for the wire to reach from the control box to the working area, it has to be long and unruly.

New probe

For the first issue, I’ve changed to a much easier to assemble design requiring:

  • a magnet with an m3 countersunk hole
  • an m3 flathead screw
  • a ring connector
  • an acorn nut to make it look fancy.

Now assembly just requires screwing the parts together and crimping or soldering the ring connector to the probe wire.

A magnetic probe with no wire attached
Assembled probe

The (temporary) screw shown here has a black oxide coating making it non-conductive. This will be replaced with a steel screw before use.

New wiring

For the wire issue, I’ve replaced the bulk of the wire with a relatively long (6 foot) retractable audio cable that I found, and spliced that to a short length of flexible wire for the probe and touch plate.

The flat audio cable in the cord retractor uses relatively fragile, enamel coated wire, which requires burning the coating off before it car be soldered. I designed and printed a cable strain relief for this end to help keep them stable when pulling the cable around.

A small (40mm) black plastic 3d-printed housing and lid, with interlocking pegs to keep the lid in place when closed. The input side of the housing has a thin serpentine channel containing a flat audio cable, the other output side has two wider channels for individual stranded wires. In between the housing has two larger separated channels where the input and output wires have been individually soldered together.
Strain relief with soldered connections

Another way to solve the wire issue would be to run a probe wire and convenient output plug through the drag chain to the spindle. Then, I’d only ever need a relatively short wire to reach the workpiece.

A cable retractor connected to a black box on one side, and a magnetic probe and touch plate on the other, with a black strain relief between them.
The finished product

I also made a new touchplate holder that uses a bolt to give the probe somewhere to stick when not in use. You can find it, and the original probe and mount design here: https://www.printables.com/model/30072-millright-mega-v-touch-plate-holder-and-magnetic-p

A black plastic shell that the bottom of the touch plate slides into. The probe attaches magnetically to a bolt passing through the housing
3d printed touchplate holder

I’ve shared this project in a few places, but like many other projects, I’ve neglected to add it to this blog.

I’m currently using CNCjs with my GRBL-based CNC, and while there are some really nice pendants out there for 32-bit grblHAL boards, the options for physical devices and interfaces for older 8 bit boards like mine, with CNCjs, are very limited.

Previous to this, I was using another pendant I put together, cncjs-pendant-keyboardreader, using a wireless keyboard.

This supported smooth jogging, and a handful of macros and preset actions, but with no visual feedback, it was difficult to remember all of the commands and shortcuts.

I found that if I hadn’t used the machine in a while, I completely forgot which keys did what. Was it control or alt for faster jogging? How do you dismiss notifications or unpause a job? And so on.

I set out to create a more usable web interface for touch devices, with the intention of also supporting a physical Streamdeck.

The Streamdeck is interesting. At its heart, it’s a single, self-contained LCD touchscreen, with a clever approach to physical buttons. The buttons themselves are transparent, with a membrane that runs along the outside edge of the button to activate the screen when pressed, giving the illusion of multiple discrete displays.

Being self contained, you can send images in jpeg format to the device to populate the display, instead of treating it as an external monitor. This is great for my use, since I can continue to use a headless Raspberry Pi for performance.

The result is the (creatively named) cncjs-pendant-streamdeck, an obsessively configurable frontend for CNCjs with these goals in mind. The included configuration is for a 3x5 Streamdeck, but it supports both the mini (2x3) and XL (4x8). I have not tested it with newer devices with secondary display areas, since I don’t own one.

Streamdeck running cncjs-pendant-streamdeck
Streamdeck running cncjs-pendant-streamdeck

Out of the box, it supports:

  • Multi-axis smooth or incremental jogging. The web interface supports multitouch, so two (or more) jog direction buttons can be used at the same time.
  • Multiple pages
  • Templated text to display CNC state, like current position
  • Conditional button display/disabling
  • Custom images/colors
  • Execute macros (ex: for probing) and cncjs custom commands
  • Execute actions on press, release, or button hold
  • Display and manage alarms, holds, and pause events
  • Job selection from the CNCjs watch folder
  • Numeric input
  • (Animated) gcode rendering and thumbnails

It can be used with or without a physical Streamdeck, with nice, large buttons for use on a phone or tablet.

Web view of the cncjs-pendant-streamdeck interface
Web view of the cncjs-pendant-streamdeck interface

From a development perspective, the project is built as a Vue application, with a separate nodejs renderering pipeline for the Streamdeck output, and a handful of adapters to abstract the differences between the Node and Web display. This allows all of the business logic (and configuration) to be shared between the two display types.

Ideally, I’d love to build a visual configurator to generate the config file, but honestly there hasn’t really been enough interest to warrant the effort.

That said, the project is extensively documented, and I’ve published the cncjs-pendant-streamdeck-validator package, which can be used from the commandline, in a javascript project, or you can use the schema directly, so there should be enough there for somebody to build that tool if desired.


I installed Home Assistant with Dokku. It was very easy, but there were a couple of gotchas that slowed it down a bit.

Setup

Create a new homeassistant app in Dokku

dokku apps:create homeassistant

Set the timezone

dokku config:set homeassistant TZ=America/Chicago

Create a folder that will be used for Home Assistant configuration

mkdir /path/to/my-config
dokku storage:mount homeassistant /path/to/my-config:/config

Home Assistant uses its own init procedure, and we need to disable automatic init

dokku scheduler-docker-local:set homeassistant init-process false

Initialize Home Assistant app from its docker image

dokku git:from-image homeassistant ghcr.io/home-assistant/home-assistant:stable

Dokku didn’t correctly configure the correct port for the container, so:

Remove the default proxy port if needed:

dokku proxy:ports-remove homeassistant http:80:5000

Add the correct proxy port:

dokku proxy:ports-add homeassistant http:80:8123

To enable autodiscovery, Home Assistant needs to be connected to the host network. Other than autodiscovery, I had no issues using the default network and normal port mapping.

You can enable this option in dokku with

dokku docker-options:add homeassistant deploy,run "--network=host"

Once Home Assistant is on the host network, dokku’s zero-downtime restarts will fail when updating or restarting the service, because the bound port will already be in use.

Zero downtime restarting can be disabled with

dokku checks:disable homeassistant

Updating Home Assistant

To update Home Assistant, use git:from-image again, pointing to the newest SHA digest for your architecture (instead of using the :stable tag)

dokku git:from-image homeassistant ghcr.io/home-assistant/home-assistant@sha256:<shadigest>

You can get this digest from docker hub for your architecture: docker hub, or you can pull the image locally and use:

docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/home-assistant/home-assistant:stable

My favorite automations (so far) turn my printer’s light off automatically after a print completes, and lets me know to open windows when the weather is cooler and less humid outside, and air quality is ok.