plc-checkweigher 1.15.0 → 1.17.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.
package/bin/cli.js CHANGED
@@ -86,28 +86,34 @@ function buildBanner() {
86
86
  return lines.join('\n');
87
87
  }
88
88
 
89
+ const STAMP_FILE = '/tmp/.plc-checkweigher-postinstall';
90
+
89
91
  function showAccessDenied() {
90
92
  const banner = buildBanner();
91
93
  const isPostinstall = process.env.npm_lifecycle_event === 'postinstall';
92
94
 
93
95
  if (isPostinstall) {
94
- // npm 7+ pipes away stdout/stderr of dependency lifecycle scripts.
95
- // Write directly to /dev/tty so it reaches the terminal regardless.
96
- // Skip when invoked by npx — npx installs into ~/.npm/_npx/ before
97
- // running the CLI, which would show the banner twice.
98
- const isNpx = __dirname.includes('_npx');
99
- if (!isNpx) {
100
- try {
101
- const fd = fs.openSync('/dev/tty', 'w');
102
- fs.writeSync(fd, banner + '\n');
103
- fs.closeSync(fd);
104
- } catch (_) {
105
- // No real terminal attached (CI, pipe) — silently skip.
106
- }
107
- }
96
+ // npm 7+ pipes away stdout/stderr write directly to /dev/tty.
97
+ try {
98
+ const fd = fs.openSync('/dev/tty', 'w');
99
+ fs.writeSync(fd, banner + '\n');
100
+ fs.closeSync(fd);
101
+ } catch (_) {}
102
+ // Always stamp regardless of whether /dev/tty was available,
103
+ // so the CLI invocation npx runs right after skips the banner.
104
+ try { fs.writeFileSync(STAMP_FILE, String(Date.now())); } catch (_) {}
108
105
  process.exit(0);
109
106
  }
110
107
 
108
+ // Suppress duplicate: if postinstall printed the banner within the last 3s, skip.
109
+ try {
110
+ const ts = parseInt(fs.readFileSync(STAMP_FILE, 'utf8'), 10);
111
+ if (Date.now() - ts < 3000) {
112
+ fs.unlinkSync(STAMP_FILE);
113
+ process.exit(1);
114
+ }
115
+ } catch (_) {}
116
+
111
117
  console.log(banner);
112
118
  process.exit(1);
113
119
  }
@@ -151,7 +151,16 @@ case "$CMD" in
151
151
 
152
152
  # ── System diagnostic ─────────────────────────────────────────────────────────
153
153
  status|check|diag)
154
- exec "${PYTHON}" "${INSTALL_DIR}/debugger.py" "$@"
154
+ set +e
155
+ "${PYTHON}" "${INSTALL_DIR}/debugger.py" "$@"
156
+ _STATUS_EXIT=$?
157
+ set -e
158
+ if [[ $_STATUS_EXIT -ne 0 ]]; then
159
+ echo ""
160
+ warn "Errors detected — starting auto-fix ..."
161
+ sleep 1
162
+ "$0" fix
163
+ fi
155
164
  ;;
156
165
 
157
166
  # ── Live logs ─────────────────────────────────────────────────────────────────
@@ -577,7 +586,326 @@ smb-config)
577
586
  ;;
578
587
 
579
588
  # ─────────────────────────────────────────────────────────────────────────────
580
- # UNINSTALLremove everything setup.sh installed
589
+ # FIXauto-detect and repair common issues
590
+ # Usage: fix [-wifi] [-health] [-programs] (no flags = run all)
591
+ # ─────────────────────────────────────────────────────────────────────────────
592
+ fix)
593
+ FIX_WIFI=0; FIX_HEALTH=0; FIX_PROGRAMS=0
594
+ if [[ $# -eq 0 ]]; then
595
+ FIX_WIFI=1; FIX_HEALTH=1; FIX_PROGRAMS=1
596
+ else
597
+ for _flag in "$@"; do
598
+ case "$_flag" in
599
+ -wifi) FIX_WIFI=1 ;;
600
+ -health) FIX_HEALTH=1 ;;
601
+ -programs) FIX_PROGRAMS=1 ;;
602
+ *) warn "Unknown flag: $_flag (valid: -wifi -health -programs)" ;;
603
+ esac
604
+ done
605
+ fi
606
+
607
+ LOG_DIR="/home/pi/reports/logs"
608
+ mkdir -p "$LOG_DIR" 2>/dev/null || true
609
+ # Build mode tag: "all" or hyphen-joined active scopes
610
+ _LOG_MODES=""
611
+ [[ $FIX_WIFI -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}wifi"
612
+ [[ $FIX_HEALTH -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}health"
613
+ [[ $FIX_PROGRAMS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}programs"
614
+ [[ "$_LOG_MODES" == "wifi-health-programs" ]] && _LOG_MODES="all"
615
+ LOG_FILE="${LOG_DIR}/fix_${_LOG_MODES}_$(date '+%Y%m%d_%H%M%S').log"
616
+ flog() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
617
+ ffix_ok() { spin_ok "$*"; flog "FIXED : $*"; FIX_COUNT=$((FIX_COUNT+1)); }
618
+ ffix_info() { spin_ok "$*"; flog "INFO : $*"; }
619
+ ffix_warn() { spin_warn "$*"; flog "WARN : $*"; }
620
+ ffix_err() { spin_err "$*"; flog "ERROR : $*"; }
621
+
622
+ FIX_COUNT=0
623
+ flog "=== Run started | wifi=${FIX_WIFI} health=${FIX_HEALTH} programs=${FIX_PROGRAMS} ==="
624
+
625
+ banner "Auto Fix"
626
+ echo ""
627
+ need_sudo
628
+
629
+ # ─────────────────────────────────────────────────────────────────────────
630
+ # WIFI
631
+ # ─────────────────────────────────────────────────────────────────────────
632
+ if [[ $FIX_WIFI -eq 1 ]]; then
633
+ echo -e " ${B}▸ WiFi${NC}"; hr; echo ""
634
+
635
+ spin_start "NetworkManager"
636
+ if ! systemctl is-active --quiet NetworkManager 2>/dev/null; then
637
+ sudo systemctl start NetworkManager 2>/dev/null || true; sleep 2
638
+ if systemctl is-active --quiet NetworkManager 2>/dev/null; then
639
+ ffix_ok "NetworkManager was down — restarted"
640
+ else
641
+ ffix_err "NetworkManager failed to start"
642
+ fi
643
+ else
644
+ ffix_info "NetworkManager running"
645
+ fi
646
+
647
+ spin_start "WiFi hardware block"
648
+ if rfkill list wifi 2>/dev/null | grep -q "Hard blocked: yes"; then
649
+ ffix_warn "Hard blocked — check physical WiFi switch"
650
+ elif rfkill list wifi 2>/dev/null | grep -q "Soft blocked: yes"; then
651
+ rfkill unblock wifi 2>/dev/null || true
652
+ ffix_ok "WiFi soft-block removed"
653
+ else
654
+ ffix_info "rfkill OK"
655
+ fi
656
+
657
+ spin_start "wlan0 interface"
658
+ if ! ip link show wlan0 &>/dev/null; then
659
+ ffix_warn "wlan0 not found — adapter missing?"
660
+ elif ! ip link show wlan0 2>/dev/null | grep -q "UP"; then
661
+ sudo ip link set wlan0 up 2>/dev/null || true; sleep 1
662
+ if ip link show wlan0 2>/dev/null | grep -q "UP"; then
663
+ ffix_ok "wlan0 was down — brought up"
664
+ else
665
+ ffix_err "Could not bring up wlan0"
666
+ fi
667
+ else
668
+ ffix_info "wlan0 UP"
669
+ fi
670
+
671
+ spin_start "WiFi connection"
672
+ _WIFI_CON=$(nmcli -t -f NAME,DEVICE,TYPE con show --active 2>/dev/null \
673
+ | grep ":wlan0:802-11-wireless" | cut -d: -f1 | head -1 || echo "")
674
+ if [[ -z "$_WIFI_CON" ]]; then
675
+ _LAST_CON=$(nmcli -t -f NAME,TYPE con show 2>/dev/null \
676
+ | grep ":802-11-wireless" | head -1 | cut -d: -f1 || echo "")
677
+ if [[ -n "$_LAST_CON" ]]; then
678
+ _spin_kill
679
+ spin_start "Reconnecting to '${_LAST_CON}'"
680
+ sudo nmcli connection up "$_LAST_CON" 2>/dev/null || true; sleep 3
681
+ _WIFI_NOW=$(nmcli -t -f NAME,DEVICE,TYPE con show --active 2>/dev/null \
682
+ | grep ":wlan0:802-11-wireless" | cut -d: -f1 | head -1 || echo "")
683
+ if [[ -n "$_WIFI_NOW" ]]; then
684
+ _NEW_IP=$(ip -4 addr show wlan0 2>/dev/null \
685
+ | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "")
686
+ ffix_ok "Reconnected to '${_WIFI_NOW}' (IP: ${_NEW_IP:-?})"
687
+ else
688
+ ffix_warn "Could not reconnect — run: plc_checkweigher wifi"
689
+ fi
690
+ else
691
+ ffix_warn "No saved connections — run: plc_checkweigher wifi"
692
+ fi
693
+ else
694
+ _WIFI_IP=$(ip -4 addr show wlan0 2>/dev/null \
695
+ | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "?")
696
+ ffix_info "'${_WIFI_CON}' (${_WIFI_IP})"
697
+ fi
698
+
699
+ spin_start "Internet connectivity"
700
+ if ping -c 2 -W 2 8.8.8.8 &>/dev/null; then
701
+ ffix_info "Internet reachable"
702
+ else
703
+ ffix_warn "No internet — local LAN may still work"
704
+ fi
705
+
706
+ echo ""
707
+ fi
708
+
709
+ # ─────────────────────────────────────────────────────────────────────────
710
+ # HEALTH
711
+ # ─────────────────────────────────────────────────────────────────────────
712
+ if [[ $FIX_HEALTH -eq 1 ]]; then
713
+ echo -e " ${B}▸ System Health${NC}"; hr; echo ""
714
+
715
+ spin_start "Disk space"
716
+ _DISK_PCT=$(df / --output=pcent 2>/dev/null | tail -1 | tr -d ' %' || echo "0")
717
+ if [[ "${_DISK_PCT:-0}" -ge 95 ]]; then
718
+ sudo journalctl --vacuum-size=100M 2>/dev/null || true
719
+ _DISK_AFTER=$(df / --output=pcent 2>/dev/null | tail -1 | tr -d ' %' || echo "?")
720
+ ffix_ok "Disk ${_DISK_PCT}% → ${_DISK_AFTER}% after journal clean"
721
+ elif [[ "${_DISK_PCT:-0}" -ge 85 ]]; then
722
+ ffix_warn "Disk ${_DISK_PCT}% — consider removing old reports"
723
+ else
724
+ ffix_info "Disk ${_DISK_PCT}%"
725
+ fi
726
+
727
+ spin_start "CPU temperature"
728
+ if command -v vcgencmd &>/dev/null; then
729
+ _TEMP=$(vcgencmd measure_temp 2>/dev/null | grep -oP '[\d.]+' | head -1 || echo "0")
730
+ _TEMP_INT=${_TEMP%.*}
731
+ if [[ "${_TEMP_INT:-0}" -ge 80 ]]; then
732
+ ffix_warn "${_TEMP}°C — check ventilation"
733
+ else
734
+ ffix_info "${_TEMP}°C"
735
+ fi
736
+ else
737
+ ffix_info "Temperature check not available"
738
+ fi
739
+
740
+ spin_start "Memory"
741
+ _MEM_FREE=$(free -m 2>/dev/null | awk '/^Mem:/{print $7}' || echo "999")
742
+ _MEM_TOTAL=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}' || echo "1")
743
+ if [[ "${_MEM_FREE:-999}" -lt 50 ]]; then
744
+ echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null 2>&1 || true
745
+ _MEM_AFTER=$(free -m 2>/dev/null | awk '/^Mem:/{print $7}' || echo "?")
746
+ ffix_ok "Low memory ${_MEM_FREE}MB → ${_MEM_AFTER}MB after cache drop"
747
+ else
748
+ ffix_info "${_MEM_FREE}MB / ${_MEM_TOTAL}MB free"
749
+ fi
750
+
751
+ spin_start "System time"
752
+ if timedatectl status 2>/dev/null | grep -q "synchronized: yes"; then
753
+ ffix_info "NTP synchronized"
754
+ else
755
+ sudo timedatectl set-ntp true 2>/dev/null || true
756
+ command -v chronyc &>/dev/null && sudo chronyc makestep 2>/dev/null || true
757
+ sleep 2
758
+ if timedatectl status 2>/dev/null | grep -q "synchronized: yes"; then
759
+ ffix_ok "NTP sync restored"
760
+ else
761
+ ffix_warn "Time not synced — no internet?"
762
+ fi
763
+ fi
764
+
765
+ spin_start "Reports directory"
766
+ _REPORTS_DIR="/home/pi/reports"
767
+ if [[ ! -d "$_REPORTS_DIR" ]]; then
768
+ mkdir -p "$_REPORTS_DIR" 2>/dev/null && ffix_ok "Created ${_REPORTS_DIR}" \
769
+ || ffix_warn "Could not create ${_REPORTS_DIR}"
770
+ else
771
+ ffix_info "${_REPORTS_DIR} OK"
772
+ fi
773
+
774
+ spin_start "SMB config file"
775
+ if [[ ! -f "$SMB_CFG" ]]; then
776
+ printf 'SMB_ENABLED = False\nSMB_HOST = ""\nSMB_SHARE = "Reports"\nSMB_USERNAME = ""\nSMB_PASSWORD = ""\nSMB_SUBDIR = ""\n' \
777
+ > "$SMB_CFG" 2>/dev/null
778
+ ffix_ok "Created default smb_config.py (disabled — run: plc_checkweigher smb-config)"
779
+ else
780
+ ffix_info "smb_config.py present"
781
+ fi
782
+
783
+ echo ""
784
+ fi
785
+
786
+ # ─────────────────────────────────────────────────────────────────────────
787
+ # PROGRAMS
788
+ # ─────────────────────────────────────────────────────────────────────────
789
+ if [[ $FIX_PROGRAMS -eq 1 ]]; then
790
+ echo -e " ${B}▸ Programs & Services${NC}"; hr; echo ""
791
+
792
+ for _SVC in plc_watcher plc_web; do
793
+ spin_start "${_SVC}"
794
+ _SVC_STATE=$(systemctl is-active "$_SVC" 2>/dev/null || echo "inactive")
795
+ if [[ "$_SVC_STATE" != "active" ]]; then
796
+ sudo systemctl enable "$_SVC" 2>/dev/null || true
797
+ sudo systemctl restart "$_SVC" 2>/dev/null || true; sleep 2
798
+ _SVC_NOW=$(systemctl is-active "$_SVC" 2>/dev/null || echo "inactive")
799
+ if [[ "$_SVC_NOW" == "active" ]]; then
800
+ ffix_ok "${_SVC} restarted (was: ${_SVC_STATE})"
801
+ else
802
+ ffix_err "${_SVC} failed — check: journalctl -u ${_SVC} -n 30"
803
+ fi
804
+ else
805
+ ffix_info "${_SVC} active"
806
+ fi
807
+ done
808
+
809
+ spin_start "Python venv"
810
+ if [[ ! -x "$PYTHON" ]]; then
811
+ ffix_err "Venv missing at ${PYTHON} — re-run installer"
812
+ else
813
+ ffix_info "Venv OK"
814
+ fi
815
+
816
+ spin_start "Python packages"
817
+ _PKG_MISSING=""
818
+ for _pkg in flask reportlab pymcprotocol impacket; do
819
+ "${PYTHON}" -c "import ${_pkg}" &>/dev/null 2>&1 || _PKG_MISSING="${_PKG_MISSING} ${_pkg}"
820
+ done
821
+ if [[ -n "$_PKG_MISSING" ]]; then
822
+ _spin_kill
823
+ spin_start "Installing:${_PKG_MISSING}"
824
+ "${PYTHON}" -m pip install ${_PKG_MISSING} --quiet 2>/dev/null || true
825
+ _PKG_STILL=""
826
+ for _pkg in ${_PKG_MISSING}; do
827
+ "${PYTHON}" -c "import ${_pkg}" &>/dev/null 2>&1 || _PKG_STILL="${_PKG_STILL} ${_pkg}"
828
+ done
829
+ if [[ -z "$_PKG_STILL" ]]; then
830
+ ffix_ok "Installed:${_PKG_MISSING}"
831
+ else
832
+ ffix_warn "Could not install:${_PKG_STILL}"
833
+ fi
834
+ else
835
+ ffix_info "All packages present"
836
+ fi
837
+
838
+ spin_start "Delivery queue"
839
+ _QUEUE="${INSTALL_DIR}/delivery_queue.json"
840
+ if [[ ! -f "$_QUEUE" ]]; then
841
+ echo "[]" > "$_QUEUE"
842
+ ffix_ok "Created missing delivery_queue.json"
843
+ elif ! "${PYTHON}" -c "import json; json.load(open('${_QUEUE}'))" &>/dev/null 2>&1; then
844
+ mv "$_QUEUE" "${_QUEUE}.broken.$(date +%s)" 2>/dev/null || true
845
+ echo "[]" > "$_QUEUE"
846
+ ffix_ok "Corrupt delivery queue reset (backup saved)"
847
+ else
848
+ ffix_info "delivery_queue.json valid"
849
+ fi
850
+
851
+ spin_start "systemd unit files"
852
+ _RESTORED=""
853
+ for _src in "${INSTALL_DIR}/plc_watcher.service" "${INSTALL_DIR}/web/plc_web.service"; do
854
+ [[ ! -f "$_src" ]] && continue
855
+ _dst="/etc/systemd/system/$(basename "$_src")"
856
+ if [[ ! -f "$_dst" ]]; then
857
+ sudo cp "$_src" "$_dst" 2>/dev/null || true
858
+ _RESTORED="${_RESTORED} $(basename "$_src")"
859
+ fi
860
+ done
861
+ if [[ -n "$_RESTORED" ]]; then
862
+ sudo systemctl daemon-reload 2>/dev/null || true
863
+ ffix_ok "Restored:${_RESTORED}"
864
+ else
865
+ ffix_info "Unit files present"
866
+ fi
867
+
868
+ echo ""
869
+ fi
870
+
871
+ # ── Summary ───────────────────────────────────────────────────────────────
872
+ flog "=== Complete: ${FIX_COUNT} fix(es) applied ==="
873
+ hr
874
+ echo ""
875
+ if [[ $FIX_COUNT -gt 0 ]]; then
876
+ ok "${FIX_COUNT} fix(es) applied"
877
+ else
878
+ ok "All checks passed — no fixes needed"
879
+ fi
880
+ info "Log: ${LOG_FILE}"
881
+
882
+ # ── Push log to SMB share ─────────────────────────────────────────────────
883
+ _SMB_HOST=$(smb_get "SMB_HOST")
884
+ _SMB_SHARE=$(smb_get "SMB_SHARE")
885
+ _SMB_USER=$(smb_get "SMB_USERNAME")
886
+ _SMB_PASS=$(smb_get "SMB_PASSWORD")
887
+ _SMB_EN=$(grep "^SMB_ENABLED" "${SMB_CFG}" 2>/dev/null | grep -qi "true" && echo "1" || echo "0")
888
+
889
+ if [[ "$_SMB_EN" == "1" && -n "$_SMB_HOST" && -n "$_SMB_USER" ]] \
890
+ && command -v smbclient &>/dev/null; then
891
+ spin_start "Uploading log to SMB share"
892
+ _LOG_BASENAME="$(basename "$LOG_FILE")"
893
+ if smbclient "//${_SMB_HOST}/${_SMB_SHARE}" \
894
+ -U "${_SMB_USER}%${_SMB_PASS}" \
895
+ -c "mkdir logs; put ${LOG_FILE} logs/${_LOG_BASENAME}" &>/dev/null 2>&1; then
896
+ spin_ok "Log pushed → //${_SMB_HOST}/${_SMB_SHARE}/logs/${_LOG_BASENAME}"
897
+ flog "INFO : Log uploaded to SMB //${_SMB_HOST}/${_SMB_SHARE}/logs/${_LOG_BASENAME}"
898
+ else
899
+ spin_warn "SMB upload failed — log saved locally only"
900
+ flog "WARN : SMB log upload failed"
901
+ fi
902
+ fi
903
+
904
+ echo ""
905
+ ;;
906
+
907
+ # ─────────────────────────────────────────────────────────────────────────────
908
+ # UNINSTALL — two modes: software-only or full drive wipe
581
909
  # ─────────────────────────────────────────────────────────────────────────────
582
910
  uninstall)
583
911
  PI_USER="${PI_USER:-pi}"
@@ -591,46 +919,83 @@ uninstall)
591
919
  echo -e "${R} ║ PLC CHECK-WEIGHER UNINSTALLER ║${NC}"
