Tek Circuit Computer: 3D Printed Cursor Milling Fixture

The original Tektronix Circuit Computer cursor was probably die-cut from a larger sheet carrying pre-printed hairlines:

Tek CC - genuine - detail
Tek CC – genuine – detail

Machining a punch-and-die setup lies well beyond my capabilities, particularly given the ahem anticipated volume, so milling seems the only practical way to produce a few cursors.

Attaching a cursor blank to a fixture with sticky tape showed that the general idea worked reasonably well:

Tek CC - Cursor blank on fixture
Tek CC – Cursor blank on fixture

However, the tape didn’t have quite enough griptivity to hold the edges completely flat against milling forces (a downcut bit might have worked better) and I found myself chasing the cutter with a screwdriver to hold the cursor in place. Worse, the tape’s powerful attraction to swarf made it a single-use item.

Some tinkering showed a single screw in the (pre-drilled) pivot hole, without adhesive underneath, lacked enough oomph to keep the far end of the cursor in place, which meant I had to think about how to hold it down with real clamps.

Which, of course, meant conjuring a fixture from the vasty digital deep. The solid model includes the baseplate, two cutting templates, and a clamping fixture for engraving the cursor hairline:

Cursor Fixture - build layout
Cursor Fixture – build layout

The perimeter of the Clamp template on the far left is 0.5 mm inside the cursor perimeter. Needing only one Clamp, I could trace it on a piece of acrylic, bandsaw it pretty close, introduce it to Mr Belt Sander for final shaping, and finally drill the hole:

Tek CC Cursor Fixture - clamp drilling
Tek CC Cursor Fixture – clamp drilling

The Rough template is 1.0 mm outside the cursor perimeter, so I can trace those outlines on a PET sheet:

Tek CC Cursor Fixture - Rough template layout
Tek CC Cursor Fixture – Rough template layout

Then cut the patterns with a scissors, stack ’em up, and tape the edges to keep them aligned:

TekCC Cursor Fixture - Rough template
TekCC Cursor Fixture – Rough template

Align the stack by feel, apply the Clamp to hold them in place, and secure the stack with a Sherline clamp:

Tek CC Cursor Fixture - outline rear clamp
Tek CC Cursor Fixture – outline rear clamp

The alert reader will note it’s no longer possible to machine the entire perimeter in one pass; more on that in a while.

The baseplate pretty much fills the entire Sherline tooling plate. It sports several alignment pips at known offsets from the origin at the center of the pivot hole:

Tek CC Cursor Fixture - touch-off point
Tek CC Cursor Fixture – touch-off point

Dropping the laser alignment dot into a convenient pip, then touching off X and Y to the known offset sets the origin without measuring anything. Four screws in the corners align the plate well enough to not worry about angular tweakage.

The OpenSCAD source code as a GitHub Gist:

