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: 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.

Makergear M2: Initial PrusaSlicer Configuration

After replacing the nozzle and the filament drive body on the M2, I figured I might as well throw all the balls in the air and switch to PrusaSlicer for all my slicing needs. It’s built from the Slic3r project, gaining features used by Prusa’s printers / filaments and a considerably improved UI, with a full-time paid staff working on it:

PrusaSlicer screenshot
PrusaSlicer screenshot

Of course, I immediately turned on Expert mode.

CAUTION: My heavily customized start_gcode will crash your M2, because you haven’t relocated the Z-axis switch, haven’t calibrated Z=0 at the platform surface, and don’t put the XY=0 origin in the center of the platform.

You have been warned: consider this as a serving suggestion, not a finished product.

Because everything I design looks more-or-less like a bracket, I absolutely don’t care about surface finish, and I’m content to use only a few colors of PETG from a single supplier, a single Slic3r configuration has sufficed for nearly everything I print. A few manual tweaks for specific models, perhaps to change the number of perimeters or the infill percentage, handle the remaining cases.

With all that in mind, here’s the current result of File → Export → Export Config as a GitHub Gist:

# generated by PrusaSlicer 2.2.0+linux-x64 on 2021-01-01 at 13:33:03 UTC
avoid_crossing_perimeters = 0
bed_custom_model =
bed_custom_texture =
bed_shape = -100x-125,100x-125,100x125,-100x125
bed_temperature = 90
before_layer_gcode =
between_objects_gcode =
bottom_fill_pattern = hilbertcurve
bottom_solid_layers = 3
bottom_solid_min_thickness = 0
bridge_acceleration = 0
bridge_angle = 0
bridge_fan_speed = 100
bridge_flow_ratio = 1
bridge_speed = 50
brim_width = 0
clip_multipart_objects = 1
colorprint_heights =
complete_objects = 0
cooling = 1
cooling_tube_length = 5
cooling_tube_retraction = 91.5
default_acceleration = 0
default_filament_profile = ""
default_print_profile =
deretract_speed = 0
disable_fan_first_layers = 6
dont_support_bridges = 1
draft_shield = 0
duplicate_distance = 6
elefant_foot_compensation = 0
end_filament_gcode = "; Filament-specific end gcode \n;END gcode for filament\n"
end_gcode = ;-- PrusaSlicer End G-Code for M2 starts --\n; Ed Nisley KE4NZU - 15 November 2013\nG1 Z160 F2000 ; lower bed\nG1 X135 Y100 F30000 ; nozzle to right, bed front\nM104 S0 ; drop extruder temperature\nM140 S0 ; drop bed temperature\nM106 S0 ; bed fan off\nM84 ; disable motors\n;-- PrusaSlicer End G-Code ends --\n\n
ensure_vertical_shell_thickness = 1
external_perimeter_extrusion_width = 0
external_perimeter_speed = 50%
external_perimeters_first = 0
extra_loading_move = -2
extra_perimeters = 1
extruder_clearance_height = 20
extruder_clearance_radius = 20
extruder_colour = ""
extruder_offset = 0x0
extrusion_axis = E
extrusion_multiplier = 0.95
extrusion_width = 0.4
fan_always_on = 0
fan_below_layer_time = 15
filament_colour = #29B2B2
filament_cooling_final_speed = 3.4
filament_cooling_initial_speed = 2.2
filament_cooling_moves = 4
filament_cost = 25
filament_density = 0.95
filament_deretract_speed = nil
filament_diameter = 1.72
filament_load_time = 0
filament_loading_speed = 28
filament_loading_speed_start = 3
filament_max_volumetric_speed = 0
filament_minimal_purge_on_wipe_tower = 15
filament_notes = ""
filament_ramming_parameters = "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6"
filament_retract_before_travel = nil
filament_retract_before_wipe = nil
filament_retract_layer_change = nil
filament_retract_length = nil
filament_retract_lift = nil
filament_retract_lift_above = nil
filament_retract_lift_below = nil
filament_retract_restart_extra = nil
filament_retract_speed = nil
filament_settings_id = "M2 Esun PETG"
filament_soluble = 0
filament_toolchange_delay = 0
filament_type = PET
filament_unload_time = 0
filament_unloading_speed = 90
filament_unloading_speed_start = 100
filament_vendor = (Unknown)
filament_wipe = nil
fill_angle = 45
fill_density = 25%
fill_pattern = 3dhoneycomb
first_layer_acceleration = 0
first_layer_bed_temperature = 90
first_layer_extrusion_width = 0
first_layer_height = 0.25
first_layer_speed = 15
first_layer_temperature = 250
gap_fill_speed = 25
gcode_comments = 0
gcode_flavor = marlin
gcode_label_objects = 0
high_current_on_filament_swap = 0
host_type = octoprint
infill_acceleration = 0
infill_every_layers = 1
infill_extruder = 1
infill_extrusion_width = 0
infill_first = 1
infill_only_where_needed = 0
infill_overlap = 15%
infill_speed = 60
interface_shells = 0
layer_gcode =
layer_height = 0.25
machine_max_acceleration_e = 10000,5000
machine_max_acceleration_extruding = 10000,1250
machine_max_acceleration_retracting = 10000,1250
machine_max_acceleration_x = 2500,1000
machine_max_acceleration_y = 2500,1000
machine_max_acceleration_z = 2500,200
machine_max_feedrate_e = 10000,5000
machine_max_feedrate_x = 450,200
machine_max_feedrate_y = 450,200
machine_max_feedrate_z = 100,30
machine_max_jerk_e = 100,50
machine_max_jerk_x = 25,10
machine_max_jerk_y = 25,10
machine_max_jerk_z = 10,5
machine_min_extruding_rate = 0,0
machine_min_travel_rate = 0,0
max_fan_speed = 100
max_layer_height = 0
max_print_height = 200
max_print_speed = 80
max_volumetric_speed = 0
min_fan_speed = 100
min_layer_height = 0.1
min_print_speed = 10
min_skirt_length = 25
notes =
nozzle_diameter = 0.35
only_retract_when_crossing_perimeters = 1
ooze_prevention = 0
output_filename_format = [input_filename_base].gcode
overhangs = 1
parking_pos_retraction = 92
perimeter_acceleration = 0
perimeter_extruder = 1
perimeter_extrusion_width = 0
perimeter_speed = 50
perimeters = 3
post_process =
print_host =
print_settings_id = M2 Default
printer_model =
printer_notes =
printer_settings_id = M2 Default
printer_technology = FFF
printer_variant =
printer_vendor =
printhost_apikey =
printhost_cafile =
raft_layers = 0
remaining_times = 0
resolution = 0.01
retract_before_travel = 3
retract_before_wipe = 0%
retract_layer_change = 0
retract_length = 1
retract_length_toolchange = 10
retract_lift = 0
retract_lift_above = 0
retract_lift_below = 0
retract_restart_extra = 0
retract_restart_extra_toolchange = 0
retract_speed = 60
seam_position = nearest
serial_port =
serial_speed = 250000
silent_mode = 1
single_extruder_multi_material = 0
single_extruder_multi_material_priming = 1
skirt_distance = 3
skirt_height = 1
skirts = 3
slice_closing_radius = 0.049
slowdown_below_layer_time = 5
small_perimeter_speed = 25%
solid_infill_below_area = 70
solid_infill_every_layers = 0
solid_infill_extruder = 1
solid_infill_extrusion_width = 0
solid_infill_speed = 75%
spiral_vase = 0
standby_temperature_delta = -5
start_filament_gcode = "; Filament gcode\n"
start_gcode = ;-- PrusaSlicer Start G-Code for M2 starts --\n; Ed Nisley KE4NZU\n; Makergear V4 hot end\n; Origin at platform center, set by MANUAL_X_HOME_POS compiled constants\n; Z-min switch at platform, must move nozzle to X=135 to clear\nG90 ; absolute coordinates\nG21 ; millimeters\nM83 ; relative extrusion distance\nM104 S[first_layer_temperature] ; start extruder heating\nM140 S[first_layer_bed_temperature] ; start bed heating\nM17 ; enable steppers\nG4 P500 ; ... wait for power up\nG92 Z0 ; set Z to zero, wherever it might be now\nG0 Z10 F1000 ; move platform downward to clear nozzle; may crash at bottom\nG28 Y ; home Y to clear plate, offset from compiled constant\nG28 X ; home X, offset from M206 X, offset from compiled constant\nG0 X135 Y0 F15000 ; move off platform to right side, center Y\nG28 Z ; home Z to platform switch, offset from M206 Z measured\nG0 Z2.0 F1000 ; get air under switch\nG0 Y-126 F10000 ; set up for priming, zig around corner\nG0 X0 ; center X\nG0 Y-124.5 ; just over platform edge\nG0 Z0 F500 ; exactly at platform\nM190 S[first_layer_bed_temperature] ; wait for bed to finish heating\nM109 S[first_layer_temperature] ; set extruder temperature and wait\nG1 E20 F300 ; prime to get pressure, generate blob on edge\nG0 Y-123 F5000 ; shear off blob\nG0 X15 F15000 ; jerk away from blob, move over surface\nG4 P500 ; pause to attach\nG1 X45 F500 ; slowly smear snot to clear nozzle\nG1 Z1.0 F2000 ; clear bed for travel\n;-- PrusaSlicer Start G-Code ends --\n
support_material = 0
support_material_angle = 0
support_material_auto = 1
support_material_buildplate_only = 0
support_material_contact_distance = 0.2
support_material_enforce_layers = 0
support_material_extruder = 1
support_material_extrusion_width = 0.31
support_material_interface_contact_loops = 0
support_material_interface_extruder = 1
support_material_interface_layers = 3
support_material_interface_spacing = 0
support_material_interface_speed = 100%
support_material_pattern = rectilinear
support_material_spacing = 2.5
support_material_speed = 60
support_material_synchronize_layers = 0
support_material_threshold = 0
support_material_with_sheath = 1
support_material_xy_spacing = 50%
temperature = 250
thin_walls = 1
threads = 4
thumbnails =
toolchange_gcode =
top_fill_pattern = hilbertcurve
top_infill_extrusion_width = 0
top_solid_infill_speed = 50%
top_solid_layers = 3
top_solid_min_thickness = 0
travel_speed = 300
use_firmware_retraction = 0
use_relative_e_distances = 0
use_volumetric_e = 0
variable_layer_height = 1
wipe = 0
wipe_into_infill = 0
wipe_into_objects = 0
wipe_tower = 0
wipe_tower_bridging = 10
wipe_tower_no_sparse_layers = 0
wipe_tower_rotation_angle = 0
wipe_tower_width = 60
wipe_tower_x = 180
wipe_tower_y = 140
wiping_volumes_extruders = 70,70
wiping_volumes_matrix = 0
xy_size_compensation = 0
z_offset = 0