592
920
  echo -e "${R} ╚══════════════════════════════════════════════════════════╝${NC}"
593
921
  echo ""
594
- echo -e " This will permanently remove:"
922
+ echo -e " Choose uninstall mode:"
595
923
  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)"
924
+ echo -e " ${Y}1)${NC} ${W}Software only${NC}"
925
+ echo -e " ${D}Remove PLC services, code, venv and kernel config.${NC}"
926
+ echo -e " ${D}OS, user files and WiFi credentials are kept intact.${NC}"
606
927
  echo ""
607
- echo -e " ${D}System packages (git, python3-venv, samba-client) are NOT removed.${NC}"
928
+ echo -e " ${R}2)${NC} ${W}Clean drive${NC} — prepare for fresh OS flash"
929
+ echo -e " ${D}Everything above PLUS all user data and WiFi credentials,${NC}"
930
+ echo -e " ${D}then zeros the entire SD card.${NC}"
931
+ echo -e " ${R} ⚠ The Pi will NOT boot after this. Reflash required.${NC}"
608
932
  echo ""
609
- hr
933
+
934
+ while true; do
935
+ read -r -p " Mode [1 / 2]: " UNINSTALL_MODE </dev/tty
936
+ [[ "$UNINSTALL_MODE" == "1" || "$UNINSTALL_MODE" == "2" ]] && break
937
+ echo -e " ${R}Enter 1 or 2${NC}"
938
+ done
610
939
  echo ""
