Fix bug in ULN2003

Fix bug in ULN2003

nested vitamins (LEDs) not correctly wrapped in ULN2003 vitamin
statement

/*
	Vitamin: ULN2003DriverBoard
	Model of a stepper driver board.
	Based on http://42bots.com/tutorials/28byj-48-stepper-motor-with-uln2003-driver-and-arduino-uno/

	Local Frame:
		Centred on the bottom left mounting hole
*/

ULN2003Driver_BoardHeight = 35;
ULN2003Driver_BoardWidth = 30;
ULN2003Driver_PCBThickness = 1.5;
ULN2003Driver_HoleDia = 3;
ULN2003Driver_HoleInset = 2.5;

// Connectors
ULN2003DriverBoard_Con_LowerLeft	= [ [0, 0, 0], [0, 0, -1], 0, ULN2003Driver_PCBThickness, ULN2003Driver_HoleDia];
ULN2003DriverBoard_Con_LowerRight	= [ [ULN2003Driver_BoardWidth - 2 * ULN2003Driver_HoleInset, 0, 0], [0, 0, -1], 0, ULN2003Driver_PCBThickness, ULN2003Driver_HoleDia];
ULN2003DriverBoard_Con_UpperLeft	= [ [0, ULN2003Driver_BoardHeight - 2 * ULN2003Driver_HoleInset, 0], [0, 0, -1], 0, ULN2003Driver_PCBThickness, ULN2003Driver_HoleDia];
ULN2003DriverBoard_Con_UpperRight	= [ [ULN2003Driver_BoardWidth - 2 * ULN2003Driver_HoleInset, ULN2003Driver_BoardHeight - 2 * ULN2003Driver_HoleInset, 0], [0, 0, -1], 0, ULN2003Driver_PCBThickness, ULN2003Driver_HoleDia];

ULN2003DriverBoard_Con_Stepper		= [ [4 - ULN2003Driver_HoleInset, 22 - ULN2003Driver_HoleInset, ULN2003Driver_PCBThickness + 2.25], [0, 0, -1], 0, 0 ];
ULN2003DriverBoard_Con_Arduino		= [ [6 - ULN2003Driver_HoleInset, 6 - ULN2003Driver_HoleInset, ULN2003Driver_PCBThickness + 2.25], [0, 0, -1], 180, 0 ];
ULN2003DriverBoard_Con_Power		= [ [24.5 - ULN2003Driver_HoleInset, 9 - ULN2003Driver_HoleInset, ULN2003Driver_PCBThickness + 2.25], [0, 0, -1], 90, 0 ];

ULN2003DriverBoard_Cons = [
	ULN2003DriverBoard_Con_LowerLeft,
	ULN2003DriverBoard_Con_LowerRight,
	ULN2003DriverBoard_Con_UpperLeft,
	ULN2003DriverBoard_Con_UpperRight,
	ULN2003DriverBoard_Con_Stepper,
	ULN2003DriverBoard_Con_Arduino,
	ULN2003DriverBoard_Con_Power
];

module ULN2003DriverBoard_PCB(includeFixings=true, thickness=ULN2003Driver_PCBThickness) {
	// Base PCB
	// offset origin to bottom left
	color([0.2,0.6,0.3])
	translate([(ULN2003Driver_BoardWidth / 2) - ULN2003Driver_HoleInset, (ULN2003Driver_BoardHeight / 2) - ULN2003Driver_HoleInset, 0])
		linear_extrude(thickness)
		difference() {
			square([ULN2003Driver_BoardWidth, ULN2003Driver_BoardHeight], center=true);

			// Mounting holes
			if (includeFixings)
				for (i = [0:1], j = [0:1])
				mirror([i, 0, 0])
				mirror([0, j, 0])
				translate([ULN2003Driver_BoardWidth / 2 - ULN2003Driver_HoleInset, ULN2003Driver_BoardHeight / 2 - ULN2003Driver_HoleInset, 0])
					circle(r = ULN2003Driver_HoleDia / 2);
		}
}

module ULN2003DriverBoard() {

	if (DebugCoordinateFrames) frame();

	if (DebugConnectors)
		for (c = ULN2003DriverBoard_Cons)
			connector(c);

	vitamin(
		"vitamins/ULN2003DriverBoard.scad",
		str("ULN2003 Driver Board"),
		str("ULN2003DriverBoard()")
	) {
		view(d=140);

		ULN2003DriverBoard_PCB();

		// everything else
		translate([(ULN2003Driver_BoardWidth / 2) - ULN2003Driver_HoleInset, (ULN2003Driver_BoardHeight / 2) - ULN2003Driver_HoleInset, 0])
		{
			// Move us to the bottom left of the PCB
			translate([-ULN2003Driver_BoardWidth / 2, -ULN2003Driver_BoardHeight / 2, 0]) {

				// ULN2003 chip
				color("darkgrey")
				translate([1, 8, ULN2003Driver_PCBThickness - eta])
					cube([20, 10, 8]);

				// Arduino header
				translate([6, 6, ULN2003Driver_PCBThickness - eta])
					PcbPinStrip(4);

				// Stepper Connection
				color("white")
				translate([1.5, 19, ULN2003Driver_PCBThickness - eta])
					difference() {
						cube([15.5, 6, 8]);
						translate([1, 1, 1])
							cube([13.5, 4, 8]);
					}
				translate([4, 22, ULN2003Driver_PCBThickness - eta])
					PcbPinStrip(5, 0, false);

				// Power & On/Off Jumper
				translate([24.5, 9, ULN2003Driver_PCBThickness - eta])
					PcbPinStrip(4, 90);

				// LEDS
				translate([6, 29, ULN2003Driver_PCBThickness - eta])
					LED_3mm();
				translate([10, 29, ULN2003Driver_PCBThickness - eta])
					LED_3mm();
				translate([14.5, 29, ULN2003Driver_PCBThickness - eta])
					LED_3mm();
				translate([19, 29, ULN2003Driver_PCBThickness - eta])
					LED_3mm();
			}
		}
	}
}

module PcbPinStrip(numPins = 4, a = 0, spacers = true)
{
    // Standard PCB header pin
    pcbholepitch        = 2.54;         // spacing of PCB holes
    holediameter        = 1.25;         // diameter of PCB hole
    pinheight           =   11;         // length of PCB pins
    pinoffset           =    3;         // offset of PCB pins
    pinwidth            = 0.63;         // width/gauge of PCB pins
    spacerwidth         = 2.25;         // height of breakaway pin frame
    spacerheight        = 2.25;         // height of breakaway pin frame

