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 Needle LEDs: First Light

    With the boost converter mounted and the needle LEDs wired up:

     Kenmore 158 Needle Light - heatsink
    Kenmore 158 Needle Light – heatsink

    The Kenmore 158 sewing machine crash test dummy has plenty of light:

    Kenmore 158 LED Lighting - first light
    Kenmore 158 LED Lighting – first light

    Well, as long as you don’t mind the clashing color balance. The needle LEDs turned out warmer than I expected, but Mary says she can cope. I should build a set of warm-white LED strips when it’s time to refit her real sewing machine and add another boost supply to drive them at their rated current.

    Much to our relief, the two LEDs at the needle don’t cast offensively dark shadows:

    Kenmore 158 LED Lighting - detail
    Kenmore 158 LED Lighting – detail

    All in all, it looks pretty good.

  • Generic PCB Holder: Boost Power Supply

    The DC-DC boost power supply for the LED needle lights has four mounting holes, two completely blocked by the heatsink and the others against components with no clearance for screw heads, soooo

    3D printing to the rescue:

    Boost converter - installed
    Boost converter – installed

    Now that the hulking ET227 operates in saturation mode, I removed the blower to make room for the power supply. Two strips of double-stick foam tape fasten the holder to the removable tray inside the Dell GX270’s case.

    It’s basically a rounded slab with recesses for the PCB and clearance for solder-side components:

    Boost converter mount - as printed
    Boost converter mount – as printed

    The solid model shows the screw holes sitting just about tangent to the PCB recess:

    XW029 Booster PCB Mount
    XW029 Booster PCB Mount

    That’s using the new OpenSCAD with length scales along each axis; they won’t quite replace my layout grid over the XY plane, but they certainly don’t require as much computation.

    I knew my lifetime supply of self-tapping hex head 4-40 screws would come in handy for something:

    Boost converter in mount
    Boost converter in mount

    The program needs to know the PCB dimensions and how much clearance you want for the stuff hanging off the bottom:

    PCBoard = [66,35,IntegerMultiple(1.8,ThreadThick)];
    
    BottomParts = [[1.5,-1.0,0,0],	// xyz offset of part envelope
    				[60.0,37.0,IntegerMultiple(3.0,ThreadThick)]];	// xyz envelope size (z should be generous)
    

    That’s good enough for my simple needs.

    The hole locations form a list-of-vectors that the code iterates through:

    Holes = [			// PCB mounting screw holes: XY + rotation
    		[Margin - ScrewOffset,MountBase[Y]/2,180/6],
    		[MountBase[X] - Margin + ScrewOffset/sqrt(2),MountBase[Y] - Margin + ScrewOffset/sqrt(2),15],
    		[MountBase[X] - Margin + ScrewOffset/sqrt(2),Margin - ScrewOffset/sqrt(2),-15],
    		];
    
    ... snippage ...
    
    for (h = Holes) {
    	translate([h[X],h[Y],-Protrusion]) rotate(h[Z])
    		PolyCyl(Tap4_40,MountBase[Z] + 2*Protrusion,6);
    }
    

    That’s the first occasion I’ve had to try iterating a list and It Just Worked; I must break the index habit. The newest OpenSCAD version has Python-ish list comprehensions which ought to come in handy for something.

    The “Z coordinate” of each hole position gives its rotation, so I could snuggle them up a bit closer to the edge by forcing the proper polygon orientation. The square roots in the second two holes make them tangent to the corners of the PCB, rather than the sides, which wasn’t true for the first picture. Fortunately, the washer head of those screws turned out to be just big enough to capture the PCB anyway.

    The OpenSCAD source code:

    // PCB mounting bracket for XW029 DC-DC booster
    // Ed Nisley - KE4ZNU - January 2015
    
    Layout = "Build";			// PCB Block Mount Build
    
    //- Extrusion parameters must match reality!
    //  Print with 4 shells and 3 solid layers
    
    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);
    
    X = 0;						// useful subscripts
    Y = 1;
    Z = 2;
    
    //----------------------
    // Dimensions
    
    inch = 25.4;
    
    Tap4_40 = 0.089 * inch;
    Clear4_40 = 0.110 * inch;
    Head4_40 = 0.211 * inch;
    Head4_40Thick = 0.065 * inch;
    Nut4_40Dia = 0.228 * inch;
    Nut4_40Thick = 0.086 * inch;
    Washer4_40OD = 0.270 * inch;
    Washer4_40ID = 0.123 * inch;
    
    PCBoard = [66,35,IntegerMultiple(1.8,ThreadThick)];
    
    BottomParts = [[1.5,-1.0,0,0],				// xyz offset of part envelope
    				[60.0,37.0,IntegerMultiple(3.0,ThreadThick)]];			// xyz envelope size (z should be generous)
    
    Margin = IntegerMultiple(Washer4_40OD,ThreadWidth);
    
    MountBase = [PCBoard[X] + 2*Margin,
    			PCBoard[Y] + 2*Margin,
    			IntegerMultiple(5.0,ThreadThick) + PCBoard[Z] + BottomParts[1][Z]
    			];
    echo("Mount base: ",MountBase);
    
    ScrewOffset = Clear4_40/2;
    
    Holes = [									// PCB mounting screw holes: XY + rotation
    		[Margin - ScrewOffset,MountBase[Y]/2,180/6],
    		[MountBase[X] - Margin + ScrewOffset/sqrt(2),MountBase[Y] - Margin + ScrewOffset/sqrt(2),15],
    		[MountBase[X] - Margin + ScrewOffset/sqrt(2),Margin - ScrewOffset/sqrt(2),-15],
    		];
    
    CornerRadius = Washer4_40OD / 2;
    
    //----------------------
    // 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);
    
    }
    
    //----------------------
    // Build things
    
    module PCB() {
    
    	union() {
    		cube(PCBoard);
    		translate(BottomParts[X] - [0,0,BottomParts[1][Z]])
    			cube(BottomParts[Y] + [0,0,Protrusion]);
    	}
    
    }
    
    module Block() {
    	translate([MountBase[X]/2,MountBase[Y]/2,0])
    		hull()
    			for (i = [-1,1], j = [-1,1])
    				translate([i*(MountBase[X]/2 - CornerRadius),j*(MountBase[Y]/2 - CornerRadius)],0)
    					cylinder(r=CornerRadius,h=MountBase[Z] - Protrusion,$fn=8*4);
    }
    
    module Mount() {
    
    	difference() {
    		Block();
    
    		translate([MountBase[X]/2 - PCBoard[X]/2 + BottomParts[0][X] - Protrusion,
    					-MountBase[Y]/2,
    					MountBase[Z] - PCBoard[Z] - BottomParts[1][Z]])
    			cube([BottomParts[1][X] + 2*Protrusion,
    					2*MountBase[Y],
    					2*BottomParts[1][Z]]);
    
    		translate([MountBase[X]/2 - PCBoard[X]/2,		// PCB recess
    					MountBase[Y]/2 - PCBoard[Y]/2,
    					MountBase[Z] - PCBoard[Z]])
    			PCB();
    		for (h = Holes) {
    			translate([h[X],h[Y],-Protrusion]) rotate(h[Z])
    				PolyCyl(Tap4_40,MountBase[Z] + 2*Protrusion,6);
    		}
    	}
    
    }
    
    //ShowPegGrid();
    
    if (Layout == "PCB")
    	PCB();
    
    if (Layout == "Block")
    	Block();
    
    if (Layout == "Mount")
    	Mount();
    
    if (Layout == "Build")
    	translate([-MountBase[X]/2,-MountBase[Y]/2,0])
    	Mount();
    
  • Last of the Energizer CR2032 Cells

    All three Energizer CR2032 lithium cells installed at the end of November failed in December, with this being the most dramatic example:

    Attic - Insulated Box - Early battery failure
    Attic – Insulated Box – Early battery failure

    Now, granted, it was mighty chilly in the attic, but failing after 18 hours seems unreasonable. So much for last month’s data.

    I’ve started a batch of Maxell cells with the more reasonable date code 3O, which seems to indicate a manufacturing date of 2013 October.

    We shall see…

  • Kenmore 158 UI: Button Framework Functions

    Given the data structures defining the buttons, this code in the main loop() detects a touch, identifies the corresponding button, and does what’s needed:

    if (CleanTouch(&pt)) {
    	BID = FindHit(pt);
    	if (BID) {
    		HitButton(BID);
    	}
    	while(ts.touched())				// stall waiting for release
    		ts.getPoint();
    }
    

    The CleanTouch() function handles touch detection, cleanup, and rotation, delivering a coordinate that matches one of the LCD pixels. Given that you’re using a fingertip, errors caused by poor calibration or nonlinearities Just Don’t Matter.

    This function matches that coordinate against the target region of each button, draws a white rectangle on the first matching button, and returns that button ID:

    byte FindHit(TS_Point hit) {
    
    byte i;
    TS_Point ul,lr;
    
    #define MARGIN 12
    
    //	printf("Hit test: (%d,%d)\r\n",hit.x,hit.y);
    
    	for (i=0; i<NumButtons ; i++) {
    		ul.x = Buttons[i].ulX + Buttons[i].szX/MARGIN;
    		ul.y = Buttons[i].ulY + Buttons[i].szY/MARGIN;
    		lr.x = Buttons[i].ulX + ((MARGIN - 1)*Buttons[i].szX)/MARGIN;
    		lr.y = Buttons[i].ulY + ((MARGIN - 1)*Buttons[i].szY)/MARGIN;
    //		printf(" i: %d BID: %d S: %d ul=(%d,%d) sz=(%d,%d)\r\n",
    //			   i,Buttons[i].ID,Buttons[i].Status,ul.x,ul.y,lr.x,lr.y);
    		if ((hit.x >= ul.x && hit.x < lr.x) &&
    			(hit.y >= ul.y && hit.y <= lr.y)) {
    			// should test for being disabled and discard hit
    //			printf(" Hit i: %d ",i);
    			break;
    		}
    	}
    
    	if (i < NumButtons) {
    		tft.drawRect(ul.x,ul.y,lr.x-ul.x,lr.y-ul.y,ILI9341_WHITE);
    		return Buttons[i].ID;
    	}
    	else {
    		printf(" No hit!\r\n");
    		return 0;
    	}
    
    }
    

    You can enable as much debugging as you need by fiddling with the commented-out lines.

    After some empirical fiddling, a non-sensitive margin of 1/12 the button size helped prevent bogus hits. There’s no real need to draw the target rectangle, other than for debugging:

    Kenmore 158 UI buttons - hit target
    Kenmore 158 UI buttons – hit target

    The target shows the button graphics aren’t quite centered, because that’s how the ImageMagick script placed them while generating the shadow effect, but it still works surprisingly well. The next version of the buttons will center the graphics, specifically so I don’t have to explain what’s going on.

    Because the margin is 1/12 the size of the button, it rounds off to zero for the tiny button in the upper right corner, so that the touch target includes the entire graphic.

    The return value will be zero if the touch missed all the buttons, which is why a button ID can’t be zero.

    Given the button ID, this function un-pushes the other button(s) in its radio button group, then pushes the new button:

    byte HitButton(byte BID) {
    
    byte i,BX;
    byte Group;
    
    	if (!BID)											// not a valid ID
    		return 0;
    
    	BX = FindButtonIndex(BID);
    	if (BX == NumButtons)								// no button for that ID
    		return 0;
    
    	Group = Buttons[BX].Group;
    
    //	printf(" Press %d X: %d G: %d\r\n",BID,BX,Group);
    
    // If in button group, un-push other buttons
    
    	if (Group) {
    		for (i=0; i<NumButtons; i++) {
    			if ((Group == Buttons[i].Group) && (BT_DOWN == Buttons[i].Status)) {
    				if (i == BX) {							// it's already down, fake going up
    					Buttons[i].Status = BT_UP;
    				}
    				else {									// un-push other down button(s)
    //					printf(" unpress %d X: %d \r\n",Buttons[i].ID);
    					Buttons[i].pAction(Buttons[i].ID);
    				}
    			}
    		}
    	}
    
    	Buttons[BX].pAction(BID);
    
    	return 1;
    }
    

    The ID validation shouldn’t be necessary, but you know how things go. A few messages in there would help debugging.

    The default button action routine that I use for all the buttons just toggles the button’s Status and draws the new button graphic:

    void DefaultAction(byte BID) {
    
    byte i,BX;
    
    	if (!BID) {											// not a valid ID
    		printf("** Button ID zero in DefaultAction\r\n");
    		return;
    	}
    
    	BX = FindButtonIndex(BID);
    	if (BX == NumButtons) {								// no button for that ID
    		printf("** No table entry for ID: %d\r\n",BID);
    		return;
    	}
    
    	Buttons[BX].Status = (Buttons[BX].Status == BT_DOWN) ? BT_UP : BT_DOWN;
    
    	printf("Button %d hit, now %d\r\n",BID,Buttons[BX].Status);
    	DrawButton(BID,Buttons[BX].Status);
    
    }
    

    The little color indicator button has a slightly different routine to maintain a simple counter stepping through all ten resistor color codes in sequence:

    void CountColor(byte BID) {
    
    byte i,BX;
    static byte Count = 0;
    
    	if (!BID) {											// not a valid ID
    		printf("** Zero button ID\r\n");
    		return;
    	}
    
    	BX = FindButtonIndex(BID);
    	if (BX == NumButtons) {								// no button for that ID
    		printf("** No entry for ID: %d\r\n",BID);
    		return;
    	}
    
    	Buttons[BX].Status = BT_DOWN;						// this is always pressed
    
    	Count = (Count < 9) ? ++Count : 0;					// bump counter & wrap
    
    //	printf("Indicator %d hit, now %d\r\n",BID,Count);
    	DrawButton(BID,Count);
    
    }
    

    The indicator “button” doesn’t go up when pressed and its function controls what’s displayed.

    I think the button action function should have an additional parameter giving the next Status value, so that it knows what’s going on, thus eliminating the need to pre-push & redraw buttons in HitButton(), which really shouldn’t peer inside the button data.

    It needs more work and will definitely change, but this gets things started.

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