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

  • Makergear M2: CNC Platform Corner Clips

    The CNC version of the corner clips looks much better than the prototypes:

    M2 glass retaining clip
    M2 glass retaining clip

    Tightening the screws until the clip just flattens puts enough force on the glass + heat spreader stack to hold it firmly against the balls in the bottom pad. The solid rubber L-shaped bumpers and screws hold the glass in position against XY forces… and the whole affair looks much better than the original (and perfectly serviceable) bulldog clips. These clips free up the entire surface of the glass plate, minus four 12 mm triangles that you could, if you were desperate, print right over.

    Although it’d be easier to just hack out an angular clip, I wrote a bit of G-Code to put a nice radius on each corner. The clip sits atop the rubber bumper with a 0.5 mm margin to keep the metal edges away from fingers; they’re smooth, but it’s still a strip of 6 mil (= 0.15 mm) phosphor bronze and feels a lot like a knife edge if you press hard enough.

    The radius on the three outside corners is a special-case solution of the general circle-through-three-points problem, taking advantage of the symmetry and right-triangle-ness of the corners. This sketch shows the details:

    M2 Platform Clip Doodles 4 - corner fairing with margin
    M2 Platform Clip Doodles 4 – corner fairing with margin

    The two corners on the bevel over the glass plate have a fixed radius. I reworked my original fairing arc solution for outside cutting and doodled it up for this situation:

    M2 Platform Clip Doodles 5 - bevel full solution
    M2 Platform Clip Doodles 5 – bevel full solution

    The outside corner radius worked out to 5 mm and I set the bevel radius at 3 mm. I think the latter made those corners a bit too sharp, but it’s Good Enough for my simple needs.

    Drilling and machining the clips required a fixture:

    M2 platform clips - milling edges
    M2 platform clips – milling edges

    That’s a story for another day.

    I used cutter diameter compensation to mill the edges, starting oversize by 1.5 mm and working downward by 0.5 mm on each pass to the actual diameter. That gradually trimmed off the edges without any excitement, so I could start with rough-trimmed stock and not worry about precision hand trimming.

    I thought climb milling (CW around the part) would produce better results, but it tended to smear the phosphor bronze against the fixture:

    M2 Corner Clips - Climb milling tool paths
    M2 Corner Clips – Climb milling tool paths

    Conventional milling (CCW around the part) actually worked, but it required fancier entry and exit moves:

    M2 Corner Clips - Conventional milling tool paths
    M2 Corner Clips – Conventional milling tool paths

    This part is the kind and size of machining perfectly suited to a Sherline CNC mill…

    The LinuxCNC G-Code source:

    ( M2 Build Platform Corner Clips )
    ( Ed Nisley - KE4ZNU - July 2013 )
    ( Fixture origin at right-front corner pip )
    
    ( Flow Control )
    #<_Do_Drill> = 0		( Drill two holes in clip )
    #<_Do_Mill> = 1			( Mill clip outline )
    #<_Climb_Mill> = 0		( 0 = conventional 1 = climb)
    
    ( Fixture info )
    #<_Drill_X_Fixture> = 5.0	( Drill station origin )
    #<_Drill_Y_Fixture> = 5.0
    
    #<_Drill_Num> = 30			( Drill number in tool table)
    #<_Drill_Retract> = 15
    #<_Drill_Depth> = -1.0
    #<_Drill_Feed> = 300
    #<_Drill_Speed> = 3000
    
    #<_Mill_X_Fixture> = 40.0	( Mill station origin )
    #<_Mill_Y_Fixture> = 5.0
    
    #<_Mill_Num> = 3			( Mill number in tool table)
    #<_Mill_Dia> = 4.60			( actual tool diameter)
    #<_Mill_Dia_Incr> = 0.50
    #<_Mill_Dia_Steps> = 3
    #<_Mill_Retract> = 15
    #<_Mill_Depth> = -0.5
    #<_Mill_Feed> = 300
    #<_Mill_Speed> = 8000
    
    (----------------)
    
    (  Initialize first tool length at probe switch )
    (     Assumes G59.3 is still in machine units, returns in G54 )
    (  ** Must set these constants to match G20 / G21 condition! )
    
    #<_Probe_Speed>     = 400            ( set for something sensible in mm or inch )
    #<_Probe_Retract>   =   1            ( ditto )
    
    O<Probe_Tool> SUB
    
    G49                     ( clear tool length compensation )
    G30                     ( move above probe switch )
    G59.3                   ( coord system 9 )
    
    G38.2 Z0 F#<_Probe_Speed>           ( trip switch on the way down )
    G0 Z[#5063 + #<_Probe_Retract>]     ( back off the switch )
    G38.2 Z0 F[#<_Probe_Speed> / 10]    ( trip switch slowly )
    
    #<_ToolZ> = #5063                    ( save new tool length )
    
    G43.1 Z[#<_ToolZ> - #<_ToolRefZ>]    ( set new length )
    
    G54                     ( coord system 0 )
    G30                     ( return to safe level )
    
    O<Probe_Tool> ENDSUB
    
    (-------------------)
    (-- Initialize first tool length at probe switch )
    
    O<Probe_Init> SUB
    
    #<_ToolRefZ> = 0.0      ( set up for first call )
    
    O<Probe_Tool> CALL
    
    #<_ToolRefZ> = #5063    ( save trip point )
    
    G43.1 Z0                ( tool entered at Z=0, so set it there )
    
    O<Probe_Init> ENDSUB
    
    (-------------------)
    (-- Mill one pass around outline with tool diameter passed in #1 )
    
    O<MillOutline> SUB
    
    #<X_Size> = 22.0		( size of support spider pad = nominal clip size )
    #<Y_Size> = 22.0
    #<Base_Bevel> = 3.2		( X or Y length of corners clipped from spider pad )
    
    #<Bevel_Size> = 9.0		( remaining part of trimmed edges on clip )
    #<Bevel_Radius> = 3.0	( fairing radius at bevel corners on clip)
    
    #<R_Div_Root2> = [#<Bevel_Radius> / SQRT[2]]
    #<R_1M_Recip_R2> = [#<Bevel_Radius> * [1 - 1/SQRT[2]]]
    #<R_Root2_M1> = [#<Bevel_Radius> * [SQRT[2] - 1]]
    
    #<Margin> = 0.5			( recess inside of nominal )
    
    #<X_Min> = [#<Margin>]
    #<X_Max> = [#<X_Size> - #<Margin>]
    
    #<Y_Min> = [#<Margin>]
    #<Y_Max> = [#<Y_Size> - #<Margin>]
    
    #<Corner_Rad> = [[#<Margin> * [1 - SQRT[2]] + [#<Base_Bevel> / SQRT[2]]] / [SQRT[2] - 1]]
    
    O<Climb> IF [#<_Climb_Mill>]
    
    G0 X#<X_Min> Y[#<Y_Max> + 3*#<_Mill_Dia>]
    G1 Z#<_Mill_Depth> F#<_Mill_Feed>
    
    G41.1 D#1
    
    G3 X[#<X_Min>] Y#<Y_Max> I0 J[0-1.5*#<_Mill_Dia>]	( cutter comp on: entry move)
    
    G1 X[#<Bevel_Size> - #<R_Root2_M1>]
    G2 X[#<Bevel_Size> + #<R_1M_Recip_R2>] Y[#<Y_Max> - #<R_1M_Recip_R2>] J[0-#<Bevel_Radius>]
    
    G1 X[#<X_Max> - #<R_1M_Recip_R2>] Y[#<Bevel_Size> + #<R_1M_Recip_R2>]
    G2 X#<X_Max> Y[#<Bevel_Size> - #<R_Root2_M1>] I[0-#<R_Div_Root2>] J[0-#<R_Div_Root2>]
    
    G1 Y[#<Y_Min> + #<Corner_Rad>]
    G2 X[#<X_Max> - #<Corner_Rad>] Y#<Y_Min> I[0-#<Corner_Rad>] J0
    
    G1 X[#<X_Min> + #<Corner_Rad>]
    G2 X#<X_Min> Y[#<Y_Min> + #<Corner_Rad>] I0 J#<Corner_Rad>
    
    G1 Y[#<Y_Max> - #<Corner_Rad>]
    G2 X[#<X_Min> + #<Corner_Rad>] Y#<Y_Max> I#<Corner_Rad> J0
    
    G40
    
    G0 X#<X_Min> Y[#<Y_Max> + 3*#<_Mill_Dia>]
    (G3 X#<Bevel_Size> Y[#<Y_Max> + 3*#<_Mill_Dia>] I0 J[1.5*#<_Mill_Dia>])	( cutter comp off: safe exit)
    
    G0 X#<X_Min>			( return to start)
    
    O<Climb> ELSE
    
    G0 X#<X_Size> Y[#<Y_Size> + #1/2]
    
    G1 Z#<_Mill_Depth> F#<_Mill_Feed>
    
    G42.1 D#1
    
    G1 X#<Bevel_Size> Y[#<Y_Max>]	( cutter comp on: entry move)
    
    G1 X[#<X_Min> + #<Corner_Rad>]
    G3 X#<X_Min> Y[#<Y_Max> - #<Corner_Rad>] I0 J[0-#<Corner_Rad>]
    
    G1 Y[#<Y_Min> + #<Corner_Rad>]
    G3 X[#<X_Min> + #<Corner_Rad>] Y[#<Y_Min>] I#<Corner_Rad> J0
    
    G1 X[#<X_Max> - #<Corner_Rad>]
    G3 X[#<X_Max>] Y[#<Y_Min> + #<Corner_Rad>] I0 J#<Corner_Rad>
    
    G1 Y[#<Bevel_Size> - #<R_Root2_M1>]
    G3 X[#<X_Max> - #<R_1M_Recip_R2>] Y[#<Bevel_Size> + #<R_1M_Recip_R2>] I[-#<Bevel_Radius>]
    
    G1 X[#<Bevel_Size> + #<R_1M_Recip_R2>] Y[#<Y_Max> - #<R_1M_Recip_R2>]
    G3 X[#<Bevel_Size> - #<R_Root2_M1>] Y#<Y_Max> I[-#<R_Div_Root2>] J[-#<R_Div_Root2>]
    
    G2 Y[#<Y_Max> + 3*#<_Mill_Dia>] J[#<_Mill_Dia>*1.5]		( get away from corner)
    G40
    
    G0 X#<X_Size>					( cutter comp off: safe exit)
    G0 Y[#<Y_Size> + #1/2]			( return to start)
    
    O<Climb> ENDIF
    
    O<MillOutline> ENDSUB
    
    (----------------)
    ( Start machining... )
    
    G17 G40 G49 G54 G80 G90 G94 G99	( reset many things )
    
    G21								( metric! )
    G91.1 							( incremental arc centers)
    
    (msg,Verify: G30.1 position in G54 above tool change switch? )
    M0
    (msg,Verify: fixture origin XY touched off? )
    M0
    (msg,Verify: Current tool Z=0 touched off? )
    M0
    
    ( Set up probing)
    O<Probe_Init> CALL
    T0 M6
    
    (---- Drill holes)
    
    O<DoDrill> IF [#<_Do_Drill>]
    
    (debug,Insert drill tool = #<_Drill_Num>)
    T#<_Drill_Num> M6
    O<Probe_Tool> CALL
    (debug,Set spindle to #<_Drill_Speed> rpm )
    M0
    
    G0 X#<_Drill_X_Fixture> Y#<_Drill_Y_Fixture>
    G0 Z#<_Drill_Retract>
    
    G10 L20 P2 X0 Y0 Z#<_Drill_Retract>	( P2 = G55)
    G55					( drill station coordinates )
    
    G81 X5.0 Y15.0 Z#<_Drill_Depth> R#<_Drill_Retract> F#<_Drill_Feed>
    
    G81 X15.0 Y5.0
    
    G54
    
    O<DoDrill> ENDIF
    
    (---- Mill outline )
    ( Start with large diameter and end with actual diameter to trim in stages)
    
    O<DoMill> IF [#<_Do_Mill>]
    
    (debug,Insert mill tool = #<_Mill_Num>)
    T#<_Mill_Num> M6
    O<Probe_Tool> CALL
    (debug,Set spindle to #<_Mill_Speed> rpm )
    M0
    
    G0 X#<_Mill_X_Fixture> Y#<_Mill_Y_Fixture>
    G0 Z#<_Mill_Retract>
    
    G10 L20 P2 X0 Y0 Z#<_Mill_Retract>	( P2 = G55)
    G55					( mill station coordinates )
    
    #<PassCount> = 0
    
    O<MillLoop> DO
    #<Diameter> = [#<_Mill_Dia> + [#<_Mill_Dia_Steps> - #<PassCount>]*#<_Mill_Dia_Incr>]
    
    O<MillOutline> CALL [#<Diameter>]
    
    #<PassCount> = [#<PassCount> + 1]
    O<MillLoop> WHILE [#<PassCount> LE #<_Mill_Dia_Steps>]
    
    ( Finishing pass with zero cut )
    O<MillOutline> CALL [#<Diameter>]
    
    G0 Z#<_Mill_Retract>
    G54
    
    O<DoMill> ENDIF
    
    G30
    
    (msg,Done!)
    M2
    

    The rest of the doodles, which don’t match up with the final G-Code because they represent the earliest versions of the layout:

    M2 Platform Clip Doodles 1 - overall layout
    M2 Platform Clip Doodles 1 – overall layout
    M2 Platform Clip Doodles 2 - bevel
    M2 Platform Clip Doodles 2 – bevel
    M2 Platform Clip Doodles 3 - corner fairing without margin
    M2 Platform Clip Doodles 3 – corner fairing without margin
  • Optical Filament Diameter Sensor Doodles

    It should be possible to sense the filament diameter with a cheap webcam and some optics:

    Filament Diameter Sensor - Optical Path Layout
    Filament Diameter Sensor – Optical Path Layout

    The general idea:

    Given that LinuxCNC runs on a bone-stock PC, you can plug in a stock USB webcam and capture pictures (I have done this already). Because LinuxCNC isolates the motion control in a hard real time process, you can run heavy metal image manipulation code in userland (think ImageMagick) without affecting the motors.

    So you can put a macro lens in front of a webcam (like that macro lens holder) and mount it just above the extruder with suitable lighting to give a high-contrast view of the filament. Set it so the filament diameter maps to about 1/4 of the width of the image, for reasons explained below.

    For a crappy camera with 640×480 resolution, this gives you 160 pixel / 1.75 mm filament = 91 pixel/mm → about 0.01 mm resolution = 0.6%. Use a better camera, get better resolution: 1280 pixel = 0.3% resolution.

    That gives you roughly 1% or 0.5% resolution in area. This is pretty close to the holy grail for DIY filament diameter measurement.

    Add two first-surface mirrors / prisms aligned at right angles, so that the camera sees three views of the filament: straight on, plus two views at right angles, adjacent to the main view. Set the optics so they’re all about 1/4 of the image width, to produce an image with three parts filament and one part high-contrast background separating them. This is the ideal, reality will be messier.

    Figure 1 shows an obvious arrangement, the mirrors in Figure 2 give more equal distances.

    You could align the mirrors to provide three views at mutual 120° angles, which would equalize the distances and give you three identical angles for roundness computation, should that matter.

    Diameter measurement process:

    • Extract one (*) scan line across the image.
    • Convert to binary pixels: 1 = filament, 0 = background, perhaps with ImageMagick auto thresholding.
    • Add pixel values across the line, divide by 3, multiply by mm/pixel → average filament diameter.
    • Done!

    Adding binary pixels is easy: it’s just the histogram, which ImageMagick does in one step. Dump data to a file / pipe, process it with Python. It all feeds into a LinuxCNC HAL component, which may constrain the language to C / Python / something else.

    (*) You can get vertical averaging over a known filament length, essentially for free. Extract three (or more) scan lines, process as above, divide by 3 (or more), and you get a nicely averaged average.

    Win: the image is insensitive to position / motion / vibration within reasonable limits, because you’re doing the counting on pixel values, not filament position. The camera can mount near, but not on, the extruder, so you can measure the filament just above the drive motor without cooking the optics or vibrating the camera to death.

    Win: it’s non-contacting, so there’s not much to get dirty

    Win: you get multiple simultaneous diameter measurements around one slice of the filament

    You could mount the camera + optics at one end of the printer’s axis (on the M2, the X axis). Drive the extruder to a known X position, take a picture of the straight-on view, drive to another position, take a picture of the mirrored views, and you have two pictures in perfect focus. Combine & process as above.

    You can do that every now and again, because any reasonable filament won’t vary that much over a few tens of millimeters. Maybe you do it once per layer, as part of the Z step process?

    You could generalize this to a filament QC instrument that isn’t on the printer itself: stream the filament from spool to spool while measuring it every 10 mm, report the statistics. That measurement could run without stopping, because you don’t reposition the filament between measurements: it’s all fixed-focus against a known background. You could have decent roller guides for the filament to ensure it’s in a known position.

    Heck, that instrument could produce a huge calibration file that gives diameter / roundness vs. position along the entire length of the filament. Use it to accept/reject incoming plastic supplies or, even better, feed the data into the printer along with the spool to calibrate the extrusion on the fly without fancy optics or measurements.

    Dan wonders if this might be patented. I’m sure it is: I’m nowhere near as bright as the average engineering bear at a company that’s been spending Real Money for three decades. My working assumption: all the knowledge is out there, behind a barrier I can’t see through or reach around: there’s no point in looking for it beyond a casual Google search on the obvious terms that, so far, hasn’t produced anything similar.

    Memo to Self: Might even be marketable, right up until they crush me like a bug…

  • Mesa 7i76 vs. Stepper Motor: First Motion

    The cables with their tidy terminations make it a little neater, but all this stuff really needs a permanent home:

    Stepper motor - first motion
    Stepper motor – first motion

    I used the LinuxCNC PNCConf utility to define a minimal system with little more than the X axis parameters filled in:

    PNCConf - X Axis
    PNCConf – X Axis

    Then I could jog the stepper motor using the Axis UI:

    7i76 - First Motion
    7i76 – First Motion

    And it worked!

    Actually, it didn’t. The first motion instantly tripped a Following Error, so I bumped those values up a bit. Then I fiddled with accelerations and speeds and suchlike. Then I adjusted the Axis defaults to not be so nose-pickin’ slow. And then it Just Worked.

    Not much to show, but at least I know the whole LinuxCNC to 5i25 to 7i76 to M542 to motor chain functions pretty much as it should, which is worth knowing. From here on out, it’s a matter of fine tuning…

  • Broom Handle Screw Thread: Now With Dedendum

    Although I don’t need another threaded plug, the most recent OpenSCAD version can handle a model including the thread dedendum:

    Broom Handle Screw - full thread - solid model
    Broom Handle Screw – full thread – solid model

    This hyper-close view (as always, clicky for more dots) shows the problem: the region where the addendum and dedendum meet at the pitch cylinder consists of a bazillion tiny faces:

    Broom Handle Screw - full thread - detail
    Broom Handle Screw – full thread – detail

    The previous version simply couldn’t handle that many elements, but the new version has a parameter that I tweaked (to 100,000), allowing it to complete the rendering. Compiling to a solid model requires about 45 minutes, most of which probably involves those unprintably small facets.

    The thread elements now taper slightly in the downhill direction, so that each quasi-cylinder nests cleanly inside the next to avoid the tiny slivers that stuck out of the joints in the previous model.

    And the new Slic3r version (from GitHub) has better internal support for those indentations around the base, which means that AC vent plug might be build-able, too.

    The OpenSCAD source code, with a few tweaks to nest the thread cylinders and properly locate the dedendum:

    // Broom Handle Screw End Plug
    // Ed Nisley KE4ZNU June 2013
    
    //- Extrusion parameters must match reality!
    //  Print with 2 shells and 3 solid layers
    
    HoleWindage = 0.2;
    
    Protrusion = 0.1;			// make holes end cleanly
    
    //----------------------
    // Dimensions
    
    PostOD = 22.3;				// post inside metal handle
    PostLength = 25.0;
    
    FlangeOD = 24.0;			// stop flange
    FlangeLength = 3.0;
    
    PitchDia = 15.5;			// thread center diameter
    ScrewLength = 20.0;
    
    ThreadFormOD = 2.5;			// diameter of thread form
    ThreadPitch = 5.0;
    
    BoltOD = 7.0;				// clears 1/4-20 bolt
    BoltSquare = 6.5;			// across flats
    BoltHeadThick = 3.0;
    
    RecessDia = 6.0;			// recesss to secure post in handle
    
    OALength = PostLength + FlangeLength + ScrewLength;
    
    $fn=8*4;					// default cylinder sides
    
    echo("Pitch dia: ",PitchDia);
    echo("Root dia: ",PitchDia - ThreadFormOD);
    echo("Crest dia: ",PitchDia + ThreadFormOD);
    
    Pi = 3.14159265358979;
    
    //----------------------
    // Useful routines
    
    // Wrap cylindrical thread segments around larger plug cylinder
    
    module CylinderThread(Pitch,Length,PitchDia,ThreadOD,PerTurn=32) {
    
    CylFudge = 1.02;				// force overlap
    
        RotIncr = 1/PerTurn;
        PitchRad = PitchDia/2;
    
        Turns = Length/Pitch;
        NumCyls = Turns*PerTurn;
    
        ZStep = Pitch / PerTurn;
    
        HelixAngle = atan(Pitch/(Pi*PitchDia));
        CylLength = CylFudge * (Pi*(PitchDia + ThreadOD) / PerTurn) / cos(HelixAngle);
    
    	for (i = [0:NumCyls-1]) {
    		assign(Angle = 360*i/PerTurn)
    			translate([PitchRad*cos(Angle),PitchRad*sin(Angle),i*ZStep])
    				rotate([90+HelixAngle,0,Angle])
    					cylinder(r1=ThreadOD/2,
    							r2=ThreadOD/(2*CylFudge),
    							h=CylLength,
    							center=true,$fn=12);
    	}
    }
    
    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) {
    
      Range = floor(50 / Space);
    
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //-------------------
    // Build it...
    
    ShowPegGrid();
    
    difference() {
        union() {
            cylinder(r=PostOD/2,h=PostLength);
            cylinder(r=PitchDia/2,h=OALength);
            translate([0,0,PostLength])
                cylinder(r=FlangeOD/2,h=FlangeLength);
            color("Orange")
            translate([0,0,(PostLength + FlangeLength)])
                CylinderThread(ThreadPitch,(ScrewLength - ThreadFormOD/2),PitchDia,ThreadFormOD);
        }
    
        translate([0,0,-Protrusion])
            PolyCyl(BoltOD,(OALength + 2*Protrusion),6);
        translate([0,0,(OALength - BoltHeadThick)])
            PolyCyl(BoltSquare,(BoltHeadThick + Protrusion),4);
    
        translate([0,0,(PostLength + FlangeLength + ThreadFormOD/2)])
    		rotate(-90)
            CylinderThread(ThreadPitch,ScrewLength,PitchDia,ThreadFormOD);
    
    	for (i = [0:90:270]) {
    		rotate(i)
    			translate([PostOD/2,0,PostLength/2])
    				sphere(r=RecessDia/2,$fn=8);
    	}
    }
    
  • Mesa 5i25 + 7i76: HAL Pins

    Some notes on setting up the Mesa 5i25 FPGA card (the manual) with the 7i76 daughter card (the manual) inside a new-to-me off-lease Dell Optiplex 760

    First up: note that Mesa uses a capital I (“eye”) in the part numbers, a decision which they’ve surely had plenty of time to regret, as many common fonts exhibit nearly identical capital-I and digit-1 characters.

    The 7i76 connects to the 5i25 in the PC through a Mesa-supplied IEEE-1284 printer cable. I cobbled up a 24 VDC power supply (which I’ll eventually be using for the M2 motors) to provide “field power” and let the firmware identify the daughtercard:

    24 VDC power supply - Mesa 7i76 - stepper driver
    24 VDC power supply – Mesa 7i76 – stepper driver

    The default jumper positions on both cards work fine.

    The unconnected stepper driver brick and motor will serve as a simple demonstration after I’ve built the Eagle parts to represent the 5i25’s components. However, the first demo of any new hardware must be a blinking LED.

    To see whether the cards work and are detected, load the hostmot2 drivers in halrun and dump all the information:

    halrun
    halcmd: loadrt hostmot2
    halcmd: loadrt hm2_pci
    halcmd: show all
    Loaded HAL Components:
    ID      Type  Name                                      PID   State
         5  RT    hm2_pci                                         ready
         3  User  halcmd5010                                 5010 ready
         4  RT    hostmot2                                        ready
    
    Component Pins:
    Owner   Type  Dir         Value  Name
         5  bit   OUT         FALSE  hm2_5i25.0.7i76.0.0.input-00
         5  bit   OUT         FALSE  hm2_5i25.0.7i76.0.0.input-00-not
         5  bit   OUT         FALSE  hm2_5i25.0.7i76.0.0.input-01
    ... snippage ...
         5  bit   OUT         FALSE  hm2_5i25.0.7i76.0.0.input-30
         5  bit   OUT         FALSE  hm2_5i25.0.7i76.0.0.input-30-not
         5  bit   OUT         FALSE  hm2_5i25.0.7i76.0.0.input-31
         5  bit   OUT         FALSE  hm2_5i25.0.7i76.0.0.input-31-not
         5  bit   IN          FALSE  hm2_5i25.0.7i76.0.0.output-00
         5  bit   IN          FALSE  hm2_5i25.0.7i76.0.0.output-01
    ... snippage ...
         5  bit   IN          FALSE  hm2_5i25.0.7i76.0.0.output-15
         5  bit   IN          FALSE  hm2_5i25.0.7i76.0.0.spindir
         5  bit   IN          FALSE  hm2_5i25.0.7i76.0.0.spinena
         5  float IN              0  hm2_5i25.0.7i76.0.0.spinout
         5  s32   OUT             0  hm2_5i25.0.encoder.00.count
         5  s32   OUT             0  hm2_5i25.0.encoder.00.count-latched
         5  bit   I/O         FALSE  hm2_5i25.0.encoder.00.index-enable
         5  bit   IN          FALSE  hm2_5i25.0.encoder.00.latch-enable
         5  bit   IN          FALSE  hm2_5i25.0.encoder.00.latch-polarity
         5  float OUT             0  hm2_5i25.0.encoder.00.position
         5  float OUT             0  hm2_5i25.0.encoder.00.position-latched
         5  s32   OUT             0  hm2_5i25.0.encoder.00.rawcounts
         5  s32   OUT             0  hm2_5i25.0.encoder.00.rawlatch
         5  bit   IN          FALSE  hm2_5i25.0.encoder.00.reset
         5  float OUT             0  hm2_5i25.0.encoder.00.velocity
         5  s32   OUT             0  hm2_5i25.0.encoder.01.count
    ... snippage ...
         5  float OUT             0  hm2_5i25.0.encoder.01.velocity
         5  bit   OUT         FALSE  hm2_5i25.0.gpio.000.in
         5  bit   OUT          TRUE  hm2_5i25.0.gpio.000.in_not
         5  bit   OUT         FALSE  hm2_5i25.0.gpio.001.in
    ... snippage ...
         5  bit   OUT          TRUE  hm2_5i25.0.gpio.032.in
         5  bit   OUT         FALSE  hm2_5i25.0.gpio.032.in_not
         5  bit   OUT          TRUE  hm2_5i25.0.gpio.033.in
         5  bit   OUT         FALSE  hm2_5i25.0.gpio.033.in_not
         5  bit   IN          FALSE  hm2_5i25.0.led.CR01
         5  bit   IN          FALSE  hm2_5i25.0.led.CR02
         5  u32   IN     0x00000000  hm2_5i25.0.sserial.channel
         5  u32   IN     0x00000000  hm2_5i25.0.sserial.parameter
         5  u32   IN     0x00000000  hm2_5i25.0.sserial.port
         5  u32   OUT    0x00000000  hm2_5i25.0.sserial.port-0.fault-count
         5  u32   OUT    0x00000000  hm2_5i25.0.sserial.port-0.port_state
         5  bit   IN           TRUE  hm2_5i25.0.sserial.port-0.run
         5  bit   IN          FALSE  hm2_5i25.0.sserial.read
         5  u32   OUT    0x00000000  hm2_5i25.0.sserial.state
         5  u32   IN     0x00000000  hm2_5i25.0.sserial.value
         5  bit   IN          FALSE  hm2_5i25.0.sserial.write
         5  bit   IN          FALSE  hm2_5i25.0.stepgen.00.control-type
         5  s32   OUT             0  hm2_5i25.0.stepgen.00.counts
         5  float OUT             0  hm2_5i25.0.stepgen.00.dbg_err_at_match
         5  float OUT             0  hm2_5i25.0.stepgen.00.dbg_ff_vel
         5  float OUT             0  hm2_5i25.0.stepgen.00.dbg_pos_minus_prev_
         5  float OUT             0  hm2_5i25.0.stepgen.00.dbg_s_to_match
         5  s32   OUT             0  hm2_5i25.0.stepgen.00.dbg_step_rate
         5  float OUT             0  hm2_5i25.0.stepgen.00.dbg_vel_error
         5  bit   IN          FALSE  hm2_5i25.0.stepgen.00.enable
         5  float IN              0  hm2_5i25.0.stepgen.00.position-cmd
         5  float OUT             0  hm2_5i25.0.stepgen.00.position-fb
         5  float IN              0  hm2_5i25.0.stepgen.00.velocity-cmd
         5  float OUT             0  hm2_5i25.0.stepgen.00.velocity-fb
         5  bit   IN          FALSE  hm2_5i25.0.stepgen.01.control-type
    ... snippage ...
         5  float OUT             0  hm2_5i25.0.stepgen.09.velocity-fb
         5  bit   I/O         FALSE  hm2_5i25.0.watchdog.has_bit
    
    ... snippage ...
    
    Parameters:
    Owner   Type  Dir         Value  Name
         5  bit   RW          FALSE  hm2_5i25.0.7i76.0.0.output-00-invert
         5  bit   RW          FALSE  hm2_5i25.0.7i76.0.0.output-01-invert
    ... snippage ...
         5  bit   RW          FALSE  hm2_5i25.0.7i76.0.0.output-15-invert
         5  u32   RO     0x100000A5  hm2_5i25.0.7i76.0.0.serial-number
         5  bit   RW          FALSE  hm2_5i25.0.7i76.0.0.spindir-invert
         5  bit   RW          FALSE  hm2_5i25.0.7i76.0.0.spinena-invert
         5  float RW            100  hm2_5i25.0.7i76.0.0.spinout-maxlim
         5  float RW              0  hm2_5i25.0.7i76.0.0.spinout-minlim
         5  float RW            100  hm2_5i25.0.7i76.0.0.spinout-scalemax
         5  u32   RO     0x00000000  hm2_5i25.0.7i76.0.0.status
         5  bit   RW          FALSE  hm2_5i25.0.encoder.00.counter-mode
         5  bit   RW           TRUE  hm2_5i25.0.encoder.00.filter
         5  bit   RW          FALSE  hm2_5i25.0.encoder.00.index-invert
         5  bit   RW          FALSE  hm2_5i25.0.encoder.00.index-mask
         5  bit   RW          FALSE  hm2_5i25.0.encoder.00.index-mask-invert
         5  float RW              1  hm2_5i25.0.encoder.00.scale
         5  float RW            0.5  hm2_5i25.0.encoder.00.vel-timeout
         5  bit   RW          FALSE  hm2_5i25.0.encoder.01.counter-mode
    ... snippage ...
         5  float RW            0.5  hm2_5i25.0.encoder.01.vel-timeout
         5  bit   RW          FALSE  hm2_5i25.0.gpio.000.invert_output
         5  bit   RW          FALSE  hm2_5i25.0.gpio.000.is_opendrain
         5  bit   RW          FALSE  hm2_5i25.0.gpio.001.invert_output
    ... snippage ...
         5  bit   RW          FALSE  hm2_5i25.0.gpio.030.invert_output
         5  bit   RW          FALSE  hm2_5i25.0.gpio.030.is_opendrain
         5  bit   RW          FALSE  hm2_5i25.0.gpio.030.is_output
         5  bit   RW          FALSE  hm2_5i25.0.io_error
         5  s32   RO              0  hm2_5i25.0.pet_watchdog.time
         5  s32   RW              0  hm2_5i25.0.pet_watchdog.tmax
         5  s32   RO              0  hm2_5i25.0.read.time
         5  s32   RW              0  hm2_5i25.0.read.tmax
         5  s32   RO              0  hm2_5i25.0.read_gpio.time
         5  s32   RW              0  hm2_5i25.0.read_gpio.tmax
         5  u32   RW     0x00000001  hm2_5i25.0.sserial.port-0.fault-dec
         5  u32   RW     0x0000000A  hm2_5i25.0.sserial.port-0.fault-inc
         5  u32   RW     0x000000C8  hm2_5i25.0.sserial.port-0.fault-lim
         5  u32   RW     0x00077FE2  hm2_5i25.0.stepgen.00.dirhold
         5  u32   RW     0x00077FE2  hm2_5i25.0.stepgen.00.dirsetup
         5  float RW              1  hm2_5i25.0.stepgen.00.maxaccel
         5  float RW              0  hm2_5i25.0.stepgen.00.maxvel
         5  float RW              1  hm2_5i25.0.stepgen.00.position-scale
         5  u32   RW     0x00000000  hm2_5i25.0.stepgen.00.step_type
         5  u32   RW     0x00077FE2  hm2_5i25.0.stepgen.00.steplen
         5  u32   RW     0x00077FE2  hm2_5i25.0.stepgen.00.stepspace
         5  u32   RW     0x00077FE2  hm2_5i25.0.stepgen.01.dirhold
    ... snippage ...
         5  u32   RW     0x00077FE2  hm2_5i25.0.stepgen.09.stepspace
         5  u32   RW     0x004C4B40  hm2_5i25.0.watchdog.timeout_ns
         5  s32   RO              0  hm2_5i25.0.write.time
         5  s32   RW              0  hm2_5i25.0.write.tmax
         5  s32   RO              0  hm2_5i25.0.write_gpio.time
         5  s32   RW              0  hm2_5i25.0.write_gpio.tmax
    
    Parameter Aliases:
     Alias                                      Original Name
    
    Exported Functions:
    Owner   CodeAddr  Arg       FP   Users  Name
     00005  fc3d2582  f1b17000  NO       0   hm2_5i25.0.pet_watchdog
     00005  fc3c49dc  f1b17000  YES      0   hm2_5i25.0.read
     00005  fc3c4906  f1b17000  YES      0   hm2_5i25.0.read_gpio
     00005  fc3c4936  f1b17000  YES      0   hm2_5i25.0.write
     00005  fc3c48d6  f1b17000  YES      0   hm2_5i25.0.write_gpio
    
    ... snippage ...
    

    Extract the 5i25 pin assignments from the kernel log file:
    dmesg | grep hm2

    Which produces this:

    [ed@lcnc-m2 LinuxCNC for M2]$ dmesg | grep hm2
    [ 7299.887856] hm2: loading Mesa HostMot2 driver version 0.15
    [ 7407.514601] hm2_pci: loading Mesa AnyIO HostMot2 driver version 0.7
    [ 7407.514631] hm2_pci 0000:04:02.0: PCI INT A -> GSI 18 (level, low) -> IRQ 18
    [ 7407.514634] hm2_pci: discovered 5i25 at 0000:04:02.0
    [ 7407.514656] hm2: no firmware specified in config modparam!  the board had better have firmware configured already, or this won't work
    [ 7407.515018] hm2/hm2_5i25.0: Smart Serial Firmware Version 38
    [ 7407.632326] hm2/hm2_5i25.0: 34 I/O Pins used:
    [ 7407.632329] hm2/hm2_5i25.0:     IO Pin 000 (P3-01): StepGen #0, pin Direction (Output)
    [ 7407.632331] hm2/hm2_5i25.0:     IO Pin 001 (P3-14): StepGen #0, pin Step (Output)
    [ 7407.632334] hm2/hm2_5i25.0:     IO Pin 002 (P3-02): StepGen #1, pin Direction (Output)
    [ 7407.632336] hm2/hm2_5i25.0:     IO Pin 003 (P3-15): StepGen #1, pin Step (Output)
    [ 7407.632338] hm2/hm2_5i25.0:     IO Pin 004 (P3-03): StepGen #2, pin Direction (Output)
    [ 7407.632340] hm2/hm2_5i25.0:     IO Pin 005 (P3-16): StepGen #2, pin Step (Output)
    [ 7407.632343] hm2/hm2_5i25.0:     IO Pin 006 (P3-04): StepGen #3, pin Direction (Output)
    [ 7407.632345] hm2/hm2_5i25.0:     IO Pin 007 (P3-17): StepGen #3, pin Step (Output)
    [ 7407.632347] hm2/hm2_5i25.0:     IO Pin 008 (P3-05): StepGen #4, pin Direction (Output)
    [ 7407.632349] hm2/hm2_5i25.0:     IO Pin 009 (P3-06): StepGen #4, pin Step (Output)
    [ 7407.632352] hm2/hm2_5i25.0:     IO Pin 010 (P3-07): Smart Serial Interface #0, pin TxData0 (Output)
    [ 7407.632354] hm2/hm2_5i25.0:     IO Pin 011 (P3-08): Smart Serial Interface #0, pin RxData0 (Input)
    [ 7407.632356] hm2/hm2_5i25.0:     IO Pin 012 (P3-09): IOPort
    [ 7407.632358] hm2/hm2_5i25.0:     IO Pin 013 (P3-10): IOPort
    [ 7407.632360] hm2/hm2_5i25.0:     IO Pin 014 (P3-11): Encoder #0, pin Index (Input)
    [ 7407.632362] hm2/hm2_5i25.0:     IO Pin 015 (P3-12): Encoder #0, pin B (Input)
    [ 7407.632364] hm2/hm2_5i25.0:     IO Pin 016 (P3-13): Encoder #0, pin A (Input)
    [ 7407.632367] hm2/hm2_5i25.0:     IO Pin 017 (P2-01): StepGen #5, pin Direction (Output)
    [ 7407.632369] hm2/hm2_5i25.0:     IO Pin 018 (P2-14): StepGen #5, pin Step (Output)
    [ 7407.632371] hm2/hm2_5i25.0:     IO Pin 019 (P2-02): StepGen #6, pin Direction (Output)
    [ 7407.632373] hm2/hm2_5i25.0:     IO Pin 020 (P2-15): StepGen #6, pin Step (Output)
    [ 7407.632376] hm2/hm2_5i25.0:     IO Pin 021 (P2-03): StepGen #7, pin Direction (Output)
    [ 7407.632378] hm2/hm2_5i25.0:     IO Pin 022 (P2-16): StepGen #7, pin Step (Output)
    [ 7407.632380] hm2/hm2_5i25.0:     IO Pin 023 (P2-04): StepGen #8, pin Direction (Output)
    [ 7407.632382] hm2/hm2_5i25.0:     IO Pin 024 (P2-17): StepGen #8, pin Step (Output)
    [ 7407.632385] hm2/hm2_5i25.0:     IO Pin 025 (P2-05): StepGen #9, pin Direction (Output)
    [ 7407.632387] hm2/hm2_5i25.0:     IO Pin 026 (P2-06): StepGen #9, pin Step (Output)
    [ 7407.632389] hm2/hm2_5i25.0:     IO Pin 027 (P2-07): IOPort
    [ 7407.632391] hm2/hm2_5i25.0:     IO Pin 028 (P2-08): IOPort
    [ 7407.632392] hm2/hm2_5i25.0:     IO Pin 029 (P2-09): IOPort
    [ 7407.632394] hm2/hm2_5i25.0:     IO Pin 030 (P2-10): IOPort
    [ 7407.632396] hm2/hm2_5i25.0:     IO Pin 031 (P2-11): Encoder #1, pin Index (Input)
    [ 7407.632398] hm2/hm2_5i25.0:     IO Pin 032 (P2-12): Encoder #1, pin B (Input)
    [ 7407.632401] hm2/hm2_5i25.0:     IO Pin 033 (P2-13): Encoder #1, pin A (Input)
    [ 7407.632443] hm2/hm2_5i25.0: registered
    [ 7407.632445] hm2_5i25.0: initialized AnyIO board at 0000:04:02.0
    [ 7487.136417] hm2_5i25.0: dropping AnyIO board at 0000:04:02.0
    [ 7487.136422] hm2/hm2_5i25.0: unregistered
    [ 7487.136440] hm2_pci 0000:04:02.0: PCI INT A disabled
    [ 7487.136459] hm2_pci: driver unloaded
    [ 7487.138640] hm2: unloading
    

    I am, perhaps, easily confused, but it took me a while to realize those pin assignments apply to the 5i25 back panel and on-card connectors, not the 7i76 daughter card’s screw terminals. Yeah, it says 5i25 right there in the dump, but …

    The Fine 7i76 Manual gives the 7i76 pin connections, so they’re not even slightly hidden. [sigh]

    Next, to see if it actually works …

  • LinuxCNC: Optiplex 760 Setup

    I planned to use an old Dell Inspiron 531S AMD desktop for the LinuxCNC installation, but it turned out to have terrible interrupt latency, despite fiddling with all the available BIOS settings and video drivers. Mostly, it ran fine, but would occasionally burp up a millisecond-long latency spike for no apparent reason. So it’s now on the harvest / recycle heap.

    A new-to-me off-lease Dell Optiplex 760 Core 2 Duo in the SDT (Small Desktop Tower) configuration has similar latency numbers:

    Optiplex 760 latency - isolcpu 1
    Optiplex 760 latency – isolcpu 1

    What’s important here is that the latency remains rock-solid stable at those numbers. Contrary to my experience with the D520 and D525 Atoms, isolating one CPU for the real-time tasks didn’t make any noticeable difference, but it’s running that way because the overall performance isn’t a problem.

    Latency around 20 μs is near the upper limit for successful software step generation at any reasonable pace; the LinuxCNC description has more details. In round numbers, running the M2 at 500 mm/s needs a 40 kHz step rate in 1/16 microstep mode = a 25 μs period, which means 20 μs of jitter wouldn’t work well at all. Which is why I’m using Mesa FPGA card to get hardware step generation: it makes such problems Go Away.

    The Optiplex arrived with Windows Vista Business preinstalled; it dates back to mid-2009. I used System Rescue CD to shrink the Windows partition, added a few more, then installed LinuxCNC direct from the CD image (based on Ubuntu 10.04 LTS) and Xubuntu 13.04. The latter serves as a general-purpose installation for times when I don’t need LinuxCNC, because 10.04 is pretty much obsolete for anything other than real-time control.

    Digression 1: Yes, 10.04 LTS. TheRTAI project hasn’t released the patches that will slip the real-time kernel under the stock 3.x Linux kernel: LinuxCNC remains stuck at 10.04 LTS. Those changes have been coming Real Soon Now for quite a while; as with most Open Source projects, they could use more manpower and money. This isn’t a problem, as LinuxCNC is used for motion control, not a general-purpose operating system.

    The SDT case has room for two PCI cards and one PCI-E video card, so I installed the dual-head video card that couldn’t handle the U2711 monitor’s dual-DVI connection (although I’m using only DVI Output 1) and a Mesa 5i25. The middle “card” is actually a tiny PCB connected to a ribbon cable that brings out a second serial port (remember serial ports?) and what could be either or both of a PS2 keyboard or mouse connection (remember PS/2?).

    Optiplex 760 SDT - dual DVI - serial - 5i25
    Optiplex 760 SDT – dual DVI – serial – 5i25

    The back panel has a parallel printer port (which may come in handy for something) and a serial port, although you’re expected to have USB mice and keyboards these days. The front panel even has a floppy drive…

    Digression 2: LinuxCNC does not require a parallel printer port; this seems to be a common misconception among folks who don’t actually know how it works. The Mesa 5i25 FPGA card with a 7i76 step-direction daughter board provides high-resolution timing for five axes, rotary encoder inputs, a bunch of buffered digital I/O bits, a watchdog timer, plus various other useful odds and ends, all behind handy screw terminals.

    The Optiplex 760 has on-board VGA-class video that would also work fine, but the monitor I’m using has its VGA input connected to the box driving the Sherline mill and an unused DVI input. Having that dual-DVI monitor card lying around, I figured I could attach the same monitor to both systems and just poke the monitor’s input section button; I’ve found KVM switches unreliable in this application.

    The usual setup preps the system for public-key SSH on a nonstandard port, sets up the NFS mounts, and tweaks this-and-that: it’s running just fine.

    Digression 3: SSH kvetches when you swap server boxes at the same IP address, as well it should. If you’re foolish enough to have two separate Linux installs on the same box with the same IP, SSH reminds you every time you boot the other distro…

  • Automated Scan-and-Enhance: ImageMagick to the Rescue

    Mary’s folks enjoy the daily crossword, but they wanted a slightly larger edition… and, after a bit of procrastination, I conjured up an automated way to make it happen, so her father need not do this manually with The GIMP and Xsane.

    The scanner, an old HP Scanjet 3970, dropped off the Windows driver list after Vista, so it now runs only with Linux.

    Doing the scan is straightforward, as it’s the default scanner:

    scanimage --mode Gray --opt_emulategray=yes --resolution 300 -x 115 -y 210 --format=pnm & scan.pnm
    

    The X and Y coordinates set the scan dimensions in millimeters, which should be as small as possible consistent with scanning the whole crossword.

    The driver produces output image files in PNM format, which isn’t particularly common these days, or TIFFImageMagick knows what to do with both of them; I picked PNM.

    Unfortunately, for some unknown reason, the SANE driver produces a severely low-contrast image:

    HP3900 Grayscale Scan
    HP3900 Grayscale Scan

    ImageMagick can produce a histogram:

    convert scan.pnm histogram:hist.png
    

    Which shows the problem:

    HP3900 Grayscale Histogram
    HP3900 Grayscale Histogram

    That’s using the grayscale emulation mode: the driver does a Color scan and converts to Gray mode for the output image. It seems having the driver do the conversion produces better results than scanning directly in Color and then applying ImageMagick, but it’s not my scanner and I don’t have a lot of experience with it.

    Given the PNM image:

    • Blow out the contrast
    • Resize the scan to fill the page
    • Crisp up the edges a bit
    convert scan.pnm -level 45%,60% -resize 2400x3000 +repage -unsharp 0 trim.png
    

    Which looks like this:

    Crossword - contrasty resize
    Crossword – contrasty resize

    This being Linux, the best way to print something is with either Postscript or PDF. I used PDF, because then we can look at the results with Reader, a more familiar program than, say, Evince:

    convert -density 300 -size 2550x3300 canvas:white trim.png -gravity center -composite page.pdf
    

    Which centers the crossword on the page over a white background with enough margin to keep the printer happy:

    Crossword - full page
    Crossword – full page

    That PDF goes to the default printer queue, where it’s turned into Postscript and comes out exactly like it should:

    lp page.pdf
    

    I gimmicked the default printer instance to use only black ink by creating a separate CUPS printer with the appropriate defaults. Other programs pay no attention to that setting and the printer uses colored inks. There is no explanation I can find for any of this; Linux / CUPS printing is basically a black box operation.

    In theory, you could print the composited image file as a PNG or some such, but I cannot make it come out the right size in the right place.

    You could do all of that in one line, with one huge ImageMagick invocation kicking off the scan and firing the result to the printer, but leaving some intermediate results lying along the trail isn’t necessarily a Bad Thing. I should probably use random temporary file names, though, in the interest of not polluting the namespace.

    All this happened remotely, with me signed on through SSH: hooray for the command line. Had to use SCP a few times to fetch those intermediate files to puzzle over the results, too.

    The complete Bash script:

    #!/bin/bash
    scanimage --mode Gray --opt_emulategray=yes --resolution 300 -x 115 -y 210 --format=pnm > /tmp/scan.pnm
    convert /tmp/scan.pnm -level 45%,60% -resize 2400x3000 +repage -unsharp 0 /tmp/trim.png
    convert -density 300 -size 2550x3300 canvas:white /tmp/trim.png -gravity center -composite /tmp/page.pdf
    lp /tmp/page.pdf
    

    A slightly closer scan crop with left and top margins may also work, at the cost of more precise positioning on the scanner:

    #!/bin/bash
    scanimage --mode Gray --opt_emulategray=yes --resolution 300 -l 5 -t 6 -x 105 -y 190 --format=pnm > /tmp/scan.pnm