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: Machine Shop

Mechanical widgetry

  • Brita Water Filter Innards

    Brita Water Filter Innards

    Having replaced our disintegrating Brita pitcher a few years ago, I finally got around to opening a used filter to see what’s inside. Start by cutting off the flexible rim (intended as a seal against the pitcher) to reveal the joint, then pry the lid off:

    Brita pitcher filter - opening
    Brita pitcher filter – opening

    Stand it upright before getting the lid off, because the filter contains a zillion charcoal granules and two zillion ion-exchange resin beads:

    Brita pitcher filter - granules
    Brita pitcher filter – granules

    The inside of the lid has mesh screens to keep the innards in place while distributing the raw water:

    Brita pitcher filter - lid
    Brita pitcher filter – lid

    Similarly, mesh on the bottom drains let the filtered water out:

    Brita pitcher filter - emptied
    Brita pitcher filter – emptied

    No surprises, but now we all know what’s in there.

  • Beckman DM73: Package Armor

    Beckman DM73: Package Armor

    For reasons not relevant here, I sent the Beckman DM73 to a good home in Europe. Having some experience with the brutality applied to innocent packages by various package-delivery organizations, I filled a Priority Mail Flat Rate Small Box with a solid block of corrugated cardboard:

    DM73 - cardboard armor
    DM73 – cardboard armor

    One inner layer has a cutout for the manual:

    DM73 - Operator Manual package
    DM73 – Operator Manual package

    The meter and its leads tuck into form-fitting cutouts:

    Beckman DM73 - cardboard packing
    Beckman DM73 – cardboard packing

    I bandsawed the cutouts from a block with enough layers for some space on the top and bottom:

    DM73 - bandsawing cardboard package
    DM73 – bandsawing cardboard package

    After mulling that layout overnight, I made a similar block with the saw cuts on diagonally opposite corners, so pressure on the center of the edges won’t collapse the unsupported sides. A slightly larger meter cutout allowed a wrap of closed-cell foam sheet that likely doesn’t make any difference at all.

    With everything in place, the box had just enough space for a pair of plastic sheets to better distribute any top & bottom impacts.

    I won’t know how the armor performed for a few weeks, but it’s definitely the best packaging idea I’ve had so far.

    Update: After nearly two weeks, the package arrived undamaged and the meter was in fine shape. Whew!

  • Arducam Motorized Focus Camera: Depth of Field

    Arducam Motorized Focus Camera: Depth of Field

    According to the Arducam doc, their Motorized Focus Camera has a 54°×41° field of view, (roughly) equivalent to an old-school wide(-ish) angle 35 mm lens on a 35 mm still camera. For my simple purposes, the camera will be focused on objects within maybe 200 mm:

    Arducam Motorized Focus Camera - desktop test range
    Arducam Motorized Focus Camera – desktop test range

    The numeric keys are 6.36 mm = ¼ inch tall, the function keys are 5.3 mm tall, and the rows are 10 to 11 mm apart.

    The focusing equation converting distance to lens DAC values depends critically on my crude measurements, so the focus distance accuracy isn’t spot on. Bonus: there’s plenty of room for discussion about where the zero origin should be, but given the tune-for-best-picture nature of focusing, it’s good enough.

    I set the CANCEL legend at 50 mm and it’s in good focus with the lens set to that distance:

    Arducam Motorized Focus Camera - 50 mm
    Arducam Motorized Focus Camera – 50 mm

    Focusing at 55 mm sharpens the ON key legend, while the CANCEL legend remains reasonably crisp:

    Arducam Motorized Focus Camera - 55 mm
    Arducam Motorized Focus Camera – 55 mm

    Adding another 5 mm to focus at 60 mm near the front of the second row shows the DoF is maybe 15 mm total:

    Arducam Motorized Focus Camera - 60 mm
    Arducam Motorized Focus Camera – 60 mm

    Focusing at 65 mm, near the middle of the second row, softens the first and fourth rows. Both of the middle two rows seem OK, making the DoF about 20 mm overall:

    Arducam Motorized Focus Camera - 65 mm
    Arducam Motorized Focus Camera – 65 mm

    Jumping to 100 mm, near the top of the first function row:

    Arducam Motorized Focus Camera - 100 mm
    Arducam Motorized Focus Camera – 100 mm

    At 150 mm, about the top of the far row just under the display:

    Arducam Motorized Focus Camera - 150 mm
    Arducam Motorized Focus Camera – 150 mm

    I think 200 mm may be the far limit of useful detail for a 5 MP camera:

    Arducam Motorized Focus Camera - 200 mm
    Arducam Motorized Focus Camera – 200 mm

    At 300 mm the DoF includes the mug at 600 mm, but the calculator keyboard is uselessly fuzzy:

    Arducam Motorized Focus Camera - 300 mm
    Arducam Motorized Focus Camera – 300 mm

    At 500 mm, the mug becomes as crisp as it’ll get and the text on the box at 750 mm is entirely legible:

    Arducam Motorized Focus Camera - 500 mm
    Arducam Motorized Focus Camera – 500 mm

    At 1000 mm, which is basically the edge of the desk all this junk sits atop, the mug and text become slightly fuzzy, so the DoF doesn’t quite reach them:

    Arducam Motorized Focus Camera - 1000 mm
    Arducam Motorized Focus Camera – 1000 mm

    I limited the focus range to 1500 mm, which doesn’t much change the results:

    Arducam Motorized Focus Camera - 1500 mm
    Arducam Motorized Focus Camera – 1500 mm

    I could focus-stack a set of still images along the entire range to get one of those unnatural everything-in-focus pictures.

  • Arducam Motorized Focus Camera: Rotary Encoder and Equation

    Arducam Motorized Focus Camera: Rotary Encoder and Equation

    Mashing rotary encoder reading together with the focus-distance-to-DAC equation produces well-behaved camera focusing.

    First, set up another test range:

    Arducam Motorized Focus Camera - desktop test range
    Arducam Motorized Focus Camera – desktop test range

    Run the test code:

    # Simpleminded focusing test for
    #  Arducam Motorized Focus Camera
    # Gets events through evdev from rotary encoder knob
    
    # Ed Nisley - KE4ZNU
    # 2020-10-20
    
    import sys
    import math
    import evdev
    import smbus
    
    # useful functions
    
    def DAC_from_distance(dist):
        return math.trunc(256*(10.8 + 2180/dist))
    
    # write DAC word to camera I2C bus device
    #  and ignore the omnipresent error return
    
    def write_lens_DAC(bus,addr,val):
        done = False
        while not done:
            try:
                bus.write_word_data(addr,val >> 8,val & 0xff)
            except OSError as e:
                if e.errno == 121:
    #                print('OS remote error ignored')
                    done = True
            except:
                print(sys.exc_info()[0],sys.exc_info()[1])
            else:
                print('Write with no error!')
                done = True
    
    # set up focus distances
    
    closest = 50            # mm
    farthest = 500
    nominal = 100           # default focus distance
    
    foci = [n for n in range(closest,nominal,5)] \
         + [n for n in range(nominal,250,10)]  \
         + [n for n in range(250,1501,25)]
    
    # compute DAC equivalents for each distance
    
    foci_DAC = list(map(DAC_from_distance,foci))
    
    focus_index = foci.index(nominal)
    
    # set up the I2C bus
    
    f = smbus.SMBus(0)
    lens = 0x0c
    
    # set up the encoder device handler
    # requires rotary-encoder dtoverlay aimed at pins 20 & 21
    
    d = evdev.InputDevice('/dev/input/by-path/platform-rotary@14-event')
    print('Rotary encoder device: {}'.format(d.name))
    
    # set initial focus
    
    write_lens_DAC(f,lens,foci_DAC[focus_index])
    
    # fetch I2C events and update the focus forever
    
    for e in d.read_loop():
    #    print('Event: {}'.format(e))
    
        if e.type == evdev.ecodes.EV_REL:
    #        print('Rel: {}'.format(e.value))
    
            if (e.value > 0 and focus_index < len(foci) - 1) or (e.value < 0 and focus_index > 0):
                focus_index += e.value
    
            dist = foci[focus_index]
            dac = foci_DAC[focus_index]
    
            print('Distance: {:4d} mm DAC: {:5d} {:04x} i: {:3d}'.format(dist,dac,dac,focus_index))
    
            write_lens_DAC(f,lens,dac)
    

    Because the knob produces increments of ±1, the code accumulates them into an index for the foci & foci_DAC lists, then sends the corresponding entry from the latter to the lens on every rotary encoder event.

    And then It Just Works!

    The camera powers up with the lens focused at infinity (or slightly beyond), but setting it to 100 mm seems more useful:

    Arducam Motorized Focus Camera - 100 mm
    Arducam Motorized Focus Camera – 100 mm

    Turning the knob counterclockwise runs the focus inward to 50 mm:

    Arducam Motorized Focus Camera - 50 mm
    Arducam Motorized Focus Camera – 50 mm

    Turning it clockwise cranks it outward to 1500 mm:

    Arducam Motorized Focus Camera - 1500 mm
    Arducam Motorized Focus Camera – 1500 mm

    The mug is about 300 mm away, so the depth of field extends from there to infinity (and beyond).

    It needs more work, but now it has excellent upside potential!

  • Raspberry Pi Rotary Encoder: evdev Proof of Concept

    Raspberry Pi Rotary Encoder: evdev Proof of Concept

    After Bill Wittig pointed me in the right direction, writing a Python program to correctly read a rotary encoder knob on a Raspberry Pi is straightforward. At least given some hints revealed by knowing the proper keywords.

    First, enhance the knob’s survivability & usability by sticking it on a perfboard scrap:

    RPi rotary encoder - improved test fixture
    RPi rotary encoder – improved test fixture

    Then find the doc in /boot/overlays/README:

    Name: rotary-encoder
    Info: Overlay for GPIO connected rotary encoder.
    Load: dtoverlay=rotary-encoder,
    =
    Params: pin_a GPIO connected to rotary encoder channel A
    (default 4).
    pin_b GPIO connected to rotary encoder channel B
    (default 17).
    relative_axis register a relative axis rather than an
    absolute one. Relative axis will only
    generate +1/-1 events on the input device,
    hence no steps need to be passed.
    linux_axis the input subsystem axis to map to this
    rotary encoder. Defaults to 0 (ABS_X / REL_X)
    rollover Automatic rollover when the rotary value
    becomes greater than the specified steps or
    smaller than 0. For absolute axis only.
    steps-per-period Number of steps (stable states) per period.
    The values have the following meaning:
    1: Full-period mode (default)
    2: Half-period mode
    4: Quarter-period mode
    steps Number of steps in a full turnaround of the
    encoder. Only relevant for absolute axis.
    Defaults to 24 which is a typical value for
    such devices.
    wakeup Boolean, rotary encoder can wake up the
    system.
    encoding String, the method used to encode steps.
    Supported are "gray" (the default and more
    common) and "binary".

    Add a line to /boot/config.txt to configure the hardware:

    dtoverlay=rotary-encoder,pin_a=20,pin_b=21,relative_axis=1,steps-per-period=2

    The overlay enables the pullup resistors by default, so the encoder just pulls the pins to common. Swapping the pins reverses the sign of the increments, which may be easier than swapping the connector after you have it all wired up.

    The steps-per-period matches the encoder in hand, which has 30 detents per full turn; the default value of 1 step/period resulted in every other detent doing nothing. A relative axis produces increments of +1 and -1, rather than the accumulated value useful for an absolute encoder with hard physical stops.

    Reboot that sucker and an event device pops up:

    ll /dev/input
    total 0
    drwxr-xr-x 2 root root 80 Oct 18 07:46 by-path
    crw-rw---- 1 root input 13, 64 Oct 18 07:46 event0
    crw-rw---- 1 root input 13, 65 Oct 18 07:46 event1
    crw-rw---- 1 root input 13, 63 Oct 18 07:46 mice

    I’m unable to find the udev rule (or whatever) creating those aliases and, as with all udev trickery, the device’s numeric suffix is not deterministic. The only way you (well, I) can tell which device is the encoder and which is the power-off button is through their aliases:

    ll /dev/input/by-path/
    total 0
    lrwxrwxrwx 1 root root 9 Oct 18 07:46 platform-rotary@14-event -> ../event0
    lrwxrwxrwx 1 root root 9 Oct 18 07:46 platform-soc:shutdown_button-event -> ../event1

    The X axis of the mice device might report the same values, but calling a rotary encoder a mouse seems fraught with technical debt.

    The name uses the hex equivalent of the A channel pin number (20 = 0x14), so swapping the pins in the configuration will change the device name; rewiring the connector may be easier.

    Using the alias means you always get the correct device:

    # Rotary encoder using evdev
    # Add to /boot/config.txt
    #  dtoverlay=rotary-encoder,pin_a=20,pin_b=21,relative_axis=1,steps-per-period=2
    # Tweak pins and steps to match the encoder
    
    import evdev
    
    d = evdev.InputDevice('/dev/input/by-path/platform-rotary@14-event')
    print('Rotary encoder device: {}'.format(d.name))
    
    position = 0
    
    for e in d.read_loop():
        print('Event: {}'.format(e))
        if e.type == evdev.ecodes.EV_REL:
            position += e.value
            print('Position: {}'.format(position))
    

    Which should produce output along these lines:

    Rotary encoder device: rotary@14
    Event: event at 1603019654.750255, code 00, type 02, val 01
    Position: 1
    Event: event at 1603019654.750255, code 00, type 00, val 00
    Event: event at 1603019654.806492, code 00, type 02, val 01
    Position: 2
    Event: event at 1603019654.806492, code 00, type 00, val 00
    Event: event at 1603019654.949199, code 00, type 02, val 01
    Position: 3
    Event: event at 1603019654.949199, code 00, type 00, val 00
    Event: event at 1603019655.423506, code 00, type 02, val -1
    Position: 2
    Event: event at 1603019655.423506, code 00, type 00, val 00
    Event: event at 1603019655.493140, code 00, type 02, val -1
    Position: 1
    Event: event at 1603019655.493140, code 00, type 00, val 00
    Event: event at 1603019655.624685, code 00, type 02, val -1
    Position: 0
    Event: event at 1603019655.624685, code 00, type 00, val 00
    Event: event at 1603019657.652883, code 00, type 02, val -1
    Position: -1
    Event: event at 1603019657.652883, code 00, type 00, val 00
    Event: event at 1603019657.718956, code 00, type 02, val -1
    Position: -2
    Event: event at 1603019657.718956, code 00, type 00, val 00
    Event: event at 1603019657.880569, code 00, type 02, val -1
    Position: -3
    Event: event at 1603019657.880569, code 00, type 00, val 00
    

    The type 00 events are synchronization points, which might be more useful with more complex devices.

    Because the events happen outside the kernel scheduler’s notice, you (well, I) can now spin the knob as fast as possible and the machinery will generate one increment per transition, so the accumulated position changes smoothly.

    Much better!

  • Arducam Motorized Focus Camera: Focusing Equation

    Arducam Motorized Focus Camera: Focusing Equation

    The values written to the I²C register controlling the Arducam Motorized Focus Camera lens position are strongly nonlinear with distance, so a simple linear increment / decrement isn’t particularly useful. If one had an equation for the focus value given the distance, one could step linearly by distance.

    So, we begin.

    Set up a lens focus test range amid the benchtop clutter with found objects marking distances:

    Arducam Motorized Focus camera - test setup
    Arducam Motorized Focus camera – test setup

    Fire up the video loopback arrangement to see through the camera:

    Arducam Motorized Focus test - focus infinity
    Arducam Motorized Focus test – focus infinity

    The camera defaults to a focus at infinity (or, perhaps, a bit beyond), corresponding to 0 in its I²C DAC (or whatever). The blue-green scenery visible through the window over on the right is as crisp as it’ll get through a 5 MP camera, the HP spectrum analyzer is slightly defocused at 80 cm, and everything closer is fuzzy.

    Experimentally, the low byte of the I²C word written to the DAC doesn’t change the focus much at all, so what you see below comes from writing a focus value to the high byte and zero to the low byte.

    For example, to write 18 (decimal) to the camera:

    i2cset -y 0 0x0c 18 0

    That’s I²C bus 0 (through the RPi camera ribbon cable), camera lens controller address 0x0c (you could use 12 decimal), focus value 18 * 256 + 0 = 0x12 + 0x00 = 4608 decimal.

    Which yanks the focus inward to 30 cm, near the end of the ruler:

    Arducam Motorized Focus test - focus 30 cm
    Arducam Motorized Focus test – focus 30 cm

    The window is now blurry, the analyzer becomes better focused, and the screws at the far end of the yellow ruler look good. Obviously, the depth of field spans quite a range at that distance, but iterating a few values at each distance gives a good idea of the center point.

    A Bash one-liner steps the focus inward from infinity while you arrange those doodads on the ruler:

    for i in {0..31} ; do let h=i*2 ; echo "high: " $h ; let rc=1 ; until (( rc < 1 )) ; do i2cset -y 0 0x0c $h 0 ; let rc=$? ; echo "rc: " $rc ; done ; sleep 1 ; done

    Write 33 to set the focus at 10 cm:

    Arducam Motorized Focus test - focus 10 cm
    Arducam Motorized Focus test – focus 10 cm

    Then write 55 for 5 cm:

    Arducam Motorized Focus test - focus 5 cm
    Arducam Motorized Focus test – focus 5 cm

    The tick marks show the depth of field might be 10 mm.

    Although the camera doesn’t have a “thin lens” in the optical sense, for my simple purposes the ideal thin lens equation gives some idea of what’s happening. I think the DAC value moves the lens more-or-less linearly with respect to the sensor, so it should be more-or-less inversely related to the focus distance.

    Take a few data points, reciprocate & scale, plot on a doodle pad:

    Arducam Motorized Focus RPi Camera - focus equation doodles
    Arducam Motorized Focus RPi Camera – focus equation doodles

    Dang, I loves me some good straight-as-a-ruler plotting action!

    The hook at the upper right covers the last few millimeters of lens travel where the object distance is comparable to the sensor distance, so I’ll give the curve a pass.

    Feed the points into a calculator and curve-fit to get an equation you could publish:

    DAC MSB = 10.8 + 218 / (distance in cm)
    = 10.8 + 2180 / distance in mm)

    Given the rather casual test setup, the straight-line section definitely doesn’t support three significant figures for the slope and we could quibble about exactly where the focus origin sits with respect to the camera.

    So this seems close enough:

    DAC MSB = 11 + 2200 / (distance in mm)

    Anyhow, I can now tweak a “distance” value in a linear-ish manner (perhaps with a knob, but through evdev), run the equation, send the corresponding DAC value to the camera lens controller, and have the focus come out pretty close to where it should be.

    Now, to renew my acquaintance with evdev

  • RPi HQ Camera: 4.8 mm Computar Video Lens

    RPi HQ Camera: 4.8 mm Computar Video Lens

    The Big Box o’ Optics disgorged an ancient new-in-box Computar 4.8 mm lens, originally intended for a TV camera, with a C mount perfectly suited for the Raspberry Pi HQ camera:

    RPi HQ Camera - Computar 4.8mm - front view
    RPi HQ Camera – Computar 4.8mm – front view

    Because it’s a video lens, it includes an aperture driver expecting a video signal from the camera through a standard connector:

    Computar 4.8 mm lens - camera plug
    Computar 4.8 mm lens – camera plug

    The datasheet tucked into the box (!) says it expects 8 to 16 V DC on the red wire (with black common) and video on white:

    Computar Auto Iris TV Lens Manual
    Computar Auto Iris TV Lens Manual

    Fortunately, applying 5 V to red and leaving white unconnected opens the aperture all the way. Presumably, the circuitry thinks it’s looking at a really dark scene and isn’t fussy about the missing sync pulses.

    Rather than attempt to find / harvest a matching camera connector, the cord now terminates in a JST plug, with the matching socket hot-melt glued to the Raspberry Pi case:

    RPi HQ Camera - 4.8 mm Computar lens - JST power
    RPi HQ Camera – 4.8 mm Computar lens – JST power

    The Pi has +5 V and ground on the rightmost end of its connector, so the Computar lens will be jammed fully open.

    I gave it something to look at:

    RPi HQ Camera - Computar 4.8mm - overview
    RPi HQ Camera – Computar 4.8mm – overview

    With the orange back plate about 150 mm from the RPi, the 4.8 mm lens delivers this scene:

    RPi HQ Camera - 4.8 mm Computar lens - 150mm near view
    RPi HQ Camera – 4.8 mm Computar lens – 150mm near view

    The focus is on the shutdown / startup button just to the right of the heatsink, so the depth of field is maybe 25 mm front-to-back.

    For comparison, the official 16 mm lens stopped down to f/8 has a tighter view with good depth of field:

    RPi HQ Camera - 16 mm lens - 150mm near view
    RPi HQ Camera – 16 mm lens – 150mm near view

    It’d be nice to have a variable aperture, but it’s probably not worth the effort.