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: RPi

Raspberry Pi

  • Raspberry Pi vs. Avahi

    It turns out that the various Avahi daemons performing the magick between whatever.local names and dotted-quad 192.168.1.101 addresses for Raspberry Pi descend into gibbering madness when confronted with:

    • One name corresponding to multiple IP addresses
    • One IP address used for multiple MAC addresses
    • Multiple names for one IP address
    • Multiple names for one MAC address
    • Multiple IP addresses for one MAC address
    • Multiple MAC addresses for one IP address
    • Any and all combinations of the above at various times

    The least of the confusion involved an incorrect IP address linked to a familiar name pulled from deep history by a baffled daemon doing the best it can with what it thinks it knows. Despite what I concluded, rather early in the process, there’s no real error, other than my performing what amounted to a self-inflicted fast-flux nameserver attack.

    Anyhow, I devoted the better part of an afternoon to sorting out the mess, which involved labeling all the streaming radio players with their MAC addresses and rebooting them one-by-one to allow all the daemons time to recognize the current situation:

    Raspberry Pi 3 - WiFi MAC address
    Raspberry Pi 3 – WiFi MAC address

    That label corresponds to the Pi 3’s on-board WiFi adapter.

    For Pi 2 boxen, the MAC address travels with the WiFi adapter jammed into a USB port:

    SunFounder WiFi Adapter - MAC address
    SunFounder WiFi Adapter – MAC address

    I didn’t label the (unused) Ethernet jacks, figuring I’d solve that problem after it trips me up.

  • Raspberry Pi Streaming Radio Player: Room Customization

    Sometimes you (well, I) want a bit of late-night music, which is now one button press away. However, I initially set things up so the Raspberry Pi’s startup code executed a Python script on a network share from the file server in the basement, which shuts down around midnight after the daily backup.

    Keeping a local copy meant having to update that copy whenever I tweak the code, a nuisance not to be tolerated. This Bash (or whatever) code in /etc/rc.local figures out if the server is up and, if so, updates the local copy from the server. If the server isn’t up, then it just runs with what it has:

    #!/bin/sh
    # was !/bin/sh -e
    
    ... snippage ...
    
    server=192.168.1.4
    
    ping -c 1 $server
    if [ $? -eq 0 ]
    then
      mount -o ro ${server}:/mnt/bulkdata/Project\ Files/Streaming\ Media\ Player/Firmware/ /mnt/part
      rsync -auv /mnt/part/Streamer.py /home/pi
      umount /mnt/part
    fi
    
    sudo -u pi sh -c 'python /home/pi/Streamer.py any' &
    

    N.B.: you must remove the -e from the shebang, because otherwise the script jams to a stop when the ping fails. Took me a while to figure that out, yup.

    Use raspi-config to force the startup sequence to wait until the network is available. Turns out that the DHCP process can stall for half a minute, so fixed timeouts don’t work.

    Hardcoding the server IP address eliminates a whole bunch of mysterious failures apparently due to whatever handles the translation from mollusk.local to the dotted quad. Maybe that’s not really a problem, but I’ll run with it.

    Now the streamers fetch the Latest and Greatest version whenever they’re on during the day and run their local copy, with the room parameter telling it where it lives.

    Life is good!

  • Raspberry Pi Streaming Radio Player: Command Line Parsing

    Some experience suggested different default stations & volume settings for the streamers in various rooms, so the Python code now parses its command line to determine how to configure itself:

    import argparse as args
    
    cmdline = args.ArgumentParser(description='Streaming Radio Player',epilog='KE4ZNU - http://softsolder.com')
    cmdline.add_argument('Loc',help='Location: BR1 BR2 ...',default='any',nargs='?')
    args = cmdline.parse_args()
    

    I should definitely pick a different variable name to avoid the obvious clash.

    With that in hand, the customization takes very effort:

    CurrentKC = 'KEY_KP7'
    MuteDelay = 8.5         # delay before non-music track; varies with buffering
    UnMuteDelay = 7.5       # delay after non-music track
    MixerVol = '15'         # mixer gain
    
    Location = vars(args)['Loc'].upper()
    print 'Player location: ',Location
    logging.info('Player setup for: ' + Location)
    
    if Location == 'BR1':
      CurrentKC = 'KEY_KPDOT'
      MixerVol = '10'
    elif Location == 'BR2':
      MuteDelay = 6.0
      UnMuteDelay = 8.0
    MixerVol = '5'
    

    The Location = vars() idiom returns a dictionary of all the variables and their values, of which there’s only one at the moment. The rest of the line extracts the value and normalizes it to uppercase.

    Now we can poke the button and get appropriate music without having to think very hard.

    Life is good!

    The Python source code, which remains in dire need of refactoring, as a GitHub Gist:

    from evdev import InputDevice,ecodes,KeyEvent
    import subprocess32 as subp
    import select
    import re
    import sys
    import time
    import logging
    import os.path
    import argparse as args
    cmdline = args.ArgumentParser(description='Streaming Radio Player',epilog='KE4ZNU – http://softsolder.com')
    cmdline.add_argument('Loc',help='Location: BR1 BR2 …',default='any',nargs='?')
    args = cmdline.parse_args()
    Media = {'KEY_KP7' : ['Classical',False,['mplayer','–quiet','-playlist','http://stream2137.init7.net/listen.pls'%5D%5D,
    'KEY_KP8' : ['Jazz',False,['mplayer','–quiet','-playlist','http://stream2138.init7.net/listen.pls'%5D%5D,
    'KEY_KP9' : ['WMHT',False,['mplayer','–quiet','http://wmht.streamguys1.com/wmht1'%5D%5D,
    'KEY_KP4' : ['Classic 1000',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/1000classicalhits.m3u'%5D%5D,
    'KEY_KP5' : ['DCNY 911',False,['mplayer','–quiet','-playlist','http://www.broadcastify.com/scripts/playlists/1/12305/-5857889408.m3u'%5D%5D,
    'KEY_KP6' : ['WAMC',False,['mplayer','–quiet','http://pubint.ic.llnwd.net/stream/pubint_wamc'%5D%5D,
    'KEY_KP1' : ['60s',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/all60sallthetime-keepfreemusiccom.m3u'%5D%5D,
    'KEY_KP2' : ['50-70s',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/golden-50-70s-hits.m3u'%5D%5D,
    'KEY_KP3' : ['Soft Rock',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/softrockradio.m3u'%5D%5D,
    'KEY_KP0' : ['Zen',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/zen-for-you.m3u'%5D%5D,
    'KEY_KPDOT' : ['Ambient',False,['mplayer','–quiet','http://185.32.125.42:7331/maschinengeist.org.mp3'%5D%5D
    }
    Controls = {'KEY_KPSLASH' : '//////',
    'KEY_KPASTERISK' : '******',
    'KEY_KPENTER' : ' ',
    'KEY_KPMINUS' : '<',
    'KEY_KPPLUS' : '>',
    'KEY_VOLUMEUP' : '*',
    'KEY_VOLUMEDOWN' : '/'
    }
    MuteStrings = ["TargetSpot","[Unknown]","Advert:","+++","—","SRR","Srr","ZEN FOR","Intro of","Jingle – ","*bumper*"]
    Muted = False # keep track of muted state
    MixerChannel = 'PCM' # which amixer thing to use
    logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',filename='/tmp/Streamer.log',level=logging.INFO)
    logger = logging.getLogger()
    # Set up defaults based on where we are
    CurrentKC = 'KEY_KP7'
    MuteDelay = 8.5 # delay before non-music track; varies with buffering
    UnMuteDelay = 7.5 # delay after non-music track
    MixerVol = '15' # mixer gain
    Location = vars(args)['Loc'].upper()
    print 'Player location: ',Location
    logging.info('Player setup for: ' + Location)
    if Location == 'BR1':
    CurrentKC = 'KEY_KPDOT'
    MixerVol = '10'
    elif Location == 'BR2':
    MuteDelay = 6.0
    UnMuteDelay = 8.0
    MixerVol = '5'
    # 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)
    # if volume control knob exists, then set up its events
    VolumeDevice = '/dev/input/volume'
    vp = select.poll()
    if os.path.exists(VolumeDevice):
    v = InputDevice(VolumeDevice)
    v.grab()
    vp.register(v.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    # set up file for output tracing
    lw = open('/tmp/mp.log','w') # mplayer piped output
    # set the mixer output low enough that the initial stream won't wake the dead
    subp.call(['amixer','sset',MixerChannel,MixerVol])
    # Start the player with the default stream, set up for polling
    print 'Starting mplayer on',Media[CurrentKC][0],' -> ',Media[CurrentKC][-1][-1]
    logging.info('Starting mplayer on %s -> %s',Media[CurrentKC][0],Media[CurrentKC][-1][-1])
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp = select.poll() # this may be valid for other invocations, but is not pretty
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    #——————–
    #— Play the streams
    while True:
    # pluck next line from mplayer and decode it
    if [] != pp.poll(10):
    text = p.stdout.readline()
    if 'ICY Info: ' in text: # these lines may contain track names
    lw.write(text)
    lw.flush()
    trkinfo = text.split(';') # also splits at semicolon embedded in track name
    # logging.info('Raw split line: %s', trkinfo)
    for ln in trkinfo:
    if 'StreamTitle' in ln: # this part probably contains the track name
    NeedMute = False # assume a listenable track
    trkhit = re.search(r"StreamTitle='(.*)'",ln) # extract title if possible
    if trkhit: # regex returned valid result?
    TrackName = trkhit.group(1) # get string between two quotes
    else:
    print ' … regex failed for line: ', ln
    logging.info('Regex failed for line: [' + ln + ']')
    TrackName = "Invalid StreamTitle format"
    print 'Track name: [', TrackName, ']'
    logging.info('Track name: [%s]', TrackName)
    if Media[CurrentKC][1] and ( (len(TrackName) == 0) or any(m in TrackName for m in MuteStrings) ) :
    NeedMute = True
    if NeedMute:
    print ' … muting:',
    if Media[CurrentKC][1] and not Muted:
    time.sleep(MuteDelay) # brute-force assumption about buffer leadtime
    subp.call(['amixer','-q','sset',MixerChannel,'mute'])
    Muted = True
    print 'done'
    logging.info('Track muted')
    else:
    print ' … unmuting:',
    if Muted:
    if Media[CurrentKC][1]:
    time.sleep(UnMuteDelay) # another brute-force timing assumption
    Muted = False
    subp.call(['amixer','-q','sset',MixerChannel,'unmute'])
    print 'done'
    logging.info('Track unmuted')
    elif 'Exiting.' in text: # mplayer just imploded
    lw.write(text)
    lw.flush()
    print 'Got EOF / stream cutoff: []',text,']'
    logging.info('EOF or stream cutoff')
    print ' … killing dead mplayer'
    pp.unregister(p.stdout.fileno())
    p.terminate() # p.kill()
    p.wait()
    # print ' … flushing pipes'
    # lw.truncate(0)
    print ' … discarding keys'
    while [] != kp.poll(0):
    kev = k.read
    time.sleep(10)
    print ' … restarting mplayer: ',Media[CurrentKC][0]
    logging.info('Restarting mplayer')
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    logging.info(' … running')
    # accept pending events from volume control knob
    if [] != vp.poll(10):
    vev = v.read()
    lw.write('Volume')
    lw.flush()
    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]
    logging.info('Error sending volume, restarting player: ' + str(e))
    try:
    pp.unregister(p.stdout.fileno())
    except Exception as e:
    print 'Trouble unregistering stdout: ',e
    logging.info('Cannot unregister stdout: ' + str(e))
    time.sleep(2)
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    logging.info(' … running')
    # accept pending events from keypad
    if [] != kp.poll(10):
    kev = k.read()
    lw.write("Keypad")
    lw.flush()
    for e in kev:
    if e.type == ecodes.EV_KEY:
    kc = KeyEvent(e).keycode
    if kc == 'KEY_NUMLOCK': # discard these, as we don't care
    continue
    # print 'Got: ',kc
    if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold):
    if True:
    print 'Backspace = shutdown!'
    p.kill()
    logging.shutdown()
    q = subp.call(['sudo','shutdown','-P','now'])
    q.wait()
    time.sleep(5)
    print "Oddly, we did not die…"
    else:
    print 'BS = bail from main!'
    logging.shutdown()
    sys.exit(0)
    break
    if KeyEvent(e).keystate != KeyEvent.key_down: # discard key up & other rubbish
    continue
    if kc in Controls:
    print 'Control:', kc
    logging.info('Control: ' + kc)
    try:
    p.stdin.write(Controls[kc])
    except Exception as e:
    print "Can't send control: ",e
    print ' … restarting player: ',Media[CurrentKC][0]
    logging.info('Error sending controls, restarting player: ' + str(e))
    try:
    pp.unregister(p.stdout.fileno())
    except Exception as e:
    print 'Trouble unregistering stdout: ',e
    logging.info('Cannot unregister stdout: ' + str(e))
    p.terminate() # p.kill()
    p.wait()
    time.sleep(2)
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    logging.info(' … running')
    if kc in Media:
    print 'Switching stream to ',Media[kc][0],' -> ',Media[kc][-1][-1]
    logging.info('Switching stream: ' + Media[kc][0] + ' -> ' + Media[kc][-1][-1])
    CurrentKC = kc
    print ' … halting player'
    try:
    pp.unregister(p.stdout.fileno())
    except Exception as e:
    print 'Trouble unregistering stdout: ',e
    logging.info('Cannot unregister stdout: ' + str(e))
    try:
    p.communicate(input='q')
    except Exception as e:
    print 'Perhaps mplayer already died? ',e
    logging.info('Already died? ' + str(e))
    try:
    p.terminate() # p.kill()
    p.wait()
    except Exception as e:
    print 'Trouble with terminate or wait: ',e
    logging.info('Trouble with terminate or wait: ' + str(e))
    time.sleep(2)
    print ' … restarting player: ',Media[CurrentKC][0]
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    logging.info(' … running')
    print 'Out of loop!'
    logging.shutdown()
    view raw Streamer.py hosted with ❤ by GitHub
  • Read-Only MicroSDHC Card

    I iterated this sequence three times before I caught on:

    • ssh into Raspberry Pi
    • Edit /etc/rc.local, save changes
    • Reboot, observe the changes had no effect
    • cat /etc/rc.local shows no changes

    Then I:

    • Edited / saved
    • Listed the file to verify the changes
    • Rebooted, observe no effect from changes
    • Listed the file again: the changes were gone

    Huh.

    Defunct 8 GB MicroSDHC card
    Defunct 8 GB MicroSDHC card

    It turns out the card went read-only without warning, so I was displaying the contents of the file cache buffers after the edit, not the data stored on the card. Rebooting started with empty caches, read the previous file contents, and behaved accordingly.

    The F3 utilities now live in the Ubuntu repository and no longer require compiling from source. The result:

    sudo f3probe --time-ops /dev/sdb
    F3 probe 6.0
    Copyright (C) 2010 Digirati Internet LTDA.
    This is free software; see the source for copying conditions.
    
    WARNING: Probing normally takes from a few seconds to 15 minutes, but
             it can take longer. Please be patient.
    
    Probe finished, recovering blocks... Done
    
    Bad news: The device `/dev/sdb' is damaged
    
    Device geometry:
    	         *Usable* size: 0.00 Byte (0 blocks)
    	        Announced size: 7.35 GB (15415296 blocks)
    	                Module: 8.00 GB (2^33 Bytes)
    	Approximate cache size: 0.00 Byte (0 blocks), need-reset=no
    	   Physical block size: 512.00 Byte (2^9 Bytes)
    
    Probe time: 164.4ms
     Operation: total time / count = avg time
          Read: 107.1ms / 4098 = 26us
         Write: 56.6ms / 2049 = 27us
         Reset: 0us / 0 = 0us
    

    That card has been kicking around for a while and started out as a no-name generic in some random gadget. Of course, those fancy Sony MicroSD cards weren’t shining examples of durability, either.

    I’m mildly astonished the streaming player worked perfectly with what amounts to a read-only filesystem, but that’s what caching is all about: there was no need to write the data to “disk”.

  • Electronics vs. Dark Rooms

    Despite its diminutive size, the white LED on the end of the Dell AC511 USB SoundBar lights up a dark bedroom surprisingly well:

    Dell AC511 USB SoundBar - white power LED
    Dell AC511 USB SoundBar – white power LED

    That’s pretty much the only power-on indicator for the streaming players, so I didn’t want to just slap a strip of black tape over it. Instead, because white LEDs don’t emit much energy toward the red end of the spectrum, I made a cute little filter from a snippet of Primary Red gel filter material, surrounded by a black Gorilla Tape donut:

    Red filter for Dell AC511 USB power LED
    Red filter for Dell AC511 USB power LED

    Two layers of Primary Red cut the light intensity to a dim glow that’s barely visible in daylight and completely inoffensive at night:

    Red filter for Dell AC511 - installed
    Red filter for Dell AC511 – installed

    The blue activity LED on the SunFounder got the black electrical tape treatment, however, with just a sliver showing through to give a hint that it’s still active:

    SunFounder RT5370 USB WiFi Adapter - masked LED
    SunFounder RT5370 USB WiFi Adapter – masked LED

    One of the other WiFi adapters has a pinhole over a red LED that’s barely visible. Another, seemingly identical one, lacks the red LED under the pinhole; when I asked the vendor about that, I was told it was removed “to save power.” Yeah, right. That was part of the motivation to try a different adapter next time around, with good results.

    Of course, you must wrap an opaque black case around the Raspberry Pi to tamp down the red and green LEDs on the PCB. It’s possible to control them in software, with varying degrees of difficulty depending on which Pi you have, but …

  • Raspberry Pi WiFi Adapters

    One might be forgiven for thinking these two USB Wifi adapters are essentially identical:

    USB Wifi adapters
    USB Wifi adapters

    Turns out the SunFounder RT5370 (on the top, with the stylin’ curved case) has better performance than the Wifi With Antenna (on the bottom, with full-frontal chunk goin’ on), by a not inconsiderable 5 to 10 dB. Boosting the received power level in the fringe areas of our house from -70 dBm to -63 dBm makes all the difference between not working and steady streaming.

    The built-in WiFi antenna on a Raspberry Pi 3 ticks along 10 dB lower, with -80 dBm (10 pW!) at the receiver making for poor communication: a Pi 3 works perfectly within reasonable line-of-sight of the router (even through our wood floor) and wakes up blind in fringe areas. Hacking an external antenna probably helps, but definitely isn’t a net win compared to ten bucks worth of USB adapter.

    The wavemon utility (it’s in the Raspbian repo) comes in handy for figuring that sort of thing.

    There is, of course, no way to determine anything important about the adapters from their product descriptions, which are essentially identical, right down to the price. Neither have any product identification on their cases. The back of the package for the SunFounder gadget gives some specs, none of which may mean anything (clicky for more dots):

    SunFounder RT5370 USB WiFi Adapter Specs
    SunFounder RT5370 USB WiFi Adapter Specs

    I ordered another SunFounder adapter, Just In Case it comes in handy, with the hope that both behave the same way.

  • Raspberry Pi 3 Reset Switch

    The (relatively) new Raspberry Pi 3 PCB layout puts the Run header in a different location than in the Pi 2, but a minute of filing gnaws a suitable opening:

    Raspberry Pi 3 - Reset Switch
    Raspberry Pi 3 – Reset Switch

    As before, a hot-melt glue blob holds the switch in place. I’d prefer a black case, if only to hide the blob, but clear-ish is what’s available right now.

    Remember those orderly shutdowns, even at the cost of a keypad button!