Open-Source Thermal Simulation of PCBs

Simulation has always been the dream of PCB design. In most cases, it hasn’t been practical – Either in the domain of expensive or esoteric tools that are compute-cycle hungry and limited in scope. But we’re at a turning point.

This is the process for current carrying / voltage potential simulations with KiCad and ElmerFEM.

 

Inspired mostly by some tweets by Daniel Giesbrecht, whereupon I picked his brain a bit.

https://x.com/DanSGiesbrecht/status/1787223477977837972

https://x.com/DanSGiesbrecht/status/1810312545242378332

 

There’s also this two-hour long youtube video by Lukas Henkel that goes over the exact procedure I’ll be describing. Coincidentally, he also just did a cool podcast episode of The Amp Hour.

This is an abbreviated set of notes, mostly for myself, but it’s possible that others will benefit, so I’m hitting the “publish” button.

 

I’m doing this test on the large comb-like trace in the centre of this PCB. The purpose doesn’t matter, and the test parameters are fake.

 

The process is a bit tedious, and uses four different software packages. But it’s already way better than it was, even last year. KiCad’s new export formats have eliminated the need for a whole other software package.

I’ll be using Windows for this, but other than different download links, I believe this process is fully cross compatible.

Let’s begin.

Software number one:

KiCad.

Newer version of KiCad (Currently nightlies 8.99, but likely mainlined starting KiCad 9) allow exporting in more formats than ever before. XAO is recommended for usage in Salome.

  • Download KiCad 8.99 development, as of this writing.
  • Open up the PCB file
  • Go to File->Export->Step / GLB / BREP / XAO / etc
  • Change the top left box to XAO
  • Initially, use simple settings to test process, and not test your CPU
  • Uncheck “Export board body / components”
  • Export tracks, vias, pads, zones
  • Fuse shapes
  • Use the Net Filter box to only export your copper of interest (again, for now)
  • Choose a filename and hit export

Software number two. Download Salome:

  • In Salome, go to geometry workbench and import XAO
  • Unhide geometry
  • Right click geometry in tree->Create group
  • Change it to flat plane, name in “input”, select input faces, add, apply
  • Same for outputs
  • Important for later: Click on output in the sidebar, then at the top, Inspection->Basic Properties. Take note of the Surface. In my case, 1.168319. This is in mm^2
  • Save
  • Go to mesh workbench
  • Create mesh, 1D-2D-3D strategy, add hypothesis
  • Max and min need to be played with, in millimetres – 02 / 0.08 worked well. Defaults were okay for me, but too fine.
  • Apply out of that, right click mesh->Calculate
  • right click the mesh in the sidebar, export to UNV

You’re done in Salome!

Onto software number three, ElmerFEM (GUI, No MPI).

  • In Elmer, file->open the UNV
  • Equation->add static current conduction
  • IF THIS ISN’T HERE
  • File->Definitions->Append
  • edf/statcurrent.xml
  • Close out of that, add new equation
  • In static current conduction:
  • Active, Apply to Body Property 1
  • Click “Edit Solver Settings”
  • Solver Specific Options->Calculate Joule Heating and Calculate Volume Current
  • Apply, Update, and OK out of that menu
  • Material->Add->Static Current Conduction
  • Select copper (generic) from material library
  • Change electric conductivity to 47.00e6, Apply to body
  • Boundary condition->Add
  • Call it “Input” at the bottom
  • Static current conduction->Potential: 12V? Whatever
  • Hit new, name it Output, Static current conduction->Current density
  • It is in A/m^2 – grab the previously calced surface area (1.168319mm^2), convert to m^2 -> 1.168319e-6
  • Divide 0.5A / 1.168319e-6 = 427965
  • Current flows OUT, therefore = -427965
  • OK out of both windows
  • Find the boundary conditions (input/output) on the sidebar in Geometry->Boundary. Double click, set appropriate input or output for both.
  • Save
  • Sif->Generate. Sif->View log is also the main sim input settings, useful for debugging.
  • Run->Start Solver
  • If the solver doesn’t converge to near-zero (probably), then:
  • In the sidebar, open up the Equation, then Edit Solver Settings
  • Iterative->BiCGStabl
  • Convergence Tolerance->1.0e-8
  • Preconditioning->ILU2
  • Hit Update, generate Sif, check the Sif file if desired
  • Start solver
  • If it converges to approximately zero (like, 1x10e-18), then it was successful! A .vtu file should have been saved.

At this point, the solver should complete. In my case, I first used the built-in ElmerVTK to view the output.

Who knew? Probably shouldn’t slam 500mA through a single via.

 

I recommend playing around with ElmerVTK within ElmerGUI for a bit. It wasn’t too hard to get it to display heatmaps based on my solver parameters. The results weren’t incredibly aesthetic, but they were simple to get results from.

 

But then if you want something prettier, move onto software number four: ParaView. Also no MPI.