	rotate(a = a)
	for (i = [0 : numPins - 1]) {
		translate([i * pcbholepitch, 0, 0]) {
			// Header Pins
			color("white")
			translate([0, 0, -pinoffset])
			linear_extrude(pinheight)
				square(pinwidth, center = true);

			if (spacers) {
				// Pin Spacers
				color(Grey20)
				linear_extrude(spacerheight)
					square(spacerwidth, center = true);

				// Break-Away Material
				color(Grey20)
				translate([pcbholepitch/2, 0, 0])
				linear_extrude(spacerheight)
					square([pcbholepitch - spacerwidth, spacerwidth * 0.85], center = true);
			}
		}
	}
}

/*
	Vitamin: ULN2003DriverBoard
	Model of a stepper driver board.
	Based on http://42bots.com/tutorials/28byj-48-stepper-motor-with-uln2003-driver-and-arduino-uno/

	Local Frame:
		Centred on the bottom left mounting hole
*/

ULN2003Driver_BoardHeight = 35;
ULN2003Driver_BoardWidth = 30;
ULN2003Driver_PCBThickness = 1.5;
ULN2003Driver_HoleDia = 3;
ULN2003Driver_HoleInset = 2.5;

// Connectors
ULN2003DriverBoard_Con_LowerLeft	= [ [0, 0, 0], [0, 0, -1], 0, ULN2003Driver_PCBThickness, ULN2003Driver_HoleDia];
ULN2003DriverBoard_Con_LowerRight	= [ [ULN2003Driver_BoardWidth - 2 * ULN2003Driver_HoleInset, 0, 0], [0, 0, -1], 0, ULN2003Driver_PCBThickness, ULN2003Driver_HoleDia];
ULN2003DriverBoard_Con_UpperLeft	= [ [0, ULN2003Driver_BoardHeight - 2 * ULN2003Driver_HoleInset, 0], [0, 0, -1], 0, ULN2003Driver_PCBThickness, ULN2003Driver_HoleDia];
ULN2003DriverBoard_Con_UpperRight	= [ [ULN2003Driver_BoardWidth - 2 * ULN2003Driver_HoleInset, ULN2003Driver_BoardHeight - 2 * ULN2003Driver_HoleInset, 0], [0, 0, -1], 0, ULN2003Driver_PCBThickness, ULN2003Driver_HoleDia];

ULN2003DriverBoard_Con_Stepper		= [ [4 - ULN2003Driver_HoleInset, 22 - ULN2003Driver_HoleInset, ULN2003Driver_PCBThickness + 2.25], [0, 0, -1], 0, 0 ];
ULN2003DriverBoard_Con_Arduino		= [ [6 - ULN2003Driver_HoleInset, 6 - ULN2003Driver_HoleInset, ULN2003Driver_PCBThickness + 2.25], [0, 0, -1], 180, 0 ];
ULN2003DriverBoard_Con_Power		= [ [24.5 - ULN2003Driver_HoleInset, 9 - ULN2003Driver_HoleInset, ULN2003Driver_PCBThickness + 2.25], [0, 0, -1], 90, 0 ];

ULN2003DriverBoard_Cons = [
	ULN2003DriverBoard_Con_LowerLeft,
	ULN2003DriverBoard_Con_LowerRight,
	ULN2003DriverBoard_Con_UpperLeft,
	ULN2003DriverBoard_Con_UpperRight,
	ULN2003DriverBoard_Con_Stepper,
	ULN2003DriverBoard_Con_Arduino,
	ULN2003DriverBoard_Con_Power
];

module ULN2003DriverBoard_PCB(includeFixings=true, thickness=ULN2003Driver_PCBThickness) {
	// Base PCB
	// offset origin to bottom left
	color([0.2,0.6,0.3])
	translate([(ULN2003Driver_BoardWidth / 2) - ULN2003Driver_HoleInset, (ULN2003Driver_BoardHeight / 2) - ULN2003Driver_HoleInset, 0])
		linear_extrude(thickness)
		difference() {
			square([ULN2003Driver_BoardWidth, ULN2003Driver_BoardHeight], center=true);

			// Mounting holes
			if (includeFixings)
				for (i = [0:1], j = [0:1])
				mirror([i, 0, 0])
				mirror([0, j, 0])
				translate([ULN2003Driver_BoardWidth / 2 - ULN2003Driver_HoleInset, ULN2003Driver_BoardHeight / 2 - ULN2003Driver_HoleInset, 0])
					circle(r = ULN2003Driver_HoleDia / 2);
		}
}

module ULN2003DriverBoard() {

	vitamin(
		"vitamins/ULN2003DriverBoard.scad",
		str("ULN2003 Driver Board"),
		str("ULN2003DriverBoard()")
	) {
		view(d=140);
	}

	if (DebugCoordinateFrames) frame();

	if (DebugConnectors)
		for (c = ULN2003DriverBoard_Cons)
			connector(c);

	ULN2003DriverBoard_PCB();

	// everything else
	translate([(ULN2003Driver_BoardWidth / 2) - ULN2003Driver_HoleInset, (ULN2003Driver_BoardHeight / 2) - ULN2003Driver_HoleInset, 0])
	{
		// Move us to the bottom left of the PCB
		translate([-ULN2003Driver_BoardWidth / 2, -ULN2003Driver_BoardHeight / 2, 0]) {

			// ULN2003 chip
			color("darkgrey")
			translate([1, 8, ULN2003Driver_PCBThickness - eta])
				cube([20, 10, 8]);

			// Arduino header
			translate([6, 6, ULN2003Driver_PCBThickness - eta])
				PcbPinStrip(4);

			// Stepper Connection
			color("white")
			translate([1.5, 19, ULN2003Driver_PCBThickness - eta])
				difference() {
					cube([15.5, 6, 8]);
					translate([1, 1, 1])
						cube([13.5, 4, 8]);
				}
			translate([4, 22, ULN2003Driver_PCBThickness - eta])
				PcbPinStrip(5, 0, false);

			// Power & On/Off Jumper
			translate([24.5, 9, ULN2003Driver_PCBThickness - eta])
				PcbPinStrip(4, 90);

			// LEDS
			translate([6, 29, ULN2003Driver_PCBThickness - eta])
				LED_3mm();
			translate([10, 29, ULN2003Driver_PCBThickness - eta])
				LED_3mm();
			translate([14.5, 29, ULN2003Driver_PCBThickness - eta])
				LED_3mm();
			translate([19, 29, ULN2003Driver_PCBThickness - eta])
				LED_3mm();
		}
	}
}

