plc-checkweigher 1.16.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/bin/plc_checkweigher +468 -56
  2. package/package.json +1 -1
  3. package/setup.sh +158 -31
@@ -6,7 +6,8 @@ set -euo pipefail
6
6
 
7
7
  INSTALL_DIR="/home/pi/plc_checkweigher"
8
8
  PYTHON="/home/pi/plc_env/bin/python3"
9
- SMB_CFG="${INSTALL_DIR}/smb_config.py"
9
+ DATA_DIR="${INSTALL_DIR}/data"
10
+ SMB_CFG="${DATA_DIR}/smb_config.py"
10
11
 
11
12
  # ── TTY detection — animations only when connected to a real terminal ─────────
12
13
  [[ -t 1 ]] && _TTY=1 || _TTY=0
@@ -151,7 +152,16 @@ case "$CMD" in
151
152
 
152
153
  # ── System diagnostic ─────────────────────────────────────────────────────────
153
154
  status|check|diag)
154
- exec "${PYTHON}" "${INSTALL_DIR}/debugger.py" "$@"
155
+ set +e
156
+ "${PYTHON}" "${INSTALL_DIR}/debugger.py" "$@"
157
+ _STATUS_EXIT=$?
158
+ set -e
159
+ if [[ $_STATUS_EXIT -ne 0 ]]; then
160
+ echo ""
161
+ warn "Errors detected — starting auto-fix ..."
162
+ sleep 1
163
+ "$0" fix
164
+ fi
155
165
  ;;
156
166
 
157
167
  # ── Live logs ─────────────────────────────────────────────────────────────────
@@ -201,8 +211,8 @@ stop)
201
211
  queue)
202
212
  banner "SMB Delivery Queue"
203
213
  echo ""
204
- QUEUE="${INSTALL_DIR}/delivery_queue.json"
205
- LEDGER="${INSTALL_DIR}/delivery_sent.log"
214
+ QUEUE="${DATA_DIR}/delivery_queue.json"
215
+ LEDGER="${DATA_DIR}/delivery_sent.log"
206
216
  if [[ -f "$QUEUE" ]]; then
207
217
  COUNT=$(python3 -c "import json; d=json.load(open('$QUEUE')); print(len(d))" 2>/dev/null || echo 0)
208
218
  if [[ "$COUNT" -eq 0 ]]; then
@@ -577,7 +587,335 @@ smb-config)
577
587
  ;;
578
588
 
579
589
  # ─────────────────────────────────────────────────────────────────────────────
580
- # UNINSTALLremove everything setup.sh installed
590
+ # FIXauto-detect and repair common issues
591
+ # Usage: fix [-wifi] [-health] [-programs] (no flags = run all)
592
+ # ─────────────────────────────────────────────────────────────────────────────
593
+ fix)
594
+ FIX_WIFI=0; FIX_HEALTH=0; FIX_PROGRAMS=0
595
+ if [[ $# -eq 0 ]]; then
596
+ FIX_WIFI=1; FIX_HEALTH=1; FIX_PROGRAMS=1
597
+ else
598
+ for _flag in "$@"; do
599
+ case "$_flag" in
600
+ -wifi) FIX_WIFI=1 ;;
601
+ -health) FIX_HEALTH=1 ;;
602
+ -programs) FIX_PROGRAMS=1 ;;
603
+ *) warn "Unknown flag: $_flag (valid: -wifi -health -programs)" ;;
604
+ esac
605
+ done
606
+ fi
607
+
608
+ LOG_DIR="/home/pi/reports/logs"
609
+ mkdir -p "$LOG_DIR" 2>/dev/null || true
610
+ # Build mode tag: "all" or hyphen-joined active scopes
611
+ _LOG_MODES=""
612
+ [[ $FIX_WIFI -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}wifi"
613
+ [[ $FIX_HEALTH -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}health"
614
+ [[ $FIX_PROGRAMS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}programs"
615
+ [[ "$_LOG_MODES" == "wifi-health-programs" ]] && _LOG_MODES="all"
616
+ LOG_FILE="${LOG_DIR}/fix_${_LOG_MODES}_$(date '+%Y%m%d_%H%M%S').log"
617
+ flog() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
618
+ ffix_ok() { spin_ok "$*"; flog "FIXED : $*"; FIX_COUNT=$((FIX_COUNT+1)); }
619
+ ffix_info() { spin_ok "$*"; flog "INFO : $*"; }
620
+ ffix_warn() { spin_warn "$*"; flog "WARN : $*"; }
621
+ ffix_err() { spin_err "$*"; flog "ERROR : $*"; }
622
+
623
+ FIX_COUNT=0
624
+ flog "=== Run started | wifi=${FIX_WIFI} health=${FIX_HEALTH} programs=${FIX_PROGRAMS} ==="
625
+
626
+ banner "Auto Fix"
627
+ echo ""
628
+ need_sudo
629
+
630
+ # ─────────────────────────────────────────────────────────────────────────
631
+ # WIFI
632
+ # ─────────────────────────────────────────────────────────────────────────
633
+ if [[ $FIX_WIFI -eq 1 ]]; then
634
+ echo -e " ${B}▸ WiFi${NC}"; hr; echo ""
635
+
636
+ spin_start "NetworkManager"
637
+ if ! systemctl is-active --quiet NetworkManager 2>/dev/null; then
638
+ sudo systemctl start NetworkManager 2>/dev/null || true; sleep 2
639
+ if systemctl is-active --quiet NetworkManager 2>/dev/null; then
640
+ ffix_ok "NetworkManager was down — restarted"
641
+ else
642
+ ffix_err "NetworkManager failed to start"
643
+ fi
644
+ else
645
+ ffix_info "NetworkManager running"
646
+ fi
647
+
648
+ spin_start "WiFi hardware block"
649
+ if rfkill list wifi 2>/dev/null | grep -q "Hard blocked: yes"; then
650
+ ffix_warn "Hard blocked — check physical WiFi switch"
651
+ elif rfkill list wifi 2>/dev/null | grep -q "Soft blocked: yes"; then
652
+ rfkill unblock wifi 2>/dev/null || true
653
+ ffix_ok "WiFi soft-block removed"
654
+ else
655
+ ffix_info "rfkill OK"
656
+ fi
657
+
658
+ spin_start "wlan0 interface"
659
+ if ! ip link show wlan0 &>/dev/null; then
660
+ ffix_warn "wlan0 not found — adapter missing?"
661
+ elif ! ip link show wlan0 2>/dev/null | grep -q "UP"; then
662
+ sudo ip link set wlan0 up 2>/dev/null || true; sleep 1
663
+ if ip link show wlan0 2>/dev/null | grep -q "UP"; then
664
+ ffix_ok "wlan0 was down — brought up"
665
+ else
666
+ ffix_err "Could not bring up wlan0"
667
+ fi
668
+ else
669
+ ffix_info "wlan0 UP"
670
+ fi
671
+
672
+ spin_start "WiFi connection"
673
+ _WIFI_CON=$(nmcli -t -f NAME,DEVICE,TYPE con show --active 2>/dev/null \
674
+ | grep ":wlan0:802-11-wireless" | cut -d: -f1 | head -1 || echo "")
675
+ if [[ -z "$_WIFI_CON" ]]; then
676
+ _LAST_CON=$(nmcli -t -f NAME,TYPE con show 2>/dev/null \
677
+ | grep ":802-11-wireless" | head -1 | cut -d: -f1 || echo "")
678
+ if [[ -n "$_LAST_CON" ]]; then
679
+ _spin_kill
680
+ spin_start "Reconnecting to '${_LAST_CON}'"
681
+ sudo nmcli connection up "$_LAST_CON" 2>/dev/null || true; sleep 3
682
+ _WIFI_NOW=$(nmcli -t -f NAME,DEVICE,TYPE con show --active 2>/dev/null \
683
+ | grep ":wlan0:802-11-wireless" | cut -d: -f1 | head -1 || echo "")
684
+ if [[ -n "$_WIFI_NOW" ]]; then
685
+ _NEW_IP=$(ip -4 addr show wlan0 2>/dev/null \
686
+ | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "")
687
+ ffix_ok "Reconnected to '${_WIFI_NOW}' (IP: ${_NEW_IP:-?})"
688
+ else
689
+ ffix_warn "Could not reconnect — run: plc_checkweigher wifi"
690
+ fi
691
+ else
692
+ ffix_warn "No saved connections — run: plc_checkweigher wifi"
693
+ fi
694
+ else
695
+ _WIFI_IP=$(ip -4 addr show wlan0 2>/dev/null \
696
+ | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "?")
697
+ ffix_info "'${_WIFI_CON}' (${_WIFI_IP})"
698
+ fi
699
+
700
+ spin_start "Internet connectivity"
701
+ if ping -c 2 -W 2 8.8.8.8 &>/dev/null; then
702
+ ffix_info "Internet reachable"
703
+ else
704
+ ffix_warn "No internet — local LAN may still work"
705
+ fi
706
+
707
+ echo ""
708
+ fi
709
+
710
+ # ─────────────────────────────────────────────────────────────────────────
711
+ # HEALTH
712
+ # ─────────────────────────────────────────────────────────────────────────
713
+ if [[ $FIX_HEALTH -eq 1 ]]; then
714
+ echo -e " ${B}▸ System Health${NC}"; hr; echo ""
715
+
716
+ spin_start "Disk space"
717
+ _DISK_PCT=$(df / --output=pcent 2>/dev/null | tail -1 | tr -d ' %' || echo "0")
718
+ if [[ "${_DISK_PCT:-0}" -ge 95 ]]; then
719
+ sudo journalctl --vacuum-size=100M 2>/dev/null || true
720
+ _DISK_AFTER=$(df / --output=pcent 2>/dev/null | tail -1 | tr -d ' %' || echo "?")
721
+ ffix_ok "Disk ${_DISK_PCT}% → ${_DISK_AFTER}% after journal clean"
722
+ elif [[ "${_DISK_PCT:-0}" -ge 85 ]]; then
723
+ ffix_warn "Disk ${_DISK_PCT}% — consider removing old reports"
724
+ else
725
+ ffix_info "Disk ${_DISK_PCT}%"
726
+ fi
727
+
728
+ spin_start "CPU temperature"
729
+ if command -v vcgencmd &>/dev/null; then
730
+ _TEMP=$(vcgencmd measure_temp 2>/dev/null | grep -oP '[\d.]+' | head -1 || echo "0")
731
+ _TEMP_INT=${_TEMP%.*}
732
+ if [[ "${_TEMP_INT:-0}" -ge 80 ]]; then
733
+ ffix_warn "${_TEMP}°C — check ventilation"
734
+ else
735
+ ffix_info "${_TEMP}°C"
736
+ fi
737
+ else
738
+ ffix_info "Temperature check not available"
739
+ fi
740
+
741
+ spin_start "Memory"
742
+ _MEM_FREE=$(free -m 2>/dev/null | awk '/^Mem:/{print $7}' || echo "999")
743
+ _MEM_TOTAL=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}' || echo "1")
744
+ if [[ "${_MEM_FREE:-999}" -lt 50 ]]; then
745
+ echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null 2>&1 || true
746
+ _MEM_AFTER=$(free -m 2>/dev/null | awk '/^Mem:/{print $7}' || echo "?")
747
+ ffix_ok "Low memory ${_MEM_FREE}MB → ${_MEM_AFTER}MB after cache drop"
748
+ else
749
+ ffix_info "${_MEM_FREE}MB / ${_MEM_TOTAL}MB free"
750
+ fi
751
+
752
+ spin_start "System time"
753
+ if timedatectl status 2>/dev/null | grep -q "synchronized: yes"; then
754
+ ffix_info "NTP synchronized"
755
+ else
756
+ sudo timedatectl set-ntp true 2>/dev/null || true
757
+ command -v chronyc &>/dev/null && sudo chronyc makestep 2>/dev/null || true
758
+ sleep 2
759
+ if timedatectl status 2>/dev/null | grep -q "synchronized: yes"; then
760
+ ffix_ok "NTP sync restored"
761
+ else
762
+ ffix_warn "Time not synced — no internet?"
763
+ fi
764
+ fi
765
+
766
+ spin_start "Reports directory"
767
+ _REPORTS_DIR="/home/pi/reports"
768
+ if [[ ! -d "$_REPORTS_DIR" ]]; then
769
+ mkdir -p "$_REPORTS_DIR" 2>/dev/null && ffix_ok "Created ${_REPORTS_DIR}" \
770
+ || ffix_warn "Could not create ${_REPORTS_DIR}"
771
+ else
772
+ ffix_info "${_REPORTS_DIR} OK"
773
+ fi
774
+
775
+ spin_start "SMB config file"
776
+ mkdir -p "${DATA_DIR}" 2>/dev/null || true
777
+ if [[ ! -f "$SMB_CFG" ]]; then
778
+ printf 'SMB_ENABLED = False\nSMB_HOST = ""\nSMB_SHARE = "Reports"\nSMB_USERNAME = ""\nSMB_PASSWORD = ""\nSMB_SUBDIR = ""\n' \
779
+ > "$SMB_CFG" 2>/dev/null || sudo tee "$SMB_CFG" > /dev/null << 'SMBC'
780
+ SMB_ENABLED = False
781
+ SMB_HOST = ""
782
+ SMB_SHARE = "Reports"
783
+ SMB_USERNAME = ""
784
+ SMB_PASSWORD = ""
785
+ SMB_SUBDIR = ""
786
+ SMBC
787
+ ffix_ok "Created default smb_config.py (disabled — run: plc_checkweigher smb-config)"
788
+ else
789
+ ffix_info "smb_config.py present"
790
+ fi
791
+
792
+ echo ""
793
+ fi
794
+
795
+ # ─────────────────────────────────────────────────────────────────────────
796
+ # PROGRAMS
797
+ # ─────────────────────────────────────────────────────────────────────────
798
+ if [[ $FIX_PROGRAMS -eq 1 ]]; then
799
+ echo -e " ${B}▸ Programs & Services${NC}"; hr; echo ""
800
+
801
+ for _SVC in plc_watcher plc_web; do
802
+ spin_start "${_SVC}"
803
+ _SVC_STATE=$(systemctl is-active "$_SVC" 2>/dev/null || echo "inactive")
804
+ if [[ "$_SVC_STATE" != "active" ]]; then
805
+ sudo systemctl enable "$_SVC" 2>/dev/null || true
806
+ sudo systemctl restart "$_SVC" 2>/dev/null || true; sleep 2
807
+ _SVC_NOW=$(systemctl is-active "$_SVC" 2>/dev/null || echo "inactive")
808
+ if [[ "$_SVC_NOW" == "active" ]]; then
809
+ ffix_ok "${_SVC} restarted (was: ${_SVC_STATE})"
810
+ else
811
+ ffix_err "${_SVC} failed — check: journalctl -u ${_SVC} -n 30"
812
+ fi
813
+ else
814
+ ffix_info "${_SVC} active"
815
+ fi
816
+ done
817
+
818
+ spin_start "Python venv"
819
+ if [[ ! -x "$PYTHON" ]]; then
820
+ ffix_err "Venv missing at ${PYTHON} — re-run installer"
821
+ else
822
+ ffix_info "Venv OK"
823
+ fi
824
+
825
+ spin_start "Python packages"
826
+ _PKG_MISSING=""
827
+ for _pkg in flask reportlab pymcprotocol impacket; do
828
+ "${PYTHON}" -c "import ${_pkg}" &>/dev/null 2>&1 || _PKG_MISSING="${_PKG_MISSING} ${_pkg}"
829
+ done
830
+ if [[ -n "$_PKG_MISSING" ]]; then
831
+ _spin_kill
832
+ spin_start "Installing:${_PKG_MISSING}"
833
+ "${PYTHON}" -m pip install ${_PKG_MISSING} --quiet 2>/dev/null || true
834
+ _PKG_STILL=""
835
+ for _pkg in ${_PKG_MISSING}; do
836
+ "${PYTHON}" -c "import ${_pkg}" &>/dev/null 2>&1 || _PKG_STILL="${_PKG_STILL} ${_pkg}"
837
+ done
838
+ if [[ -z "$_PKG_STILL" ]]; then
839
+ ffix_ok "Installed:${_PKG_MISSING}"
840
+ else
841
+ ffix_warn "Could not install:${_PKG_STILL}"
842
+ fi
843
+ else
844
+ ffix_info "All packages present"
845
+ fi
846
+
847
+ spin_start "Delivery queue"
848
+ _QUEUE="${DATA_DIR}/delivery_queue.json"
849
+ mkdir -p "${DATA_DIR}" 2>/dev/null || true
850
+ if [[ ! -f "$_QUEUE" ]]; then
851
+ echo "[]" > "$_QUEUE" 2>/dev/null || true
852
+ ffix_ok "Created missing delivery_queue.json"
853
+ elif ! "${PYTHON}" -c "import json; json.load(open('${_QUEUE}'))" &>/dev/null 2>&1; then
854
+ mv "$_QUEUE" "${_QUEUE}.broken.$(date +%s)" 2>/dev/null || true
855
+ echo "[]" > "$_QUEUE" 2>/dev/null || true
856
+ ffix_ok "Corrupt delivery queue reset (backup saved)"
857
+ else
858
+ ffix_info "delivery_queue.json valid"
859
+ fi
860
+
861
+ spin_start "systemd unit files"
862
+ _RESTORED=""
863
+ for _src in "${INSTALL_DIR}/plc_watcher.service" "${INSTALL_DIR}/web/plc_web.service"; do
864
+ [[ ! -f "$_src" ]] && continue
865
+ _dst="/etc/systemd/system/$(basename "$_src")"
866
+ if [[ ! -f "$_dst" ]]; then
867
+ sudo cp "$_src" "$_dst" 2>/dev/null || true
868
+ _RESTORED="${_RESTORED} $(basename "$_src")"
869
+ fi
870
+ done
871
+ if [[ -n "$_RESTORED" ]]; then
872
+ sudo systemctl daemon-reload 2>/dev/null || true
873
+ ffix_ok "Restored:${_RESTORED}"
874
+ else
875
+ ffix_info "Unit files present"
876
+ fi
877
+
878
+ echo ""
879
+ fi
880
+
881
+ # ── Summary ───────────────────────────────────────────────────────────────
882
+ flog "=== Complete: ${FIX_COUNT} fix(es) applied ==="
883
+ hr
884
+ echo ""
885
+ if [[ $FIX_COUNT -gt 0 ]]; then
886
+ ok "${FIX_COUNT} fix(es) applied"
887
+ else
888
+ ok "All checks passed — no fixes needed"
889
+ fi
890
+ info "Log: ${LOG_FILE}"
891
+
892
+ # ── Push log to SMB share ─────────────────────────────────────────────────
893
+ _SMB_HOST=$(smb_get "SMB_HOST")
894
+ _SMB_SHARE=$(smb_get "SMB_SHARE")
895
+ _SMB_USER=$(smb_get "SMB_USERNAME")
896
+ _SMB_PASS=$(smb_get "SMB_PASSWORD")
897
+ _SMB_EN=$(grep "^SMB_ENABLED" "${SMB_CFG}" 2>/dev/null | grep -qi "true" && echo "1" || echo "0")
898
+
899
+ if [[ "$_SMB_EN" == "1" && -n "$_SMB_HOST" && -n "$_SMB_USER" ]] \
900
+ && command -v smbclient &>/dev/null; then
901
+ spin_start "Uploading log to SMB share"
902
+ _LOG_BASENAME="$(basename "$LOG_FILE")"
903
+ if smbclient "//${_SMB_HOST}/${_SMB_SHARE}" \
904
+ -U "${_SMB_USER}%${_SMB_PASS}" \
905
+ -c "mkdir logs; put ${LOG_FILE} logs/${_LOG_BASENAME}" &>/dev/null 2>&1; then
906
+ spin_ok "Log pushed → //${_SMB_HOST}/${_SMB_SHARE}/logs/${_LOG_BASENAME}"
907
+ flog "INFO : Log uploaded to SMB //${_SMB_HOST}/${_SMB_SHARE}/logs/${_LOG_BASENAME}"
908
+ else
909
+ spin_warn "SMB upload failed — log saved locally only"
910
+ flog "WARN : SMB log upload failed"
911
+ fi
912
+ fi
913
+
914
+ echo ""
915
+ ;;
916
+
917
+ # ─────────────────────────────────────────────────────────────────────────────
918
+ # UNINSTALL — two modes: software-only or full drive wipe
581
919
  # ─────────────────────────────────────────────────────────────────────────────
582
920
  uninstall)
583
921
  PI_USER="${PI_USER:-pi}"
@@ -591,46 +929,83 @@ uninstall)
591
929
  echo -e "${R} ║ PLC CHECK-WEIGHER UNINSTALLER ║${NC}"
