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.

Category: Electronics Workbench

Electrical & Electronic gadgets

  • Kenmore 158 UI: Button Framework Data Structures

    The trouble with grafting a fancy LCD on an 8 bit microcontroller like an Arduino is that there’s not enough internal storage for button images and barely enough I/O bandwidth to shuffle bitmap files from the SD card to the display. Nevertheless, that seems to be the least awful way of building a serviceable UI that doesn’t involve drilling holes for actual switches, indicators, and ugly 2×20 character LCD panels.

    Rather than hardcoding the graphics into the Arduino program, though, it makes sense to build a slightly more general framework to handle button images and overall UI design, more-or-less independently of the actual program. I haven’t been able to find anything that does what I need, without doing a whole lot more, sooooo here’s a quick-and-dirty button framework.

    Calling it a framework might be overstating the case: it’s just a data structure and some functions. It really should be a separate library, but …

    After considerable discussion with the user community, the first UI can control just three functions:

    • Needle stop position: up, down, don’t care
    • Operating mode: follow pedal position, triggered single-step, normal run
    • Speed: slow, fast, ultra-fast

    That boils down into a simple nine-button display, plus a tiny tenth button (in brown that looks red) at the top right:

    Kenmore 158 UI buttons - first pass
    Kenmore 158 UI buttons – first pass

    The viciously skeuomorphic button shading should show the button status. For example, in the leftmost column:

    • The two upper button are “up”, with light glinting from upward-bulging domes
    • The lower button is “down”, with light glinting from its pushed-in dome

    Frankly, that works poorly for me and the entire user community. The whole point of this framework is to let me re-skin the UI without re-hacking the underlying code; the buttons aren’t the limiting factor right now.

    Anyhow.

    The three buttons in each column are mutually exclusive radio buttons, but singleton checkbox buttons for firmware switches / modes would also be helpful, so there’s motivation to be a bit more general.

    A struct defines each button:

    enum bstatus_t {BT_DISABLED,BT_UP,BT_DOWN};
    
    typedef void (*pBtnFn)(byte BID);		// button action function called when hit
    
    struct button_t {
    	byte ID;					// button identifier, 0 unused
    	byte Group;					// radio button group, 0 for none
    	byte Status;				// button status
    	word ulX,ulY;				// origin: upper left
    	word szX,szY;				// button image size
    	pBtnFn pAction;				// button function
    	char NameStem[9];			// button BMP file name - stem only
    };
    

    The ID uniquely identifies each button in the rest of the code. That should be an enum, but for now I’m using an unsigned integer.

    The Group integer will be zero for singleton buttons and a unique nonzero value for each radio button group. Only one button in each Group can be pressed at a time.

    The Status indicates whether the button can be pushed and, if so, whether it’s up or down. Right now, the framework doesn’t handle disabled buttons at all.

    The next four entries define the button’s position and size.

    The pAction entry contains a pointer to the function that handles the button’s operation. It gets invoked whenever the touch screen registers a hit over the button, with an ID parameter identifying the button so you can use a single function for the entire group. I think it’ll eventually get another parameter indicating the desired Status, but it’s still early.

    The NameStem string holds the first part of the file name on the SD card. The framework prefixes the stem with a default directory (“/UserIntf/”), suffixes it with the Status value (an ASCII digit ‘0’ through ‘9’), tacks on the extension (“.bmp”), and comes up with the complete file name.

    An array of those structs defines the entire display:

    struct button_t Buttons[] = {
    	{ 1,	0, BT_UP,	  0,0,		 80,80,	DefaultAction,	"NdUp"},
    	{ 2,	0, BT_UP,	  0,80,		 80,80,	DefaultAction,	"NdAny"},
    	{ 3,	0, BT_DOWN,	  0,160,	 80,80,	DefaultAction,	"NdDn"},
    
    	{ 4,	2, BT_DOWN,	 80,0,		120,80,	DefaultAction,	"PdRun"},
    	{ 5,	2, BT_UP,	 80,80,		120,80,	DefaultAction,	"PdOne"},
    	{ 6,	2, BT_UP,	 80,160,	120,80,	DefaultAction,	"PdFol"},
    
    	{ 7,	3, BT_UP,	200,0,		 80,80,	DefaultAction,	"SpMax"},
    	{ 8,	3, BT_DOWN,	200,80,		 80,80,	DefaultAction,	"SpMed"},
    	{ 9,	3, BT_UP,	200,160,	 80,80,	DefaultAction,	"SpLow"},
    
    	{10,	0, BT_UP,	311,0,		  8,8,	CountColor,		"Res"}
    };
    
    byte NumButtons = sizeof(Buttons) / sizeof(struct button_t);
    

    Those values produce the screen shown in the picture. The first three buttons should be members of radio button group 1, but they’re singletons here to let me test that path.

    Contrary to what you see, the button ID values need not be in ascending order, consecutive, or even continuous. The IDs identify a specific button, so as long as they’re a unique number in the range 1 through 255, that’s good enough. Yes, I faced down a brutal wrong-variable error and then fixed a picket-fence error.

    The file name stems each refer to groups of BMP files on the SD card. For example, NdUp (“Needle stop up”) corresponds to the three files NdUp0.bmp, NdUp1.bmp, and NdUp2.bmp, with contents corresponding to the bstatus_t enumeration.

    The constant elements of that array should come from a configuration file on the SD card: wrap a checksum around it, stuff it in EEPROM, and then verify it on subsequent runs. A corresponding array in RAM should contain only the Status values, with the array index extracted from the EEPROM data. That would yank a huge block of constants out of the all-too-crowded RAM address space and, even better, prevents problems from overwritten values; a trashed function pointer causes no end of debugging fun.

    A more complex UI would have several such arrays, each describing a separate panel of buttons. There’s no provision for that right now.

    Next: functions to make it march…

  • Adafruit TFT LCD: Color Indicator Spots

    These spots might come in handy as status indicators and tiny mode control buttons:

    Resistor Color Code Spots
    Resistor Color Code Spots

    The montage is 800% of the actual 8×8 pixel size that’s appropriate for the Adafruit TFT LCD.

    They’re generated from the standard colors, with the “black” patch being a dark gray so it doesn’t vanish:

    # create resistor-coded color spots
    # Ed Nisley - KE4ZNU
    # January 2015
    
    SZ=8x8
    
    convert -size $SZ canvas:gray10 -type truecolor Res0.bmp
    convert -size $SZ canvas:brown	-type truecolor Res1.bmp
    convert -size $SZ canvas:red	-type truecolor Res2.bmp
    convert -size $SZ canvas:orange	-type truecolor Res3.bmp
    convert -size $SZ canvas:yellow -type truecolor Res4.bmp
    convert -size $SZ canvas:green	-type truecolor Res5.bmp
    convert -size $SZ canvas:blue	-type truecolor Res6.bmp
    convert -size $SZ canvas:purple	-type truecolor Res7.bmp
    convert -size $SZ canvas:gray80	-type truecolor Res8.bmp
    convert -size $SZ canvas:white	-type truecolor Res9.bmp
    
    montage Res*bmp -tile 5x -geometry +2+2 -resize 800% Res.png
    

    For a pure indicator, it’d be easier to slap a spot on the screen with the Adafruit GFX library’s fillRect() function. If you’re setting up a generic button handler, then button bitmap images make more sense.

  • Adafruit TFT Shield: Firmware Heartbeat Spot

    Being that type of guy, I want a visible indication that the firmware continues trudging around the Main Loop.  The standard Arduino LED works fine for that (unless you’re using hardware SPI), but the Adafruit 2.8 inch Touch-screen TFT LCD shield covers the entire Arduino board, so I can’t see the glowing chip.

    Given a few spare pixels and the Adafruit GFX library, slap a mood light in the corner:

    Adafruit TFT - heartbeat spot
    Adafruit TFT – heartbeat spot

    The library defines the RGB color as a 16 bit word, so this code produces a dot that changes color every half second around the loop() function:

    #define PIN_HEARTBEAT 13
    
    unsigned long MillisThen,MillisNow;
    #define UPDATEMS 500
    
    ... snippage ...
    
    void loop() {
    	MillisNow = millis();
    
    ... snippage ...
    
    	if ((MillisNow - MillisThen) > UPDATEMS) {
    
    		TogglePin(PIN_HEARTBEAT);
    		tft.fillCircle(315,235,4,(word)MillisNow);			// colorful LCD heartbeat
    
    		MillisThen = MillisNow;
    	}
    }
    

    millis() produces an obvious counting sequence of colors. If that matters, you use random(0x10000).

    A square might be slightly faster than a circle. If that matters, you need an actual measurement in place of an opinion.

    Not much, but it makes me happy…

    There’s an obvious extension for decimal values: five adjacent spots in the resistor color code show you an unsigned number. Use dark gray for black to prevent it from getting lost; light gray and white would be fine. Prefix it with a weird color spot for the negative sign, should you need such a thing.

    Hexadecimal values present a challenge. That’s insufficient justification to bring back octal notation.

    In this day and age, color-coded numeric readouts should be patentable, as casual searching didn’t turn up anything similar. You saw it here first… [grin]

    Now that I think about it, a set of tiny buttons that control various modes might be in order.

  • APRS Turn Slope Units

    There’s a fundamental error in my writeup about setting the APRS Smart Beaconing parameters for the bike trackers: I blundered the units of Turn Slope.

    Rich Painter recently explained how that works:

    I ran across your blog on Smart Beaconing and saw something that needed correction.

    You state the Turn Slope is in units Degrees / MPH

    This is incorrect. Although the term Turn Slope is not a real slope (such as rise/run classically) that is what the originators used albeit incorrectly. They do however correctly attribute the units to MPH * Degrees (a product and hence not really a slope).

    In their formula they calculate a turn threshold as:
    turn_threshold = min_turn_angle + turn_slope / speed

    Looking at the units we see:
    = Degrees + (MPH * Degrees) / MPH

    which yields
    = Degrees + Degrees

    Which makes sense. It is too bad that the originators used the wrong term of Turn Slope which confuses most people. A better term would have been Turn Product.

    In looking back over that post, I have no idea where or how I got the wrong units, other than by the plain reading of the “variable name”.

    As he explained in a followup note:

    As for units… I was introduced to making unit balance way back in 1967-1968 science class in HS by a really fine science teacher. It has served me all my life and I’m thankful for that training.

    I have ever since told that teacher so!

    A while back, our Larval Engineer rammed an engineering physics class head-on and sent me a meme image, observing that I’d trained her well: if the units don’t work out, then you’re doing it wrong.

    Yes, yes, I do care about the units:

    Give a shit about the units
    Give a shit about the units
  • Bad Batteries Are Bad: Cold Weather Edition

    So we took an out-and-back walk across the Walkway Over the Hudson, after which I spotted this amusing sight:

    Parking Meter - empty battery box
    Parking Meter – empty battery box

    The horrible color balance comes from using a preset tuned for the M2’s new LED lights, rather than letting the camera figure things out on its own, then fighting it down after cropping.

    Anyhow, we did a bit over two miles of walking with outdoor temperature just over freezing. The camera lives in the left cargo pocket of my pants and the spare NB-5L battery in the camera case faces outward. Neither battery would power the camera at ambient temperature; evidently, being that cold reduced their output voltage below the level that the camera would accept.

    With a cold battery, the camera grunted, displayed a message about replacing the battery, and promptly shut itself off. Warming one of the batteries boosted its terminal voltage enough to take the picture, which accounts for not getting the proper color balance: I was fully occupied just getting the camera working.

    Back home and warmed up, the camera said both batteries were fully charged. They came from the BNF27 lot that produced low terminal voltages, so I’ll reserve them for warmer weather and use the BNI13 lot during the next few months.

     

  • Pilot InstaBoost: Battery Capacity

    The cardboard package liner claims the lithium-ion battery inside our Larval Engineer’s shiny new InstaBoost jump starter is good for 10.8 A·h and and the minuscule inscription on the case truncates it to 10 A·h. Given what I’ve seen for other batteries, either value would be true when measured under the most favorable conditions, but these curves still came as a bit of a surprise (clicky for more dots):

    Pilot Instaboost
    Pilot Instaboost

    The three short, abruptly dropping curves come from the main terminals, with the battery clamps attached to similar clamps (with a glitch when they shifted position) plugged into my CBA II/IV battery tester, showing that the InstaBoost shuts off after a few minutes, regardless of load. That makes good sense: don’t connect a lithium battery to a lead-acid battery for more than a few minutes!

    The two longer curves come from the 12 V jack on the side and show that it will run until the battery goes flat. Evidently, the internal battery protection circuit cuts out at less than the 10 V minimum I used for these tests.

    I didn’t bother testing the USB charging outlet, as I assume it would produce 5 V at 1 A for slightly less than twice as long.

    Under the most favorable conditions I could come up with, the actual battery capacity of 3.5 A·h is a third of what it should be. I’d expect that from the usual eBay supplier, not Lowe’s.

    Given the cheapnified clamps, perhaps Pilot deliberately gutted the battery capacity to save a few bucks. After all, the customers will never notice. Will they?

    Except…

    Another customer took his apart and found three 3.6 A·h “high output” (whatever that means) lithium cells in series. In that configuration, the individual cell capacity does not add and the pack should produce about 3.6 A·h. Those curves show it produces slightly less than that when discharged to 10 V, which means the thing works exactly like you’d expect. Indeed, it’s better than a typical second-tier product and much better than typical eBay crap.

    The most charitable explanation would be that somebody screwed up, multiplied the number of cells by their individual capacity, put that number in the specs, and everyone downstream ran with it. If the cells were in parallel, then the total capacity in ampere·hours would equal the sum of the cell capacity.

    If you change the specs to match the as-built hardware, then, apart from those cheapnified clamps, it’s working just fine…

  • Rounded Cable Clips

    This isn’t quite the smoothly rounded clip I had in mind:

    LED Cable Clip - rounded channel
    LED Cable Clip – rounded channel

    It seems somewhat better looking than the square design, though:

    LED Cable Clips
    LED Cable Clips

    I ran off a few of both styles to have some on hand:

    Cable clips - on platform
    Cable clips – on platform

    They’re in a bag until I install the new LED strips and needle light.

    The OpenSCAD source code:

    // LED Cable Clips
    // Ed Nisley - KE4ZNU - October 2014
    
    Layout = "Oval";			// Oval Square Build
    
    //- Extrusion parameters must match reality!
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;			// extra clearance
    
    Protrusion = 0.1;			// make holes end cleanly
    
    AlignPinOD = 1.70;			// assembly alignment pins: filament dia
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    //----------------------
    // Dimensions
    
    Base = [12.0,12.0,IntegerMultiple(2.0,ThreadThick)];	// base over sticky square
    
    CableOD = 2.0;
    
    BendRadius = 3.0;
    
    Bollard = [BendRadius,(sqrt(2)*Base[0]/2 - CableOD - BendRadius),2*CableOD];
    B_BOT = 0;
    B_TOP = 1;
    B_LEN = 2;
    
    NumSides = (Shape == "Square") ? 5*4 : 6*3;
    
    //----------------------
    // 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);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      RangeX = floor(100 / Space);
      RangeY = floor(125 / Space);
    
    	for (x=[-RangeX:RangeX])
    	  for (y=[-RangeY:RangeY])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //-- Square clip with central bollard
    
    module SquareBollard() {
    
    	intersection() {
    		translate([0,0,(Base[2] + Bollard[B_LEN])/2])			// overall XYZ outline
    			cube(Base + [0,0,Bollard[2]],center=true);
    
    		union() {
    			translate([0,0,Base[2]/2])						// oversize mount base
    				scale([2,2,1])
    					cube(Base,center=true);
    
    			for (i=[-1,1] , j=[-1,1]) {						// corner bollards
    				translate([i*Base[0]/2,j*Base[1]/2,(Base[2] - Protrusion)])
    					rotate(180/NumSides)
    					cylinder(r=Bollard[B_BOT],h=(Bollard[B_LEN] + Protrusion),center=false,$fn=NumSides);
    
    			translate([0,0,(Base[2] - Protrusion)])			// center tapered bollard
    				cylinder(r1=Bollard[B_BOT],r2=Bollard[B_TOP],
    						 h=(Bollard[B_LEN] + Protrusion),
    						 center=false,$fn=NumSides);
    			}
    		}
    	}
    
    }
    
    //-- Oval clip with central passage
    
    module OvalPass() {
    
    	intersection() {
    		translate([0,0,(Base[2] + Bollard[B_LEN])/2])		// overall XYZ outline
    			cube(Base + [0,0,2*CableOD],center=true);
    
    		union() {
    			translate([0,0,Base[2]/2])						// oversize mount base
    				scale([2,2,1])
    					cube(Base,center=true);
    
    			for (j=[-1,1])									// bending ovals
    				translate([0,j*Base[1]/2,(Base[2] - Protrusion)])
    					resize([Base[0]/0.75,0,0])
    						cylinder(d1=0.75*(Base[1]-CableOD),d2=(Base[1]-CableOD)/cos(180/NumSides),
    								h=(Bollard[B_LEN] + Protrusion),
    								center=false,$fn=NumSides);
    		}
    	}
    /*
    #	translate([0,0,6])
    		rotate([0,90,0])
    			cylinder(d=CableOD,h=10,center=true,$fn=48);
    */
    }
    
    //----------------------
    // Build it
    
    ShowPegGrid();
    
    if (Layout == "Square")
    	SquareBollard();
    
    if (Layout == "Oval")
    	OvalPass();