KeyboardIO Atreus Keymapping

Having a customizable keyboard like the KeyboardIO Atreus means one must customize it. As it turns out, I wanted to use some features of the underlying QMK Kaleidoscope firmware that aren’t exposed by Chrysalis, KeyboardIO’s otherwise competent keymap configuration utility, so what you see below runs on hard mode.

Start by installing QMK, compiling the default Atreus layout, and flashing the keyboard just to confirm all the steps work:

Atreus keyboard - overview
Atreus keyboard – overview

With all that working, add (or create) two lines to the rules.mk file in the keymap directory you’re tweaking:

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

Enabling Auto-Shift lets you generate shifted characters (like Z) by briefly holding down the unshifted key (like z). This requires unlearning an entire lifetime of touch typing practice, but is definitely worthwhile; if a thumb still reaches for the shift key, there’s no harm done. There are, of course, a myriad options, all of which I left unchanged.

Complex passwords suffer, as you must blind-type carefully while tapping each key rapidly.

Enabling Tap Dance lets a key generate one character when tapped and another when double-tapped; you can go crazy with more taps. An enum{} in the keymap.c file generates indexes for the keys and an array holds the action definitions:

enum {
    TD_SPC_ENT,
    TD_BS_DEL,
};

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

Then each key uses a TD() macro in the keymap.c file:

… TD(TD_BS_DEL), … TD(TD_SPC_ENT), …

In contrast, layer shifting uses straightforward built-in macros. The Fun key produces a momentary shift to Layer 1 (known as _RS) when held down:

… MO(_RS), …

The ESC key in the lower left corner emits the expected Escape key code when tapped and switches to Layer 2 (a.k.a. _LW) when held:

LT(_LW,KC_ESC), …

For reference, the current state of the keymap.c file:

// Modified from the KeyboardIO layout
// Ed Nisley - KE4ZNU
// Enable Auto Shift and Tap Dance in rules.mk

#include QMK_KEYBOARD_H

enum layer_names {
    _QW,
    _RS,
    _LW,
};

enum {
    TD_SPC_ENT,
    TD_BS_DEL,
};

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

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [_QW] = LAYOUT( /* Qwerty */
    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(_LW,KC_ESC), KC_TAB, KC_LGUI,  TD(TD_BS_DEL), KC_LSFT,  KC_LCTL, KC_ENT , TD(TD_SPC_ENT),  MO(_RS), KC_MINS, KC_QUOT, KC_BSLS),

  [_RS] = LAYOUT( /* [> RAISE <] */
    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 ),

  [_LW] = LAYOUT( /* [> LOWER <] */
    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(_QW), KC_PSCR, KC_SLCK, KC_PAUS )
};