592
930
  echo -e "${R} ╚══════════════════════════════════════════════════════════╝${NC}"
593
931
  echo ""
594
- echo -e " This will permanently remove:"
932
+ echo -e " Choose uninstall mode:"
595
933
  echo ""
596
- echo -e " ${R}✗${NC} systemd services plc_watcher plc_web"
597
- echo -e " ${R}✗${NC} project code ${INSTALL_DIR}"
598
- echo -e " ${R}✗${NC} Python venv ${VENV_DIR}"
599
- echo -e " ${R}✗${NC} CLI tool /usr/local/bin/plc_checkweigher"
600
- echo -e " ${R}✗${NC} Plymouth theme saismruth (reverts to default)"
601
- echo -e " ${R}✗${NC} RT kernel config (reverts /boot/firmware/config.txt)"
602
- echo -e " ${R}✗${NC} LightDM drop-in /etc/systemd/system/lightdm.service.d/"
603
- echo -e " ${R}✗${NC} NetworkManager cfg /etc/systemd/system/NetworkManager-wait-online.service.d/"
604
- echo -e " ${R}✗${NC} Hotspot connection plc-hotspot (nmcli)"
605
- echo -e " ${Y}!${NC} reports folder ${REPORTS_DIR} (you will be asked)"
934
+ echo -e " ${Y}1)${NC} ${W}Software only${NC}"
935
+ echo -e " ${D}Remove PLC services, code, venv and kernel config.${NC}"
936
+ echo -e " ${D}OS, user files and WiFi credentials are kept intact.${NC}"
606
937
  echo ""