// Machining fixtures for Tek Circuit Computer cursor
// Ed Nisley KE4ZNU Jan 2021
Layout = "Show"; // [Show, Build, Cursor, Clamp, Rough, Engrave]
/* [Hidden] */
ThreadThick = 0.25;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
inch = 25.4;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
FixDia = Dia / cos(180/Sides);
cylinder(d=(FixDia + HoleWindage),h=Height,$fn=Sides);
}
//----------------------
// Dimensions
CursorHubOD = 1.0*inch; // original Tek CC was hard inch!
CursorTipWidth = (9.0/16.0)*inch;
CursorTipRadius = (1.0/16.0)*inch;
CursorThick = 0.5; // plastic sheet thickness
CutterOD = 3.175; // milling cutter dia
CutterDepth = 2.0; // … depth of cut
CutterLip = 0.5; // … clearance under edge
ScribeOD = 3.0; // diamond scribe shank
StudOC = [1.16*inch,1.16*inch]; // Sherline tooling plate grid
StudClear = 5.0; // … screw clearance
StudWasher = 11.0; // … washer OD
CursorOffset = [-2*StudOC.x,0,0]; // hub center relative to fixture center
// must have even multiples of stud spacing to put studs along centerlines
BasePlateStuds = [6*StudOC.x,2*StudOC.y]; // fixture screws
echo(str("Stud spacing: ",StudOC));
CornerRad = 10.0; // corner radius
BasePlate = [2*StudWasher + BasePlateStuds.x,2*StudWasher + BasePlateStuds.y,5.0];
echo(str("Base Plate: ",BasePlate));
EngravePlate = [5*StudOC.x,1.5*StudOC.y,BasePlate.z];
echo(str("Engrave Plate: ",EngravePlate));
TemplateThick = 6*ThreadThick;
LegendThick = 2*ThreadThick;
Gap = 3.0;
//----------------------
// Import SVG of cursor outline
// Requires our hub OD to match reality
// Hub center at origin
module CursorSVG(t=CursorThick,od=0) {
hr = CursorHubOD/2;
translate([-hr,-hr,0])
linear_extrude(height=t,convexity=3)
offset(r=od/2)
import(file="/mnt/bulkdata/Project Files/Tektronix Circuit Computer/Firmware/TekCC-Cursor-Mark.svg",center=false);
}
//----------------------
// Milling fixture for cursor blanks
module Fixture() {
difference() {
hull() // basic plate shape
for (i=[-1,1], j=[-1,1])
translate([i*(BasePlate.x/2 - CornerRad),j*(BasePlate.y/2 - CornerRad),0])
cylinder(r=CornerRad,h=BasePlate.z,$fn=24);
translate(CursorOffset + [0,0,BasePlate.z - CutterDepth])
difference() {
CursorSVG(CutterDepth + Protrusion,1.5*CutterOD);
CursorSVG(CutterDepth + Protrusion,-CutterLip);
}
translate(CursorOffset + [0,0,BasePlate.z - 2*ThreadThick]) { // alignment pips
for (x=[-20.0,130.0], y=[-30.0,0.0,30.0])
translate([x,y,0])
cylinder(d=4*ThreadWidth,h=1,$fn=6);
for (x=[-30.0,130.0,150.0])
translate([x,0,0])
cylinder(d=4*ThreadWidth,h=1,$fn=6);
}
for (i=[-1,1], j=[-1,1]) // mounting stud holes
translate([i*BasePlateStuds.x/2,j*BasePlateStuds.y/2,-Protrusion])
rotate(180/6)
PolyCyl(StudClear,BasePlate.z + 2*Protrusion,6);
translate(CursorOffset + [0,0,-Protrusion]) // hub clamp hole
rotate(180/6)
PolyCyl(StudClear,BasePlate.z + 2*Protrusion,6);
translate([2*StudOC.x,0,-Protrusion]) // tip clamp hole
rotate(180/6)
PolyCyl(StudClear,BasePlate.z + 2*Protrusion,6);
for (i=[-2:2], j=[-1,1]) // side clamp holes
translate([i*StudOC.x,j*StudOC.y,-Protrusion])
rotate(180/6)
PolyCyl(StudClear,BasePlate.z + 2*Protrusion,6);
}
}
//----------------------
// Show-n-Tell cursor
module Cursor() {
difference() {
CursorSVG(CursorThick,0.0);
translate([0,0,-Protrusion])
rotate(180/6)
PolyCyl(StudClear,TemplateThick + 2*Protrusion,6);
}
}
//----------------------
// Template for rough-cutting blanks
module Rough() {
bb = [40,12,LegendThick];
difference() {
CursorSVG(TemplateThick,1.0);
translate([0,0,-Protrusion])
rotate(180/6)
PolyCyl(StudClear,TemplateThick + 2*Protrusion,6);
difference() {
translate([bb.x/2 + CursorHubOD/2,0,TemplateThick - bb.z/2 + Protrusion])
cube(bb + [0,0,Protrusion],center=true);
translate([bb.x/2 + CursorHubOD/2,0,TemplateThick - bb.z])
linear_extrude(height=bb.z,convexity=10)
text(text="Rough",size=7,spacing=1.00,font="DejaVu Sans:style:Bold",halign="center",valign="center");
}
}
}
//----------------------
// Template for aluminium clamping plate
module Clamp() {
bb = [40,12,LegendThick];
difference() {
CursorSVG(TemplateThick,-1.0);
translate([0,0,-Protrusion])
rotate(180/6)
PolyCyl(StudClear,TemplateThick + 2*Protrusion,6);
difference() {
translate([bb.x/2 + CursorHubOD/2,0,TemplateThick - bb.z/2 + Protrusion])
cube(bb + [0,0,Protrusion],center=true);
translate([bb.x/2 + CursorHubOD/2,0,TemplateThick - bb.z])
linear_extrude(height=bb.z,convexity=10)
text(text="Clamp",size=7,spacing=1.00,font="DejaVu Sans:style:Bold",halign="center",valign="center");
}
}
}
//----------------------
// Engraving clamp
module Engrave() {
difference() {
hull() // clamp outline
for (i=[-1,1], j=[-1,1])
translate([i*(EngravePlate.x/2 - CornerRad),j*(EngravePlate.y/2 - CornerRad),0])
cylinder(r=CornerRad,h=EngravePlate.z,$fn=24);
translate(CursorOffset + [0,0,-Protrusion])
CursorSVG(CursorThick + Protrusion,0.5); // pocket for blank cursor
translate(CursorOffset + [0,0,-Protrusion])
rotate(180/6)
PolyCyl(StudClear,EngravePlate.z + 2*Protrusion,6);
translate([2*StudOC.x,0,-Protrusion])
rotate(180/6)
PolyCyl(StudClear,EngravePlate.z + 2*Protrusion,6);
hull() {
for (i=[-1,1])
translate([i*1.5*StudOC.x,0,-Protrusion])
PolyCyl(2*ScribeOD,EngravePlate.z + 2*Protrusion,8);
}
}
}
//----------------------
// Build it
if (Layout == "Cursor") {
Cursor();
}
if (Layout == "Clamp") {
Clamp();
}
if (Layout == "Rough") {
Rough();
}
if (Layout == "Engrave") {
Engrave();
}
if (Layout == "Show") {
Fixture();
color("Green",0.3)
translate(CursorOffset + [0,0,BasePlate.z + Protrusion])
Cursor();
color("Orange")
translate(CursorOffset + [0,0,BasePlate.z + 10])
Rough();
color("Brown")
translate(CursorOffset + [0,0,BasePlate.z + 20])
Clamp();
color("Gold")
translate(0*CursorOffset + [0,0,BasePlate.z + 40])
Engrave();
}
if (Layout == "Build"){
rotate(90) {
Fixture();
translate([0,-((BasePlate.y + EngravePlate.y)/2 + Gap),EngravePlate.z])
rotate([180,0,0])
Engrave();
translate(CursorOffset + [0,(BasePlate.y + CursorHubOD)/2 + Gap,0])
Rough();
translate(CursorOffset + [0,(BasePlate.y + 3*CursorHubOD)/2 + 2*Gap,0])
Clamp();
}
}
view raw Cursor Fixture.scad hosted with ❤ by GitHub