With all that set up, It Just Works and I can contemplate grafting a status LED into the thing.

MTD Snowthrower: Replacement Throttle Knob

The throttle knob on our MTD snowthrower (a.k.a. snowblower) cracked apart around its metal shaft when I pulled it upward. A temporary fix involving duct tape and cable ties sufficed to start the engine, although the usual intense vibration shook the knob loose somewhere along the driveway during the next hour.

Update: Found it!

Although I have no photographic evidence, I did make a few quick measurements:

Throttle Knob Dimension Doodles
Throttle Knob Dimension Doodles

It fits an MTD model E6A4E, but I suspect nearly all their engines have identical throttle shafts:

Snowthrower Throttle Knob - stem end - solid model
Snowthrower Throttle Knob – stem end – solid model

The only practical way to build the thing has it standing on the shaft end, surrounded by a brim to improve adhesion, so I added (actually, subtracted) a pair of holes for music-wire reinforcements:

Snowthrower throttle knob - reinforcing wires
Snowthrower throttle knob – reinforcing wires

It definitely has a stylin’ look, next to the original choke control knob:

Snowthrower throttle knob - installed
Snowthrower throttle knob – installed

I omitted the finger grip grooves for obvious reasons.

The slot-and-hole came out slightly smaller than the metal shaft and, rather than wait for epoxy to cure, I deployed a 230 W soldering gun (not a piddly temperature-controlled iron suitable for electronics) on the shaft and melted it into the knob.

More snow may arrive this week and I printed another knob just in case …

The OpenSCAD source code as a GitHub Gist:

// MTD Snowthrower Throttle Knob
// Ed Nisley KE4ZNU 2020-12-18
/* [Options] */
Layout = "Show"; // [Build, Show]
// Extrusion parameters
/* [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);
//----------------------
// Dimensions
Throttle = [17.0,1.85,6.5]; // blade insertion, thickness, width
PaddleSize = [25,30,9];
PaddleRound = 4.0;
PaddleThick = 8.5;
StemDia = 13.0;
StemLength = 20.0;
PinDia = 1.6;
PinLength = PaddleSize.x + StemLength/2;
echo(str("Pin: ",PinLength," x ",PinDia," mm"));
//----------------------
// Useful routines
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
FixDia = Dia / cos(180/Sides);
cylinder(r=(FixDia + HoleWindage)/2,
h=Height,
$fn=Sides);
}
//----------------------
// Pieces
module Paddle() {
difference() {
hull() {
translate([PaddleSize.x/2,0,0]) {
for (i=[-1,1], j=[-1,1])
translate([i*(PaddleSize.x - PaddleRound)/2,j*(PaddleSize.y - PaddleRound)/2,0])
sphere(d=PaddleRound,$fn=12);
rotate([0,90,0]) rotate(180/12)
cylinder(d=PaddleThick,h=PaddleSize.x,,center=true,$fn=12);
}
translate([-StemLength,0,0])
rotate([0,90,0]) rotate(180/12)
cylinder(d=StemDia,h=Throttle.x,center=false,$fn=12);
}
translate([-StemLength,0,0])
cube([2*Throttle.x,Throttle.y,Throttle.z],center=true);
translate([-(StemLength + Protrusion),0,0])
rotate([0,90,0]) rotate(0*180/6)
PolyCyl(2*Throttle.y,Throttle.x,6);
for (j=[-1,1])
translate([-StemLength/2,j*PaddleSize.y/6,0])
rotate([0,90,0]) rotate(180/4)
PolyCyl(PinDia,PinLength,4);
}
}
//----------------------
// Build it
if (Layout == "Show")
Paddle();
if (Layout == "Build") {
translate([0,0,StemLength])
rotate([0,-90,0])
Paddle();
}

Astable Multivibrator: Dressed-up LED Spider

Adding a bit of trim to the bottom of the LED spider makes it look better and helps keep the strut wires in place:

Astable Multivibrator - Alkaline - Radome trim
Astable Multivibrator – Alkaline – Radome trim

It’s obviously impossible to build like that, so it’s split across the middle of the strut:

Astable Multivibrator - Alkaline - Radome trim
Astable Multivibrator – Alkaline – Radome trim

Glue it together with black adhesive and a couple of clamps:

LED Spider - glue clamping
LED Spider – glue clamping

The aluminum fixtures (jigs?) are epoxied around snippets of strut wire aligning the spider parts:

LED Spider - gluing fixture
LED Spider – gluing fixture

Those grossly oversized holes came pre-drilled in an otherwise suitable aluminum rod from the Little Tray o’ Cutoffs. I faced off the ends, chopped the rod in two, recessed the new ends, and declared victory. Might need better ones at some point, but they’ll do for now.

Next step: wire up an astable with a yellow LED to go with the green and blue boosted LEDs.

USB Memory: Premature Deaths

After about a year of streaming music, the music died over the course of a month, producing progressively bizarre symptoms on all the local Icecast stations. Killing the streaming server and yanking all the USB memory sticks produced this tableau:

USB Memory - streamer failures
USB Memory – streamer failures

The USB 2.0 32 GB SanDisk Cruzer Fit (tiny, black, upper left) holds images from various network cameras and is not involved with music. It’s nigh onto seven years old and, apparently, still going strong.

The USB 2.0 Centron (gray-and-retroreflective, upper right) was forgotten from the last time I set up a drive for our Forester’s player. There’s another one just like it in the car; they’re impossibly old, as you’d expect from their minuscule size.

The USB 3.0 64 GB Samsung Fit (small, white, lower left) is totally dead, to the extent it doesn’t even announce its presence when plugged into a USB socket. It’s 2.5 years into a five year warranty, but their new USB 3.1 version is twelve bucks; Samsung wins. It formerly contained an extensive selection of public-domain music.

The 64 GB Sandisk Cruzer (huge, black, lower right) suffered some serious damage:

sudo mount -o ro /dev/sdg1 /mnt/part
ll /mnt/part
 ls: cannot access '/mnt/part/PILZ': Input/output error
 total 384K
 drwxr-xr-x   6 ed   users 4.0K Nov 28  2019 ./
 drwxr-xr-x  17 root root  4.0K Jun  7  2019 ../
 -rw-r--r--   1 ed   ed    215K Mar  9  2019 CDClassical.m3u
 drwxrwxr-x  56 ed   ed    4.0K Mar  9  2019 Classical/
 drwx------   2 root root   16K Mar  9  2019 lost+found/
 d?????????   ? ?    ?        ?            ? PILZ/
 drwxrwxr-x 116 ed   ed     12K Mar  9  2019 Pop/
 -rw-r--r--   1 ed   ed    117K Nov 28  2019 Pop.m3u

It still contains a fair amount of music ripped from the CDs we’ve collected over the decades, but it’s obviously unusable. Just for fun, I tried reformatting and copying some files to it, but it eventually hard-crashed with I/O errors:

[37787.872410] usb 2-1: new high-speed USB device number 2 using xhci_hcd
 [37788.013027] usb 2-1: New USB device found, idVendor=0781, idProduct=5530, bcdDevice= 1.00
 [37788.013030] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
 [37788.013032] usb 2-1: Product: Cruzer
 [37788.013034] usb 2-1: Manufacturer: SanDisk
 [37788.013036] usb 2-1: SerialNumber: 4C530001151215101233
 [37788.013604] usb-storage 2-1:1.0: USB Mass Storage device detected
 [37788.014778] scsi host9: usb-storage 2-1:1.0
 [37789.033409] scsi 9:0:0:0: Direct-Access     SanDisk  Cruzer           1.00 PQ: 0 ANSI: 6
 [37789.034569] sd 9:0:0:0: [sdf] 120225792 512-byte logical blocks: (61.6 GB/57.3 GiB)
 [37789.035820] sd 9:0:0:0: [sdf] Write Protect is off
 [37789.035825] sd 9:0:0:0: [sdf] Mode Sense: 43 00 00 00
 [37789.036137] sd 9:0:0:0: [sdf] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
 [37789.086533]  sdf: sdf1
 [37789.089418] sd 9:0:0:0: [sdf] Attached SCSI removable disk
 [38035.071013] EXT4-fs (sdf1): mounting ext3 file system using the ext4 subsystem
 [38035.183172] EXT4-fs (sdf1): mounted filesystem with ordered data mode. Opts: (null)
 [38485.302549] usb 2-1: reset high-speed USB device number 2 using xhci_hcd
 [38490.622285] usb 2-1: device descriptor read/64, error -110
 [38506.195617] usb 2-1: device descriptor read/64, error -110
 [38506.425616] usb 2-1: reset high-speed USB device number 2 using xhci_hcd
 [38511.742339] usb 2-1: device descriptor read/64, error -110
 <<< snippage >>>
 [38548.845743] usb 2-1: USB disconnect, device number 2
 [38548.858925] blk_update_request: I/O error, dev sdf, sector 99556320 op 0x1:(WRITE) flags 0x4800 phys_seg 30 prio class 0
 [38548.858933] EXT4-fs warning (device sdf1): ext4_end_bio:309: I/O error 10 writing to inode 1531939 (offset 0 size 0 starting block 12444541
 )
 [38548.858937] Buffer I/O error on device sdf1, logical block 12444284
 [38548.858944] EXT4-fs warning (device sdf1): ext4_end_bio:309: I/O error 10 writing to inode 1531939 (offset 0 size 0 starting block 12444542
 )
 <<< snippage >>>
 [38548.858984] Buffer I/O error on device sdf1, logical block 12444293
 [38548.859034] blk_update_request: I/O error, dev sdf, sector 99017520 op 0x1:(WRITE) flags 0x4000 phys_seg 3 prio class 0
 [38548.859158] blk_update_request: I/O error, dev sdf, sector 99556560 op 0x1:(WRITE) flags 0x4800 phys_seg 30 prio class 0
 [38548.859224] blk_update_request: I/O error, dev sdf, sector 99017760 op 0x1:(WRITE) flags 0x4000 phys_seg 2 prio class 0
 [38548.859237] blk_update_request: I/O error, dev sdf, sector 99018000 op 0x1:(WRITE) flags 0x4000 phys_seg 2 prio class 0
 >>
 [38549.230765] JBD2: Detected IO errors while flushing file data on sdf1-8
 [38549.230920] Aborting journal on device sdf1-8.
 [38549.231008] Buffer I/O error on dev sdf1, logical block 1545, lost sync page write
 [38549.231011] JBD2: Error -5 detected when updating journal superblock for sdf1-8.
 [38549.231325] Buffer I/O error on dev sdf1, logical block 0, lost sync page write
 [38549.231332] EXT4-fs (sdf1): I/O error while writing superblock
 [38549.231333] EXT4-fs error (device sdf1): ext4_journal_check_start:61: Detected aborted journal
 [38549.231334] EXT4-fs (sdf1): Remounting filesystem read-only
 <<< and so forth and so on >>>

So, yeah, it’s dead, Jim.

The Icecast streaming server reads data continuously from the USB sticks and, given that I set up half a dozen “stations”, there’s plenty of reading going on. The drives are formatted as ext3 and mounted with the noatime option, so there shouldn’t be any writing going on, but it seems a year of constant reading can kill a USB drive.

Fortunately, the original data lives elsewhere, with scripts to copy the appropriate files to the right places, so rebuilding the drives on a pair of new USB sticks wasn’t a big deal.