-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathsetup_usb.sh
More file actions
1916 lines (1648 loc) · 69.1 KB
/
setup_usb.sh
File metadata and controls
1916 lines (1648 loc) · 69.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
set -euo pipefail
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ===== Early memory optimization (critical for Pi Zero/2W) =====
# Enable swap and free memory BEFORE any package installations
early_memory_optimization() {
echo "Preparing system memory for installation..."
# Stop lightdm to free ~100MB RAM (critical for package installs)
if systemctl is-active --quiet lightdm 2>/dev/null; then
echo " Stopping display manager to free memory..."
systemctl stop lightdm 2>/dev/null || true
fi
# Enable swap if available
if [ -f /var/swap/fsck.swap ]; then
swapon /var/swap/fsck.swap 2>/dev/null || true
fi
# Drop caches to free memory
sync
echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true
echo " Memory optimization complete"
}
# Handle legacy /var/swap file (Raspberry Pi OS creates it as a file;
# we need it to be a directory for /var/swap/fsck.swap)
if [ -f "/var/swap" ] && [ ! -d "/var/swap" ]; then
echo " Moving legacy /var/swap file to /var/swap.old..."
swapoff /var/swap 2>/dev/null || true
mv /var/swap /var/swap.old
fi
# Run early optimization before any package installs
early_memory_optimization
# Check if yq is installed (required to read config.yaml)
if ! command -v yq &> /dev/null; then
echo "yq is not installed. Installing yq and python3-yaml..."
apt-get update -qq
apt-get install -y yq python3-yaml
echo "✓ yq and python3-yaml installed"
fi
# Source the configuration file
if [ -f "$SCRIPT_DIR/scripts/config.sh" ]; then
source "$SCRIPT_DIR/scripts/config.sh"
else
echo "Error: Configuration file not found at $SCRIPT_DIR/scripts/config.sh"
exit 1
fi
# Validate that required config values are set
if [ -z "$GADGET_DIR" ] || [ -z "$TARGET_USER" ] || [ -z "$IMG_CAM_NAME" ] || [ -z "$IMG_LIGHTSHOW_NAME" ]; then
echo "Error: Required configuration values not set in config.sh"
exit 1
fi
# Override TARGET_USER if running via sudo (prefer SUDO_USER)
if [ -n "${SUDO_USER-}" ]; then
TARGET_USER="$SUDO_USER"
fi
IMG_CAM_PATH="$GADGET_DIR/$IMG_CAM_NAME"
IMG_LIGHTSHOW_PATH="$GADGET_DIR/$IMG_LIGHTSHOW_NAME"
IMG_MUSIC_PATH="$GADGET_DIR/$IMG_MUSIC_NAME"
# ===== Image Dashboard Functions =====
# Format bytes to human-readable GiB/MiB string
bytes_to_human() {
local bytes="$1"
local mib=$(( bytes / 1024 / 1024 ))
if [ "$mib" -ge 1024 ]; then
local gib_int=$(( mib / 1024 ))
local gib_frac=$(( (mib % 1024) * 10 / 1024 ))
echo "${gib_int}.${gib_frac} GiB"
else
echo "${mib} MiB"
fi
}
show_image_dashboard() {
local total_logical=0
local image_lines=""
# Collect per-image info
for img_label_pair in "TeslaCam:$IMG_CAM_PATH" "Lightshow:$IMG_LIGHTSHOW_PATH" "Music:$IMG_MUSIC_PATH"; do
local label="${img_label_pair%%:*}"
local path="${img_label_pair#*:}"
# Skip music if not required
if [ "$label" = "Music" ] && [ "$MUSIC_REQUIRED" -eq 0 ]; then
continue
fi
if [ -f "$path" ]; then
local logical_bytes
logical_bytes=$(stat --format=%s "$path" 2>/dev/null || echo 0)
local fs_type
fs_type=$(blkid -o value -s TYPE "$path" 2>/dev/null || echo "unknown")
total_logical=$(( total_logical + logical_bytes ))
image_lines+="$(printf " %-10s %-10s %s (%s)" "$label:" "$(bytes_to_human $logical_bytes)" "$path" "$fs_type")\n"
else
image_lines+="$(printf " %-10s %-10s %s" "$label:" "MISSING" "$path")\n"
fi
done
# Filesystem totals
mkdir -p "$GADGET_DIR" 2>/dev/null || true
local fs_total_bytes fs_free_bytes os_reserve_bytes free_for_images_bytes
fs_total_bytes=$(df -B1 --output=size "$GADGET_DIR" | tail -n 1 | tr -d ' ')
fs_free_bytes=$(df -B1 --output=avail "$GADGET_DIR" | tail -n 1 | tr -d ' ')
# OS reserve = total size - free space - space used by everything (including images)
# free_for_images = fs_free (already excludes existing files) + existing image logical sizes - those logical sizes
# Simpler: free_for_images = total - os_used - image_logical
# where os_used = total - free - image_logical_on_disk... but df free already accounts for real disk usage
# Most accurate: OS reserve = total - free - total_logical (of existing images)
# This treats image logical size as "committed" even if sparse
local fs_used_bytes
fs_used_bytes=$(df -B1 --output=used "$GADGET_DIR" | tail -n 1 | tr -d ' ')
os_reserve_bytes=$(( fs_used_bytes - total_logical ))
# If images aren't fully allocated (sparse), os_reserve could be negative — clamp to 0
if [ "$os_reserve_bytes" -lt 0 ]; then
os_reserve_bytes=0
fi
# Add the configured safety headroom (default 5G)
local safety_bytes=$(( 5 * 1024 * 1024 * 1024 ))
local os_reserve_display=$(( os_reserve_bytes + safety_bytes ))
# Archive reserve for RecentClips backup
local archive_reserve_str="${ARCHIVE_RESERVE_SIZE:-50G}"
local archive_reserve_bytes=0
if [[ "$archive_reserve_str" =~ ^([0-9]+)([Gg])$ ]]; then
archive_reserve_bytes=$(( ${BASH_REMATCH[1]} * 1024 * 1024 * 1024 ))
fi
free_for_images_bytes=$(( fs_total_bytes - os_reserve_display - archive_reserve_bytes - total_logical ))
if [ "$free_for_images_bytes" -lt 0 ]; then
free_for_images_bytes=0
fi
echo ""
echo "============================================"
echo "Existing Image Dashboard"
echo "============================================"
echo ""
printf " Total storage: %s\n" "$(bytes_to_human $fs_total_bytes)"
printf " OS reserve: %s (OS + 5 GiB headroom)\n" "$(bytes_to_human $os_reserve_display)"
printf " Archive reserve: %s (RecentClips backup)\n" "$(bytes_to_human $archive_reserve_bytes)"
echo " ────────────────────────────────────────"
printf "%b" "$image_lines"
echo " ────────────────────────────────────────"
printf " Free for new images: %s\n" "$(bytes_to_human $free_for_images_bytes)"
echo ""
}
delete_all_images() {
echo "Deleting all existing image files..."
for img_pair in "TeslaCam:$IMG_CAM_PATH" "Lightshow:$IMG_LIGHTSHOW_PATH" "Music:$IMG_MUSIC_PATH"; do
local label="${img_pair%%:*}"
local path="${img_pair#*:}"
if [ -f "$path" ]; then
rm -f "$path"
echo " Deleted: $path ($label)"
fi
done
echo "All image files deleted."
echo ""
}
# ===== Check if image files already exist =====
MUSIC_ENABLED_LC="$(printf '%s' "${MUSIC_ENABLED:-false}" | tr '[:upper:]' '[:lower:]')"
MUSIC_REQUIRED=$([ "$MUSIC_ENABLED_LC" = "true" ] && echo 1 || echo 0)
# Count existing images
EXISTING_COUNT=0
[ -f "$IMG_CAM_PATH" ] && EXISTING_COUNT=$((EXISTING_COUNT + 1))
[ -f "$IMG_LIGHTSHOW_PATH" ] && EXISTING_COUNT=$((EXISTING_COUNT + 1))
if [ $MUSIC_REQUIRED -eq 1 ] && [ -f "$IMG_MUSIC_PATH" ]; then
EXISTING_COUNT=$((EXISTING_COUNT + 1))
fi
REQUIRED_COUNT=2
[ $MUSIC_REQUIRED -eq 1 ] && REQUIRED_COUNT=3
MISSING_COUNT=$(( REQUIRED_COUNT - EXISTING_COUNT ))
if [ "$EXISTING_COUNT" -eq 0 ]; then
# ── Path A: Fresh install ──
echo "No existing image files found. Will create all required images."
SKIP_IMAGE_CREATION=0
NEED_CAM_IMAGE=1
NEED_LIGHTSHOW_IMAGE=1
NEED_MUSIC_IMAGE=$MUSIC_REQUIRED
echo ""
else
# ── Path B: Upgrade (some or all images exist) ──
show_image_dashboard
# Determine which images are missing
NEED_CAM_IMAGE=0
NEED_LIGHTSHOW_IMAGE=0
NEED_MUSIC_IMAGE=0
[ ! -f "$IMG_CAM_PATH" ] && NEED_CAM_IMAGE=1
[ ! -f "$IMG_LIGHTSHOW_PATH" ] && NEED_LIGHTSHOW_IMAGE=1
[ $MUSIC_REQUIRED -eq 1 ] && [ ! -f "$IMG_MUSIC_PATH" ] && NEED_MUSIC_IMAGE=1
# Build menu options dynamically
echo "What would you like to do?"
echo ""
OPTION_NUM=1
OPT_CREATE_MISSING=""
OPT_DELETE_ALL=""
OPT_KEEP=""
if [ "$MISSING_COUNT" -gt 0 ]; then
OPT_CREATE_MISSING="$OPTION_NUM"
MISSING_NAMES=""
[ "$NEED_CAM_IMAGE" -eq 1 ] && MISSING_NAMES="${MISSING_NAMES}TeslaCam "
[ "$NEED_LIGHTSHOW_IMAGE" -eq 1 ] && MISSING_NAMES="${MISSING_NAMES}Lightshow "
[ "$NEED_MUSIC_IMAGE" -eq 1 ] && MISSING_NAMES="${MISSING_NAMES}Music "
echo " ${OPTION_NUM}) Create missing image(s): ${MISSING_NAMES}(using available space)"
OPTION_NUM=$((OPTION_NUM + 1))
fi
OPT_DELETE_ALL="$OPTION_NUM"
echo " ${OPTION_NUM}) Delete ALL images and reconfigure sizes"
OPTION_NUM=$((OPTION_NUM + 1))
OPT_KEEP="$OPTION_NUM"
echo " ${OPTION_NUM}) Keep existing images, skip image configuration"
echo ""
read -r -p "Select an option [${OPT_KEEP}]: " UPGRADE_CHOICE
UPGRADE_CHOICE="${UPGRADE_CHOICE:-$OPT_KEEP}"
if [ -n "$OPT_CREATE_MISSING" ] && [ "$UPGRADE_CHOICE" = "$OPT_CREATE_MISSING" ]; then
# Option: Create only missing images
echo ""
echo "Will create only missing image(s)."
SKIP_IMAGE_CREATION=0
elif [ "$UPGRADE_CHOICE" = "$OPT_DELETE_ALL" ]; then
# Option: Delete all and reconfigure
echo ""
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ WARNING: This will permanently delete ALL image files ║"
echo "║ and their contents. ║"
echo "║ ║"
echo "║ You can download your lock chimes, light shows, wraps, ║"
echo "║ and other content from the TeslaUSB web UI before ║"
echo "║ proceeding. ║"
echo "╚══════════════════════════════════════════════════════════╝"
echo ""
read -r -p "Type YES to confirm deletion: " CONFIRM_DELETE
if [ "$CONFIRM_DELETE" != "YES" ]; then
echo "Deletion not confirmed. Aborting."
exit 0
fi
echo ""
delete_all_images
SKIP_IMAGE_CREATION=0
NEED_CAM_IMAGE=1
NEED_LIGHTSHOW_IMAGE=1
NEED_MUSIC_IMAGE=$MUSIC_REQUIRED
elif [ "$UPGRADE_CHOICE" = "$OPT_KEEP" ]; then
# Option: Keep existing, skip configuration
echo ""
echo "Keeping existing images. Skipping size configuration and image creation."
SKIP_IMAGE_CREATION=1
else
echo "Invalid option. Aborting."
exit 1
fi
echo ""
fi
# ===== Friendly image sizing (safe defaults; avoid filling rootfs) =====
mib_to_gib_str() {
local mib="$1"
local gib=$(( mib / 1024 ))
if [ "$gib" -lt 1 ]; then
echo "${mib}M"
else
echo "${gib}G"
fi
}
round_down_gib_mib() {
local mib="$1"
local rounded=$(( (mib / 1024) * 1024 ))
if [ "$rounded" -lt 512 ]; then
rounded=512
fi
echo "$rounded"
}
fs_avail_bytes_for_path() {
local path="$1"
df -B1 --output=avail "$path" | tail -n 1 | tr -d ' '
}
size_to_bytes() {
local s="$1"
if [[ "$s" =~ ^([0-9]+)([Mm])$ ]]; then
echo $(( ${BASH_REMATCH[1]} * 1024 * 1024 ))
elif [[ "$s" =~ ^([0-9]+)([Gg])$ ]]; then
echo $(( ${BASH_REMATCH[1]} * 1024 * 1024 * 1024 ))
else
echo "Invalid size format: $s (use 512M or 5G)" >&2
exit 2
fi
}
# If sizes are not configured and we need to create images, suggest safe defaults based on free space
# on the filesystem that will store the image files (GADGET_DIR).
NEED_SIZE_VALIDATION=0
USABLE_MIB=0
if [ "$SKIP_IMAGE_CREATION" = "0" ] && { [ -z "${PART1_SIZE}" ] || [ -z "${PART2_SIZE}" ] || { [ $MUSIC_REQUIRED -eq 1 ] && [ -z "${PART3_SIZE}" ]; }; }; then
# Ensure parent directory exists for df check
mkdir -p "$GADGET_DIR" 2>/dev/null || true
FS_AVAIL_BYTES="$(fs_avail_bytes_for_path "$GADGET_DIR")"
# Headroom: default 5G, user-adjustable
DEFAULT_RESERVE_STR="5G"
if [ -z "${RESERVE_SIZE}" ]; then
read -r -p "OS reserve — headroom to leave free (default ${DEFAULT_RESERVE_STR}): " RESERVE_INPUT
RESERVE_SIZE="${RESERVE_INPUT:-$DEFAULT_RESERVE_STR}"
fi
RESERVE_BYTES="$(size_to_bytes "$RESERVE_SIZE")"
# Archive reserve: space set aside for RecentClips archival on SD card
ARCHIVE_RESERVE_STR="${ARCHIVE_RESERVE_SIZE:-50G}"
ARCHIVE_RESERVE_BYTES="$(size_to_bytes "$ARCHIVE_RESERVE_STR")"
TOTAL_RESERVE_BYTES=$(( RESERVE_BYTES + ARCHIVE_RESERVE_BYTES ))
if [ "$FS_AVAIL_BYTES" -le "$TOTAL_RESERVE_BYTES" ]; then
echo "ERROR: Not enough free space to safely create image files under $GADGET_DIR."
echo "Free: $((FS_AVAIL_BYTES / 1024 / 1024)) MiB"
echo "OS reserve: $RESERVE_SIZE ($((RESERVE_BYTES / 1024 / 1024)) MiB)"
echo "Archive reserve: $ARCHIVE_RESERVE_STR ($((ARCHIVE_RESERVE_BYTES / 1024 / 1024)) MiB)"
echo "Free up space or move GADGET_DIR to a larger filesystem."
exit 1
fi
USABLE_BYTES=$(( FS_AVAIL_BYTES - TOTAL_RESERVE_BYTES ))
USABLE_MIB=$(( USABLE_BYTES / 1024 / 1024 ))
# Default sizes: Lightshow 10G, Music 32G (if enabled), remaining to TeslaCam
DEFAULT_P2_MIB=10240
DEFAULT_P2_STR="10G"
DEFAULT_P3_MIB=32768
DEFAULT_P3_STR="32G"
# Compute suggestions only for images being created
SUG_P1_MIB=0
SUG_P1_STR=""
SUG_P2_STR=""
SUG_P3_STR=""
# Count how many images need creation
IMAGES_TO_CREATE=0
[ "$NEED_CAM_IMAGE" = "1" ] && IMAGES_TO_CREATE=$((IMAGES_TO_CREATE + 1))
[ "$NEED_LIGHTSHOW_IMAGE" = "1" ] && IMAGES_TO_CREATE=$((IMAGES_TO_CREATE + 1))
[ "$NEED_MUSIC_IMAGE" = "1" ] && IMAGES_TO_CREATE=$((IMAGES_TO_CREATE + 1))
if [ "$IMAGES_TO_CREATE" -eq 1 ]; then
# Single missing image gets all usable space as suggestion
SINGLE_MIB="$(round_down_gib_mib $USABLE_MIB)"
SINGLE_STR="$(mib_to_gib_str "$SINGLE_MIB")"
if [ "$NEED_CAM_IMAGE" = "1" ]; then
SUG_P1_MIB="$SINGLE_MIB"; SUG_P1_STR="$SINGLE_STR"
elif [ "$NEED_LIGHTSHOW_IMAGE" = "1" ]; then
SUG_P2_STR="$SINGLE_STR"
elif [ "$NEED_MUSIC_IMAGE" = "1" ]; then
SUG_P3_STR="$SINGLE_STR"
fi
else
# Multiple images: use defaults for lightshow/music, remainder to TeslaCam
REMAINING_MIB=$USABLE_MIB
BASELINE_MIB=0
if [ "$NEED_LIGHTSHOW_IMAGE" = "1" ]; then
SUG_P2_STR="$DEFAULT_P2_STR"
REMAINING_MIB=$(( REMAINING_MIB - DEFAULT_P2_MIB ))
BASELINE_MIB=$(( BASELINE_MIB + DEFAULT_P2_MIB ))
fi
if [ "$NEED_MUSIC_IMAGE" = "1" ]; then
SUG_P3_STR="$DEFAULT_P3_STR"
REMAINING_MIB=$(( REMAINING_MIB - DEFAULT_P3_MIB ))
BASELINE_MIB=$(( BASELINE_MIB + DEFAULT_P3_MIB ))
fi
if [ "$BASELINE_MIB" -gt 0 ] && [ "$USABLE_MIB" -le "$BASELINE_MIB" ]; then
echo "ERROR: Not enough usable space for defaults after OS reserve."
echo "Usable: ${USABLE_MIB} MiB, Baseline required: ${BASELINE_MIB} MiB"
echo "Free up space or reduce Lightshow/Music size."
exit 1
fi
if [ "$NEED_CAM_IMAGE" = "1" ]; then
SUG_P1_MIB="$(round_down_gib_mib $REMAINING_MIB)"
SUG_P1_STR="$(mib_to_gib_str "$SUG_P1_MIB")"
fi
fi
echo ""
echo "============================================"
echo "TeslaUSB image sizing"
echo "============================================"
echo "Images will be created under: $GADGET_DIR"
echo "Filesystem free space: $((FS_AVAIL_BYTES / 1024 / 1024)) MiB"
echo "OS reserve: $((RESERVE_BYTES / 1024 / 1024)) MiB"
echo "Archive reserve: $((ARCHIVE_RESERVE_BYTES / 1024 / 1024)) MiB (RecentClips backup)"
echo "Usable for images: ${USABLE_MIB} MiB"
echo ""
echo "Recommended sizes (safe, leaves headroom for Raspberry Pi OS):"
[ "$NEED_LIGHTSHOW_IMAGE" = "1" ] && echo " Lightshow (PART2_SIZE): $SUG_P2_STR"
[ "$NEED_MUSIC_IMAGE" = "1" ] && echo " Music (PART3_SIZE): $SUG_P3_STR"
[ "$NEED_CAM_IMAGE" = "1" ] && echo " TeslaCam (PART1_SIZE): $SUG_P1_STR (uses remaining usable space)"
echo ""
# Only prompt for sizes needed for missing images
if [ "$NEED_LIGHTSHOW_IMAGE" = "1" ] && [ -z "${PART2_SIZE}" ]; then
read -r -p "Enter Lightshow size (default ${SUG_P2_STR}): " PART2_SIZE_INPUT
PART2_SIZE="${PART2_SIZE_INPUT:-$SUG_P2_STR}"
# Validate format immediately
if ! size_to_bytes "$PART2_SIZE" >/dev/null 2>&1; then
echo "ERROR: Invalid size format for Lightshow: $PART2_SIZE"
echo "Use format like 512M or 5G (whole numbers only)"
exit 2
fi
elif [ "$NEED_LIGHTSHOW_IMAGE" = "0" ]; then
# Image exists, set dummy size to satisfy validation
PART2_SIZE="${PART2_SIZE:-1G}"
fi
if [ $MUSIC_REQUIRED -eq 1 ] && [ "$NEED_MUSIC_IMAGE" = "1" ] && [ -z "${PART3_SIZE}" ]; then
read -r -p "Enter Music size (default ${SUG_P3_STR}): " PART3_SIZE_INPUT
PART3_SIZE="${PART3_SIZE_INPUT:-$SUG_P3_STR}"
if ! size_to_bytes "$PART3_SIZE" >/dev/null 2>&1; then
echo "ERROR: Invalid size format for Music: $PART3_SIZE"
echo "Use format like 512M or 5G (whole numbers only)"
exit 2
fi
elif [ $MUSIC_REQUIRED -eq 1 ] && [ "$NEED_MUSIC_IMAGE" = "0" ]; then
PART3_SIZE="${PART3_SIZE:-1G}"
fi
if [ "$NEED_CAM_IMAGE" = "1" ] && [ -z "${PART1_SIZE}" ]; then
read -r -p "Enter TeslaCam size (default ${SUG_P1_STR}): " PART1_SIZE_INPUT
PART1_SIZE="${PART1_SIZE_INPUT:-$SUG_P1_STR}"
# Validate format immediately
if ! size_to_bytes "$PART1_SIZE" >/dev/null 2>&1; then
echo "ERROR: Invalid size format for TeslaCam: $PART1_SIZE"
echo "Use format like 512M or 5G (whole numbers only)"
exit 2
fi
elif [ "$NEED_CAM_IMAGE" = "0" ]; then
# Image exists, set dummy size to satisfy validation
PART1_SIZE="${PART1_SIZE:-1G}"
fi
echo ""
echo "Selected sizes:"
[ "$NEED_CAM_IMAGE" = "1" ] && echo " PART1_SIZE=$PART1_SIZE" || echo " PART1_SIZE=(existing)"
[ "$NEED_LIGHTSHOW_IMAGE" = "1" ] && echo " PART2_SIZE=$PART2_SIZE" || echo " PART2_SIZE=(existing)"
if [ $MUSIC_REQUIRED -eq 1 ]; then
[ "$NEED_MUSIC_IMAGE" = "1" ] && echo " PART3_SIZE=$PART3_SIZE" || echo " PART3_SIZE=(existing)"
fi
echo ""
NEED_SIZE_VALIDATION=1
fi
# Set default sizes if images already exist and sizes not configured
if [ "$SKIP_IMAGE_CREATION" = "1" ]; then
PART1_SIZE="${PART1_SIZE:-1G}" # Dummy value - image already exists
PART2_SIZE="${PART2_SIZE:-1G}" # Dummy value - image already exists
[ $MUSIC_REQUIRED -eq 1 ] && PART3_SIZE="${PART3_SIZE:-1G}"
fi
# Validate user exists
if ! id "$TARGET_USER" >/dev/null 2>&1; then
echo "User $TARGET_USER not found. Create it or run with a different sudo user."
exit 1
fi
TARGET_UID=$(id -u "$TARGET_USER")
TARGET_GID=$(id -g "$TARGET_USER")
echo "Target user: $TARGET_USER (uid=$TARGET_UID gid=$TARGET_GID)"
# Helper: convert size to MiB
to_mib() {
local s="$1"
if [[ "$s" =~ ^([0-9]+)([Mm])$ ]]; then
echo "${BASH_REMATCH[1]}"
elif [[ "$s" =~ ^([0-9]+)([Gg])$ ]]; then
echo $(( ${BASH_REMATCH[1]} * 1024 ))
else
echo "Invalid size format: $s (use 2048M or 4G)" >&2
exit 2
fi
}
P1_MB=$(to_mib "$PART1_SIZE")
P2_MB=$(to_mib "$PART2_SIZE")
if [ $MUSIC_REQUIRED -eq 1 ]; then
P3_MB=$(to_mib "$PART3_SIZE")
else
P3_MB=0
fi
# Note: We no longer need TOTAL_MB since we're creating separate images
# Validate selected sizes against usable space (if computed and images need creation)
if [ "${NEED_SIZE_VALIDATION:-0}" = "1" ] && [ "$SKIP_IMAGE_CREATION" = "0" ]; then
# Only sum sizes for images actually being created (exclude dummy values for existing images)
TOTAL_MIB=0
[ "$NEED_CAM_IMAGE" = "1" ] && TOTAL_MIB=$(( TOTAL_MIB + P1_MB ))
[ "$NEED_LIGHTSHOW_IMAGE" = "1" ] && TOTAL_MIB=$(( TOTAL_MIB + P2_MB ))
[ "$NEED_MUSIC_IMAGE" = "1" ] && TOTAL_MIB=$(( TOTAL_MIB + P3_MB ))
if [ "$TOTAL_MIB" -gt "$USABLE_MIB" ]; then
echo "ERROR: Selected sizes exceed safe usable space under $GADGET_DIR."
echo "Usable: ${USABLE_MIB} MiB (after OS + archive reserve)"
echo "Chosen: ${TOTAL_MIB} MiB (only counting images being created)"
echo "Reduce TeslaCam, Lightshow, and/or Music sizes."
exit 1
fi
fi
# Skip preview if both images already exist
if [ "$SKIP_IMAGE_CREATION" = "0" ]; then
echo "============================================"
echo "Preview"
echo "============================================"
if [ "$NEED_CAM_IMAGE" = "1" ] || [ "$NEED_LIGHTSHOW_IMAGE" = "1" ] || [ "$NEED_MUSIC_IMAGE" = "1" ]; then
echo "This will create the following image files:"
[ "$NEED_CAM_IMAGE" = "1" ] && echo " - TeslaCam : $IMG_CAM_PATH size=$PART1_SIZE label=$LABEL1 (read-write)" || echo " - TeslaCam : already exists"
[ "$NEED_LIGHTSHOW_IMAGE" = "1" ] && echo " - Lightshow : $IMG_LIGHTSHOW_PATH size=$PART2_SIZE label=$LABEL2 (read-only)" || echo " - Lightshow : already exists"
if [ $MUSIC_REQUIRED -eq 1 ]; then
if [ "$NEED_MUSIC_IMAGE" = "1" ]; then
echo " - Music : $IMG_MUSIC_PATH size=$PART3_SIZE label=$LABEL3 (read-only by Tesla)"
else
echo " - Music : already exists"
fi
fi
fi
echo ""
echo "Images are stored under: $GADGET_DIR"
echo "If these sizes are too large, the Pi can run out of disk and behave badly."
echo ""
read -r -p "Proceed with these sizes? [y/N]: " PROCEED
PROCEED_LC="$(printf '%s' "$PROCEED" | tr '[:upper:]' '[:lower:]')"
case "$PROCEED_LC" in
y|yes) echo "Proceeding..." ;;
*) echo "Aborted by user."; exit 0 ;;
esac
echo ""
fi
# Install prerequisites (only fetch/install if something is missing)
REQUIRED_PACKAGES=(
parted
dosfstools
exfatprogs
util-linux
psmisc
python3-flask
python3-waitress
python3-av
python3-pil
python3-yaml
python3-protobuf
python3-cryptography
protobuf-compiler
yq
samba
samba-common-bin
ffmpeg
watchdog
wireless-tools
iw
hostapd
dnsmasq
)
# Note on packages:
# - python3-yaml: YAML parser for config.yaml (shared config file)
# - yq: Command-line YAML processor for bash scripts (reads config.yaml)
# - python3-waitress: Production WSGI server (10-20x faster than Flask dev server)
# - python3-av: PyAV for video frame extraction
# - python3-pil: PIL/Pillow for image resizing
# - ffmpeg: Used by lock chime service for audio validation and re-encoding
# - rclone: Installed separately via official script (distro version is too old for OneDrive)
# - python3-cryptography: Fernet encryption for credential security
# Lightweight apt helpers (reduce OOM risk on Pi Zero/2W)
apt_update_safe() {
local attempt=1
local max_attempts=3
while [ $attempt -le $max_attempts ]; do
echo "Running apt-get update (attempt $attempt/$max_attempts)..."
if apt-get update \
-o Acquire::Retries=3 \
-o Acquire::http::No-Cache=true \
-o Acquire::Languages=none \
-o APT::Update::Reduce-Download-Size=true \
-o Acquire::PDiffs=true \
-o Acquire::http::Pipeline-Depth=0; then
return 0
fi
echo "apt-get update failed (attempt $attempt). Cleaning lists and retrying..."
rm -rf /var/lib/apt/lists/*
attempt=$((attempt + 1))
sleep 2
done
echo "apt-get update failed after $max_attempts attempts" >&2
return 1
}
install_pkg_safe() {
local pkg="$1"
echo "Installing $pkg (no-recommends)..."
if apt-get install -y --no-install-recommends "$pkg"; then
return 0
fi
echo "Retrying $pkg with default recommends..."
apt-get install -y "$pkg"
}
enable_install_swap() {
INSTALL_SWAP="/var/swap/teslausb_pkg.swap"
if swapon --show | grep -q "$INSTALL_SWAP" 2>/dev/null; then
echo "Temporary swap already active"
return
fi
echo "Enabling temporary swap for package installs (1GB)..."
# Use existing swap if available, otherwise create temporary
if [ -f "/var/swap/fsck.swap" ] && ! swapon --show | grep -q "fsck.swap" 2>/dev/null; then
echo " Using existing fsck swap file"
swapon /var/swap/fsck.swap 2>/dev/null && return
fi
# Create temporary 1GB swap
mkdir -p /var/swap
if fallocate -l 1G "$INSTALL_SWAP" 2>/dev/null || dd if=/dev/zero of="$INSTALL_SWAP" bs=1M count=1024 status=none; then
chmod 600 "$INSTALL_SWAP"
mkswap "$INSTALL_SWAP" >/dev/null 2>&1 || { echo "mkswap failed"; return 1; }
swapon "$INSTALL_SWAP" 2>/dev/null || { echo "swapon failed"; return 1; }
echo " Swap enabled: $(swapon --show | grep -E 'teslausb|fsck' || echo 'NONE - FAILED')"
else
echo "ERROR: could not create temporary swap"
return 1
fi
}
disable_install_swap() {
if [ -n "${INSTALL_SWAP-}" ] && [ -f "$INSTALL_SWAP" ]; then
swapoff "$INSTALL_SWAP" 2>/dev/null || true
rm -f "$INSTALL_SWAP"
fi
}
stop_nonessential_services() {
# Stop heavy memory users during package install (keep WiFi up)
echo "Stopping memory-intensive services..."
systemctl is-active gadget_web.service >/dev/null 2>&1 && systemctl stop gadget_web.service 2>/dev/null || true
systemctl is-active chime_scheduler.service >/dev/null 2>&1 && systemctl stop chime_scheduler.service 2>/dev/null || true
systemctl is-active chime_scheduler.timer >/dev/null 2>&1 && systemctl stop chime_scheduler.timer 2>/dev/null || true
systemctl is-active smbd >/dev/null 2>&1 && systemctl stop smbd 2>/dev/null || true
systemctl is-active nmbd >/dev/null 2>&1 && systemctl stop nmbd 2>/dev/null || true
systemctl is-active cups.service >/dev/null 2>&1 && systemctl stop cups.service 2>/dev/null || true
systemctl is-active cups-browsed.service >/dev/null 2>&1 && systemctl stop cups-browsed.service 2>/dev/null || true
systemctl is-active ModemManager.service >/dev/null 2>&1 && systemctl stop ModemManager.service 2>/dev/null || true
systemctl is-active packagekit.service >/dev/null 2>&1 && systemctl stop packagekit.service 2>/dev/null || true
systemctl is-active lightdm.service >/dev/null 2>&1 && systemctl stop lightdm.service 2>/dev/null || true
echo " Stopped active services to free memory"
}
start_nonessential_services() {
echo "Restarting services..."
systemctl is-enabled smbd >/dev/null 2>&1 && systemctl start smbd 2>/dev/null || true
systemctl is-enabled nmbd >/dev/null 2>&1 && systemctl start nmbd 2>/dev/null || true
systemctl is-enabled chime_scheduler.timer >/dev/null 2>&1 && systemctl start chime_scheduler.timer 2>/dev/null || true
systemctl is-enabled gadget_web.service >/dev/null 2>&1 && systemctl start gadget_web.service 2>/dev/null || true
# Only restart if enabled (don't re-enable lightdm if we just disabled it)
systemctl is-enabled lightdm.service >/dev/null 2>&1 && systemctl start lightdm.service 2>/dev/null || true
systemctl is-enabled cups.service >/dev/null 2>&1 && systemctl start cups.service 2>/dev/null || true
echo " Services restarted"
}
# ===== Clean up old/unused services from previous installations =====
cleanup_old_services() {
echo "Checking for old/unused services from previous installations..."
# Stop and disable old thumbnail generator service (replaced by on-demand generation)
if systemctl list-unit-files | grep -q 'thumbnail_generator'; then
echo " Removing old thumbnail_generator service..."
systemctl stop thumbnail_generator.service 2>/dev/null || true
systemctl stop thumbnail_generator.timer 2>/dev/null || true
systemctl disable thumbnail_generator.service 2>/dev/null || true
systemctl disable thumbnail_generator.timer 2>/dev/null || true
systemctl unmask thumbnail_generator.service 2>/dev/null || true
systemctl unmask thumbnail_generator.timer 2>/dev/null || true
rm -f /etc/systemd/system/thumbnail_generator.service
rm -f /etc/systemd/system/thumbnail_generator.timer
systemctl daemon-reload
echo " ✓ Removed thumbnail_generator service and timer"
fi
# Remove old template files if they exist
if [ -f "$GADGET_DIR/templates/thumbnail_generator.service" ] || [ -f "$GADGET_DIR/templates/thumbnail_generator.timer" ]; then
echo " Removing old thumbnail generator templates..."
rm -f "$GADGET_DIR/templates/thumbnail_generator.service"
rm -f "$GADGET_DIR/templates/thumbnail_generator.timer"
echo " ✓ Removed old template files"
fi
# Remove old background thumbnail generation script
if [ -f "$GADGET_DIR/scripts/generate_thumbnails.py" ]; then
echo " Removing old background thumbnail generator script..."
rm -f "$GADGET_DIR/scripts/generate_thumbnails.py"
echo " ✓ Removed generate_thumbnails.py"
fi
# Remove old wifi-powersave-off service (replaced by network-optimizations.service)
if systemctl list-unit-files | grep -q 'wifi-powersave-off'; then
echo " Removing old wifi-powersave-off service (replaced by network-optimizations)..."
systemctl stop wifi-powersave-off.service 2>/dev/null || true
systemctl disable wifi-powersave-off.service 2>/dev/null || true
rm -f /etc/systemd/system/wifi-powersave-off.service
systemctl daemon-reload
echo " ✓ Removed wifi-powersave-off service"
fi
echo "Old service cleanup complete."
}
# ===== Optimize memory for setup (disable unnecessary services) =====
optimize_memory_for_setup() {
echo "Optimizing memory for setup..."
# Disable graphical desktop services if present (saves 50-60MB on Pi Zero 2W)
if systemctl is-enabled lightdm.service >/dev/null 2>&1; then
echo " Disabling graphical desktop (lightdm)..."
systemctl stop lightdm graphical.target 2>/dev/null || true
systemctl disable lightdm 2>/dev/null || true
systemctl set-default multi-user.target 2>/dev/null || true
echo " ✓ Graphical desktop disabled (saves ~50-60MB RAM)"
else
echo " Graphical desktop not installed or already disabled"
fi
# Ensure swap is available early (critical for low-memory systems)
if ! swapon --show 2>/dev/null | grep -q '/'; then
echo " No active swap detected, enabling swap for setup..."
# Try to use existing fsck swap if available
if [ -f "/var/swap/fsck.swap" ]; then
echo " Using existing fsck.swap file"
swapon /var/swap/fsck.swap 2>/dev/null && echo " ✓ Swap enabled (fsck.swap)" && return
fi
# Try to use any existing swapfile
if [ -f "/swapfile" ]; then
echo " Using existing /swapfile"
swapon /swapfile 2>/dev/null && echo " ✓ Swap enabled (/swapfile)" && return
fi
# Create temporary swap for setup
echo " Creating temporary 512MB swap..."
if dd if=/dev/zero of=/swapfile bs=1M count=512 status=none 2>/dev/null; then
chmod 600 /swapfile
mkswap /swapfile >/dev/null 2>&1
swapon /swapfile 2>/dev/null && echo " ✓ Temporary swap created and enabled (512MB)"
else
echo " Warning: Could not create swap (may cause OOM on low-memory systems)"
fi
else
echo " Swap already active: $(swapon --show 2>/dev/null | tail -n +2 | awk '{print $1, $3}')"
fi
echo "Memory optimization complete."
echo ""
}
# Run cleanup before package installation
cleanup_old_services
# Optimize memory before package installation (critical for Pi Zero/2W)
optimize_memory_for_setup
MISSING_PACKAGES=()
for pkg in "${REQUIRED_PACKAGES[@]}"; do
if ! dpkg -s "$pkg" >/dev/null 2>&1; then
MISSING_PACKAGES+=("$pkg")
fi
done
if [ ${#MISSING_PACKAGES[@]} -gt 0 ]; then
echo "Installing missing packages: ${MISSING_PACKAGES[*]}"
# Prepare for low-memory install
stop_nonessential_services
enable_install_swap || { echo "ERROR: Failed to enable swap. Cannot proceed."; exit 1; }
# Run apt-get update
apt_update_safe
# Install packages one at a time to avoid OOM on low-memory systems
for pkg in "${MISSING_PACKAGES[@]}"; do
install_pkg_safe "$pkg" || echo "Warning: install of $pkg reported an error"
done
# Cleanup
disable_install_swap
start_nonessential_services
# Remove orphaned packages to save disk space
echo "Removing orphaned packages..."
apt-get autoremove -y >/dev/null 2>&1 || true
echo " ✓ Orphaned packages removed"
else
echo "All required packages already installed; skipping apt install."
fi
# Install rclone from official source (distro version is too old for OneDrive)
RCLONE_MIN_VERSION="1.65.0"
RCLONE_CURRENT=$(rclone version 2>/dev/null | head -1 | grep -oP 'v\K[0-9.]+' || echo "0.0.0")
if [ "$(printf '%s\n' "$RCLONE_MIN_VERSION" "$RCLONE_CURRENT" | sort -V | head -1)" != "$RCLONE_MIN_VERSION" ]; then
echo "Installing rclone from official source (current: v${RCLONE_CURRENT}, need >= v${RCLONE_MIN_VERSION})..."
curl -sL https://rclone.org/install.sh | bash 2>/dev/null || {
echo "Warning: rclone install from official source failed, falling back to apt"
apt-get install -y rclone 2>/dev/null || true
}
echo " ✓ rclone $(rclone version 2>/dev/null | head -1 | grep -oP 'v[0-9.]+' || echo 'installed')"
else
echo "rclone v${RCLONE_CURRENT} already meets minimum v${RCLONE_MIN_VERSION}"
fi
# Ensure hostapd/dnsmasq don't auto-start outside our controller
systemctl disable hostapd 2>/dev/null || true
systemctl stop hostapd 2>/dev/null || true
systemctl disable dnsmasq 2>/dev/null || true
systemctl stop dnsmasq 2>/dev/null || true
# Configure NetworkManager to ignore virtual AP interface (uap0)
NM_CONF_DIR="/etc/NetworkManager/conf.d"
NM_UNMANAGED_CONF="$NM_CONF_DIR/unmanaged-uap0.conf"
if [ ! -f "$NM_UNMANAGED_CONF" ]; then
mkdir -p "$NM_CONF_DIR"
cat > "$NM_UNMANAGED_CONF" <<EOF
[keyfile]
unmanaged-devices=interface-name:uap0
EOF
echo "Created NetworkManager config to ignore uap0 interface"
if systemctl is-active --quiet NetworkManager; then
systemctl reload NetworkManager 2>/dev/null || true
fi
else
echo "NetworkManager already configured to ignore uap0"
fi
# Configure WiFi roaming for mesh/extender networks (multiple APs with same SSID)
# NetworkManager controls wpa_supplicant via D-Bus, so we configure NM directly
NM_ROAMING_CONF="$NM_CONF_DIR/wifi-roaming.conf"
if [ ! -f "$NM_ROAMING_CONF" ]; then
mkdir -p "$NM_CONF_DIR"
cat > "$NM_ROAMING_CONF" <<EOF
[device]
# Enable aggressive WiFi roaming for better mesh/extender network support
wifi.scan-rand-mac-address = no
[connection]
# Disable power save to maintain better connection stability and faster roaming
# This is the most important setting for responsive roaming
wifi.powersave = 2
# Enable MAC randomization for privacy
wifi.mac-address-randomization = 1
[connectivity]
# Check connectivity frequently to detect network issues and trigger roaming
interval = 60
EOF
echo "Created WiFi roaming configuration for mesh/extender networks"
if systemctl is-active --quiet NetworkManager; then
systemctl reload NetworkManager 2>/dev/null || true
fi
else
echo "WiFi roaming configuration already exists"
fi
# Note: NetworkManager manages wpa_supplicant directly via D-Bus (-u -s flags)
# and does not use /etc/wpa_supplicant/wpa_supplicant.conf files.
# Background scanning (bgscan) parameters are hardcoded in NetworkManager.
# The wifi.powersave=2 setting above is the key to aggressive roaming.
# ===== Detect and disable conflicting USB gadget services =====
# Raspberry Pi OS Bookworm+ ships with rpi-usb-gadget enabled by default on
# OTG-capable boards (e.g. Pi Zero 2 W). It configures a USB Ethernet gadget
# that claims the UDC, preventing TeslaUSB's mass-storage gadget from binding.
# We also check for usb-gadget.service (alternative naming on some images).
for svc in rpi-usb-gadget.service usb-gadget.service; do
if systemctl list-unit-files "$svc" >/dev/null 2>&1 && \
systemctl list-unit-files "$svc" | grep -q "$svc"; then
echo "Detected conflicting service: $svc"
# Stop it if running (releases UDC)
if systemctl is-active --quiet "$svc" 2>/dev/null; then
echo " Stopping $svc..."
systemctl stop "$svc" 2>/dev/null || true
sleep 0.5
fi
# Disable so it doesn't start on next boot
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
echo " Disabling $svc..."
systemctl disable "$svc" 2>/dev/null || true
fi
# Mask to prevent manual/dependency activation
echo " Masking $svc to prevent conflicts..."
systemctl mask "$svc" 2>/dev/null || true
echo " $svc has been stopped, disabled, and masked."
fi
done
# Also clean up any gadget left behind by rpi-usb-gadget in configfs
# (it typically creates /sys/kernel/config/usb_gadget/g1)
for other_gadget in /sys/kernel/config/usb_gadget/*/; do
gadget_name="$(basename "$other_gadget")"
# Skip our own gadget
[ "$gadget_name" = "teslausb" ] && continue
[ "$gadget_name" = "*" ] && continue
if [ -d "$other_gadget" ]; then
echo "Cleaning up leftover USB gadget: $gadget_name"
# Unbind UDC
if [ -f "$other_gadget/UDC" ]; then
echo "" > "$other_gadget/UDC" 2>/dev/null || true
sleep 0.3
fi
# Remove function links from configs
for cfg in "$other_gadget"/configs/*/; do
[ -d "$cfg" ] || continue
find "$cfg" -maxdepth 1 -type l -delete 2>/dev/null || true
rmdir "$cfg"/strings/* 2>/dev/null || true
rmdir "$cfg" 2>/dev/null || true
done
# Remove functions
for func in "$other_gadget"/functions/*/; do
[ -d "$func" ] || continue
rmdir "$func" 2>/dev/null || true
done
# Remove strings and gadget
rmdir "$other_gadget"/strings/* 2>/dev/null || true
rmdir "$other_gadget" 2>/dev/null || true
echo " Removed gadget: $gadget_name"
fi
done
# Ensure config.txt contains dtoverlay=dwc2 and dtparam=watchdog=on under [all]
# Note: We use dtoverlay=dwc2 WITHOUT dr_mode parameter to allow auto-detection
CONFIG_CHANGED=0
if [ -f "$CONFIG_FILE" ]; then
# Check if [all] section exists
if grep -q '^\[all\]' "$CONFIG_FILE"; then
# [all] section exists - check and add entries if needed
# Check and add dtoverlay=dwc2 (only if not already present)
if ! grep -q '^dtoverlay=dwc2$' "$CONFIG_FILE"; then
# Add dtoverlay=dwc2 right after [all] line
sed -i '/^\[all\]/a dtoverlay=dwc2' "$CONFIG_FILE"
echo "Added dtoverlay=dwc2 under [all] section in $CONFIG_FILE"
CONFIG_CHANGED=1
else
echo "dtoverlay=dwc2 already present in $CONFIG_FILE"
fi
# Check and add dtparam=watchdog=on (only if not already present)
if ! grep -q '^dtparam=watchdog=on$' "$CONFIG_FILE"; then
# Add dtparam=watchdog=on right after [all] line
sed -i '/^\[all\]/a dtparam=watchdog=on' "$CONFIG_FILE"
echo "Added dtparam=watchdog=on under [all] section in $CONFIG_FILE"
CONFIG_CHANGED=1
else
echo "dtparam=watchdog=on already present in $CONFIG_FILE"
fi
# Reduce GPU memory to 16MB (headless system doesn't need GPU, frees 48MB RAM)
if ! grep -q '^gpu_mem=' "$CONFIG_FILE"; then
sed -i '/^\[all\]/a gpu_mem=16' "$CONFIG_FILE"