The original doodle with some notions and dimensions that didn’t survive contact with reality:

Cursor Fixture doodle
Cursor Fixture doodle

I have no idea why the Sherline tooling plate has a 10-32 screw grid on 1.16 inch = 29.46 mm centers, but there they are.

Homage Tektronix Circuit Computer: Laser Printed Scales

Given the proper command-line options, GCMC can produce an SVG image and, after some Bash fiddling and a bank shot off Inkscape, the same GCMC program I’ve been using to plot Homage Tektronix Circuit Computer decks can produce laser-printed decks:

Tek CC - laser - detail
Tek CC – laser – detail

Pen-plotting on yellow Astrobrights paper showed how much ink bleeds on slightly porous paper, but laser-printing the same paper produces crisp lines:

Tek CC - laser - yellow detail
Tek CC – laser – yellow detail

Laser printing definitely feels like cheating, but, for comparison, here’s a Genuine Tektronix Circuit Computer:

Tek CC - genuine - detail
Tek CC – genuine – detail

Plotting the decks on hard mode was definitely a learning experience!

Obviously, my cursor engraving hand remains weak.

KeyboardIO Atreus: LED Diffuser

After staring at the RGB LED I installed in my Atreus keyboard for a while, I converted the stub of a ¼-20 nylon screw into a light diffuser:

Atreus keyboard - LED diffuser
Atreus keyboard – LED diffuser

It stands slightly proud of the surface plate so I can extract it without dismantling the whole keyboard again:

Atreus keyboard - LED diffuser installed
Atreus keyboard – LED diffuser installed

I’ll eventually make a better-looking diffuser from a recently arrived translucent acrylic rod, but this will reduce the accumulation of fuzz inside the keyboard until the matching Round Tuit arrives.

KeyboardIO Atreus: RGB LED Firmware

Having wired a WS2812 RGB LED into my KeyboardIO Atreus, lighting it up requires some QMK firmware configuration. It’s easiest to set up a “new” keymap based on the QMK Atreus files, as described in the QMK startup doc:

qmk new-keymap -kb keyboardio/atreus -km ednisley

Obviously, you’ll pick a different keymap name than I did. All the files mentioned below will reside in the new subdirectory, which starts out with only a keymap.c file copied from the default layout.

The rules.mk file enables RGB Lighting, as well as Auto Shift and Tap Dance:

AUTO_SHIFT_ENABLE = yes			# allow automagic shifting
TAP_DANCE_ENABLE = yes			# allow multi-tap keys

RGBLIGHT_ENABLE = yes			# addressable LEDs

If you had different hardware, you could specify the driver with a WS2812_DRIVER option.

QMK can also control single-color LEDs with PWM (a.k.a. backlighting), and per-key RGB LEDs (a.k.a. RGB Matrix). These functions, their configuration / controls / data, and their documentation overlap and intermingle to the extent that I spent most of my time figuring out what not to include.

Some configuration happens in the config.h file:

#define RGB_DI_PIN B2
#define RGBLED_NUM 1

// https://github.com/qmk/qmk_firmware/blob/master/docs/ws2812_driver.md
//#define WS2812_TRST_US 280
//#define WS2812_BYTE_ORDER WS2812_BYTE_ORDER_GRB

#define RGBLIGHT_LAYERS
#define RGBLIGHT_EFFECT_RGB_TEST
#define RGBLIGHT_LIMIT_VAL 63

#define NO_DEBUG
#define NO_PRINT

The first two lines describe a single WS2812 RGB LED wired to pin B2 (a.k.a. MOSI) of the Atmel 32U4 microcontroller. The default Reset duration and Byte Order values work for the LED I used

Protip: swapping the order from GRB to RGB is a quick way to discover if the firmware actually writes to the LED, even before you get anything else working: it’ll be red with the proper setting and green with the wrong one.

Dialing the maximum intensity down works well with a bright LED shining directly at your face from a foot away.

Turning on RGBLIGHT_LAYERS is what makes this whole thing happen. The RGBLIGHT_EFFECT_RGB_TEST option enables a simple test animation at the cost of a few hundred bytes of code space; remove that line after everything works.

The last two lines remove the debugging facilities; as always with microcontroller projects, there’s enough room for either your code or the debugger required to get it running, but not both.

With those files set up, the keymap.c file does the heavy lifting:

// Modified from the KeyboardIO layout
// Ed Nisley - KE4ZNU

#include QMK_KEYBOARD_H

enum layer_names {
    _BASE,
    _SHIFTS,
    _FUNCS,
    _NLAYERS
};

// Tap Dance

enum {
    TD_SPC_ENT,
};

qk_tap_dance_action_t tap_dance_actions[] = {
    [TD_SPC_ENT] = ACTION_TAP_DANCE_DOUBLE(KC_SPC, KC_ENT),
};


// Layer lighting

// Undefine this to enable simple test mode
// Also put #define RGBLIGHT_EFFECT_RGB_TEST in config.h

#define LED_LL

#ifdef LED_LL

