USB Memory: Premature Deaths

After about a year of streaming music, the music died over the course of a month, producing progressively bizarre symptoms on all the local Icecast stations. Killing the streaming server and yanking all the USB memory sticks produced this tableau:

USB Memory - streamer failures
USB Memory – streamer failures

The USB 2.0 32 GB SanDisk Cruzer Fit (tiny, black, upper left) holds images from various network cameras and is not involved with music. It’s nigh onto seven years old and, apparently, still going strong.

The USB 2.0 Centron (gray-and-retroreflective, upper right) was forgotten from the last time I set up a drive for our Forester’s player. There’s another one just like it in the car; they’re impossibly old, as you’d expect from their minuscule size.

The USB 3.0 64 GB Samsung Fit (small, white, lower left) is totally dead, to the extent it doesn’t even announce its presence when plugged into a USB socket. It’s 2.5 years into a five year warranty, but their new USB 3.1 version is twelve bucks; Samsung wins. It formerly contained an extensive selection of public-domain music.

The 64 GB Sandisk Cruzer (huge, black, lower right) suffered some serious damage:

sudo mount -o ro /dev/sdg1 /mnt/part
ll /mnt/part
 ls: cannot access '/mnt/part/PILZ': Input/output error
 total 384K
 drwxr-xr-x   6 ed   users 4.0K Nov 28  2019 ./
 drwxr-xr-x  17 root root  4.0K Jun  7  2019 ../
 -rw-r--r--   1 ed   ed    215K Mar  9  2019 CDClassical.m3u
 drwxrwxr-x  56 ed   ed    4.0K Mar  9  2019 Classical/
 drwx------   2 root root   16K Mar  9  2019 lost+found/
 d?????????   ? ?    ?        ?            ? PILZ/
 drwxrwxr-x 116 ed   ed     12K Mar  9  2019 Pop/
 -rw-r--r--   1 ed   ed    117K Nov 28  2019 Pop.m3u

It still contains a fair amount of music ripped from the CDs we’ve collected over the decades, but it’s obviously unusable. Just for fun, I tried reformatting and copying some files to it, but it eventually hard-crashed with I/O errors:

