#!/bin/sh
# shellcheck disable=SC3043 # aka 'local VAR'
# 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" ] && [ "$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

# Verify that we have p910nd settings
if ! uci -q get $DAEMON; then
	touch /etc/config/$DAEMON
	uci -q add $DAEMON $DAEMON
	uci -q commit
fi

# 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 "$PARENT_DEVPATH/iSerialNumber" 2>/dev/null ) || iSerialNumber=$( cat "$PARENT_DEVPATH/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 && uci -q commit $DAEMON

# 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=$(printf '\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
	# shellcheck disable=SC2068
	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 DRV
	local CID
	local CMT
	local SN
	local VER


	# Build array of /dev/usb/lpX character devices already configured
	match_current_device

	uqgddu_cmd='uci -q get '"$DAEMON".@"$DAEMON"[$UCI_DEV_CFG_NUMBER]
	uqsddu_cmd='uci -q set '"$DAEMON".@"$DAEMON"[$UCI_DEV_CFG_NUMBER]

	# set Internal Field Separator to semicolon found in ieee1284_id files
	IFS=";"
	# Got 1284 Device ID string
	set -- "$ieee1284info"
	[ "$DEBUG" ] && echo ieee1284info: "$ieee1284info"

	# shellcheck disable=SC2068
	for i in $@; do
		[ "$DEBUG" ] && echo i:"$i"

		case $i in
			MFG:* | MANUFACTURER:* | MFR:* ) 
				MFG=${i##*:};;
			MDL:* | MODEL:* )
				MDL=${i##*:};;
			CMD:* | "COMMAND SET:*" )
				CMD=${i##*:};;
			CLS:* )
				CLS=${i##*:};;
			DES:* | DESCRIPTION:* )
				DES=${i##*:};;
			DRV:* )
				DRV=${i##*:};;
			CID:* | COMPATIBLEID:* )
				CID=${i##*:};;
			COMMENT:* )
				CMT=${i##*:};;
			SN:* | SERIALNUMBER:* | SERN:* )
				SN=${i##*:};;
			VER:* )
				VER=${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
		[ -z "$(eval "$uqgddu_cmd".bidirectional)" ] && eval "$uqsddu_cmd.bidirectional='$BIDIR'"
		[ -z "$(eval "$uqgddu_cmd".port)" ] && eval "$uqsddu_cmd.port='0'"
		[ -z "$(eval "$uqgddu_cmd".enabled)" ] && eval "$uqsddu_cmd.enabled='1'"
		[ -z "$(eval "$uqgddu_cmd".usbvidpid)" ] && [ -n "$THIS_USB_VIDPID" ] && eval "$uqsddu_cmd.usbvidpid='$THIS_USB_VIDPID'"
		# Safe to default to on for mDNS if we found one of the mandatory properties (e.g. MDL)
		[ -z "$(eval "$uqgddu_cmd".mdns)" ] && [ -n "$MDL" ] && eval "$uqsddu_cmd.mdns='1'"
		[ -z "$(eval "$uqgddu_cmd".mdns_ty)" ] && [ -n "$DES" ] && eval "$uqsddu_cmd.mdns_ty='$DES'"
		[ -z "$(eval "$uqgddu_cmd".mdns_product)" ] && [ -n "$DES" ] && eval "$uqsddu_cmd.mdns_product='($DES)'"
		[ -z "$(eval "$uqgddu_cmd".mdns_mfg)" ] && [ -n "$MFG" ] && eval "$uqsddu_cmd.mdns_mfg='$MFG'"
		[ -z "$(eval "$uqgddu_cmd".mdns_mdl)" ] && [ -n "$MDL" ] && eval "$uqsddu_cmd.mdns_mdl='$MDL'"
		[ -z "$(eval "$uqgddu_cmd".mdns_cmd)" ] && [ -n "$CMD" ] && eval "$uqsddu_cmd.mdns_cmd='$CMD'"
		[ -z "$(eval "$uqgddu_cmd".mdns_note)" ] && eval "$uqsddu_cmd.mdns_note='Located near router'"
		# Optional ieee1284_id parameters
		[ -z "$(eval "$uqgddu_cmd".mdns_cid)" ] && [ -n "$CID" ] && eval "$uqsddu_cmd.mdns_cid='$CID'"
		[ -z "$(eval "$uqgddu_cmd".mdns_cls)" ] && [ -n "$CLS" ] && eval "$uqsddu_cmd.mdns_cls='$CLS'"
		[ -z "$(eval "$uqgddu_cmd".mdns_cmt)" ] && [ -n "$CMT" ] && eval "$uqsddu_cmd.mdns_cmt='$CMT'"
		[ -z "$(eval "$uqgddu_cmd".mdns_drv)" ] && [ -n "$DRV" ] && eval "$uqsddu_cmd.mdns_drv='$DRV'"
		[ -z "$(eval "$uqgddu_cmd".mdns_sn)" ] && [ -n "$SN" ] && eval "$uqsddu_cmd.mdns_sn='$SN'"
		[ -z "$(eval "$uqgddu_cmd".mdns_ver)" ] && [ -n "$VER" ] && eval "$uqsddu_cmd.mdns_ver='$VER'"

		# No previously configured device? Configure this instance. Set CHAR_DEV so we can send driver.
		[ $UCI_DEV_CFG_NUMBER -eq -1 ] && eval "$uqsddu_cmd.device=/dev/'$DEVNAME'" && CHAR_DEV=/dev/"$DEVNAME"

		if [ -n "$MFG" ] && [ -n "$MDL" ] && [ -n "$DRIVER_BLOBNAME_TAIL" ] && [ -z "$(eval "$uqgddu_cmd".driver_file)" ]; then
			DRIVER_FILE="$MFG"_"$MDL"_"$DRIVER_BLOBNAME_TAIL"
			# Make blob filename more friendly: change space to underscore
			DRIVER_FILE="$DRIVER_HOME"/"$(echo "$DRIVER_FILE" | sed 's/ /_/g')"
			[ "$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 ]."
		fi
	else
		logger -t "$DAEMON_HOTPLUG"  -p "$DAEMON_INFO" "No driver file: $DRIVER_FILE for $CHAR_DEV [ $THIS_USB_VIDPID ] (upload it if your printer needs a driver loading)."
	fi
}

case "$ACTION" in
	add)
		# Set permissions on the /dev/usb/lpX char dev
		[ -n "${DEVNAME}" ] && [ "${DEVNAME##usb/lp*}" = "" ] && {
			chmod 660 /dev/"$DEVNAME"
			chgrp lp /dev/"$DEVNAME"
		}

		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" ] && [ -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

		daemon_restart

		;;
	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
