Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 47 additions & 40 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,77 +9,84 @@
# Installation & Paths
# ============================================================================
installation:
gadget_dir: /home/pi/TeslaUSB # Installation directory where TeslaUSB is installed
target_user: pi # Linux user running the TeslaUSB services
mount_dir: /mnt/gadget # Mount directory for USB drives
gadget_dir: /home/pi/TeslaUSB # Installation directory where TeslaUSB is installed
target_user: pi # Linux user running the TeslaUSB services
mount_dir: /mnt/gadget # Mount directory for USB drives

# ============================================================================
# Disk Images
# ============================================================================
disk_images:
cam_name: usb_cam.img # TeslaCam disk image filename
cam_name: usb_cam.img # TeslaCam disk image filename
lightshow_name: usb_lightshow.img # LightShow disk image filename
cam_label: TeslaCam # Filesystem label for TeslaCam drive
lightshow_label: Lightshow # Filesystem label for LightShow drive
boot_fsck_enabled: true # Auto-repair filesystems on boot (recommended)
cam_label: TeslaCam # Filesystem label for TeslaCam drive
lightshow_label: Lightshow # Filesystem label for LightShow drive
music_name: usb_music.img # Music disk image filename (optional third LUN)
music_label: Music # Filesystem label for Music drive (Tesla expects FAT32)
music_enabled: true # Create and present music partition (LUN2)
music_fs: fat32 # Filesystem for music image (fat32 recommended for Tesla)
boot_fsck_enabled: true # Auto-repair filesystems on boot (recommended)

# ============================================================================
# Setup Configuration (used only by setup_usb.sh)
# ============================================================================
# These values are specific to the setup process and can be configured here
# or left empty ("") for interactive prompts during setup.
setup:
part1_size: "" # TeslaCam drive size (e.g., "50G" or leave empty for interactive)
part2_size: "" # LightShow drive size (e.g., "10G" or leave empty for interactive)
reserve_size: "" # Headroom to leave free on Pi filesystem (default: 5G)
part1_size: "" # TeslaCam drive size (e.g., "50G" or leave empty for interactive)
part2_size: "" # LightShow drive size (e.g., "10G" or leave empty for interactive)
part3_size: "" # Music drive size (e.g., "32G" or leave empty to skip/interactive)
reserve_size: "" # Headroom to leave free on Pi filesystem (default: 5G)

# ============================================================================
# Network & Security
# ============================================================================
network:
samba_password: tesla # Samba password for authenticated user (CHANGE THIS!)
web_port: 80 # Web interface port (80 required for captive portal - do not change)
samba_password: tesla # Samba password for authenticated user (CHANGE THIS!)
web_port: 80 # Web interface port (80 required for captive portal - do not change)

# ============================================================================
# Offline Access Point Configuration
# ============================================================================
# Fallback WiFi access point for in-car/mobile access when STA WiFi is unavailable
offline_ap:
enabled: true # Set to false to disable fallback AP
interface: wlan0 # WiFi interface used for AP/STA
ssid: TeslaUSB # SSID broadcast when AP is active (CHANGE THIS!)
passphrase: teslausb1234 # WPA2 passphrase 8-63 chars (CHANGE THIS!)
channel: 6 # 2.4GHz channel (1-11)
ipv4_cidr: 192.168.4.1/24 # Static IP for AP interface
dhcp_start: 192.168.4.10 # DHCP range start
dhcp_end: 192.168.4.50 # DHCP range end
check_interval: 20 # Seconds between health checks
disconnect_grace: 30 # Seconds offline before starting AP
min_rssi: -70 # Minimum RSSI (dBm) to tear down AP
stable_seconds: 20 # Seconds of good link before stopping AP
ping_target: 8.8.8.8 # Ping target to confirm WAN reachability
retry_seconds: 300 # While AP is active, retry STA join every N seconds
virtual_interface: uap0 # Virtual AP interface name (concurrent mode always enabled)
force_mode: auto # Persistent force mode: auto, force_on, force_off
enabled: true # Set to false to disable fallback AP
interface: wlan0 # WiFi interface used for AP/STA
ssid: TeslaUSB # SSID broadcast when AP is active (CHANGE THIS!)
passphrase: teslausb1234 # WPA2 passphrase 8-63 chars (CHANGE THIS!)
channel: 6 # 2.4GHz channel (1-11)
ipv4_cidr: 192.168.4.1/24 # Static IP for AP interface
dhcp_start: 192.168.4.10 # DHCP range start
dhcp_end: 192.168.4.50 # DHCP range end
check_interval: 20 # Seconds between health checks
disconnect_grace: 30 # Seconds offline before starting AP
min_rssi: -70 # Minimum RSSI (dBm) to tear down AP
stable_seconds: 20 # Seconds of good link before stopping AP
ping_target: 8.8.8.8 # Ping target to confirm WAN reachability
retry_seconds: 300 # While AP is active, retry STA join every N seconds
virtual_interface: uap0 # Virtual AP interface name (concurrent mode always enabled)
force_mode: auto # Persistent force mode: auto, force_on, force_off

