Optiplex 9010: Xsetwacom vs. Dual Monitors

Having replaced the Dell Optiplex 980 (running from an eBay NOS power supply) with an off-lease Optiplex 9010, I was mildly surprised to find two Displayport outputs from the built-in Intel graphics chipset. Not being a gamer, I don’t care much about graphic performance, but plugging two 2560×1440 monitors into the jacks and having them Just Work was delightful. Indeed, Dell even managed to fix work around the error in the U2711  firmware requiring me to power-cycle the damned thing before booting the PC; now I can just turn the PC on and It Just Works.

Mysteriously, the incantation required to limit the Wacom tablet to the left-hand landscape monitor now uses DP1 instead of HEAD-0:

xsetwacom --verbose set "Wacom Graphire3 6x8 stylus" MapToOutput "DP1"
xsetwacom --verbose set "Wacom Graphire3 6x8 eraser" MapToOutput "DP1"
#xsetwacom --verbose set "Wacom Graphire3 6x8 Pen stylus" MapToOutput "HEAD-0"
#xsetwacom --verbose set "Wacom Graphire3 6x8 Pen eraser" MapToOutput "HEAD-0"

I’ll leave the “HEAD-0 incantations as comments, so as to have a hint the next time …

Raspberry Pi vs. Music via NFS

Every now & again, streaming music from distant servers fails, for no reason I can determine. In that situation, it would be nice to have a local source and, as mplayer works just fine when aimed at an MP3 file, I tried to set up a USB stick on the ASUS router.

That requires getting their version of SAMBA working with the Raspbian Lite installed on the streaming players. After screwing around for far too long, I finally admitted defeat, popped the USB stick into the Raspberry Pi running the APRS iGate in the attic stairwell, and configured it as an NFS server.

To slightly complicate the discussion, there’s also a file server in the basement which turns itself off after its nightly backup. The local music files must be available when it’s off, so the always-up iGate machine gets the job.

On the NFS server:

Install rpcbind and nfs-common, both of which should already be included in stock Raspbian Lite, and nfs-kernel-server, which isn’t. There were problems with earlier Raspbian versions involving the startup order which should be history by now; this post may remind me what’s needed in the event the iGate NFS server wakes up dead after the next power blink.

Set up /etc/exports to share the mount point:

/mnt/music	*(ro,async,insecure,no_subtree_check)
# blank line so you can see the underscores in the previous one

Plug in the USB stick, mount, copy various music directories from the file server’s pile o’ music to the stick’s root directory.

Create a playlist from the directory entries and maybe edit it a bit:

ls -1 /mnt/part/The_Music_Directory > playlist.tmp
sed 's/this/that/' < playlist.tmp > playlist.txt
rm playlist.tmp

Tuck the playlist into the Playlists directory on the basement file server, from whence the streamer’s /etc/rc.local will copy the file to its local directory during the next boot.

On every streamer, create the /mnt/music mountpoint and edit /etc/rc.local to mount the directory:

<<< snippage >>>
mount -v -o ro $nfs_music:/mnt/music /mnt/music
# blank line so you can see the underscores in the previous one 

In the Python streaming program on the file server, associate the new “station” with a button:

         'KEY_KP8'   : ['Newname',False,['mplayer','-shuffle','-playlist','/home/pi/Playlists/playlist.txt']],

The startup script also fetches the latest copy of the Python program whenever the file server is up, so the new version should Just Work.

I set the numeric keypad button associated with that program as the fallback in case of stream failures, so when the Interwebs go down, we still have music. Life is good …

Vacuum Tube LEDs: Knockoff Arduino Nano USB Connector

The LEDs adorning the 0D3 rectifier tube became unreliable:

0D3 Octal - 25 mm socket - raised LED
0D3 Octal – 25 mm socket – raised LED

After failing to plug in a different USB power supply, a close look at the USB connector showed the problem:

Knockoff Arduino Nano - broken Mini-B connector
Knockoff Arduino Nano – broken Mini-B connector

A bit of needle-nose tweezering extracted the culprit from the power supply’s connector:

Knockoff Arduino Nano - broken Mini-B connector - fragment
Knockoff Arduino Nano – broken Mini-B connector – fragment

I tried applying the world’s smallest dot of epoxy to the fracture, probably slobbered epoxy along the pins while reinserting it, and the Nano still doesn’t light up.

