Created
February 26, 2026 06:56
-
-
Save RGreeneRI/cae2341222687d97cb45571f282d04e5 to your computer and use it in GitHub Desktop.
Create an embedded LIVI instance on a tablet from a clean base Debian install.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -e | |
| # Run this after installing a minimal debian 13 with only standard system utilities and SSH server. | |
| # Be sure to create user "carplay" during install. | |
| # Define variables | |
| CARPLAY_USER="carplay" | |
| USER_HOME=$(getent passwd "$CARPLAY_USER" | cut -d: -f6) | |
| PROFILE_FILE="$USER_HOME/.profile" | |
| AUTOLOGIN_FILE_PATH="/etc/systemd/system/getty@tty1.service.d" | |
| AUTOLOGIN_FILE="/etc/systemd/system/getty@tty1.service.d/autologin.conf" | |
| RULE_FILE="/etc/udev/rules.d/99-LIVI.rules" | |
| START_SH="$USER_HOME/LIVI/start.sh" | |
| GRUB_FILE="/etc/default/grub" | |
| KERNEL_PRINTK_FILE="/etc/sysctl.d/20-quiet-printk.conf" | |
| GRUBD_FILE_10="/etc/grub.d/10_linux" | |
| LIVI_URL="https://github.com/f-io/LIVI/releases/download/v5.2.1/LIVI-5.2.1-x86_64.AppImage" | |
| # Check if carplay user exists, exit if it doesn't. | |
| echo "Ensuring carplay user exists..." | |
| if ! id "$CARPLAY_USER" >/dev/null 2>&1; then | |
| echo "User $CARPLAY_USER does not exist." | |
| exit 1 | |
| fi | |
| # Install required packages | |
| echo "Installing required packages..." | |
| apt update && apt install -y -qq --no-install-recommends \ | |
| cron wget sudo ca-certificates \ | |
| linux-image-amd64 firmware-linux \ | |
| libinput10 wayland-protocols \ | |
| cage foot acpi mesa-vulkan-drivers \ | |
| mesa-utils xserver-xorg-input-libinput \ | |
| fuse libfuse2t64 libnspr4 libnss3 \ | |
| libatk1.0-0t64 libatk-bridge2.0-0t64 \ | |
| libcups2t64 libcairo2 libgtk-3-0t64 \ | |
| libinput-tools wlr-randr libinput-bin | |
| # Add carplay user to sudo group | |
| echo "Adding $CARPLAY_USER to sudo group..." | |
| usermod -aG sudo $CARPLAY_USER | |
| # Create autologin file | |
| echo "Creating autologin file..." | |
| mkdir -p "$AUTOLOGIN_FILE_PATH" | |
| cat <<EOF > "$AUTOLOGIN_FILE" | |
| [Service] | |
| ExecStart= | |
| ExecStart=-/sbin/agetty --autologin $CARPLAY_USER --noclear %I \$TERM | |
| EOF | |
| # Create LIVI dir and download LIVI AppImage. | |
| echo "Downloading LIVI..." | |
| sudo -u "$CARPLAY_USER" mkdir -p "$USER_HOME/LIVI" | |
| sudo -u "$CARPLAY_USER" wget -O "$USER_HOME/LIVI/LIVI.AppImage" "$LIVI_URL" | |
| sudo -u "$CARPLAY_USER" chmod +x "$USER_HOME/LIVI/LIVI.AppImage" | |
| # Set udev rules for dongle and disabling the trackpad | |
| echo "Creating udev rules for dongle and Touchpad." | |
| echo "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"1314\", ATTR{idProduct}==\"152*\", MODE=\"0660\", OWNER=\"$CARPLAY_USER\"" > "$RULE_FILE" | |
| cat <<'EOF' >> "$RULE_FILE" | |
| ATTRS{name}=="*Touchpad*", ENV{LIBINPUT_IGNORE_DEVICE}="1" | |
| ATTRS{name}=="*Mouse*", ENV{LIBINPUT_IGNORE_DEVICE}="1" | |
| EOF | |
| udevadm control --reload-rules | |
| udevadm trigger | |
| # Ensure profile exists | |
| echo "Setting up profile file..." | |
| touch "$PROFILE_FILE" | |
| chown "$CARPLAY_USER:$CARPLAY_USER" "$PROFILE_FILE" | |
| # Remove existing LIVI_LAUNCH block if present | |
| sed -i '/# >>> LIVI_LAUNCH START >>>/,/# <<< LIVI_LAUNCH END <<</d' "$PROFILE_FILE" | |
| # Append fresh block | |
| cat <<EOF >> "$PROFILE_FILE" | |
| # >>> LIVI_LAUNCH START >>> | |
| alias ll='ls -lah' | |
| # Launch LIVI | |
| if [ -z "\$WAYLAND_DISPLAY" ] && [ "\$(tty)" = "/dev/tty1" ]; then | |
| exec cage "$START_SH" | |
| fi | |
| # <<< LIVI_LAUNCH END <<< | |
| EOF | |
| # Create start script with crash-loop protection | |
| echo "Creating start script..." | |
| sudo -u "$CARPLAY_USER" touch "$START_SH" | |
| sudo -u "$CARPLAY_USER" chmod +x "$START_SH" | |
| cat <<'EOF' > "$START_SH" | |
| #!/bin/bash | |
| # Maximum number of quick restarts allowed | |
| MAX_RESTARTS=5 | |
| # Time window in seconds to count restarts | |
| WINDOW=10 | |
| restart_count=0 | |
| first_restart_time=$(date +%s) | |
| while true; do | |
| "$HOME/LIVI/LIVI.AppImage" | |
| # Check how quickly it exited | |
| now=$(date +%s) | |
| if (( now - first_restart_time <= WINDOW )); then | |
| ((restart_count++)) | |
| else | |
| # Reset count if enough time has passed | |
| restart_count=1 | |
| first_restart_time=$now | |
| fi | |
| # If it crashes too fast repeatedly, pause longer to avoid CPU burn | |
| if (( restart_count > MAX_RESTARTS )); then | |
| echo "LIVI crashed $restart_count times within $WINDOW seconds. Waiting before restart..." | |
| sleep 10 | |
| restart_count=0 | |
| first_restart_time=$(date +%s) | |
| else | |
| # Short pause before normal restart | |
| sleep 1 | |
| fi | |
| done | |
| EOF | |
| # Reduce boot time and screen clutter by tweaking grub | |
| echo "Tweaking grub, etc for quicker and cleaner boot..." | |
| # Set GRUB_TIMEOUT=0 | |
| if grep -q "^GRUB_TIMEOUT=" "$GRUB_FILE"; then | |
| sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/' "$GRUB_FILE" | |
| else | |
| echo 'GRUB_TIMEOUT=0' >> "$GRUB_FILE" | |
| fi | |
| # Set GRUB_CMDLINE_LINUX_DEFAULT | |
| NEW_CMDLINE='GRUB_CMDLINE_LINUX_DEFAULT="quiet loglevel=1 vt.global_cursor_default=0 systemd.show_status=false rd.udev.log_level=3 nowatchdog mitigations=off video=efifb:nobgrt console=tty3"' | |
| if grep -q "^GRUB_CMDLINE_LINUX_DEFAULT=" "$GRUB_FILE"; then | |
| sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|$NEW_CMDLINE|" "$GRUB_FILE" | |
| else | |
| echo "$NEW_CMDLINE" >> "$GRUB_FILE" | |
| fi | |
| echo 'GRUB_TIMEOUT_STYLE=hidden' >> "$GRUB_FILE" | |
| # Ensure GRUB_TERMINAL=console is set in $GRUB_FILE | |
| if grep -qE '^[[:space:]]*#?[[:space:]]*GRUB_TERMINAL=' "$GRUB_FILE"; then | |
| # Replace existing (commented or not) line | |
| sed -i 's|^[[:space:]]*#\?[[:space:]]*GRUB_TERMINAL=.*|GRUB_TERMINAL=console|' "$GRUB_FILE" | |
| else | |
| # Add it to the end of the file | |
| echo 'GRUB_TERMINAL=console' >> "$GRUB_FILE" | |
| fi | |
| sed -i 's/quiet_boot="0"/quiet_boot="1"/g' "$GRUBD_FILE_10" | |
| echo "kernel.printk = 3 3 3 3" > "$KERNEL_PRINTK_FILE" | |
| update-grub | |
| # more screen clutter removal | |
| echo "Clearing MOTD..." | |
| > /etc/motd | |
| rm /etc/update-motd.d/10-uname | |
| # Create power monitor script | |
| echo "Creating power monitor script..." | |
| touch /root/power-monitor.sh | |
| chmod +x /root/power-monitor.sh | |
| cat <<'EOF' > "/root/power-monitor.sh" | |
| #!/bin/bash | |
| ACPI_CMD="/usr/bin/acpi" | |
| SHUTDOWN_SECS="5" | |
| RECHECK_SECS="2" | |
| # Loop to check power status | |
| while true; do | |
| # Get uptime | |
| UPTIME_SECS=$(cut -d. -f1 /proc/uptime) | |
| # Get the battery status | |
| STATUS=$($ACPI_CMD -a) | |
| # Check if the tablet is plugged in (AC connected) | |
| if [[ "$STATUS" != *"on-line"* ]]; then | |
| # If not plugged in, initiate shutdown | |
| echo "AC adapter is unplugged." | |
| if [[ $UPTIME_SECS -ge 300 ]]; then | |
| echo "Initiating shutdown in $SHUTDOWN_SECS seconds..." | |
| sleep $SHUTDOWN_SECS | |
| /sbin/shutdown -P now | |
| exit 0 | |
| fi | |
| echo "Uptime under 5 minutes, staying on in case you booted up unplugged." | |
| echo "Disabling auto-shutdown." | |
| exit 0 | |
| fi | |
| if [[ "$STATUS" == *"on-line"* ]]; then | |
| echo "AC adapter is plugged in. Checking again in $RECHECK_SECS seconds." | |
| fi | |
| # Sleep for X seconds before checking again | |
| sleep $RECHECK_SECS | |
| done | |
| EOF | |
| # Add power monitor cron job | |
| echo "Creating power monitor cron job..." | |
| CRON_CMD="/root/power-monitor.sh" | |
| CRON_JOB="@reboot $CRON_CMD" | |
| EXISTING_CRON=$(crontab -l -u root 2>/dev/null || true) | |
| if ! echo "$EXISTING_CRON" | grep -Fq "$CRON_CMD"; then | |
| printf "%s\n%s\n" "$EXISTING_CRON" "$CRON_JOB" | crontab -u root - | |
| echo "Cron job added." | |
| else | |
| echo "Cron job already exists." | |
| fi | |
| echo "Done setting up LIVI embedded tablet! Reboot to load LIVI." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment