Auto mount USB drives

Raspbian OS (Raspberry Pi) do not mount USB drives automatically out of the box.

I’m pretty annoyed by the lack of easy to use packages by 2021 and I still have to do it myself with the instructions here: https://github.com/avanc/mopidy-vintage/wiki/Automount-USB-sticks

These are cookbook instructions, but I’ll add some insights to what each component means so it’s easier to remember the steps.

At top level, to auto-detect and mount USB drives, we need the following components

  • udev: analogous to what happens behind device manager, it keeps track of and updates devices as they are connected and disconnected immediately. Need to register USB sticks by adding a event handler, which triggers a systemd service (see below)
  • systemd: analogous to Windows’ services. Need to register the service by stating what commands it will call on start (mostly mounting) and cleanup (mostly unmounting)
  • automount script: it’s a user defined script that abstracts most of the hard work detecting the partitions on the USB stick, assign the mount point names, and mount them
# Register udev event handler (rules)
# /etc/udev/rules.d/usbstick.rules

ACTION=="add", KERNEL=="sd[a-z][0-9]", TAG+="systemd", ENV{SYSTEMD_WANTS}="usbstick-handler@%k"

# It triggers a systemd call to "usbstick-handler@" service registered under /etc/systemd/system/
# Register systemd service
# /etc/systemd/system/usbstick-handler@.service
# (Note: instructions used /lib instead of /etc. It's better to add it as /etc as this is manually registered as user-defined service rather than from a package)

[Unit]
Description=Mount USB sticks
BindsTo=dev-%i.device
After=dev-%i.device

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/automount %I
ExecStop=/usr/bin/pumount /dev/%I

# %I is the USB stick's device name under /dev, usually sda
# Abstracted the logic of determining the mount point name and mounting to 'automount' (see below)

Create the file /usr/local/bin/automount and give it execution permission: chmod +x /usr/local/bin/automount

#!/bin/bash

# $1 (first argument) is usually "sda" (supposedly USB stick device name) seen from %I in the systemd service commands
PART=$1

# Within the "sda" (USB stick device of interest), extract the partition labels (if applicable) from lsblk command. The first column (name) is dropped
FS_LABEL=`lsblk -o name,label | grep ${PART} | awk '{print $2}'`

# Decide the mount point name {partition label}_{partition name}
# e.g. MS-DOS_sda1
tokens=($FS_LABEL)
tokens+=($PART)
MOUNT_LABEL=$(IFS='_'; echo "${tokens[*]}")
# Using string array makes it easier to drop the prefix if there's no {partition label}
# Bash use IFS to specify separators for listing all elements of the array

# Suggestion: drop --sync for faster USB access (if you can umount properly)
/usr/bin/pmount --umask 000 --noatime -w --sync /dev/${PART} /media/${MOUNT_LABEL}

This automount script is adapted from https://raspberrypi.stackexchange.com/questions/66169/auto-mount-usb-stick-on-plug-in-without-uuid with my improvements.

Loading

Get myself comfortable with Raspberry Pi

I2C is disabled by default. Use raspi-config to enable it. Editing config file /boot/config.txt directly might not work

Locale & Keyboard (105 keys) defaults to UK out of the box. Shift+3 “#” (hash) sign became “£” pound sign. Use raspi-config to change the keyboard.

It reads random garbage partitions for MFT assigned to FAT16 drives. Just use FAT32

USB drives does not automount by default. usbmount is messy as it creates dummy /media/usb[0-7] folders. Do this instead.

Loading

Flashing router firmware through Serial Port: CFE bootloader (usually Broadcom) based routers