const rgblight_segment_t PROGMEM ll_0[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_WHITE} );
const rgblight_segment_t PROGMEM ll_1[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_MAGENTA} );
const rgblight_segment_t PROGMEM ll_2[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_CYAN} );
const rgblight_segment_t PROGMEM ll_3[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_BLUE} );
const rgblight_segment_t PROGMEM ll_4[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_GREEN} );
const rgblight_segment_t PROGMEM ll_5[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_RED} );
const rgblight_segment_t PROGMEM ll_6[] = RGBLIGHT_LAYER_SEGMENTS( {0,1,HSV_YELLOW} );

const rgblight_segment_t* const PROGMEM ll_layers[] = RGBLIGHT_LAYERS_LIST(
    ll_0,ll_1,ll_2,ll_3,ll_4,ll_5,ll_6
);

#endif

void keyboard_post_init_user(void) {

#ifdef LED_LL
    rgblight_layers = ll_layers;
    rgblight_set_layer_state(0, 1);
#else
    rgblight_enable_noeeprom();
    rgblight_mode_noeeprom(RGBLIGHT_MODE_RGB_TEST);
//    rgblight_mode_noeeprom(RGBLIGHT_MODE_BREATHING + 3);
#endif

}


#ifdef LED_LL

layer_state_t layer_state_set_user(layer_state_t state) {
    for (uint8_t i=0 ; i < _NLAYERS; i++)
        rgblight_set_layer_state(i, layer_state_cmp(state, i));

    return state;
}
#endif


// Key maps

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [_BASE] = LAYOUT(                             // base layer for typing
    KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,                      KC_Y,    KC_U,    KC_I,    KC_O,    KC_P    ,
    KC_A,    KC_S,    KC_D,    KC_F,    KC_G,                      KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN ,
    KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,    KC_GRV,  KC_LALT, KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH ,
    LT(_FUNCS,KC_ESC), KC_TAB, KC_LGUI,  KC_BSPC, KC_LSFT,  KC_LCTL, KC_ENT , TD(TD_SPC_ENT),  MO(_SHIFTS), KC_MINS, KC_QUOT, KC_BSLS),

  [_SHIFTS] = LAYOUT(                           // shifted chars and numpad
    KC_EXLM, KC_AT,   KC_UP,   KC_DLR,  KC_PERC,                  KC_PGUP, KC_7,    KC_8,   KC_9, KC_HOME,
    KC_LPRN, KC_LEFT, KC_DOWN, KC_RGHT, KC_RPRN,                  KC_PGDN, KC_4,    KC_5,   KC_6, KC_END,
    KC_LBRC, KC_RBRC, KC_HASH, KC_LCBR, KC_RCBR, KC_CIRC, KC_AMPR,KC_ASTR, KC_1,    KC_2,   KC_3, KC_PLUS,
    KC_NO  , KC_INS,  KC_LGUI, KC_DEL , KC_BSPC, KC_LCTL, KC_LALT,KC_SPC,  KC_TRNS, KC_DOT, KC_0, KC_EQL ),

  [_FUNCS] = LAYOUT(                            // function keys
    KC_INS,  KC_HOME, KC_UP,   KC_END,  KC_PGUP,                   KC_UP,   KC_F7,   KC_F8,   KC_F9,   KC_F10  ,
    KC_DEL,  KC_LEFT, KC_DOWN, KC_RGHT, KC_PGDN,                   KC_DOWN, KC_F4,   KC_F5,   KC_F6,   KC_F11  ,
    KC_NO,   KC_VOLU, KC_NO,   KC_NO,   RESET,   _______, _______, KC_NO,   KC_F1,   KC_F2,   KC_F3,   KC_F12  ,
    KC_NO,   KC_VOLD, KC_LGUI, KC_LSFT, KC_BSPC, KC_LCTL, KC_LALT, KC_SPC,  TO(_BASE), KC_PSCR, KC_SLCK, KC_PAUS )
};

Undefine LED_LL to enable the test mode, compile, flash, and the LED should cycle red / green / blue forever; you also need the RGB_TEST option in the config.h file.

