plc-checkweigher 1.17.0 → 1.21.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 +237 -14
  2. package/package.json +1 -1
  3. package/setup.sh +109 -21
@@ -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
@@ -210,8 +211,8 @@ stop)
210
211
  queue)
211
212
  banner "SMB Delivery Queue"
212
213
  echo ""
213
- QUEUE="${INSTALL_DIR}/delivery_queue.json"
214
- LEDGER="${INSTALL_DIR}/delivery_sent.log"
214
+ QUEUE="${DATA_DIR}/delivery_queue.json"
215
+ LEDGER="${DATA_DIR}/delivery_sent.log"
215
216
  if [[ -f "$QUEUE" ]]; then
216
217
  COUNT=$(python3 -c "import json; d=json.load(open('$QUEUE')); print(len(d))" 2>/dev/null || echo 0)
217
218
  if [[ "$COUNT" -eq 0 ]]; then
@@ -587,19 +588,20 @@ smb-config)
587
588
 
588
589
  # ─────────────────────────────────────────────────────────────────────────────
589
590
  # FIX — auto-detect and repair common issues
590
- # Usage: fix [-wifi] [-health] [-programs] (no flags = run all)
591
+ # Usage: fix [-wifi] [-health] [-programs] [-errors] (no flags = run all)
591
592
  # ─────────────────────────────────────────────────────────────────────────────
592
593
  fix)
593
- FIX_WIFI=0; FIX_HEALTH=0; FIX_PROGRAMS=0
594
+ FIX_WIFI=0; FIX_HEALTH=0; FIX_PROGRAMS=0; FIX_ERRORS=0
594
595
  if [[ $# -eq 0 ]]; then
595
- FIX_WIFI=1; FIX_HEALTH=1; FIX_PROGRAMS=1
596
+ FIX_WIFI=1; FIX_HEALTH=1; FIX_PROGRAMS=1; FIX_ERRORS=1
596
597
  else
597
598
  for _flag in "$@"; do
598
599
  case "$_flag" in
599
600
  -wifi) FIX_WIFI=1 ;;
600
601
  -health) FIX_HEALTH=1 ;;
601
602
  -programs) FIX_PROGRAMS=1 ;;
602
- *) warn "Unknown flag: $_flag (valid: -wifi -health -programs)" ;;
603
+ -errors) FIX_ERRORS=1 ;;
604
+ *) warn "Unknown flag: $_flag (valid: -wifi -health -programs -errors)" ;;
603
605
  esac
604
606
  done
605
607
  fi
@@ -611,7 +613,8 @@ fix)
611
613
  [[ $FIX_WIFI -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}wifi"
612
614
  [[ $FIX_HEALTH -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}health"
613
615
  [[ $FIX_PROGRAMS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}programs"
614
- [[ "$_LOG_MODES" == "wifi-health-programs" ]] && _LOG_MODES="all"
616
+ [[ $FIX_ERRORS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}errors"
617
+ [[ "$_LOG_MODES" == "wifi-health-programs-errors" ]] && _LOG_MODES="all"
615
618
  LOG_FILE="${LOG_DIR}/fix_${_LOG_MODES}_$(date '+%Y%m%d_%H%M%S').log"
616
619
  flog() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
617
620
  ffix_ok() { spin_ok "$*"; flog "FIXED : $*"; FIX_COUNT=$((FIX_COUNT+1)); }
@@ -620,7 +623,7 @@ fix)
620
623
  ffix_err() { spin_err "$*"; flog "ERROR : $*"; }
621
624
 
622
625
  FIX_COUNT=0
623
- flog "=== Run started | wifi=${FIX_WIFI} health=${FIX_HEALTH} programs=${FIX_PROGRAMS} ==="
626
+ flog "=== Run started | wifi=${FIX_WIFI} health=${FIX_HEALTH} programs=${FIX_PROGRAMS} errors=${FIX_ERRORS} ==="
624
627
 
625
628
  banner "Auto Fix"
626
629
  echo ""
@@ -772,9 +775,17 @@ fix)
772
775
  fi
773
776
 
774
777
  spin_start "SMB config file"
778
+ mkdir -p "${DATA_DIR}" 2>/dev/null || true
775
779
  if [[ ! -f "$SMB_CFG" ]]; then
776
780
  printf 'SMB_ENABLED = False\nSMB_HOST = ""\nSMB_SHARE = "Reports"\nSMB_USERNAME = ""\nSMB_PASSWORD = ""\nSMB_SUBDIR = ""\n' \
777
- > "$SMB_CFG" 2>/dev/null
781
+ > "$SMB_CFG" 2>/dev/null || sudo tee "$SMB_CFG" > /dev/null << 'SMBC'
782
+ SMB_ENABLED = False
783
+ SMB_HOST = ""
784
+ SMB_SHARE = "Reports"
785
+ SMB_USERNAME = ""
786
+ SMB_PASSWORD = ""
787
+ SMB_SUBDIR = ""
788
+ SMBC
778
789
  ffix_ok "Created default smb_config.py (disabled — run: plc_checkweigher smb-config)"
779
790
  else
780
791
  ffix_info "smb_config.py present"
@@ -836,13 +847,14 @@ fix)
836
847
  fi
837
848
 
838
849
  spin_start "Delivery queue"
839
- _QUEUE="${INSTALL_DIR}/delivery_queue.json"
850
+ _QUEUE="${DATA_DIR}/delivery_queue.json"
851
+ mkdir -p "${DATA_DIR}" 2>/dev/null || true
840
852
  if [[ ! -f "$_QUEUE" ]]; then
841
- echo "[]" > "$_QUEUE"
853
+ echo "[]" > "$_QUEUE" 2>/dev/null || true
842
854
  ffix_ok "Created missing delivery_queue.json"
843
855
  elif ! "${PYTHON}" -c "import json; json.load(open('${_QUEUE}'))" &>/dev/null 2>&1; then
844
856
  mv "$_QUEUE" "${_QUEUE}.broken.$(date +%s)" 2>/dev/null || true
845
- echo "[]" > "$_QUEUE"
857
+ echo "[]" > "$_QUEUE" 2>/dev/null || true
846
858
  ffix_ok "Corrupt delivery queue reset (backup saved)"
847
859
  else
848
860
  ffix_info "delivery_queue.json valid"
@@ -868,6 +880,216 @@ fix)
868
880
  echo ""
869
881
  fi
870
882
 