The settings are mostly the same as ElmerVTK, just more complicated. Open up the .vtu file, find the same settings, and admire.

 

Future steps:

I have not done any research on what the preconditioning parameter above is, and that seems like an important topic to investigate.

This was originally done with an eye to creating a one-click KiCad plugin, but considering the multitude of softwares and settings used, there isn’t an obvious avenue to attack to lower the barrier to entry on this. This will remain in the back of my mind.

I may attempt to replace the Salome meshing steps with Gmsh. Gmsh is more lightweight, and should be able to do everything with the command-line, which would help with automation.

There are also a few ElmerFEM solvers that merit more investigation. Electrostatics, RF, and thermal solvers are all very relevant, depending on the project.

Adding in all of the other traces, signals, FR4, and embedding it in atmosphere is the very next step, for better thermal simulation.

1 Pound Combat Robot

This post is about three years late, but I’m catching up.

 

Combat robots like Battlebots are seeing something of a revival right now.

The ones on the Battlebots show are 250lbs, and cost tens of thousands of dollars – most of that in batteries.

Much more accessibly, there are a bunch of regional competitions, at a variety of weight classes. 3 and 30 pounds are the most popular, as a trade-off between expense, challenge, and excitement (heavier has more of the latter, at the cost of the first two).

Someone local to me was trying to start a one pound league, so I took the opportunity to build one as a way to dip my toes in.

Unlike my previous hacky RC car, the roadmap for the electronics is a lot better defined.

 

A wedgebot (that is, just a driving platform, no active weapon) consists mostly of these components:

  • Wheels
  • Wheel motors
  • Chassis
  • Power switch
  • ESC
  • Receiver / RX
  • Battery
  • Transmitter / Controller

All of these are easy commodity parts.

I 3D printed the chassis and then the internal frame to hold everything together – PETG for most of it, and then some soft rubbery TPU shockmounts to attach the internal and external components, ideally kinda cushioning the electronics. Similarly the motors are held on with some thin PETG to allow flex to absorb shocks.

 

 

The motors are 800 RPM DC motors. That seemed like a good compromise on speed vs. control with this size of wheel.

The wheels are 45mm rubber/foam wheels.

The ESC is a 5A board, that can take a forward/direction control, and turn it into left/right tank steering. Originally I had an ESC designed in for each motor, but the ones I specced out didn’t work with the tank steering setup. This one nicely controlled both motors, after translating the two data channels into what I needed.

This was critical because my controller is a Spektrum DX3s, which only has forward/back and left/right channels. The controller also came with a compatible receiver.

The battery is a tiny 550mAh thing.

There are two philosophies on power input from the battery. Some people run power from the battery-positive to a pin on a connector, with the rest of the system connected to the connector’s other pin. This allows the whole system to be powered up by plugging in the mating connector with both pins soldered to each other. This is cheap and easy, but it’s pretty common to see the link fly off during fights.

The other solution is a custom power switch that a few industry people sell. It seemed rather expensive for what it is, so I made my own with a copper bolt and copper plate and 3D printed PLA.

 

Anyway, shortly after I finished it, the prospective event organiser of the 1lb league had to skip town, so this has just been lying in a box since then. That’s a pity, it would be fun to smash it up. I would undoubtedly learn a lot.

It’s pretty zippy.

 

Adding a hide button to Facebook Marketplace with Tampermonkey

Tampermonkey, the successor to Greasemonkey, allows on-the-fly modifications to webpages. Things like adding buttons, or modifying text on a page.

 

Facebook Marketplace is fast becoming the only viable used item listing in my area. Which is annoying – It’s inferior to Craigslist in any way, but the latter is clearly dying a slow death.

The best listing site seems to be regionally dependant, but here we are.

 

Instead of just griping about the situation, I’ll fix the single lacking feature that improves my life the most. The “hide” button that CL has.

 

So here is a Tampermonkey script, written in JS/jQuery. It’s my first real dabble in jQuery, and there is a little bit of magic that certainly makes DOM manipulation much easier.