module PcbPinStrip(numPins = 4, a = 0, spacers = true)
{
    // Standard PCB header pin
    pcbholepitch        = 2.54;         // spacing of PCB holes
    holediameter        = 1.25;         // diameter of PCB hole
    pinheight           =   11;         // length of PCB pins
    pinoffset           =    3;         // offset of PCB pins
    pinwidth            = 0.63;         // width/gauge of PCB pins
    spacerwidth         = 2.25;         // height of breakaway pin frame
    spacerheight        = 2.25;         // height of breakaway pin frame

	rotate(a = a)
	for (i = [0 : numPins - 1]) {
		translate([i * pcbholepitch, 0, 0]) {
			// Header Pins
			color("white")
			translate([0, 0, -pinoffset])
			linear_extrude(pinheight)
				square(pinwidth, center = true);

			if (spacers) {
				// Pin Spacers
				color(Grey20)
				linear_extrude(spacerheight)
					square(spacerwidth, center = true);

				// Break-Away Material
				color(Grey20)
				translate([pcbholepitch/2, 0, 0])
				linear_extrude(spacerheight)
					square([pcbholepitch - spacerwidth, spacerwidth * 0.85], center = true);
			}
		}
	}
}


грамматика

грамматика


Fix definition of the output mode in unit tests

/**
 * @license
 * MIT License
 *
 * Copyright (c) 2017 Jean-Sebastien CONAN
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/**
 * Part of the camelSCAD library.
 *
 * Unit testing API.
 *
 * @package util/test
 * @author jsconan
 */

/**
 * Sets the console mode to prevent HTML use.
 * Should be set using the CLI.
 * @type Boolean
 */
CONSOLE = false;

/**
 * Activate the ANSI support for colored console output.
 * Should be set using the CLI.
 * @type Boolean
 */
ANSI = false;

/**
 * Activate the CSV support
 * Should be set using the CLI.
 * @type Boolean
 */
CSV = false;

/**
 * The mode used to output the results.
 * Default to HTML
 * @type String
 */
outputMode = CSV        ? "csv"
            :ANSI       ? "ansi"
            :CONSOLE    ? "text"
                        : "html"
;

/**
 * Formats a title with respect to the current output mode.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitle(title, type, big) =
    outputMode == "html" ? formatTestTitleHTML(title, type, big)
   :outputMode == "ansi" ? formatTestTitleANSI(title, type, big)
   :outputMode == "csv"  ? formatTestTitleCSV(title, type, big)
                          : formatTestTitleText(title, type, big)
;

/**
 * Formats an assert result with respect to the current output mode.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param Array [details]
 * @returns String
 */
function formatAssertResult(result, message, details) =
    let(details = assertDetails(details))
    outputMode == "html" ? formatAssertResultHTML(result, message, details)
   :outputMode == "ansi" ? formatAssertResultANSI(result, message, details)
   :outputMode == "csv"  ? formatAssertResultCSV(result, message, details)
                          : formatAssertResultText(result, message, details)
;

/**
 * Formats the message related to the expected number of asserts with respect to the current output mode.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAsserts(actual, expected, type) =
    outputMode == "html" ? formatExpectedAssertsHTML(actual, expected, type)
   :outputMode == "ansi" ? formatExpectedAssertsANSI(actual, expected, type)
   :outputMode == "csv"  ? formatExpectedAssertsCSV(actual, expected, type)
                          : formatExpectedAssertsText(actual, expected, type)
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODE: HTML
//

/**
 * Formats a title to be displayed on HTML container.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitleHTML(title, type, big) = htmlColorize(
    big ? "blue" : "darkblue",
    str(
        string(type),
        htmlTag(big ? "big" : "small", wrap(" [", title, "]"))
    )
);

/**
 * Formats an assert result to be displayed on HTML container.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param String [details]
 * @returns String
 */
function formatAssertResultHTML(result, message, details) = htmlColorize(
    result ? "green" : "red",
    str(
        htmlTag("b", assertStatus(result)),
        htmlTag("small", wrap(" [", assertContext(), "]")),
        " ",
        htmlTag("code", message),
        wrap(" (", !result && details ? htmlTag("tt", details) : "", ")")
    )
);

/**
 * Formats the message related to the expected number of asserts to be displayed on HTML container.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAssertsHTML(actual, expected, type) =
    let(
        result = checkExpectedAsserts(actual, expected),
        message = messageExpectedAsserts(actual, expected, type)
    )
    htmlColorize(
        result ? "green" : "red",
        str(
            htmlTag("small", wrap("[", assertContext(), "] ")),
            htmlTag("code", message)
        )
    )
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODE: ANSI
//

/**
 * Formats a title to be displayed on ANSI console.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitleANSI(title, type, big) = str(
    ansiForeground(ANSI_COLOR_BLUE, big),
    formatTestTitleText(title, type, big),
    ansiCodeOff()
);

/**
 * Formats an assert result to be displayed on ANSI console.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param String [details]
 * @returns String
 */
function formatAssertResultANSI(result, message, details) = str(
    ansiForeground(result ? ANSI_COLOR_GREEN : ANSI_COLOR_RED),
    formatAssertResultText(result, message, details),
    ansiCodeOff()
);

/**
 * Formats the message related to the expected number of asserts to be displayed on ANSI console.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAssertsANSI(actual, expected, type) =
    let(
        result = checkExpectedAsserts(actual, expected)
    )
    str(
        ansiForeground(result ? ANSI_COLOR_GREEN : ANSI_COLOR_RED),
        formatExpectedAssertsText(actual, expected, type),
        ansiCodeOff()
    )
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODE: TEXT
//

/**
 * Formats a title as a simple text.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitleText(title, type, big) = str(
    string(type),
    wrap(" [", title, "]")
);

/**
 * Formats an assert result to be displayed as simple text.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param String [details]
 * @returns String
 */
