#!/bin/sh
+# c 2024 systemcrash (GitHub)
+
+#hotplug.d triggers this script on the plug {in|out} of USB printers
+
+# Define the uci config section
+DAEMON=p910nd
+DAEMON_HOTPLUG="$DAEMON hotplug"
+DAEMON_ERR="daemon.err"
+DAEMON_INFO="daemon.info"
+DRIVER_HOME_DEFAULT="/opt/"$DAEMON"_drivers"
+SYSUPGRADE_CONF="/etc/sysupgrade.conf"
+
+# Assumptions:
+# * There is no guarantee that multiple devices are re-assigned the same
+# character device upon plug/unplug unless connection hierarchy/tree is
+# unchanged i.e. reboot gives the same order if connection topology is identical.
+# * Depends on udev. char dev number assignment order not guaranteed.
+# * most users likely only have a single printer connected (mDNS announces one)
+
+# Step 1. Get /dev/usb/lpX and build THIS_USB_VIDPID from hotplug passed device.
+
+# Step 2a. Absent p910nd settings, auto configure settings with provided info.
+# A usbvidpid is an anchor: to ensure the printer receives the right blob.
+# Add other ieee1284_id derived info.
+
+# Step 2b. For a matching character device, augment its existing config with any
+# missing usbvidpid and ieee1284_id derived info.
+
+# Step 3. For matching character device and usbvidpid: send_driver
+
+# Caveat: hotplug always maps the first plugged device as /dev/usb/lp0. The 1st
+# /dev/usb/lp0 match in config gets augmented with THIS_USB_VIDPID, whether
+# it is the same "device" or not.
+# The process below runs send_driver, but the driver is not yet on disk.
+# This hotplug script aims for convenience, not technical perfection.
+# Note that this does not matter if the configured lp0 does not match the
+# current lp0. Why? We chose a specific filename for the blob, to which the user
+# provides the file post-factum. So worst-case: soft-bricking if the user puts
+# the wrong blob at the specified file path. This is an acceptable compromise to
+# perfection until we find better ways of shooting ourselves in the foot. :)
+
+# It is a configuration complexity that a p910nd end-user should anyhow be aware
+# of: that multiple devices cannot simultaneously use the same /dev/usb/lpX.
+
+
+# If run as hotplug usb module (as opposed to usbmisc module):
+# DEV_TYPE_FILTER="usb_device"
+
+# Test the script by running $0 -d.
+if [ -n "$1" -a "$1" == "-d" ]; then
+ # Set the variable DEBUG to true (or anything) for extra debug output
+ DEBUG=true
+
+ # Normal hotplug invocation provides these parameters:
+ DEVPATH="/devices/platform/ahb/1b400000.usb/usb2/2-1/2-1:1.0/usbmisc/lp0"
+ DEVNAME="usb/lp0"
+ ACTION="add"
+fi
+
+# For usbmisc, hotplug passes the following usable variables:
+# $0: /sbin/hotplug-call
+# $1: usbmisc
+# $ACTION: add|remove
+# $DEVNAME: usb/lp0
+# $DEVPATH: /devices/platform/ahb/1b400000.usb/usb2/2-1/2-1:1.0/usbmisc/lp0
+# $DEVICENAME: lp0
+# $SEQNUM: 1555
+# $MAJOR: 180
+# $MINOR: 0
+
+# For usb, hotplug passes the following usable variables:
+# outputs:
+# $0: /sbin/hotplug-call
+# $1: usb
+# $ACTION: add|remove|bind|unbind
+# $DEVNAME: bus/usb/002/009
+# $DEVNUM: 009
+# $DEVPATH: /devices/platform/ahb/1b400000.usb/usb2/2-1
+# $DEVICENAME: 2-1
+# $DEVTYPE: usb_device
+# $DRIVER: usb
+# $TYPE: 0/0/0
+# $PRODUCT: 3f0/4117/100
+# $SEQNUM: 1534
+# $BUSNUM: 002
+# $MAJOR: 180
+# $MINOR: 0
+
+
+# usbmisc scripts have access to fewer parameters than usb hotplug scripts, so
+# we must be able to assemble THIS_USB_VIDPID ourselves.
+
+# use % for shortest match, and trim away "/usbmisc/lp*"
+ACTUAL_DEVPATH=${DEVPATH%/usbmisc/lp*}
+# Prepend /sys/ to get actual device path,
+ACTUAL_DEVPATH="/sys$ACTUAL_DEVPATH"
+[ $DEBUG ] && echo ACTUAL_DEVPATH is $ACTUAL_DEVPATH
+PARENT_DEVPATH=$( dirname "${ACTUAL_DEVPATH}" )
+# We might need to do this if symlinks are problematic. Might not:
+# devpath="$(readlink -f $ACTUAL_DEVPATH)"
+
+
+# https://www.usb.org/sites/default/files/usbprint11a021811.pdf
+# Check whether connected device is a "Printer"
+[ "$(cat "$ACTUAL_DEVPATH/bInterfaceClass")" == "07" ] && [ "$(cat "$ACTUAL_DEVPATH/bInterfaceSubClass")" == "01" ] && iAmAPrinter=true
+# Not a printer? Bail.
+[ ! $iAmAPrinter ] && exit 0
+
+
+# Port directionality
+BIP=$( cat "$ACTUAL_DEVPATH/bInterfaceProtocol" )
+[ $DEBUG ] && echo BIP: $BIP
+case $BIP in
+ 01 )
+ BIDIR=0
+ ;;
+ 02 | 03 )
+ BIDIR=1
+ ;;
+esac
+
+
+# Next, we need THIS_USB_VIDPID. This is to ensure that we send the right blob
+# to the right USB printer. THIS_USB_VIDPID is an anchor, or filter, if you will.
+
+# THIS_USB_VIDPID is formed by: idVendor/idProduct
+# Found under: /sys/bus/usb/devices/*/idVendor
+# Avoid anchoring also to bcdDevice which is like a hw version
+# printer driver blobs account for different hw versions anyway, so ignore it.
+# THIS_USB_VIDPID="3f0/4117"
+
+idVendor=$( cat ""$PARENT_DEVPATH"/idVendor" )
+idProduct=$( cat ""$PARENT_DEVPATH"/idProduct" )
+[ $DEBUG ] && echo idVendor+idProduct: $idVendor + $idProduct
+THIS_USB_VIDPID="$idVendor/$idProduct"
+# Driver blob e.g.: Hewlett-Packard_HP_LaserJet_1018_03f0_4117.bin
+
+
+# Not always available:
+iSerialNumber=$( cat "iSerialNumber" 2>/dev/null ) || iSerialNumber=$( cat "serial" 2>/dev/null )
+[ $DEBUG ] && echo iSerialNumber is $iSerialNumber
+
+
+# Get the special IEEE1284 Device ID string (apparently limited to 127 chars)
+ieee1284info=$(cat ""$ACTUAL_DEVPATH"/ieee1284_id" )
+[ $DEBUG ] && echo ieee1284info is $ieee1284info
+
+
+# Absent the uci daemon hotplug config group, set it to a default
+[ -z $(uci -q get $DAEMON.@hotplug[0]) ] && uci -q add $DAEMON hotplug
+
+# # Absent the driver_home path config, set it to a default
+[ -z $(uci -q get $DAEMON.@hotplug[0].driver_home) ] && uci -q set $DAEMON.@hotplug[-1].driver_home=$DRIVER_HOME_DEFAULT
+
+# Make the driver folder hierarchy
+if ! $(mkdir -p $DRIVER_HOME_DEFAULT); then
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_ERR "Error running 'mkdir -p "$DRIVER_HOME_DEFAULT"'."
+fi
+
+# Help the folder survive a sysupgrade:
+if ! $( grep -q "^$DRIVER_HOME_DEFAULT$" "$SYSUPGRADE_CONF" ); then
+ # TODO: remove old non-existent p910nd paths from $SYSUPGRADE_CONF?
+ # Absent the path, try to add it to $SYSUPGRADE_CONF
+ if ! echo $DRIVER_HOME_DEFAULT >> $SYSUPGRADE_CONF; then
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_ERR "Problem adding "$DRIVER_HOME_DEFAULT" path to $SYSUPGRADE_CONF."
+ else
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_INFO "Added "$DRIVER_HOME_DEFAULT" path to $SYSUPGRADE_CONF."
+ fi
+fi
+
+DRIVER_HOME=$(uci -q get $DAEMON.@hotplug[-1].driver_home)
+[ $DEBUG ] && echo DRIVER_HOME is $DRIVER_HOME
+# Trim trailing forward slash if it crept in somehow.
+DRIVER_HOME=${DRIVER_HOME%/}
+DRIVER_BLOBNAME_TAIL=""$idVendor"_"$idProduct".bin"
+[ $DEBUG ] && echo DRIVER_BLOBNAME_TAIL is $DRIVER_BLOBNAME_TAIL
+
+
+# Global device config number variable
+UCI_DEV_CFG_NUMBER=-1
+
+
+# find which daemon configs have the matching lpX interface
+match_current_device() {
+ # Build array of /dev/usb/lpX character devices already configured
+ set -- $(IFS="\n" && uci -q batch <<- EOI
+ get $DAEMON.@$DAEMON[0].device
+ get $DAEMON.@$DAEMON[1].device
+ get $DAEMON.@$DAEMON[2].device
+ get $DAEMON.@$DAEMON[3].device
+ get $DAEMON.@$DAEMON[4].device
+ get $DAEMON.@$DAEMON[5].device
+ get $DAEMON.@$DAEMON[6].device
+ get $DAEMON.@$DAEMON[7].device
+ get $DAEMON.@$DAEMON[8].device
+ get $DAEMON.@$DAEMON[9].device
+ EOI
+ )
+ # $1-$10 are now set
+
+ x=0
+ for i in $@; do
+ # $DEVNAME is passed by hotplug
+ [ $DEBUG ] && echo UCI_DEV_CFG_NUMBER is $UCI_DEV_CFG_NUMBER and CHAR_DEV is $CHAR_DEV
+ [ $DEVNAME == ${i#/dev/} ] && UCI_DEV_CFG_NUMBER=$x && CHAR_DEV=$i
+ # TODO: multiple configured devices could have same CHAR_DEV if not connected concurrently
+ x=$(( $x+1 ))
+ done
+}
+
+
+get_and_store_printer_info() {
+ # gets /sys/bus/usb/devices/2-1:1.0/ieee1284_id:
+ # MFG:Hewlett-Packard
+ # MDL:HP LaserJet 1018
+ # CMD:ACL
+ # CLS:PRINTER
+ # DES:HP LaserJet 1018
+ local MFG
+ local MDL
+ local CMD
+ local CLS
+ local DES
+ local CID
+ local CMT
+ local SN
+
+
+ # Build array of /dev/usb/lpX character devices already configured
+ match_current_device
+
+ # set Internal Field Separator to semicolon found in ieee1284_id files
+ IFS=";"
+ # Got 1284 Device ID string
+ set -- $ieee1284info
+ [ $DEBUG ] && echo ieee1284info: $ieee1284info
+
+ for i in "$@"; do
+ [ $DEBUG ] && echo "$i"
+ [ $DEBUG ] && echo
+
+ case $i in
+ MFG:* | MANUFACTURER:* )
+ MFG=${i##*:};;
+ MDL:* | MODEL:* )
+ MDL=${i##*:};;
+ CMD:* | "COMMAND SET:*" )
+ CMD=${i##*:};;
+ CLS:* )
+ CLS=${i##*:};;
+ DES:* )
+ DES=${i##*:};;
+ CID:* | COMPATIBLEID:* )
+ CID=${i##*:};;
+ COMMENT:* )
+ CMT=${i##*:};;
+ SN:* )
+ SN=${i##*:};;
+ esac
+
+ [ -n "$SN" ] || SN=$iSerialNumber
+ [ $DEBUG ] && echo ${MFG:+MFG=$MFG} ${MDL:+MDL=$MDL} ${CMD:+CMD=$CMD} ${CLS:+CLS=$CLS} ${DES:+DES=$DES} ${SN:+SN=$SN}
+
+ [ $DEBUG ] && echo 'uci set' for UCI_DEV_CFG_NUMBER: $UCI_DEV_CFG_NUMBER
+ # Take the USB info as fact: set bidir regardless. It seems to be a source of confusion.
+ uci -q set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].bidirectional="$BIDIR"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].port)" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].port="0"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].enabled)" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].enabled="1"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].usbvidpid)" -a -n "$THIS_USB_VIDPID" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].usbvidpid="$THIS_USB_VIDPID"
+ # [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns)" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns="1"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_ty)" -a -n "$DES" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_ty="$DES"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_product)" -a -n "$DES" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_product="($DES)"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_mfg)" -a -n "$MFG" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_mfg="$MFG"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_mdl)" -a -n "$MDL" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_mdl="$MDL"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_cmd)" -a -n "$CMD" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_cmd="$CMD"
+ [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_note)" -a -n "$SN" ] && uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].mdns_note="SN:"$SN" Auto-configured by $DAEMON_HOTPLUG"
+
+ if [ -n "$MFG" -a -n "$MDL" -a -n "$DRIVER_BLOBNAME_TAIL" ] && [ -z "$(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].driver_file)" ]; then
+ DRIVER_FILE="$MFG"_"$MDL"_"$DRIVER_BLOBNAME_TAIL"
+ # Make blob filename more friendly: change space to underscore
+ DRIVER_FILE="$DRIVER_HOME"/"${DRIVER_FILE// /_}"
+ [ $DEBUG ] && echo DRIVER_FILE: $DRIVER_FILE
+ uci set $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].driver_file="$DRIVER_FILE"
+ fi
+
+ done
+
+}
+
+daemon_restart() {
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_INFO "(Re)starting $DAEMON"
+ /etc/init.d/$DAEMON restart
+}
+daemon_stop() {
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_INFO "Stopping $DAEMON"
+ /etc/init.d/$DAEMON stop
+}
+send_driver() {
+ DRIVER_FILE=$( uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].driver_file )
+
+ if [ -e $DRIVER_FILE ]; then
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_INFO "Sending driver to $DAEMON printer $THIS_USB_VIDPID"
+
+ if ! cat $DRIVER_FILE > $CHAR_DEV; then
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_ERR "Sending driver to $CHAR_DEV [ $THIS_USB_VIDPID ] failed for some reason."
+ else
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_INFO "Sent $DRIVER_FILE to $CHAR_DEV [ $THIS_USB_VIDPID ]."
+ daemon_restart
+ fi
+ else
+ logger -t "$DAEMON_HOTPLUG" -p $DAEMON_ERR "Missing driver file: $DRIVER_FILE for $CHAR_DEV [ $THIS_USB_VIDPID ] (please upload it)."
+ fi
+}
case "$ACTION" in
- add)
+ add)
+ # Set permissions on the /dev/usb/lpX char dev
[ -n "${DEVNAME}" ] && [ "${DEVNAME##usb/lp*}" = "" ] && {
chmod 660 /dev/"$DEVNAME"
chgrp lp /dev/"$DEVNAME"
}
- ;;
- remove)
- # device is gone
- ;;
+
+ get_and_store_printer_info
+
+ [ $DEBUG ] && echo THIS_USB_VIDPID: $THIS_USB_VIDPID
+ [ $DEBUG ] && echo CHAR_DEV: $CHAR_DEV
+ # usb subsys only:
+ # [ $DEBUG ] && echo DEVTYPE: $DEVTYPE
+ # [ $DEBUG ] && echo DEV_TYPE_FILTER: $DEV_TYPE_FILTER
+ # [ $DEBUG ] && echo PRODUCT: $PRODUCT
+
+ # Extra checks available when run as hotplug usb script:
+ # [ "$DEVTYPE" == "${DEV_TYPE_FILTER}" ]
+ # [ -z "${PRODUCT##*$THIS_USB_VIDPID*}" ]
+
+ # Ensure dev is character device
+ if [ -n "$THIS_USB_VIDPID" -a -c $CHAR_DEV ]; then
+ # if zero string, i.e. usb_ID is a match for $PRODUCT supplied by hotplug
+ if [ $(uci -q get $DAEMON.@$DAEMON[$UCI_DEV_CFG_NUMBER].usbvidpid) == "$THIS_USB_VIDPID" ]; then
+ [ $DEBUG ] && echo "THIS_USB_VIDPID match for $DAEMON device $THIS_USB_VIDPID."
+
+ send_driver
+
+ else
+ [ $DEBUG ] && echo "No THIS_USB_VIDPID match."
+ fi
+ fi
+
+ ;;
+ remove)
+ # device is gone
+ ;;
+ # Special actions available to "usb" subsystem
+ # bind)
+ # # special action
+ # ;;
+ # unbind)
+ # # special action
+ # ;;
esac
+
+# Commit any changes
+[ -n $( uci -q changes $DAEMON ) ] && uci commit $DAEMON