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

Fabric arts and machines

  • Kenmore 158 Base: Cable Port

    The Crash Test Dummy machine arrived from the usual eBay seller in a cardboard box with a few rigid foam strips and some closed-cell foam sheets tossed inside. The seller thought the machine was “adequately protected”, which turned out to be, at best, optimistic:

    Kenmore 158 - Crash Test Dummy Case
    Kenmore 158 – Crash Test Dummy Case

    Fortunately, the crushed case protected the sewing machine itself and, given that I specifically bought it with the intent of making mistakes thereupon, it worked well enough. At one point, it vibrated off a desk, landed face-down on the concrete basement floor, and now the stitch selection / length cam followers don’t follow their cams very well at all.

    I modified the cracked-but-workable base to pass the connectors on the AC motor power and LED power / position sensor cables:

    Kenmore 158 Base - cable hole
    Kenmore 158 Base – cable hole

    That was done by chucking a hole saw in the drill press, running at lowest speed, resting the other end of the case on my thigh, and tipping my foot to drive the case upward into the saw. Worked surprisingly well, but I’d appreciate it if you didn’t use that technique yourself.

    Now that the Crash Test Dummy resembles a sewing machine again, running a few trial stitches in scrap fabric showed that it works well enough for straight-line sewing and free-motion quilting:

    Kenmore 158 Crash Test Dummy - test stitching
    Kenmore 158 Crash Test Dummy – test stitching

    We installed it in the Quilting Room, ready for a more extensive evaluation on an actual quilt…

  • Kenmore 158 Motor Controller: Button Command Decoder

    Now that the sewing machine motor controller receives commands from the UI (or typed in on a console), it must decode them. The “parser” doesn’t amount to much, because the commands consist of exactly two characters wrapped in square brackets. For simplicity, if the format doesn’t match or the command isn’t exactly right, the decoder simply tosses it on the floor and moves on:

    void ParseCmd(char *pBuff) {
    
    	if ((CmdBuffer[0] != '[') || (CmdBuffer[3] != ']')) {
    		printf("** Bad cmd format: %s\r\n",CmdBuffer);
    		return;	
    	}
    
    	switch (CmdBuffer[1]) {
    	case 'N':							// needle park position
    		switch (CmdBuffer[2]) {
    		case 'u':
    			MotorDrive.ParkPosition = NS_UP;
    //			ParkNeedle(NS_UP);
    			break;
    		case 'a':
    			MotorDrive.ParkPosition = NS_NONE;
    			break;
    		case 'd':
    			MotorDrive.ParkPosition = NS_DOWN;
    //			ParkNeedle(NS_DOWN);
    			break;
    		default:
    			printf("** Bad Needle cmd: %s\r\n",CmdBuffer);
    		}
    		break;
    	case 'P':							// pedal mode
    		switch (CmdBuffer[2]) {
    		case 'r':
    			MotorDrive.PedalMode = PD_RUN;
    			break;
    		case '1':
    			MotorDrive.PedalMode = PD_SINGLE;
    			break;
    		case 'f':
    			MotorDrive.PedalMode = PD_FOLLOW;
    			break;
    		default:
    			printf("** Bad Pedal cmd: %s\r\n",CmdBuffer);
    		}
    		break;
    	case 'S':							// motor speed range
    		switch (CmdBuffer[2]) {
    		case 'h':
    			MotorDrive.SpeedRange = SPEED_HIGH;
    			PedalMaxClamp = PEDALMAX;
    			break;
    		case 'm':
    			MotorDrive.SpeedRange = SPEED_MEDIUM;
    			PedalMaxClamp = (3 * PEDALMAX) / 4;
    			break;
    		case 'l':
    			MotorDrive.SpeedRange = SPEED_LOW;
    			PedalMaxClamp = PEDALMAX / 2;
    			break;
    		default:
    			printf("** Bad Speed cmd: %s\r\n",CmdBuffer);
    		}
    		break;
    	default:
    		printf("** Bad command string: %s\r\n",CmdBuffer);
    	}
    	
    	return;
    }
    

    So much for recursive descent parser design theory, eh?

  • Kenmore 158 UI: Button Commands

    The button definition table now includes a string that becomes the button’s command to the sewing machine motor controller:

    #define MAXCMDLEN 2
    
    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 Cmd[MAXCMDLEN + 1];	// command string
    	char NameStem[9];			// button BMP file name - stem only
    };
    
    struct button_t Buttons[] = {
    	{ 1,	1, BT_UP,		  0,0,		 80,80,	DefaultAction,	"Nu",	"NdUp"},
    	{ 2,	1, BT_UP,		  0,80,		 80,80,	DefaultAction,	"Na",	"NdAny"},
    	{ 3,	1, BT_DOWN,		  0,160,	 80,80,	DefaultAction,	"Nd",	"NdDn"},
    
    	{ 4,	2, BT_DOWN,		 80,0,		120,80,	DefaultAction,	"Pr",	"PdRun"},
    	{ 5,	2, BT_UP,		 80,80,		120,80,	DefaultAction,	"P1",	"PdOne"},
    	{ 6,	2, BT_UP,		 80,160,	120,80,	DefaultAction,	"Pf",	"PdFol"},
    
    	{ 7,	3, BT_DOWN,		200,0,		 80,80,	DefaultAction,	"Sh",	"SpMax"},
    	{ 8,	3, BT_UP,		200,80,		 80,80,	DefaultAction,	"Sm",	"SpMed"},
    	{ 9,	3, BT_UP,		200,160,	 80,80,	DefaultAction,	"Sl",	"SpLow"},
    
    //	{10,	0, BT_UP,	311,0,		  8,8,	CountColor,	'\0',	'\0',	"Res"}
    };
    
    byte NumButtons = sizeof(Buttons) / sizeof(struct button_t);
    

    The default button handler now sends the button’s command string whenever it finds the button down after all the processing:

    #define TRACEACTION false
    
    void DefaultAction(byte BID) {
    
    byte i,BX;
    byte Group;
    
    	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;
    	}
    
    #if TRACEACTION
    	printf("Default action: BID %d St %d -- ",BID,Buttons[BX].Status);
    #endif
    
    	if (Buttons[BX].Status == BT_DISABLED) {		// cannot do anything to disabled buttons
    #if TRACEACTION
    		printf("disabled\r\n");
    #endif
    		return;
    	}
    
    	Group = Buttons[BX].Group;
    
    	if (Group) {									// member of group?
    		if (Buttons[BX].Status == BT_DOWN) {		//  if down, remain that way
    #if TRACEACTION
    			printf("already down\r\n");
    #endif
    		}
    		else {										//  is up
    			for (i=0; i<NumButtons; i++) {			//   so unpush other buttons in group
    				if ((Buttons[i].Group == Group) && (Buttons[i].Status == BT_DOWN) && (i != BX)) {
    #if TRACEACTION
    					printf("release ID %d - ",Buttons[i].ID);
    #endif
    					Buttons[i].Status = BT_UP;
    					DrawButton(Buttons[i].ID,Buttons[i].Status);
    				}
    			}
    #if TRACEACTION
    			printf("push\r\n");
    #endif
    			Buttons[BX].Status = BT_DOWN;			//   and push this button down
    		}
    	}
    	else {											// not a group, so just toggle
    #if TRACEACTION
    		printf("toggle\r\n");
    #endif
    		Buttons[BX].Status = (Buttons[BX].Status == BT_DOWN) ? BT_UP : BT_DOWN;
    	}
    
    	DrawButton(BID,Buttons[BX].Status);
    
    	if (Buttons[BX].Status == BT_DOWN) {			// is this button now (or still) pressed?
    		SendCmd(Buttons[BX].Cmd);
    	}
    
    }
    

    That means the controller will see identical commands each time the button gets pressed, which doesn’t have any downsides. You could build an increment / decrement speed function without much trouble, although there’s still no way to display any returned values on the LCD.

    Working under the possibly unwarranted assumption that serial communications between the two Arduinos won’t encounter any errors, I just wrap the command string in a distinctive marker and send it off:

    void SendCmd(char *pCmd) {
    
    char Msg[MAXCMDLEN + 3];
    
    	strcpy(Msg,"[");
    	strcat(Msg,pCmd);
    	strcat(Msg,"]");
    
    	Serial.print("Cmd: ");							// copy to console
    	Serial.println(Msg);
    
    	Serial1.println(Msg);							// send command!
    
    }
    

    The Serial1 port runs at a nose-pickin’ 9600 baud, because the motor controller often gets wrapped up in what it’s doing. On the other paw, when the controller gets distracted, the operator will be feeding fabric past the needle at a pretty good clip and won’t have a finger to spare for the UI buttons, so it would probably work no matter what.

    That mismatch, however, allows the motor controller to babble on at length, without overruning the UI’s console output. This routine collects lines from the controller:

    char GetStatLine(void) {
    
    static byte Index = 0;
    char NewChar;
    
    	if (!Serial1.available()) {					// return if no chars in queue
    		return 0;
    	}
    
    	do {
    		NewChar = Serial1.read();
    		switch (NewChar) {
    		case '\r':								// end-of-line on CR
    			MCtlBuffer[Index] = 0;
    			Index = 0;
    			return strlen(MCtlBuffer);			// return from mid-loop
    			break;								//  unnecessary
    		case '\n':								// discard NL
    			break;
    		default:
    			MCtlBuffer[Index] = NewChar;		// store all others
    			Index += (Index < STATMAXLEN) ? 1 : 0;
    		}
    	} while (Serial1.available());
    
    	return 0;
    
    }
    

    A call in the main loop dumps each line after the terminating CR:

    char GetStatLine(void) {
    
    static byte Index = 0;
    char NewChar;
    
    	if (!Serial1.available()) {					// return if no chars in queue
    		return 0;
    	}
    
    	do {
    		NewChar = Serial1.read();
    		switch (NewChar) {
    		case '\r':								// end-of-line on CR
    			MCtlBuffer[Index] = 0;
    			Index = 0;
    			return strlen(MCtlBuffer);			// return from mid-loop
    			break;								//  unnecessary
    		case '\n':								// discard NL
    			break;
    		default:
    			MCtlBuffer[Index] = NewChar;		// store all others
    			Index += (Index < STATMAXLEN) ? 1 : 0;
    		}
    	} while (Serial1.available());
    
    	return 0;
    
    }
    

    Which produces output like this:

    Kenmore Model 158 User Interface
     Compiled: Jan 26 2015 at 15:33:52
    Ed Nisley - KE4ZNU
    TS... OK
    SD... OK
    LCD... should be active
    Cmd: [Nd]
    Cmd: [Pr]
    Cmd: [Sh]
    MC |** Bad command string: [--]
    MC |  540, 65535,   194
    MC |  610,     0,   194
    MC |  783,    55,   236
    MC | 1262,    84,   391
    MC | 1452,   116,   394
    MC | 1633,   123,   394
    MC | 1494,   132,   405
    MC | 1768,   126,   406
    MC | 1488,   126,   406
    MC | 1425,   137,   406
    MC | 1517,   132,   406
    MC | 1461,   126,   209
    MC |Coast: 1099
    MC |Parking Stop down: Done
    MC | stopped
    

    The “bad command string” isn’t actually an error. The first outbound line consists of [--] and a carriage return, which isn’t a valid command, just to make sure that the motor controller’s incoming serial port buffer doesn’t contain any junk. Obviously, I should add that string to the command decoder…

  • Adafruit Touch-screen TFT LCD Rotation

    The alert reader will have noted that the Kenmore 158 UI twisted around to a new orientation atop its fancy holder, with the USB port now poking out from the right side:

    Kenmore 158 UI - PCB holder
    Kenmore 158 UI – PCB holder

    That lets me position the whole affair to the right of the sewing machine, in what seems to be its natural position, without having the cable form a loop that would push it off the platform. It’s not entirely clear how we’ll keep a straight cable from pulling it off, but that’s in the nature of fine tuning.

    Anyhow, rotating the LCD isn’t a big deal, because the Adafruit library does all the heavy lifting:

    // LCD orientation: always landscape, 1=USB upper left / 3=USB lower right
    #define LCDROTATION 3
    
    ... snippage ...
    tft.begin();
    tft.setRotation(LCDROTATION);	// landscape, 1=USB upper left / 3=USB lower right
    

    Flipping the touch screen coordinates required just interchanging the “to” bounds of the map() functions, with a conditional serving as institutional memory in the not-so-unlikely event I must undo this:

    #if LCDROTATION == 1
    	p->x = map(t.y, TS_Min.y, TS_Max.y, 0, tft.width());	// rotate & scale to TFT boundaries
    	p->y = map(t.x, TS_Min.x, TS_Max.x, tft.height(), 0);	//   ... USB port at upper left
    #elif LCDROTATION == 3
    	p->x = map(t.y, TS_Min.y, TS_Max.y, tft.width(), 0);	// rotate & scale to TFT boundaries
    	p->y = map(t.x, TS_Min.x, TS_Max.x, 0, tft.height());	//   ... USB port at lower right
    #endif
    

    And then It Just Worked.

  • Arduino Mega PCB Holder

    Flushed with success from making the boost power supply mount, here’s a holder for the Arduino Mega that’s supporting the Kenmore 158 sewing machine UI:

    Kenmore 158 UI - PCB holder
    Kenmore 158 UI – PCB holder

    The solid model shows two screws holding the PCB in place:

    Arduino Mega PCB Mount
    Arduino Mega PCB Mount

    I decided to edge-clamp the board, rather than fuss with the built-in screws, just because 3D printing makes it so easy.

    Of course, the UI needs a real case that will hold it at an angle, so as to make the LCD and touch screen more visible and convenient; this mount just keeps the PCB up off the conductive surface of the insulating board we’re using in lieu of a Real Sewing Platform.

    This sewing machine project involves a lot of parts…

    The OpenSCAD source code:

    // PCB mounting bracket for Arduino Mega
    // 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 = [102,54,IntegerMultiple(1.8,ThreadThick)];
    
    BottomParts = [[2.5,-5.0,0,0],				// xyz offset of part envelope
    				[96,80,IntegerMultiple(5.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,MountBase[Y]/2,180/6],
    		];
    
    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();
    
  • 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();