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.

Author: Ed

  • Eyeglass Nose Pad Adjustment

    Eyeglass Nose Pad Adjustment

    A new set of glasses arrived from Zenni with one of the nose pads totally misaligned:

    Zenni glasses nosepad - as received
    Zenni glasses nosepad – as received

    Definitely a QC escape.

    Start by removing the tiny screw and the nose pad:

    Zenni glasses nosepad - socket
    Zenni glasses nosepad – socket

    Apply a metal bending pliers (with the concave jaw around bottom of the socket), twist until it lines up properly, then reinstall the pad:

    Zenni glasses nosepad - aligned
    Zenni glasses nosepad – aligned

    No big deal if you happen to have the pliers. Bonus: apply Parafilm to prevent scratching the lenses, OK?

    For what it’s worth, the latest set of four spectacles with high-index (not polycarbonate) progressive lenses in metal frames cost a bit over $200 delivered. The last time around, two specs cost a bit more than half that.

    I’m satisfied with Zenni and EyeBuyDirect (although Luxottica borged EBD and they now push spendy “brand name” frames), but you must be ready / willing / able to apply final tweakage to whatever arrives.

    On the other paw, I’ve been repairing Mary’s collection of full-frontal retail glasses for quite a while, because she’s frustrated with making multiple trips to have The Nice Man repeatedly apply final tweakage.

  • Cleaning Up After The Spider

    Cleaning Up After The Spider

    We found an industrious spider below one of the living room windows, tucked behind the furniture:

    Spider above debris field
    Spider above debris field

    Considering the number of husks and the defunct fly, the spider has been eating like a king for quite a while.

    It’s now in the flower garden by the patio, which may not be quite as good a location but definitely increases net happiness.

  • 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: Knob Switch Key

    Raspberry Pi Rotary Encoder: Knob Switch Key

    The rotary encoder knob I’m using for these tests has a pushbutton switch in its shaft:

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

    Now that I know where to look, it turns out there’s a Raspberry Pi overlay for that:

    Name:   gpio-key
    Info:   This is a generic overlay for activating GPIO keypresses using
            the gpio-keys library and this dtoverlay. Multiple keys can be
            set up using multiple calls to the overlay for configuring
            additional buttons or joysticks. You can see available keycodes
            at https://github.com/torvalds/linux/blob/v4.12/include/uapi/
            linux/input-event-codes.h#L64
    Load:   dtoverlay=gpio-key,<param>=<val>
    Params: gpio                    GPIO pin to trigger on (default 3)
            active_low              When this is 1 (active low), a falling
                                    edge generates a key down event and a
                                    rising edge generates a key up event.
                                    When this is 0 (active high), this is
                                    reversed. The default is 1 (active low)
            gpio_pull               Desired pull-up/down state (off, down, up)
                                    Default is "up". Note that the default pin
                                    (GPIO3) has an external pullup
            label                   Set a label for the key
            keycode                 Set the key code for the button
    
    

    Snuggle the button configuration next to the encoder in /boot/config.txt:

    dtoverlay=rotary-encoder,pin_a=20,pin_b=21,relative_axis=1,steps-per-period=2<br>dtoverlay=gpio-key,gpio=26,keycode=83,label="KNOB"
    

    I haven’t yet discovered where the label text appears, because I picked a keycode defining the button as the decimal point key on a numeric keypad. Perhaps one could create a unique key from whole cloth, but that’s in the nature of fine tuning. In any event, pressing / releasing the button produces key-down / key-up events just like you’d get from a real keyboard.

    The four pins required for the encoder + switch make a tidy block at the right (in this view, left as shown above) end of the RPi’s header:

    Raspberry Pi pinout
    Raspberry Pi pinout

    If you needed the SPI1 hardware, you’d pick different pins.

    Reboot that sucker and another input device appears:

    ll /dev/input/by-path/
    total 0
    lrwxrwxrwx 1 root root 9 Oct 18 10:00 platform-button@1a-event -> ../event0
    lrwxrwxrwx 1 root root 9 Oct 18 10:00 platform-rotary@14-event -> ../event2
    lrwxrwxrwx 1 root root 9 Oct 18 10:00 platform-soc:shutdown_button-event -> ../event1

    As with the encoder device, the button device name includes the hex equivalent of the pin number: 26 decimal = 0x1a.

    Run some code:

    # Keypress from Raspberry Pi GPIO pin using evdev
    # Add to /boot/config.txt
    #  dtoverlay=gpio-key,gpio=26,keycode=83,label="KNOB"
    
    import evdev
    
    b = evdev.InputDevice('/dev/input/by-path/platform-button@1a-event')
    print('Button device: {}'.format(b.name))
    
    print(' caps: {}'.format(b.capabilities(verbose=True)))
    print(' fd: {}'.format(b.fd))
    
    for e in b.read_loop():
        print('Event: {}'.format(e))
        if e.type == evdev.ecodes.EV_KEY:
            print('Key {}: {}'.format(e.code,e.value))
    
    

    Which produces this output:

    Button device: button@1a
    caps: {('EV_SYN', 0): [('SYN_REPORT', 0), ('SYN_CONFIG', 1)], ('EV_KEY', 1): [('KEY_KPDOT', 83)]}
    fd: 3
    Event: event at 1603036309.683348, code 83, type 01, val 01
    Key 83: 1
    Event: event at 1603036309.683348, code 00, type 00, val 00
    Event: event at 1603036310.003329, code 83, type 01, val 00
    Key 83: 0
    Event: event at 1603036310.003329, code 00, type 00, val 00

    All in all, that was easy …