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

General-purpose computers doing something specific

  • Slicing Anomaly: Resolved, With Cross-Check

    Some pix that serve as a stick in the ground showing that my current Slic3r configuration constellation doesn’t produce thin infill

    All of the layers in the 20 mm calibration cube look just like this:

    Solid cube - Slic3r normal infill
    Solid cube – Slic3r normal infill

    The bottom layer of the Tux mold comes out solid:

    Tux thread fill - bottom
    Tux thread fill – bottom

    As does the top:

    Tux thread fill - top
    Tux thread fill – top

    The Gcode Analyzer algorithm that assigns colors to numeric values tends to produce many aliases, although most of the time you can figure out what’s going on. If somebody wants to dive into the code, I’d like to have unique colors and get the color table sorted in ascending order.

    The current Slic3r configuration:

    # generated by Slic3r 1.1.1 on Sat May  3 10:31:36 2014
    avoid_crossing_perimeters = 0
    bed_size = 190,250
    bed_temperature = 70
    bottom_solid_layers = 3
    bridge_acceleration = 0
    bridge_fan_speed = 100
    bridge_flow_ratio = 1
    bridge_speed = 150
    brim_width = 0
    complete_objects = 0
    cooling = 1
    default_acceleration = 0
    disable_fan_first_layers = 1
    duplicate_distance = 6
    end_gcode = ;-- Slic3r End G-Code for M2 starts --\n;  Ed Nisley KE4NZU - 15 November 2013\nM104 S0		; drop extruder temperature\nM140 S0		; drop bed temperature\nM106 S0		; bed fan off\nG1 Z180 F2000	; lower bed\nG1 X130 Y125 F30000	; nozzle to right, bed front\nM84     	; disable motors\n;-- Slic3r End G-Code ends --
    external_perimeter_speed = 25
    external_perimeters_first = 0
    extra_perimeters = 1
    extruder_clearance_height = 25
    extruder_clearance_radius = 15
    extruder_offset = 0x0
    extrusion_axis = E
    extrusion_multiplier = 1.07
    extrusion_width = 0.4
    fan_always_on = 0
    fan_below_layer_time = 30
    filament_diameter = 1.79
    fill_angle = 45
    fill_density = 100%
    fill_pattern = rectilinear
    first_layer_acceleration = 0
    first_layer_bed_temperature = 70
    first_layer_extrusion_width = 0.4
    first_layer_height = 100%
    first_layer_speed = 25
    first_layer_temperature = 175
    g0 = 0
    gap_fill_speed = 50
    gcode_arcs = 0
    gcode_comments = 0
    gcode_flavor = reprap
    infill_acceleration = 0
    infill_every_layers = 3
    infill_extruder = 1
    infill_extrusion_width = 0
    infill_first = 1
    infill_only_where_needed = 0
    infill_speed = 150
    interface_shells = 0
    layer_gcode = 
    layer_height = 0.2
    max_fan_speed = 100
    min_fan_speed = 75
    min_print_speed = 4
    min_skirt_length = 15
    notes = 
    nozzle_diameter = 0.35
    only_retract_when_crossing_perimeters = 1
    ooze_prevention = 0
    output_filename_format = [input_filename_base].gcode
    overhangs = 1
    perimeter_acceleration = 0
    perimeter_extruder = 1
    perimeter_extrusion_width = 0.4
    perimeter_speed = 150
    perimeters = 2
    post_process = 
    print_center = 0,0
    raft_layers = 0
    randomize_start = 1
    resolution = 0.05
    retract_before_travel = 1
    retract_layer_change = 0
    retract_length = 1
    retract_length_toolchange = 5
    retract_lift = 0
    retract_restart_extra = 0
    retract_restart_extra_toolchange = 0
    retract_speed = 60
    skirt_distance = 3
    skirt_height = 1
    skirts = 3
    slowdown_below_layer_time = 20
    small_perimeter_speed = 25
    solid_fill_pattern = rectilinear
    solid_infill_below_area = 5
    solid_infill_every_layers = 0
    solid_infill_extrusion_width = 0
    solid_infill_speed = 150
    spiral_vase = 0
    standby_temperature_delta = -5
    start_gcode = ;-- Slic3r Start G-Code for M2 starts --\n;  Ed Nisley KE4NZU - 15 Nov 2013\n;  28 Feb 2014 - 6 Mar 2014 - tweak Z offset\n; Z-min switch at platform, must move nozzle to X=130 to clear\nM140 S[first_layer_bed_temperature]	; start bed heating\nG90				; absolute coordinates\nG21				; millimeters\nM83				; relative extrusion distance\nG92 Z0			; set Z to zero, wherever it might be now\nG1 Z10 F1000	; move platform downward to clear nozzle; may crash at bottom\nG28 Y0			; home Y to be sure of clearing probe point\nG92 Y-127 		; set origin so 0 = center of plate\nG28 X0			; home X\nG92 X-95		; set origin so 0 = center of plate\nG1 X130 Y0 F30000	; move off platform to right side, center Y\nG28 Z0			; home Z with switch near center of platform\nG92 Z-4.40		; set origin to measured z offset\nG0 Z2.0			; get air under switch\nG0 Y-127 F10000	; set up for priming, zig around corner\nG0 X0			;  center X\nM109 S[first_layer_temperature]	; set extruder temperature and wait\nM190 S[first_layer_bed_temperature]	; wait for bed to finish heating\nG1 Z0.0 F500	; put extruder near plate \nG1 E25 F300		; prime to get pressure, generate blob\nG1 Z5 F2000		; rise above blob\nG1 X15 Y-125 F20000	; jerk away from blob, move over surface\nG1 Z0.0 F1000	; dab nozzle to attach outer snot to platform\nG4 P1			; pause to attach\nG1 X35 F500		; slowly smear snot to clear nozzle\nG1 Z1.0 F2000	; clear bed for travel\n;-- Slic3r Start G-Code ends --
    start_perimeters_at_concave_points = 1
    start_perimeters_at_non_overhang = 1
    support_material = 0
    support_material_angle = 0
    support_material_enforce_layers = 0
    support_material_extruder = 1
    support_material_extrusion_width = 0
    support_material_interface_extruder = 1
    support_material_interface_layers = 0
    support_material_interface_spacing = 0
    support_material_pattern = honeycomb
    support_material_spacing = 2.5
    support_material_speed = 150
    support_material_threshold = 0
    temperature = 175
    thin_walls = 1
    threads = 2
    toolchange_gcode = 
    top_infill_extrusion_width = 0.4
    top_solid_infill_speed = 25
    top_solid_layers = 3
    travel_speed = 250
    use_firmware_retraction = 0
    use_relative_e_distances = 0
    vibration_limit = 0
    wipe = 0
    z_offset = 0
    
  • Monthly Science: Minimum Groundwater Temperatures, 2006-2014

    The picture says it all:

    Basement Air Groundwater Minimum Temperatures - 2006-2014
    Basement Air Groundwater Minimum Temperatures – 2006-2014

    Much as we thought, this past winter was really cold.

    The data consists of all 3/4 million data logger records concatenated into one huge CSV file, fed through a Sed pipe to normalize all the dates & suchlike, then passed into a Python script that produces one record for each day (all 2561 of ’em) containing the date, minimum air & water temperatures, and the minimum relative humidity.

    This needs (a lot) more work to be pretty, but at least the pieces hang together.

    The Python source code:

    #!/usr/bin/python3
    ''' Extract minimum groundwater / air temperatures & humidity from CSV files
    '''
    
    import sys
    import csv
    import datetime
    import string
    
    # Columns in Hobo datalogger CSV file
    
    SEQNUM = 0
    DATETIME = 1
    AIRTEMP = 2
    RELHUM = 3
    WATERTEMP = 4
    
    datapoints = {}
    
    with open('AllClean.csv',encoding='iso-8859-15') as dbi:
        for row in csv.reader(dbi):
            if (not row[SEQNUM].startswith("#")):            # discard comments
                logdt = datetime.datetime.strptime(row[DATETIME],'%m/%d/%Y %H:%M:%S')
                logdate = datetime.datetime.date(logdt)
                if (logdate in datapoints):             # accumulate minimum temps & RH
                    datapoints[logdate][0] = min(datapoints[logdate][0],row[AIRTEMP]) 
                    datapoints[logdate][1] = min(datapoints[logdate][1],row[RELHUM]) 
                    datapoints[logdate][2] = min(datapoints[logdate][2],row[WATERTEMP]) 
                else:
                    datapoints[logdate] = [row[AIRTEMP], row[RELHUM], row[WATERTEMP]]
    
    with open('AllMinData.csv','w',newline='') as csvf:
        dbo = csv.writer(csvf)
        dbo.writerow(('#Date','Min Air T','Min RH','Min Water T'))
        for key,value in sorted(datapoints.items()):
            dbo.writerow([key,value[0],value[1],value[2]])
    

    The encoding='iso-8859-15' for the input file turns out to be absolutely essential, as the Hoboware program generating the CSV files uses a 0xb0 character for the usual degree symbol. Alas, that chokes the default utf-8, ascii, and even cp437 codecs. Took a while to figure that out, it did, indeed.

    There remain random anomalies in the data, in addition to the glitches produced by unplugging the remote temperature sensor cable. I may simply discard the last few records of each CSV file; right now, the Gnuplot code simply ignores temperatures under 30 °F and over 80 °F.

    The Gnuplot script that produced the graph consisted of some hand-fed tweakery based on the guts of the routine that plotted the original records, with the output image bank-shotting off the clipboard into GIMP on its way to becoming a PNG file. Phew!

  • Slicing Anomaly: Thin Fill

    Having discovering that the chocolate mold positives suffered from sparse top infill, to the extent that silicone rubber would flow right though the surface…

    Tux Gradient - PLA positive detail
    Tux Gradient – PLA positive detail

    … I ran off a few variations of the classic 20 mm calibration “cube” (which is 10 mm tall):

    Solid cube - thin top infill - on platform
    Solid cube – thin top infill – on platform

    Not only were the infilled surfaces porous, I could see right through the block! That’s impossible to photograph, but here’s a laser beam shining through the entire 10 mm stack, showing how precisely the M2 aligns 50 under-filled thread layers:

    Solid cube - laser transmission
    Solid cube – laser transmission

    The yellow spot in the middle marks the overexposed laser beam. There’s a distinct beam passing through the block that, with the proper orientation, can create a spot on the cutting mat atop my desk.

    In fact, I can blow air through the blocks; one could use them as (rather coarse) air filters.

    Normally, underfill happens when a mechanical problem prevents the printer from feeding enough filament to keep up with demand, but that’s not the case here: the perimeter threads came out exactly 0.4 mm wide for the entire height of the cube, as you can see if you click the picture for more dots. The top and bottom infill, plus all the interior threads, seem to be about half the nominal width and don’t touch their neighbors on the same XY plane at all.

    Alex Ustyantsev’s incomparable G-Code Analyzer shows that Slic3r baked the problem right into the G-Code, so the M2 is cranking out exactly the right amount of filament:

    Solid cube - Slic3r thin infill
    Solid cube – Slic3r thin infill

    The colors show the length of extruder filament per millimeter of XY motion, not the usual XY speed, with the two perimeter threads at 0.033 mm/mm and the interior at 0.18 mm/mm. In round numbers, the G-Code starves the infill by a factor of 1.8, which is close enough to the factor of two I’d guessed going into this mess.

    Being that type of guy, I set the exact extrusion thickness and width (0.20 x 0.40 mm), rather than let Slic3r pick them. The extruded thread has a fixed cross-section of (roughly) 0.080 mm2 and a millimeter of XY motion thus requires 0.080 mm3 of filament.

    The PLA filament measures 1.79 mm diameter, for a cross-section of 2.5 mm2. Getting 0.080 mm3 from the incoming filament requires feeding 0.032 mm into the extruder, which is almost exactly what you see for the perimeter threads.

    After restoring Slic3r’s default configuration, the problem Went Away, which suggests that I backed the algorithms into a corner with some perverse combination of settings. Rebuilding my usual configuration from the defaults also worked fine, so it’s obviously not Slic3r’s problem.

    Which one is not like the other ones?

    Solid cube tests
    Solid cube tests

    You can see the thin infill on three of those cubes, with the solid one in the lower right showing how it should look.

    The solid cube weighs 4.4 g and the thin-fill variations weigh 2.7 to 2.9 g. Assuming PLA density = 1.25 g/cm3 and “cube” volume = 4 cm3, a completely solid cube should weigh 5.0 g. I think 4.4 g is close enough; the top surface came out flat with nice adjacent-thread fusion. Working backwards, the average fill = 88%; the perimeter is fused-glass solid, so the actual infill will be a bit under that.

    I generally run Slic3r from my desktop box, with ~/.Slic3r symlinked to the actual config directory and its files on the NFS server downstairs. Perhaps running different versions of Slic3r on two or three different boxes, all using the same config files, wrecked something that didn’t show up in the UI and produced bad slices. I probably ran two different versions of Slic3r at the same time against the same files, although I wasn’t simultaneously typing at both keyboards.

    Moral of the story: check the G-Code before assuming a hardware failure!

  • Revised Thinwall Open Box Calibration Object

    Which thinwall open box is better?

    Object A:

    Thinwall Open Box - Minkowski - solid model
    Thinwall Open Box – Minkowski – solid model

    or Object B:

    Thinwall Open Box - hull - solid model
    Thinwall Open Box – hull – solid model

    The latter, of course: I blundered the inner corner radius, which occasionally produced little tiny dots of infill that shouldn’t be there. Just one of those errors that hides in plain sight until something else goes wrong, then it’s obvious.

    Rather than fix the Minkowski version, I rebuilt it using the hull() operator to shrinkwrap four cylinders for each solid, then remove the smaller block from the larger. Commenting out the hull() operators  shows that the cylinders now line up properly:

    Thinwall Open Box - un-hulled - solid model
    Thinwall Open Box – un-hulled – solid model

    The OpenSCAD source code:

    // Thin wall open box calibration piece
    // Adapted from Coasterman's Calibration set
    // Ed Nisley - KE4ZNU - Dec 2011
    // Adjust for Slic3r/M2 - March 2013
    // Reworked for hull() with correct corner radii - April 2014
    
    //-------
    //- Extrusion parameters must match reality!
    //  None of the fill parameters matter
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    Protrusion = 0.1;           // make holes end cleanly
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    //-------
    // Dimensions
    
    Height = IntegerMultiple(5.0,ThreadThick);
    
    WallThick = ThreadWidth;
    
    CornerRadius = 2.0;
    CornerSides = 4*8;
    
    SideLen = 20.0 - 2*CornerRadius;
    
    Rotation = 45;
    
    //-------
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      Range = floor(50 / Space);
    
        for (x=[-Range:Range])
          for (y=[-Range:Range])
            translate([x*Space,y*Space,Size/2])
              %cube(Size,center=true);
    }
    
    //-------
    
    ShowPegGrid();
    
    rotate(Rotation)
    	difference() {
    		hull() {
    			for (i=[-1,1], j=[-1,1])
    				translate([i*SideLen/2,j*SideLen/2,0])
    					cylinder(r=CornerRadius,h=Height,$fn=CornerSides);
    		}
    		hull() {
    			for (i=[-1,1], j=[-1,1])
    				translate([i*SideLen/2,j*SideLen/2,-Protrusion])
    					cylinder(r=(CornerRadius - WallThick),h=(Height + 2*Protrusion),$fn=CornerSides);
    		}
    	}
    
  • Wide-Angle Lens Distortion Correction

    The Sony HDR-AS30V camera lens has a view angle of 120° or 170°, achieved by internal image processing rather than mechanical lens adjustments. For most action-camera purposes you don’t care about fisheye distortion, but sometimes a more rectilinear picture will look better, in which case the GIMP’s Lens Distortion filter comes in handy.

    A still image at 120°, which doesn’t look all that bad, really:

    Sony HDR-AS30V 120 angle - as captured
    Sony HDR-AS30V 120 angle – as captured

    Applying Main=-25 gives this:

    Sony HDR-AS30V 120 angle - corrected
    Sony HDR-AS30V 120 angle – corrected

    A frame captured from video at 170°, with the overhead wires hanging upward:

    Sony HDR-AS30V 170 angle - as captured
    Sony HDR-AS30V 170 angle – as captured

    Applying Main=-25, Edge=-12.5, Zoom=+8 flattens them enough to be acceptable:

    Sony HDR-AS30V 170 angle - corrected
    Sony HDR-AS30V 170 angle – corrected

    The main effect of the Zoom parameter seems to be discarding the severely distorted remnants around the edges of the corrected 170° view. Sometimes, those pixels around the edges can be very, very important, so I’d rather make that decision after the fact.

    If you must fix many images at once, Fred’s defisheye ImageMagick Script would certainly be useful. There’s also a bare-knuckles ImageMagick version, including how to measure lens parameters.

  • Strobe Photography: Control Program

    A solderless breadboard sufficed for the simple circuitry behind the strobe controller:

    Strobe Photography - control breadboard
    Strobe Photography – control breadboard

    I used a separate 7.5 V supply for the Arduino Pro Mini to keep the relay noise out of the VCC circuit, but that’s probably not really necessary; you could back-drive the Pro Mini’s regulator with +5 V and it’d be perfectly happy. There’s a +5 V wall wart for the relay, LEDs, and so forth.

    Protip: you do not want to drive all the other circuitry through the Pro Mini’s tiny little regulator. Work out the power dissipation in the regulator caused by a 130 Ω relay, about 10 mA for the laser, 100 mA for the white LED, and whatever the Pro Mini draws. Yeah, some of those are intermittent loads, but work it out anyway.

    A 1.5 V bench supply powers the Xenon strobe in place of the AA alkaline cell I used at first. The boost circuit pins the supply at 3 A for a few seconds, then settles at about 350 mA (!) while idling; no wonder the poor little AA cells don’t last very long!

    The control program is also dead simple; it’s mostly a state machine that notices when the photocurrent drops to zero, then steps through a series of fixed delays while turning the laser, LED, and strobe outputs on and off.

    The default values highlight a falling object about 200 mm below the laser beam-break sensor, assuming you release the object just above the beam:

    Ball at 200 mm - detail
    Ball at 200 mm – detail

    The laser beam is at the 200 mm mark, so that ball passing 400 mm has dropped 200 mm.

    The quadrature encoder knob recycles the same interrupt handler I used earlier, with the shaft button selecting either the LED delay (pushed) or the Xenon strobe delay (released). There’s precious little error checking, as befits a quick hack job, so use at your own risk…

    The Arduino source code:

    // Optical flash triggering
    // Ed Nisley - KE4ANU - March 2014
    
    //----------
    // Pin assignments
    
    const byte PIN_KNOB_A = 2;			// knob A switch - must be on ext interrupt 2
    const byte PIN_KNOB_B = 4;			//  .. B switch
    const byte PIN_KNOB_SWITCH = A3;	//  .. shaft push switch
    
    const byte PIN_PHOTOCURRENT = A0;	// photodiode current input
    
    const byte PIN_LASER = 8;   		// laser drive -active
    const byte PIN_LED = 7;		   		// LED drive -active
    const byte PIN_FLASH = 12;			// Xenon flash relay -active
    
    const byte PIN_SYNC = 13;			// scope sync - and Arduino LED
    
    //----------
    // Constants
    
    enum FALLING_STATES {F_IDLE,F_WAIT,F_DETECT,F_PREFALL,F_LED,F_MD,F_FLASH,F_CLEAR};
    
    enum KNOB_STATES {KNOB_CLICK_0,KNOB_CLICK_1};
    
    //----------
    // Globals
    
    const unsigned long UPDATEMS = 250;	// update displays only this many ms apart
    
    volatile char KnobCounter = 0;
    volatile byte KnobState;
    
    byte Button, PrevButton;
    
    byte Falling = F_IDLE;				// cold start the detection state machine
    
    unsigned long FallStart;			// when we we detected the falling object
    
    unsigned int DetectLevel = 200;		// ADC reading for object detection
    
    unsigned int DelayLED = 1;			// ms from trigger detect to LED preflash
    
    unsigned int DelayFlash = 180;		//  ... to Xenon flash
    
    unsigned int DelayClear = 6000;		//  ... after impact to allow camera restart
    
    const byte PulseLED = 50;			// ms LED on to pass motion detection threshold
    const byte PulseFlash = 20;			// ms Xenon flash relay on
    
    const unsigned int RelayAdvance = 3;	// ms relay activation to Xenon flash
    
    unsigned long MillisNow;
    unsigned long MillisThen;
    
    //-- Helper routine for printf()
    
    int s_putc(char c, FILE *t) {
      Serial.write(c);
    }
    
    //-- Knob interrupt handler
    
    void KnobHandler(void)
    {
    	byte Inputs;
    	Inputs = digitalRead(PIN_KNOB_B) << 1 | digitalRead(PIN_KNOB_A);  // align raw inputs
    //	Inputs ^= 0x02;                             // fix direction
    
    	switch (KnobState << 2 | Inputs) {
    	case 0x00 : 				// 0 00 - glitch
            break;
    	case 0x01 : 				 // 0 01 - UP to 1
            KnobCounter++;
    		KnobState = KNOB_CLICK_1;
    		break;
    	case 0x03 : 				 // 0 11 - DOWN to 1
            KnobCounter--;
    		KnobState = KNOB_CLICK_1;
    		break;
    	case 0x02 : 				 // 0 10 - glitch
            break;
    	case 0x04 : 				 // 1 00 - DOWN to 0
            KnobCounter--;
    		KnobState = KNOB_CLICK_0;
    		break;
    	case 0x05 : 				 // 1 01 - glitch
            break;
    	case 0x07 : 				 // 1 11 - glitch
            break;
    	case 0x06 : 				 // 1 10 - UP to 0
            KnobCounter++;
    		KnobState = KNOB_CLICK_0;
    		break;
    	default :  					// something is broken!
            KnobCounter = 0;
    		KnobState = KNOB_CLICK_0;
    	}
    }
    
    //------------------
    // Set things up
    
    void setup() {
    
    	pinMode(PIN_SYNC,OUTPUT);
    	digitalWrite(PIN_SYNC,LOW);			// show we arrived
    
    	pinMode(PIN_KNOB_B,INPUT_PULLUP);
    	pinMode(PIN_KNOB_A,INPUT_PULLUP);
    	pinMode(PIN_KNOB_SWITCH,INPUT_PULLUP);
    
        pinMode(PIN_LASER,OUTPUT);
        digitalWrite(PIN_LASER,HIGH);
    
        pinMode(PIN_LED,OUTPUT);
        digitalWrite(PIN_LED,HIGH);
    
        pinMode(PIN_FLASH,OUTPUT);
        digitalWrite(PIN_FLASH,HIGH);
    
    	KnobState = digitalRead(PIN_KNOB_A);
    	Button = PrevButton = !digitalRead(PIN_KNOB_SWITCH);
    
    	attachInterrupt((PIN_KNOB_A - 2),KnobHandler,CHANGE);
    
    	Falling = F_IDLE;
    
    	Serial.begin(9600);
    	fdevopen(&s_putc,0);				// set up serial output for printf()
    
    	printf("Xenon Flash Trigger\r\nEd Nisley - KE4ZNU - March 2014\r\n");
    
    	MillisThen = millis();
    
    }
    
    //------------------
    // Go flash!
    
    void loop() {
    
    	MillisNow = millis();
    
    	if (KnobCounter) {
    		Button = !digitalRead(PIN_KNOB_SWITCH);
    		if (Button)
    			DelayLED += KnobCounter;
    		else
    			DelayFlash += KnobCounter;
    
    		DelayLED = min(DelayLED,DelayFlash - PulseLED);
    		printf("Knob: %d, LED: %d, Flash: %d\n",KnobCounter,DelayLED,DelayFlash);
    		KnobCounter = 0;
    	}
    
    	digitalWrite(PIN_SYNC,HIGH);
    
    	switch (Falling) {
    	case F_IDLE :								// turn on laser for object detection
    		digitalWrite(PIN_LASER,LOW);
    		printf("Laser on, stabilizing... ");
    		while (analogRead(PIN_PHOTOCURRENT) <= DetectLevel) {
    			printf("*");
    		}
    		printf("\nReady!\n");
    		Falling = F_WAIT;
    		break;
    	case F_WAIT :								// record starting time of beam break
    		if (analogRead(PIN_PHOTOCURRENT) < DetectLevel) {
    			FallStart = millis();
    			Falling = F_DETECT;
    		}
    		break;
    	case F_DETECT :								// turn off laser to signal detection
    		digitalWrite(PIN_LASER,HIGH);
    		Falling = F_PREFALL;
    		break;
    	case F_PREFALL :							// turn on LED to trigger camera motion detection
    		if ((millis() - FallStart) >= DelayLED) {
    			digitalWrite(PIN_LED,LOW);
    			Falling = F_LED;
    		}
    		break;
    	case F_LED : 								// turn off LED
    		if ((millis() - FallStart) >= (DelayLED + PulseLED)) {
    			digitalWrite(PIN_LED,HIGH);
    			Falling = F_MD;
    		}
    		break;
    	case F_MD :									// fire the strobe to take picture
    		if ((millis() - FallStart) >= (DelayFlash - RelayAdvance)) {
    			digitalWrite(PIN_FLASH,LOW);
    			Falling = F_FLASH;
    		}
    		break;
    	case F_FLASH :								// turn off strobe relay
    		if ((millis() - FallStart) >= (DelayFlash - RelayAdvance + PulseFlash)) {
    			digitalWrite(PIN_FLASH,HIGH);
        		printf("Flash with LED delay: %d, Xenon delay: %d ...",DelayLED,DelayFlash);
    			Falling = F_CLEAR;
    		}
    		break;
    	case F_CLEAR :								// wait for camera to recycle
    		if ((millis() - FallStart) >= DelayClear) {
    			printf("done\n");
    			Falling = F_IDLE;
    		}
    		break;
    	default :
    		printf("** Bad Falling state: %02X",Falling);
    		Falling = F_IDLE;
    	}
    
    	digitalWrite(PIN_SYNC,LOW);
    
    	if ((MillisNow - MillisThen) > UPDATEMS) {
    //		printf("State: %02X\n",Falling);
    		MillisThen = MillisNow;
    	}
    
    }
    
    
  • Laser-photodiode Beam-Break Sensor Fixture

    The game plan: drop a small object through a laser beam that shines on a photodiode, thus causing an electrical signal that triggers various flashes and cameras and so forth and so on. This fixture holds the laser and photodiode in the proper orientation, with enough stability that you (well, I) can worry about other things:

    Laser-photodiode fixture - on blade
    Laser-photodiode fixture – on blade

    It’s mounted on the blade of a dirt-cheap 2 foot machinist’s square clamped to the bench which will probably get a few holes drilled in its baseplate for more permanent mounting.

    The solid model looks about like you’d expect:

    Laser-photodiode fixture - solid model
    Laser-photodiode fixture – solid model

    There’s a small hole in the back for an 8-32 setscrew that locks it to the blade; the fit turned out snug enough to render the screw superfluous. I added those two square blocks with the holes after I taped the wires to the one in the picture.

    The two semicircular (well, half-octagonal) trenches have slightly different diameters to suit the heatshrink tubing around the photodiode (a.k.a., IR LED) and brass laser housing. A dab of fabric adhesive holds the tubes in place, in addition to the Gorilla Tape on the ends.

    The laser came focused at infinity, of course. Unscrewing the lens almost all the way put the focus about 3/4 of the way across the ring; call it 40 mm. The beam is rectangular, about 1×2 mm, at the center of the ring, and I rotated the body to make the short axis vertical; that’s good enough for my purposes.

    The cable came from a pair of cheap earbuds with separate Left/Right pairs all the way from the plug.

    The model builds in one piece, of course, and pops off the platform ready to use:

    Laser-photodiode fixture - on platform
    Laser-photodiode fixture – on platform

    If you were doing this for an analytic project, you’d want a marker for the beam centerline on the vertical scale, but that’s in the nature of fine tuning. As it stands, the beam sits 8 mm above the base and flush with the top surface of the ring; if that were 10 mm, it’d be easier to remember.

    The OpenSCAD source code has a few tweaks and improvements:

    // Laser and LED-photodiode break-beam sensor
    // Ed Nisley - KE4ZNU - March 2014
    
    Layout = "Show";			// Build Show Ring Mount Guide
    
    //- Extrusion parameters must match reality!
    //  Print with 2 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
    
    inch = 25.4;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    //----------------------
    // Dimensions
    
    LaserOD = 6.0;				// brass focus tube
    LaserLength = 20.0;			//  ... wire clearance
    
    SensorOD = 6.5;				// including light shield
    SensorLength = 20.0;		//  ... wire clearance
    
    RingSize = [50.0,70.0,8.0,8*4];	// support ring dimensions
    RING_ID = 0;
    RING_OD = 1;
    RING_THICK = 2;
    RING_SIDES = 3;
    
    StrutWidth = 2.5;					// strut supporting this thing
    StrutLength = 26.5;
    
    StrutBlock = [10.0,35.0,20.0];		// block around the clearance slot
    BLOCK_WIDTH = 0;
    BLOCK_LENGTH = 1;
    BLOCK_HEIGHT = 2;
    
    StrutScrewTap = 2.7;				// 6-32 SHCS
    
    GuideID = 4.0;						// guide for cables
    GuideOD = 3*GuideID;
    
    BuildSpace = 3.0;					// spacing between objects on platform
    
    //----------------------
    // 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);
    
    }
    
    module Ring() {
    
    	difference() {
    		union() {
    			rotate(180/RingSize[RING_SIDES])
    				cylinder(d=RingSize[RING_OD],h=RingSize[RING_THICK],
    						$fn=RingSize[RING_SIDES]);
    			translate([-LaserOD,(-LaserLength - RingSize[RING_ID]/2),0])
    				cube([2*LaserOD,LaserLength,RingSize[RING_THICK]],center=false);
    			translate([-SensorOD,(-0*SensorLength + RingSize[RING_ID]/2),0])
    				cube([2*SensorOD,SensorLength,RingSize[RING_THICK]],center=false);
    		}
    		rotate(180/RingSize[RING_SIDES])
    			translate([0,0,-Protrusion])
    				cylinder(d=RingSize[RING_ID],h=(RingSize[RING_THICK] + 2*Protrusion),
    						$fn=RingSize[RING_SIDES]);
    		translate([0,0,RingSize[RING_THICK]])
    			rotate([90,0,0]) rotate(180/8)
    				PolyCyl(LaserOD,3*LaserLength,8);
    		translate([0,0,RingSize[RING_THICK]])
    			rotate([-90,0,0]) rotate(180/8)
    				PolyCyl(SensorOD,3*SensorLength,8);
    	}
    }
    
    module Mount() {
    	translate([0,0,StrutBlock[2]/2])
    		difference() {
    			cube(StrutBlock,center=true);
    			cube([StrutWidth,StrutLength,2*StrutBlock[2]],center=true);
    			translate([0,-StrutLength/3,0])
    				rotate([90,0,0])
    					PolyCyl(StrutScrewTap,StrutLength/2,6);
    		}
    }
    
    module Guide() {
    
    	difference() {
    		translate([0,0,RingSize[RING_THICK]/2])
    			cube([GuideOD,GuideOD,RingSize[RING_THICK]],center=true);
    		translate([0,0,-Protrusion]) rotate(180/8)
    			PolyCyl(GuideID,(RingSize[RING_THICK] + 2*Protrusion),8);
    	}
    }
    
    module Assembly() {
    	Ring();
    	translate([(RingSize[RING_OD]/2 + StrutBlock[BLOCK_LENGTH]/2
    				- (StrutBlock[BLOCK_LENGTH] - StrutLength)/2) + Protrusion,0,0])
    		rotate(90)
    			Mount();
    	for (i=[-1,1])
    		translate([(RingSize[RING_OD]/2 + GuideID/2),
    				  i*(StrutBlock[BLOCK_WIDTH]/2 + GuideID),
    				  0])
    			Guide();
    }
    
    //- Build it
    
    ShowPegGrid();
    
    if (Layout == "Ring") {
    	Ring();
    }
    
    if (Layout == "Mount") {
    	Mount();
    }
    
    if (Layout == "Guide") {
    	Guide();
    }
    
    if (Layout == "Show") {
    	Assembly();
    }
    
    if (Layout == "Build") {
    
    	translate([-5/2,-5/2,0])
    		cube(5);
    }