A bedroom rearrangement displaced the Dell Sound Bar attached to the streaming music player from its accustomed perch, so I conjured a mount from the parts bin to hang it from a shelf:
Dell sound bar mount – installed
The sound bar originally fit below any Dell monitor with the appropriate lugs under the bezel, but a bit of bandsaw work and hand filing produced a reasonable facsimile from an aluminum sheet:
Dell sound bar mount – plate installed
The bar’s plastic bits require a few millimeters of clearance above the sheet, now provided by a matching plywood shape:
Dell sound bar mount – parts
A trial fit showed all the parts would fly in formation:
Dell sound bar mount – trial fit
A laser-cut cardboard template maintained alignment and spacing while I stood on my head screwing the mount in place.
The CNC-3018XL and MPCNC machines each have a Raspberry Pi feeding G-Code into an Arduino clone controlling the stepper motors. The former grew a USB WiFi interface in place of its internal WiFi hardware when it seemed to have difficulty connecting to the house router, while the latter pretty much worked. Of late, however, I’ve been trying to reduce the number of WiFi devices cluttering the airwaves, with the result of wiring both machines to an old Ethernet switch from the Box o’ Network Stuff:
LinkSys Switch for CNC machines
The blue puck is the KVM button to select one of the machines for the keyboard / mouse / monitor on the bench.
One key point I generally screw up: the WiFi IP address cannot become the wired IP address without rebooting everything else on the network. Instead, just change the IP addresses and be done with it.
Collecting all the pieces in one place:
Disable the both internal WiFi hardware and Bluetooth in /boot/config.txt, thereby eliminating the need to force the WiFi down in /etc/rc.local:
One of the streaming media players behaved funny, which always results in a numeric keypad battery replacement. This AmazonBasics AAA alkaline was down to about 0.5 V and long past its best-used-by date:
Suggestions that Amazon monitors their Marketplace sellers to figure out what’s profitable, then promote a Good Enough house brand product to kill off the competition, seem to describe the situation just about perfectly.
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
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 >>>
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.
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
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
Focusing at 55 mm sharpens the ON key legend, while the CANCEL legend remains reasonably crisp:
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
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
Jumping to 100 mm, near the top of the first function row:
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
I think 200 mm may be the far limit of useful detail for a 5 MP camera:
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
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
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
I limited the focus range to 1500 mm, which doesn’t much change the results:
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 – 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
Turning the knob counterclockwise runs the focus inward to 50 mm:
Arducam Motorized Focus Camera – 50 mm
Turning it clockwise cranks it outward to 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!
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:
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
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