611
940
 
612
- read -r -p " Type YES to confirm full uninstall: " CONFIRM </dev/tty
613
- [[ "$CONFIRM" == "YES" ]] || { echo " Aborted."; exit 0; }
941
+ # ── Mode-specific confirmation ────────────────────────────────────────────
942
+ KEEP_REPORTS="Y"
943
+ if [[ "$UNINSTALL_MODE" == "1" ]]; then
944
+ hr
945
+ echo ""
946
+ echo -e " ${R}✗${NC} systemd services plc_watcher plc_web"
947
+ echo -e " ${R}✗${NC} project code ${INSTALL_DIR}"
948
+ echo -e " ${R}✗${NC} Python venv ${VENV_DIR}"
949
+ echo -e " ${R}✗${NC} CLI tool plc_checkweigher"
950
+ echo -e " ${R}✗${NC} Plymouth theme saismruth → default"
951
+ echo -e " ${R}✗${NC} RT kernel config /boot/firmware/config.txt"
952
+ echo -e " ${Y}!${NC} reports folder ${REPORTS_DIR} (you will be asked)"
953
+ echo ""
954
+ echo -e " ${D}System packages (git, python3-venv, samba-client) NOT removed.${NC}"
955
+ echo ""
956
+ hr; echo ""
957
+ read -r -p " Type YES to confirm: " _CONFIRM </dev/tty
958
+ [[ "$_CONFIRM" == "YES" ]] || { echo " Aborted."; exit 0; }
959
+ echo ""
960
+ read -r -p " Keep report PDFs in ${REPORTS_DIR}? [Y/n]: " KEEP_REPORTS </dev/tty
961
+ KEEP_REPORTS="${KEEP_REPORTS:-Y}"
962
+ else
963
+ # Detect the drive that holds the root filesystem
964
+ _ROOT_SRC=$(findmnt -n -o SOURCE / 2>/dev/null || echo "")
965
+ _ROOT_DEV=$(lsblk -no pkname "$_ROOT_SRC" 2>/dev/null || echo "")
966
+ _ROOT_DEV_PATH="/dev/${_ROOT_DEV}"
614
967
 