function formatAssertResultText(result, message, details) = str(
    "assert ",
    assertStatus(result),
    wrap(" [", assertContext(), "]"),
    " ",
    string(message),
    wrap(" (", !result ? string(details) : "", ")")
);

/**
 * Formats the message related to the expected number of asserts to be displayed as simple text.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAssertsText(actual, expected, type) =
    let(
        message = messageExpectedAsserts(actual, expected, type)
    )
    str(
        "expect ",
        wrap("[", assertContext(), "] "),
        string(message)
    )
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODE: CSV
//

/**
 * Formats a title to be displayed as CSV.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitleCSV(title, type, big) = str(
    string(type),
    ";",
    string(title)
);

/**
 * Formats an assert result to be displayed as CSV.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param String [details]
 * @returns String
 */
function formatAssertResultCSV(result, message, details) = str(
    "assert;",
    assertStatus(result),
    ";",
    assertContext(),
    ";",
    string(message),
    ";",
    !result ? string(details) : ""
);

/**
 * Formats the message related to the expected number of asserts to be displayed as CSV.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAssertsCSV(actual, expected, type) =
    let(
        result = checkExpectedAsserts(actual, expected),
        message = messageExpectedAsserts(actual, expected, type)
    )
    str(
        "expect;",
        string(type),
        ";",
        uor(expected, ""),
        ";",
        integer(actual),
        ";",
        result,
        ";",
        assertContext(),
        ";",
        string(message)
    )
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// HELPERS
//

/**
 * Gets the right assert status label.
 *
 * @private
 * @param Boolean result
 * @returns String
 */
function assertStatus(result) = result ? "#OK" : "#FAILED";

/**
 * Build the context label for the current assert.
 *
 * @private
 * @returns String
 */
function assertContext() = str(
    string($testModule),
    $testModule && $testUnit ? ":" : "",
    string($testUnit)
);

/**
 * Formats the context of an assert.
 *
 * @private
 * @param Array details
 * @returns String
 */
function assertDetails(details) =
    let(l = len(details))
    l > 1 ? str("[expected: ", details[1], "] [actual: ", details[0], "]")
   :l > 0 ? str("[value: ", details[0], "]")
          : ""
;

/**
 * Checks if the expected number of asserts is respected.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @returns Boolean
 */
function checkExpectedAsserts(actual, expected) =
    let(
        actual = integer(actual),
        single = isUndef(expected),
        expected = integer(expected)
    )
    single ? !!actual : actual == expected
;

/**
 * Builds the message related to the expected number of asserts.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function messageExpectedAsserts(actual, expected, type) =
    let(
        type = string(type),
        actual = integer(actual),
        single = isUndef(expected),
        expected = integer(expected)
    )
    str(
        !actual && expected ? str("Expected ", expected, " ", type, expected > 1 ? "s" : "", " but none were run")
        :!actual ? str("Expected at least one ", type, ", but none were run")
        :single ? str(actual, " ", type, actual > 1 ? "s were run" : " was run")
        :actual == expected ? str(actual, " ", type, expected > 1 ? "s" : "", " of ", expected, actual > 1 ? " were run" : " was run")
        :str("Expected ", expected, " ", type, expected > 1 ? "s" : "", " but ", actual, actual > 1 ? " were run" : " was run")
    )
;

/**
 * @license
 * MIT License
 *
 * Copyright (c) 2017 Jean-Sebastien CONAN
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/**
 * Part of the camelSCAD library.
 *
 * Unit testing API.
 *
 * @package util/test
 * @author jsconan
 */

/**
 * Sets the console mode to prevent HTML use.
 * Should be set using the CLI.
 * @type Boolean
 */
CONSOLE = false;

/**
 * Activate the ANSI support for colored console output.
 * Should be set using the CLI.
 * @type Boolean
 */
ANSI = false;

/**
 * Activate the CSV support
 * Should be set using the CLI.
 * @type Boolean
 */
CSV = false;

/**
 * The mode used to output the results.
 * Default to HTML
 * @type String
 */
$outputMode = $outputMode ? $outputMode
             :CSV         ? "csv"
             :ANSI        ? "ansi"
             :CONSOLE     ? "text"
                          : "html"
;

/**
 * Formats a title with respect to the current output mode.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitle(title, type, big) =
    $outputMode == "html" ? formatTestTitleHTML(title, type, big)
   :$outputMode == "ansi" ? formatTestTitleANSI(title, type, big)
   :$outputMode == "csv"  ? formatTestTitleCSV(title, type, big)
                          : formatTestTitleText(title, type, big)
;

/**
 * Formats an assert result with respect to the current output mode.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param Array [details]
 * @returns String
 */
function formatAssertResult(result, message, details) =
    let(details = assertDetails(details))
    $outputMode == "html" ? formatAssertResultHTML(result, message, details)
   :$outputMode == "ansi" ? formatAssertResultANSI(result, message, details)
   :$outputMode == "csv"  ? formatAssertResultCSV(result, message, details)
                          : formatAssertResultText(result, message, details)
;

/**
 * Formats the message related to the expected number of asserts with respect to the current output mode.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAsserts(actual, expected, type) =
    $outputMode == "html" ? formatExpectedAssertsHTML(actual, expected, type)
   :$outputMode == "ansi" ? formatExpectedAssertsANSI(actual, expected, type)
   :$outputMode == "csv"  ? formatExpectedAssertsCSV(actual, expected, type)
                          : formatExpectedAssertsText(actual, expected, type)
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODE: HTML
//

/**
 * Formats a title to be displayed on HTML container.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitleHTML(title, type, big) = htmlColorize(
    big ? "blue" : "darkblue",
    str(
        string(type),
        htmlTag(big ? "big" : "small", wrap(" [", title, "]"))
    )
);

/**
 * Formats an assert result to be displayed on HTML container.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param String [details]
 * @returns String
 */
function formatAssertResultHTML(result, message, details) = htmlColorize(
    result ? "green" : "red",
    str(
        htmlTag("b", assertStatus(result)),
        htmlTag("small", wrap(" [", assertContext(), "]")),
        " ",
        htmlTag("code", message),
        wrap(" (", !result && details ? htmlTag("tt", details) : "", ")")
    )
);

