2026-08-19 23:00:47 -04:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
#
|
|
|
|
|
# One-time system setup for the CH347 USB-I2C adapter and the RM3100 logger.
|
|
|
|
|
# Idempotent: safe to re-run.
|
|
|
|
|
#
|
2026-08-23 18:59:51 -04:00
|
|
|
# Needs sudo for exactly one thing: installing the udev rule that grants this
|
|
|
|
|
# user access to the adapter's USB node.
|
2026-08-19 23:00:47 -04:00
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
VID=1a86
|
|
|
|
|
PID=55db
|
2026-08-23 20:50:56 -04:00
|
|
|
|
|
|
|
|
# The 60- prefix is load-bearing, not a style choice. udev sorts every rules file
|
|
|
|
|
# from every directory into one lexicographic sequence (udev(7), "RULES FILES"),
|
|
|
|
|
# and the only thing that acts on TAG+="uaccess" is this line in systemd's own
|
|
|
|
|
# /usr/lib/udev/rules.d/73-seat-late.rules:
|
|
|
|
|
#
|
|
|
|
|
# TAG=="uaccess", ENV{MAJOR}!="", RUN{builtin}+="uaccess"
|
|
|
|
|
#
|
|
|
|
|
# A rule numbered above 73 adds the tag *after* the rule that would have honoured
|
|
|
|
|
# it has already run: the tag is set, nothing ever reads it, and no ACL is
|
|
|
|
|
# granted. That was silently the case at 99-, which is why Bazzite ended up with
|
|
|
|
|
# no access at all -- there uaccess is the only mechanism that can work, since
|
|
|
|
|
# plugdev does not exist and usermod cannot add the group (see section 4).
|
|
|
|
|
# systemd keeps its own uaccess rules in 70-uaccess.rules for this reason; 60 is
|
|
|
|
|
# the prefix other projects settled on after hitting the same bug.
|
|
|
|
|
RULE_FILE=/etc/udev/rules.d/60-ch347.rules
|
|
|
|
|
# Written by earlier versions of this script. Removed on sight: a stale copy
|
|
|
|
|
# matching the same device would keep applying its own (wrong) permissions.
|
|
|
|
|
LEGACY_RULE_FILES=(/etc/udev/rules.d/99-ch347.rules /etc/udev/rules.d/70-ch347.rules)
|
2026-08-19 23:00:47 -04:00
|
|
|
|
|
|
|
|
cd "$(dirname "$0")"
|
|
|
|
|
|
|
|
|
|
echo "== 1. Checking for the CH347 adapter =="
|
|
|
|
|
if ! lsusb -d "$VID:$PID" >/dev/null 2>&1; then
|
|
|
|
|
echo "ERROR: no device $VID:$PID found on the USB bus." >&2
|
|
|
|
|
echo " Plug in the Waveshare adapter and make sure it is in Mode 1" >&2
|
|
|
|
|
echo " (UART1+I2C+SPI): DTR1 pulled high, RTS1 pulled low." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
lsusb -d "$VID:$PID"
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "== 2. Installing udev rule =="
|
2026-08-23 18:59:51 -04:00
|
|
|
# Access is granted two ways, because no single mechanism covers every distro:
|
|
|
|
|
#
|
2026-08-23 20:50:56 -04:00
|
|
|
# TAG+="uaccess" systemd-logind puts an ACL on the node for whoever holds
|
|
|
|
|
# the local seat. Needs no group and no logout, and is the
|
|
|
|
|
# only thing that can work unaided on atomic Fedora
|
2026-08-23 18:59:51 -04:00
|
|
|
# (Bazzite, Silverblue, Kinoite) -- see section 4.
|
|
|
|
|
# GROUP=/MODE= the traditional fallback, for ssh sessions and seatless
|
|
|
|
|
# systems where there is no local seat for uaccess to grant.
|
|
|
|
|
#
|
2026-08-23 20:50:56 -04:00
|
|
|
# They go on SEPARATE LINES, which matters more than it looks. udev drops a
|
|
|
|
|
# whole rule line whose GROUP= cannot be resolved, and since systemd 258 that
|
|
|
|
|
# includes any group which exists but is not a *system* group ("Group 'x' is not
|
|
|
|
|
# a system group, ignoring"). With both directives on one line, an unusable
|
|
|
|
|
# group therefore takes the uaccess tag down with it -- losing the fallback
|
|
|
|
|
# costs the primary mechanism too. Split, each stands or falls alone.
|
2026-08-23 18:59:51 -04:00
|
|
|
ACCESS_GROUP=""
|
2026-08-23 20:50:56 -04:00
|
|
|
ACCESS_GROUP_GID=""
|
2026-08-23 18:59:51 -04:00
|
|
|
for g in plugdev dialout; do
|
2026-08-23 20:50:56 -04:00
|
|
|
if gid=$(getent group "$g" | cut -d: -f3) && [[ -n "$gid" ]]; then
|
|
|
|
|
ACCESS_GROUP=$g
|
|
|
|
|
ACCESS_GROUP_GID=$gid
|
|
|
|
|
break
|
|
|
|
|
fi
|
2026-08-23 18:59:51 -04:00
|
|
|
done
|
|
|
|
|
|
2026-08-23 20:50:56 -04:00
|
|
|
MATCH="SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"$VID\", ATTRS{idProduct}==\"$PID\""
|
|
|
|
|
RULE="# Installed by rm3100/setup.sh. Prefix must stay below 73 so that
|
|
|
|
|
# 73-seat-late.rules still sees the uaccess tag; see the comments in setup.sh.
|
|
|
|
|
$MATCH, TAG+=\"uaccess\""
|
|
|
|
|
|
|
|
|
|
if [[ -z "$ACCESS_GROUP" ]]; then
|
|
|
|
|
# The normal case on Fedora and its derivatives: plugdev was removed years
|
|
|
|
|
# ago and dialout is for serial ports. uaccess alone is the supported path.
|
2026-08-23 18:59:51 -04:00
|
|
|
echo "No plugdev or dialout group here; relying on uaccess alone."
|
2026-08-23 20:50:56 -04:00
|
|
|
elif [[ "$ACCESS_GROUP_GID" -ge 1000 ]]; then
|
|
|
|
|
# systemd 258 refuses these outright, so writing the line would achieve
|
|
|
|
|
# nothing while looking like a fallback.
|
|
|
|
|
echo "Group '$ACCESS_GROUP' has GID $ACCESS_GROUP_GID, which systemd 258+"
|
|
|
|
|
echo "rejects as a non-system group; relying on uaccess alone."
|
|
|
|
|
ACCESS_GROUP=""
|
|
|
|
|
else
|
|
|
|
|
RULE="$RULE
|
|
|
|
|
$MATCH, GROUP=\"$ACCESS_GROUP\", MODE=\"0660\""
|
|
|
|
|
echo "Access via uaccess, falling back to group '$ACCESS_GROUP'."
|
2026-08-23 18:59:51 -04:00
|
|
|
fi
|
|
|
|
|
|
2026-08-23 20:50:56 -04:00
|
|
|
STALE=()
|
|
|
|
|
for old in "${LEGACY_RULE_FILES[@]}"; do
|
|
|
|
|
[[ -e "$old" ]] && STALE+=("$old")
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
if [[ -f "$RULE_FILE" ]] && [[ "$(cat "$RULE_FILE")" == "$RULE" ]] &&
|
|
|
|
|
[[ ${#STALE[@]} -eq 0 ]]; then
|
2026-08-19 23:00:47 -04:00
|
|
|
echo "$RULE_FILE already up to date, skipping."
|
|
|
|
|
else
|
|
|
|
|
echo "Writing $RULE_FILE (needs sudo)..."
|
|
|
|
|
printf '%s\n' "$RULE" | sudo tee "$RULE_FILE" >/dev/null
|
2026-08-23 20:50:56 -04:00
|
|
|
if [[ ${#STALE[@]} -gt 0 ]]; then
|
|
|
|
|
echo "Removing superseded ${STALE[*]}"
|
|
|
|
|
sudo rm -f "${STALE[@]}"
|
|
|
|
|
fi
|
2026-08-19 23:00:47 -04:00
|
|
|
sudo udevadm control --reload-rules
|
2026-08-23 18:59:51 -04:00
|
|
|
# --action=add, not the default 'change': the uaccess builtin that sets the
|
|
|
|
|
# ACL only runs on add, so a change event would install the rule without
|
|
|
|
|
# granting anything until the next replug. Matched to this device alone so
|
|
|
|
|
# re-running setup does not re-add every USB device on the system.
|
|
|
|
|
sudo udevadm trigger --action=add --subsystem-match=usb \
|
|
|
|
|
--attr-match=idVendor="$VID" --attr-match=idProduct="$PID"
|
|
|
|
|
# trigger returns before the rule has necessarily been applied.
|
2026-08-19 23:00:47 -04:00
|
|
|
sudo udevadm settle
|
|
|
|
|
echo "Installed."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo
|
2026-08-23 20:50:56 -04:00
|
|
|
echo "== 3. Creating virtualenv and installing dependencies =="
|
|
|
|
|
# pyusb drives the adapter; numpy and matplotlib are needed by every analysis
|
|
|
|
|
# tool (capture/plot/characterize/compare), so installing only pyusb leaves
|
|
|
|
|
# noise_floor_test.sh failing on an ImportError halfway through a capture.
|
|
|
|
|
# pytest runs the suite in tests/, which needs no hardware.
|
|
|
|
|
DEPS=(pyusb numpy matplotlib pytest)
|
2026-08-19 23:00:47 -04:00
|
|
|
if [[ ! -d .venv ]]; then
|
|
|
|
|
python3 -m venv .venv
|
|
|
|
|
echo "Created .venv"
|
|
|
|
|
else
|
|
|
|
|
echo ".venv already exists"
|
|
|
|
|
fi
|
|
|
|
|
./.venv/bin/pip install --quiet --upgrade pip
|
2026-08-23 20:50:56 -04:00
|
|
|
./.venv/bin/pip install --quiet "${DEPS[@]}"
|
|
|
|
|
./.venv/bin/python - <<'EOF'
|
|
|
|
|
import importlib.metadata as md
|
|
|
|
|
for name in ("pyusb", "numpy", "matplotlib", "pytest"):
|
|
|
|
|
print(f" {name} {md.version(name)}")
|
|
|
|
|
EOF
|
2026-08-19 23:00:47 -04:00
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "== 4. Verifying device node permissions =="
|
2026-08-23 18:59:51 -04:00
|
|
|
# The node itself is the ground truth, so nothing above is warned about
|
2026-08-23 20:50:56 -04:00
|
|
|
# speculatively. This also says *which* mechanism granted access, because the
|
|
|
|
|
# two fail in completely different ways and the fix differs accordingly.
|
|
|
|
|
USER_NAME=$(id -un)
|
2026-08-19 23:00:47 -04:00
|
|
|
NODE=$(lsusb -d "$VID:$PID" | head -1 |
|
|
|
|
|
sed -E 's|Bus ([0-9]+) Device ([0-9]+).*|/dev/bus/usb/\1/\2|')
|
|
|
|
|
ls -l "$NODE"
|
2026-08-23 18:59:51 -04:00
|
|
|
getfacl -p "$NODE" 2>/dev/null | grep -E "^user:[^:]+:" || true
|
|
|
|
|
|
2026-08-23 20:50:56 -04:00
|
|
|
# An ACL entry naming this user is the fingerprint of uaccess having run. Its
|
|
|
|
|
# absence is the symptom the 60- prefix exists to prevent.
|
|
|
|
|
has_uaccess_acl() {
|
|
|
|
|
getfacl -p "$NODE" 2>/dev/null | grep -qE "^user:$USER_NAME:[^:]*w"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Exactly what the uaccess builtin requires: an active session on a seat. Asked
|
|
|
|
|
# through the property API rather than by parsing `loginctl list-sessions`,
|
|
|
|
|
# whose columns move between systemd versions.
|
|
|
|
|
has_active_seat_session() {
|
|
|
|
|
command -v loginctl >/dev/null 2>&1 || return 1
|
|
|
|
|
local sessions session
|
|
|
|
|
sessions=$(loginctl show-user "$USER_NAME" --property=Sessions --value 2>/dev/null) || return 1
|
|
|
|
|
for session in $sessions; do
|
|
|
|
|
[[ "$(loginctl show-session "$session" --property=Active --value 2>/dev/null)" == "yes" ]] &&
|
|
|
|
|
[[ -n "$(loginctl show-session "$session" --property=Seat --value 2>/dev/null)" ]] &&
|
|
|
|
|
return 0
|
|
|
|
|
done
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 23:00:47 -04:00
|
|
|
if [[ -w "$NODE" ]]; then
|
2026-08-23 20:50:56 -04:00
|
|
|
if has_uaccess_acl; then
|
|
|
|
|
echo "OK: $NODE is writable by $USER_NAME (uaccess ACL)."
|
|
|
|
|
else
|
|
|
|
|
echo "OK: $NODE is writable by $USER_NAME (group membership)."
|
|
|
|
|
fi
|
2026-08-19 23:00:47 -04:00
|
|
|
else
|
2026-08-23 18:59:51 -04:00
|
|
|
echo "WARNING: $NODE is not writable by $USER_NAME." >&2
|
|
|
|
|
echo >&2
|
2026-08-23 20:50:56 -04:00
|
|
|
echo " 1. Unplug and replug the adapter. The rule is applied when the" >&2
|
|
|
|
|
echo " device appears, so an already-connected node keeps whatever" >&2
|
|
|
|
|
echo " permissions it was given before. This alone is usually enough." >&2
|
2026-08-23 18:59:51 -04:00
|
|
|
echo >&2
|
2026-08-23 20:50:56 -04:00
|
|
|
if has_active_seat_session; then
|
|
|
|
|
echo " 2. You do hold an active local seat, so uaccess should apply." >&2
|
|
|
|
|
echo " If a replug does not fix it, the rules are not loaded:" >&2
|
|
|
|
|
echo " sudo udevadm control --reload-rules && sudo udevadm trigger" >&2
|
|
|
|
|
echo " Bazzite in particular is known not to reload /etc/udev/rules.d" >&2
|
|
|
|
|
echo " when it switches to the final rootfs, so rules can sit inert" >&2
|
|
|
|
|
echo " until that is run once after each boot (ublue-os/bazzite#2516)." >&2
|
|
|
|
|
else
|
|
|
|
|
echo " 2. You have no active local seat session -- this looks like ssh" >&2
|
|
|
|
|
echo " or a headless login. uaccess grants nothing there, by design," >&2
|
|
|
|
|
echo " so the group fallback is the only route." >&2
|
|
|
|
|
fi
|
2026-08-23 18:59:51 -04:00
|
|
|
if [[ -n "$ACCESS_GROUP" ]] &&
|
|
|
|
|
! id -nG | tr ' ' '\n' | grep -qx "$ACCESS_GROUP"; then
|
|
|
|
|
echo >&2
|
|
|
|
|
if [[ -f /run/ostree-booted ]]; then
|
|
|
|
|
# usermod edits /etc/group directly, while getent also sees
|
|
|
|
|
# /usr/lib/group through nss_altfiles. On rpm-ostree systems the
|
|
|
|
|
# group is often only in the latter, so it looks present to every
|
|
|
|
|
# query and still fails to add: "group '$ACCESS_GROUP' does not
|
|
|
|
|
# exist". Copying the line across is what makes usermod agree.
|
2026-08-23 20:50:56 -04:00
|
|
|
echo " 3. This is an rpm-ostree system (Bazzite/Silverblue), where" >&2
|
2026-08-23 18:59:51 -04:00
|
|
|
echo " '$ACCESS_GROUP' may exist only in /usr/lib/group. usermod" >&2
|
|
|
|
|
echo " reads /etc/group alone and will refuse. Copy it over:" >&2
|
|
|
|
|
echo " grep -E '^$ACCESS_GROUP:' /usr/lib/group | sudo tee -a /etc/group" >&2
|
|
|
|
|
echo " then:" >&2
|
|
|
|
|
else
|
2026-08-23 20:50:56 -04:00
|
|
|
echo " 3. Add yourself to the group:" >&2
|
2026-08-23 18:59:51 -04:00
|
|
|
fi
|
|
|
|
|
echo " sudo usermod -aG $ACCESS_GROUP $USER_NAME" >&2
|
|
|
|
|
echo " and log out and back in for it to take effect." >&2
|
2026-08-23 20:50:56 -04:00
|
|
|
elif [[ -z "$ACCESS_GROUP" ]]; then
|
|
|
|
|
echo >&2
|
|
|
|
|
echo " 3. There is no group fallback on this system (no system-group" >&2
|
|
|
|
|
echo " plugdev or dialout), which is normal on Fedora and its" >&2
|
|
|
|
|
echo " derivatives. uaccess is the supported path there." >&2
|
2026-08-23 18:59:51 -04:00
|
|
|
fi
|
2026-08-19 23:00:47 -04:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo
|
2026-08-23 20:50:56 -04:00
|
|
|
echo "Setup complete. Check the wiring end to end with:"
|
|
|
|
|
echo " ./.venv/bin/python diagnose-comms.py"
|
|
|
|
|
echo "then log:"
|
2026-08-19 23:00:47 -04:00
|
|
|
echo " ./.venv/bin/python logger.py --duration 10"
|
2026-08-23 20:50:56 -04:00
|
|
|
echo "The test suite needs no hardware:"
|
|
|
|
|
echo " ./.venv/bin/python -m pytest"
|