615
- echo ""
616
- read -r -p " Keep report PDFs in ${REPORTS_DIR}? [Y/n]: " KEEP_REPORTS </dev/tty
617
- KEEP_REPORTS="${KEEP_REPORTS:-Y}"
968
+ hr; echo ""
969
+ echo -e " ${R}⚠ DESTRUCTIVE READ CAREFULLY${NC}"
970
+ echo ""
971
+ echo -e " ${R}✗${NC} All PLC software, services and kernel config"
972
+ echo -e " ${R}✗${NC} All reports, logs and SMB credentials"
973
+ echo -e " ${R}✗${NC} SSH keys, bash history, npm cache, user configs"
974
+ echo -e " ${R}✗${NC} All saved WiFi connections"
975
+ echo -e " ${R}✗${NC} Entire drive zeroed: ${W}${_ROOT_DEV_PATH}${NC}"
976
+ echo ""
977
+ echo -e " ${R}The Pi will become unresponsive during the wipe.${NC}"
978
+ echo -e " ${D}Power off after ~2 minutes, then reflash with Raspberry Pi Imager.${NC}"
979
+ echo ""
980
+ hr; echo ""
981
+ read -r -p " Type WIPE to confirm drive wipe: " _CONFIRM </dev/tty
982
+ [[ "$_CONFIRM" == "WIPE" ]] || { echo " Aborted."; exit 0; }
983
+ fi
618
984
 
