The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Tag: Improvements

Making the world a better place, one piece at a time

  • Nothing Exceeds Like Excess: Tektronix CT-5 Current Transformer Probe

    When a 100 A current probe won’t do the job, another order of magnitude can make all the difference:

    Tek CT-5 A6302 Current Probe - 500 W bulb
    Tek CT-5 A6302 Current Probe – 500 W bulb

    That’s a Tektronix CT-5 current transformer, rated for 1 kA between -3 dB points at 0.5 Hz and 20 MHz, with an A6302 20 A probe snapped around its 1000:1 output winding.

    The eBay deal didn’t include the 015-0190-00 1000:1 bucking coil that lets you measure small AC signals against high DC current; if you happen to find one for considerably less than the $100 I was unwilling to pay, let me know. I doubt I’ll ever need it, but ya never know.

    Lacking a calibrated current source with sufficient moxie to exercise the thing, I settled for a 500 W incandescent bulb: 514 W and 4.38 A rms, according to a Kill-A-Watt meter off to the left.

    The 1000:1 output, seen through the A6302 probe at 2 mA/div = 2 A/div:

    Tek CT-5 A6302 - 2 mA div 1000 ratio - 514 W 4.38 A
    Tek CT-5 A6302 – 2 mA div 1000 ratio – 514 W 4.38 A

    The 22.22 mVrms corresponds to 4.4 A = (22.22 / 10) * 0.002 * 1000.

    Moving the probe to the 20:1 output at 100 mA/div = 2 A/div:

    Tek CT-5 A6302 - 100 mA div 20 ratio - 514 W 4.38 A
    Tek CT-5 A6302 – 100 mA div 20 ratio – 514 W 4.38 A

    Again, the scope’s 21.67 mVrms works out to 4.3 A = (21.67 / 10) * 0.1 A * 20.

    Close enough, methinks.

     

     

  • Tektronix AM503, A6302, and A6303 In Full Effect

    Over the past few months I picked up a pair of Tektronix AM503 Current Probe Amplifiers, plus A6302 20 A and A6303 100 A Hall effect probes. The proper calibration procedures require rather specialized (and, in some cases, custom-built) equipment that I don’t have, but I’ll mostly use these things for non-contact / isolated current measurements where just seeing what’s going on counts for more than absolute accuracy.

    For a quick check, I set up a pair of 100 W incandescent bulbs with a plug/socket that breaks out the line conductor into a widowmaker zip cord intended for a foot switch, but I’m not fussy:

    Tektronix A6302 A6303 Current Probes - test load
    Tektronix A6302 A6303 Current Probes – test load

    That’s an old (pronounced “vintage” in eBay-speak) Radio Shack (“Micronta”) clamp-on AC ammeter that, for my present purposes, I can regard as the Gold Standard for current measurement. The 200 W resistive load reads 1.6 A, which is pretty close to the 1.7 A you’d expect.

    The big A6303 probe loafs along at the low end of its range:

    Tek A6303 probe - 200 W incandescent
    Tek A6303 probe – 200 W incandescent

    The scope says 17.78 mV RMS, which translates to 1.8 A with the AM503 set to 1 A/div. A bit hot, perhaps, but not off by too much.

    The two AM503 amps produce slightly different results when switching the probes back and forth, but this arrangement looks consistent:

    Tek A6303 A6302 probes - 1.6 A rms
    Tek A6303 A6302 probes – 1.6 A rms

    With the AM503 amps set to 2 A/div, 7.546 mV = 1.5 A and 7.994 mV = 1.6 A. The last few digits of those RMS calculations absolutely don’t matter.

    The overall error (at least for low-range AC) looks to be around 10%, which is certainly good enough for my immediate needs. I doubt that I can gimmick up a square wave current calibration fixture that I’d trust.

    Labeling the amps improves the odds that I’ll plug the probes in correctly:

    Tektronix TM502 Mainframe with AM503 Current Probe Amps
    Tektronix TM502 Mainframe with AM503 Current Probe Amps

    The A6303 amp lights the “high range” indicator, the A6302 lights the “low range” indicator. Newer (but still obsolete) AM503A and AM503B amps have an LED readout showing the current/division, but …

  • Raspberry Pi Streaming Radio Player: Mostly Viable Product

    The latest version of my simpleminded streaming radio player includes:

    • More durable parsing for track titles with embedded quotes and semicolons
    • Muting during empty / non-music Radionomy tracks
    • The Dutchess County E911 service

    Audionomy’s empty / non-music tracks include a remarkable number of mis-encoded MP3 sections triggering decoding problems; those problems don’t occur during music tracks. Some tracks come through as advertisements, which would be mostly OK apart from the garbled / high-volume gibberish, but on the whole they’re un-listenable:

    ICY Info: StreamTitle='';
    A:1271.0 (21:10.9) of 0.0 (00.0)  4.0% 44%
    [mp3float @ 0x7623e080]overread, skip -7 enddists: -5 -5
    [mp3float @ 0x7623e080]overread, skip -9 enddists: -6 -6
    A:1271.2 (21:11.2) of 0.0 (00.0)  4.0% 45%
    [mp3float @ 0x7623e080]overread, skip -7 enddists: -5 -5
    A:1309.1 (21:49.1) of 0.0 (00.0)  4.0% 42%
    ICY Info: StreamTitle='Targetspot - TargetSpot';
    A:1316.4 (21:56.4) of 0.0 (00.0)  4.0% 40%
    [mp3float @ 0x7623e080]overread, skip -5 enddists: -4 -4
    [mp3float @ 0x7623e080]overread, skip -5 enddists: -2 -2
    

    Muting happens in the mixer, because that seems easier than messing with mplayer in mid-flight. Rather than attempt to control the muted state with specific timeouts, I just un-mute after a new track title arrives; that has no effect if it’s already un-muted. The delays depend on the buffer fill level and avoid the worst of the gibberish.

    The player still falls over dead / jams solid on occasion, generally because the incoming data has stopped streaming or delivered severe encoding problems. Other than that, it runs pretty much all day, every day, on at least one of the Raspberry Pi streamers.

    Still no track display. Mostly, we still don’t miss it.

    The Python source code as a GitHub Gist:

    from evdev import InputDevice,ecodes,KeyEvent
    import subprocess32 as subprocess
    import select
    import re
    import sys
    import time
    Media = {'KEY_KP7' : ['Classical',['mplayer','-playlist','http://stream2137.init7.net/listen.pls'%5D%5D,
    'KEY_KP8' : ['Jazz',['mplayer','-playlist','http://stream2138.init7.net/listen.pls'%5D%5D,
    'KEY_KP9' : ['WMHT',['mplayer','http://live.str3am.com:2070/wmht1'%5D%5D,
    'KEY_KP4' : ['Classic 1000',['mplayer','-playlist','http://listen.radionomy.com/1000classicalhits.m3u'%5D%5D,
    'KEY_KP5' : ['DCNY 911',['mplayer','-playlist','http://www.broadcastify.com/scripts/playlists/1/12305/-5857889408.m3u'%5D%5D,
    'KEY_KP6' : ['WAMC',['mplayer','http://pubint.ic.llnwd.net/stream/pubint_wamc'%5D%5D,
    'KEY_KP1' : ['60s',['mplayer','-playlist','http://listen.radionomy.com/all60sallthetime-keepfreemusiccom.m3u'%5D%5D,
    'KEY_KP2' : ['50-70s',['mplayer','-playlist','http://listen.radionomy.com/golden-50-70s-hits.m3u'%5D%5D,
    'KEY_KP3' : ['Soft Rock',['mplayer','-playlist','http://listen.radionomy.com/softrockradio.m3u'%5D%5D,
    'KEY_KP0' : ['Zen',['mplayer','-playlist','http://listen.radionomy.com/zen-for-you.m3u'%5D%5D
    }
    CurrentKC = 'KEY_KP7'
    Controls = {'KEY_KPSLASH' : '//////',
    'KEY_KPASTERISK' : '******',
    'KEY_KPENTER' : ' ',
    'KEY_KPMINUS' : '<',
    'KEY_KPPLUS' : '>',
    'KEY_VOLUMEUP' : '*',
    'KEY_VOLUMEDOWN' : '/'
    }
    MuteDelay = 5.5 # delay before non-music track; varies with buffering
    UnMuteDelay = 7.3 # delay after non-music track
    MixerChannel = 'PCM' # which amixer thing to use
    # set up event inputs and polling objects
    # This requires some udev magic to create the symlinks
    k = InputDevice('/dev/input/keypad')
    k.grab()
    kp = select.poll()
    kp.register(k.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    v = InputDevice('/dev/input/volume')
    v.grab()
    vp = select.poll()
    vp.register(v.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    # set up files for mplayer pipes
    lw = open('/tmp/mp.log','w') # mplayer piped output
    lr = open('/tmp/mp.log','r') # … reading that output
    # set the mixer output low enough that the initial stream won't wake the dead
    subprocess.call(['amixer','sset',MixerChannel,'10'])
    # Start the player with the default stream
    print 'Starting mplayer on',Media[CurrentKC][0],' -> ',Media[CurrentKC][-1][-1]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    #——————–
    #— Play the streams
    while True:
    # pluck next line from mplayer and decode it
    text = lr.readline()
    if 'ICY Info: ' in text: # these lines may contain track names
    trkinfo = text.split(';') # also splits at semicolon embedded in track name
    for ln in trkinfo:
    if 'StreamTitle' in ln: # this part contains a track name
    if 2 == ln.count("'"): # exactly two single quotes = probably none embedded in track name
    trkhit = re.search(r"StreamTitle='(.*)'",ln)
    if trkhit: # true for valid search results
    TrackName = trkhit.group(1) # the string between the two quotes
    print 'Track name: ', TrackName
    if ('' == TrackName or 'TargetSpot' in TrackName) and 'radionomy' in Media[CurrentKC][-1][-1]:
    print ' … muting empty Radionomy track'
    time.sleep(MuteDelay)
    subprocess.call(['amixer','-q','sset',MixerChannel,'mute'])
    else:
    print ' … unmuting'
    time.sleep(UnMuteDelay)
    subprocess.call(['amixer','-q','sset',MixerChannel,'unmute'])
    else:
    print ' … semicolon in track name: ', ln
    else:
    print ' … quotes in track name: ', ln
    elif 'Exiting…' in text:
    print 'Got EOF / stream cutoff'
    print ' … killing dead mplayer'
    p.kill()
    print ' … flushing pipes'
    lw.truncate(0)
    print ' … discarding keys'
    while [] != kp.poll(0):
    kev = k.read
    print ' … restarting mplayer: ',Media[CurrentKC][0]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    continue
    # accept pending events from volume control knob
    if [] != vp.poll(10):
    vev = v.read()
    for e in vev:
    if e.type == ecodes.EV_KEY:
    kc = KeyEvent(e).keycode
    # print 'Volume kc: ',kc
    if kc in Controls:
    print 'Vol Control: ',kc
    try:
    p.stdin.write(Controls[kc])
    except Exception as e:
    print "Can't send control: ",e
    print ' … restarting player: ',Media[CurrentKC][0]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    # accept pending events from keypad
    if [] != kp.poll(10):
    kev = k.read()
    for e in kev:
    if e.type == ecodes.EV_KEY:
    kc = KeyEvent(e).keycode
    if kc == 'KEY_NUMLOCK':
    continue
    # print 'Got: ',kc
    if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold):
    if True:
    print 'Backspace = shutdown!'
    p = subprocess.call(['sudo','shutdown','-HP','now'])
    else:
    print 'BS = bail from main, ssh to restart!'
    sys.exit(0)
    if KeyEvent(e).keystate != KeyEvent.key_down:
    continue
    if kc in Controls:
    print 'Control:', kc
    try:
    p.stdin.write(Controls[kc])
    except Exception as e:
    print "Can't send control: ",e
    print ' … restarting player: ',Media[CurrentKC][0]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    if kc in Media:
    print 'Switching stream to ',Media[kc][0],' -> ',Media[kc][-1][-1]
    CurrentKC = kc
    print ' … halting player'
    try:
    p.communicate(input='q')
    except Exception as e:
    print 'Perhaps mplayer died?',e
    print ' … killing it for sure'
    p.kill()
    print ' … flushing pipes'
    lw.truncate(0)
    print ' … restarting player: ',Media[CurrentKC][0]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    print 'Out of loop!'
    view raw Streamer.py hosted with ❤ by GitHub
  • Monthly Image: Groundhog Burrow

    Our new back-yard groundhog made extensive renovations and improved the landscaping before moving into the unoccupied burrow against the garage wall:

    Groundhog at garage burrow - cobbles
    Groundhog at garage burrow – cobbles

    It seems the same architect designed this project:

    Cobbled walk - 37 Fairmont Ave
    Cobbled walk – 37 Fairmont Ave

    I cannot explain the post in the middle of the walk; perhaps they’ll remove it when everything’s finished.

    The top photo is through three layers of 1950s glass. I cropped the bottom one from a helmet camera image.

  • Ed’s High-Traction Multi-Grain Bread, V 1.1

    A somewhat lighter, more rye-tasting loaf than my classic recipe:

    Combine dry ingredients in a 4.5-quart mixer bowl:

    • ½ Tbsp dry yeast (1 Tbsp for more boost)
    • 2 Tbsp brown sugar
    • ½ cup dried milk

    Stir in:

    • 1-½ cup warm water

    Dump more dry ingredients on top, do not mix:

    • ½ cup flax seed meal
    • 1 cup bread flour
    • 1 cup rye flour
    • 2-½ cup whole wheat flour
    • 1 tsp salt

    Let the dry ingredients sit on top of the liquid for 15 minutes as the yeast revs up, then run the mixer until the dough ball cleans the bowl sides. Oil the bowl, cover, and let the dough rise for maybe an hour (45 minutes in warm weather / oven).

    Form the ball into an ingot, pack it into a non-stick loaf pan, cover loosely with aluminum foil, and let it rise another hour (30 minutes in warm weather / oven) to fill the pan with a nice loaf. I’ve been using a King Authur 8-½ x 4-½ inch non-stick bread pan to good effect.

    Bake loaf with the foil on top in a 350 °F oven for 50 minutes. You can set the oven to start at a convenient time, run for an hour at 350 °F from a cold start, and the bread will come out fine. If it’s too durable, try 325 °F.

    Drop loaf onto a cooling rack, wait five minutes, slice generous QC sample from one end, apply (peanut) butter, give thanks to the yeast, enjoy.

  • Makerspace Starter Kit: Shipped!

    So I spent the last month (*) extracting the tools, parts, and stock I use on a regular basis, filling 20-ish boxes with stuff I wanted to keep:

    Basement shop - right - before
    Basement shop – right – before

    After I moved all those boxes out of the way, three very industrious guys (and two teens who gradually got into the spirit of the thing) from MakerSmiths devoted all of a Saturday and a bit of Sunday morning converting an entire basement like that into this:

    Basement Shop - right
    Basement Shop – right

    The stuff filled about 3/4 of the floor space in a pair of 26 foot box trucks:

    dsc08699 - Truck 1

    Each truck had a snug 10,000 pound load limit and the stuff didn’t stack well:

    dsc08698 - Truck 2

    The strap under the pile of metal, plus some plywood stiffeners, prevented it from running amok during transit. As long as they didn’t flip the truck, everything seemed well packed and cross-braced.

    Only a few minor injuries; all’s well that ends well.

    Alas, most of the spatial memory that let me find a tool or a part is now wrong; it’ll take a while to re-learn the new locations.

    (*) Samuel Johnson: “… when a man knows he is to be hanged in a fortnight, it concentrates his mind wonderfully.”

  • Clover Seam Ripper Cap

    Mary wanted a rigid cap for a Clover seam ripper that came with a small plastic sheath, so I called one from the vasty digital deep:

    Clover Seam Ripper - new cap
    Clover Seam Ripper – new cap

    The solid model looks about like you’d expect, with a brim around the bottom to paste it on the platform:

    Clover Seam Ripper Cap - Slic3r preview
    Clover Seam Ripper Cap – Slic3r preview

    I added a slightly tapered entry to work around the usual tolerance problems:

    Clover Seam Ripper Cap - bottom view
    Clover Seam Ripper Cap – bottom view

    The taper comes from a hull wrapped around eight small spheres:

    Clover Seam Ripper Cap - Entry Pyramid
    Clover Seam Ripper Cap – Entry Pyramid

    That’s surprisingly easy to accomplish, at least after you get used to this sort of thing:

    hull() {																		// entry taper
    	for (i=[-1,1] , j=[-1,1])
    		translate([i*(HandleEntry[0]/2 - StemRadius),j*(HandleEntry[1]/2 - StemRadius),0])
    			sphere(r=StemRadius,$fn=4*4);
    	for (i=[-1,1] , j=[-1,1])
    		translate([i*(HandleStem[0]/2 - StemRadius),j*(HandleStem[1]/2 - StemRadius),HandleEntry[2] - StemRadius])
    			sphere(r=StemRadius,$fn=4*4);	
    }
    

    The side walls are two threads thick and, at least in PETG, entirely too rigid to slide on easily. I think a single-thread wall with a narrow ridge would provide more spring; if this one gets too annoying, I’ll try that.

    The OpenSCAD source code as a GitHub gist:

    // Clover seam ripper cap
    // Ed Nisley KE4ZNU – April 2016
    //- Extrusion parameters – must match reality!
    // Build with a 5 mm brim to keep it glued to the platform
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    Protrusion = 0.1;
    //——
    // Dimensions
    StemRadius = 0.50; // corner radius
    HandleStem = [6.1, 7.1, 9.0];
    HandleEntry = HandleStem + [1.0,1.0,-4.0]; // Z is -(straight part of stem)
    Cap = [8.5,11.0,45.0]; // XY exterior, Z interior
    //———————-
    //- Build it
    difference() {
    union() {
    translate([0,0,Cap[2]/2]) // main body column
    cube(Cap,center=true);
    translate([-Cap[0]/2,0,Cap[2]]) // rounded cap
    rotate([0,90,0])
    cylinder(d=Cap[1],h=Cap[0],$fn=8*4);
    translate([Cap[0]/2 – Protrusion,0,(Cap[2] + Cap[1]/2)/2]) // text
    rotate([0,90,0])
    linear_extrude(height=ThreadWidth,convexity=10)
    text("Mary Nisley",halign="center",valign="center",size=0.5*Cap[1],font="Arial");
    }
    hull() // stem + blade clearance
    for (i=[-1,1] , j=[-1,1])
    translate([i*(HandleStem[0]/2 – StemRadius),j*(HandleStem[1]/2 – StemRadius),-Protrusion])
    cylinder(r=StemRadius,h=Cap[2] + Protrusion,$fn=4*4);
    hull() { // entry taper
    for (i=[-1,1] , j=[-1,1])
    translate([i*(HandleEntry[0]/2 – StemRadius),j*(HandleEntry[1]/2 – StemRadius),0])
    sphere(r=StemRadius,$fn=4*4);
    for (i=[-1,1] , j=[-1,1])
    translate([i*(HandleStem[0]/2 – StemRadius),j*(HandleStem[1]/2 – StemRadius),HandleEntry[2] – StemRadius])
    sphere(r=StemRadius,$fn=4*4);
    }
    }