// ==UserScript==
// @name FBMP Hider
// @namespace http://tampermonkey.net/
// @version 2024-09-08
// @description Add a button to hide FB MP listings
// @author Jarrett Rainier
// @match https://www.facebook.com/marketplace/*
// @require https://code.jquery.com/jquery-3.2.1.min.js
// @require https://gist.github.com/raw/2625891/waitForKeyElements.js
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
function addHidden(id) {
console.log("Adding " + id + " to the hidden list");
var timestamp = Date.now();
GM_setValue(id.toString(),timestamp);
}
function getHidden(id) {
//console.log("Checking " + id + " on the hidden list");
var store = GM_getValue(id.toString(), null);
if (store != null) {
var timestamp = Date.now();
//Reset the clock on keeping the listing hidden
GM_setValue(id.toString(),timestamp);
}
return store
}
function getMPId(url) {
var ids = url.split('/');
//console.log(url.split('/')[3]);
return ids[3];
}
function hideMPClass(jNode, id) {
if (id != null) {
console.log("Hiding " + id);
jNode.parent().parent().parent().parent().parent().parent().parent().parent().hide()
} else {
//console.log("Didn't hide " + id);
//console.log(jNode);
}
}
function insertMPHideButton(jNode, id) {
var insertA = document.createElement('input');
//insertA.href = '#';
insertA.setAttribute('type',"button");
insertA.setAttribute('value',"hide");
insertA.setAttribute('id',"hide" + id);
insertA.innerHTML = "Hide";
insertA.addEventListener("click", function(event) { event.stopPropagation(); addHidden(id); hideMPClass(jNode, id); }, true);
jNode.parent().append(insertA);
var href = jNode.attr('href');
var newHTML = jNode.children().eq(0).html().replace("div", "a href='" + href + "' ");
var n = newHTML.lastIndexOf("div");
newHTML = newHTML.slice(0, n) + newHTML.slice(n).replace("div", "a");
jNode.attr('href', '');
newHTML = jNode.parent().html().replace("a", "div");
n = newHTML.lastIndexOf("a");
newHTML = newHTML.slice(0, n) + newHTML.slice(n).replace("a", "div");
}
function checkMPclass (jNode) {
//xlil10hfl
//console.log (jNode);
var id = getMPId(jNode.attr("href"));
if (getHidden(id) != null) {
hideMPClass(jNode, id);
} else {
insertMPHideButton(jNode, id);
}
}
(function() {
console.log ("Do you see this?");
//waitForKeyElements (".xjp7ctv > div > span > div > div > a", checkMPclass);
waitForKeyElements (".xjp7ctv > div > span > div > div > div > div > a", checkMPclass);
console.log ("Script run to completion");
})();

view raw

fbmp.js

hosted with ❤ by GitHub

Click on “view raw” and Tampermonkey should prompt for installation.

It’s already broken once due to Facebook changing their DOM around, but that’s over nearly a year of usage. If it happens repeatedly, I’m sure I can find a more resilient method.

 

 

Very Remote Control

Instead of a TV at home, I use a projector. It’s on my ceiling, with the buttons inaccessible. The remote for it also isn’t really working anymore. Problem.

There’s nothing on the remote control’s PCB except for an obscure microcontroller (TTR013), IR LED and driving transistors, and some carbon contacts for the buttons. The intermittent operation is not the contacts deteriorating, and the microcontroller is getting power, so troubleshooting opportunities are limited.

 

But, the internet is a wonderful place, and someone has just straight up recorded the exact remote control I need and posted it up on Github.

The config file is here, but they also have the raw recordings. Apparently it’s a config file for a Linux IR transmitter driver called LIRC. The config documentation is here.

 

It’s pretty straightforward, so I set out to duplicate the waveform using the RMT peripheral of the ESP32. The RMT peripheral is an arbitrary waveform generator, and one of the typical use-cases is an IR transmitter. Perfect.

My final code is here. The From my reading of the LIRC docs, the relevant config data is this:

 

header 9077 4504
one 602 1622
zero 602 511
ptrail 604
gap 108167

begin codes
ON 0x000CF20D

 

Where a binary 1 is denoted by an on-pulse of 602 microseconds and then an off-pulse of 1622 microseconds. Similarly for 0, it’s 602-on and 511-off.

The code for ON uses that sequence to write out 0x000CF20D.

 

The whole packet starts with the header sequence (9077-on, 4504-off) then 0x000CF20D, then the tail/gap sequence (604-on, 108167-off). Seems straightforward.

 

I put the ESP32 on an oscilloscope to make sure the RMT was doing what I wanted it to do.

 

It seems to be! But this is more of a logic analyser task, so I pulled that out. The waveform looked exactly as I expected, so I tested it out with an IR LED.

 

 

And it didn’t work at all. Nuts.

By connecting the remote control to the logic analyser and powering it with a nice bench power supply, I was sometimes able to get output data. Enough to grab a capture after several minutes of mashing the carbon contacts with a brass standoff.

Here’s the full waveform:

It’s worth noting that the waveform is inverted from the actual LED current, due to measuring at the LED with low-side switching. Don’t worry about it too much, just invert the logic.

Sigrok can export the waveform in a Value Change Dump format, which looked good enough for my purposes. Then I wrote a python script to convert VCD files into the RMT packet format.

It looked good, with the exception of some glitches caused by the RMT not being able to handle very long delays, relative to the fast switching. This strategy still didn’t work on my project.

Speaking of fast switching, let’s investigate that further. Abandoning the Github config files so quickly didn’t sit quite right with me. It was too perfect.

Going back to my capture of the remote, here’s a binary 1:

 

Here’s a 0:

 

And then here’s the header, along with the data portion:

 

And the header with the whole data portion:

 

It does look right. But why was this so different from the Github repo?

Well, obviously this has a carrier wave that I totally blew by.

 