619
985
  echo ""
620
986
  need_sudo
621
987
 
622
- # Step counter
623
- _US=0; _UT=9
988
+ # ── Step counter (9 for software-only, 12 for clean drive) ───────────────
989
+ _UT=$([[ "$UNINSTALL_MODE" == "1" ]] && echo "9" || echo "12")
990
+ _US=0
624
991
  ustep() { _US=$((_US + 1)); spin_start "[${_US}/${_UT}] $*"; }
625
992
 
626
993
  # ── 1. Stop and disable services ─────────────────────────────────────────
627
994
  echo ""
628
995
  ustep "Stopping and disabling services"
629
996
  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
997
+ systemctl is-active --quiet "$SVC" 2>/dev/null && sudo systemctl stop "$SVC" 2>/dev/null || true
998
+ systemctl is-enabled --quiet "$SVC" 2>/dev/null && sudo systemctl disable "$SVC" 2>/dev/null || true
634
999
  done
635
1000
  sudo rm -f /etc/systemd/system/plc_watcher.service \
636
1001
  /etc/systemd/system/plc_web.service
@@ -657,27 +1022,23 @@ uninstall)
657
1022
  fi
658
1023
  spin_ok "Reverted to '${DEFAULT_THEME:-default}'"
659
1024
 
660
- # ── 4. Rebuild initramfs (slowest step — keep spinner alive) ─────────────
1025
+ # ── 4. Rebuild initramfs ─────────────────────────────────────────────────
661
1026
  ustep "Rebuilding initramfs ${D}(~30 s)${NC}"
662
1027
  sudo update-initramfs -u > /tmp/uninstall_initramfs.log 2>&1 \
663
- && spin_ok \
664
- || spin_warn "Warnings — see /tmp/uninstall_initramfs.log"
1028
+ && spin_ok || spin_warn "Warnings — see /tmp/uninstall_initramfs.log"
665
1029
 
666
1030
  # ── 5. RT kernel revert ──────────────────────────────────────────────────
667
1031
  ustep "Reverting RT kernel config"
668
1032
  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"
1033
+ sudo sed -i '/### PLC-RT-BLOCK-START ###/,/### PLC-RT-BLOCK-END ###/d' "${BOOT_FW}/config.txt"
671
1034
  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"
1035
+ sudo rm -f "${BOOT_FW}/kernel8-rt.img" "${BOOT_FW}/initramfs8-rt" "${BOOT_FW}/kernel8-stock.img"
1036
+ spin_ok "Stock kernel restored"
676
1037
  else
677
1038
  spin_warn "config.txt not found — skipped"
678
1039
  fi
679
1040
 
680
- # ── 6. Hotspot + nmcli cleanup ───────────────────────────────────────────
1041
+ # ── 6. Network cleanup ───────────────────────────────────────────────────
681
1042
  ustep "Cleaning up network connections"
682
1043
  sudo nmcli connection delete "plc-hotspot" 2>/dev/null || true
683
1044
  sudo systemctl daemon-reload
@@ -685,15 +1046,10 @@ uninstall)
685
1046
 
686
1047
  # ── 7. Python venv ───────────────────────────────────────────────────────
687
1048
  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
1049
+ [[ -d "$VENV_DIR" ]] && rm -rf "$VENV_DIR" && spin_ok "Removed ${VENV_DIR}" || spin_ok "Already gone"
694
1050
 
695
- # ── 8. Reports (optional) + temp files ───────────────────────────────────
696
- ustep "Cleaning up runtime files"
1051
+ # ── 8. Runtime files + CLI ───────────────────────────────────────────────
1052
+ ustep "Cleaning up runtime files and CLI"
697
1053
  rm -f /tmp/plc_live.json 2>/dev/null || true