/**
 * Formats the message related to the expected number of asserts to be displayed on HTML container.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAssertsHTML(actual, expected, type) =
    let(
        result = checkExpectedAsserts(actual, expected),
        message = messageExpectedAsserts(actual, expected, type)
    )
    htmlColorize(
        result ? "green" : "red",
        str(
            htmlTag("small", wrap("[", assertContext(), "] ")),
            htmlTag("code", message)
        )
    )
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODE: ANSI
//

/**
 * Formats a title to be displayed on ANSI console.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitleANSI(title, type, big) = str(
    ansiForeground(ANSI_COLOR_BLUE, big),
    formatTestTitleText(title, type, big),
    ansiCodeOff()
);

/**
 * Formats an assert result to be displayed on ANSI console.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param String [details]
 * @returns String
 */
function formatAssertResultANSI(result, message, details) = str(
    ansiForeground(result ? ANSI_COLOR_GREEN : ANSI_COLOR_RED),
    formatAssertResultText(result, message, details),
    ansiCodeOff()
);

/**
 * Formats the message related to the expected number of asserts to be displayed on ANSI console.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAssertsANSI(actual, expected, type) =
    let(
        result = checkExpectedAsserts(actual, expected)
    )
    str(
        ansiForeground(result ? ANSI_COLOR_GREEN : ANSI_COLOR_RED),
        formatExpectedAssertsText(actual, expected, type),
        ansiCodeOff()
    )
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODE: TEXT
//

/**
 * Formats a title as a simple text.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitleText(title, type, big) = str(
    string(type),
    wrap(" [", title, "]")
);

/**
 * Formats an assert result to be displayed as simple text.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param String [details]
 * @returns String
 */
function formatAssertResultText(result, message, details) = str(
    "assert ",
    assertStatus(result),
    wrap(" [", assertContext(), "]"),
    " ",
    string(message),
    wrap(" (", !result ? string(details) : "", ")")
);

/**
 * Formats the message related to the expected number of asserts to be displayed as simple text.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAssertsText(actual, expected, type) =
    let(
        message = messageExpectedAsserts(actual, expected, type)
    )
    str(
        "expect ",
        wrap("[", assertContext(), "] "),
        string(message)
    )
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MODE: CSV
//

/**
 * Formats a title to be displayed as CSV.
 *
 * @private
 * @param String title
 * @param String type
 * @param Boolean [big]
 * @returns String
 */
function formatTestTitleCSV(title, type, big) = str(
    string(type),
    ";",
    string(title)
);

/**
 * Formats an assert result to be displayed as CSV.
 *
 * @private
 * @param Boolean result
 * @param String message
 * @param String [details]
 * @returns String
 */
function formatAssertResultCSV(result, message, details) = str(
    "assert;",
    assertStatus(result),
    ";",
    assertContext(),
    ";",
    string(message),
    ";",
    !result ? string(details) : ""
);

/**
 * Formats the message related to the expected number of asserts to be displayed as CSV.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function formatExpectedAssertsCSV(actual, expected, type) =
    let(
        result = checkExpectedAsserts(actual, expected),
        message = messageExpectedAsserts(actual, expected, type)
    )
    str(
        "expect;",
        string(type),
        ";",
        uor(expected, ""),
        ";",
        integer(actual),
        ";",
        result,
        ";",
        assertContext(),
        ";",
        string(message)
    )
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// HELPERS
//

/**
 * Gets the right assert status label.
 *
 * @private
 * @param Boolean result
 * @returns String
 */
function assertStatus(result) = result ? "#OK" : "#FAILED";

/**
 * Build the context label for the current assert.
 *
 * @private
 * @returns String
 */
function assertContext() = str(
    string($testModule),
    $testModule && $testUnit ? ":" : "",
    string($testUnit)
);

/**
 * Formats the context of an assert.
 *
 * @private
 * @param Array details
 * @returns String
 */
function assertDetails(details) =
    let(l = len(details))
    l > 1 ? str("[expected: ", details[1], "] [actual: ", details[0], "]")
   :l > 0 ? str("[value: ", details[0], "]")
          : ""
;

/**
 * Checks if the expected number of asserts is respected.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @returns Boolean
 */
function checkExpectedAsserts(actual, expected) =
    let(
        actual = integer(actual),
        single = isUndef(expected),
        expected = integer(expected)
    )
    single ? !!actual : actual == expected
;

/**
 * Builds the message related to the expected number of asserts.
 *
 * @private
 * @param Number actual
 * @param Number [expected]
 * @param String [type]
 * @returns String
 */
function messageExpectedAsserts(actual, expected, type) =
    let(
        type = string(type),
        actual = integer(actual),
        single = isUndef(expected),
        expected = integer(expected)
    )
    str(
        !actual && expected ? str("Expected ", expected, " ", type, expected > 1 ? "s" : "", " but none were run")
        :!actual ? str("Expected at least one ", type, ", but none were run")
        :single ? str(actual, " ", type, actual > 1 ? "s were run" : " was run")
        :actual == expected ? str(actual, " ", type, expected > 1 ? "s" : "", " of ", expected, actual > 1 ? " were run" : " was run")
        :str("Expected ", expected, " ", type, expected > 1 ? "s" : "", " but ", actual, actual > 1 ? " were run" : " was run")
    )
;


Add connector (currently centre of whole thing!)

// Battery packs
// battery_pack_linear - all batteries in a row
// battery_pack_double - 2 rows of batteries (can't cope with odd numbers!)

// Point of origin is the centre of the end of the end battery.

include <../config/colors.scad>

// AAs - http://en.wikipedia.org/wiki/AA_battery

//                    Battery len, Battery dia, Pack width, Pack depth, Pack height, Linear?, Batteries, Name
BatteryPack_AA_4_SQ = [ 50.5,       14.5,        31.4,       27.5,       57.4,        0,      , 4,       "AA" ];

// Default:
BatteryPack_AA = BatteryPack_AA_4_SQ;

// Constants for access:
BatteryPack_Const_BLen = 0;
BatteryPack_Const_BDia = 1;
BatteryPack_Const_PWidth = 2;
BatteryPack_Const_PDepth = 3;
BatteryPack_Const_PHeight = 4;
BatteryPack_Const_Linear = 5;
BatteryPack_Const_Count = 6;
BatteryPack_Const_Name = 7;

// Connectors:
function BatteryPack_Con_Centre(BP) = [
  [
    BP[BatteryPack_Const_PWidth]/2 + BP[BatteryPack_Const_BDia]/2,
    BP[BatteryPack_Const_PDepth]/2 + BP[BatteryPack_Const_BDia]/2,
    BP[BatteryPack_Const_PHeight]/2,
  ],
  [ 0,0,0 ],
  0, 0, 0
];