607
- echo -e " ${D}System packages (git, python3-venv, samba-client) are NOT removed.${NC}"
938
+ echo -e " ${R}2)${NC} ${W}Clean drive${NC} — prepare for fresh OS flash"
939
+ echo -e " ${D}Everything above PLUS all user data and WiFi credentials,${NC}"
940
+ echo -e " ${D}then zeros the entire SD card.${NC}"
941
+ echo -e " ${R} ⚠ The Pi will NOT boot after this. Reflash required.${NC}"
608
942
  echo ""
609
- hr
943
+
944
+ while true; do
945
+ read -r -p " Mode [1 / 2]: " UNINSTALL_MODE </dev/tty
946
+ [[ "$UNINSTALL_MODE" == "1" || "$UNINSTALL_MODE" == "2" ]] && break
947
+ echo -e " ${R}Enter 1 or 2${NC}"
948
+ done
610
949
  echo ""
611
950
 
612
- read -r -p " Type YES to confirm full uninstall: " CONFIRM </dev/tty
613
- [[ "$CONFIRM" == "YES" ]] || { echo " Aborted."; exit 0; }
951
+ # ── Mode-specific confirmation ────────────────────────────────────────────
952
+ KEEP_REPORTS="Y"
953
+ if [[ "$UNINSTALL_MODE" == "1" ]]; then
954
+ hr
955
+ echo ""
956
+ echo -e " ${R}✗${NC} systemd services plc_watcher plc_web"
957
+ echo -e " ${R}✗${NC} project code ${INSTALL_DIR}"
958
+ echo -e " ${R}✗${NC} Python venv ${VENV_DIR}"
959
+ echo -e " ${R}✗${NC} CLI tool plc_checkweigher"
960
+ echo -e " ${R}✗${NC} Plymouth theme saismruth → default"
961
+ echo -e " ${R}✗${NC} RT kernel config /boot/firmware/config.txt"
962
+ echo -e " ${Y}!${NC} reports folder ${REPORTS_DIR} (you will be asked)"
963
+ echo ""
964
+ echo -e " ${D}System packages (git, python3-venv, samba-client) NOT removed.${NC}"
965
+ echo ""
966
+ hr; echo ""
967
+ read -r -p " Type YES to confirm: " _CONFIRM </dev/tty
968
+ [[ "$_CONFIRM" == "YES" ]] || { echo " Aborted."; exit 0; }
969
+ echo ""
970
+ read -r -p " Keep report PDFs in ${REPORTS_DIR}? [Y/n]: " KEEP_REPORTS </dev/tty
971
+ KEEP_REPORTS="${KEEP_REPORTS:-Y}"
972
+ else
973
+ # Detect the drive that holds the root filesystem
974
+ _ROOT_SRC=$(findmnt -n -o SOURCE / 2>/dev/null || echo "")
975
+ _ROOT_DEV=$(lsblk -no pkname "$_ROOT_SRC" 2>/dev/null || echo "")
976
+ _ROOT_DEV_PATH="/dev/${_ROOT_DEV}"
614
977
 
615
- echo ""
616
- read -r -p " Keep report PDFs in ${REPORTS_DIR}? [Y/n]: " KEEP_REPORTS </dev/tty
617
- KEEP_REPORTS="${KEEP_REPORTS:-Y}"
978
+ hr; echo ""
979
+ echo -e " ${R}⚠ DESTRUCTIVE READ CAREFULLY${NC}"
980
+ echo ""
981
+ echo -e " ${R}✗${NC} All PLC software, services and kernel config"
982
+ echo -e " ${R}✗${NC} All reports, logs and SMB credentials"
983
+ echo -e " ${R}✗${NC} SSH keys, bash history, npm cache, user configs"
984
+ echo -e " ${R}✗${NC} All saved WiFi connections"
985
+ echo -e " ${R}✗${NC} Entire drive zeroed: ${W}${_ROOT_DEV_PATH}${NC}"
986
+ echo ""
987
+ echo -e " ${R}The Pi will become unresponsive during the wipe.${NC}"
988
+ echo -e " ${D}Power off after ~2 minutes, then reflash with Raspberry Pi Imager.${NC}"
989
+ echo ""
990
+ hr; echo ""
991
+ read -r -p " Type WIPE to confirm drive wipe: " _CONFIRM </dev/tty
992
+ [[ "$_CONFIRM" == "WIPE" ]] || { echo " Aborted."; exit 0; }
993
+ fi
618
994
 
619
995
  echo ""
620
996
  need_sudo
621
997
 
622
- # Step counter
623
- _US=0; _UT=9
998
+ # ── Step counter (9 for software-only, 12 for clean drive) ───────────────
999
+ _UT=$([[ "$UNINSTALL_MODE" == "1" ]] && echo "9" || echo "12")
1000
+ _US=0
624
1001
  ustep() { _US=$((_US + 1)); spin_start "[${_US}/${_UT}] $*"; }
625
1002
 
626
1003
  # ── 1. Stop and disable services ─────────────────────────────────────────
627
1004
  echo ""
628
1005
  ustep "Stopping and disabling services"
629
1006
  for SVC in plc_watcher plc_web; do
630
- systemctl is-active --quiet "$SVC" 2>/dev/null \
631
- && sudo systemctl stop "$SVC" 2>/dev/null || true
632
- systemctl is-enabled --quiet "$SVC" 2>/dev/null \
633
- && sudo systemctl disable "$SVC" 2>/dev/null || true
1007
+ systemctl is-active --quiet "$SVC" 2>/dev/null && sudo systemctl stop "$SVC" 2>/dev/null || true
1008
+ systemctl is-enabled --quiet "$SVC" 2>/dev/null && sudo systemctl disable "$SVC" 2>/dev/null || true
634
1009
  done
635
1010
  sudo rm -f /etc/systemd/system/plc_watcher.service \
636
1011
  /etc/systemd/system/plc_web.service
@@ -657,27 +1032,23 @@ uninstall)
657
1032
  fi
658
1033
  spin_ok "Reverted to '${DEFAULT_THEME:-default}'"