698
1054
  BASHRC="${HOME_DIR}/.bashrc"
699
1055
  [[ -f "$BASHRC" ]] && sed -i '/export PATH.*\.local\/bin/d' "$BASHRC" 2>/dev/null || true
@@ -704,16 +1060,61 @@ uninstall)
704
1060
  fi
705
1061
  spin_ok "CLI and temp files removed"
706
1062
 
707
- # ── 9. Remove project directory (self-deletes last) ───────────────────────
1063
+ # ── 9. Project code ──────────────────────────────────────────────────────
708
1064
  ustep "Removing project code"
709
1065
  if [[ -d "$INSTALL_DIR" ]]; then
710
- rm -rf "$INSTALL_DIR"
711
- spin_ok "Removed ${INSTALL_DIR}"
1066
+ rm -rf "$INSTALL_DIR" && spin_ok "Removed ${INSTALL_DIR}"
712
1067
  else
713
1068
  spin_ok "Already gone"
714
1069
  fi
715
1070
 
716
- # ── Done ─────────────────────────────────────────────────────────────────
1071
+ # ─────────────────────────────────────────────────────────────────────────
1072
+ # MODE 2 ONLY: wipe all user data then zero the drive
1073
+ # ─────────────────────────────────────────────────────────────────────────
1074
+ if [[ "$UNINSTALL_MODE" == "2" ]]; then
1075
+
1076
+ # ── 10. All WiFi connections ─────────────────────────────────────────
1077
+ ustep "Removing all WiFi connections"
1078
+ nmcli -t -f NAME con show 2>/dev/null | while IFS= read -r _CON; do
1079
+ [[ -n "$_CON" ]] && sudo nmcli connection delete "$_CON" 2>/dev/null || true
1080
+ done
1081
+ spin_ok "All network connections cleared"
1082
+
1083
+ # ── 11. User data ────────────────────────────────────────────────────
1084
+ ustep "Wiping user data"
1085
+ rm -rf "${REPORTS_DIR}" 2>/dev/null || true
1086
+ rm -rf "${HOME_DIR}/.ssh" 2>/dev/null || true
1087
+ rm -f "${HOME_DIR}/.bash_history" 2>/dev/null || true
1088
+ rm -rf "${HOME_DIR}/.npm" 2>/dev/null || true
1089
+ rm -rf "${HOME_DIR}/.config" 2>/dev/null || true
1090
+ rm -rf "${HOME_DIR}/.cache" 2>/dev/null || true
1091
+ rm -rf "${HOME_DIR}/.local" 2>/dev/null || true
1092
+ spin_ok "User data cleared"
1093
+
1094
+ # ── 12. Zero the drive ───────────────────────────────────────────────
1095
+ ustep "Zeroing drive — ${_ROOT_DEV_PATH}"
1096
+ _spin_kill
1097
+ echo ""
1098
+ echo -e " ${R}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
1099
+ echo -e " ${R} Starting drive wipe. The system will become unresponsive.${NC}"
1100
+ echo -e " ${R} Power off after 2 minutes and reflash with Pi Imager.${NC}"
1101
+ echo -e " ${R}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
1102
+ echo ""
1103
+ for _i in 10 9 8 7 6 5 4 3 2 1; do
1104
+ printf "\r Wiping in %2d s ... (Ctrl-C to abort) " "$_i"
1105
+ sleep 1
1106
+ done
1107
+ printf '\r\033[K'
1108
+ echo -e " ${R}Wiping ${_ROOT_DEV_PATH} ...${NC}"
1109
+ echo ""
1110
+ sudo dd if=/dev/zero of="${_ROOT_DEV_PATH}" bs=4M status=progress 2>&1 || true
1111
+ # The system will have crashed by here. This line is a safety net.
1112
+ echo ""
1113
+ warn "Wipe complete — power off and reflash."
1114
+ exit 0
1115
+ fi
1116
+
1117
+ # ── Done (mode 1 only) ────────────────────────────────────────────────────
717
1118
  echo ""
718
1119
  echo -e "${G}"
719
1120
  echo " ╔══════════════════════════════════════════════════════════╗"
@@ -724,13 +1125,10 @@ uninstall)
724
1125
  echo -e "${NC}"
725
1126
  [[ "${KEEP_REPORTS^^}" == "Y" ]] && info "PDFs still at: ${REPORTS_DIR} (remove manually if needed)"
726
1127
  echo ""
727
-
728
1128
  read -r -p " Reboot now? [Y/n]: " DO_REBOOT </dev/tty
729
1129
  DO_REBOOT="${DO_REBOOT:-Y}"
730
1130
  if [[ "${DO_REBOOT^^}" == "Y" ]]; then
731
- spin_start "Rebooting"
732
- sleep 1
733
- sudo reboot
1131
+ spin_start "Rebooting"; sleep 1; sudo reboot
734
1132
  else
735
1133
  warn "Remember to reboot for kernel changes to take effect."
736
1134
  echo ""
@@ -745,7 +1143,11 @@ help|--help|-h)
745
1143
  echo -e "${B} plc_checkweigher${NC} — PLC Check-Weigher system CLI"
746
1144
  echo ""
747
1145
  echo -e " ${W}Diagnostics${NC}"
748
- echo " status Full system diagnostic all checks + fix hints"
1146
+ echo " status Full system diagnostic (auto-runs fix on errors)"
1147
+ echo " fix Auto-detect and repair all issues + write log"
1148
+ echo " fix -wifi Fix WiFi connectivity only"
1149
+ echo " fix -health Fix disk, memory, time, dirs"
1150
+ echo " fix -programs Fix services, packages, queue, unit files"
749
1151
  echo " logs Stream live logs (plc_watcher + plc_web)"
750
1152
  echo " queue Show SMB pending queue and delivery ledger"
751
1153
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.15.0",
3
+ "version": "1.17.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
  # =============================================================================
@@ -494,7 +495,7 @@ Nice=-5
494
495
 
495
496
  LimitNOFILE=65536
496
497
  EOF
497
- ok "LightDM: CPUAffinity=0-2, Nice=-5, network dep removed"
498
+ ok "LightDM: CPUAffinity=0-2, Nice=-5, restarts up to 20×, network dep removed"
498
499
 
499
500
  # ── Fix utmpx — PAM needs /run/utmp to track sessions ───────────────────
500
501
  cat > /etc/tmpfiles.d/utmp-fix.conf << 'EOF'
@@ -503,20 +504,57 @@ EOF
503
504
  systemd-tmpfiles --create /etc/tmpfiles.d/utmp-fix.conf 2>/dev/null || true
504
505
  ok "/run/utmp fixed (utmpx PAM session tracking)"
505
506
 
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"
510
- else
511
- echo "gpu_mem=128" >> "${BOOT_FW}/config.txt"
512
- fi
513
- ok "gpu_mem=128 set in config.txt (128 MB VRAM)"
507
+ # gpu_mem is intentionally left at Pi OS firmware default.
508
+ # display_auto_detect=1 (already in config.txt) handles VRAM and output
509
+ # automatically whether the Pi is headless or a display is connected.
514
510
 
515
511
  systemctl daemon-reload
516
512
  systemctl enable lightdm.service 2>/dev/null || true
517
513
  ok "LightDM enabled — starts on every boot when display is connected"
518
514
  }
519
515
 
516
+ # ── 11b. VS Code server priority ──────────────────────────────────────────────
517
+ setup_vscode_priority() {
518
+ step "VS Code server priority ..."
519
+
520
+ cat > /usr/local/bin/vscode-priority-daemon << 'DAEMON'
521
+ #!/usr/bin/env bash
522
+ # Apply CPU affinity (cores 0-2) and Nice=-5 to VS Code server processes.
523
+ # Core 3 is reserved exclusively for the SCHED_FIFO PLC process.
524
+ # Runs every 60s so newly-spawned extension host processes are caught promptly.
525
+ while true; do
526
+ mapfile -t pids < <(pgrep -u pi -f '\.vscode-server' 2>/dev/null || true)
527
+ for pid in "${pids[@]}"; do
528
+ taskset -cp 0-2 "$pid" >/dev/null 2>&1 || true
529
+ renice -n -5 -p "$pid" >/dev/null 2>&1 || true
530
+ done
531
+ sleep 60
532
+ done
533
+ DAEMON
534
+ chmod +x /usr/local/bin/vscode-priority-daemon
535
+
536
+ cat > /etc/systemd/system/vscode-priority.service << 'EOF'
537
+ [Unit]
538
+ Description=VS Code Server priority manager (cores 0-2, Nice=-5)
539
+ After=multi-user.target
540
+ Wants=multi-user.target
541
+
542
+ [Service]
543
+ Type=simple
544
+ ExecStart=/usr/local/bin/vscode-priority-daemon
545
+ Restart=always
546
+ RestartSec=10
547
+ User=root
548
+
549
+ [Install]
550
+ WantedBy=multi-user.target
551
+ EOF
552
+
553
+ systemctl daemon-reload
554
+ systemctl enable vscode-priority.service
555
+ ok "vscode-priority.service (cores 0-2, Nice=-5) — starts on every boot"
556
+ }
557
+
520
558
  # ── 12. RT kernel — installed LAST so only one reboot is needed ───────────────
521
559
  install_rt_kernel() {
522
560
  step "PREEMPT_RT kernel (final step before reboot) ..."
@@ -621,9 +659,10 @@ main() {
621
659
  setup_smb # 7 — interactive SMB config → smb_config.py
622
660
  setup_network_online # 8
623
661
  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
662
+ setup_boot_logo # 10 — Plymouth: logo + "SAI SAMARTH ENGG"
663
+ setup_display # 11 — LightDM priority, CPU isolation, utmpx
664
+ setup_vscode_priority # 11bVS Code: cores 0-2, Nice=-5
665
+ install_rt_kernel # 12 — LAST, so only one reboot needed
627
666
  do_reboot # 12 — single reboot applies everything
628
667
  }
629
668