// We have the total battery length, assume the positive terminal is 1/20th of the length
module battery(BP) {

  Battery_len = BP[BatteryPack_Const_BLen];
  Battery_dia = BP[BatteryPack_Const_BDia];

  button_h = (1/20)*Battery_len;
  top_h = (1/3)*Battery_len;
  bottom_h = Battery_len-(top_h+button_h);

  color("black")
  linear_extrude(height=bottom_h)
    circle(r=Battery_dia/2);

  color("red")
  translate([0, 0, bottom_h])
    linear_extrude(height=top_h)
      circle(r=Battery_dia/2);

  color(MetalColor)
  translate([0, 0, bottom_h+top_h])
    linear_extrude(height=button_h)
      circle(r=Battery_dia/4);

}

module battery_pack_linear(BP, battery_sep, battery_count) {

  Battery_len = BP[BatteryPack_Const_BLen];
  Battery_dia = BP[BatteryPack_Const_BDia];

  for(i=[0:battery_count-1]) {
    translate([i*(Battery_dia+battery_sep), 0, 0])
      if(i % 2 == 1) {
        translate([0,0,Battery_len/2])
          rotate([180, 0, 0])
            translate([0,0,-Battery_len/2])
              battery(BP);
      } else {
        battery(BP);
      }
  }
}

module battery_pack_double(BP, battery_sep, battery_count) {

  Battery_len = BP[BatteryPack_Const_BLen];
  Battery_dia = BP[BatteryPack_Const_BDia];

  for(i=[0:(battery_count/2)-1]) {
    translate([i*(Battery_dia+battery_sep), 0, 0])
      battery();
    translate([i*(Battery_dia+battery_sep), Battery_dia+battery_sep, 0])
      translate([0,0,Battery_len/2])
        rotate([180, 0, 0])
          translate([0,0,-Battery_len/2])
            battery();
  }
}

// NB, This completely ignores battery count/linear setting! 2x2 is what you get!
module BatteryPack(BP) {

  Battery_len = BP[BatteryPack_Const_BLen];
  Battery_dia = BP[BatteryPack_Const_BDia];
  BatteryPack_height = BP[BatteryPack_Const_PHeight];
  BatteryPack_width = BP[BatteryPack_Const_BWidth];
  BatteryPack_depth = BP[BatteryPack_Const_BDepth];

  // Actually measured, it overhangs by 2mm, 1 because this is actually
  // a radius -- how far is the center-line of the battery offset from the edge
  // of the case (in the y direction).
  battery_depth_offset = Battery_dia/2 - 1;

  render()
  translate([-Battery_dia/2, -Battery_dia/2,0])
  difference() {
   linear_extrude(height=BatteryPack_height)
     hull() {
      // Main body
      translate([Battery_dia/2, Battery_dia/2, 0])
        circle(r=Battery_dia/2);
      translate([BatteryPack_width-Battery_dia/2, Battery_dia/2, 0])
        circle(r=Battery_dia/2);
      translate([Battery_dia/2, BatteryPack_depth-Battery_dia/2, 0])
        circle(r=Battery_dia/2);
      translate([BatteryPack_width-Battery_dia/2, BatteryPack_depth-Battery_dia/2, 0])
        circle(r=Battery_dia/2);
   }

   // Subtract battery shapes
   translate([Battery_dia/2-eta, battery_depth_offset-eta, (BatteryPack_height-Battery_len)/2])
     cylinder(h=Battery_len, r=Battery_dia/2);

   translate([BatteryPack_width-Battery_dia/2+eta, battery_depth_offset-eta, (BatteryPack_height-Battery_len)/2])
    cylinder(h=Battery_len, r=Battery_dia/2);

   translate([Battery_dia/2-eta, BatteryPack_depth-battery_depth_offset+eta, (BatteryPack_height-Battery_len)/2])
    cylinder(h=Battery_len, r=Battery_dia/2);
   translate([BatteryPack_width-Battery_dia/2+eta,BatteryPack_depth-battery_depth_offset+eta,(BatteryPack_height-Battery_len)/2])
    cylinder(h=Battery_len, r=Battery_dia/2);

   translate([BatteryPack_width/4,-BatteryPack_depth+6,(BatteryPack_height-Battery_len)/2])
     cube([BatteryPack_width/2, BatteryPack_depth, Battery_len]);
   translate([BatteryPack_width/4,BatteryPack_depth-6,(BatteryPack_height-Battery_len)/2])
     cube([BatteryPack_width/2, BatteryPack_depth, Battery_len]);

 }
}

*battery_pack_linear(BatteryPack_AA, 2,4);
*battery_pack_double(BatteryPack_AA, 2, 4);

// Battery packs
// battery_pack_linear - all batteries in a row
// battery_pack_double - 2 rows of batteries (can't cope with odd numbers!)

// Point of origin is the centre of the end of the end battery.

include <../config/colors.scad>

// AAs - http://en.wikipedia.org/wiki/AA_battery

//                    Battery len, Battery dia, Pack width, Pack depth, Pack height, Linear?, Name
BatteryPack_AA = [ 50.5,       14.5,        31.4,       27.5,       57.4,        0,       "AA" ];

// Constants for access:
BatteryPack_Const_BLen = 0;
BatteryPack_Const_BDia = 1;
BatteryPack_Const_PWidth = 2;
BatteryPack_Const_PDepth = 3;
BatteryPack_Const_PHeight = 4;
BatteryPack_Const_Linear = 5;
BatteryPack_Const_Name = 6;

// We have the total battery length, assume the positive terminal is 1/20th of the length
module battery(BP) {

  Battery_len = BP[BatteryPack_Const_BLen];
  Battery_dia = BP[BatteryPack_Const_BDia];

  button_h = (1/20)*Battery_len;
  top_h = (1/3)*Battery_len;
  bottom_h = Battery_len-(top_h+button_h);

  color("black")
  linear_extrude(height=bottom_h)
    circle(r=Battery_dia/2);

  color("red")
  translate([0, 0, bottom_h])
    linear_extrude(height=top_h)
      circle(r=Battery_dia/2);

  color(MetalColor)
  translate([0, 0, bottom_h+top_h])
    linear_extrude(height=button_h)
      circle(r=Battery_dia/4);

}