# ============================================================================
# System Configuration File Paths
# ============================================================================
system:
config_file: /boot/firmware/config.txt # Raspberry Pi boot configuration
samba_conf: /etc/samba/smb.conf # Samba configuration file
config_file: /boot/firmware/config.txt # Raspberry Pi boot configuration
samba_conf: /etc/samba/smb.conf # Samba configuration file

# ============================================================================
# Web Application Configuration
# ============================================================================
web:
secret_key: CHANGE-THIS-TO-A-RANDOM-SECRET-KEY-ON-FIRST-INSTALL # Flask secret key (auto-generated if default)
max_lock_chime_size: 1048576 # 1 MiB (1024 * 1024 bytes)
max_lock_chime_duration: 10.0 # 10 seconds (configurable per Tesla model)
min_lock_chime_duration: 0.3 # 300ms minimum
speed_range_min: 0.5 # Half speed (audio trimmer)
speed_range_max: 2.0 # Double speed (audio trimmer)
speed_step: 0.05 # Fine-grained control (audio trimmer)
lock_chime_filename: LockChime.wav # Active lock chime filename
chimes_folder: Chimes # Folder on part2 where custom chimes are stored
lightshow_folder: LightShow # Folder on part2 where light shows are stored
secret_key: CHANGE-THIS-TO-A-RANDOM-SECRET-KEY-ON-FIRST-INSTALL # Flask secret key (auto-generated if default)
max_lock_chime_size: 1048576 # 1 MiB (1024 * 1024 bytes)
max_lock_chime_duration: 10.0 # 10 seconds (configurable per Tesla model)
min_lock_chime_duration: 0.3 # 300ms minimum
speed_range_min: 0.5 # Half speed (audio trimmer)
speed_range_max: 2.0 # Double speed (audio trimmer)
speed_step: 0.05 # Fine-grained control (audio trimmer)
lock_chime_filename: LockChime.wav # Active lock chime filename
chimes_folder: Chimes # Folder on part2 where custom chimes are stored
lightshow_folder: LightShow # Folder on part2 where light shows are stored
max_upload_size_mb: 2048 # Max accepted upload size for music/lightshow (MiB)
max_upload_chunk_mb: 16 # Chunk size for streaming uploads (MiB)
10 changes: 8 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,13 @@ When WiFi is unavailable, the Pi automatically creates a fallback access point:
- Download all camera views as zip file
- Delete entire events (Edit mode only) - deletes all camera views for the session

**Music Tab**:
- Tesla scans music only from a root-level `Music` folder; the app enforces this and automatically creates it if missing
- Browse folders with breadcrumb navigation and clean per-folder views
- Drag-and-drop or select files and whole folders; chunked uploads keep memory low and preserve subfolder structure
- Per-file progress and status indicators with size limit validation
- Create folders, move files, and delete files or entire folders (Edit mode)
- Usage gauge shows used/free space for the music partition

**Lock Chimes Tab**:
- Upload WAV/MP3 files (auto-converted to Tesla format)
Expand Down Expand Up @@ -413,7 +420,7 @@ sudo systemctl disable present_usb_on_boot.service

The hardware watchdog automatically reboots the Pi if the system becomes unresponsive. The default configuration is intentionally simple and reliable:

```
```bash
watchdog-device = /dev/watchdog
watchdog-timeout = 60
max-load-1 = 24
Expand Down Expand Up @@ -532,4 +539,3 @@ All screenshots shown in dark mode.
<img src="examples/lock-chimes.png" alt="Lock Chimes Management" width="400">
<img src="examples/lock-chime-editor-waveform.png" alt="Lock Chime Audio Editor with Waveform" width="400">
<img src="examples/light-shows.png" alt="Light Shows Management" width="400">

6 changes: 6 additions & 0 deletions scripts/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,16 @@ eval "$(yq -r '
"MNT_DIR=\"" + .installation.mount_dir + "\"",
"IMG_CAM_NAME=\"" + .disk_images.cam_name + "\"",
"IMG_LIGHTSHOW_NAME=\"" + .disk_images.lightshow_name + "\"",
"IMG_MUSIC_NAME=\"" + (.disk_images.music_name // "usb_music.img") + "\"",
"LABEL1=\"" + .disk_images.cam_label + "\"",
"LABEL2=\"" + .disk_images.lightshow_label + "\"",
"LABEL3=\"" + (.disk_images.music_label // "Music") + "\"",
"MUSIC_ENABLED=\"" + (.disk_images.music_enabled // true | tostring) + "\"",
"MUSIC_FS=\"" + (.disk_images.music_fs // "fat32") + "\"",
"BOOT_FSCK_ENABLED=\"" + (.disk_images.boot_fsck_enabled | tostring) + "\"",
"PART1_SIZE=\"" + .setup.part1_size + "\"",
"PART2_SIZE=\"" + .setup.part2_size + "\"",
"PART3_SIZE=\"" + (.setup.part3_size // "") + "\"",
"RESERVE_SIZE=\"" + .setup.reserve_size + "\"",
"SAMBA_PASS=\"" + .network.samba_password + "\"",
"WEB_PORT=\"" + (.network.web_port | tostring) + "\"",
Expand Down Expand Up @@ -74,4 +79,5 @@ eval "$(yq -r '
# ============================================================================
IMG_CAM="$GADGET_DIR/$IMG_CAM_NAME"
IMG_LIGHTSHOW="$GADGET_DIR/$IMG_LIGHTSHOW_NAME"
IMG_MUSIC="$GADGET_DIR/$IMG_MUSIC_NAME"
STATE_FILE="$GADGET_DIR/state.txt"
100 changes: 95 additions & 5 deletions scripts/edit_usb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ log_timing "Script start"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/config.sh"

MUSIC_ENABLED_LC="$(printf '%s' "${MUSIC_ENABLED:-false}" | tr '[:upper:]' '[:lower:]')"
MUSIC_ENABLED_BOOL=0
[ "$MUSIC_ENABLED_LC" = "true" ] && MUSIC_ENABLED_BOOL=1

# Check for active file operations before proceeding
LOCK_FILE="$GADGET_DIR/.quick_edit_part2.lock"
LOCK_TIMEOUT=30
Expand Down Expand Up @@ -171,7 +175,11 @@ if [ -d "$CONFIGFS_GADGET" ]; then
# NOW unmount read-only mounts after gadget is fully disconnected
echo "Unmounting read-only mounts from present mode..."
RO_MNT_DIR="/mnt/gadget"
for mp in "$RO_MNT_DIR/part1-ro" "$RO_MNT_DIR/part2-ro"; do
RO_UNMOUNT_TARGETS=("$RO_MNT_DIR/part1-ro" "$RO_MNT_DIR/part2-ro")
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
RO_UNMOUNT_TARGETS+=("$RO_MNT_DIR/part3-ro")
fi
for mp in "${RO_UNMOUNT_TARGETS[@]}"; do
if mountpoint -q "$mp" 2>/dev/null; then
echo " Unmounting $mp..."
if ! safe_unmount_dir "$mp"; then
Expand All @@ -191,7 +199,11 @@ elif lsmod | grep -q '^g_mass_storage'; then
# Unmount any read-only mounts from present mode first
echo "Unmounting read-only mounts from present mode..."
RO_MNT_DIR="/mnt/gadget"
for mp in "$RO_MNT_DIR/part1-ro" "$RO_MNT_DIR/part2-ro"; do
LEGACY_RO_TARGETS=("$RO_MNT_DIR/part1-ro" "$RO_MNT_DIR/part2-ro")
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
LEGACY_RO_TARGETS+=("$RO_MNT_DIR/part3-ro")
fi
for mp in "${LEGACY_RO_TARGETS[@]}"; do
if mountpoint -q "$mp" 2>/dev/null; then
echo " Unmounting $mp..."
if ! safe_unmount_dir "$mp"; then
Expand Down Expand Up @@ -229,7 +241,11 @@ fi

# Verify all mounts are released (quick check - already unmounted above)
RO_MNT_DIR="/mnt/gadget"
for mp in "$RO_MNT_DIR/part1-ro" "$RO_MNT_DIR/part2-ro"; do
VERIFY_RO_TARGETS=("$RO_MNT_DIR/part1-ro" "$RO_MNT_DIR/part2-ro")
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
VERIFY_RO_TARGETS+=("$RO_MNT_DIR/part3-ro")
fi
for mp in "${VERIFY_RO_TARGETS[@]}"; do
if sudo nsenter --mount=/proc/1/ns/mnt mountpoint -q "$mp" 2>/dev/null; then
echo " Clearing remaining mount: $mp"
safe_unmount_dir "$mp" || true
Expand All @@ -241,7 +257,11 @@ log_timing "Mounts released"
# After clearing LUN files and unmounting, loop devices may still exist
# We must detach them before creating fresh ones to avoid accumulation
echo "Cleaning up existing loop devices..."
for img in "$IMG_CAM" "$IMG_LIGHTSHOW"; do
LOOP_IMAGES=("$IMG_CAM" "$IMG_LIGHTSHOW")
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
LOOP_IMAGES+=("$IMG_MUSIC")
fi
for img in "${LOOP_IMAGES[@]}"; do
for loop in $(losetup -j "$img" 2>/dev/null | cut -d: -f1); do
if [ -n "$loop" ]; then
echo " Detaching $loop..."
Expand All @@ -260,7 +280,12 @@ sudo chown "$TARGET_USER:$TARGET_USER" "$MNT_DIR/part1" "$MNT_DIR/part2"

# Ensure previous mounts are cleared before setting up new loop devices
# This prevents remounting while drives are still in use
for PART_NUM in 1 2; do
PART_RANGE=(1 2)
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
PART_RANGE+=(3)
fi

for PART_NUM in "${PART_RANGE[@]}"; do
MP="$MNT_DIR/part${PART_NUM}"
if mountpoint -q "$MP" 2>/dev/null; then
echo "Unmounting existing mount at $MP"
Expand Down Expand Up @@ -314,6 +339,28 @@ if [ -z "$VERIFY" ]; then
fi
echo "Verified: $LOOP_LIGHTSHOW is attached to $IMG_LIGHTSHOW"

if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
echo "Setting up loop device for Music..."
LOOP_MUSIC=$(create_loop "$IMG_MUSIC")
if [ -z "$LOOP_MUSIC" ]; then
echo "ERROR: Failed to get/create loop device for $IMG_MUSIC"
sudo losetup -d "$LOOP_CAM" 2>/dev/null || true
sudo losetup -d "$LOOP_LIGHTSHOW" 2>/dev/null || true
exit 1
fi
echo "Using loop device for Music: $LOOP_MUSIC"

VERIFY=$(sudo losetup -l | grep "$LOOP_MUSIC" | grep "$IMG_MUSIC" || true)
if [ -z "$VERIFY" ]; then
echo "ERROR: Loop device $LOOP_MUSIC is not attached to $IMG_MUSIC"
sudo losetup -d "$LOOP_CAM" 2>/dev/null || true
sudo losetup -d "$LOOP_LIGHTSHOW" 2>/dev/null || true
sudo losetup -d "$LOOP_MUSIC" 2>/dev/null || true
exit 1
fi
echo "Verified: $LOOP_MUSIC is attached to $IMG_MUSIC"
fi

sleep 0.5

# Trap to log on failure but NOT detach loop devices (they may be reused/shared)
Expand All @@ -333,6 +380,17 @@ trap log_failure_on_exit EXIT
# Mount drives
echo "Mounting drives..."

# Ensure mount points exist (present mode may remove them)
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
sudo mkdir -p "$MNT_DIR/part1" "$MNT_DIR/part2" "$MNT_DIR/part3"
else
sudo mkdir -p "$MNT_DIR/part1" "$MNT_DIR/part2"
fi
sudo chown "$TARGET_USER:$TARGET_USER" "$MNT_DIR/part1" "$MNT_DIR/part2" 2>/dev/null || true
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
sudo chown "$TARGET_USER:$TARGET_USER" "$MNT_DIR/part3" 2>/dev/null || true
fi

# Mount TeslaCam drive (part1) in system mount namespace
MP="$MNT_DIR/part1"
FS_TYPE=$(sudo blkid -o value -s TYPE "$LOOP_CAM" 2>/dev/null || echo "unknown")
Expand Down Expand Up @@ -373,11 +431,36 @@ if ! sudo nsenter --mount=/proc/1/ns/mnt mountpoint -q "$MP"; then
fi
echo " Mounted $LOOP_LIGHTSHOW at $MP (filesystem: $FS_TYPE)"

if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
echo "Mounting Music drive (part3) in system mount namespace"
MP="$MNT_DIR/part3"
FS_TYPE=$(sudo blkid -o value -s TYPE "$LOOP_MUSIC" 2>/dev/null || echo "unknown")
echo " Mounting $LOOP_MUSIC at $MP..."

if [ "$FS_TYPE" = "exfat" ]; then
sudo nsenter --mount=/proc/1/ns/mnt mount -t exfat -o rw,uid=$UID_VAL,gid=$GID_VAL,umask=000 "$LOOP_MUSIC" "$MP"
elif [ "$FS_TYPE" = "vfat" ]; then
sudo nsenter --mount=/proc/1/ns/mnt mount -t vfat -o rw,uid=$UID_VAL,gid=$GID_VAL,umask=000 "$LOOP_MUSIC" "$MP"
else
echo " Warning: Unknown filesystem type '$FS_TYPE', attempting generic mount"
sudo nsenter --mount=/proc/1/ns/mnt mount -o rw "$LOOP_MUSIC" "$MP"
fi

if ! sudo nsenter --mount=/proc/1/ns/mnt mountpoint -q "$MP"; then
echo "Error: Failed to mount $LOOP_MUSIC at $MP" >&2
exit 1
fi
echo " Mounted $LOOP_MUSIC at $MP (filesystem: $FS_TYPE)"
fi

# Refresh Samba so shares expose the freshly mounted drives
echo "Refreshing Samba shares..."
# Close any cached shares and reload config (faster than full restart)
sudo smbcontrol all close-share gadget_part1 2>/dev/null || true
sudo smbcontrol all close-share gadget_part2 2>/dev/null || true
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
sudo smbcontrol all close-share gadget_part3 2>/dev/null || true
fi
# If Samba is running, reload config is sufficient; otherwise start it
if systemctl is-active --quiet smbd; then
sudo smbcontrol all reload-config 2>/dev/null || true
Expand All @@ -393,6 +476,9 @@ fi
if [ -d "$MNT_DIR/part2" ]; then
echo " Part2 files: $(ls -A "$MNT_DIR/part2" 2>/dev/null | wc -l) items"
fi
if [ $MUSIC_ENABLED_BOOL -eq 1 ] && [ -d "$MNT_DIR/part3" ]; then
echo " Part3 files: $(ls -A "$MNT_DIR/part3" 2>/dev/null | wc -l) items"
fi

echo "Updating mode state..."
echo "edit" > "$STATE_FILE"
Expand All @@ -406,6 +492,10 @@ echo "Drives are now mounted locally and accessible via Samba shares:"
echo " - Part 1: $MNT_DIR/part1"
echo " - Part 2: $MNT_DIR/part2"
echo " - Samba shares: gadget_part1, gadget_part2"
if [ $MUSIC_ENABLED_BOOL -eq 1 ]; then
echo " - Part 3: $MNT_DIR/part3"
echo " - Samba shares: gadget_part3 (music)"
fi

log_timing "Script completed successfully"
echo "[PERFORMANCE] Total execution time: $(($(date +%s%3N) - SCRIPT_START))ms"
Loading