diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8f383d0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,23 @@ +[submodule "pi_image/pi-gen"] + path = pi_image/pi-gen + url = https://github.com/RPi-Distro/pi-gen.git + branch = master + shallow = true +[submodule "pi_image/pi-gen-arm64"] + path = pi_image/pi-gen-arm64 + url = https://github.com/RPi-Distro/pi-gen.git + branch = arm64 + shallow = true +[submodule "pi_image/stage-usb4vc-deps/00-dependencies/files/xpadneo"] + path = pi_image/stage-usb4vc-deps/00-dependencies/files/xpadneo + url = https://github.com/atar-axis/xpadneo.git + branch = release/v0.9 + shallow = true +[submodule "pi_image/stage-usb4vc-deps/00-dependencies/files/dkms-hid-nintendo"] + path = pi_image/stage-usb4vc-deps/00-dependencies/files/dkms-hid-nintendo + url = https://github.com/nicman23/dkms-hid-nintendo.git + shallow = true +[submodule "pi_image/stage-usb4vc-deps/00-dependencies/files/joycond"] + path = pi_image/stage-usb4vc-deps/00-dependencies/files/joycond + url = https://github.com/DanielOgorchock/joycond.git + shallow = true diff --git a/pi_image/build-docker.sh b/pi_image/build-docker.sh new file mode 100755 index 0000000..622b937 --- /dev/null +++ b/pi_image/build-docker.sh @@ -0,0 +1,63 @@ +#!/bin/bash -eu +# see: https://github.com/RPi-Distro/pi-gen for details on this process + +DEPS_STAGE="stage-usb4vc-deps" +APP_STAGE="stage-usb4vc-app" + +BUILD_32BIT=${BUILD_32BIT:-1} +BUILD_64BIT=${BUILD_64BIT:-0} +SKIP_PI_STAGES=${SKIP_PI_STAGES:-0} +SKIP_DEPS_STAGE=${SKIP_DEPS_STAGE:-0} + +setup_stages() { + # prevent "lite" image output + touch ./$1/stage2/SKIP_IMAGES + + # clean up skips + rm -f ./$1/stage0/SKIP ./$1/stage1/SKIP ./$1/stage2/SKIP ./$DEPS_STAGE/SKIP + + if [ $SKIP_PI_STAGES = 1 ]; then + touch ./$1/stage0/SKIP ./$1/stage1/SKIP ./$1/stage2/SKIP + fi + + if [ $SKIP_DEPS_STAGE = 1 ]; then + touch ./$DEPS_STAGE/SKIP + fi +} + +# prevent "lite" image output +setup_stages pi-gen +setup_stages pi-gen-arm64 + +# setup for the pi-gen build-docker script +CONTAINER_NAME=${CONTAINER_NAME:-pigen_usb4vc_work} + +DEPS_STAGE_PATH=$(realpath -s "./${DEPS_STAGE}" || realpath "./${DEPS_STAGE}") +APP_STAGE_PATH=$(realpath -s "./${APP_STAGE}" || realpath "./${APP_STAGE}") +USERPROGRAM_PATH=$(realpath -s "../user_program" || realpath "../user_program") + +# paths relative to inside pi-gen +CONFIG_FILE=${CONFIG_FILE:-"../config"} +PIGEN_DOCKER_OPTS=${PIGEN_DOCKER_OPTS:-""} +PIGEN_DOCKER_OPTS="${PIGEN_DOCKER_OPTS} --volume ${DEPS_STAGE_PATH}:/pi-gen/${DEPS_STAGE}" +PIGEN_DOCKER_OPTS="${PIGEN_DOCKER_OPTS} --volume ${APP_STAGE_PATH}:/pi-gen/${APP_STAGE}" +PIGEN_DOCKER_OPTS="${PIGEN_DOCKER_OPTS} --volume ${USERPROGRAM_PATH}:/pi-gen/${APP_STAGE}/00-user-program/files/rpi_app:ro" + +export CONTAINER_NAME +export PIGEN_DOCKER_OPTS + +if [ $BUILD_32BIT = 1 ]; then + echo "Starting pi-gen..." + pushd ./pi-gen && ./build-docker.sh -c $CONFIG_FILE && popd || popd && exit + echo "pi-gen complete" +fi + +if [ $BUILD_64BIT = 1 ]; then + echo "Starting pi-gen for arm64..." + export CONTAINER_NAME="arm64_${CONTAINER_NAME}" + export PIGEN_DOCKER_OPTS=${PIGEN_DOCKER_OPTS:-" -e IS_PIGEN_ARM64=1"} + pushd ./pi-gen-arm64 && ./build-docker.sh -c $CONFIG_FILE && popd || popd && exit + echo "pi-gen arm64 complete" +fi + +echo "All images should be created, check the deploy folder of each pi-gen." diff --git a/pi_image/config b/pi_image/config new file mode 100644 index 0000000..1d99b82 --- /dev/null +++ b/pi_image/config @@ -0,0 +1,15 @@ +IMG_NAME=raspios-bookworm +FIRST_USER_NAME=pi +FIRST_USER_PASS=usb4vc +TARGET_HOSTNAME=usb4vc +KEYBOARD_KEYMAP=us +KEYBOARD_LAYOUT="English (US)" +TIMEZONE_DEFAULT="Etc/UTC" +LOCALE_DEFAULT="en_US.UTF-8" +ENABLE_SSH=1 +DISABLE_FIRST_BOOT_USER_RENAME=1 +STAGE_LIST="stage0 stage1 stage2 stage-usb4vc-deps stage-usb4vc-app" + +# optional +# APT_PROXY=http://172.17.0.1:3142 + diff --git a/pi_image/pi-gen b/pi_image/pi-gen new file mode 160000 index 0000000..d966897 --- /dev/null +++ b/pi_image/pi-gen @@ -0,0 +1 @@ +Subproject commit d9668973950853ce3dc026246cad24fcbb5dbcfa diff --git a/pi_image/pi-gen-arm64 b/pi_image/pi-gen-arm64 new file mode 160000 index 0000000..78444ea --- /dev/null +++ b/pi_image/pi-gen-arm64 @@ -0,0 +1 @@ +Subproject commit 78444eaf0709b0308fcf71cd2834d857c7ad8a3f diff --git a/pi_image/pi_image_creation.md b/pi_image/pi_image_creation.md new file mode 100644 index 0000000..cc17aa7 --- /dev/null +++ b/pi_image/pi_image_creation.md @@ -0,0 +1,142 @@ +# Raspberry Pi Disk Image +#### Building a Raspberry Pi disk image containing usb4vc using pi-gen. + +This process currently relies on Docker and pi-gen. Docker is the easiest method for using pi-gen as it requires very specific versions of ubuntu or debian to run standalone. + +Please read through the pi-gen documentation to have a good understanding of how to setup the build environment and what stages / config mean: +- https://github.com/RPi-Distro/pi-gen + +## Setup +- First the submodules for pi-gen and the usb4vc-deps must be cloned: + ```bash + git submodule update --init --recursive + ``` + These are shallow clones by default, so to switch branch (e.g. to bullseye from bookworm) you will need to perform a fetch. +- Ensure docker is installed and accessible from the command line. Linux hosts / WSL should work fine. + https://docs.docker.com/engine/install/ + +## Overview +- `pi-gen`: A clone of pi-gen for armhf / armv7 (32bit ARM) +- `pi-gen-arm64`: A clone of pi-gen for arm64 (64bit ARM, newer Pi's) +- `stage-usb4vc-deps`: A pi-gen stage containing all software dependencies for usb4vc. +- `stage-usb4vc-app`: A pi-gen stage containing the scripts to install usb4vc inside the image. +- `config`: The pi-gen config used for the build. +- `build-docker.sh`: Helper script to automatically call pi-gen(-arm64)'s build-docker.sh with the correct arguments. + +## Quick Usage +Once docker is installed and the submodules have been updated / cloned, you can run the build script. +- To build just the 32bit image (the default): + ```bash + ./build-docker.sh + ``` +- To build 32bit and 64bit images: + ```bash + BUILD_32BIT=1 BUILD_64BIT=1 ./build-docker.sh + ``` +- To build just the 64bit image: + ```bash + BUILD_32BIT=0 BUILD_64BIT=1 ./build-docker.sh + ``` +- To preserve the container to speed up iteration: + ```bash + BUILD_32BIT=1 BUILD_64BIT=1 PRESERVE_CONTAINER=1 ./build-docker.sh + ``` +- To continue and retry a failed build: + ```bash + BUILD_32BIT=1 BUILD_64BIT=1 PRESERVE_CONTAINER=1 CONTINUE=1 ./build-docker.sh + ``` + +Outputs can be found in: +- `pi-gen/deploy` +- `pi-gen-arm64/deploy` + +### APT Proxy +The APT proxy can be used to cache packages locally to speed up image building from scratch. See `APT_PROXY` under `Config` in pi-gen: https://github.com/RPi-Distro/pi-gen#config + +You can then uncomment / modify the line in the `config` file. + +## Script Configuration +All configuration for `build-docker.sh` is controlled via environment variables. +- `BUILD_32BIT` (Default: `1`) + + Toggle the pi-gen build for a 32bit image. +- `BUILD_64BIT` (Default: `0`) + + Toggle the pi-gen-arm64 build for a 64bit image. +- `SKIP_PI_STAGES` (Default: `0`) + + Toggle skipping the base pi image stages (stage0, stage1, stage2). It will automatically create the `SKIP` files in each stage directory. This can help speed up image build time when combines with `PRESERVE_CONTAINER=1` and `CONTINUE=1` when something goes wrong in the `stage-usb4vc-deps` or `stage-usb4vc-app` stage. +- `SKIP_DEPS_STAGE` (Default: `0`) + + Toggle skipping the `stage-usb4vc-deps` stage. Similar to `SKIP_PI_STAGES` but will skip the dependencies stage specifically (much faster if you already have a preserved base of stage 0-2 with deps you just want to update the app inside). + +All environment variables are passed through to the underlying pi-gen build-docker.sh, but some are manipulated by the script here: +- `CONFIG_FILE` (Default `"../config"`) + + Path to the config file (relative from inside pi-gen as it mounts it to the container itself). +- `CONTAINER_NAME` (Default `"pigen_usb4vc_work"`) + + The name of the container used for the pi-gen work. When `BUILD_64BIT` is `1`, this container name will have `arm64_` prepended to the name as the pi-gen build-docker checks if any container matches the container name (meaning it throws an error if you try to preserve both the 32bit and 64bit containers). +- `PIGEN_DOCKER_OPTS` (Default `""`) + + This passes extra options to the `docker run` call for pi-gen. By default this is empty, but the script always adds the volumes for the usb4vc stages and the user_program. You can use this to specify any additonal docker options. + +The script mounts some volumes in docker for easy iteration outside the container environment: +- `./stage-usb4vc-deps:/pi-gen/stage-usb4vc-deps` +- `./stage-usb4vc-app:/pi-gen/stage-usb4vc-app` +- `../user_program:/pi-gen/stage-usb4vc-app/00-user-program/files/rpi_app` + +## Stages +The build stages run in this order by default (specified in `config`): +- stage0 +- stage1 +- stage2 (this is a "lite" rpi-image) +- stage-usb4vc-deps +- stage-usb4vc-app (this is where the final image is exported) + +### `stage-usb4vc-deps` +This stage installs all software dependencies for usb4vc. See `00-packages` for exact list, but in general: +- python3 +- stm32flash +- i2c-tools +- dfutil +- etc.. + +A virtualenv is created for usb4vc in `/opt/usb4vc/venv`, this is a requirement in newer raspios distributions for installing non api provided python packages. +- Creates python3 venv in `/opt/usb4vc/venv`. +- Installs `evdev`, `spidev`, `serial` and `luma.oled`. +- Purges pip cache to save disk space. + +It also compiles and installs (see `00-run.sh`): +- xpadneo + - Only the source code for usb4vc to compile on startup in `/opt/xpadneo`. +- joycond +- dkms-hid-nintendo + - For this module, the script must find all kernels inside the image and install dkms for each (to ensure the module works on every pi the image supports). + +After the dependencies are installed, this stage performs some system tweaks specific to usb4vc. See `01-tweaks` and `01-run.sh`: +- Installs uniput and udisks udev rules with a script started by systemd (`usb-automount.service` and `udev_usb_monitor.sh`) to monitor for newly attached USB storage devices and automount them to `/media`. This replaces `usbmount` from old Debian systems which is no longer maintained. +- Attempts to disable boot wait in `config.txt`. +- Disables the interactive serial console and enables hardware serial. +- Enables SPI and I2C. +- Disables the boot splash. +- Sets the boot delay to 0. +- Disables the ctrl-alt-del systemd target. +- Enables the uhid kernel module (for xpadneo). +- Masks the userconf systemd service. + - This may not be necessary but in some cases when connecting a keyboard it was constantly running a background process to configure the key map. +- If the image being built is 32bit: + - Sets arm_64bit to 0 in `config.txt`. This is to prevent the 64bit kernel from loading in the 32bit image, this is the default behaviour with modern rpi images and it breaks compiling xpadneo as there are no cross compilation kernel headers available for the system. + +### `stage-usb4vc-app` +This stage copies `user_program` into `/opt/usb4vc/rpi_app` and is also the stage where `EXPORT_IMAGE` is specified. +- Creates the `rpi_app`, `config`, `firmware` and `temp` directories under `/opt/usb4vc`. +- Installs the `usb4vc.service` systemd service to run usb4vc in the background on boot. + - This service configures the environment variables to override the compatability for `/home/pi` in usb4vc, and starts usb4vc using the virtualenv. + - Logs for usb4vc are available via `journalctl -u usb4vc.service`. +- Copies across some helper scripts and a note about the debug log. +- Copies `*.py` and `*.ttf` from `user_program` to `rpi_app`. +- Enables the `usb4vc.service` systemd service. + +## Outputs +All outputs can be found in their respective `pi-gen`'s `deploy` folder. Currently 64bit and 32bit images have the same output name, but they output to different folders. The 64bit one can be renamed for upload. \ No newline at end of file diff --git a/pi_image/stage-usb4vc-app/00-user-program/01-run.sh b/pi_image/stage-usb4vc-app/00-user-program/01-run.sh new file mode 100755 index 0000000..ddce459 --- /dev/null +++ b/pi_image/stage-usb4vc-app/00-user-program/01-run.sh @@ -0,0 +1,19 @@ +#!/bin/bash -e + +install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/opt/usb4vc/rpi_app" +install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/opt/usb4vc/config" +install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/opt/usb4vc/firmware" +install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/opt/usb4vc/temp" + +install -v -m 664 "files/usb4vc.service" "${ROOTFS_DIR}/etc/systemd/system/usb4vc.service" +install -v -o 1000 -g 1000 -m 755 "files/start_usb4vc_manual.sh" "${ROOTFS_DIR}/opt/usb4vc/start_usb4vc_manual.sh" +install -v -o 1000 -g 1000 -m 644 "files/usb4vc_debug_log.txt" "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/usb4vc_debug_log.txt" + +rsync -r --include="*.py" --include="*.ttf" --exclude="*" \ + "files/rpi_app/" "${ROOTFS_DIR}/opt/usb4vc/rpi_app/" + +chown -R 1000:1000 "${ROOTFS_DIR}/opt/usb4vc/rpi_app" + +on_chroot << EOF +systemctl enable usb4vc.service +EOF \ No newline at end of file diff --git a/pi_image/stage-usb4vc-app/00-user-program/files/start_usb4vc_manual.sh b/pi_image/stage-usb4vc-app/00-user-program/files/start_usb4vc_manual.sh new file mode 100755 index 0000000..ebb8a5d --- /dev/null +++ b/pi_image/stage-usb4vc-app/00-user-program/files/start_usb4vc_manual.sh @@ -0,0 +1,14 @@ +#!/bin/sh -e + +if [ $(id -u) -ne 0 ]; then + printf "usb4vc must be run as root. Try 'sudo start_usb4vc_manual.sh'\n" + exit 1 +fi + +pushd /opt/usb4vc/rpi_app +export USB4VC_IS_SYSTEMD=0 +export USB4VC_INSTALL_PATH=/opt/usb4vc +export XPADNEO_SOURCE_PATH=/opt/xpadneo + +/opt/usb4vc/venv/bin/python3 -u usb4vc_main.py || echo "usb4vc did not exit cleanly: $?" +popd diff --git a/pi_image/stage-usb4vc-app/00-user-program/files/usb4vc.service b/pi_image/stage-usb4vc-app/00-user-program/files/usb4vc.service new file mode 100644 index 0000000..8d73847 --- /dev/null +++ b/pi_image/stage-usb4vc-app/00-user-program/files/usb4vc.service @@ -0,0 +1,19 @@ +[Unit] +Description=usb4vc background service +After=multi-user.target + +[Service] +User=root +WorkingDirectory=/opt/usb4vc/rpi_app +Environment=USB4VC_IS_SYSTEMD=1 +Environment=USB4VC_INSTALL_PATH=/opt/usb4vc +Environment=XPADNEO_SOURCE_PATH=/opt/xpadneo +ExecStart=/opt/usb4vc/venv/bin/python3 -u usb4vc_main.py +Restart=always +RestartSec=1 + +# 169 << 8 ? (from keep_alive.py) +RestartPreventExitStatus=43264 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/pi_image/stage-usb4vc-app/00-user-program/files/usb4vc_debug_log.txt b/pi_image/stage-usb4vc-app/00-user-program/files/usb4vc_debug_log.txt new file mode 100644 index 0000000..c8cca8d --- /dev/null +++ b/pi_image/stage-usb4vc-app/00-user-program/files/usb4vc_debug_log.txt @@ -0,0 +1,6 @@ +Looking for the debug log after a base pi image update? +The usb4vc service has moved to systemd and its install directory to /opt/usb4vc + +Use journalctl to get logs: +sudo journalctl -u usb4vc (-f can be added to follow the log) + diff --git a/pi_image/stage-usb4vc-app/EXPORT_IMAGE b/pi_image/stage-usb4vc-app/EXPORT_IMAGE new file mode 100644 index 0000000..fca9d5f --- /dev/null +++ b/pi_image/stage-usb4vc-app/EXPORT_IMAGE @@ -0,0 +1,7 @@ +IMG_SUFFIX="-usb4vc" +if [ "${IS_PIGEN_ARM64}" = "1" ]; then + export IMG_SUFFIX="${IMG_SUFFIX}-arm64" +fi +if [ "${USE_QEMU}" = "1" ]; then + export IMG_SUFFIX="${IMG_SUFFIX}-qemu" +fi diff --git a/pi_image/stage-usb4vc-app/prerun.sh b/pi_image/stage-usb4vc-app/prerun.sh new file mode 100755 index 0000000..9acd13c --- /dev/null +++ b/pi_image/stage-usb4vc-app/prerun.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +if [ ! -d "${ROOTFS_DIR}" ]; then + copy_previous +fi diff --git a/pi_image/stage-usb4vc-deps/00-dependencies/00-packages b/pi_image/stage-usb4vc-deps/00-dependencies/00-packages new file mode 100644 index 0000000..bba36eb --- /dev/null +++ b/pi_image/stage-usb4vc-deps/00-dependencies/00-packages @@ -0,0 +1,19 @@ +git +dkms +cmake +i2c-tools +stm32flash +dfu-util +libjpeg-dev +zlib1g-dev +libfreetype6-dev +liblcms2-dev +libopenjp2-7 +libtiff-dev +libudev-dev +libevdev-dev +raspberrypi-kernel-headers +udisks2 +python3-pip +python3-pil +python3-venv diff --git a/pi_image/stage-usb4vc-deps/00-dependencies/01-run.sh b/pi_image/stage-usb4vc-deps/00-dependencies/01-run.sh new file mode 100755 index 0000000..e2c31b8 --- /dev/null +++ b/pi_image/stage-usb4vc-deps/00-dependencies/01-run.sh @@ -0,0 +1,46 @@ +#!/bin/bash -e + +install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/opt/usb4vc" +install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/opt/xpadneo" +install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/opt/dkms-hid-nintendo" +install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/opt/joycond" + +rsync -lr --exclude=.git --chown=1000:1000 "files/xpadneo/" "${ROOTFS_DIR}/opt/xpadneo/" +rsync -lr --exclude=.git --chown=1000:1000 "files/dkms-hid-nintendo/" "${ROOTFS_DIR}/opt/dkms-hid-nintendo/" +rsync -lr --exclude=.git --chown=1000:1000 "files/joycond/" "${ROOTFS_DIR}/opt/joycond/" + +# create venv for usb4vc and install luma.oled +on_chroot << EOF + python3 -m venv --system-site-packages /opt/usb4vc/venv + + source /opt/usb4vc/venv/bin/activate + pip install evdev spidev serial luma.oled + pip cache purge || echo "pip cache purge did nothing" + deactivate + + chown -R 1000:1000 /opt/usb4vc/venv +EOF + +# build and install nintendo hid, joycond +on_chroot << EOF + pushd /opt/dkms-hid-nintendo + dkms remove -m nintendo -v 3.2 --all || echo "" + dkms add . + + # find the kernel version in the rootfs for dkms to target + KERNEL_VERSIONS=(\$(ls -1 "/lib/modules/")) + + for kernel in "\${KERNEL_VERSIONS[@]}"; do + if [ -d "/usr/src/linux-headers-\$kernel" ]; then + dkms build nintendo -v 3.2 -k \$kernel + dkms install nintendo -v 3.2 -k \$kernel + fi + done + popd + + pushd /opt/joycond + cmake . + make install + systemctl enable joycond + popd +EOF diff --git a/pi_image/stage-usb4vc-deps/00-dependencies/files/dkms-hid-nintendo b/pi_image/stage-usb4vc-deps/00-dependencies/files/dkms-hid-nintendo new file mode 160000 index 0000000..2712136 --- /dev/null +++ b/pi_image/stage-usb4vc-deps/00-dependencies/files/dkms-hid-nintendo @@ -0,0 +1 @@ +Subproject commit 2712136b19eed75bff01c1a6ffe2a23daf78a7bb diff --git a/pi_image/stage-usb4vc-deps/00-dependencies/files/joycond b/pi_image/stage-usb4vc-deps/00-dependencies/files/joycond new file mode 160000 index 0000000..cdec328 --- /dev/null +++ b/pi_image/stage-usb4vc-deps/00-dependencies/files/joycond @@ -0,0 +1 @@ +Subproject commit cdec32865c6093bd4761326ea461aaa2fcf7d1b4 diff --git a/pi_image/stage-usb4vc-deps/00-dependencies/files/xpadneo b/pi_image/stage-usb4vc-deps/00-dependencies/files/xpadneo new file mode 160000 index 0000000..d245321 --- /dev/null +++ b/pi_image/stage-usb4vc-deps/00-dependencies/files/xpadneo @@ -0,0 +1 @@ +Subproject commit d2453218678cd5069be0c51f331593b98ace6f62 diff --git a/pi_image/stage-usb4vc-deps/01-tweaks/01-run.sh b/pi_image/stage-usb4vc-deps/01-tweaks/01-run.sh new file mode 100755 index 0000000..a1d07dd --- /dev/null +++ b/pi_image/stage-usb4vc-deps/01-tweaks/01-run.sh @@ -0,0 +1,75 @@ +#!/bin/bash -e + +install -v -m 644 "files/40-uinput.rules" "${ROOTFS_DIR}/etc/udev/rules.d/" +install -v -m 644 "files/99-udisks2.rules" "${ROOTFS_DIR}/etc/udev/rules.d/" + +# tmpfiles conf to clean /media on boot if stale mounts exist +install -v -m 644 "files/tmpfiles_media.conf" "${ROOTFS_DIR}/etc/tmpfiles.d/media.conf" +install -v -m 755 "files/udev_usb_monitor.sh" "${ROOTFS_DIR}/opt/udev_usb_monitor.sh" +install -v -m 664 "files/usb-automount.service" "${ROOTFS_DIR}/etc/systemd/system/usb-automount.service" + +# Disable splash, waiting for network, interactive logon serial +# Enable spi and i2c +# (raspi config 1 or 0 is opposite to what you would expect, dialogue boxes on "Yes" return 0) +on_chroot << EOF + do_nothing() { + echo "" + } + + alias modprobe=do_nothing + alias dtparam=do_nothing + + if [ -e /boot/firmware/config.txt ] ; then + FIRMWARE=/firmware + else + FIRMWARE= + fi + CONFIG=/boot\${FIRMWARE}/config.txt + + echo "boot config location: \$CONFIG" + + echo "disabling boot wait" + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint do_boot_wait 1 || echo "disabling boot wait not supported on this raspi-config version" + + echo "disabling interactive serial but enabling UART" + { + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint do_serial_cons 1; + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint do_serial_hw 0; + } || { + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint do_serial 2; + } + + echo "enabling spi (ignore configfs, modprobe errors)" + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint do_spi 0 + + echo "enabling i2c (ignore configfs, modprobe errors)" + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint do_i2c 0 + + echo "disabling splash" + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint set_config_var disable_splash 1 \$CONFIG + + echo "setting boot delay to 0" + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint set_config_var boot_delay 0 \$CONFIG + + echo "masking ctrl-alt-drl systemd target" + systemctl mask ctrl-alt-del.target + + echo "masking userconf systemd service" + systemctl mask userconfig.service + + echo "enabling the usb automount service" + systemctl enable usb-automount.service + + if ! [ \`getconf LONG_BIT\` = "64" ]; then + echo "disabling arm_64bit in boot config" + SUDO_USER="${FIRST_USER_NAME}" raspi-config nonint set_config_var arm_64bit 0 \$CONFIG + fi + + echo "enabling uhid module" + if ! grep -q "uhid" /etc/modules; then + echo 'uhid' | tee -a /etc/modules + fi + + unalias modprobe + unalias dtparam +EOF diff --git a/pi_image/stage-usb4vc-deps/01-tweaks/files/40-uinput.rules b/pi_image/stage-usb4vc-deps/01-tweaks/files/40-uinput.rules new file mode 100644 index 0000000..63aa011 --- /dev/null +++ b/pi_image/stage-usb4vc-deps/01-tweaks/files/40-uinput.rules @@ -0,0 +1,2 @@ +# allow input group access to uinput +SUBSYSTEM=="misc", KERNEL=="uinput", MODE="0660", GROUP="input" diff --git a/pi_image/stage-usb4vc-deps/01-tweaks/files/99-udisks2.rules b/pi_image/stage-usb4vc-deps/01-tweaks/files/99-udisks2.rules new file mode 100644 index 0000000..04d3940 --- /dev/null +++ b/pi_image/stage-usb4vc-deps/01-tweaks/files/99-udisks2.rules @@ -0,0 +1,5 @@ +# UDISKS_FILESYSTEM_SHARED +# ==1: mount filesystem to a shared directory (/media/VolumeName) +# ==0: mount filesystem to a private directory (/run/media/$USER/VolumeName) +# See udisks(8) +ENV{ID_FS_USAGE}=="filesystem|other|crypto", ENV{ID_USB_DRIVER}=="usb-storage", ENV{UDISKS_FILESYSTEM_SHARED}="1" \ No newline at end of file diff --git a/pi_image/stage-usb4vc-deps/01-tweaks/files/tmpfiles_media.conf b/pi_image/stage-usb4vc-deps/01-tweaks/files/tmpfiles_media.conf new file mode 100644 index 0000000..bb52bfc --- /dev/null +++ b/pi_image/stage-usb4vc-deps/01-tweaks/files/tmpfiles_media.conf @@ -0,0 +1 @@ +D /media 0755 root root 0 - \ No newline at end of file diff --git a/pi_image/stage-usb4vc-deps/01-tweaks/files/udev_usb_monitor.sh b/pi_image/stage-usb4vc-deps/01-tweaks/files/udev_usb_monitor.sh new file mode 100755 index 0000000..bcc50ec --- /dev/null +++ b/pi_image/stage-usb4vc-deps/01-tweaks/files/udev_usb_monitor.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +pathtoname() { + echo "$1" | awk -v FS== '/DEVNAME/ {print $2}' +} + +pathtodevtype() { + echo "$1" | awk -v FS== '/DEVTYPE/ {print $2}' +} + +pathtodriver() { + echo "$1" | awk -v FS== '/ID_USB_DRIVER/ {print $2}' +} + +stdbuf -oL -- udevadm monitor --udev -s block | while read -r -- _ _ event devpath _; do + if [ "$event" = add ]; then + devinfo=$(udevadm info -p /sys/"$devpath") + devname=$(pathtoname "$devinfo") + devtype=$(pathtodevtype "$devinfo") + devdriver=$(pathtodriver "$devinfo") + + if [ "$devdriver" = usb-storage -a "$devtype" = partition ]; then + echo "Mounting $devdriver $devtype $devname" + udisksctl mount --block-device "$devname" --no-user-interaction + fi + fi +done diff --git a/pi_image/stage-usb4vc-deps/01-tweaks/files/usb-automount.service b/pi_image/stage-usb4vc-deps/01-tweaks/files/usb-automount.service new file mode 100644 index 0000000..38d72fb --- /dev/null +++ b/pi_image/stage-usb4vc-deps/01-tweaks/files/usb-automount.service @@ -0,0 +1,12 @@ +[Unit] +Description=udev monitor for USB auto mount +After=multi-user.target + +[Service] +User=root +ExecStart=/opt/udev_usb_monitor.sh +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/pi_image/stage-usb4vc-deps/SKIP b/pi_image/stage-usb4vc-deps/SKIP new file mode 100644 index 0000000..e69de29 diff --git a/pi_image/stage-usb4vc-deps/prerun.sh b/pi_image/stage-usb4vc-deps/prerun.sh new file mode 100755 index 0000000..9acd13c --- /dev/null +++ b/pi_image/stage-usb4vc-deps/prerun.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +if [ ! -d "${ROOTFS_DIR}" ]; then + copy_previous +fi diff --git a/user_program/keep_alive.py b/user_program/keep_alive.py index 030d39d..adcb381 100644 --- a/user_program/keep_alive.py +++ b/user_program/keep_alive.py @@ -1,6 +1,7 @@ import os import time +# original keep_alive for compatibility with existing image rc.local while 1: exit_code = os.system("cd /home/pi/usb4vc/rpi_app; python3 -u usb4vc_main.py") >> 8 print("App died! Exit code:", exit_code) diff --git a/user_program/sync.sh b/user_program/sync.sh index 74684ba..4b66299 100644 --- a/user_program/sync.sh +++ b/user_program/sync.sh @@ -2,7 +2,7 @@ # sh sync.sh; ssh -t pi@192.168.1.62 "pkill python3;cd ~/usb4vc/rpi_app;python3 usb4vc_main.py" -scp ./* pi@192.168.1.62:~/usb4vc/rpi_app +scp ./* pi@192.168.1.62:/opt/usb4vc/rpi_app # ssh -t pi@192.168.1.60 "pkill python3;cd ~/usb4vc/rpi_app;python3 usb4vc_main.py" # ssh -t pi@192.168.1.60 "pkill python3;cd ~/usb4vc/rpi_app;python3 usb4vc_check_update.py" diff --git a/user_program/usb4vc_check_update.py b/user_program/usb4vc_check_update.py index 3290d38..e246381 100644 --- a/user_program/usb4vc_check_update.py +++ b/user_program/usb4vc_check_update.py @@ -7,16 +7,18 @@ import zipfile import shutil -from usb4vc_shared import RPI_APP_VERSION_TUPLE -from usb4vc_shared import this_app_dir_path -from usb4vc_shared import config_dir_path -from usb4vc_shared import firmware_dir_path -from usb4vc_shared import temp_dir_path -from usb4vc_shared import ensure_dir -from usb4vc_shared import i2c_bootloader_pbid -from usb4vc_shared import usb_bootloader_pbid +from usb4vc_shared import ( + RPI_APP_VERSION_TUPLE, + this_app_dir_path, + firmware_dir_path, + usb4vc_releases_url, + firmware_releases_url, + firmware_download_url_template, + usb_bootloader_pbid, + temp_dir_path, + ensure_dir +) -usb4vc_release_url = "https://api.github.com/repos/dekuNukem/usb4vc/releases/latest" def is_internet_available(): try: @@ -33,7 +35,7 @@ def get_remote_tag_version(): try: if is_internet_available() is False: return 1, 'Internet Unavailable' - result_dict = json.loads(urllib.request.urlopen(usb4vc_release_url).read()) + result_dict = json.loads(urllib.request.urlopen(usb4vc_releases_url).read()) return 0, versiontuple(result_dict['tag_name']) except Exception as e: return 2, f'exception: {e}' @@ -47,7 +49,7 @@ def download_latest_usb4vc_release(save_path): try: if is_internet_available() is False: return 1, 'Internet Unavailable' - result_dict = json.loads(urllib.request.urlopen(usb4vc_release_url).read()) + result_dict = json.loads(urllib.request.urlopen(usb4vc_releases_url).read()) header = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',} for item in result_dict['assets']: if item['name'].lower().startswith('usb4vc_src') and item['name'].lower().endswith('.zip'): @@ -93,18 +95,20 @@ def update(temp_path): return 4, 'Too few files' except Exception as e: return 5, f'Unknown error: {e}' + + current_dir_owner = os.stat(this_app_dir_path) os.system(f'rm -rfv {os.path.join(this_app_dir_path, "*")}') os.system(f'cp -fv {os.path.join(src_code_path, "*")} {this_app_dir_path}') - os.system(f'chown pi {os.path.join(this_app_dir_path, "*")}') # change owner from root back to pi for easy editing - return 0, 'Success' -firmware_url = 'https://api.github.com/repos/dekuNukem/USB4VC/contents/firmware/releases?ref=master' + # change owner from root back to original user for easy editing + os.system(f'chown {current_dir_owner.st_uid}:{current_dir_owner.st_gid} {os.path.join(this_app_dir_path, "*")}') + return 0, 'Success' # version number most recent to least recent # dont forget to check extension def get_firmware_list(pcard_id): try: - file_list = json.loads(urllib.request.urlopen(firmware_url).read()) + file_list = json.loads(urllib.request.urlopen(firmware_releases_url).read()) fw_list = [x['name'] for x in file_list if 'name' in x and 'type' in x and x['type'] == 'file'] fw_list = [d for d in fw_list if d.startswith('PBFW') and f"PBID{pcard_id}" in d] fw_list.sort(key=lambda s: list(map(int, s.lower().split('_v')[1].split('.')[0].replace('_', '.').split('.'))), reverse=True) @@ -122,7 +126,7 @@ def download_latest_firmware(pcard_id): if len(fw_list) == 0: return 1 fw_filename = fw_list[0] - fw_download_url = f"https://github.com/dekuNukem/USB4VC/raw/master/firmware/releases/{fw_filename}" + fw_download_url = firmware_download_url_template.format(fw_filename = fw_filename) ensure_dir(firmware_dir_path) os.system(f'rm -rfv {os.path.join(firmware_dir_path, "*")}') print("downloading", fw_download_url) diff --git a/user_program/usb4vc_main.py b/user_program/usb4vc_main.py index 2038555..671ca07 100644 --- a/user_program/usb4vc_main.py +++ b/user_program/usb4vc_main.py @@ -7,8 +7,13 @@ import usb4vc_ui import subprocess +from usb4vc_shared import xpadneo_source_path, rpi_model_save_file_path + # usb4vc_ui.reset_pboard() +def get_kernel_version(): + return int(subprocess.getoutput("uname -r | awk -F. '{ printf \"%03d%03d\",$1,$2 }'").strip()) + def get_current_rpi_model(): current_model = 'Unknown' try: @@ -17,12 +22,16 @@ def get_current_rpi_model(): print('get_current_rpi_model:', e) return current_model -rpi_model_save_file = '/home/pi/rpi_model.txt' - def get_stored_rpi_model(): stored_model = 'Unknown' + + # migrate rpi_model.txt from old location for compat + old_rpi_model_save_file_path = "/home/pi/rpi_model.txt" + if os.path.exists(old_rpi_model_save_file_path): + os.system(f"mv {old_rpi_model_save_file_path} {rpi_model_save_file_path}") + try: - with open(rpi_model_save_file, 'r') as file: + with open(rpi_model_save_file_path, 'r') as file: stored_model = file.read().replace('\n', '').replace('\r', '').strip() except Exception as e: print('get_stored_rpi_model:', e) @@ -30,7 +39,7 @@ def get_stored_rpi_model(): def save_rpi_model(model_str): try: - with open(rpi_model_save_file, 'w') as file: + with open(rpi_model_save_file_path, 'w') as file: file.write(model_str) except Exception as e: print('save_rpi_model:', e) @@ -47,9 +56,9 @@ def check_rpi_model(): print("!!!!!!!!!! DO NOT UNPLUG UNTIL I REBOOT !!!!!!!!!!") print("!!!!!!!!!! DO NOT UNPLUG UNTIL I REBOOT !!!!!!!!!!") time.sleep(0.1) - os.system("cd /home/pi/xpadneo/; sudo ./uninstall.sh") + os.system(f"cd {xpadneo_source_path}; sudo ./uninstall.sh") usb4vc_ui.oled_print_oneline("Recompiling...") - os.system("cd /home/pi/xpadneo/; sudo ./install.sh") + os.system(f"cd {xpadneo_source_path}; sudo ./install.sh") usb4vc_ui.oled_print_reboot() save_rpi_model(current_model) time.sleep(2) @@ -68,21 +77,28 @@ def check_rpi_model(): except Exception as e: print('usbhid.mousepoll exception:', e) -usb4vc_ui.ui_init() -usb4vc_ui.ui_thread.start() - -usb4vc_usb_scan.usb_device_scan_thread.start() -usb4vc_usb_scan.raw_input_event_parser_thread.start() - -while 1: - time.sleep(2) - if os.path.exists("/sys/module/bluetooth/parameters/disable_ertm"): - try: - ertm_status = subprocess.getoutput("cat /sys/module/bluetooth/parameters/disable_ertm").replace('\n', '').replace('\r', '').strip() - if ertm_status != 'Y': - print('ertm_status:', ertm_status) - print("Disabling ERTM....") - subprocess.call('echo 1 > /sys/module/bluetooth/parameters/disable_ertm') - print("DONE") - except Exception: - continue +try: + ertm_requires_disable = get_kernel_version() < 6000 + + usb4vc_ui.ui_init() + usb4vc_ui.ui_thread.start() + + usb4vc_usb_scan.usb_device_scan_thread.start() + usb4vc_usb_scan.raw_input_event_parser_thread.start() + + while 1: + time.sleep(2) + if ertm_requires_disable and os.path.exists("/sys/module/bluetooth/parameters/disable_ertm"): + try: + ertm_status = subprocess.getoutput("cat /sys/module/bluetooth/parameters/disable_ertm").replace('\n', '').replace('\r', '').strip() + if ertm_status != 'Y': + print('ertm_status:', ertm_status) + print("Disabling ERTM....") + subprocess.call('echo 1 > /sys/module/bluetooth/parameters/disable_ertm') + print("DONE") + except Exception: + continue +finally: + GPIO.cleanup() + + diff --git a/user_program/usb4vc_shared.py b/user_program/usb4vc_shared.py index b801568..4f86f88 100644 --- a/user_program/usb4vc_shared.py +++ b/user_program/usb4vc_shared.py @@ -1,9 +1,25 @@ import os -this_app_dir_path = "/home/pi/usb4vc/rpi_app" -config_dir_path = "/home/pi/usb4vc/config" -firmware_dir_path = "/home/pi/usb4vc/firmware" -temp_dir_path = "/home/pi/usb4vc/temp" +# allow overriding some paths with environment +xpadneo_source_path = os.getenv("XPADNEO_SOURCE_PATH", "/home/pi/xpadneo") +base_install_path = os.getenv("USB4VC_INSTALL_PATH", "/home/pi/usb4vc") +github_repo_name = os.getenv("USB4VC_REPO", "dekuNukem/USB4VC") +firmware_branch_ref = os.getenv("USB4VC_FIRMWARE_BRANCH_REF", "master") +running_in_systemd = os.getenv("USB4VC_IS_SYSTEMD", "0") == "1" + +usb4vc_releases_url = f"https://api.github.com/repos/{github_repo_name}/releases/latest" +firmware_releases_url = f"https://api.github.com/repos/{github_repo_name}/contents/firmware/releases?ref={firmware_branch_ref}" +firmware_download_url_template = f"https://github.com/{github_repo_name}/raw/{firmware_branch_ref}/firmware/releases/{{fw_filename}}" + +rpi_model_save_file_name = "rpi_model.txt" +config_file_name = "config.json" + +this_app_dir_path = os.path.join(base_install_path, "rpi_app") +config_dir_path = os.path.join(base_install_path, "config") +firmware_dir_path = os.path.join(base_install_path, "firmware") +temp_dir_path = os.path.join(base_install_path, "temp") +rpi_model_save_file_path = os.path.join(base_install_path, rpi_model_save_file_name) +config_file_path = os.path.join(config_dir_path, config_file_name) def ensure_dir(dir_path): print('ensure_dir', dir_path) @@ -60,7 +76,7 @@ def ensure_dir(dir_path): dropped mouse busy drop """ -RPI_APP_VERSION_TUPLE = (0, 3, 3) +RPI_APP_VERSION_TUPLE = (0, 3, 4) code_name_to_value_lookup = { 'KEY_RESERVED':(0, 'kb_key'), @@ -624,4 +640,4 @@ def ensure_dir(dir_path): 'BTN_TRIGGER_HAPPY37', 'BTN_TRIGGER_HAPPY38', 'BTN_TRIGGER_HAPPY39', - 'BTN_TRIGGER_HAPPY40',] \ No newline at end of file + 'BTN_TRIGGER_HAPPY40',] diff --git a/user_program/usb4vc_ui.py b/user_program/usb4vc_ui.py index 4a01ff5..99982a7 100644 --- a/user_program/usb4vc_ui.py +++ b/user_program/usb4vc_ui.py @@ -16,8 +16,6 @@ from usb4vc_shared import * -config_file_path = os.path.join(config_dir_path, 'config.json') - ensure_dir(this_app_dir_path) ensure_dir(config_dir_path) ensure_dir(firmware_dir_path) @@ -161,7 +159,7 @@ def is_pressed(self): def get_list_of_usb_drive(): usb_drive_set = set() try: - usb_drive_path = subprocess.getoutput(f"timeout 2 df -h | grep -i usb").replace('\r', '').split('\n') + usb_drive_path = subprocess.getoutput(f"(timeout 2 df -h | grep -e '/media/*') 2>/dev/null").replace('\r', '').split('\n') for item in [x for x in usb_drive_path if len(x) > 2]: usb_drive_set.add(os.path.join(item.split(' ')[-1], 'usb4vc')) except Exception as e: @@ -175,7 +173,11 @@ def copy_debug_log(): for this_path in usb_drive_set: if os.path.isdir(this_path): print('copying debug log to', this_path) - os.system(f'sudo cp -v /home/pi/usb4vc/usb4vc_debug_log.txt {this_path}') + if running_in_systemd: + dest_log_path = os.path.join(this_path, "usb4vc_debug_log.txt") + os.system(f'sudo journalctl -u usb4vc > {dest_log_path}') + else: + os.system(f'sudo cp -v {base_install_path}/usb4vc_debug_log.txt {this_path}') return True def check_usb_drive(): @@ -281,13 +283,16 @@ def update_pboard_firmware(this_pid): return True return False +# copy configuration from usb, but preserve config.json def update_from_usb(usb_config_path): if usb_config_path is not None: - os.system(f'cp -v /home/pi/usb4vc/config/config.json {usb_config_path}') - os.system('mv -v /home/pi/usb4vc/config/config.json /home/pi/usb4vc/config.json') - os.system('rm -rfv /home/pi/usb4vc/config/*') - os.system(f"cp -v {os.path.join(usb_config_path, '*')} /home/pi/usb4vc/config") - os.system("mv -v /home/pi/usb4vc/config.json /home/pi/usb4vc/config/config.json") + config_temp_backup_path = os.path.join(base_install_path, config_file_name) + + os.system(f'cp -v {config_file_path} {usb_config_path}') + os.system(f'mv -v {config_file_path} {config_temp_backup_path}') + os.system(f"rm -rfv {os.path.join(config_dir_path, '*')}") + os.system(f"cp -v {os.path.join(usb_config_path, '*')} {config_dir_path}") + os.system(f"mv -v {config_temp_backup_path} {config_file_path}") ibmpc_keyboard_protocols = [PROTOCOL_OFF, PROTOCOL_AT_PS2_KB, PROTOCOL_XT_KB] ibmpc_mouse_protocols = [PROTOCOL_OFF, PROTOCOL_PS2_MOUSE_NORMAL, PROTOCOL_MICROSOFT_SERIAL_MOUSE, PROTOCOL_MOUSESYSTEMS_SERIAL_MOUSE] @@ -399,6 +404,12 @@ def get_paired_devices(): def load_config(): global configuration_dict + + # save default config if a file doesn't exist + if not os.path.exists(config_file_path): + save_config() + return + try: with open(config_file_path) as json_file: temp_dict = json.load(json_file) diff --git a/user_program/usb4vc_usb_scan.py b/user_program/usb4vc_usb_scan.py index f15f5d6..55ce2a3 100644 --- a/user_program/usb4vc_usb_scan.py +++ b/user_program/usb4vc_usb_scan.py @@ -4,6 +4,7 @@ import math import spidev import evdev +import struct import threading import RPi.GPIO as GPIO import usb4vc_ui @@ -753,6 +754,12 @@ def raw_input_event_worker(): last_gamepad_msg = None in_deadzone_list = [] last_mouse_button_msg = None + + # should be 16 bytes total on a 32bit system, 24 on a 64bit + input_event_time_size = struct.calcsize("PP") # 64bit kernel uses 64bit longs for timestamps + input_event_data_size = struct.calcsize("HHi") + input_event_size = input_event_time_size + input_event_data_size + print("raw_input_event_worker started") while 1: now = time.time() @@ -769,7 +776,7 @@ def raw_input_event_worker(): this_device = opened_device_dict[key] this_id = this_device['id'] try: - data = this_device['file'].read(16) + data = this_device['file'].read(input_event_size) except OSError: print("Device disappeared:", this_device['name']) this_device['file'].close() @@ -784,7 +791,11 @@ def raw_input_event_worker(): if this_device['is_gp'] and this_id not in gamepad_status_dict: gamepad_status_dict[this_id] = {} - data = list(data[8:]) + data = list(data[input_event_time_size:]) + if len(data) < input_event_data_size: + print("unexpected event data length") + continue + event_code = data[3] * 256 + data[2] # event is a key press