The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Tag: CNC

Making parts with mathematics

  • Automated Cookie Cutters: Plastic!

    The jellyfish cookie cutter and press look about the way you’d expect:

    Jellyfish Cookie Cutter and Press - separate
    Jellyfish Cookie Cutter and Press – separate

    They’re a bit more impressive on the build platform, where the skirt thread around the perimeter extends slightly beyond the usual 100 mm width limit into the no-go zone behind the nozzle wiper. The bizarre lighting from the warm-white front LEDs and the cool-white overhead LED ring emphasizes the 3D features:

    Jellyfish Cookie Cutter - on build platform
    Jellyfish Cookie Cutter – on build platform

    Somewhat to my surprise, the gritty nature of the bitmap source image didn’t cause a problem. The perimeters consist of many tiny segments, but most of the time goes to filling the interior (20% density, square pattern) and covering the flat surfaces, so the whole thing chugs along at a pretty good pace. Overall, it took something over an hour.

    So, given a height-map grayscale image:

    Manually tweaked jellyfish-high.png
    Manually tweaked jellyfish-high.png

    I (and you!) can automatically create the solid model of a matched cookie cutter and press:

    jellyfish-high - Cutter and Press - top view
    jellyfish-high – Cutter and Press – top view

    And then we can produce as many chunks of plastic as needed for our baking session!

    Printing that huge block of plastic did, however, uncover two longstanding mechanical problems. More tomorrow…

  • Automated Cookie Cutters: Putting It All Together

    With that musing, image processing, and data reformatting in hand, this now seems to work pretty well…

    This is MakeCutter.sh, the Bash file that controls the overall process:

    #!/bin/bash
    DotsPerMM=3
    MapHeight=5
    ImageName=${1%%.*}
    rm ${ImageName}_* ${ImageName}.stl
    echo Normalize and prepare grayscale image...
    convert $1 -trim -type Grayscale -depth 8 -auto-level -flip -negate -flop ${ImageName}_prep.png
    echo Create PGM files...
    convert ${ImageName}_prep.png -compress none ${ImageName}_map.pgm
    convert ${ImageName}_prep.png -white-threshold 1 -compress none ${ImageName}_plate.pgm
    echo Create height map data files...
    ImageX=`identify -format '%[fx:w]' ${ImageName}_map.pgm`
    ImageY=`identify -format '%[fx:h]' ${ImageName}_map.pgm`
    cat ${ImageName}_map.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > {ImageName}_map.dat
    cat ${ImageName}_plate.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_plate.dat
    echo Create height map STL files...
    time openscad -D DotsPerMM=$DotsPerMM -D Height=$MapHeight -D FileName=\"${ImageName}_map.dat\" -o ${ImageName}_map.stl MakeSurface.scad
    time openscad -D DotsPerMM=$DotsPerMM -D Height=255 -D FileName=\"${ImageName}_plate.dat\" -o ${ImageName}_plate.stl MakeSurface.scad
    echo Create cookie press and cutter...
    time openscad -D fnPress=\"${ImageName}_map.stl\" -D fnPlate=\"${ImageName}_plate.stl\" -D ImageX=$ImageX -D ImageY=$ImageY -o ${ImageName}.stl Cookie\ Cutter.scad
    

    Hand it an image file name and it just does the rest:

    ./MakeCutter.sh jellyfish-high.png
    

    I assume you’ll pick a workable dots/mm resolution and suchlike for your setup, so they’re hardcoded into the script. You could add them to the command line if you like.

    Starting from the same jellyfish.svg file as before, I came up with a slightly different jellyfish-high.png grayscale height map image with more dots (246×260 dots at 3 dot/mm = 82×87 mm). The original 160×169 file required about half an hour to render and, as you’d expect, increasing the number of dots by 1.5 (nearly √2) doubled the time to just over an hour:

    Manually tweaked jellyfish-high.png
    Manually tweaked jellyfish-high.png

    The first convert step turns that into the basic height map jellyfish_prep.png file:

    jellyfish-high_prep.png
    jellyfish-high_prep.png

    The next two convert steps produce two ASCII PGM files:

    • jellyfish-high_map.pgm
    • jellyfish-high_plate.pgm

    The _map file has the grayscale height map data, which is identical to the prepared PNG image:

    jellyfish-high_map
    jellyfish-high_map

    The _plate file defines the outline of the cookie cutter with a completely white interior, which is why the original cookie height map image can’t contain any pure white areas inside the outline (they’d become black here and produce islands). It seems the OpenSCAD minkowski() function runs significantly faster when it doesn’t process a whole bunch of surface detail; all we care about is the outline, so that’s all it gets:

    jellyfish-high_plate
    jellyfish-high_plate

    I originally composited the height maps on a known-size platen and worked with those dimensions, but it’s easier to just extract the actual image dimensions and feed them into the code as needed. As with all ImageMagick programs, identify has a myriad options.

    Those two lines of Bash gibberish reformat the ASCII PGM files into the ASCII DAT arrays required by OpenSCAD’s surface() function.

    The MakeSurface.scad script eats a DAT height map file and spits out a corresponding STL file. The Height parameter defines the overall Z-axis size of the slab; the maximum 255 corresponds to pure white in the image file:

    // Generate object from image height map
    // Ed Nisley - KE4ZNU
    // October 2012
    // This leaves the rectangular slab below Z=0 over the full image area
    //		... reduced in thickness by the Height/255 ratio
    
    FileName = "noname.dat";				// override with -D FileName="whatever.dat"
    Height = 255;						// overrride with -D Height=number
    DotsPerMM = 2;						// overrride with -D DotsPerMM=number
    
    ScaleXYZ = [1/DotsPerMM,1/DotsPerMM,Height/255];
    
    echo("File: ",FileName);
    echo("Height: ",Height);
    echo("Dots/mm: ",DotsPerMM);
    echo("Scale: ",ScaleXYZ);
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
      Range = floor(50 / Space);
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    }
    
    //- Build it
    
    ShowPegGrid();
    
    scale(ScaleXYZ)
    	surface(FileName,center=true,convexity=10);
    

    The grid defining the build platform doesn’t show up in the output file; it’s there just for manual fiddling inside the GUI. When run from the command line, OpenSCAD simply creates the output file.

    The _map.stl file has the height map data that will form the cookie press:

    jellyfish-high_map - Model
    jellyfish-high_map – Model

    The _plate.stl file is basically a digital bar cookie that will define the cutter:

    jellyfish-high_plate - Model
    jellyfish-high_plate – Model

    It’s possible to produce the cutter in one shot, starting from the DAT files, but having the two STL files makes it (barely) feasible to experiment interactively within the OpenSCAD GUI.

    This OpenSCAD program produces the cutter and press:

    // Cookie cutter from grayscale height map using Minkowski sum
    // Ed Nisley KE4ZNU - November 2012
    
    //-----------------
    // Cookie cutter files
    
    BuildPress = true;
    BuildCutter = true;
    
    fnPress = "nofile.stl";					// override with -D 'fnPress="whatever.stl"'
    fnPlate = "nofile.stl";					// override with -D 'fnPlate="whatever.stl"'
    
    ImageX = 10;						// overrride with -D ImageX=whatever
    ImageY = 10;
    
    MaxConvexity = 5;					// used for F5 previews in OpenSCAD GUI
    
    echo("Press File: ",fnPress);
    echo("Plate File: ",fnPlate);
    echo("Image X: ",ImageX," Y: ",ImageY);
    
    //- Extrusion parameters - must match reality!
    
    ThreadThick = 0.25;
    ThreadWidth = 2.0 * ThreadThick;
    
    //- Cookie cutter parameters
    
    TipHeight = IntegerMultiple(8,ThreadThick);		// cutting edge
    TipWidth = 4*ThreadWidth;
    
    WallHeight = IntegerMultiple(7,ThreadThick);		// center section
    WallWidth = 8*ThreadWidth;
    
    LipHeight = IntegerMultiple(2.0,ThreadThick);		// cutter handle
    LipWidth = IntegerMultiple(8.0,ThreadWidth);
    
    CutterGap = IntegerMultiple(2.0,ThreadWidth);		// gap between cutter and press
    
    PlateThick = IntegerMultiple(3.0,ThreadThick);		// solid plate under press relief
    
    //- Build platform
    
    PlatenX = 100;						// build platform size
    PlatenY = 120;
    PlatenZ = 120;						// max height for any object
    PlatenFuzz = 2;
    
    MaxSize = max(PlatenX,PlatenY);				// larger than any possible dimension ...
    
    ZFuzz = 0.20;						// height of numeric junk left by grayscale conversion
    ZCut = 1.20;						// thickness of block below Z=0
    
    //- Useful info
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;					// make holes and unions work correctly
    
    //-----------------
    // Import height map STL, convert to cookie press image
    
    module PressSurface() {
    
    	translate([0,0,-ZFuzz])
    		difference() {
    			import(fnPress,convexity=MaxConvexity);
    			translate([-(ImageX + PlatenFuzz)/2,-(ImageY + PlatenFuzz)/2,-ZCut])
    				cube([(ImageX + PlatenFuzz),(ImageY + PlatenFuzz),ZCut+ZFuzz],center=false);
    		}
    }
    
    //-----------------
    // Import plate STL, slice off a slab to define outline
    
    module Slab(Thick=1.0) {
    	intersection() {
    		translate([0,0,Thick/2])
    			cube([(PlatenX+PlatenFuzz),(PlatenY+PlatenFuzz),Thick],center=true);
    		translate([0,0,-1])
    			import(fnPlate,convexity=MaxConvexity);
    	}
    }
    
    //- Put peg grid on build surface
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      Range = floor(50 / Space);
    
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //- Build it
    
    ShowPegGrid();
    
    if (BuildPress) {
    	echo("Building press");
    	union() {
    		minkowski() {					// fingernail ridge around press
    			Slab(LipHeight - 1);			//  ... same thickness as cutter lip
    			cylinder(r=CutterGap/2,h=1);
    		}
    		translate([0,0,(LipHeight - Protrusion)])	// solid plate under press
    			Slab(PlateThick - LipHeight + Protrusion);
    		translate([0,0,PlateThick])			// cookie press height map
    			intersection() {
    				import(fnPress,convexity=MaxConvexity);
    				translate([0,0,-Protrusion])
    					Slab(PlatenZ + Protrusion);
    			}
    
    	}
    }
    
    if (BuildCutter) {
    	echo("Building cutter");
    	difference() {
    		union() {					// stack cutter layers
    			translate([0,0,(WallHeight + LipHeight - 1)])
    				minkowski() {
    					Slab(TipHeight - 1);
    					cylinder(r=(TipWidth + CutterGap),h=1);
    				}
    			translate([0,0,LipHeight - 1])
    				minkowski() {
    					Slab(WallHeight - 1);
    					cylinder(r=(WallWidth + CutterGap),h=1);
    				}
    			minkowski() {
    				Slab(LipHeight - 1);
    				cylinder(r=(LipWidth + CutterGap),h=1);
    			}
    		}
    		minkowski() {					// punch out opening for cookie press
    			translate([0,0,-2])
    				Slab(PlatenZ);
    			cylinder(r=CutterGap,h=1);
    		}
    	}
    }
    

    The top view shows the height map press nested inside the cutter blade, but it’s not obvious they’re two separate pieces:

    jellyfish-high - Cutter and Press - top view
    jellyfish-high – Cutter and Press – top view

    The bottom view shows the 1 mm gap between the two objects:

    jellyfish-high - Cutter and Press - bottom view
    jellyfish-high – Cutter and Press – bottom view

    Now, to print the thing [Update: like that] so I can make some cookies…

    Further musings:

    • Printing separately would allow a tighter fit between the press and the cutter: an exact fit won’t work, but 2 mm may be too much.
    • A knob glued on the flat surface of the press would be nice; the fingernail ridge may get annoying.
    • PLA seems more socially acceptable than ABS for foodie projects. Frankly, I doubt that it matters, at least in this application.
    • A bigger build platform would be nice…
  • Automated Cookie Cutters: Creating the Height Map Data File

    That process produces a grayscale height map PNG image in the proper orientation:

    Jellyfish - prepared image
    Jellyfish – prepared image

    OpenSCAD, however, requires a flat ASCII data file to build a 3D model, as described there.

    It turns out that a PGM (“Portable Graymap”) file is almost exactly what we need: a fixed format header in ASCII text, followed by the pixel data in either ASCII or binary. To get an ASCII-formatted PGM file from that PNG image:

    convert jellyfish_prep.png -compress None jellyfish.pgm
    

    The top of the PGM file looks like this:

    P2
    149 159
    255
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 255
    ... more data ...
    

    The data isn’t in the XY array layout that OpenSCAD expects; It Would Be Very Nice If OpenSCAD could read PGM files, including the header describing the array size, but it doesn’t. Fortunately, some Bash-fu can handle the reformatting.

    First, store the number of pixels along the X axis in a Bash variable:

    ImageX=`identify -format '%[fx:w]' ${imagename}_prep.png`
    

    Note the backticks around the whole mess that tell Bash to execute what’s inside and return the value. The -format operation returns the width as an integer, which is what we need.

    Now, returning our attention to the PGM file, convert multiple blanks and line ends to single line ends, thus putting one entry on each output line with no other whitespace:

    cat filename.pgm | tr -s ' \012' '\012'
    

    Nota bene: there’s a leading blank in the first character string and the escape sequences should read “reverse-slash zero one two” to denote the Unix-style line end character (ASCII 10 = newline). I think the meta-markup works around the usual WordPress formatting, but ya never know what can go wrong.

    The first four lines then contain the magic number, X size, Y size, and the maximum data value, respectively:

    P2
    149
    159
    255
    

    Those values are all known, because:

    • The magic number is P2 for ASCII PGM files. It would be useful to verify this, but …
    • The XY values correspond to the image size
    • The maximum data value will be 255 because of  the auto-level operation applied to the image’s 8-bit grayscaleness

    Strip off the first four lines and wrap the remaining data into an array corresponding to the image size:

    tail -n +5 | column -x -c $((8*${ImageX})) > filename.dat
    

    The $((8*${ImageX)) magic comes from the way the column command works: it’s right-aligning each data values in an 8 character column, so you specify the total width of the result in character columns. Think of the parameter as specifying the screen width and you’ll be on the right track.

    That fits neatly into a single line of Bash gibberish:

    cat filename.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*${ImageX})) > filename.dat
    

    The first complete line of that file goes on basically forever, but it actually has the right stuff. You can examine it thusly:

    head -1 filename.dat
    

    The file should then Just Work when sucked into OpenSCAD with the surface() function:

    surface("filename.dat",center=true,convexity=10);
    

    And, indeed, it does:

    Jellyfish - surface model
    Jellyfish – surface model

    Note that it’s oriented with the head in the +Y direction, the tentacles (or whatever) in the -Y direction, and the freckle over the eye on the proper side. Here’s the original PNG image of the cookie for reference:

    Jellyfish - height map image
    Jellyfish – height map image

    Using center=true centers the object on the XY plate, but the base of the solid remains at Z=-1. That makes some sense, as the “solid” part of the model lies below the Z values set by the data: the model includes a one unit thick slab below Z=0 for all points.

    The convexity=10 parameter helps OpenSCAD’s quick rendering code (invoked by hitting F5 in the GUI) determine when it can stop looking for intersections between the visible ray and the object. It doesn’t affect the F6 CGAL compilation & export to STL. Because this routine will eventually be used only from the command line, the value doesn’t matter.

    That works and the height map looks OK, but the model is too large in all directions and the slab below Z=0 has got to go. But it’s looking good…

  • Automated Cookie Cutters: Height Map Image File Preparation

    Having established the OpenSCAD can produce a height map from an input array, a bit more doodling showed how to produce such an array from a grayscale image. I certainly didn’t originate all of this, but an hour or two of searching with the usual keywords produced snippets that, with a bit of programming-as-an-experimental-science tinkering, combine into a useful whole.

    Not being much of an artist, I picked a suitable SVG image from the Open ClipArt Library:

    Jellyfish - color
    Jellyfish – color

    That’s pretty, but we need a grayscale image. Some Inkscape fiddling eliminated all the nice gradients, changed the outline to a dark gray, made all the interior fills a lighter gray, and tweaked the features:

    Jellyfish - gray
    Jellyfish – gray

    Admittedly, it looks rather dour without the big smile, but so it goes. This is still an SVG file, so you have vector-mode lines & areas.

    A bit more work changed the grays to produce different heights, duplicated one of the spots for obvious asymmetry, and exported it as a gritty 160×169 pixel PNG image:

    Jellyfish - height map image
    Jellyfish – height map image

    The low resolution corresponds to a 2 pixel/mm scale factor: 169 pixel = 84.5 mm tall. The cutter wrapped around this image will have a lip that adds about 12 mm, a 1 or 2 mm gap separates the press from the cutter, and there’s a skirt around the whole affair. My Thing-O-Matic build platform measures a scant 120 mm in the Y direction, which puts a real crimp on the proceedings.

    That’s assuming the usual 1 unit = 1 mm conversion factor. If your toolchain regards units as inches, then you need a different scale factor.

    Low resolution also speeds up the OpenSCAD processing; you can use as many pixel/mm as you wish, but remember that the extruded filament is maybe 0.5 mm wide, so anything beyond 4 pixel/mm might not matter, even if the motion control could benefit from the smoother sides. Features down near the resolution limit of the model may produce unusual effects for thin walls near the thread width, due to interpolation & suchlike (which is why I got rid of the smile). The processing time varies roughly with the number of pixels, so twice the resolution means four times more thumb-twiddling.

    Caveats:

    • You’re looking at a cookie lying on a table: this is the top view
    • Background surrounding the image should be full white = 255
    • Highest points should be very light gray, not full white, to avoid creating islands
    • Lowest points may be black; I use a very dark gray
    • No need for an outline
    • Smooth gradients are OK, although they’ll become harshly quantized by the layer thickness
    • You can probably use JPG instead of PNG, but these aren’t big files
    • Remember this is a cookie press, not a work of art

    With a suitable PNG image file in hand, use ImageMagick to prepare the image:

    • Crop to just the interesting part: -trim (depends on the four corners having background color)
    • Convert the image to grayscale: -type Grayscale (in case it’s a color image)
    • Make it 8 bit/pixel: -depth 8 (more won’t be helpful)
    • Stretch the contrast: -auto-level (to normalize the grayscale to the full range = full height)
    • Reverse left-to-right to make a cookie press: -flop (think about it)
    • Invert the grayscale to make the cookie press surface: -negate (again, think about it)
    • Reverse top-to-bottom to correct for upcoming OpenSCAD surface() reversal: -flip

    Thusly:

    convert filename.png -trim -type Grayscale -depth 8 -auto-level -flop -negate -flip filename_prep.png
    

    Which produces this image:

    Jellyfish - prepared image
    Jellyfish – prepared image

    Combining -flop and -flip just rotates the image 180° around its center, but I can’t help but believe transposing the bits works out better & faster than actually rotating the array & interpolating the result back to a grid. On the other paw, if there isn’t a special case for (multiples of) right-angle rotation(s), there should be. [grin]

    The prepared image is 149×159, because the -trim operation removed the surrounding whitespace. You can do that manually, of course, keeping in mind that the corners must be full white to identify the background.

    Next: convert that image to a data array suitable for OpenSCAD’s surface() function…

  • Automated Cookie Cutters: OpenSCAD surface() Function

    While pondering the notion of making cookie cutters, it occurred to me that the process could be automated: a grayscale height map image of the cookie should be enough to define the geometry. The existing height map solutions (like William Adams’s fine work) seem entirely too heavyweight, what with custom Windows or Java programs and suchlike. Some doodling indicates that a simpler solution may suffice for my simple needs, although the devil always hides in the details.

    The overall problem with a cookie press involves producing a non-rectangular solid with a bumpy upper surface corresponding to the grayscale values of an image: dark pixels = low points of the surface, light pixels = peaks. The image size controls the XY extent of the solid and the pixel values control the Z, with some known value (likely either black or white) acting as a mask around the perimeter. Given such a solid, you can then wrap a cutter blade and handle around the outline, much as I did for the Tux cutter.

    OpenSCAD has a lightly documented surface() function that reads an ASCII text file consisting of an array of numeric values. Each array element defines a 1 × 1 unit square of the resulting 3D object; the example in the doc shows a 10 x 10 array producing a 10 x 10 unit object. Each numeric value sets the height of the surface at the center of the square.

    This array, slightly modified from the one in the doc, shows how that works:

    9 9 8 7 6 5 5 5 5 1
    9 9 7 6 6 4 3 2 1 0
    8 7 6 6 4 3 2 1 0 0
    7 6 6 4 3 2 1 0 0 0
    6 6 4 3 2 1 1 0 0 0
    6 6 3 2 1 1 1 0 0 0
    6 6 2 1 1 1 9 9 9 0
    6 6 1 0 0 0 9 8 9 0
    3 1 0 0 0 0 9 9 9 0
    9 8 7 6 5 4 3 2 1 0
    

    Feeding that into this OpenSCAD program:

    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      Range = floor(50 / Space);
    
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    ShowPegGrid();
    surface("/tmp/test.dat",center=true,convexity=10);
    

    Produces this object, surrounded by a few non-printing 1 unit alignment cubes on the Z=0 plane for scale:

    Example Object
    Example Object

    Some things to note:

    • The text array looks like it builds downward from the upper left, but the solid model builds from the origin toward the +X and +Y directions, with the first line of the array appearing along Y=0. This reverses the object along the Y axis: the first line of the array is the front side of the object.
    • The “center=true” option centers the object in XY around the Z axis, with a 1 unit thick slab below the entire array; the top surface of that slab (at Z=0) represents the level corresponding to 0 elements in the array.
    • Each array element becomes a square one unit on a side; the RepRap software chain regards units as millimeters
    • The center point of each element’s square is at the nominal height
    • The Z coordinate of the edges of those squares linearly interpolate between adjacent centers
    • Vertical edges become slanted triangular facets

    Remember that STL files contain a triangular tessellation (or whatever you call it in 3D) of the object surface, which means rectangles aren’t natural. The edge interpolation make the whole thing work, because an array of pure square pillars probably won’t be a 2-manifold object: some pillars would share only a common vertical edge. The interpolation does, however, produce a bazillion facets atop the object.

    So the problem reduces to generating such an array from a grayscale image, for which some ImageMagick and Bash-fu should suffice, and then manipulating it into a model that will produce a cookie press and cutter. More on that tomorrow…

    [Update: image file, height map file, solid modeling, printing]

  • Longboard Electronics Case: Now With Mouse Ears

    Our Larval Engineer may have a commission to fit her Speed-Sensing Ground Effect Lighting controller to another longboard. To that end, the case now sports mouse ears to spread the force from the cooling ABS over more of the Kapton tape, in the hope the plastic won’t pull the tape off the aluminum build platform:

    Longboard Case Solid Model - mouse ears
    Longboard Case Solid Model – mouse ears

    That view shows the bottom slice that will hold the battery, but the ears appear on all three layers.

    The OpenSCAD source code is now up on Github, which should make it easier to update & share.

  • KG-UV3D GPS+Voice Interface: APRS Bicycle Mobile

    Wouxun KG-UV3D with GPS-audio interface
    Wouxun KG-UV3D with GPS-audio interface

    Both of the GPS+voice interfaces for the Wouxun KG-UV3D radios have been working fine for a while, so I should show the whole installation in all its gory detail.

    If you haven’t been following the story, the Big Idea boils down to an amateur radio HT wearing a backpack that replaces its battery, combines the audio output of a Byonics TinyTrak3+ GPS encoder with our voice audio for transmission, and routes received audio to an earbud. Setting the radios to the APRS standard frequency (144.39 MHz) routes our GPS position points to the global packet database and, with 100 Hz tone squelch, we can use the radios as tactical intercoms without listening to all much of the data traffic.

    The local APRS network wizards approved our use of voice on the data channel, seeing as how we’re transmitting brief voice messages using low power through bad antennas from generally terrible locations. This wouldn’t work well in a dense urban environment with more APRS traffic; you’d need one of the newfangled radios that can switch frequencies for packet and voice transmissions.

    So, with that in mind, making it work required a lot of parts…

    Tour Easy - KG-UV3D GPS interface
    Tour Easy – KG-UV3D GPS interface

    A water bottle holder attaches to the seat base rail with a machined circumferential clamp. Inside the holder, a bike seat wedge pack contains the radio with its GPS+voice interface box and provides a bit of cushioning; a chunk of closed-cell foam on the bottom mostly makes me feel good.

    The flat 5 A·h Li-ion battery pack on the rack provides power for the radio; it’s intended for a DVD player and has a 9 V output that’s a trifle hot for the Wouxun radios. Some Genuine Velcro self-adhesive strips hold the packs to the racks and have survived surprisingly well.

    Just out of the picture to the left of the battery pack sits a Byonics GPS2 receiver puck atop a fender washer glued to the rack, with a black serial cable passing across the rack and down to the radio bag.

    A dual-band mobile antenna screws into the homebrew mount attached to the upper seat rail with another circumferential clamp. It’s on the left side of the rail, just barely out of the way of our helmets, and, yes, the radiating section of the antenna sits too close to our heads. The overly long coax cable has its excess coiled and strapped to the front of the rack; I pretend that’s an inductor to choke RF off the shield braid. The cable terminates in a PL-259 UHF plug, with an adapter to the radio’s reverse-polarity SMA socket.

    The push-to-talk button on the left handgrip isn’t quite visible in the picture. That cable runs down the handlebar, along the upper frame tube, under the seat, and emerges just in front of the radio bag, where it terminates in a 3.5 mm audio plug.

    The white USB cable from the helmet carries the boom mic and earbud audio over the top of the seat, knots around the top frame bar, and continues down to the radio. USB cables aren’t intended for this service and fail every few years, but they’re cheap and work well enough. The USB connector separates easily, which prevents us from being firmly secured to a dropped bike during a crash. I’d like much more supple cables, a trait that’s simply not in the USB cable repertoire. This is not a digital USB connection: I’m just using a cheap & readily available cable.

    All cables converge on the bag holding the radio:

    Tour Easy - KG-UV3D + GPS interface - detail
    Tour Easy – KG-UV3D + GPS interface – detail

    Now you can see why I put that dab of white on the top of the knob!

    The bag on my bike hasn’t accumulated quite so much crud, because it’s only a few months old, but it’s just as crowded:

    KG-UV3D + GPS interface on Tour Easy - top view
    KG-UV3D + GPS interface on Tour Easy – top view

    This whole “bicycle mobile APRS system”, to abuse a term, slowly grew from a voice-only interface for our ICOM IC-Z1A radios. Improving (and replacing!) one piece at a time occasionally produced horrible compatibility problems, while showing why commercial solutions justify owning metalworking tools, PCB design software, and a 3D printer.

    I long ago lost track of the number of Quality Shop Time hours devoted to all this, which may be the whole point…

    In other news, the 3D-printed fairing mountsblinky light mounts, and helmet mirror mounts continue to work fine; I’m absurdly proud of the mirrors. Mary likes her colorful homebrew seat cover that replaced a worn-out black OEM cover for a minute fraction of the price.