Wow, okay. Back to the LIRC documentation. There is a frequency option that specifies carrier wave, and defaults to 38kHz. So, it’s not in the config, because it’s set by default, and is one line in the documentation. No wonder I missed it.

Honestly, my brutish Python script that specifies all of changing values is probably good enough, but it feels wrong to use a 1500-line lookup table instead of a fixed carrier frequency and 15 lines of actual data. The RMT peripheral made it incredibly easy to fix up.

The end result looks really good, but let’s compare with the captured data again.

Keeping in mind that the data to be sent is 0x000CF20D, I’ve annotated the capture:

 

And it looks mostly good, except… What’s that block at the end?

I can’t find anything in the original config or documentation for the config that would explain that last block. That will remain a mystery for now.

 

Anyway, it was about this time that I got suspicious of the ancient IR LEDs that were in my parts bin. If I cranked the current, they looked visibly blue, which, obviously is the wrong side of the spectrum when I’m looking for IR LEDs. I grabbed a spectrometer and measured it – Yep, that’s not right.

After combing through my parts bin, I found another IR LED and measured it: 815nm. Still not right. I was fortunately able to juuust barely be able to measure the wavelength, though.

Looks like a very broad band 815nm, if the peak stretches all the way to 750nm.

I’m pretty sure I’m looking for a 940nm LED, so some more combing and I found an IR proximity sensor. I don’t have a datasheet, but pointing a TV remote at it triggered the on-board red LED, so I knew it was simpatico.

A quick hack-job later, and I drove the LED directly from the ESP32. My projector turned on, without any further changes to my code. And it successfully turned off the projector, too.

So that’s how I controlled my home entertainment setup with an IR proximity sensor.

 

Sometime in the middle of this journey, I hooked up the LA to a little 315MHz receiver module and triggered my garage door remote. I won’t be posting the waveforms here, but minor modifications to my Python script and code worked out of the box to clone the remote. That whole process took about 15 minutes, so it was a nice and useful diversion. Because it’s attached to an ESP32, I can now trigger my garage door over the internet. From anywhere in the world! Very, very remotely.

 

This whole project is very specific to my needs, but it could be helpful to others. There is now a pipeline for converting LIRC config files into ESP32 RMT outputs. I doubt I’ll ever do this again, but, just for giggles, here’s a GPT-assisted script that will do it:


import re
import argparse
class LIRCConfigParser:
def __init__(self, file_path):
self.file_path = file_path
self.config = {}
self.header = None
self.one = None
self.zero = None
self.ptrail = None
self.gap = None
self._parse_file()
def _parse_file(self):
with open(self.file_path, 'r') as file:
content = file.read()
self._parse_timing_parameters(content)
remote_blocks = re.findall(r'begin remote(.*?)end remote', content, re.DOTALL)
for block in remote_blocks:
remote_name = re.search(r'name\s+(\S+)', block)
if remote_name:
remote_name = remote_name.group(1)
self.config[remote_name] = self._parse_remote_block(block)
def _parse_timing_parameters(self, content):
header = re.search(r'header\s+(\d+)\s+(\d+)', content)
if header:
self.header = (int(header.group(1)), int(header.group(2)))
one = re.search(r'one\s+(\d+)\s+(\d+)', content)
if one:
self.one = (int(one.group(1)), int(one.group(2)))
zero = re.search(r'zero\s+(\d+)\s+(\d+)', content)
if zero:
self.zero = (int(zero.group(1)), int(zero.group(2)))
ptrail = re.search(r'ptrail\s+(\d+)', content)
if ptrail:
self.ptrail = int(ptrail.group(1))
gap = re.search(r'gap\s+(\d+)', content)
if gap:
self.gap = int(gap.group(1))
def _parse_remote_block(self, block):
remote_config = {}
lines = block.splitlines()
key_section = False
for line in lines:
line = line.strip()
if line.startswith('begin codes'):
key_section = True
remote_config['codes'] = {}
elif line.startswith('end codes'):
key_section = False
elif key_section:
parts = line.split()
if len(parts) == 2:
key, value = parts
remote_config['codes'][key] = value
else:
if ' ' in line:
key, value = line.split(None, 1)
remote_config[key] = value
return remote_config
def get_config(self):
return self.config
def get_remote_names(self):
return list(self.config.keys())
def get_remote(self, remote_name):
return self.config.get(remote_name, None)
c_start = '''
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/rmt.h"
#include "driver/gpio.h"
#include "esp_system.h"
#include "esp_log.h"
#define RMT_IO (GPIO_NUM_18)
#define ON_OFF_CONTROL (GPIO_NUM_19)
#define PIN_INDICATOR (GPIO_NUM_5)
static const char* TAG = "ir_transmitter";
#define RMT_END {{{ 0, 1, 0, 0 }}}
'''
c_end = '''static void send_on()
{
ESP_ERROR_CHECK(rmt_write_items(RMT_CHANNEL_0, on_key, sizeof(on_key) / sizeof(on_key[0]), true));
vTaskDelay(44/portTICK_RATE_MS);
ESP_ERROR_CHECK(rmt_write_items(RMT_CHANNEL_0, tail_key, sizeof(tail_key) / sizeof(tail_key[0]), true));
}
static void send_off()
{
//Off needs to be pressed twice
ESP_ERROR_CHECK(rmt_write_items(RMT_CHANNEL_0, off_key, sizeof(off_key) / sizeof(off_key[0]), true));
vTaskDelay(2000/portTICK_RATE_MS);
ESP_ERROR_CHECK(rmt_write_items(RMT_CHANNEL_0, off_key, sizeof(off_key) / sizeof(off_key[0]), true));
}
void app_main(void)
{
rmt_config_t config = RMT_DEFAULT_CONFIG_TX(RMT_IO, RMT_CHANNEL_0);
// set count to 1us
config.clk_div = 80;
config.tx_config.carrier_en = true;
config.tx_config.carrier_freq_hz = 38000;
config.tx_config.carrier_duty_percent = 35;
ESP_ERROR_CHECK(rmt_config(&config));
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, 0));
gpio_set_direction(ON_OFF_CONTROL, GPIO_MODE_INPUT);
gpio_set_pull_mode(ON_OFF_CONTROL, GPIO_PULLUP_ONLY);
gpio_set_direction(PIN_INDICATOR, GPIO_MODE_OUTPUT);
while (1) {
gpio_set_level(PIN_INDICATOR, 1);
if (gpio_get_level(ON_OFF_CONTROL) == 1) {
send_on();
} else {
send_off();
}
vTaskDelay(1);
gpio_set_level(PIN_INDICATOR, 0);
vTaskDelay(40/portTICK_RATE_MS);
}
}
'''
def generate_c_code(config, output_path, timings):
with open(output_path, 'w') as file:
file.write(c_start)
if timings.header and timings.one and timings.zero and timings.ptrail and timings.gap:
file.write(f'#define PACKET_HEADER {{{{ {timings.header[0]}, 1, {timings.header[1]}, 0 }}}}\n')
file.write(f'#define ONE_BIT {{{{ {timings.one[0]}, 1, {timings.one[1]}, 0 }}}}\n')
file.write(f'#define ZERO_BIT {{{{ {timings.zero[0]}, 1, {timings.zero[1]}, 0 }}}}\n')
file.write(f'#define PACKET_TAIL {{{{ {timings.ptrail}, 1, {timings.gap}, 0 }}}}\n\n')
file.write(f'#define PACKET_ZERO_NIBBLE ZERO_BIT,ZERO_BIT,ZERO_BIT,ZERO_BIT\n\n')
for remote_name, remote_config in config.items():
file.write(f"// Remote: {remote_name}\n")
if 'codes' in remote_config:
for key, value in remote_config['codes'].items():
file.write(f"//0x{value}\n")
file.write(f"static const rmt_item32_t {key.lower()}_key[] = {{\n")
file.write(f" PACKET_HEADER,\n")
for nibble in [value[i:i+4] for i in range(2, len(value), 4)]:
file.write(f" //{nibble}\n")
for bit in bin(int(nibble, 16))[2:].zfill(4):
if bit == '1':
file.write(f" ONE_BIT,\n")
else:
file.write(f" ZERO_BIT,\n")
file.write(f" PACKET_TAIL,\n")
file.write(f" RMT_END\n")
file.write(f"}};\n\n")
file.write(c_end)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Parse LIRC config files and generate a minimal C file.')
parser.add_argument('file_path', nargs='?', default='lircd.conf', help='Path to the LIRC config file')
parser.add_argument('–output', '-o', default='main.c', help='Output C file')
args = parser.parse_args()
lirc_parser = LIRCConfigParser(args.file_path)
config = lirc_parser.get_config()
generate_c_code(config, args.output, lirc_parser)
print(f"Generated C code has been written to {args.output}")

Run this with python esp32_convert_lirc_to_rmt.py config.conf --output main.c

It will generate a main.c file that should just compile and have the ESP32 happily spitting out valid data.

Key Storage

I’m on a home improvement kick. When I walking my home, there’s a long, featureless hallway with kitchen at the end of it. I was using a tray on my kitchen counter to toss keys onto when I walked in the door. This takes up valuable kitchen counterspace, and being at the end of the hallway, was not ideal.

 

I designed up a wall-mounted tray that looked kinda nice. Then I CNCed it out of a scrap chunk of pine. I did a bad job estimating the size, so it turned out way too small and not really usable.

 

Instead of just recreating it, but larger, I an additional feature to hold cards would be useful. Additionally, the CNCed wood required some hand sanding that I didn’t really want to deal with again, not to mention the CNC setup time.

None of that was particularly time consuming or difficult, but honestly, it was faster and less tedious to 3D print it and cast it in cement than to break out the CNC again.