module battery_pack_linear(BP, battery_sep, battery_count) {

  Battery_len = BP[BatteryPack_Const_BLen];
  Battery_dia = BP[BatteryPack_Const_BDia];

  for(i=[0:battery_count-1]) {
    translate([i*(Battery_dia+battery_sep), 0, 0])
      if(i % 2 == 1) {
        translate([0,0,Battery_len/2])
          rotate([180, 0, 0])
            translate([0,0,-Battery_len/2])
              battery(BP);
      } else {
        battery(BP);
      }
  }
}

module battery_pack_double(BP, battery_sep, battery_count) {

  Battery_len = BP[BatteryPack_Const_BLen];
  Battery_dia = BP[BatteryPack_Const_BDia];

  for(i=[0:(battery_count/2)-1]) {
    translate([i*(Battery_dia+battery_sep), 0, 0])
      battery();
    translate([i*(Battery_dia+battery_sep), Battery_dia+battery_sep, 0])
      translate([0,0,Battery_len/2])
        rotate([180, 0, 0])
          translate([0,0,-Battery_len/2])
            battery();
  }
}

// NB, This completely ignores battery count/linear setting! 2x2 is what you get!
module BatteryPack(BP) {

  Battery_len = BP[BatteryPack_Const_BLen];
  Battery_dia = BP[BatteryPack_Const_BDia];
  BatteryPack_height = BP[BatteryPack_Const_PHeight];
  BatteryPack_width = BP[BatteryPack_Const_BWidth];
  BatteryPack_depth = BP[BatteryPack_Const_BDepth];

  // Actually measured, it overhangs by 2mm, 1 because this is actually
  // a radius -- how far is the center-line of the battery offset from the edge
  // of the case (in the y direction).
  battery_depth_offset = Battery_dia/2 - 1;

  render()
  translate([-Battery_dia/2, -Battery_dia/2,0])
  difference() {
   linear_extrude(height=BatteryPack_height)
     hull() {
      // Main body
      translate([Battery_dia/2, Battery_dia/2, 0])
        circle(r=Battery_dia/2);
      translate([BatteryPack_width-Battery_dia/2, Battery_dia/2, 0])
        circle(r=Battery_dia/2);
      translate([Battery_dia/2, BatteryPack_depth-Battery_dia/2, 0])
        circle(r=Battery_dia/2);
      translate([BatteryPack_width-Battery_dia/2, BatteryPack_depth-Battery_dia/2, 0])
        circle(r=Battery_dia/2);
   }

   // Subtract battery shapes
   translate([Battery_dia/2-eta, battery_depth_offset-eta, (BatteryPack_height-Battery_len)/2])
     cylinder(h=Battery_len, r=Battery_dia/2);

   translate([BatteryPack_width-Battery_dia/2+eta, battery_depth_offset-eta, (BatteryPack_height-Battery_len)/2])
    cylinder(h=Battery_len, r=Battery_dia/2);

   translate([Battery_dia/2-eta, BatteryPack_depth-battery_depth_offset+eta, (BatteryPack_height-Battery_len)/2])
    cylinder(h=Battery_len, r=Battery_dia/2);
   translate([BatteryPack_width-Battery_dia/2+eta,BatteryPack_depth-battery_depth_offset+eta,(BatteryPack_height-Battery_len)/2])
    cylinder(h=Battery_len, r=Battery_dia/2);

   translate([BatteryPack_width/4,-BatteryPack_depth+6,(BatteryPack_height-Battery_len)/2])
     cube([BatteryPack_width/2, BatteryPack_depth, Battery_len]);
   translate([BatteryPack_width/4,BatteryPack_depth-6,(BatteryPack_height-Battery_len)/2])
     cube([BatteryPack_width/2, BatteryPack_depth, Battery_len]);

 }
}

*battery_pack_linear(BatteryPack_AA, 2,4);
*battery_pack_double(BatteryPack_AA, 2, 4);


body: convexity

body: convexity

include <constants.scad>;
HEIGHT_BODY = 44.5;
DIAMETER_BODY = 10.4;
BORDER_RADIUS_BODY = 0.2;
HEIGHT_NIPPLE = 0.8;

module body()
{
//Top face of main body
translate([0,0,-(HEIGHT_NIPPLE + BORDER_RADIUS_BODY)])
    cylinder(
        h = BORDER_RADIUS_BODY,
        d = (DIAMETER_BODY - (2 * BORDER_RADIUS_BODY)),
        $fn = FINE);

//Body top shoulder
translate([0,0,-(HEIGHT_NIPPLE + BORDER_RADIUS_BODY)])
    rotate_extrude(convexity = CONVEXITY, $fn = FINE)
    translate([(DIAMETER_BODY/2)-BORDER_RADIUS_BODY, 0, 0])
        circle(r = BORDER_RADIUS_BODY, $fn = FINE);

//Main body cylinder
translate([0,0,-(HEIGHT_BODY - BORDER_RADIUS_BODY)])
    cylinder(
        h = (HEIGHT_BODY - (HEIGHT_NIPPLE + (2* BORDER_RADIUS_BODY))),
        d = DIAMETER_BODY,
        $fn = FINE);

//Body bottom shoulder
translate([0,0,-(HEIGHT_BODY - BORDER_RADIUS_BODY)])
    rotate_extrude(convexity = CONVEXITY, $fn = FINE)
    translate([(DIAMETER_BODY/2)-BORDER_RADIUS_BODY, 0, 0])
        circle(r = BORDER_RADIUS_BODY, $fn = FINE);

//Bottom face of body
translate([0,0,-(HEIGHT_BODY)])
    cylinder(
        h = BORDER_RADIUS_BODY,
        d = (DIAMETER_BODY - (2 * BORDER_RADIUS_BODY)),
        $fn = FINE);
}

body();

include <constants.scad>;
HEIGHT_BODY = 44.5;
DIAMETER_BODY = 10.4;
BORDER_RADIUS_BODY = 0.2;
HEIGHT_NIPPLE = 0.8;