Here’s a summary of learnings from dd-wrt’s serial recovery instructions:

  1. Use a UART controller that signals at 3.3V (e.g. FTDI TTL-232R-3V3) to talk to the board. Regular serial RS-232 ports requires a voltage level shifter converting the signal to swing between 0V to 3.3V.
  2. You only need 3 pins: Tx, Rx and Ground. There’s voltage contention if you plug in the Vcc from TTL-232R-3V3 (It’s the USB’s 5V despite the signaling is 3.3V) to the 3.3V supply of the router. You don’t need the Vcc pin. It didn’t harm anything or do anything when I connected the Vcc pin.
  3. Stick with all serial port defaults and only set the baud to 115200 (default is 9600) and turn off flow control (default is xon/xoff). I use Putty for terminal.
  4. The terminal serves as the monitor for the computer on the router that shows a text console. Broadcom uses CFE bootloader (others use U-Boot with busybox).
  5. CFE bootloader defaults to 192.168.1.1 with subnet mask 255.255.255.0 (aka /24). Set up the network interface to have a static IP on the same subnet to talk to the board.
  6. Good habit: nvram erase
  7. The flash program relies on TFTP protocol to receive the firmware file. So get your TFTP client ready. Microsoft included a TFTP client/server since Windows 7 but usually disabled (turn it on in Windows OptionalFeatures.exe).
  8. TFTP is a simple push(put)/pull(get) design. You can either “push a file on your computer” or “get a file as filename”. You’d want to specify -i switch (binary image transfer) with Windows tftp.exe.
  9. So type this command at the command prompt but don not press enter until your router is ready to grab the file: tftp -i 192.168.1.1 put {path to whatever TRX firmware file}
  10. Go back to the serial terminal and tell the router to accept a TFTP push (in a window of a few seconds before it time out) and flash the memory region flash1.trx with this command: flash -ctheader : flash1.trx
  11. Immediately initiate the TFTP push from your computer (Windows command line example in Step #9 above)
  12. Wait for a couple of hours! The terminal might tell you that it has received the file completely, but it won’t show anything when it’s writing to the flash! It’s a painfully slow process with no feedback. Just be patient!

Some observations

  • FreshTomato firmware absolutely won’t tell you on the screen after it has done flashing (Merlin-WRT does). Just turn the router back on after a couple of hours.
  • Merlin firmware repeats (exposes) the raw passwords to the serial port!
  • FreshTomato firmware boots to a linux prompt on the serial port

Loading

Asus-wrt Merlin Firmware DDNS update interval hack

The “WAN – DDNS” page only allows users to set the DDNS updater to check as frequently as every 30 minutes. My DDNS provider does not have an update frequency limit, so I’d like to have the update client check for every 1 minute. The setting is called “Verify every”:

Attempting to set it to every 1 minute gives this error message:

I searched for the “WAN-DDNS” config webpage file (Advanced_ASUSDDNS_Content.asp) in the firmware source code, and found that it’s under /www folder in the router’s linux root.

Since “Verify every” is such generic words, and Github does not support exact phrase match in search (I use “in:file” specifier in the search box), I pick “WAN IP and hostname verification” (the closest setting which I expect the code to be in the proximity of the one corresponding to “Verify every”) so it has more unique keywords. The first jump:

Since it’s just a dictionary files, we search for the associated internal variable name
DDNS_verification_enable” which points to the this line in Advanced_ASUSDDNS_Content.asp:

Since this name appeared nowhere else, I traced the “id” attribute above, which is “check_ddns_field” and I see a Javascript (.js) file that process the data from the web page forms:

The variable check_ddns_field appears in the if-else-if branches of change_ddns_settings(), so one of the few next few variables after it is likely to correspond to “Verify with”.

The variable name showed up in 4 branches of if-elseif-else switches (switching DDNS service providers), which ddns_regular_period comes right after

Searching for the class member (or struct field)

Bingo. Here’s the entry value range check code. I’ll change the “30” minutes to “1” minute to enable checking at 1 minute intervals (which I think it’s reasonably responsive for testing and general use).


I’d prefer to check if the input range check is there out of feasibility (i.e. what is the smallest increment) or it’s just set to prevent people from getting banned by the DDNS provider for checking too frequently. I looked into the last occurrence of ddns_regular_period and found this:

Which means the web forms is updating NVRAM (environmental) variable of the same name ddns_regular_period, which appears to be called only in watchdog.c:

And Dang! The code enforces if the ddns_regular_period (on NVRAM) is set to be less than the original 30 minute minimum (invalid condition), it’d be set to the default 60 minutes (1 hr).

It’s actually sloppy coding because the defaults are specified in struct fields in defaults.c:

yet that 60 minutes is hard-coded in watchdog.c. That means if I don’t catch it and only changed the default in one place, the behavior will not be what I expected given the right conditions. This is an example of why software feature expansion are likely to break things. If you have solid code, bugs on updates are likely to happen.

I was curious why it says (period*2)

and suspected the ddns_check_count is incremented in 30 second (half-minute) interval. Since it’s watchdog.c, my natural guess is that the watchdog checks every 30 seconds for these event hooks. Turns out the notes (comments) in the code has “30 seconds periods” noted everywhere.

I searched a little bit more about linux watchdogs and found this useful webpage which explained how it works. I didn’t see /dev/watchdog in my router’s rootfs (root file system) so I assumed it’s a hardware watchdog (embedded linux, so duh).

I was about to dig up the hardware manual for the chipset for my router, but I search for they string HW_RTC_WATCHDOG  first and it showed up in linux kernel code (duh):

Note that the HW_RTC_WATCHDOG is a register in this code base, not the number of seconds from Christian’s Blog. i.e. they are completely different things, but it provided a good keyword lead for me to start digging.

The code seems to be the same for various kernel version so I picked any one of them to understand the behavior. First occurrence is in wdt_enable():

The other places are suspend/resume, so I’ll ignore those for now. Note that wdt_enable() is a static function, so only need to search within the same file. The only active place that calls it is wdt_ping():

So there are only 2 things I’ll need to find out: heartbeat and WDOG_COUNTER_RATE:

…. unfinished

https://bitsum.com/firmware_mod_kit.htm


While it’s a lot of useful learning about Embedded Linux and hunting down source code, for the meantime, given that namecheap does not care if you blindly update every minute, it’s easier to just set up a cron job that runs at every N minutes using curl/wget.

dd-wrt has a place for you to enter the cron scripts with the web interface, but you might need to log into the router using SSH and register the cron job yourself:

The core command is called ‘cru‘, which typing it in the command prompt will show you the very simple usage:


admin@Router:/tmp/home/root# cru

Cron Utility
add:    cru a <unique id> <"min hour day month week command">
delete: cru d <unique id>
list:   cru l

<unique id> is just a tag that you make up to name your task. Again the one-liner command needs to be direct absolute path. My ‘curl‘ program is located in /usr/sbin, so the command is:

cru a ncddns * * * * * /usr/sbin/curl "https://dynamicdns.park-your-domain.com/update?host={subdomain or @ for root}&domain={registered domain name}&password={DDNS-specific password generated by namecheap's domain administration page under Advanced DNS}"

The “* * * * *” refers to run at every “minute, hour, date of month, month, date of week”, in other words, run at every minute in every waking moment. The wild card * means ALL-OF.

Cron job registration through CRU is not persistent, so to make it survive reboots, add the above cru command as a line to /jffs/scripts/services-start script. It should be executable by default, if not, make sure you set it to be executable or it won’t run.

Loading