Given that knockoff Nano boards cost a touch over two bucks delivered, it’s not clear transplanting a connector from one of the never-sufficiently-to-be-damned counterfeit FTDI USB adapters makes any sense.

Vacuum Tube LEDs: Mogul Bulb Side Light

The knockoff Neopixel on the 500 W mogul-base bulb failed in the usual way, so I rebuilt it with an SK6812 RGBW LED in a round cap:

Mogul lamp socket - SK6812 LED side cap
Mogul lamp socket – SK6812 LED side cap

The nice 1-¼ inch stainless socket-head cap screws replace the 1 inch pan-head screws that engaged maybe one thread due to the additional spacer between the USB port and the upper hard drive platter I added for good looks.

I tried a few iterations of an aluminized Mylar (*) disk with various sized pinholes over the RGB trio to crisp up the filament shadow, because the SK6812 LED casts a more diffuse light than the W2812 LEDs:

Aluminized Mylar pinholes for SK6812 RGBW LED
Aluminized Mylar pinholes for SK6812 RGBW LED

Even the ⅛ inch pinhole made the bulb too dim, so I settled for a fuzzy shadow:

500 W Mogul bulb - SK6812 RGBW LED - no pinhole - green phase
500 W Mogul bulb – SK6812 RGBW LED – no pinhole – green phase

The firmware has a tweak forcing the white LED to PWM=0, because this bulb looks better in saturated colors.

(*) Here on earth, aluminized Mylar is nonconductive.

Byonics TinyTrak3+ vs. RFI

Some weeks ago, the APRS + voice adapter on my radio began randomly resetting during our rides, sending out three successive data bursts: the TinyTrak power-on message, an ID string, and the current coordinates. Mary could hear all three packets quite clearly, which was not to be tolerated.

I swapped radios + adapters so that she could ride in peace while I diagnosed the problem, which, of course, was both intermittent and generally occurred only while on the road. The TinyTrak doc mentions “… a sign of the TinyTrak3 resetting due to too much local RF energy”, so I clamped ferrite cores around All! The! Cables! and the problem Went Away.

Removing one core each week eventually left the last core on the GPS receiver’s serial cable, which makes sense, as it plugs directly into the TT3. The core had an ID large enough for several turns (no fool, I), another week established a minimum of three turns kept the RFI down, so I settled for five:

KG-UV3D APRS - ferrite on TT3 GPS cable
KG-UV3D APRS – ferrite on TT3 GPS cable

Prior to the RFI problem cropping up, nothing changed. Past experience has shown when I make such an assertion, it means I don’t yet know what changed. Something certainly has and not for the better.

I swapped the radios + adapters and all seems quiet.

Tour Easy Daytime Running Light

Pending more test rides, the flashlight fairing mount works well:

Tour Easy Fairing Flashlight Mount - front overview
Tour Easy Fairing Flashlight Mount – front overview

Despite all my fussing with three rotational angles, simply tilting the mount upward by 20° with respect to the fairing clamp aims the flashlight straight ahead, with the ball nearly centered in the clamp:

Tour Easy Fairing Flashlight Mount - front detail
Tour Easy Fairing Flashlight Mount – front detail

That obviously depends on the handlebar angle and the fairing length (which affects the strut rotation), but it’s close enough to make me think a simpler mount will suffice: clamp the flashlight into a cylinder with a slight offset angle, maybe 2°, then mount the cylinder into a much thinner ring clamp at the 20° tilt. Rotating the cylinder would give you some aim-ability, minus the bulk of a ball mount.

Or dispense with the separate cylinder, build the entire mount at the (now known) aim angle, clamp the flashlight directly into the mount, then affix mount to fairing strut. Rapid prototyping FTW!

For now, it’s great riding weather …

The OpenSCAD source code as a GitHub Gist:

// Tour Easy Fairing Flashlight Mount
// Ed Nisley KE4ZNU - July 2017
/* [Build Options] */
FlashName = "AnkerLC40"; // [AnkerLC40,AnkerLC90,J5TactV2,InnovaX5]
Component = "Mount"; // [Ball, BallClamp, Mount, Plates, Bracket]
Layout = "Show"; // [Build, Show]
Support = false;
MountSupport = true;
/* [Extrusion] */
ThreadThick = 0.25; // [0.20, 0.25]
ThreadWidth = 0.40; // [0.40]
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.01; // [0.01, 0.1]
HoleWindage = 0.2;
/* [Fairing Mount] */
ToeIn = 0; // inward from ahead
Tilt = 20; // upward from forward
Roll = 0; // outward from top
Shift = -5; // realign to plate center
//- Screws *c
/* [Hidden] */
ID = 0;
OD = 1;
/* [Screws and Inserts] */
BallInsert = [2.0,3.5,4.0];
BallScrew = [2.0,3.5,2.0];
ClampInsert = [3.0,4.2,8.0];
ClampScrew = [3.0,5.9,50.0]; // thread dia, head OD, screw length
ClampScrewWasher = [3.0,6.75,0.5];
ClampScrewNut = [3.0,6.1,4.0]; // nyloc nut
/* [Hidden] */
F_NAME = 0;
LightBodies = [
NumSides = 8*4;
echo(str("Flashlight: ",FlashName));
FlashIndex = search([FlashName],LightBodies,1,0)[F_NAME];
BallThick = IntegerMultiple(5.0,ThreadWidth); // thickness of ball wall
echo(str("Ball wall: ",BallThick));
BallOD = max(45,IntegerMultiple(LightBodies[FlashIndex][F_GRIPOD] + 2*(BallThick + BallInsert[OD]),2.0));
echo(str(" OD: ",BallOD));
BallScrewOC = BallOD - BallThick - BallInsert[OD]; // from OD to allow different body diameters
echo(str(" screw OC: ",BallScrewOC));
BallLength = min(sqrt(pow(BallOD,2) - pow(LightBodies[FlashIndex][F_GRIPOD],2)),
echo(str(" hole len: ",BallLength));
ClampThick = 2*ClampInsert[OD];
echo(str("Clamp wall: ",ClampThick));
ClampOD = BallOD + 2*ClampThick;
echo(str(" OD: ",ClampOD));
ClampScrewOC = BallOD + 2*ClampInsert[OD];
echo(str(" screw OC: ",ClampScrewOC));
ClampLength = 0.70 * BallLength;
echo(str(" length: ",ClampLength));
//- Adjust hole diameter to make the size come out right
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);
//- Ball around flashlight
// Must print two!
module BodyBall() {
difference() {
intersection() {
sphere(d=BallOD,$fn=2*NumSides); // basic ball
cube([BallLength,2*BallOD,2*BallOD],center=true); // max of flashlight grip length
rotate([0,90,0]) rotate(180/NumSides)
PolyCyl(LightBodies[FlashIndex][F_GRIPOD],2*BallOD,NumSides); // flashlight body
for (j=[-1,1])
translate([0,j*BallScrewOC/2,0]) // commmon screw offset
PolyCyl(BallInsert[ID],2*BallOD,6); // punch screw shaft through everything
PolyCyl(BallInsert[OD],(BallInsert[LENGTH] + 3*ThreadThick + Protrusion),6); // threaded insert
PolyCyl(BallScrew[OD],BallOD,6); // screw head clearance
translate([0,0,-BallOD/2]) // remove bottom half
translate([0,0,BallOD - BallThick/2]) // slice off top = bottom for E-Z build
if (Support) {
NumRibs = 24;
RibHeight = (BallOD - LightBodies[FlashIndex][F_GRIPOD]/cos(180/NumSides) - BallThick) / 2;
ChordC = 2*sqrt(BallThick*BallOD/2 - pow(BallThick/2,2));
intersection() {
cube([BallLength,2*BallOD,2*BallOD],center=true); // max of flashlight grip length
translate([0,0,BallOD/2 - BallThick/2])
for (i=[0:NumRibs - 1])
rotate(i*360/NumRibs + 180/NumRibs) // avoid screw holes
translate([ChordC/2 + BallOD/8,0,-RibHeight/2])
//- Fairing Bracket
// Magic numbers taken from the actual fairing mount
// Centered on screw hole
/* [Hidden] */
inch = 25.4;
BracketHoleOD = 0.25 * inch; // 1/4-20 bolt holes
BracketHoleOC = 1.0 * inch; // fairing hole spacing
// usually 1 inch, but 15/16 on one fairing
Bracket = [48.0,16.3,3.6 - 0.6]; // fairing bracket end plate overall size
BracketHoleOffset = (3/8) * inch; // end to hole center
BracketM = 3.0; // endcap arc height
BracketR = (pow(BracketM,2) + pow(Bracket[1],2)/4) / (2*BracketM); // ... radius
module Bracket() {
difference() {
translate([(Bracket[0]/2 - BracketHoleOffset),0,0])
intersection() {
union() {
for (i=[-1,0,1]) // middle circle fills gap
translate([i*(Bracket[0]/2 - BracketR),0])
circle(d=BracketHoleOD/cos(180/8),$fn=8); // dead center at the origin
//- General plate shape
// Centered on the hole for the fairing bracket
Plate = [100.0,30.0,6*ThreadThick + Bracket[2]];
PlateRad = Plate[1]/4;
echo(str("Base plate thick: ",Plate[2]));
module PlateBlank() {
difference() {
intersection() {
translate([0,0,Plate[2]/2]) // select upper half of spheres
for (i=[-1,1], j=[-1,1])
translate([i*(Plate[0]/2 - PlateRad),j*(Plate[1]/2 - PlateRad),0])
sphere(r=PlateRad); // nice rounded corners!
translate([2*BracketHoleOC,0,-Protrusion]) // screw holes
//- Inner plate
module InnerPlate() {
difference() {
translate([0,0,Plate[2] - Bracket[2] + Protrusion]) // punch out fairing bracket
//- Clamp around flashlight ball
module BallClamp() {
BossLength = ClampScrew[LENGTH] - ClampScrewNut[LENGTH] - 2*ClampScrewWasher[LENGTH] - 4*ThreadThick;
difference() {
union() {
intersection() {
sphere(d=ClampOD,$fn=NumSides); // exterior ball blamp
cube([ClampLength,2*ClampOD,2*ClampOD],center=true); // aiming allowance
for (i=[0])
hull() {
for (j=[-1,1])
translate([i*(ClampLength/2 - ClampScrew[OD]),j*ClampScrewOC/2,-BossLength/2])
cylinder(d=(ClampScrewWasher[OD] + 2*ThreadWidth),h=BossLength,$fn=8);
sphere(d=(BallOD + 1*ThreadThick),$fn=NumSides); // interior ball
for (i=[0] , j=[-1,1]) {
translate([i*(ClampLength/2 - ClampScrew[OD]),j*ClampScrewOC/2,-ClampOD]) // screw clearance
if (Support) { // ad-hoc supports for top half
NumRibs = 6;
RibLength = 0.5 * BallOD;
RibWidth = 1.9*ThreadWidth;
SupportOC = ClampLength / NumRibs;
cube([ClampLength,RibLength,4*ThreadThick],center=true); // base plate for adhesion
intersection() {
sphere(d=BallOD - 0*ThreadWidth); // cut at inner sphere OD
cube([ClampLength + 2*ThreadWidth,RibLength,BallOD],center=true);
union() { // ribs for E-Z build
for (j=[-1,0,1])
for (i=[0:NumRibs]) // allow +1 to fill the far end
translate([i*SupportOC - ClampLength/2,0,0])
cylinder(d=BallOD - 2*ThreadThick,
//- Mount between fairing plate and flashlight ball
module Mount() {
intersection() {
if (MountSupport) { // anchor outer corners during worst overhang
RibWidth = 1.9*ThreadWidth;
SupportOC = 0.1 * ClampLength;
difference() {
translate([Shift + 0.3,0,0])
for (i=[-4.5,-2.5,0,2.0,4.5])
translate([i*SupportOC - 0.0,0,(ClampThick + Plate[2])/2])
cube([RibWidth,0.8*ClampOD,(ClampThick + Plate[2])],center=true);
# translate([Shift,0,ClampOD/2])
sphere(d=ClampOD - 2*ThreadWidth,$fn=NumSides);
//- Build things
if (Component == "Ball")
if (Layout == "Show")
else if (Layout == "Build") {
translate([0,+1*(BallOD/2 + BallThick/2),0])
translate([0,0,BallOD/2 - BallThick/2])
translate([0,-1*(BallOD/2 + BallThick/2),0])
translate([0,0,BallOD/2 - BallThick/2])
if (Component == "BallClamp")
if (Layout == "Show")
else if (Layout == "Build") {
Both = false;
difference() {
union() {
translate([Both ? ClampLength : 0,0,0])
if (Both)
if (Component == "Mount")
if (Component == "Plates") {
if (Component == "Bracket")