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.
- package/bin/plc_checkweigher +237 -14
- package/package.json +1 -1
- package/setup.sh +109 -21
package/bin/plc_checkweigher
CHANGED
|
@@ -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
|
-
|
|
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="${
|
|
214
|
-
LEDGER="${
|
|
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
|
-
|
|
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
|
-
[[
|
|
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="${
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 > "${
|
|
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}" "${
|
|
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
|
-
|
|
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}" "${
|
|
289
|
-
ok "SMB config saved → ${
|
|
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=
|
|
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,
|
|
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
|
-
#
|
|
508
|
-
#
|
|
509
|
-
#
|
|
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
|
|
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" "
|
|
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:" "${
|
|
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
|
}
|