With the larger size, I wasn’t so sure about just slotting it into a piece of plywood without supports, so I added a steel cable. It’s a nice design accent regardless.

Note the knotted up steel cable in the mould. I also used XTC-3D, a resin coating designed to smooth out 3D prints. It was expired and a little chunky, but still seemed to work well. I’d use it again. The resulting cement form was smooth and required no post-

processing.

I also designed a tensioning mechanism for the cable. There’s a captive screw that drags a nut up and down. The nut assembly holds the cable end. The whole assembly slides into a 3/8″ drill hole. At the top of the assembly, there is a hole that is sized just right for a screwdriver to come in and engage the screw, dragging the nut assembly closer, tightening the cable tension.

 

For the backing, I made a drill jig to be able to drill straight down the 3/4″ plywood without breaking through either side. The steel cable goes in, and then I attached it to the tensioning mechanism with a combination of melting the plastic to it, and some CA glue for good measure. The whole assembly then also got glued into the drill hole.

After all of that was set up, I used plumber’s putty epoxy to fill in all of the gaps between the tray and the plywood, and bolted it to the wall!

 

This project went just about as smoothly as possible, probably about 6 hours all told.

The Worst RC Car in the World

Quite some time ago now, I wanted to make some of remote control car. And, also, eventually turn it into an autonomous sumobot or similar.

This project was done a few years ago, but I’m writing it up now.

In the interest of getting something into my hands that I can iterate on, I did it as quickly as possible.

Yes, that is a structural q-tip. The cardboard was collapsing.

This crappiness is actually a feature – because I introduced this as a workshop to a bunch of people. Mostly beginners, with minimal exposure to electronics or microcontrollers.

Operation is simple. You program it in with your WiFi credentials, then when you turn it on, it creates a webpage that is just a joystick. That controls the car. It’s surprisingly satisfying.

The repo is here. It includes materials and code.

Everything in this sort of direction in the future will be a little bit more complicated or interesting, I just wanted to break the seal.

The fun in this project is quickly and cheaply building something simple, that can be controlled easily. No part of this is too hard to understand, and therefore, improve.

So with that in mind, I ran a few workshops to get other people started.

As a fun detour, I took the opportunity to play around with a vacuformer.

I modified a VW beetle model to remove the front fenders to better fit the single castor front wheel, then 3D printed the buck.

This is in parallel with designing an internal frame to hold all of the components together. For personal stuff, I usually use OnShape for my CAD, lately. The free version forces designs to be open source / publicly searchable, which is pretty great when banging together AliExpress modules. And also for the base VW model to modify into a tricycle.

Rock Planters Rock

I’ve been wanting to play around with concrete for a while, as a project material. I’m not sure what the end goal will be, but once I’m familiar with the material, I’ll be able to shoehorn it into other projects in unexpected ways.

Concrete (actually cement, as I don’t intend to add aggregate) is different from other casting materials like silicone or resin in the cost/quantity proposition. Silicones are quite expensive, often purchased in 1L volumes or so, while cement has the opposite problem. It’s cheap, but I’m stuck buying 40 pound bags of the stuff.

Initially, I found some geometric molds off the internet and started with that, along with a convenience store drink cup.

It was easy and worked quite well.

 

I used RapidSet cement. The box is blue. There are many different kinds, and apparently this is one of the lightest in colour, while being a little smoother than most.

To make these into decent planters, there are also two additional steps: soak it to get all the lye out, and then seal it so it doesn’t shed cement dust all over.

 

The sealant foams up all strangely. It’s a granite countertop sealer.

 

This was all just practice, so that I would have a feel for how it works before I put a ton of time into something cooler.

I wanted to cast a mountain range (or two!). I really like mountains.

I started with Mount Currie – A distinctive skyline feature near a town called Pemberton. Also as a learning exercise with Blender; I’d never used it before.

Here’s the view from a place I stay at sometimes:

 

I grabbed the geodata with TouchTerrain. Here are my settings:

 

Then in Blender, I set up the camera in the same-ish position and focal length of my camera.

 

Yeah, I’m very satisfied with that.

 

Next step, make the renderer export a depth map instead.

 

Then map it around a cylinder as another displacement map. The nice thing about this is that the Blender portion of the work, which I’m not very comfortable with, is done. So I’ve got it set up just right, and I don’t have to touch it – The rest of the fiddling on this model is with 2D depth maps in Krita, an open source Photoshop facsimile.

I faked the sides of the mountain range a bit, because they don’t just drop off to nothing.

 

When I got it how I liked, I 3D printed it to get a feel for how it looks in the real world. A few back-and-forths with that, and I built a mould around the positive model in Blender.

 

Each piece took about 24 hours to print. This is surplus PLA that will never otherwise get used so the volume of plastic used is totally okay, and the size (and therefore weight) of the cement involved is a concern for a mould that would be more plastic efficient and less beefy.

And then I poured.

The thermal camera shows how hot it gets as the cement starts to kick. It got past 60 by the time I left it for the night.