659
1034
 
660
- # ── 4. Rebuild initramfs (slowest step — keep spinner alive) ─────────────
1035
+ # ── 4. Rebuild initramfs ─────────────────────────────────────────────────
661
1036
  ustep "Rebuilding initramfs ${D}(~30 s)${NC}"
662
1037
  sudo update-initramfs -u > /tmp/uninstall_initramfs.log 2>&1 \
663
- && spin_ok \
664
- || spin_warn "Warnings — see /tmp/uninstall_initramfs.log"
1038
+ && spin_ok || spin_warn "Warnings — see /tmp/uninstall_initramfs.log"
665
1039
 
666
1040
  # ── 5. RT kernel revert ──────────────────────────────────────────────────
667
1041
  ustep "Reverting RT kernel config"
668
1042
  if [[ -f "${BOOT_FW}/config.txt" ]]; then
669
- sudo sed -i '/### PLC-RT-BLOCK-START ###/,/### PLC-RT-BLOCK-END ###/d' \
670
- "${BOOT_FW}/config.txt"
1043
+ sudo sed -i '/### PLC-RT-BLOCK-START ###/,/### PLC-RT-BLOCK-END ###/d' "${BOOT_FW}/config.txt"
671
1044
  sudo sed -i '/^gpu_mem=128$/d' "${BOOT_FW}/config.txt"
672
- sudo rm -f "${BOOT_FW}/kernel8-rt.img" \
673
- "${BOOT_FW}/initramfs8-rt" \
674
- "${BOOT_FW}/kernel8-stock.img"
675
- spin_ok "Stock kernel will boot after reboot"
1045
+ sudo rm -f "${BOOT_FW}/kernel8-rt.img" "${BOOT_FW}/initramfs8-rt" "${BOOT_FW}/kernel8-stock.img"
1046
+ spin_ok "Stock kernel restored"
676
1047
  else
677
1048
  spin_warn "config.txt not found — skipped"
678
1049
  fi
679
1050
 
680
- # ── 6. Hotspot + nmcli cleanup ───────────────────────────────────────────
1051
+ # ── 6. Network cleanup ───────────────────────────────────────────────────
681
1052
  ustep "Cleaning up network connections"
682
1053
  sudo nmcli connection delete "plc-hotspot" 2>/dev/null || true
683
1054
  sudo systemctl daemon-reload
@@ -685,15 +1056,10 @@ uninstall)
685
1056
 
686
1057
  # ── 7. Python venv ───────────────────────────────────────────────────────
687
1058
  ustep "Removing Python environment"
688
- if [[ -d "$VENV_DIR" ]]; then
689
- rm -rf "$VENV_DIR"
690
- spin_ok "Removed ${VENV_DIR}"
691
- else
692
- spin_ok "Already gone"
693
- fi
1059
+ [[ -d "$VENV_DIR" ]] && rm -rf "$VENV_DIR" && spin_ok "Removed ${VENV_DIR}" || spin_ok "Already gone"
694
1060
 
695
- # ── 8. Reports (optional) + temp files ───────────────────────────────────
696
- ustep "Cleaning up runtime files"
1061
+ # ── 8. Runtime files + CLI ───────────────────────────────────────────────
1062
+ ustep "Cleaning up runtime files and CLI"
697
1063
  rm -f /tmp/plc_live.json 2>/dev/null || true
698
1064
  BASHRC="${HOME_DIR}/.bashrc"
699
1065
  [[ -f "$BASHRC" ]] && sed -i '/export PATH.*\.local\/bin/d' "$BASHRC" 2>/dev/null || true
@@ -704,16 +1070,61 @@ uninstall)
704
1070
  fi
705
1071
  spin_ok "CLI and temp files removed"
706
1072
 
707
- # ── 9. Remove project directory (self-deletes last) ───────────────────────
1073
+ # ── 9. Project code ──────────────────────────────────────────────────────
708
1074
  ustep "Removing project code"
709
1075
  if [[ -d "$INSTALL_DIR" ]]; then
710
- rm -rf "$INSTALL_DIR"
711
- spin_ok "Removed ${INSTALL_DIR}"
1076
+ sudo rm -rf "$INSTALL_DIR" && spin_ok "Removed ${INSTALL_DIR}"
712
1077
  else
713
1078
  spin_ok "Already gone"
714
1079
  fi
715
1080
 
716
- # ── Done ─────────────────────────────────────────────────────────────────
1081
+ # ─────────────────────────────────────────────────────────────────────────
1082
+ # MODE 2 ONLY: wipe all user data then zero the drive
1083
+ # ─────────────────────────────────────────────────────────────────────────
1084
+ if [[ "$UNINSTALL_MODE" == "2" ]]; then
1085
+
1086
+ # ── 10. All WiFi connections ─────────────────────────────────────────
1087
+ ustep "Removing all WiFi connections"
1088
+ nmcli -t -f NAME con show 2>/dev/null | while IFS= read -r _CON; do
1089
+ [[ -n "$_CON" ]] && sudo nmcli connection delete "$_CON" 2>/dev/null || true
1090
+ done
1091
+ spin_ok "All network connections cleared"
1092
+
1093
+ # ── 11. User data ────────────────────────────────────────────────────
1094
+ ustep "Wiping user data"
1095
+ rm -rf "${REPORTS_DIR}" 2>/dev/null || true
1096
+ rm -rf "${HOME_DIR}/.ssh" 2>/dev/null || true
1097
+ rm -f "${HOME_DIR}/.bash_history" 2>/dev/null || true
1098
+ rm -rf "${HOME_DIR}/.npm" 2>/dev/null || true
1099
+ rm -rf "${HOME_DIR}/.config" 2>/dev/null || true
1100
+ rm -rf "${HOME_DIR}/.cache" 2>/dev/null || true
1101
+ rm -rf "${HOME_DIR}/.local" 2>/dev/null || true
1102
+ spin_ok "User data cleared"
1103
+
1104
+ # ── 12. Zero the drive ───────────────────────────────────────────────
1105
+ ustep "Zeroing drive — ${_ROOT_DEV_PATH}"
1106
+ _spin_kill
1107
+ echo ""
1108
+ echo -e " ${R}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
1109
+ echo -e " ${R} Starting drive wipe. The system will become unresponsive.${NC}"
1110
+ echo -e " ${R} Power off after 2 minutes and reflash with Pi Imager.${NC}"
1111
+ echo -e " ${R}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
1112
+ echo ""
1113
+ for _i in 10 9 8 7 6 5 4 3 2 1; do
1114
+ printf "\r Wiping in %2d s ... (Ctrl-C to abort) " "$_i"
1115
+ sleep 1
1116
+ done
1117
+ printf '\r\033[K'
1118
+ echo -e " ${R}Wiping ${_ROOT_DEV_PATH} ...${NC}"
1119
+ echo ""
1120
+ sudo dd if=/dev/zero of="${_ROOT_DEV_PATH}" bs=4M status=progress 2>&1 || true
1121
+ # The system will have crashed by here. This line is a safety net.
1122
+ echo ""
1123
+ warn "Wipe complete — power off and reflash."
1124
+ exit 0
1125
+ fi
1126
+
1127
+ # ── Done (mode 1 only) ────────────────────────────────────────────────────
717
1128
  echo ""