Define LED_LL and layer lighting should then Just Work™, with the LED glowing:

  • White for the basic layer with all the letters
  • Magenta with the Fun key pressed
  • Cyan with the Esc key pressed

The key map code defines colors for layers that don’t yet exist, but it should get you started.

For convenience, I wadded all three QMK files into a GitHub Gist.

The LED is kinda subtle:

Atreus keyboard - LED installed
Atreus keyboard – LED installed

As you might expect, figuring all that out took much longer than for you to read about it, but now I have a chance of remembering what I did.

KeyboardIO Atreus: RGB LED Installation

Having scouted out the territory inside the KeyboardIO Atreus, adding an LED requires taking it completely apart to drill a hole in the aluminum faceplate:

Atreus keyboard - panel drilling
Atreus keyboard – panel drilling

Reattaching the plate to the PCB with only three screws allows marking the hole position on the PCB, which is much easier than pretending to derive the position from first principles:

Atreus keyboard - LED marking
Atreus keyboard – LED marking

Despite appearances, I traced the hole with a mechanical pencil: black graphite turns shiny silvery gray against matte black soldermask. Also, the PCB trace is off-center, not the hole.

Overlay the neighborhood with Kapton tape to protect the PCB from what comes next:

Atreus keyboard - Kapton tape

Snip a WS2812 RGB LED from a strip, stick it in place with eyeballometric alignment over the target, and wire it up:

Atreus keyboard - LED wiring
Atreus keyboard – LED wiring

Despite the terrible reliability of WS2812 RGB LEDs mounted on PCB carriers, a different set on a meter of high-density flex tape have worked reasonably well when not thermally stressed, so I’ll assume this one arrived in good order.

Aligning the LED directly under the hole required a few iterations:

Atreus keyboard - LED positioning
Atreus keyboard – LED positioning

The iridescent green patch is a diffraction pattern from the controller chip’s internal circuitry.

The data comes from MOSI, otherwise known as B2, down in the lower left corner:

Atmel 32U4 - JTAG pins
Atmel 32U4 – JTAG pins

Actually lighting the LED now becomes a simple matter of software QMK firmware.

Straightening Armature Wire

Although I was blithely unaware when I bought some useful-looking surplus, it turns out 1/16 inch armature wire works really well to seal our homebrew masks around our noses. Mary added a narrow passage along the top edge of her slightly reshaped Fu Mask pattern to retain the wire and I provided 4.5 inch lengths of straightened wire:

Armature wire - stock vs. straightened
Armature wire – stock vs. straightened

The wire comes off the roll in dead-soft condition, so I can straighten (and slightly harden) it by simply rolling each wire with eight fingertips across the battered cutting board. The slightly wavy wire shows its as-cut condition and the three straight ones are ready for their masks.

Although nearly pure aluminum wire doesn’t work-harden quickly, half a year of mask duty definitely takes its toll. This sample came from my biking mask after the edges wore out:

Armature wire - work-hardened
Armature wire – work-hardened

We initially thought using two wires would provide a better fit, but more metal just made adjusting the nose seal more difficult after each washing. The wire has work-hardened enough to make the sharper bends pretty much permanent; they can be further bent, but no longer roll out under finger pressure.

Although we’re not yet at the point where we must reuse wires, I took this as an opportunity to improve my annealing hand: heat the wire almost to its melting point, hold it there for a few seconds, then let it cool slowly. The usual technique involves covering the aluminum with something like hand soap or permanent marker ink, heat until the soap / marker burns away, then let it air-cool. Unlike steel, there’s no need for quenching or tempering.

Blue Sharpie worked surprisingly well with a propane torch:

Armature wire - annealed straightened
Armature wire – annealed straightened

As far as I can tell after a few attempts, the pigment vanishes just below the annealing temperature and requires another pass to reach the right temperature. Sweep the flame steadily, don’t pause, and don’t hold the wire over anything melt-able.

Those wires (I cut the doubled wire apart) aren’t quite as soft as the original stock, but they rolled straight and are certainly good enough for our simple needs; they’re back in the Basement Laboratory Warehouse for future (re)use.