I actually did it twice. The first one I didn’t mix up enough, so ended up scrambling to mix more, which didn’t fill in the mould completely. Honestly, I kinda like it.

 

For the second attempt, the mould had warped enough that the seam lines were very visible. After the second pour, the mould had warped enough that it wasn’t viable to pour a third time. This is a really interesting datapoint. It’s possible that PETG or some other higher temperature material would fare better. And this issue doesn’t crop up in the smaller castings I did. The cement only gets hot enough to deform the plastic when there’s a large volume curing at once.

 

 

But regardless. The result!

It’s decent. I’m about 80% happy with it. It’s recognisable as the target mountain range, but it’s not instantly identifiable. This is done as the actual aspect ratio of the mountains, with the focal length of the (cellphone) camera I took of the mountains, and it looks a little too shallow. Perhaps the aspect ratio would be a good knob to turn for future experiments, to get the mountains in the casting to look a little taller.

Force-Directed Circuit Board Footprint Autoplacement

From Wikipedia:

Force-directed graph drawing algorithms are a class of algorithms for drawing graphs in an aesthetically-pleasing way. Their purpose is to position the nodes of a graph in two-dimensional or three-dimensional space so that all the edges are of more or less equal length and there are as few crossing edges as possible, by assigning forces among the set of edges and the set of nodes, based on their relative positions, and then using these forces either to simulate the motion of the edges and nodes or to minimize their energy.

Force-directed graphs are a common way to display things like mind-maps. It’s a good way of spreading out the whole collection, while grouping related items together, and minimizing the path length between the closely related items. In my mind, this has a lot of similarities with how PCBs are laid out.

Well, there’s only one way to prove that theory.

KiCad Footprints animated in an exploding fashion

Using KiCad PCB parsing code I wrote for another project, I was quickly able to grab the nets and footprints out of a KiCad project. Displaying the nets and allowing specific ones to be turned off was a feature I identified as critical early on, because the ground or power nets would overwhelm any of the others, rendering the important nets useless.

Truthfully, a significant part of this was wrangling TKInter to build the Python GUI that I wanted. It was a success, but I’ve never used it before, and I am not a fantastic UI designer.

Under the hood, the system essentially treats each of the nets as a spring, and applies a simplified version of Hooke’s Law to each connection. Each of the centre point of the footprints acts as a charged particle, and a simplified version of Coulomb’s Law acts upon it to repulse all of the other footprints. These algorithms are a pretty typical way to do this, by essentially setting up a physics simulation. One tweak on this strategy that is unusual, is that the nets don’t act on the footprint origin. They act on the pad itself, which allows a torque to impart a rotation on the footprint.

I’ve gotten this project as far as “fun tech demo”, and it’ll likely fall dormant for a while in this state. At some point, I will build an appropriate PCB using this technology, because I love making unusual designs that are electrically fine.

The repo is here. Have at it.

Strange New Bedroom Furniture

I bought a used lathe.

 

It’s a Craftex B1979C, which seems to be nearly identical to a Craftex CX704. Similar to many such mini lathes, actually, with varying swing lengths.

The previous owner explained, a little bit sheepishly, that he tried to cut steel a little too hard, and burned out the AC motor. He replaced it with a hobby BLDC motor and ESC, designed for things like quadcopters.

I played around with that system a little bit, and I didn’t like it.

Annoyingly, he bought a $50 motor and a $50 motor controller, both sensorless versions. For these kinds of motors, you need sensors to start up at low speeds with torque on the motor. That would have cost $60 for the motor and $60 for the motor controller – Not much extra for him, but annoying for me to have to shell out the full amount for both, and then have the old ones as somewhat useless spares.

Anyway, instead of going the same route, I chose to get a more sophisticated controller so that I could precisely control parameters, like speed, or acceleration. The options are essentially vESC or ODrive, unless I want to custom build something myself (I do not).

The ODrive, at least on paper, looked a little cooler. Some good communication strategies, lots of control modes, and generic enough to work well for this somewhat strange application. Right after I’d picked mine up, they discontinued support of the V3.6 and went to a closed source model, which rubs me the wrong way. The new version is also more expensive and has a single channel instead of two channels. One of the big issues with the legacy release is that there is a bug in their UART driver – If you send enough malformed packets, like, say, because you have a data line right next to a spinning motor, then the controller eventually stops responding to all UART data. That can be an issue when the packet you want to send is “stop now, immediately”. That definitely hastened my building of a separate e-stop box.

Fortunately, after I wrote a CANBus driver and used that, it’s been reasonably solid. That was just more of a time investment than I wanted to make.

I built a front panel for it as well. It’s a couple simple PCBs holding the switches, OLED, buttons, and a dev board. The advantage of not including everything on a complicated PCB is that I can pop out the dev board to flash it on my bench, before returning it to the lathe. The dev board is an ESP32, which is driving a little OLED screen that shows the target and measured speed, current, forward and backwards directions of the motor controller. The list of features I could add is nigh-infinite, but this is good enough for now. Oh, and I can do software updates over the internet! There is no way this can end badly.

 