718
1129
  echo -e "${G}"
719
1130
  echo " ╔══════════════════════════════════════════════════════════╗"
@@ -724,13 +1135,10 @@ uninstall)
724
1135
  echo -e "${NC}"
725
1136
  [[ "${KEEP_REPORTS^^}" == "Y" ]] && info "PDFs still at: ${REPORTS_DIR} (remove manually if needed)"
726
1137
  echo ""
727
-
728
1138
  read -r -p " Reboot now? [Y/n]: " DO_REBOOT </dev/tty
729
1139
  DO_REBOOT="${DO_REBOOT:-Y}"
730
1140
  if [[ "${DO_REBOOT^^}" == "Y" ]]; then
731
- spin_start "Rebooting"
732
- sleep 1
733
- sudo reboot
1141
+ spin_start "Rebooting"; sleep 1; sudo reboot
734
1142
  else
735
1143
  warn "Remember to reboot for kernel changes to take effect."
736
1144
  echo ""
@@ -745,7 +1153,11 @@ help|--help|-h)
745
1153
  echo -e "${B} plc_checkweigher${NC} — PLC Check-Weigher system CLI"
746
1154
  echo ""
747
1155
  echo -e " ${W}Diagnostics${NC}"
748
- echo " status Full system diagnostic all checks + fix hints"
1156
+ echo " status Full system diagnostic (auto-runs fix on errors)"
1157
+ echo " fix Auto-detect and repair all issues + write log"
1158
+ echo " fix -wifi Fix WiFi connectivity only"
1159
+ echo " fix -health Fix disk, memory, time, dirs"
1160
+ echo " fix -programs Fix services, packages, queue, unit files"
749
1161
  echo " logs Stream live logs (plc_watcher + plc_web)"
750
1162
  echo " queue Show SMB pending queue and delivery ledger"
751
1163
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.16.0",
3
+ "version": "1.20.0",
4
4
  "description": "One-command installer for the PLC Check-Weigher system on Raspberry Pi (PREEMPT_RT kernel, Python stack, WiFi, SMB, systemd RT services)",