[37787.872410] usb 2-1: new high-speed USB device number 2 using xhci_hcd
 [37788.013027] usb 2-1: New USB device found, idVendor=0781, idProduct=5530, bcdDevice= 1.00
 [37788.013030] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
 [37788.013032] usb 2-1: Product: Cruzer
 [37788.013034] usb 2-1: Manufacturer: SanDisk
 [37788.013036] usb 2-1: SerialNumber: 4C530001151215101233
 [37788.013604] usb-storage 2-1:1.0: USB Mass Storage device detected
 [37788.014778] scsi host9: usb-storage 2-1:1.0
 [37789.033409] scsi 9:0:0:0: Direct-Access     SanDisk  Cruzer           1.00 PQ: 0 ANSI: 6
 [37789.034569] sd 9:0:0:0: [sdf] 120225792 512-byte logical blocks: (61.6 GB/57.3 GiB)
 [37789.035820] sd 9:0:0:0: [sdf] Write Protect is off
 [37789.035825] sd 9:0:0:0: [sdf] Mode Sense: 43 00 00 00
 [37789.036137] sd 9:0:0:0: [sdf] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
 [37789.086533]  sdf: sdf1
 [37789.089418] sd 9:0:0:0: [sdf] Attached SCSI removable disk
 [38035.071013] EXT4-fs (sdf1): mounting ext3 file system using the ext4 subsystem
 [38035.183172] EXT4-fs (sdf1): mounted filesystem with ordered data mode. Opts: (null)
 [38485.302549] usb 2-1: reset high-speed USB device number 2 using xhci_hcd
 [38490.622285] usb 2-1: device descriptor read/64, error -110
 [38506.195617] usb 2-1: device descriptor read/64, error -110
 [38506.425616] usb 2-1: reset high-speed USB device number 2 using xhci_hcd
 [38511.742339] usb 2-1: device descriptor read/64, error -110
 <<< snippage >>>
 [38548.845743] usb 2-1: USB disconnect, device number 2
 [38548.858925] blk_update_request: I/O error, dev sdf, sector 99556320 op 0x1:(WRITE) flags 0x4800 phys_seg 30 prio class 0
 [38548.858933] EXT4-fs warning (device sdf1): ext4_end_bio:309: I/O error 10 writing to inode 1531939 (offset 0 size 0 starting block 12444541
 )
 [38548.858937] Buffer I/O error on device sdf1, logical block 12444284
 [38548.858944] EXT4-fs warning (device sdf1): ext4_end_bio:309: I/O error 10 writing to inode 1531939 (offset 0 size 0 starting block 12444542
 )
 <<< snippage >>>
 [38548.858984] Buffer I/O error on device sdf1, logical block 12444293
 [38548.859034] blk_update_request: I/O error, dev sdf, sector 99017520 op 0x1:(WRITE) flags 0x4000 phys_seg 3 prio class 0
 [38548.859158] blk_update_request: I/O error, dev sdf, sector 99556560 op 0x1:(WRITE) flags 0x4800 phys_seg 30 prio class 0
 [38548.859224] blk_update_request: I/O error, dev sdf, sector 99017760 op 0x1:(WRITE) flags 0x4000 phys_seg 2 prio class 0
 [38548.859237] blk_update_request: I/O error, dev sdf, sector 99018000 op 0x1:(WRITE) flags 0x4000 phys_seg 2 prio class 0
 >>
 [38549.230765] JBD2: Detected IO errors while flushing file data on sdf1-8
 [38549.230920] Aborting journal on device sdf1-8.
 [38549.231008] Buffer I/O error on dev sdf1, logical block 1545, lost sync page write
 [38549.231011] JBD2: Error -5 detected when updating journal superblock for sdf1-8.
 [38549.231325] Buffer I/O error on dev sdf1, logical block 0, lost sync page write
 [38549.231332] EXT4-fs (sdf1): I/O error while writing superblock
 [38549.231333] EXT4-fs error (device sdf1): ext4_journal_check_start:61: Detected aborted journal
 [38549.231334] EXT4-fs (sdf1): Remounting filesystem read-only
 <<< and so forth and so on >>>

So, yeah, it’s dead, Jim.

The Icecast streaming server reads data continuously from the USB sticks and, given that I set up half a dozen “stations”, there’s plenty of reading going on. The drives are formatted as ext3 and mounted with the noatime option, so there shouldn’t be any writing going on, but it seems a year of constant reading can kill a USB drive.

Fortunately, the original data lives elsewhere, with scripts to copy the appropriate files to the right places, so rebuilding the drives on a pair of new USB sticks wasn’t a big deal.

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

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

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 …

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

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

Raspberry Pi Interrupts vs. Rotary Encoder

Thinking about using a rotary encoder to focus a Raspberry Pi lens led to a testbed:

RPi knob encoder test setup
RPi knob encoder test setup

There’s not much to it, because the RPi can enable pullup resistors on its digital inputs, whereupon the encoder switches its code bits to common. The third oscilloscope probe to the rear syncs on a trigger output from my knob driver.

I started with the Encoder library from PyPi, but the setup code doesn’t enable the pullup resistors and the interrupt (well, it’s a callback) handler discards the previous encoder state before using it, so the thing can’t work. I kept the overall structure, gutted the code, and rebuilt it around a state table. The code appears at the bottom, but you won’t need it.

Here’s the problem, all in one image:

Knob Encoder - ABT - fast - overview
Knob Encoder – ABT – fast – overview

The top two traces are the A and B encoder bits. The bottom trace is the trigger output from the interrupt handler, which goes high at the start of the handler and low at the end, with a negative blip in the middle when it detects a “no motion” situation: the encoder output hasn’t changed from the last time it was invoked.

Over on the left, where the knob is turning relatively slowly, the first two edges have an interrupt apiece. A detailed view shows them in action (the bottom half enlarge the non-shaded part of the top half):

Knob Encoder - ABT - fast - first IRQs
Knob Encoder – ABT – fast – first IRQs

Notice that each interrupt occurs about 5 ms after the edge causing it!

When the edges occur less than 5 ms apart, the driver can’t keep up. The next four edges produce only three interrupts:

Knob Encoder - ABT - fast - 4 edges 3 IRQ
Knob Encoder – ABT – fast – 4 edges 3 IRQ

A closer look at the three interrupts shows all of them produced the “no motion” pulse, because they all sampled the same (incorrect) input bits:

Knob Encoder - ABT - fast - 4 edges 3 IRQ - detail
Knob Encoder – ABT – fast – 4 edges 3 IRQ – detail

In fact, no matter how many edges occur, you only get three interrupts:

Knob Encoder - ABT - fast - 9 edges 3 IRQ
Knob Encoder – ABT – fast – 9 edges 3 IRQ

The groups of interrupts never occur less than 5 ms apart, no matter how many edges they’ve missed. Casual searching suggests the Linux Completely Fair Scheduler has a minimum timeslice / thread runtime around 5 ms, so the encoder may be running at the fastest possible response for a non-real-time Raspberry Pi kernel, at least with a Python handler.

If. I. Turn. The. Knob. Slowly. Then. It. Works. Fine. But. That. Is. Not. Practical. For. My. Purposes.

Nor anybody else’s purposes, really, which leads me to think very few people have ever tried lashing a rotary encoder to a Raspberry Pi.

So, OK, I’ll go with Nearer and Farther focusing buttons.

The same casual searching suggested tweaking the Python thread’s priority / niceness could lock it to a different CPU core and, obviously, writing the knob handler in C / C++ / any other language would improve the situation, but IMO the result doesn’t justify the effort.

It’s worth noting that writing “portable code” involves more than just getting it to run on a different system with different hardware. Rotary encoder handlers are trivial on an Arduino or, as in this case, even an ARM-based Teensy, but “the same logic” doesn’t deliver the same results on an RPi.

My attempt at a Python encoder driver + simple test program as a GitHub Gist:

# Rotary encoder test driver
# Ed Nisley - KE4ZNU
# Adapted from https://github.com/mivallion/Encoder
# State table from https://github.com/PaulStoffregen/Encoder
import RPi.GPIO as GPIO
class Encoder(object):
def __init__(self, A, B, T=None, Delay=None):
GPIO.setmode(GPIO.BCM)
self.T = T
if T is not None:
GPIO.setup(T, GPIO.OUT)
GPIO.output(T,0)
GPIO.setup(A, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(B, GPIO.IN, pull_up_down=GPIO.PUD_UP)
self.delay = Delay
self.A = A
self.B = B
self.pos = 0
self.state = (GPIO.input(B) << 1) | GPIO.input(A)
self.edges = (0,1,-1,2,-1,0,-2,1,1,-2,0,-1,2,-1,1,0)
if self.delay is not None:
GPIO.add_event_detect(A, GPIO.BOTH, callback=self.__update,
bouncetime=self.delay)
GPIO.add_event_detect(B, GPIO.BOTH, callback=self.__update,
bouncetime=self.delay)
else:
GPIO.add_event_detect(A, GPIO.BOTH, callback=self.__update)
GPIO.add_event_detect(B, GPIO.BOTH, callback=self.__update)
def __update(self, channel):
if self.T is not None:
GPIO.output(self.T,1) # flag entry
state = (self.state & 0b0011) \
| (GPIO.input(self.B) << 3) \
| (GPIO.input(self.A) << 2)
gflag = '' if self.edges[state] else ' - glitch'
if (self.T is not None) and not self.edges[state]: # flag no-motion glitch
GPIO.output(self.T,0)
GPIO.output(self.T,1)
self.pos += self.edges[state]
self.state = state >> 2
# print(' {} - state: {:04b} pos: {}{}'.format(channel,state,self.pos,gflag))
if self.T is not None:
GPIO.output(self.T,0) # flag exit
def read(self):
return self.pos
def read_reset(self):
rv = self.pos
self.pos = 0
return rv
def write(self,pos):
self.pos = pos
if __name__ == "__main__":
import encoder
import time
from gpiozero import Button
btn = Button(26)
enc = encoder.Encoder(20, 21,T=16)
prev = enc.read()
while not btn.is_held :
now = enc.read()
if now != prev:
print('{:+4d}'.format(now))
prev = now
view raw encoder.py hosted with ❤ by GitHub