It’s weird how sometimes three sentences can encompass four months’ worth of occasional project time.

 

The change gears the lathe came with are steel, which is unusual for a model like this. It came with a set of gears, from A-D: 20/80/20/80. However, this combination isn’t listed on the table anywhere.

The formula is ((A/B)×(C/D))×lead_screw_pitch/initial_ratio
where you have a 3mm lead screw, and it has a 2:1 ratio. So for example, 0.4mm pitch is ((20/50)×(40/60))×3/2 = 0.4.

That means the gears I have are about 0.09mm pitch, which is good as a powered feed, but not intended for any kind of threading.

Most of the other examples of this family of lathes use plastic change gears. Obviously using cast or machined nylon, but with a little bit of babying, I bet 3D printed gears would be Good Enough.

As near as I can figure, the change gears dimensions are module 1, with a 20 degree pressure angle. And a thickness of 8mm.

 

To practice threading, I copied a bolt that I had on hand, which was 1/2″-13 TPI, using the table to get 40/65/60/30.

 

 

Looks neat! But my tool geometry is bad and the angles are all wrong, so I reset, bought some more tooling, and moved on to something actually useful.

 

3D printed change gears work great! I’ve printed off a whole set, and I’m stoked. Arbitrary threads are unlocked.

 

 

I wanted to turn a gear shift knob, which has a thread of M12*1.25mm.

Using the formula above, the gears I need are 50/40/40/60.

The outer diameter of the stock is about 11.85mm.

 

 

This process ended up being incredibly drama free. Twenty minutes after starting, I had a very-slightly too-loose thread, but it snugged right up when I tightened on the knob.

 

 

Success! Now to do some cool things.

Brass Lamp

I haven’t properly built anything with my hands in a while, so it’s time.

 

I mocked up a few lamp ideas, and this one seemed fun. Really, really rough mock-ups.

 

That was neat looking, so I fleshed out the hinge section, which I expected would be the hardest part to figure out and build.

 

 

 

 

First, I 3D printed the hinge, just to get a feel for if my sizes felt right – I am bad at judging size in CAD, things often turn out way larger or way smaller than I expect, and therefore really difficult to build.

 

These looked fine, though.

 

I picked up a ton of brass from a hobby shop. Can you guess what the main focus of the hobby shop is?

 

 

For the hinge pieces, I didn’t have the right shapes or size of brass stock available, so I needed to cut a bunch of smaller pieces and then braze them together with silver solder. And then took them to the belt grinder.

 

Pretty close!

 

And then tried to add one more piece, dropped it while the solder was molten, and had them splatter apart on the ground.

 

 

Okay one more time.

 

 

It’s always better the second time, anyway. I used an M1.5 brass screw as the hinge pin.

 

For the stand, I 3D printed some saw guides so that I could get the tube angles perfect. The stand wouldn’t end up with the right geometry unless the cut angles were just right.

 

Then, after brazing:

 

 

It matches up with my CAD perfectly, I’m stoked.

 

Some blu-tak was used to hold the hinge pieces and light backing together to get a sense of how it was all going to go together.

 

 

The light panel itself was a circle of aluminum that I spraypainted black. Then I riveted the hinge on and laid out my LED tape.

 

 

Wiring.

This could have been done much neater. Next time.

Encircling the LED panel, I bent some flat bar aluminum, riveted it, glued it, and painted it. Then glued on a plastic diffusion circle. This was all done quickly, so I didn’t take many pictures.

 

For the base, some nice dark wood would be ideal, but that would have required a fair amount of material acquisition and hunting down tools I wasn’t up for, for this project. So I took my base, and split it up into easily printable chunks.

I was going to attach them together, sand, patch, sand, and paint them to turn them back into one object, but I kinda liked the jigsaw effect, so I left it. PETG sands really well, and I treated it with a light oil coating to keep it from taking on fingerprints.

The pieces are just bolted together, and mostly hollow to allow mounting of the electronics.

For the controller, I have a surplus prototype lighting controller of dubious origin, and wrote some firmware to handle fading and mixing the two LED channels. The 24V power supply comes from AliExpress.

Similarly, for the knobs, I went for an easy and quick solution using 3D printing for the ends to retain a little bit of the brass tube onto the potentiometers. The left knob controls the light intensity, and the right knob controls the light’s colour temperature. This lamp can fade in between a really cool white and a really warm light.

Running the wires through the brass tube was a fun adventure. What I ended up doing was fill the tube up with oil, run a single wire up through the bottom, then solder it onto the other three wires. After that, I could use the first wire to pull the whole mess back through the tube. Then I flushed the tube with alcohol to clean it all out.

And, final wiring, and it works! It looks great. I can see all the small defects, but that’s okay. I’ll do better on the next one.