5
5
  "scripts": {
6
6
  "postinstall": "node bin/cli.js"
package/setup.sh CHANGED
@@ -12,12 +12,13 @@
12
12
  # 3. Clone / update repo
13
13
  # 4. Python venv + pip install
14
14
  # 5. Create /home/<user>/reports
15
- # 6. WiFi — scan → pick from list → password
16
- # 7. SMB — enter host IP, share name, credentials → smb_config.py
15
+ # 6. WiFi — scan → pick from list → password
16
+ # 7. SMB — enter host IP, share name, credentials → smb_config.py
17
17
  # 8. NetworkManager-wait-online
18
18
  # 9. systemd services (plc_watcher + plc_web)
19
- # 10. Boot logo — Plymouth theme with logo.png + "SAI SAMARTH ENGG"
20
- # 11. Display — LightDM priority, CPU isolation, utmpx, GPU memory
19
+ # 10. Boot logo — Plymouth theme with logo.png + "SAI SAMARTH ENGG"
20
+ # 11. Display — LightDM priority, CPU isolation, utmpx
21
+ # 11b. VS Code — priority daemon: cores 0-2, Nice=-5
21
22
  # 12. PREEMPT_RT kernel ← installed last so only one reboot is needed
22
23
  # 13. REBOOT
23
24
  # =============================================================================
@@ -31,6 +32,7 @@ REPO_URL="https://github.com/Bibin-VR/plc-checkweigher.git"
31
32
  REPO_BRANCH="main"
32
33
  HOME_DIR="/home/${PI_USER}"
33
34
  INSTALL_DIR="${HOME_DIR}/plc_checkweigher"
35
+ DATA_DIR="${INSTALL_DIR}/data" # pi-writable: queue, log, smb_config
34
36
  VENV_DIR="${HOME_DIR}/plc_env"
35
37
  REPORTS_DIR="${HOME_DIR}/reports"
36
38
  BOOT_FW="/boot/firmware"
@@ -106,15 +108,20 @@ install_system_packages() {
106
108
  # ── 2. Clone / update repo ────────────────────────────────────────────────────
107
109
  setup_repo() {
108
110
  step "Repository ..."
111
+ # Mark safe so git operations work regardless of ownership state.
112
+ git config --global --add safe.directory "${INSTALL_DIR}" 2>/dev/null || true
113
+
109
114
  if [[ -d "${INSTALL_DIR}/.git" ]]; then
110
- sudo -u "${PI_USER}" git -C "${INSTALL_DIR}" pull --ff-only origin "${REPO_BRANCH}" \
115
+ # Temporarily unlock so git can write index / pack files.
116
+ chown -R root:root "${INSTALL_DIR}" 2>/dev/null || true
117
+ git -C "${INSTALL_DIR}" pull --ff-only origin "${REPO_BRANCH}" \
111
118
  && ok "Repo updated → ${INSTALL_DIR}" \
112
119
  || warn "git pull failed — using existing files"
113
120
  else
114
- sudo -u "${PI_USER}" git clone --branch "${REPO_BRANCH}" \
115
- "${REPO_URL}" "${INSTALL_DIR}"
121
+ git clone --branch "${REPO_BRANCH}" "${REPO_URL}" "${INSTALL_DIR}"
116
122
  ok "Repo cloned → ${INSTALL_DIR}"
117
123
  fi
124
+ # Permissions are finalised by lock_source_files() later.
118
125
  }
119
126
 
120
127
  # ── 3. Python venv ────────────────────────────────────────────────────────────
@@ -134,6 +141,13 @@ setup_dirs() {
134
141
  mkdir -p "${REPORTS_DIR}"
135
142
  chown "${PI_USER}:${PI_USER}" "${REPORTS_DIR}"
136
143
  ok "${REPORTS_DIR}"
144
+
145
+ # pi-writable data directory — queue, delivery log, SMB credentials live here.
146
+ # Source files above this directory are root-locked after install_services().
147
+ mkdir -p "${DATA_DIR}"
148
+ chown "${PI_USER}:${PI_USER}" "${DATA_DIR}"
149
+ chmod 755 "${DATA_DIR}"
150
+ ok "${DATA_DIR} (runtime data — pi-writable)"
137
151
  }
138
152
 
139
153
  # ── CLI tool — install plc_checkweigher command ───────────────────────────────
@@ -252,8 +266,8 @@ setup_smb() {
252
266
  prompt SMB_HOST "Host IP address" ""
253
267
  if [[ -z "${SMB_HOST}" ]]; then
254
268
  warn "SMB push disabled — no host entered."
255
- # Write a disabled smb_config.py
256
- cat > "${INSTALL_DIR}/smb_config.py" << 'EOF'
269
+ # Write a disabled smb_config.py to the pi-writable data/ directory.
270
+ cat > "${DATA_DIR}/smb_config.py" << 'EOF'
257
271
  # SMB push disabled during setup
258
272
  SMB_ENABLED = False
259
273
  SMB_HOST = ""
@@ -262,7 +276,7 @@ SMB_USERNAME = ""
262
276
  SMB_PASSWORD = ""
263
277
  SMB_SUBDIR = ""
264
278
  EOF
265
- chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/smb_config.py"
279
+ chown "${PI_USER}:${PI_USER}" "${DATA_DIR}/smb_config.py"
266
280
  return
267
281
  fi
268
282
 
@@ -274,8 +288,9 @@ EOF
274
288
  hr
275
289
  echo ""
276
290
 
277
- # Write smb_config.py (gitignored — credentials stay off GitHub)
278
- cat > "${INSTALL_DIR}/smb_config.py" << EOF
291
+ # Write smb_config.py to data/ (gitignored — credentials stay off GitHub).
292
+ # Stored in data/ so the pi user can update it via: plc_checkweigher smb-config
293
+ cat > "${DATA_DIR}/smb_config.py" << EOF
279
294
  # SMB configuration — written by setup.sh, NOT committed to git.
280
295
  SMB_ENABLED = True
281
296
  SMB_HOST = "${SMB_HOST}"
@@ -284,8 +299,8 @@ SMB_USERNAME = "${SMB_USERNAME}"
284
299
  SMB_PASSWORD = "${SMB_PASSWORD}"
285
300
  SMB_SUBDIR = "${SMB_SUBDIR}"
286
301
  EOF
287
- chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/smb_config.py"
288
- ok "SMB config saved → ${INSTALL_DIR}/smb_config.py"
302
+ chown "${PI_USER}:${PI_USER}" "${DATA_DIR}/smb_config.py"
303
+ ok "SMB config saved → ${DATA_DIR}/smb_config.py"
289
304
 
290
305
  # Test connectivity
291
306
  echo -n " Testing connection to ${SMB_HOST} ..."
@@ -478,13 +493,13 @@ setup_display() {
478
493
  # Start after hardware udev settles (HDMI/DSI detected) — not after network.
479
494
  After=systemd-udev-settle.service local-fs.target acpid.socket dbus.service
480
495
  Wants=systemd-udev-settle.service
496
+ # StartLimit* MUST be in [Unit] — ignored in [Service].
497
+ StartLimitBurst=10
498
+ StartLimitIntervalSec=60
481
499
 
482
500
  [Service]
483
- # Generous restart policy — display should always recover.
484
- StartLimitBurst=20
485
- StartLimitIntervalSec=120
486
501
  Restart=on-failure
487
- RestartSec=3
502
+ RestartSec=5
488
503
 
489
504
  # CPU cores 0-2 only — core 3 is reserved for SCHED_FIFO PLC process.
490
505
  CPUAffinity=0 1 2
@@ -494,7 +509,7 @@ Nice=-5
494
509
 
495
510
  LimitNOFILE=65536
496
511
  EOF
497
- ok "LightDM: CPUAffinity=0-2, Nice=-5, network dep removed"
512
+ ok "LightDM: CPUAffinity=0-2, Nice=-5, StartLimitBurst=10 in [Unit]"
498
513
 
499
514
  # ── Fix utmpx — PAM needs /run/utmp to track sessions ───────────────────
500
515
  cat > /etc/tmpfiles.d/utmp-fix.conf << 'EOF'
@@ -503,18 +518,127 @@ EOF
503
518
  systemd-tmpfiles --create /etc/tmpfiles.d/utmp-fix.conf 2>/dev/null || true
504
519
  ok "/run/utmp fixed (utmpx PAM session tracking)"
505
520
 
506
- # ── GPU memory 128 MB: enough for 1080p desktop and HMI use ───────────
507
- sed -i '/^gpu_mem=/d' "${BOOT_FW}/config.txt"
508
- if grep -q "### PLC-RT-BLOCK-START ###" "${BOOT_FW}/config.txt"; then
509
- sed -i '/### PLC-RT-BLOCK-START ###/i gpu_mem=128' "${BOOT_FW}/config.txt"
521
+ # ── Fix vc4-kms display driver PREEMPT_RT EPROBE_DEFER workaround ─────
522
+ # On the RT kernel, vc4_hdmi defers its probe indefinitely waiting for the
523
+ # PCM audio component (-517 = EPROBE_DEFER). noaudio removes that dependency
524
+ # so the display DRM card is created on first probe attempt.
525
+ # hdmi_force_hotplug=1 initialises HDMI hardware even when no display is
526
+ # connected at boot (required for headless + Pi Connect screen sharing).
527
+ sed -i 's/^dtoverlay=vc4-kms-v3d$/dtoverlay=vc4-kms-v3d,noaudio/' \
528
+ "${BOOT_FW}/config.txt" 2>/dev/null || true
529
+ if ! grep -q '^hdmi_force_hotplug' "${BOOT_FW}/config.txt"; then
530
+ if grep -q '^\[all\]' "${BOOT_FW}/config.txt"; then
531
+ sed -i '/^\[all\]/a hdmi_force_hotplug=1' "${BOOT_FW}/config.txt"
532
+ else
533
+ echo "hdmi_force_hotplug=1" >> "${BOOT_FW}/config.txt"
534
+ fi
510
535
  else
511
- echo "gpu_mem=128" >> "${BOOT_FW}/config.txt"
536
+ sed -i 's/^hdmi_force_hotplug=.*/hdmi_force_hotplug=1/' "${BOOT_FW}/config.txt"
512
537
  fi
513
- ok "gpu_mem=128 set in config.txt (128 MB VRAM)"
538
+ ok "vc4-kms-v3d,noaudio + hdmi_force_hotplug=1 (HDMI always initialised)"
539
+
540
+ # ── Enable rpi-connect user service (Pi Connect remote access) ──────────
541
+ loginctl enable-linger "${PI_USER}" 2>/dev/null || true
542
+ sudo -u "${PI_USER}" systemctl --user enable rpi-connect.service 2>/dev/null || true
543
+ ok "rpi-connect user service enabled (sign in with: rpi-connect signin)"
514
544
 
515
545
  systemctl daemon-reload
516
546
  systemctl enable lightdm.service 2>/dev/null || true
517
- ok "LightDM enabled — starts on every boot when display is connected"
547
+ ok "LightDM enabled — starts on every boot"
548
+ }
549
+
550
+ # ── 11b. VS Code server priority ──────────────────────────────────────────────
551
+ setup_vscode_priority() {
552
+ step "VS Code server priority ..."
553
+
554
+ cat > /usr/local/bin/vscode-priority-daemon << 'DAEMON'
555
+ #!/usr/bin/env bash
556
+ # Apply CPU affinity (cores 0-2) and Nice=-5 to VS Code server processes.
557
+ # Core 3 is reserved exclusively for the SCHED_FIFO PLC process.
558
+ # Runs every 60s so newly-spawned extension host processes are caught promptly.
559
+ while true; do
560
+ mapfile -t pids < <(pgrep -u pi -f '\.vscode-server' 2>/dev/null || true)
561
+ for pid in "${pids[@]}"; do
562
+ taskset -cp 0-2 "$pid" >/dev/null 2>&1 || true
563
+ renice -n -5 -p "$pid" >/dev/null 2>&1 || true
564
+ done
565
+ sleep 60
566
+ done
567
+ DAEMON
568
+ chmod +x /usr/local/bin/vscode-priority-daemon
569
+
570
+ cat > /etc/systemd/system/vscode-priority.service << 'EOF'
571
+ [Unit]
572
+ Description=VS Code Server priority manager (cores 0-2, Nice=-5)
573
+ After=multi-user.target
574
+ Wants=multi-user.target
575
+
576
+ [Service]
577
+ Type=simple
578
+ ExecStart=/usr/local/bin/vscode-priority-daemon
579
+ Restart=always
580
+ RestartSec=10
581
+ User=root
582
+
583
+ [Install]
584
+ WantedBy=multi-user.target
585
+ EOF
586
+
587
+ systemctl daemon-reload
588
+ systemctl enable vscode-priority.service
589
+ ok "vscode-priority.service (cores 0-2, Nice=-5) — starts on every boot"
590
+ }
591
+
592
+ # ── 11c. Lock source files — root:root 644 (requires sudo to edit) ────────────
593
+ lock_source_files() {
594
+ step "Protecting source files ..."
595
+
596
+ # Source files: root:root 644 — readable+executable by all, editable by root only.
597
+ find "${INSTALL_DIR}" -maxdepth 1 -name "*.py" -exec chown root:root {} \; \
598
+ -exec chmod 644 {} \;
599
+ find "${INSTALL_DIR}/web" -name "*.py" -exec chown root:root {} \; \
600
+ -exec chmod 644 {} \; 2>/dev/null || true
601
+ find "${INSTALL_DIR}/web/templates" -name "*.html" -exec chown root:root {} \; \
602
+ -exec chmod 644 {} \; 2>/dev/null || true
603
+ find "${INSTALL_DIR}/web/static" -type f -exec chown root:root {} \; \
604
+ -exec chmod 644 {} \; 2>/dev/null || true
605
+
606
+ # Directories: root:root 755 — pi can list/cd but cannot create new files.
607
+ chown root:root "${INSTALL_DIR}"
608
+ chmod 755 "${INSTALL_DIR}"
609
+ [[ -d "${INSTALL_DIR}/web" ]] && chown root:root "${INSTALL_DIR}/web" && chmod 755 "${INSTALL_DIR}/web"
610
+ [[ -d "${INSTALL_DIR}/web/templates" ]] && chown root:root "${INSTALL_DIR}/web/templates" && chmod 755 "${INSTALL_DIR}/web/templates"
611
+ [[ -d "${INSTALL_DIR}/web/static" ]] && chown root:root "${INSTALL_DIR}/web/static" && chmod 755 "${INSTALL_DIR}/web/static"
612
+ [[ -d "${INSTALL_DIR}/assets" ]] && chown root:root "${INSTALL_DIR}/assets" && chmod 755 "${INSTALL_DIR}/assets"
613
+ [[ -d "${INSTALL_DIR}/bin" ]] && chown root:root "${INSTALL_DIR}/bin" && chmod 755 "${INSTALL_DIR}/bin"
614
+
615
+ # service files and scripts: root:root, readable
616
+ find "${INSTALL_DIR}" -maxdepth 2 -name "*.service" -exec chown root:root {} \; \
617
+ -exec chmod 644 {} \; 2>/dev/null || true
618
+ find "${INSTALL_DIR}" -maxdepth 2 -name "*.sh" -exec chown root:root {} \; \
619
+ -exec chmod 755 {} \; 2>/dev/null || true
620
+
621
+ # Data directory: pi-owned 755 — services write queue, log, smb_config here.
622
+ chown "${PI_USER}:${PI_USER}" "${DATA_DIR}"
623
+ chmod 755 "${DATA_DIR}"
624
+
625
+ # Pre-create data files with correct ownership so pi can write on first run.
626
+ local queue="${DATA_DIR}/delivery_queue.json"
627
+ local log="${DATA_DIR}/delivery_sent.log"
628
+ [[ -f "${queue}" ]] || echo '[]' > "${queue}"
629
+ [[ -f "${log}" ]] || touch "${log}"
630
+ chown "${PI_USER}:${PI_USER}" "${queue}" "${log}"
631
+ chmod 644 "${queue}" "${log}"
632
+
633
+ # smb_config.py in data/ stays pi-owned so plc_checkweigher smb-config can update it.
634
+ [[ -f "${DATA_DIR}/smb_config.py" ]] && \
635
+ chown "${PI_USER}:${PI_USER}" "${DATA_DIR}/smb_config.py" && \
636
+ chmod 644 "${DATA_DIR}/smb_config.py"
637
+
638
+ ok "Source files locked (root:root 644 — sudo required to edit)"
639
+ ok "Data dir writable ${DATA_DIR} (queue / log / smb_config)"
640
+ info "To update source: sudo nano ${INSTALL_DIR}/plc_reader.py"
641
+ info "To update SMB: plc_checkweigher smb-config (no sudo needed)"
518
642
  }
519
643
 
520
644
  # ── 12. RT kernel — installed LAST so only one reboot is needed ───────────────
@@ -576,10 +700,11 @@ do_reboot() {
576
700
  banner "Setup Complete"
577
701
  echo ""
578
702
  PI_IP="$(hostname -I | awk '{print $1}' 2>/dev/null || echo '<pi-ip>')"
579
- printf " ${G}%-32s${NC} %s\n" "Repo:" "${INSTALL_DIR}"
703
+ printf " ${G}%-32s${NC} %s\n" "Source (root-locked):" "${INSTALL_DIR}"
704
+ printf " ${G}%-32s${NC} %s\n" "Data (pi-writable):" "${DATA_DIR}"
580
705
  printf " ${G}%-32s${NC} %s\n" "Python venv:" "${VENV_DIR}"
581
706
  printf " ${G}%-32s${NC} %s\n" "Reports output:" "${REPORTS_DIR}"
582
- printf " ${G}%-32s${NC} %s\n" "SMB config:" "${INSTALL_DIR}/smb_config.py"
707
+ printf " ${G}%-32s${NC} %s\n" "SMB config:" "${DATA_DIR}/smb_config.py"
583
708
  printf " ${G}%-32s${NC} %s\n" "RT kernel:" "kernel8-rt.img (active after reboot)"
584
709
  printf " ${G}%-32s${NC} %s\n" "Stock kernel fallback:" "kernel8-stock.img"
585
710
  echo ""
@@ -621,9 +746,11 @@ main() {
621
746
  setup_smb # 7 — interactive SMB config → smb_config.py
622
747
  setup_network_online # 8
623
748
  install_services # 9
624
- setup_boot_logo # 9 — Plymouth: logo + "SAI SAMARTH ENGG"
625
- setup_display # 10 — LightDM priority, CPU isolation, utmpx, GPU
626
- install_rt_kernel # 11LAST, so only one reboot needed
749
+ setup_boot_logo # 10 — Plymouth: logo + "SAI SAMARTH ENGG"
750
+ setup_display # 11 — LightDM priority, CPU isolation, utmpx
751
+ setup_vscode_priority # 11bVS Code: cores 0-2, Nice=-5
752
+ lock_source_files # 11c — root:root on .py, pi:pi on data/
753
+ install_rt_kernel # 12 — LAST, so only one reboot needed
627
754
  do_reboot # 12 — single reboot applies everything
628
755
  }
629
756