883
+ # ─────────────────────────────────────────────────────────────────────────
884
+ # ERRORS — journal scan, RT scheduling, permissions, PLC reach, config validity
885
+ # ─────────────────────────────────────────────────────────────────────────
886
+ if [[ $FIX_ERRORS -eq 1 ]]; then
887
+ echo -e " ${B}▸ Programmatic Errors${NC}"; hr; echo ""
888
+
889
+ # ── 1. Service restart / crash count ─────────────────────────────────
890
+ spin_start "Service stability"
891
+ _SVC_STABLE=1
892
+ for _svc in plc_watcher plc_web; do
893
+ _RC=$(systemctl show -p NRestarts --value "$_svc" 2>/dev/null || echo "0")
894
+ _RC="${_RC:-0}"
895
+ if [[ "$_RC" =~ ^[0-9]+$ && "$_RC" -gt 10 ]]; then
896
+ _spin_kill
897
+ ffix_warn "${_svc}: ${_RC} restarts — likely crash loop"
898
+ flog "WARN: ${_svc} restart count=${_RC}"
899
+ _SVC_STABLE=0
900
+ elif [[ "$_RC" =~ ^[0-9]+$ && "$_RC" -gt 3 ]]; then
901
+ _spin_kill
902
+ ffix_warn "${_svc}: ${_RC} restart(s) since last boot"
903
+ flog "WARN: ${_svc} restart count=${_RC}"
904
+ _SVC_STABLE=0
905
+ fi
906
+ done
907
+ [[ $_SVC_STABLE -eq 1 ]] && ffix_info "Both services stable (low restart count)"
908
+
909
+ # ── 2. Journal error scan — last 2 hours ─────────────────────────────
910
+ spin_start "Scanning journal for errors (last 2 h)"
911
+ _J_RAW=$(journalctl -u plc_watcher -u plc_web --since "2 hours ago" \
912
+ --no-pager -q 2>/dev/null \
913
+ | grep -iE \
914
+ '(Traceback|ImportError|ModuleNotFoundError|SyntaxError|PermissionError|FileNotFoundError|JSONDecodeError|OSError|ConnectionRefusedError|TimeoutError|CRITICAL|FATAL)' \
915
+ | grep -v '^-- Logs begin' \
916
+ | tail -30 || true)
917
+
918
+ if [[ -n "$_J_RAW" ]]; then
919
+ _spin_kill
920
+ warn "Errors found in recent journal:"
921
+ while IFS= read -r _line; do
922
+ # Trim to 110 chars for display
923
+ printf " ${R}│${NC} ${D}%.110s${NC}\n" "$_line"
924
+ flog "JOURNAL: $_line"
925
+ done <<< "$_J_RAW"
926
+ echo ""
927
+
928
+ # ── Auto-fix: missing Python module ──────────────────────────────
929
+ _MISSING_MOD=$(echo "$_J_RAW" \
930
+ | grep -oP "No module named ['\"]?\K[a-zA-Z0-9_]+" | head -1 || true)
931
+ if [[ -n "$_MISSING_MOD" ]]; then
932
+ spin_start "Auto-fix: installing missing module '${_MISSING_MOD}'"
933
+ if "${PYTHON}" -m pip install "${_MISSING_MOD}" -q 2>/dev/null; then
934
+ ffix_ok "pip installed: ${_MISSING_MOD}"
935
+ sudo systemctl restart plc_watcher plc_web 2>/dev/null || true
936
+ ffix_ok "Services restarted after module install"
937
+ else
938
+ ffix_warn "pip install failed for: ${_MISSING_MOD} — try manually"
939
+ fi
940
+ fi
941
+
942
+ # ── Auto-fix: corrupt delivery_queue.json ─────────────────────────
943
+ if echo "$_J_RAW" | grep -q "JSONDecodeError"; then
944
+ _QFILE="${DATA_DIR}/delivery_queue.json"
945
+ if [[ -f "$_QFILE" ]]; then
946
+ spin_start "Auto-fix: resetting corrupt delivery queue"
947
+ cp "$_QFILE" "${_QFILE}.broken.$(date +%s)" 2>/dev/null || true
948
+ echo "[]" > "$_QFILE"
949
+ ffix_ok "delivery_queue.json reset (broken copy saved)"
950
+ fi
951
+ fi
952
+
953
+ # ── Auto-fix: PermissionError on data/ files ──────────────────────
954
+ if echo "$_J_RAW" | grep -q "PermissionError"; then
955
+ spin_start "Auto-fix: correcting data/ permissions"
956
+ sudo chown pi:pi "${DATA_DIR}" 2>/dev/null || true
957
+ sudo chmod 755 "${DATA_DIR}" 2>/dev/null || true
958
+ sudo chown pi:pi "${DATA_DIR}"/* 2>/dev/null || true
959
+ sudo chmod 644 "${DATA_DIR}"/* 2>/dev/null || true
960
+ ffix_ok "data/ ownership set to pi:pi 755/644"
961
+ fi
962
+
963
+ # ── Auto-fix: FileNotFoundError on a data/ file ───────────────────
964
+ if echo "$_J_RAW" | grep -q "FileNotFoundError.*data"; then
965
+ spin_start "Auto-fix: recreating missing data files"
966
+ mkdir -p "${DATA_DIR}" 2>/dev/null || true
967
+ [[ -f "${DATA_DIR}/delivery_queue.json" ]] \
968
+ || echo "[]" > "${DATA_DIR}/delivery_queue.json"
969
+ [[ -f "${DATA_DIR}/delivery_sent.log" ]] \
970
+ || touch "${DATA_DIR}/delivery_sent.log"
971
+ chown pi:pi "${DATA_DIR}"/* 2>/dev/null || true
972
+ ffix_ok "Missing data files recreated"
973
+ fi
974
+
975
+ else
976
+ ffix_info "No Python exceptions in recent logs"
977
+ fi
978
+
979
+ # ── 3. RT scheduling verification ─────────────────────────────────────
980
+ spin_start "RT scheduling (plc_watcher)"
981
+ _WPD=$(systemctl show -p MainPID --value plc_watcher 2>/dev/null || echo "0")
982
+ _WPD="${_WPD//[^0-9]/}"
983
+ if [[ "${_WPD:-0}" -gt 0 ]]; then
984
+ _SCHED=$(chrt -p "$_WPD" 2>/dev/null || echo "")
985
+ if echo "$_SCHED" | grep -q "SCHED_FIFO"; then
986
+ _PRIO=$(echo "$_SCHED" | grep -oP 'priority: \K\d+' || echo "?")
987
+ _AFF=$(taskset -cp "$_WPD" 2>/dev/null | grep -oP 'list: \K.*' || echo "?")
988
+ ffix_info "SCHED_FIFO:${_PRIO} cpus:${_AFF} pid:${_WPD}"
989
+ flog "INFO: plc_watcher SCHED_FIFO:${_PRIO} cpu:${_AFF}"
990
+ else
991
+ ffix_warn "RT scheduling not active — reloading unit and restarting"
992
+ flog "WARN: plc_watcher not SCHED_FIFO: ${_SCHED}"
993
+ sudo systemctl daemon-reload 2>/dev/null || true
994
+ sudo systemctl restart plc_watcher 2>/dev/null && ffix_ok "Service restarted" || ffix_err "Restart failed"
995
+ fi
996
+ else
997
+ ffix_warn "plc_watcher not running — cannot check RT scheduling"
998
+ fi
999
+
1000
+ # ── 4. data/ directory writability ────────────────────────────────────
1001
+ spin_start "data/ directory"
1002
+ if [[ ! -d "${DATA_DIR}" ]]; then
1003
+ sudo mkdir -p "${DATA_DIR}" 2>/dev/null || true
1004
+ sudo chown pi:pi "${DATA_DIR}" && sudo chmod 755 "${DATA_DIR}"
1005
+ ffix_ok "Created missing data/"
1006
+ elif [[ ! -w "${DATA_DIR}" ]]; then
1007
+ sudo chown pi:pi "${DATA_DIR}" && sudo chmod 755 "${DATA_DIR}"
1008
+ ffix_ok "Fixed data/ permissions (was not writable by pi)"
1009
+ flog "FIXED: data/ was not writable — corrected to pi:pi 755"
1010
+ else
1011
+ ffix_info "data/ writable by pi"
1012
+ fi
1013
+
1014
+ # ── 5. smb_config.py syntax ───────────────────────────────────────────
1015
+ spin_start "smb_config.py syntax"
1016
+ if [[ -f "${SMB_CFG}" ]]; then
1017
+ _SMB_ERR=$("${PYTHON}" -c "import ast; ast.parse(open('${SMB_CFG}').read())" 2>&1 || true)
1018
+ if [[ -n "${_SMB_ERR}" ]]; then
1019
+ flog "ERROR: smb_config.py syntax: ${_SMB_ERR}"
1020
+ ffix_warn "smb_config.py has syntax errors — resetting to disabled"
1021
+ printf 'SMB_ENABLED = False\nSMB_HOST = ""\nSMB_SHARE = "Reports"\nSMB_USERNAME = ""\nSMB_PASSWORD = ""\nSMB_SUBDIR = ""\n' \
1022
+ > "${SMB_CFG}" 2>/dev/null || true
1023
+ ffix_ok "smb_config.py reset — run: plc_checkweigher smb-config"
1024
+ else
1025
+ _SMB_EN=$(grep "^SMB_ENABLED" "${SMB_CFG}" 2>/dev/null \
1026
+ | grep -qi "true" && echo "enabled" || echo "disabled")
1027
+ _SMB_H=$(smb_get "SMB_HOST")
1028
+ ffix_info "smb_config.py valid (SMB: ${_SMB_EN}${_SMB_H:+ → ${_SMB_H}})"
1029
+ fi
1030
+ else
1031
+ ffix_warn "smb_config.py missing — run: plc_checkweigher smb-config"
1032
+ flog "WARN: smb_config.py not found at ${SMB_CFG}"
1033
+ fi
1034
+
1035
+ # ── 6. Source file protection (root:root 644) ─────────────────────────
1036
+ spin_start "Source file protection"
1037
+ _WRONG=$(find "${INSTALL_DIR}" -maxdepth 1 -name "*.py" \
1038
+ ! -user root 2>/dev/null | wc -l || echo 0)
1039
+ if [[ "${_WRONG:-0}" -gt 0 ]]; then
1040
+ ffix_warn "${_WRONG} source .py file(s) not root-owned — locking"
1041
+ flog "WARN: ${_WRONG} source file(s) had wrong owner"
1042
+ sudo find "${INSTALL_DIR}" -maxdepth 1 -name "*.py" \
1043
+ -exec chown root:root {} \; -exec chmod 644 {} \; 2>/dev/null || true
1044
+ sudo find "${INSTALL_DIR}/web" -name "*.py" \
1045
+ -exec chown root:root {} \; -exec chmod 644 {} \; 2>/dev/null || true
1046
+ ffix_ok "Source files re-locked to root:root 644"
1047
+ else
1048
+ ffix_info "Source files protected (root:root 644)"
1049
+ fi
1050
+
1051
+ # ── 7. PLC TCP reachability ───────────────────────────────────────────
1052
+ spin_start "PLC TCP connection (192.168.3.250:1025)"
1053
+ if timeout 3 bash -c "echo >/dev/tcp/192.168.3.250/1025" 2>/dev/null; then
1054
+ ffix_info "PLC reachable at 192.168.3.250:1025"
1055
+ else
1056
+ ffix_warn "PLC not reachable at 192.168.3.250:1025"
1057
+ flog "WARN: PLC TCP connect failed (192.168.3.250:1025)"
1058
+ echo ""
1059
+ info "Possible causes:"
1060
+ info " • eth0 cable unplugged (check: ip link show eth0)"
1061
+ info " • PLC powered off / not ready"
1062
+ info " • SLMP not enabled in GX Works (Enable SLMP TCP port 1025)"
1063
+ info " • Wrong subnet on eth0 (should be 192.168.3.x)"
1064
+ echo ""
1065
+ fi
1066
+
1067
+ # ── 8. plc_live.json staleness ────────────────────────────────────────
1068
+ spin_start "Live state file (/tmp/plc_live.json)"
1069
+ if [[ -f "/tmp/plc_live.json" ]]; then
1070
+ _AGE=$(( $(date +%s) - $(stat -c %Y /tmp/plc_live.json 2>/dev/null || echo 0) ))
1071
+ if [[ "${_AGE:-0}" -gt 10 ]]; then
1072
+ ffix_warn "plc_live.json is ${_AGE}s old — watcher may be stuck or offline"
1073
+ flog "WARN: plc_live.json stale (${_AGE}s old)"
1074
+ else
1075
+ ffix_info "plc_live.json updated ${_AGE}s ago (OK)"
1076
+ fi
1077
+ else
1078
+ ffix_info "plc_live.json absent (normal — created when watcher connects)"
1079
+ fi
1080
+
1081
+ # ── 9. reader script path ─────────────────────────────────────────────
1082
+ spin_start "plc_reader.py present"
1083
+ if [[ ! -f "${INSTALL_DIR}/plc_reader.py" ]]; then
1084
+ ffix_err "plc_reader.py missing — watcher cannot launch it"
1085
+ flog "ERROR: plc_reader.py not found at ${INSTALL_DIR}/plc_reader.py"
1086
+ else
1087
+ ffix_info "plc_reader.py present and readable"
1088
+ fi
1089
+
1090
+ echo ""
1091
+ fi
1092
+
871
1093
  # ── Summary ───────────────────────────────────────────────────────────────
872
1094
  flog "=== Complete: ${FIX_COUNT} fix(es) applied ==="
873
1095
  hr
@@ -1063,7 +1285,7 @@ uninstall)
1063
1285
  # ── 9. Project code ──────────────────────────────────────────────────────
1064
1286
  ustep "Removing project code"
1065
1287
  if [[ -d "$INSTALL_DIR" ]]; then
1066
- rm -rf "$INSTALL_DIR" && spin_ok "Removed ${INSTALL_DIR}"
1288
+ sudo rm -rf "$INSTALL_DIR" && spin_ok "Removed ${INSTALL_DIR}"
1067
1289
  else
1068
1290
  spin_ok "Already gone"
1069
1291
  fi
@@ -1148,6 +1370,7 @@ help|--help|-h)
1148
1370
  echo " fix -wifi Fix WiFi connectivity only"
1149
1371
  echo " fix -health Fix disk, memory, time, dirs"
1150
1372
  echo " fix -programs Fix services, packages, queue, unit files"
1373
+ echo " fix -errors Scan journal, verify RT/permissions/PLC/config"
1151
1374
  echo " logs Stream live logs (plc_watcher + plc_web)"
1152
1375
  echo " queue Show SMB pending queue and delivery ledger"
1153
1376
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.17.0",
3
+ "version": "1.21.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
@@ -32,6 +32,7 @@ REPO_URL="https://github.com/Bibin-VR/plc-checkweigher.git"
32
32
  REPO_BRANCH="main"
33
33
  HOME_DIR="/home/${PI_USER}"
34
34
  INSTALL_DIR="${HOME_DIR}/plc_checkweigher"
35
+ DATA_DIR="${INSTALL_DIR}/data" # pi-writable: queue, log, smb_config
35
36
  VENV_DIR="${HOME_DIR}/plc_env"
36
37
  REPORTS_DIR="${HOME_DIR}/reports"
37
38
  BOOT_FW="/boot/firmware"
@@ -107,15 +108,20 @@ install_system_packages() {
107
108
  # ── 2. Clone / update repo ────────────────────────────────────────────────────
108
109
  setup_repo() {
109
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
+
110
114
  if [[ -d "${INSTALL_DIR}/.git" ]]; then
111
- 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}" \
112
118
  && ok "Repo updated → ${INSTALL_DIR}" \
113
119
  || warn "git pull failed — using existing files"
114
120
  else
115
- sudo -u "${PI_USER}" git clone --branch "${REPO_BRANCH}" \
116
- "${REPO_URL}" "${INSTALL_DIR}"
121
+ git clone --branch "${REPO_BRANCH}" "${REPO_URL}" "${INSTALL_DIR}"
117
122
  ok "Repo cloned → ${INSTALL_DIR}"
118
123
  fi
124
+ # Permissions are finalised by lock_source_files() later.
119
125
  }
120
126
 
121
127
  # ── 3. Python venv ────────────────────────────────────────────────────────────
@@ -135,6 +141,13 @@ setup_dirs() {
135
141
  mkdir -p "${REPORTS_DIR}"
136
142
  chown "${PI_USER}:${PI_USER}" "${REPORTS_DIR}"
137
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)"
138
151
  }
139
152
 
140
153
  # ── CLI tool — install plc_checkweigher command ───────────────────────────────
@@ -253,8 +266,8 @@ setup_smb() {
253
266
  prompt SMB_HOST "Host IP address" ""
254
267
  if [[ -z "${SMB_HOST}" ]]; then
255
268
  warn "SMB push disabled — no host entered."
256
- # Write a disabled smb_config.py
257
- 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'
258
271
  # SMB push disabled during setup
259
272
  SMB_ENABLED = False
260
273
  SMB_HOST = ""
@@ -263,7 +276,7 @@ SMB_USERNAME = ""
263
276
  SMB_PASSWORD = ""
264
277
  SMB_SUBDIR = ""
265
278
  EOF
266
- chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/smb_config.py"
279
+ chown "${PI_USER}:${PI_USER}" "${DATA_DIR}/smb_config.py"
267
280
  return
268
281
  fi
269
282
 
@@ -275,8 +288,9 @@ EOF
275
288
  hr
276
289
  echo ""
277
290
 
278
- # Write smb_config.py (gitignored — credentials stay off GitHub)
279
- 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
280
294
  # SMB configuration — written by setup.sh, NOT committed to git.
281
295
  SMB_ENABLED = True
282
296
  SMB_HOST = "${SMB_HOST}"
@@ -285,8 +299,8 @@ SMB_USERNAME = "${SMB_USERNAME}"
285
299
  SMB_PASSWORD = "${SMB_PASSWORD}"
286
300
  SMB_SUBDIR = "${SMB_SUBDIR}"
287
301
  EOF
288
- chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/smb_config.py"
289
- 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"
290
304
 
291
305
  # Test connectivity
292
306
  echo -n " Testing connection to ${SMB_HOST} ..."
@@ -479,13 +493,13 @@ setup_display() {
479
493
  # Start after hardware udev settles (HDMI/DSI detected) — not after network.
480
494
  After=systemd-udev-settle.service local-fs.target acpid.socket dbus.service
481
495
  Wants=systemd-udev-settle.service
496
+ # StartLimit* MUST be in [Unit] — ignored in [Service].
497
+ StartLimitBurst=10
498
+ StartLimitIntervalSec=60
482
499
 
483
500
  [Service]
484
- # Generous restart policy — display should always recover.
485
- StartLimitBurst=20
486
- StartLimitIntervalSec=120
487
501
  Restart=on-failure
488
- RestartSec=3
502
+ RestartSec=5
489
503
 
490
504
  # CPU cores 0-2 only — core 3 is reserved for SCHED_FIFO PLC process.
491
505
  CPUAffinity=0 1 2
@@ -495,7 +509,7 @@ Nice=-5
495
509
 
496
510
  LimitNOFILE=65536
497
511
  EOF
498
- ok "LightDM: CPUAffinity=0-2, Nice=-5, restarts up to 20×, network dep removed"
512
+ ok "LightDM: CPUAffinity=0-2, Nice=-5, StartLimitBurst=10 in [Unit]"
499
513
 
500
514
  # ── Fix utmpx — PAM needs /run/utmp to track sessions ───────────────────
501
515
  cat > /etc/tmpfiles.d/utmp-fix.conf << 'EOF'
@@ -504,13 +518,33 @@ EOF
504
518
  systemd-tmpfiles --create /etc/tmpfiles.d/utmp-fix.conf 2>/dev/null || true
505
519
  ok "/run/utmp fixed (utmpx PAM session tracking)"
506
520
 
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.
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
535
+ else
536
+ sed -i 's/^hdmi_force_hotplug=.*/hdmi_force_hotplug=1/' "${BOOT_FW}/config.txt"
537
+ fi
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)"
510
544
 
511
545
  systemctl daemon-reload
512
546
  systemctl enable lightdm.service 2>/dev/null || true
513
- ok "LightDM enabled — starts on every boot when display is connected"
547
+ ok "LightDM enabled — starts on every boot"
514
548
  }
515
549
 
516
550
  # ── 11b. VS Code server priority ──────────────────────────────────────────────
@@ -555,6 +589,58 @@ EOF
555
589
  ok "vscode-priority.service (cores 0-2, Nice=-5) — starts on every boot"
556
590
  }
557
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)"
642
+ }
643
+
558
644
  # ── 12. RT kernel — installed LAST so only one reboot is needed ───────────────
559
645
  install_rt_kernel() {
560
646
  step "PREEMPT_RT kernel (final step before reboot) ..."
@@ -614,10 +700,11 @@ do_reboot() {
614
700
  banner "Setup Complete"
615
701
  echo ""
616
702
  PI_IP="$(hostname -I | awk '{print $1}' 2>/dev/null || echo '<pi-ip>')"
617
- 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}"
618
705
  printf " ${G}%-32s${NC} %s\n" "Python venv:" "${VENV_DIR}"
619
706
  printf " ${G}%-32s${NC} %s\n" "Reports output:" "${REPORTS_DIR}"
620
- 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"
621
708
  printf " ${G}%-32s${NC} %s\n" "RT kernel:" "kernel8-rt.img (active after reboot)"
622
709
  printf " ${G}%-32s${NC} %s\n" "Stock kernel fallback:" "kernel8-stock.img"
623
710
  echo ""
@@ -662,6 +749,7 @@ main() {
662
749
  setup_boot_logo # 10 — Plymouth: logo + "SAI SAMARTH ENGG"
663
750
  setup_display # 11 — LightDM priority, CPU isolation, utmpx
664
751
  setup_vscode_priority # 11b — VS Code: cores 0-2, Nice=-5
752
+ lock_source_files # 11c — root:root on .py, pi:pi on data/
665
753
  install_rt_kernel # 12 — LAST, so only one reboot needed
666
754
  do_reboot # 12 — single reboot applies everything
667
755
  }