module body()
{
//Top face of main body
translate([0,0,-(HEIGHT_NIPPLE + BORDER_RADIUS_BODY)])
    cylinder(
        h = BORDER_RADIUS_BODY,
        d = (DIAMETER_BODY - (2 * BORDER_RADIUS_BODY)),
        $fn = FINE);

//Body top shoulder
translate([0,0,-(HEIGHT_NIPPLE + BORDER_RADIUS_BODY)])
    rotate_extrude(convexity = 10, $fn = FINE)
    translate([(DIAMETER_BODY/2)-BORDER_RADIUS_BODY, 0, 0])
        circle(r = BORDER_RADIUS_BODY, $fn = FINE);

//Main body cylinder
translate([0,0,-(HEIGHT_BODY - BORDER_RADIUS_BODY)])
    cylinder(
        h = (HEIGHT_BODY - (HEIGHT_NIPPLE + (2* BORDER_RADIUS_BODY))),
        d = DIAMETER_BODY,
        $fn = FINE);

//Body bottom shoulder
translate([0,0,-(HEIGHT_BODY - BORDER_RADIUS_BODY)])
    rotate_extrude(convexity = 10, $fn = FINE)
    translate([(DIAMETER_BODY/2)-BORDER_RADIUS_BODY, 0, 0])
        circle(r = BORDER_RADIUS_BODY, $fn = FINE);

//Bottom face of body
translate([0,0,-(HEIGHT_BODY)])
    cylinder(
        h = BORDER_RADIUS_BODY,
        d = (DIAMETER_BODY - (2 * BORDER_RADIUS_BODY)),
        $fn = FINE);
}

body();


[scad] add a small hole for pushing out the pcb

/*
	kube.scad - OpenSCAD code generating the Fluksokube enclosure

	Copyright (C) 2014 Bart Van Der Meerssche <bart@flukso.net>

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

$fn = 100; /* number of facets per circle */

/*
	_h = height
	_w = width
	_r = radius
	_g = gap
	_t = tolerance
	_s = scaling factor (printer specific)
*/

dim_s = 1;
dim_s = 1.015; /* makerbot */
kube_s = [dim_s, dim_s, dim_s];

magnet_h = 5;
magnet_t = 0.1;
magnet_r = 8 - magnet_t; /* inner diameter now! */

flk_h = 23.5;
flk_t = 0.2;
flk_w = 30.5 + 2*flk_t;

wall_w = 1;
outer_w = flk_w + 2*wall_w;

support_h = outer_w - wall_w - flk_h;
support_r = magnet_r;
support_w = 1;
support_g = 3;

ring_h = support_h - magnet_h;
ring_t = 0.2;
ring_inner_r = support_r + ring_t;
ring_outer_r = ring_inner_r + 2;

hole_r = 1.2;

echo("*** kube dimensions ***");
echo("scaling vector", "=", kube_s);
echo("magnet diameter", "=", magnet_r*2);
echo("flk width", "=", flk_w);
echo("kube width", "=", outer_w);
echo("support diameter", "=", support_r*2);
echo("support height", "=", support_h);
echo("ring height", "=", ring_h);
echo("ring inner diameter", "=", ring_inner_r*2);
echo("ring outer diameter", "=", ring_outer_r*2);
echo("***********************");

module kube()
{
	difference() {
		cube(outer_w);
		translate([wall_w, wall_w, wall_w]) {
			cube([flk_w, flk_w, outer_w - wall_w]);
		}
		translate([outer_w/2, outer_w/2, 0]) cylinder(r=hole_r, h=wall_w);
	}

	translate([outer_w/2, outer_w/2, wall_w]) {
		rotate(a=[0, 0, 45]) difference() {
			cylinder(r=support_r, h=support_h);
			cylinder(r=support_r - support_w, h=support_h);
			translate([-support_r, -support_g/2, 0]) cube([support_r*2, support_g, support_h]);
			translate([-support_g/2, -support_r, 0]) cube([support_g, support_r*2, support_h]);
		}
	}
}

module ring()
{
	difference() {
		cylinder(r=ring_outer_r, h=ring_h);
		cylinder(r=ring_inner_r, h=ring_h);
	}
}

scale(kube_s) {
	kube();
	translate([1.5*outer_w, outer_w/2, 0]) ring();
}

/*
	kube.scad - OpenSCAD code generating the Fluksokube enclosure

	Copyright (C) 2014 Bart Van Der Meerssche <bart@flukso.net>

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

$fn = 100; /* number of facets per circle */

/*
	_h = height
	_w = width
	_r = radius
	_g = gap
	_t = tolerance
	_s = scaling factor (printer specific)
*/

dim_s = 1;
dim_s = 1.015; /* makerbot */
kube_s = [dim_s, dim_s, dim_s];

magnet_h = 5;
magnet_t = 0.1;
magnet_r = 8 - magnet_t; /* inner diameter now! */

flk_h = 23.5;
flk_t = 0.2;
flk_w = 30.5 + 2*flk_t;

wall_w = 1;
outer_w = flk_w + 2*wall_w;

support_h = outer_w - wall_w - flk_h;
support_r = magnet_r;
support_w = 1;
support_g = 3;

ring_h = support_h - magnet_h;
ring_t = 0.2;
ring_inner_r = support_r + ring_t;
ring_outer_r = ring_inner_r + 2;

echo("*** kube dimensions ***");
echo("scaling vector", "=", kube_s);
echo("magnet diameter", "=", magnet_r*2);
echo("flk width", "=", flk_w);
echo("kube width", "=", outer_w);
echo("support diameter", "=", support_r*2);
echo("support height", "=", support_h);
echo("ring height", "=", ring_h);
echo("ring inner diameter", "=", ring_inner_r*2);
echo("ring outer diameter", "=", ring_outer_r*2);
echo("***********************");

module kube()
{
	difference() {
		cube(outer_w);
		translate([wall_w, wall_w, wall_w]) {
			cube([flk_w, flk_w, outer_w - wall_w]);
		}
	}

	translate([outer_w/2, outer_w/2, wall_w]) {
		rotate(a=[0, 0, 45]) difference() {
			cylinder(r=support_r, h=support_h);
			cylinder(r=support_r - support_w, h=support_h);
			translate([-support_r, -support_g/2, 0]) cube([support_r*2, support_g, support_h]);
			translate([-support_g/2, -support_r, 0]) cube([support_g, support_r*2, support_h]);
		}
	}
}

module ring()
{
	difference() {
		cylinder(r=ring_outer_r, h=ring_h);
		cylinder(r=ring_inner_r, h=ring_h);
	}
}

scale(kube_s) {
	kube();
	translate([1.5*outer_w, outer_w/2, 0]) ring();
}