5 Commits

Author SHA1 Message Date
Douglas Teles e4294ab47b Two-image build pipeline, clone support, initramfs splash
- Clone variant: mainline U-Boot v2025.10, boot.scr, clone DTS (type5)
- Initramfs splash: archr-init.c with embedded BMP, sub-second display
- Panel wizard: auto-detect with fsync persistence, evdev X-button reset
- Hotkeys: adc-keys volume for clone, HP jack detection, brightness persist
- Boot.ini: ASCII-only (no Unicode), GPIO check removed
- ES wrapper: removed framebuffer blanking (preserves splash)
- Build pipeline: both variants, splash generation, panel DTBOs
- .gitignore: exclude large bootloader trees and build artifacts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:38:47 -03:00
Douglas Teles 70765e96bd update 26.02.25 2026-02-25 17:22:34 -03:00
Douglas Teles 2ccf5383ac update 26.02.23 2026-02-23 03:03:09 -03:00
Douglas Teles 476b05cd9a fix roadmap 2026-02-22 14:46:19 -03:00
Douglas Teles a9029b419f Fix ROADMAP.md diary timeline ordering
Move Day 17-18 entry to correct chronological position
(before "What's Left for v1.0 Stable", not after it).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:43:37 -03:00
20 changed files with 3460 additions and 391 deletions
+17
View File
@@ -71,3 +71,20 @@ restore-mesa26-gbm.sh
# Website (separate project)
website/
# Environment secrets
.env
# Bootloader source trees (cloned/downloaded during build)
bootloader/rkbin/
bootloader/u-boot-mainline/
bootloader/u-boot-rk3326-clone/
# Pre-built U-Boot backups (working binaries, not part of build pipeline)
bootloader/u-boot-clone-working/
# Extracted DTBs (debugging artifacts)
bootloader/u-boot-r36s-working/extracted_dtb_*.dtb
# Clone U-Boot build output
bootloader/u-boot-clone-build/
+77 -6
View File
@@ -153,11 +153,81 @@ Arch-R/
## Supported Panels
Arch R supports 18 display panels via DTBO overlays:
Arch R supports 18 display panels via DTBO overlays. The default image boots with **Panel 4-V22** (the most common R36S panel).
**Original R36S:** Panel 0, Panel 1 (V10), Panel 2 (V12), Panel 3 (V20), Panel 4 (V22, default), Panel 5 (V22 Q8)
### Panel Auto-Detection
**Clones:** Clone Panel 1-10 (ST7703, NV3051D variants), R36 Max, RX6S
On first boot, Arch R runs a **panel detection wizard** that guides you through selecting your display panel:
1. **If your screen works** (you see text on screen): press **A** to confirm the current panel
2. **If your screen is black** (wrong panel): listen for audio beeps and use buttons to navigate:
- Each panel plays a different number of beeps (1 beep = first panel, 2 beeps = second, etc.)
- Press **B** to skip to the next panel
- Press **A** to confirm (you'll hear 3 rapid beeps)
- The system reboots with the correct panel applied
Your selection is saved permanently. To reset it later, **hold X during boot**.
### Panel List
| # | Panel | Notes |
|---|-------|-------|
| 1 | **Panel 4-V22** *(default)* | Most common (~60% of R36S units) |
| 2 | Panel 3-V20 | V20 board marking |
| 3 | Panel 5-V22 Q8 | V22 Q8 variant |
| 4 | Clone Panel 8 | ST7703 (G80CA-MB) |
| 5 | Panel 0 | Early R36S units |
| 6 | Panel 1-V10 | V10 board marking |
| 7 | Panel 2-V12 | V12 board marking |
| 8 | Clone Panel 1 | ST7703 |
| 9 | Clone Panel 3 | NV3051D |
| 10 | Clone Panel 7 | JD9365DA |
| 11 | Clone Panel 9 | NV3051D |
| 12 | Clone Panel 10 | ST7703 variant |
| 13 | Clone Panel 2 | ST7703 |
| 14 | Clone Panel 4 | NV3051D |
| 15 | Clone Panel 5 | ST7703 |
| 16 | Clone Panel 6 | NV3051D |
| 17 | R36 Max | 720x720 ST7703 |
| 18 | RX6S | NV3051D variant |
### Manual Panel Selection
If you prefer to set the panel manually (e.g., you know which panel you have from ArkOS/dArkOS):
**Step 1 — Mount the BOOT partition on your PC**
```bash
lsblk
sudo mount /dev/sdX1 /mnt
```
**Step 2 — Create panel.txt**
```bash
# Example: Panel 3-V20
echo 'PanelNum=3
PanelDTBO=ScreenFiles/Panel 3/mipi-panel.dtbo' | sudo tee /mnt/panel.txt
# Mark as confirmed (skip wizard on next boot)
echo 'confirmed' | sudo tee /mnt/panel-confirmed
```
For default Panel 4-V22 (no overlay needed):
```bash
echo 'PanelNum=4
PanelDTBO=' | sudo tee /mnt/panel.txt
echo 'confirmed' | sudo tee /mnt/panel-confirmed
```
**Step 3 — Unmount and boot**
```bash
sudo umount /mnt
sync
```
**To reset panel selection:** hold **X** during boot, or delete `panel-confirmed` from the BOOT partition.
## Boot Flow
@@ -165,11 +235,12 @@ Arch R supports 18 display panels via DTBO overlays:
Power On
→ U-Boot (idbloader → trust → uboot.img)
→ logo.bmp displayed on screen
extlinux/extlinux.conf loaded (primary)
boot.ini: load panel.txt → apply DTBO overlay (if non-default panel)
→ Kernel 6.6.89 + rk3326-gameconsole-r36s.dtb
→ systemd → archr-boot-setup (GPU + governors)
→ systemd → panel-detect.service (first boot only)
→ archr-boot-setup (GPU + governors)
→ emulationstation.service → EmulationStation UI
≈ 19 seconds total
≈ 19 seconds total (+ wizard on first boot)
```
## Contributing
+745 -152
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+30 -4
View File
@@ -44,6 +44,7 @@ usage() {
echo "Options:"
echo " --kernel Build kernel only"
echo " --rootfs Build rootfs only"
echo " --uboot Build U-Boot only"
echo " --image Build image only"
echo " --all Build everything (default)"
echo " --clean Clean all build artifacts"
@@ -53,6 +54,7 @@ usage() {
BUILD_KERNEL=false
BUILD_ROOTFS=false
BUILD_UBOOT=false
BUILD_IMAGE=false
CLEAN=false
@@ -60,14 +62,16 @@ CLEAN=false
if [ $# -eq 0 ]; then
BUILD_KERNEL=true
BUILD_ROOTFS=true
BUILD_UBOOT=true
BUILD_IMAGE=true
else
while [[ $# -gt 0 ]]; do
case $1 in
--kernel) BUILD_KERNEL=true; shift ;;
--rootfs) BUILD_ROOTFS=true; shift ;;
--uboot) BUILD_UBOOT=true; shift ;;
--image) BUILD_IMAGE=true; shift ;;
--all) BUILD_KERNEL=true; BUILD_ROOTFS=true; BUILD_IMAGE=true; shift ;;
--all) BUILD_KERNEL=true; BUILD_ROOTFS=true; BUILD_UBOOT=true; BUILD_IMAGE=true; shift ;;
--clean) CLEAN=true; shift ;;
--help) usage; exit 0 ;;
*) error "Unknown option: $1" ;;
@@ -180,13 +184,35 @@ log "═════════════════════════
chmod +x "$SCRIPT_DIR/scripts/generate-panel-dtbos.sh"
"$SCRIPT_DIR/scripts/generate-panel-dtbos.sh"
# Build custom U-Boot (optional — requires arm-linux-gnueabihf toolchain)
if [ "$BUILD_UBOOT" = true ] && [ -f "$SCRIPT_DIR/build-uboot.sh" ]; then
if command -v aarch64-linux-gnu-gcc &>/dev/null; then
log ""
log "═══════════════════════════════════════════════════════════════"
log " BUILDING U-BOOT"
log "═══════════════════════════════════════════════════════════════"
chmod +x "$SCRIPT_DIR/build-uboot.sh"
"$SCRIPT_DIR/build-uboot.sh"
else
warn "Skipping U-Boot build: aarch64-linux-gnu-gcc not found"
warn "Install: sudo apt install gcc-aarch64-linux-gnu"
fi
fi
if [ "$BUILD_IMAGE" = true ]; then
chmod +x "$SCRIPT_DIR/build-image.sh"
log ""
log "═══════════════════════════════════════════════════════════════"
log " BUILDING IMAGE"
log " BUILDING IMAGE (original)"
log "═══════════════════════════════════════════════════════════════"
chmod +x "$SCRIPT_DIR/build-image.sh"
"$SCRIPT_DIR/build-image.sh"
"$SCRIPT_DIR/build-image.sh" --variant original
log ""
log "═══════════════════════════════════════════════════════════════"
log " BUILDING IMAGE (clone)"
log "═══════════════════════════════════════════════════════════════"
"$SCRIPT_DIR/build-image.sh" --variant clone
fi
#------------------------------------------------------------------------------
+282 -110
View File
File diff suppressed because it is too large Load Diff
+35 -5
View File
@@ -31,8 +31,9 @@ KERNEL_REPO="https://github.com/rockchip-linux/kernel.git"
DEFCONFIG="rockchip_linux_defconfig"
CONFIG_FRAGMENT="$SCRIPT_DIR/config/archr-6.6-r36s.config"
# DTS target
# DTS targets
R36S_DTB="rk3326-gameconsole-r36s"
CLONE_DTB="rk3326-gameconsole-r36s-clone-type5"
# Paths
KERNEL_SRC="${KERNEL_SRC:-/home/dgateles/Documentos/Projetos/kernel}"
@@ -93,6 +94,14 @@ else
fi
fi
# Copy clone DTS (if available)
CLONE_DTS_SRC="$SCRIPT_DIR/kernel/dts/${CLONE_DTB}.dts"
CLONE_DTS_DEST="$KERNEL_SRC/arch/arm64/boot/dts/rockchip/${CLONE_DTB}.dts"
if [ -f "$CLONE_DTS_SRC" ]; then
cp "$CLONE_DTS_SRC" "$CLONE_DTS_DEST"
log " DTS (clone): copied from repo"
fi
#------------------------------------------------------------------------------
# Step 2: Configure Kernel
#------------------------------------------------------------------------------
@@ -164,6 +173,17 @@ fi
log " DTB built: ${R36S_DTB}.dtb"
# Build clone DTB (if DTS was copied)
if [ -f "$CLONE_DTS_DEST" ]; then
make -C "$KERNEL_SRC" -j$JOBS ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE \
"rockchip/${CLONE_DTB}.dtb" 2>&1 | tail -5
if [ -f "$KERNEL_SRC/arch/arm64/boot/dts/rockchip/${CLONE_DTB}.dtb" ]; then
log " DTB built (clone): ${CLONE_DTB}.dtb"
else
warn "Clone DTB build failed: ${CLONE_DTB}.dtb"
fi
fi
#------------------------------------------------------------------------------
# Step 5: Build Kernel Modules
#------------------------------------------------------------------------------
@@ -192,6 +212,12 @@ log " Copied: Image"
cp "$KERNEL_SRC/arch/arm64/boot/dts/rockchip/${R36S_DTB}.dtb" "$BOOT_DIR/"
log " Copied: ${R36S_DTB}.dtb"
# Copy clone DTB (if built)
if [ -f "$KERNEL_SRC/arch/arm64/boot/dts/rockchip/${CLONE_DTB}.dtb" ]; then
cp "$KERNEL_SRC/arch/arm64/boot/dts/rockchip/${CLONE_DTB}.dtb" "$BOOT_DIR/"
log " Copied: ${CLONE_DTB}.dtb"
fi
# Install modules (use pipefail to catch errors masked by tail)
set -o pipefail
make -C "$KERNEL_SRC" ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE \
@@ -231,9 +257,13 @@ MODULES_SIZE=$(du -sh "$MODULES_DIR" 2>/dev/null | cut -f1 || echo "N/A")
log "Kernel: $KERNEL_FULL"
log ""
CLONE_DTB_SIZE="N/A"
[ -f "$BOOT_DIR/${CLONE_DTB}.dtb" ] && CLONE_DTB_SIZE=$(du -h "$BOOT_DIR/${CLONE_DTB}.dtb" | cut -f1)
log "Artifacts:"
log " Image: $BOOT_DIR/Image ($IMAGE_SIZE)"
log " DTB: $BOOT_DIR/${R36S_DTB}.dtb ($DTB_SIZE)"
log " Modules: $MODULES_DIR/ ($MODULES_SIZE)"
log " Image: $BOOT_DIR/Image ($IMAGE_SIZE)"
log " DTB: $BOOT_DIR/${R36S_DTB}.dtb ($DTB_SIZE)"
log " DTB clone: $BOOT_DIR/${CLONE_DTB}.dtb ($CLONE_DTB_SIZE)"
log " Modules: $MODULES_DIR/ ($MODULES_SIZE)"
log ""
log "Kernel 6.6 ready for R36S!"
log "Kernel 6.6 ready for R36S (original + clone)!"
+34 -3
View File
@@ -353,6 +353,7 @@ fi
cat > /etc/systemd/system/firstboot.service << 'FB_EOF'
[Unit]
Description=Arch R First Boot Setup
After=local-fs.target
Before=emulationstation.service
ConditionPathExists=!/var/lib/archr/.first-boot-done
@@ -367,6 +368,25 @@ FB_EOF
systemctl enable firstboot
# Panel auto-detect wizard (first boot / X-button reset)
cat > /etc/systemd/system/panel-detect.service << 'PANELDET_EOF'
[Unit]
Description=Arch R Panel Detection Wizard
After=local-fs.target
Before=emulationstation.service
RequiresMountsFor=/boot
[Service]
Type=oneshot
ExecStart=/usr/local/bin/panel-detect.py
TimeoutStartSec=600
[Install]
WantedBy=multi-user.target
PANELDET_EOF
systemctl enable panel-detect
# EmulationStation launch — PRIMARY: systemd service (created by build-emulationstation.sh)
# FALLBACK: autologin + .bash_profile (if ES service not installed)
# The emulationstation.service Conflicts with getty@tty1, so only one runs.
@@ -403,9 +423,12 @@ fi
PROFILE_EOF
chown archr:archr /home/archr/.bash_profile
# Disable old custom splash if it exists
# Boot splash: handled by initramfs (archr-init with embedded BMP).
# Initramfs displays splash at ~0.7s after kernel start, before systemd.
# No systemd service needed — splash persists until ES takes DRM master.
systemctl disable splash 2>/dev/null || true
rm -f /etc/systemd/system/splash.service
systemctl disable archr-splash 2>/dev/null || true
rm -f /etc/systemd/system/splash.service /etc/systemd/system/archr-splash.service
# Battery LED warning service
cat > /etc/systemd/system/battery-led.service << 'BATT_EOF'
@@ -483,7 +506,8 @@ TIMING_EOF
systemctl enable boot-timing
# Delete initramfs (not used — extlinux.conf has no INITRD line)
# Delete stock ALARM initramfs and kernel (we use our own kernel + initramfs)
# Our initramfs.img is built by build-image.sh and placed on BOOT partition
rm -f /boot/initramfs-linux.img /boot/Image /boot/Image.gz 2>/dev/null
# Clean kernel modules: remove media tuners (154 modules, 13MB — useless for gaming)
@@ -968,6 +992,13 @@ log " ✓ Distro version installed (text.plymouth)"
install -m 755 "$SCRIPT_DIR/scripts/first-boot.sh" "$ROOTFS_DIR/usr/local/bin/first-boot.sh"
log " ✓ First boot script installed"
# Boot splash: no binary needed in rootfs — splash is embedded in initramfs /init
# (built by build-image.sh: archr-init.c + embedded splash BMP → initramfs.img)
# Panel detection wizard
install -m 755 "$SCRIPT_DIR/scripts/panel-detect.py" "$ROOTFS_DIR/usr/local/bin/panel-detect.py"
log " ✓ Panel detection wizard installed"
# RetroArch config (install to user's config dir where retroarch expects it)
mkdir -p "$ROOTFS_DIR/home/archr/.config/retroarch"
cp "$SCRIPT_DIR/config/retroarch.cfg" "$ROOTFS_DIR/home/archr/.config/retroarch/retroarch.cfg"
+174
View File
@@ -0,0 +1,174 @@
#!/bin/bash
#==============================================================================
# Arch R - Build Mainline U-Boot for R36S Clones
#==============================================================================
# Uses mainline U-Boot v2025.10 + ROCKNIX patches
# Produces: idbloader.img, uboot.img, trust.img (Rockchip format)
#
# Usage:
# ./build-uboot-clone.sh [--uart5]
#
# --uart5: Build for K36 clones that use UART5 instead of UART2
#==============================================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
UBOOT_SRC="$SCRIPT_DIR/bootloader/u-boot-mainline"
RKBIN_DIR="$SCRIPT_DIR/bootloader/rkbin"
OUTPUT_DIR="$SCRIPT_DIR/bootloader/u-boot-clone-build"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[UBOOT-CLONE]${NC} $1"; }
warn() { echo -e "${YELLOW}[UBOOT-CLONE] WARNING:${NC} $1"; }
error() { echo -e "${RED}[UBOOT-CLONE] ERROR:${NC} $1"; exit 1; }
#------------------------------------------------------------------------------
# Parse arguments
#------------------------------------------------------------------------------
UART5=0
for arg in "$@"; do
case "$arg" in
--uart5) UART5=1 ;;
esac
done
#------------------------------------------------------------------------------
# Verify prerequisites
#------------------------------------------------------------------------------
CROSS_COMPILE="aarch64-linux-gnu-"
if ! command -v ${CROSS_COMPILE}gcc &>/dev/null; then
error "Cross-compiler not found. Install: sudo apt install gcc-aarch64-linux-gnu"
fi
if ! python3 -c "import elftools" 2>/dev/null; then
error "pyelftools not found. Install: pip3 install pyelftools"
fi
if ! command -v swig &>/dev/null; then
error "swig not found. Install: sudo apt install swig"
fi
[ -d "$UBOOT_SRC" ] || error "Mainline U-Boot source not found: $UBOOT_SRC"
[ -d "$RKBIN_DIR" ] || error "rkbin not found: $RKBIN_DIR"
#------------------------------------------------------------------------------
# Rockchip firmware binaries
#------------------------------------------------------------------------------
PKG_DDR_BIN="$RKBIN_DIR/bin/rk33/rk3326_ddr_333MHz_v2.11.bin"
PKG_MINILOADER="$RKBIN_DIR/bin/rk33/rk3326_miniloader_v1.40.bin"
PKG_BL31="$RKBIN_DIR/bin/rk33/rk3326_bl31_v1.34.elf"
[ -f "$PKG_DDR_BIN" ] || error "DDR binary not found: $PKG_DDR_BIN"
[ -f "$PKG_MINILOADER" ] || error "Miniloader not found: $PKG_MINILOADER"
[ -f "$PKG_BL31" ] || error "BL31 not found: $PKG_BL31"
#------------------------------------------------------------------------------
# UART5 DDR binary (for K36 clones)
#------------------------------------------------------------------------------
if [ "$UART5" -eq 1 ]; then
log "Building UART5 variant for K36 clones"
PKG_DDR_BIN_UART5="$RKBIN_DIR/rk3326_ddr_uart5.bin"
if [ ! -f "$PKG_DDR_BIN_UART5" ]; then
log "Creating UART5-tuned DDR binary..."
DDRBIN_TOOL="$RKBIN_DIR/tools/ddrbin_tool"
if [ ! -x "$DDRBIN_TOOL" ]; then
error "ddrbin_tool not found or not executable: $DDRBIN_TOOL"
fi
cp "$PKG_DDR_BIN" "$PKG_DDR_BIN_UART5"
"$DDRBIN_TOOL" rk3326 -g "$RKBIN_DIR/rk3326_ddr_uart5.txt" "$PKG_DDR_BIN_UART5"
sed -i 's|uart id=.*$|uart id=5|' "$RKBIN_DIR/rk3326_ddr_uart5.txt"
"$DDRBIN_TOOL" rk3326 "$RKBIN_DIR/rk3326_ddr_uart5.txt" "$PKG_DDR_BIN_UART5" >/dev/null
log "UART5 DDR binary created"
fi
PKG_DDR_BIN="$PKG_DDR_BIN_UART5"
fi
#------------------------------------------------------------------------------
# Build U-Boot
#------------------------------------------------------------------------------
JOBS=$(nproc)
DEFCONFIG="rk3326-handheld_defconfig"
log "=== Building Mainline U-Boot v2025.10 for R36S Clone ==="
log "Cross-compiler: $(${CROSS_COMPILE}gcc --version | head -1)"
log "Defconfig: $DEFCONFIG"
log "Jobs: $JOBS"
[ "$UART5" -eq 1 ] && log "UART: UART5 (0xFF178000)" || log "UART: UART2 (0xFF160000)"
cd "$UBOOT_SRC"
log "Step 1: Clean..."
make CROSS_COMPILE="$CROSS_COMPILE" ARCH=arm mrproper 2>&1 | tail -1
log "Step 2: Configure..."
make CROSS_COMPILE="$CROSS_COMPILE" ARCH=arm "$DEFCONFIG" 2>&1 | tail -1
# UART5 variant: override config
if [ "$UART5" -eq 1 ]; then
./scripts/config --set-val CONFIG_DEBUG_UART_BASE 0xFF178000
./scripts/config --set-str CONFIG_DEVICE_TREE_INCLUDES "rk3326-odroid-go2-emmc.dtsi rk3326-odroid-go2-uart5.dtsi"
log "UART5 config applied"
fi
log "Step 3: Compile..."
make CROSS_COMPILE="$CROSS_COMPILE" ARCH=arm -j"$JOBS" u-boot-dtb.bin 2>&1 | tail -20
[ -f "u-boot-dtb.bin" ] || error "Build failed: u-boot-dtb.bin not found"
log "u-boot-dtb.bin built: $(du -h u-boot-dtb.bin | cut -f1)"
#------------------------------------------------------------------------------
# Package: idbloader.img
#------------------------------------------------------------------------------
log "Step 4: Creating idbloader.img..."
"$UBOOT_SRC/tools/mkimage" -n px30 -T rksd -d "$PKG_DDR_BIN" -C bzip2 idbloader.img
cat "$PKG_MINILOADER" >> idbloader.img
log "idbloader.img: $(du -h idbloader.img | cut -f1)"
#------------------------------------------------------------------------------
# Package: uboot.img
#------------------------------------------------------------------------------
log "Step 5: Creating uboot.img..."
"$RKBIN_DIR/tools/loaderimage" --pack --uboot u-boot-dtb.bin uboot.img 0x00200000 2>&1 | tail -3
log "uboot.img: $(du -h uboot.img | cut -f1)"
#------------------------------------------------------------------------------
# Package: trust.img
#------------------------------------------------------------------------------
log "Step 6: Creating trust.img..."
cat >trust.ini <<EOF
[BL30_OPTION]
SEC=0
[BL31_OPTION]
SEC=1
PATH=${PKG_BL31}
ADDR=0x00010000
[BL32_OPTION]
SEC=0
[BL33_OPTION]
SEC=0
[OUTPUT]
PATH=trust.img
EOF
"$RKBIN_DIR/tools/trust_merger" --verbose trust.ini 2>&1 | tail -5
log "trust.img: $(du -h trust.img | cut -f1)"
#------------------------------------------------------------------------------
# Copy to output directory
#------------------------------------------------------------------------------
mkdir -p "$OUTPUT_DIR"
cp idbloader.img uboot.img trust.img "$OUTPUT_DIR/"
log ""
log "=== Build Complete ==="
log "Output: $OUTPUT_DIR/"
ls -lh "$OUTPUT_DIR/"*.img
log ""
log "Flash with:"
log " ./flash-uboot-clone.sh /dev/sdX"
+65 -35
View File
@@ -1,9 +1,18 @@
#!/bin/bash
#==============================================================================
# Arch R - Build U-Boot for R36S
# Arch R - U-Boot Build Script for R36S
#==============================================================================
# Builds U-Boot bootloader for RK3326 (R36S/RG351MP compatible)
# Builds custom U-Boot with R36S original/clone auto-detection via eMMC probe.
# Cross-compiles for aarch64 (CONFIG_ARM64=y in defconfig).
#
# Custom modifications:
# - cmd/hwrev.c: R36S detection via eMMC probe (original vs clone)
# - r36s-uboot.dts: U-Boot display DTB for original (Panel 4-V22 NV3052C)
# - r36s-clone-uboot.dts: U-Boot display DTB for clone (Type5 NV3052C)
#
# Prerequisites:
# sudo apt install gcc-aarch64-linux-gnu
#==============================================================================
set -e
@@ -29,7 +38,10 @@ UBOOT_REPO="https://github.com/christianhaitian/RG351MP-u-boot"
UBOOT_SRC="$UBOOT_DIR/u-boot-rk3326"
OUTPUT_DIR="$SCRIPT_DIR/output/bootloader"
log "=== Arch R - Build U-Boot ==="
# U-Boot for RK3326 is aarch64 (CONFIG_ARM64=y in defconfig)
CROSS_COMPILE="aarch64-linux-gnu-"
log "=== Arch R - U-Boot Builder (Custom R36S) ==="
#------------------------------------------------------------------------------
# Step 1: Clone U-Boot
@@ -42,59 +54,77 @@ mkdir -p "$UBOOT_DIR"
if [ ! -d "$UBOOT_SRC" ]; then
log " Cloning U-Boot for RK3326..."
git clone --depth 1 "$UBOOT_REPO" "$UBOOT_SRC"
log " ✓ U-Boot cloned"
log " Cloned"
else
log " ✓ U-Boot source exists"
log " Source exists"
fi
#------------------------------------------------------------------------------
# Step 2: Verify toolchain
#------------------------------------------------------------------------------
log ""
log "Step 2: Checking toolchain..."
if ! command -v ${CROSS_COMPILE}gcc &>/dev/null; then
error "aarch64 toolchain not found!\n Install: sudo apt install gcc-aarch64-linux-gnu"
fi
log " $(${CROSS_COMPILE}gcc --version | head -1)"
#------------------------------------------------------------------------------
# Step 3: Build U-Boot
#------------------------------------------------------------------------------
log ""
log "Step 3: Building U-Boot..."
cd "$UBOOT_SRC"
# GCC 13+ adds stricter warnings that U-Boot 2017.09 doesn't pass.
# Disable specific -Werror flags that break with modern GCC:
# address-of-packed-member: disk/part_efi.c (packed struct pointer)
# enum-int-mismatch: common/command.c (enum command_ret_t vs int)
# maybe-uninitialized: common/edid.c (hdmi_len variable)
GCC_COMPAT_FLAGS="-Wno-error=address-of-packed-member -Wno-error=enum-int-mismatch -Wno-error=maybe-uninitialized"
make CROSS_COMPILE=$CROSS_COMPILE odroidgoa_defconfig
make CROSS_COMPILE=$CROSS_COMPILE KCFLAGS="$GCC_COMPAT_FLAGS" -j$(nproc)
log " Built"
#------------------------------------------------------------------------------
# Step 2: Build U-Boot
# Step 4: Copy artifacts
#------------------------------------------------------------------------------
log ""
log "Step 2: Building U-Boot..."
# Set up cross-compiler (use Linaro if available, otherwise system)
if [ -d "/opt/toolchains/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu" ]; then
export PATH=/opt/toolchains/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin:$PATH
fi
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
# Build for OdroidGoA (compatible with R36S/RG351MP)
./make.sh odroidgoa 2>&1 | tail -20 || true
log " ✓ U-Boot built"
#------------------------------------------------------------------------------
# Step 3: Copy artifacts
#------------------------------------------------------------------------------
log ""
log "Step 3: Copying bootloader files..."
log "Step 4: Copying bootloader files..."
mkdir -p "$OUTPUT_DIR"
# Copy the bootloader images
if [ -d "sd_fuse" ]; then
if [ -d "sd_fuse" ] && [ -f "sd_fuse/idbloader.img" ]; then
cp sd_fuse/idbloader.img "$OUTPUT_DIR/"
cp sd_fuse/uboot.img "$OUTPUT_DIR/"
cp sd_fuse/trust.img "$OUTPUT_DIR/"
log " Bootloader files copied"
log " Bootloader binaries copied"
else
error "sd_fuse directory not found - U-Boot build may have failed"
error "sd_fuse directory not found U-Boot build may have failed"
fi
# Copy R36S U-Boot DTBs
for dtb in r36s-uboot.dtb r36s-clone-uboot.dtb; do
if [ -f "arch/arm/dts/$dtb" ]; then
cp "arch/arm/dts/$dtb" "$OUTPUT_DIR/"
log " DTB copied: $dtb"
else
warn "DTB not found: $dtb"
fi
done
#------------------------------------------------------------------------------
# Summary
#------------------------------------------------------------------------------
log ""
log "=== U-Boot Build Complete ==="
log ""
log "Bootloader files:"
ls -la "$OUTPUT_DIR/"
log "Bootloader: $OUTPUT_DIR/"
ls -lh "$OUTPUT_DIR/"
log ""
log "These will be automatically installed by build-image.sh"
log ""
log "✓ U-Boot ready for R36S!"
log "These will be installed by build-image.sh"
+57 -16
View File
@@ -1,24 +1,65 @@
ODROIDGOA-UBOOT-CONFIG
odroidgoa-uboot-config
########################################################################
# Arch R Boot Configuration for R36S — Kernel 6.6 + Systemd
# Default panel: Panel 4-V22 (hardcoded in base DTB, no overlay needed)
# Primary boot: extlinux/extlinux.conf (U-Boot tries it first)
# This boot.ini is a fallback if extlinux is missing.
# Arch R Boot Configuration
#
# Auto-detects SD card MMC device:
# Original R36S: eMMC=mmc0, SD=mmc1
# Clone (no eMMC): SD=mmc0
#
# Root device (__ROOTDEV__) substituted by build-image.sh per variant.
# Panel config from panel.txt (managed by panel-detect.py).
# Panel reset handled in Linux (panel-detect.py) -- no GPIO check here
# because X button GPIO differs between boards (modular!).
########################################################################
# Memory addresses — kernel at 0x02008000 (matches working R36S firmware)
setenv dtb_loadaddr "0x01f00000"
setenv loadaddr "0x02008000"
# --- Turn on LED immediately (feedback: device is booting) ---
gpio set b5
# Load kernel Image
if load mmc 1:1 ${loadaddr} Image
then
# Load DTB
if load mmc 1:1 ${dtb_loadaddr} rk3326-gameconsole-r36s.dtb
then
# Boot without initrd — use device path directly
setenv bootargs "root=/dev/mmcblk1p2 rootwait rw console=ttyFIQ0 loglevel=0 quiet vt.global_cursor_default=0 consoleblank=0 printk.devkmsg=off fsck.mode=skip"
setenv dtbo_loadaddr "0x01e00000"
setenv dtb_loadaddr "0x01f00000"
setenv loadaddr "0x02000000"
# --- Auto-detect MMC device (loads kernel Image into memory) ---
setenv mmcdev 1
if load mmc 1:1 ${loadaddr} Image; then
setenv mmcdev 1
elif load mmc 0:1 ${loadaddr} Image; then
setenv mmcdev 0
else
sleep 15
poweroff
fi
# --- Panel overlay (read panel.txt written by panel-detect.py) ---
setenv PanelDTBO ""
mw.b ${dtbo_loadaddr} 0x00 0x400
if load mmc ${mmcdev}:1 ${dtbo_loadaddr} panel.txt; then
env import -t ${dtbo_loadaddr} ${filesize}
fi
# --- Load DTB and boot (Image already loaded above) ---
if load mmc ${mmcdev}:1 ${dtb_loadaddr} kernel.dtb; then
# Apply panel DTBO overlay if PanelDTBO is set (non-default panel)
if test -n "${PanelDTBO}"; then
if load mmc ${mmcdev}:1 ${dtbo_loadaddr} "${PanelDTBO}"; then
fdt addr ${dtb_loadaddr}
fdt resize 8192
fdt apply ${dtbo_loadaddr}
echo "Panel overlay: ${PanelDTBO}"
else
echo "WARN: overlay not found: ${PanelDTBO}"
fi
fi
setenv bootargs "root=__ROOTDEV__ rootwait rw console=ttyFIQ0 loglevel=0 quiet vt.global_cursor_default=0 consoleblank=0 printk.devkmsg=off fsck.mode=skip"
# Load initramfs (splash appears immediately after kernel boot, before systemd)
setenv initrd_loadaddr "0x04000000"
if load mmc ${mmcdev}:1 ${initrd_loadaddr} initramfs.img; then
booti ${loadaddr} ${initrd_loadaddr}:${filesize} ${dtb_loadaddr}
else
booti ${loadaddr} - ${dtb_loadaddr}
fi
fi
+139
View File
@@ -0,0 +1,139 @@
#!/bin/bash
#==============================================================================
# Arch R - Flash Mainline U-Boot to Clone SD Card
#==============================================================================
# Flashes mainline U-Boot binaries to an existing Arch R SD card for clones.
# Does NOT touch partitions or rootfs — only U-Boot area.
#
# Also creates a minimal boot.scr for mainline U-Boot (distro boot).
#
# Usage:
# ./flash-uboot-clone.sh /dev/sdX
#==============================================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[FLASH-CLONE]${NC} $1"; }
warn() { echo -e "${YELLOW}[FLASH-CLONE] WARNING:${NC} $1"; }
error() { echo -e "${RED}[FLASH-CLONE] ERROR:${NC} $1"; exit 1; }
#------------------------------------------------------------------------------
# Parse arguments
#------------------------------------------------------------------------------
if [ -z "$1" ]; then
echo "Usage: $0 /dev/sdX"
echo ""
echo "Available devices:"
lsblk -d -o NAME,SIZE,MODEL,TRAN | grep -E "sd|mmcblk" || true
exit 1
fi
DEVICE="$1"
if [ ! -b "$DEVICE" ]; then
error "Device not found: $DEVICE"
fi
# Safety check
if mount | grep -q "^${DEVICE}"; then
warn "Device $DEVICE has mounted partitions:"
mount | grep "^${DEVICE}" || true
echo ""
read -p "Continue anyway? (y/N) " -n 1 -r
echo
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
fi
#------------------------------------------------------------------------------
# Verify binaries
#------------------------------------------------------------------------------
BUILD_DIR="$SCRIPT_DIR/bootloader/u-boot-clone-build"
for f in "$BUILD_DIR/idbloader.img" "$BUILD_DIR/uboot.img" "$BUILD_DIR/trust.img"; do
[ -f "$f" ] || error "Missing: $f (run build-uboot-clone.sh first)"
done
log "Device: $DEVICE"
log "Binaries: $BUILD_DIR"
log ""
log "Will flash:"
log " idbloader.img → sector 64 ($(du -h "$BUILD_DIR/idbloader.img" | cut -f1))"
log " uboot.img → sector 16384 ($(du -h "$BUILD_DIR/uboot.img" | cut -f1))"
log " trust.img → sector 24576 ($(du -h "$BUILD_DIR/trust.img" | cut -f1))"
log ""
read -p "Flash to $DEVICE? (y/N) " -n 1 -r
echo
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
#------------------------------------------------------------------------------
# Flash U-Boot binaries (raw sectors)
#------------------------------------------------------------------------------
log "Flashing idbloader.img..."
pkexec dd if="$BUILD_DIR/idbloader.img" of="$DEVICE" bs=512 seek=64 conv=sync,noerror,notrunc 2>/dev/null
log "Flashing uboot.img..."
pkexec dd if="$BUILD_DIR/uboot.img" of="$DEVICE" bs=512 seek=16384 conv=sync,noerror,notrunc 2>/dev/null
log "Flashing trust.img..."
pkexec dd if="$BUILD_DIR/trust.img" of="$DEVICE" bs=512 seek=24576 conv=sync,noerror,notrunc 2>/dev/null
log "U-Boot binaries flashed"
#------------------------------------------------------------------------------
# Create boot.scr on BOOT partition
#------------------------------------------------------------------------------
BOOT_PART="${DEVICE}1"
if [ ! -b "$BOOT_PART" ]; then
BOOT_PART="${DEVICE}p1"
fi
if [ ! -b "$BOOT_PART" ]; then
warn "BOOT partition not found — boot.scr NOT created"
else
MOUNT_TMP=$(mktemp -d)
pkexec mount "$BOOT_PART" "$MOUNT_TMP"
# Create boot.scr from boot.ini (strip BSP-specific commands)
MKIMAGE="$SCRIPT_DIR/bootloader/u-boot-mainline/tools/mkimage"
if [ -x "$MKIMAGE" ] && [ -f "$MOUNT_TMP/boot.ini" ]; then
log "Creating boot.scr from boot.ini..."
TMP_SCR=$(mktemp)
# Strip 'odroidgoa-uboot-config' (BSP command, not in mainline)
grep -v '^odroidgoa-uboot-config' "$MOUNT_TMP/boot.ini" > "$TMP_SCR"
"$MKIMAGE" -T script -d "$TMP_SCR" "$MOUNT_TMP/boot.scr" 2>/dev/null && \
log "boot.scr created" || \
warn "boot.scr creation failed"
rm -f "$TMP_SCR"
fi
log "BOOT partition contents:"
ls "$MOUNT_TMP/" | head -20
pkexec umount "$MOUNT_TMP"
rmdir "$MOUNT_TMP"
fi
#------------------------------------------------------------------------------
# Done
#------------------------------------------------------------------------------
sync
log ""
log "=== Mainline U-Boot flashed to $DEVICE ==="
log ""
log "This is MAINLINE U-Boot (not BSP). Key differences:"
log " - Uses go2.c board detection (not hwrev.c)"
log " - Boots via boot.scr / extlinux.conf (not boot.ini directly)"
log " - Display: mainline DRM (may not show boot logo)"
log ""
log "If boot fails, restore working clone U-Boot:"
log " pkexec dd if=bootloader/u-boot-clone-working/uboot.img of=$DEVICE bs=512 seek=16384 conv=notrunc"
log " pkexec dd if=bootloader/u-boot-clone-working/idbloader.img of=$DEVICE bs=512 seek=64 conv=notrunc"
log " pkexec dd if=bootloader/u-boot-clone-working/trust.img of=$DEVICE bs=512 seek=24576 conv=notrunc"
Executable
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
#==============================================================================
# Arch R - Flash Custom U-Boot to SD Card
#==============================================================================
# Flashes custom U-Boot binaries + display DTBs to an existing Arch R SD card.
# Does NOT touch partitions or rootfs — only U-Boot area + BOOT partition DTBs.
#
# Usage:
# ./flash-uboot.sh /dev/sdX
#==============================================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[FLASH]${NC} $1"; }
warn() { echo -e "${YELLOW}[FLASH] WARNING:${NC} $1"; }
error() { echo -e "${RED}[FLASH] ERROR:${NC} $1"; exit 1; }
#------------------------------------------------------------------------------
# Parse arguments
#------------------------------------------------------------------------------
if [ -z "$1" ]; then
echo "Usage: $0 /dev/sdX"
echo ""
echo "Available devices:"
lsblk -d -o NAME,SIZE,MODEL,TRAN | grep -E "sd|mmcblk" || true
exit 1
fi
DEVICE="$1"
if [ ! -b "$DEVICE" ]; then
error "Device not found: $DEVICE"
fi
# Safety: refuse whole-disk devices that look like system disks
if mount | grep -q "^${DEVICE}"; then
warn "Device $DEVICE has mounted partitions:"
mount | grep "^${DEVICE}" || true
echo ""
read -p "Continue anyway? (y/N) " -n 1 -r
echo
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
fi
#------------------------------------------------------------------------------
# Verify binaries
#------------------------------------------------------------------------------
SDFUSE="$SCRIPT_DIR/bootloader/u-boot-rk3326/sd_fuse"
DTS_DIR="$SCRIPT_DIR/bootloader/u-boot-rk3326/arch/arm/dts"
for f in "$SDFUSE/idbloader.img" "$SDFUSE/uboot.img" "$SDFUSE/trust.img"; do
[ -f "$f" ] || error "Missing: $f"
done
for f in "$DTS_DIR/r36s-uboot.dtb" "$DTS_DIR/r36-uboot.dtb"; do
[ -f "$f" ] || error "Missing: $f"
done
log "Device: $DEVICE"
log "Binaries: $SDFUSE"
log ""
log "Will flash:"
log " idbloader.img → sector 64 ($(du -h "$SDFUSE/idbloader.img" | cut -f1))"
log " uboot.img → sector 16384 ($(du -h "$SDFUSE/uboot.img" | cut -f1))"
log " trust.img → sector 24576 ($(du -h "$SDFUSE/trust.img" | cut -f1))"
log " r36s-uboot.dtb + r36-uboot.dtb → BOOT partition"
log ""
read -p "Flash to $DEVICE? (y/N) " -n 1 -r
echo
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
#------------------------------------------------------------------------------
# Flash U-Boot binaries (raw sectors)
#------------------------------------------------------------------------------
log "Flashing idbloader.img..."
pkexec dd if="$SDFUSE/idbloader.img" of="$DEVICE" bs=512 seek=64 conv=sync,noerror,notrunc 2>/dev/null
log "Flashing uboot.img..."
pkexec dd if="$SDFUSE/uboot.img" of="$DEVICE" bs=512 seek=16384 conv=sync,noerror,notrunc 2>/dev/null
log "Flashing trust.img..."
pkexec dd if="$SDFUSE/trust.img" of="$DEVICE" bs=512 seek=24576 conv=sync,noerror,notrunc 2>/dev/null
log "U-Boot binaries flashed"
#------------------------------------------------------------------------------
# Copy display DTBs to BOOT partition
#------------------------------------------------------------------------------
BOOT_PART="${DEVICE}1"
if [ ! -b "$BOOT_PART" ]; then
# Try mmcblk style (p1)
BOOT_PART="${DEVICE}p1"
fi
if [ ! -b "$BOOT_PART" ]; then
warn "BOOT partition not found ($BOOT_PART) — display DTBs NOT copied"
warn "Copy manually: r36s-uboot.dtb + r36-uboot.dtb to BOOT partition"
else
MOUNT_TMP=$(mktemp -d)
pkexec mount "$BOOT_PART" "$MOUNT_TMP"
pkexec cp "$DTS_DIR/r36s-uboot.dtb" "$MOUNT_TMP/"
pkexec cp "$DTS_DIR/r36-uboot.dtb" "$MOUNT_TMP/"
# Verify
log "BOOT partition contents:"
ls -la "$MOUNT_TMP/"*.dtb 2>/dev/null || true
pkexec umount "$MOUNT_TMP"
rmdir "$MOUNT_TMP"
log "Display DTBs copied to BOOT partition"
fi
#------------------------------------------------------------------------------
# Done
#------------------------------------------------------------------------------
sync
log ""
log "=== Custom U-Boot flashed to $DEVICE ==="
log ""
log "Test on R36S original → should detect eMMC → r36s-uboot.dtb → logo"
log "Test on R36S clone → no eMMC → r36-uboot.dtb → logo"
log ""
log "If boot fails (red LED blink), restore working U-Boot:"
log " Original: bootloader/u-boot-r36s-working/"
log " Clone: bootloader/u-boot-clone-working/"
File diff suppressed because it is too large Load Diff
+26 -12
View File
@@ -25,9 +25,12 @@ except ImportError:
sys.exit(1)
# Volume step (percentage per key press)
VOL_STEP = 2
VOL_STEP = 5
# Brightness step (percentage per key press)
BRIGHT_STEP = 3
BRIGHT_STEP = 5
# Minimum interval between volume/brightness actions (seconds)
# adc-keys autorepeat fires at ~30Hz — throttle to ~3 events/sec
VOL_THROTTLE = 0.3
# Minimum brightness percentage (prevent black screen)
BRIGHT_MIN = 5
# Brightness persistence file
@@ -177,6 +180,7 @@ def brightness_up():
def brightness_down():
if get_brightness_pct() <= BRIGHT_MIN:
return
log(f"BRIGHT- brightnessctl s {BRIGHT_STEP}%-")
run_cmd(f"brightnessctl -q s {BRIGHT_STEP}%-")
# Clamp: if we went below minimum, set to minimum
if get_brightness_pct() < BRIGHT_MIN:
@@ -203,7 +207,7 @@ def find_devices():
name = dev.name.lower()
caps = dev.capabilities()
if 'gpio-keys' in name:
if 'gpio-keys' in name or 'adc-keys' in name:
# Distinguish vol device from gamepad by checking for KEY_VOLUMEUP
key_caps = caps.get(ecodes.EV_KEY, [])
if ecodes.KEY_VOLUMEUP in key_caps:
@@ -253,6 +257,8 @@ def main():
# Track MODE button state for brightness hotkey combo
mode_held = False
# Throttle: last time a volume/brightness action was executed
last_vol_action = 0.0
print("Hotkey daemon ready.")
# Clear previous log on fresh start
@@ -298,18 +304,26 @@ def main():
elif val == 0:
mode_held = False
# Volume keys from gpio-keys-vol (grabbed)
# Volume keys (grabbed): accept press + repeat,
# but throttle to max ~3 events/sec (300ms interval).
# adc-keys autorepeat fires at ~30Hz natively.
elif key == ecodes.KEY_VOLUMEUP and val in (1, 2):
if mode_held:
brightness_up()
else:
volume_up()
now = time.monotonic()
if now - last_vol_action >= VOL_THROTTLE:
last_vol_action = now
if mode_held:
brightness_up()
else:
volume_up()
elif key == ecodes.KEY_VOLUMEDOWN and val in (1, 2):
if mode_held:
brightness_down()
else:
volume_down()
now = time.monotonic()
if now - last_vol_action >= VOL_THROTTLE:
last_vol_action = now
if mode_held:
brightness_down()
else:
volume_down()
# Headphone jack switch
elif event.type == ecodes.EV_SW:
+325
View File
@@ -0,0 +1,325 @@
/*
* archr-init — initramfs /init for Arch R
*
* Shows splash on /dev/fb0 IMMEDIATELY after kernel boot (before systemd).
* Splash image is EMBEDDED in the binary (no file I/O needed).
* Then mounts the real root filesystem and switch_root to /sbin/init.
*
* Initramfs contents:
* /init → this binary (static aarch64, includes splash data)
* /dev/ → empty (devtmpfs mounted here)
* /proc/ → empty (proc mounted temporarily)
* /newroot/ → empty (real root mounted here)
*
* Build:
* xxd -i splash.bmp > splash_data.h
* aarch64-linux-gnu-gcc -static -O2 -o archr-init archr-init.c
*/
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <linux/fb.h>
#include <errno.h>
#include <time.h>
/* Embedded splash image (generated by xxd -i splash.bmp) */
#include "splash_data.h"
/* ---- Logging (dmesg + in-memory buffer, flushed to file after root mount) ---- */
static int kmsg_fd = -1;
#define LOG_BUF_SIZE 4096
static char log_buf[LOG_BUF_SIZE];
static int log_pos = 0;
/* Simple snprintf — avoid pulling in full stdio */
static int fmt_int(char *buf, int val)
{
if (val < 0) { *buf++ = '-'; val = -val; return 1 + fmt_int(buf, val); }
if (val >= 10) { int n = fmt_int(buf, val / 10); return n + fmt_int(buf + n, val % 10); }
*buf = '0' + val;
return 1;
}
static void klog(const char *msg)
{
/* Save msg pointer before we iterate */
const char *saved_msg = msg;
/* Write to dmesg */
if (kmsg_fd < 0)
kmsg_fd = open("/dev/kmsg", O_WRONLY);
if (kmsg_fd >= 0) {
char buf[256];
int i = 0;
const char *prefix = "archr-init: ";
while (*prefix) buf[i++] = *prefix++;
while (*msg && i < 250) buf[i++] = *msg++;
buf[i++] = '\n';
write(kmsg_fd, buf, i);
}
/* Buffer for file log (using saved pointer) */
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
if (log_pos < LOG_BUF_SIZE - 200) {
log_pos += fmt_int(log_buf + log_pos, (int)ts.tv_sec);
log_buf[log_pos++] = '.';
int ms = (int)(ts.tv_nsec / 1000000);
if (ms < 100) log_buf[log_pos++] = '0';
if (ms < 10) log_buf[log_pos++] = '0';
log_pos += fmt_int(log_buf + log_pos, ms);
log_buf[log_pos++] = ' ';
while (*saved_msg && log_pos < LOG_BUF_SIZE - 2)
log_buf[log_pos++] = *saved_msg++;
log_buf[log_pos++] = '\n';
}
}
static void klog_num(const char *prefix, int val)
{
char buf[128];
int i = 0;
while (*prefix && i < 100) buf[i++] = *prefix++;
i += fmt_int(buf + i, val);
buf[i] = 0;
klog(buf);
}
static void flush_log(const char *path)
{
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
write(fd, log_buf, log_pos);
close(fd);
}
}
/* ---- BMP parsing (from embedded data) ---- */
#pragma pack(push, 1)
typedef struct {
uint16_t type;
uint32_t size;
uint16_t reserved1;
uint16_t reserved2;
uint32_t offset;
} BMPFileHeader;
typedef struct {
uint32_t size;
int32_t width;
int32_t height;
uint16_t planes;
uint16_t bpp;
uint32_t compression;
uint32_t image_size;
int32_t x_ppm;
int32_t y_ppm;
uint32_t colors_used;
uint32_t colors_important;
} BMPInfoHeader;
#pragma pack(pop)
/* ---- Splash display (writes embedded BMP to /dev/fb0) ---- */
static void show_splash(void)
{
/* Validate embedded BMP data */
/* splash_data.h defines: unsigned char splash_bmp[] and unsigned int splash_bmp_len */
extern unsigned char splash_bmp[];
extern unsigned int splash_bmp_len;
if (splash_bmp_len < sizeof(BMPFileHeader) + sizeof(BMPInfoHeader)) {
klog("splash: embedded data too small");
return;
}
BMPFileHeader *fh = (BMPFileHeader *)splash_bmp;
BMPInfoHeader *ih = (BMPInfoHeader *)(splash_bmp + sizeof(BMPFileHeader));
if (fh->type != 0x4D42 || (ih->bpp != 24 && ih->bpp != 32)) {
klog("splash: invalid BMP header");
return;
}
int32_t bmp_w = ih->width;
int32_t bmp_h = ih->height > 0 ? ih->height : -ih->height;
int bottom_up = ih->height > 0;
uint32_t src_bpp = ih->bpp / 8;
uint32_t src_row_padded = (bmp_w * src_bpp + 3) & ~3;
uint8_t *pixel_data = splash_bmp + fh->offset;
klog("splash: BMP parsed from embedded data");
/* Wait for fb0 (DRM probe might still be finishing) */
int fb_fd = -1;
int retries;
for (retries = 0; retries < 30; retries++) {
fb_fd = open("/dev/fb0", O_RDWR);
if (fb_fd >= 0) break;
usleep(100000); /* 100ms, max 3s */
}
if (fb_fd < 0) {
klog("splash: fb0 NOT FOUND after 3s");
return;
}
klog_num("splash: fb0 opened, retries=", retries);
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0 ||
ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0) {
klog("splash: ioctl failed");
close(fb_fd);
return;
}
uint32_t fb_w = vinfo.xres;
uint32_t fb_h = vinfo.yres;
uint32_t fb_bpp_bytes = vinfo.bits_per_pixel / 8;
uint32_t stride = finfo.line_length;
uint32_t fb_size = stride * fb_h;
klog_num("splash: fb width=", (int)fb_w);
klog_num("splash: fb height=", (int)fb_h);
klog_num("splash: fb bpp=", (int)vinfo.bits_per_pixel);
/* Write directly to fb0 — row by row to avoid large malloc */
/* First: clear the framebuffer (black) */
{
uint8_t zero[2560]; /* enough for 640px * 4bpp */
memset(zero, 0, sizeof(zero));
lseek(fb_fd, 0, SEEK_SET);
for (uint32_t y = 0; y < fb_h; y++)
write(fb_fd, zero, stride < sizeof(zero) ? stride : sizeof(zero));
}
/* Then: write BMP pixels */
uint32_t copy_w = (uint32_t)bmp_w < fb_w ? (uint32_t)bmp_w : fb_w;
uint32_t copy_h = (uint32_t)bmp_h < fb_h ? (uint32_t)bmp_h : fb_h;
uint8_t row_buf[2560]; /* enough for 640px * 4bpp */
for (uint32_t y = 0; y < copy_h; y++) {
uint32_t src_y = bottom_up ? (bmp_h - 1 - y) : y;
uint8_t *src_row = pixel_data + src_y * src_row_padded;
memset(row_buf, 0, stride < sizeof(row_buf) ? stride : sizeof(row_buf));
for (uint32_t x = 0; x < copy_w; x++) {
uint8_t b = src_row[x * src_bpp + 0];
uint8_t g = src_row[x * src_bpp + 1];
uint8_t r = src_row[x * src_bpp + 2];
if (fb_bpp_bytes == 4) {
row_buf[x * 4 + 0] = b;
row_buf[x * 4 + 1] = g;
row_buf[x * 4 + 2] = r;
row_buf[x * 4 + 3] = 0xFF;
} else if (fb_bpp_bytes == 2) {
uint16_t c = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
row_buf[x * 2 + 0] = c & 0xFF;
row_buf[x * 2 + 1] = (c >> 8) & 0xFF;
}
}
lseek(fb_fd, y * stride, SEEK_SET);
write(fb_fd, row_buf, stride < sizeof(row_buf) ? stride : sizeof(row_buf));
}
klog("splash: written to fb0");
close(fb_fd);
}
/* ---- Main: initramfs /init ---- */
int main(int argc, char *argv[])
{
/* 1. Mount devtmpfs on /dev (kernel creates device nodes here) */
mkdir("/dev", 0755);
mount("devtmpfs", "/dev", "devtmpfs", 0, NULL);
klog("=== INITRAMFS STARTED ===");
/* 2. Show splash from embedded data — no file I/O needed */
show_splash();
/* 3. Parse root= and rw from kernel cmdline */
mkdir("/proc", 0755);
mount("proc", "/proc", "proc", 0, NULL);
char cmdline[4096];
memset(cmdline, 0, sizeof(cmdline));
int fd = open("/proc/cmdline", O_RDONLY);
if (fd >= 0) {
read(fd, cmdline, sizeof(cmdline) - 1);
close(fd);
}
umount("/proc");
/* Extract root= device path */
char rootdev[256] = "/dev/mmcblk1p2"; /* default: original R36S */
char *p = strstr(cmdline, "root=");
if (p) {
p += 5;
int i = 0;
while (*p && *p != ' ' && i < 255)
rootdev[i++] = *p++;
rootdev[i] = 0;
}
/* Check rw flag */
unsigned long mflags = MS_NOATIME;
if (strstr(cmdline, " rw"))
mflags |= 0; /* rw is default without MS_RDONLY */
else
mflags |= MS_RDONLY;
klog(rootdev);
/* 4. Mount real root (retry — MMC might still be probing) */
mkdir("/newroot", 0755);
int mounted = 0;
int mount_retries;
for (mount_retries = 0; mount_retries < 50; mount_retries++) {
if (mount(rootdev, "/newroot", "ext4", mflags, NULL) == 0) {
mounted = 1;
break;
}
usleep(100000); /* 100ms, max 5s total */
}
if (!mounted) {
klog("FATAL: could not mount root!");
for (;;) sleep(60);
}
klog_num("root mounted, retries=", mount_retries);
/* 5. Write diagnostic log to root filesystem */
klog("switch_root");
flush_log("/newroot/var/log/archr-init.log");
/* 6. Move /dev to new root */
mount("/dev", "/newroot/dev", NULL, MS_MOVE, NULL);
/* 7. switch_root: pivot to real rootfs and exec systemd */
if (kmsg_fd >= 0) close(kmsg_fd);
chdir("/newroot");
mount(".", "/", NULL, MS_MOVE, NULL);
chroot(".");
chdir("/");
execl("/sbin/init", "/sbin/init", NULL);
/* Should never reach here */
return 1;
}
+2 -2
View File
@@ -50,8 +50,8 @@ _bt "after_amixer"
# === BACKGROUND: Everything else runs parallel with ES SDL init (~2s window) ===
(
# Framebuffer blank (hides login text, ES takes over via KMSDRM anyway)
dd if=/dev/zero of=/dev/fb0 bs=614400 count=1 2>/dev/null
# NOTE: fb0 blank removed — archr-splash.service writes splash to fb0,
# which persists until ES takes DRM master via KMSDRM.
# Permissions + governors (single sudo)
sudo sh -c '/usr/local/bin/perfmax; chmod 666 /dev/tty1 /dev/dri/* /sys/class/backlight/backlight/brightness' 2>/dev/null
+83 -44
View File
@@ -11,12 +11,17 @@
#==============================================================================
FIRST_BOOT_FLAG="/var/lib/archr/.first-boot-done"
LOG_FILE="/var/log/first-boot.log"
# Log all output for debugging
exec > >(tee -a "$LOG_FILE") 2>&1
if [ -f "$FIRST_BOOT_FLAG" ]; then
echo "First boot already completed, skipping"
exit 0
fi
echo "=== Arch R First Boot Setup ==="
echo "=== Arch R First Boot Setup === ($(date))"
#------------------------------------------------------------------------------
# Detect SD card device
@@ -32,29 +37,49 @@ echo "SD card: $ROOT_DISK"
# Create ROMS partition (partition 3) if it doesn't exist
#------------------------------------------------------------------------------
ROMS_PART="${ROOT_DISK}p3"
ROMS_OK=false
if ! lsblk "$ROMS_PART" &>/dev/null; then
echo "Creating ROMS partition..."
# Get the end of the last partition (in sectors)
LAST_END=$(sfdisk -l "$ROOT_DISK" 2>/dev/null | awk '/^\/dev/ {end=$3} END {print end+1}')
DISK_SECTORS=$(sfdisk -l "$ROOT_DISK" 2>/dev/null | awk '/sectors$/ {print $7; exit}')
# Parse partition info using sfdisk dump format (reliable, no column alignment issues)
LAST_INFO=$(sfdisk -d "$ROOT_DISK" 2>/dev/null | grep '^/dev' | tail -1)
PART_START=$(echo "$LAST_INFO" | grep -o 'start=[[:space:]]*[0-9]*' | grep -o '[0-9]*')
PART_SIZE=$(echo "$LAST_INFO" | grep -o 'size=[[:space:]]*[0-9]*' | grep -o '[0-9]*')
if [ -n "$LAST_END" ] && [ -n "$DISK_SECTORS" ] && [ "$LAST_END" -lt "$DISK_SECTORS" ]; then
echo "${LAST_END},+,0c" | sfdisk --append "$ROOT_DISK" 2>/dev/null || \
echo ",,0c" | sfdisk --append "$ROOT_DISK" 2>/dev/null || true
if [ -n "$PART_START" ] && [ -n "$PART_SIZE" ]; then
LAST_END=$((PART_START + PART_SIZE))
DISK_SECTORS=$(blockdev --getsz "$ROOT_DISK" 2>/dev/null)
echo " Last partition ends at sector $LAST_END, disk has $DISK_SECTORS sectors"
if [ -n "$DISK_SECTORS" ] && [ "$LAST_END" -lt "$DISK_SECTORS" ]; then
echo "${LAST_END},+,0c" | sfdisk --force --append "$ROOT_DISK" 2>&1 || \
echo ",,0c" | sfdisk --force --append "$ROOT_DISK" 2>&1 || \
echo " ERROR: sfdisk --append failed"
else
echo " ERROR: No free space on disk (last=$LAST_END, total=$DISK_SECTORS)"
fi
else
echo ",,0c" | sfdisk --append "$ROOT_DISK" 2>/dev/null || true
echo " WARNING: Could not parse partition table, trying generic append"
echo ",,0c" | sfdisk --force --append "$ROOT_DISK" 2>&1 || \
echo " ERROR: sfdisk --append failed"
fi
partprobe "$ROOT_DISK"
sleep 3
# Tell kernel about the new partition (try multiple methods)
partprobe "$ROOT_DISK" 2>/dev/null
sleep 2
# Retry partprobe if device not yet visible
if ! lsblk "$ROMS_PART" &>/dev/null; then
# partprobe failed — try partx (more reliable for adding single partitions)
partx -a "$ROOT_DISK" 2>/dev/null
sleep 2
partprobe "$ROOT_DISK"
sleep 2
fi
if ! lsblk "$ROMS_PART" &>/dev/null; then
# Last resort: blockdev re-read
blockdev --rereadpt "$ROOT_DISK" 2>/dev/null
sleep 3
fi
if lsblk "$ROMS_PART" &>/dev/null; then
@@ -62,15 +87,18 @@ if ! lsblk "$ROMS_PART" &>/dev/null; then
mkfs.vfat -F 32 -n ROMS "$ROMS_PART"
echo " ROMS partition created!"
else
echo " WARNING: Failed to create ROMS partition"
echo " You can create it manually: echo ',,0c' | sudo sfdisk --append $ROOT_DISK"
echo " WARNING: Partition created in table but kernel can't see it yet"
echo " ROMS will be available after reboot"
fi
else
echo " ROMS partition already exists"
# Ensure it has a filesystem
if ! blkid "$ROMS_PART" | grep -q vfat; then
# Ensure it has a FAT32 filesystem with the ROMS label
if ! blkid "$ROMS_PART" | grep -q 'TYPE="vfat"'; then
echo " Formatting existing partition as FAT32..."
mkfs.vfat -F 32 -n ROMS "$ROMS_PART"
elif ! blkid "$ROMS_PART" | grep -q 'LABEL="ROMS"'; then
echo " Setting label to ROMS..."
fatlabel "$ROMS_PART" ROMS 2>/dev/null || true
fi
fi
@@ -82,30 +110,39 @@ echo "Setting up ROM directories..."
mkdir -p /roms
if lsblk "$ROMS_PART" &>/dev/null; then
mount "$ROMS_PART" /roms 2>/dev/null || true
if ! mountpoint -q /roms; then
mount "$ROMS_PART" /roms 2>&1 || echo " WARNING: Could not mount $ROMS_PART"
fi
fi
SYSTEMS=(
"nes" "snes" "gb" "gbc" "gba" "nds"
"megadrive" "mastersystem" "gamegear" "genesis" "segacd" "sega32x"
"n64" "psx" "psp"
"dreamcast" "saturn"
"arcade" "mame" "fbneo" "neogeo"
"atari2600" "atari7800" "atarilynx"
"pcengine" "pcenginecd" "supergrafx"
"wonderswan" "wonderswancolor"
"ngp" "ngpc"
"virtualboy"
"scummvm" "dos"
"ports"
"bios"
)
if mountpoint -q /roms; then
ROMS_OK=true
for sys in "${SYSTEMS[@]}"; do
mkdir -p "/roms/$sys"
done
SYSTEMS=(
"nes" "snes" "gb" "gbc" "gba" "nds"
"megadrive" "mastersystem" "gamegear" "genesis" "segacd" "sega32x"
"n64" "psx" "psp"
"dreamcast" "saturn"
"arcade" "mame" "fbneo" "neogeo"
"atari2600" "atari7800" "atarilynx"
"pcengine" "pcenginecd" "supergrafx"
"wonderswan" "wonderswancolor"
"ngp" "ngpc"
"virtualboy"
"scummvm" "dos"
"ports"
"bios"
)
echo " ROM directories created"
for sys in "${SYSTEMS[@]}"; do
mkdir -p "/roms/$sys"
done
mkdir -p /roms/saves /roms/states
echo " ROM directories created"
else
echo " WARNING: /roms not mounted — directories will be created on next boot"
fi
#------------------------------------------------------------------------------
# Generate SSH host keys
@@ -141,10 +178,9 @@ if [ ! -f "$RA_DIR/retroarch.cfg" ] && [ -f /etc/archr/retroarch.cfg ]; then
cp /etc/archr/retroarch.cfg "$RA_DIR/retroarch.cfg"
fi
# Set savefile/savestate directories to per-system on ROMS partition
# Set savefile/savestate directories to ROMS partition
sed -i "s|^savefile_directory =.*|savefile_directory = \"/roms/saves\"|" "$RA_DIR/retroarch.cfg" 2>/dev/null || true
sed -i "s|^savestate_directory =.*|savestate_directory = \"/roms/states\"|" "$RA_DIR/retroarch.cfg" 2>/dev/null || true
mkdir -p /roms/saves /roms/states
#------------------------------------------------------------------------------
# Configure EmulationStation
@@ -162,9 +198,12 @@ fi
chown -R archr:archr /home/archr
#------------------------------------------------------------------------------
# Mark first boot complete
# Mark first boot complete (only if ROMS partition is working)
#------------------------------------------------------------------------------
mkdir -p "$(dirname "$FIRST_BOOT_FLAG")"
touch "$FIRST_BOOT_FLAG"
echo "=== First Boot Setup Complete ==="
if [ "$ROMS_OK" = true ]; then
mkdir -p "$(dirname "$FIRST_BOOT_FLAG")"
touch "$FIRST_BOOT_FLAG"
echo "=== First Boot Setup Complete ==="
else
echo "=== First Boot Setup Partial — ROMS partition will retry on next boot ==="
fi
+2 -2
View File
@@ -8,7 +8,7 @@
# - R36S clones (Clone Panel 1-10, R36 Max, RX6S) from compiled DTBs
# in kernel/dts/R36S-Clones-DTB/
#
# These DTBOs are applied by U-Boot via PanCho panel selection system.
# These DTBOs are applied by U-Boot via boot.ini (panel auto-detect system).
# Base DTB has Panel 4-V22 hardcoded as default (works without overlay).
#==============================================================================
@@ -475,7 +475,7 @@ for panel_name in "${CLONE_ORDER[@]}"; do
done
#------------------------------------------------------------------------------
# Create ScreenFiles directory structure (for PanCho compatibility)
# Create ScreenFiles directory structure (for boot.ini panel auto-detect)
#------------------------------------------------------------------------------
log ""
log "Creating ScreenFiles directory structure..."
+453
View File
@@ -0,0 +1,453 @@
#!/usr/bin/python3
"""
Arch R - Panel Detection Wizard
Runs on first boot (or after X-button reset) to let the user select
their display panel. Provides audio feedback (beeps) for blind selection
and visual feedback on tty1 for users who already have the correct panel.
Flow:
1. Check /boot/panel-confirmed — if valid (>1 byte), exit immediately
2. Read variant from /etc/archr/variant (original or clone)
3. Initialize audio (speaker, 60% volume)
4. Cycle through panels (most common first):
- Show panel name on tty1
- Play N beeps (N = position in list, capped at 5)
- Wait 15s for input: A=confirm, B/DPAD_DOWN=next
5. On confirm: write panel.txt + panel-confirmed → sync → reboot
6. After 2 full cycles without confirm: auto-confirm default
Panel selection is persistent:
- panel.txt: U-Boot reads PanelDTBO variable (overlay path)
- panel-confirmed: marker file (>1 byte = confirmed, ≤1 byte = reset)
- Hold X during boot to reset (U-Boot overwrites panel-confirmed with 1 byte)
"""
import math
import os
import select
import struct
import subprocess
import sys
import time
from pathlib import Path
try:
import evdev
from evdev import ecodes
except ImportError:
print("ERROR: python-evdev not installed")
sys.exit(1)
# --- Paths ---
BOOT_DIR = Path("/boot")
PANEL_TXT = BOOT_DIR / "panel.txt"
PANEL_CONFIRMED = BOOT_DIR / "panel-confirmed"
VARIANT_FILE = Path("/etc/archr/variant")
# --- Buttons (from rk3326-odroid-go.dtsi gpio-keys) ---
BTN_A = ecodes.BTN_EAST # 305 — A button (confirm)
BTN_B = ecodes.BTN_SOUTH # 304 — B button (next)
BTN_X = ecodes.BTN_NORTH # 307 — X button (reset panel selection)
BTN_DOWN = ecodes.BTN_DPAD_DOWN # 545 — D-pad down (next, alternative)
# --- Audio ---
BEEP_FREQ = 880 # Hz
BEEP_DURATION = 0.12 # seconds
BEEP_GAP = 0.15 # seconds between beeps
SAMPLE_RATE = 44100
# --- Timing ---
WAIT_PER_PANEL = 15 # seconds to wait for input per panel
MAX_CYCLES = 2 # auto-confirm default after this many full cycles
# --- Panel definitions per variant ---
# (panel_num, dtbo_path, friendly_name)
# Empty dtbo_path = default panel (hardcoded in base DTB, no overlay needed)
# Order: most common first
# R36S Original — 6 panels, default is Panel 4-V22 (~60% of units)
PANELS_ORIGINAL = [
("4", "",
"Panel 4-V22 (Default)"),
("3", "ScreenFiles/Panel 3/mipi-panel.dtbo",
"Panel 3-V20"),
("5", "ScreenFiles/Panel 5/mipi-panel.dtbo",
"Panel 5-V22 Q8"),
("0", "ScreenFiles/Panel 0/mipi-panel.dtbo",
"Panel 0"),
("1", "ScreenFiles/Panel 1/mipi-panel.dtbo",
"Panel 1-V10"),
("2", "ScreenFiles/Panel 2/mipi-panel.dtbo",
"Panel 2-V12"),
]
# R36S Clone — 12 panels, default is Clone 8 ST7703 (G80CA-MB)
PANELS_CLONE = [
("C8", "",
"Clone 8 ST7703 G80CA (Default)"),
("C1", "ScreenFiles/Clone Panel 1/mipi-panel.dtbo",
"Clone 1 (ST7703)"),
("C3", "ScreenFiles/Clone Panel 3/mipi-panel.dtbo",
"Clone 3 (NV3051D)"),
("C7", "ScreenFiles/Clone Panel 7/mipi-panel.dtbo",
"Clone 7 (JD9365DA)"),
("C9", "ScreenFiles/Clone Panel 9/mipi-panel.dtbo",
"Clone 9 (NV3051D)"),
("C10", "ScreenFiles/Clone Panel 10/mipi-panel.dtbo",
"Clone 10 (ST7703)"),
("C2", "ScreenFiles/Clone Panel 2/mipi-panel.dtbo",
"Clone 2 (ST7703)"),
("C4", "ScreenFiles/Clone Panel 4/mipi-panel.dtbo",
"Clone 4 (NV3051D)"),
("C5", "ScreenFiles/Clone Panel 5/mipi-panel.dtbo",
"Clone 5 (ST7703)"),
("C6", "ScreenFiles/Clone Panel 6/mipi-panel.dtbo",
"Clone 6 (NV3051D)"),
("MAX", "ScreenFiles/R36 Max/mipi-panel.dtbo",
"R36 Max (720x720)"),
("RX6S", "ScreenFiles/RX6S/mipi-panel.dtbo",
"RX6S (NV3051D)"),
]
def get_variant():
"""Read variant from /etc/archr/variant (written by build-image.sh)."""
try:
return VARIANT_FILE.read_text().strip()
except FileNotFoundError:
# Fallback: detect via eMMC presence
if os.path.exists('/sys/block/mmcblk1'):
return "original"
return "clone"
def get_panels(variant):
"""Return panel list for this variant."""
if variant == "clone":
return PANELS_CLONE
return PANELS_ORIGINAL
def is_confirmed():
"""Check if panel selection is already confirmed."""
if not PANEL_CONFIRMED.exists():
return False
return PANEL_CONFIRMED.stat().st_size > 1
def generate_beep_wav(freq=BEEP_FREQ, duration=BEEP_DURATION):
"""Generate a short sine wave beep as WAV data (16-bit mono PCM)."""
n = int(SAMPLE_RATE * duration)
samples = b''.join(
struct.pack('<h', int(16384 * math.sin(2 * math.pi * freq * i / SAMPLE_RATE)))
for i in range(n)
)
hdr = struct.pack(
'<4sI4s4sIHHIIHH4sI',
b'RIFF', 36 + len(samples), b'WAVE',
b'fmt ', 16, 1, 1, SAMPLE_RATE, SAMPLE_RATE * 2, 2, 16,
b'data', len(samples)
)
return hdr + samples
def play_beeps(count, wav_data):
"""Play N beeps with gaps between them."""
for i in range(count):
try:
subprocess.run(
["aplay", "-q", "-"],
input=wav_data, timeout=2,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
except Exception:
pass
if i < count - 1:
time.sleep(BEEP_GAP)
def play_confirm_sound(wav_data):
"""Play 3 rapid high beeps for confirmation feedback."""
for _ in range(3):
try:
subprocess.run(
["aplay", "-q", "-"],
input=wav_data, timeout=2,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
except Exception:
pass
time.sleep(0.08)
def write_tty(msg):
"""Write message to tty1 (visible only if current panel is correct)."""
try:
with open("/dev/tty1", "w") as tty:
tty.write("\033[2J\033[H") # clear screen
tty.write("\033[1;37m") # bright white
tty.write("=" * 42 + "\n")
tty.write(" Arch R - Panel Detection\n")
tty.write("=" * 42 + "\n\n")
tty.write(f" {msg}\n\n")
tty.write(" A = Confirm this panel\n")
tty.write(" B = Try next panel\n\n")
tty.write(" (auto-advances in 15s)\n")
tty.write("\033[0m")
tty.flush()
except Exception:
pass
def find_gamepad():
"""Find gpio-keys gamepad input device."""
for path in evdev.list_devices():
try:
dev = evdev.InputDevice(path)
caps = dev.capabilities().get(ecodes.EV_KEY, [])
if BTN_A in caps and BTN_B in caps:
return dev
except Exception:
continue
return None
def is_x_held(dev):
"""Check if X button is currently held down (for panel reset).
Works on any board — uses evdev (kernel input), not raw GPIO.
Original: X = GPIO1_PA7, Clone: X = GPIO3_PC2, but evdev
abstracts this to BTN_NORTH regardless of hardware.
"""
try:
active = dev.active_keys()
return BTN_X in active
except Exception:
return False
def reset_panel():
"""Reset panel selection (triggered by holding X during boot)."""
log_boot("X held — resetting panel selection")
# Truncate panel-confirmed to trigger wizard
try:
fd = os.open(str(PANEL_CONFIRMED), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
os.write(fd, b'\x00')
os.fsync(fd)
os.close(fd)
dir_fd = os.open(str(PANEL_CONFIRMED.parent), os.O_RDONLY)
os.fsync(dir_fd)
os.close(dir_fd)
except Exception:
pass
def wait_for_button(dev, timeout):
"""Wait for A or B button press. Returns 'A', 'B', or None (timeout)."""
# Drain pending events
while dev.read_one():
pass
deadline = time.monotonic() + timeout
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
return None
r, _, _ = select.select([dev], [], [], min(remaining, 0.5))
if not r:
continue
for event in dev.read():
if event.type != ecodes.EV_KEY or event.value != 1:
continue
if event.code == BTN_A:
return 'A'
if event.code in (BTN_B, BTN_DOWN):
return 'B'
return None
def fsync_write(path, data):
"""Write data to file with explicit fsync (critical for FAT32 on SD card).
Path.write_text() doesn't fsync — data stays in page cache.
If power is cut before writeback completes, the file is lost.
"""
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
os.write(fd, data.encode())
os.fsync(fd)
os.close(fd)
# Also fsync the directory to persist the directory entry itself
dir_fd = os.open(str(path.parent), os.O_RDONLY)
os.fsync(dir_fd)
os.close(dir_fd)
def write_panel_config(panel_num, dtbo_path):
"""Write panel.txt for U-Boot to load on next boot."""
content = f"PanelNum={panel_num}\n"
if dtbo_path:
content += f"PanelDTBO={dtbo_path}\n"
else:
content += "PanelDTBO=\n"
fsync_write(PANEL_TXT, content)
def confirm_panel():
"""Write panel-confirmed marker (content > 1 byte = confirmed)."""
fsync_write(PANEL_CONFIRMED, "confirmed\n")
def init_audio():
"""Initialize audio output for panel detection beeps."""
try:
subprocess.run(
["amixer", "-q", "sset", "Playback Path", "SPK"],
timeout=3, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
subprocess.run(
["amixer", "-q", "sset", "DAC", "60%"],
timeout=3, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
except Exception:
pass
def wait_for_boot_mount():
"""Wait for /boot to be mounted (FAT32 partition with panel files)."""
for _ in range(30):
if os.path.ismount("/boot"):
return True
time.sleep(1)
return False
def log_boot(msg):
"""Log to /boot for persistent debugging (survives tmpfs /var/log)."""
try:
with open("/boot/panel-detect.log", "a") as f:
f.write(f"{time.strftime('%H:%M:%S')} {msg}\n")
except Exception:
pass
def main():
# Ensure /boot is mounted before checking panel-confirmed
if not os.path.ismount("/boot"):
wait_for_boot_mount()
log_boot(f"start: /boot mounted={os.path.ismount('/boot')}")
log_boot(f" panel-confirmed exists={PANEL_CONFIRMED.exists()}")
if PANEL_CONFIRMED.exists():
log_boot(f" panel-confirmed size={PANEL_CONFIRMED.stat().st_size}")
# Find gamepad input device (needed for X-button check AND wizard)
gamepad = None
for _ in range(10):
gamepad = find_gamepad()
if gamepad:
break
time.sleep(1)
# Check if X is held — reset panel selection (works on any board via evdev)
if gamepad and is_x_held(gamepad):
reset_panel()
log_boot("panel reset by X button")
# Quick exit if panel already confirmed
if is_confirmed():
log_boot("confirmed — exiting")
sys.exit(0)
log_boot("NOT confirmed — starting wizard")
# Determine variant and panel list
variant = get_variant()
panels = get_panels(variant)
default_panel = panels[0] # First in list is always the default
print(f"Arch R Panel Detection Wizard starting (variant: {variant})...")
print(f" {len(panels)} panels available, default: {default_panel[2]}")
# Initialize audio
init_audio()
# Generate beep WAVs
beep = generate_beep_wav()
intro_beep = generate_beep_wav(freq=660, duration=0.3)
confirm_beep = generate_beep_wav(freq=1100, duration=0.1)
if not gamepad:
print("WARNING: Gamepad not found — auto-confirming default panel")
write_panel_config(default_panel[0], default_panel[1])
confirm_panel()
subprocess.run(["sync"])
sys.exit(0)
print(f" Gamepad: {gamepad.name} ({gamepad.path})")
# Intro: 2 long beeps to signal wizard is running
play_beeps(2, intro_beep)
time.sleep(0.5)
# Panel selection loop
for cycle in range(MAX_CYCLES):
for idx, (panel_num, dtbo_path, name) in enumerate(panels):
# Write panel config (ready for confirm)
write_panel_config(panel_num, dtbo_path)
# Visual feedback on tty1
position = f"[{idx + 1}/{len(panels)}]"
if cycle > 0:
position += f" (cycle {cycle + 1})"
write_tty(f"{position} {name}")
# Audio feedback: N beeps (capped at 5)
beep_count = min(idx + 1, 5)
play_beeps(beep_count, beep)
print(f" {position} {name} — waiting...")
# Wait for button input
result = wait_for_button(gamepad, WAIT_PER_PANEL)
if result == 'A':
print(f" CONFIRMED: {name}")
play_confirm_sound(confirm_beep)
confirm_panel()
subprocess.run(["sync"])
if dtbo_path:
# Non-default panel: overlay applied by U-Boot on next reset
write_tty(f"Confirmed: {name}\n\n Press RESET to apply.")
print(f" Non-default panel — waiting for RESET")
# Hold here until user presses RESET (no timeout)
while True:
time.sleep(60)
else:
# Default panel: no overlay needed, continue booting
write_tty(f"Confirmed: {name}")
print(f" Default panel — continuing boot")
sys.exit(0)
elif result == 'B':
print(f" NEXT (B pressed)")
continue
else:
print(f" TIMEOUT — advancing")
continue
# After MAX_CYCLES without confirmation: auto-confirm default
print(f" No selection made — auto-confirming default {default_panel[2]}")
write_panel_config(default_panel[0], default_panel[1])
write_tty(f"Auto-confirmed: {default_panel[2]}")
play_confirm_sound(generate_beep_wav(freq=440, duration=0.2))
confirm_panel()
subprocess.run(["sync"])
if __name__ == "__main__":
main()