plc-checkweigher 1.32.3 → 1.33.1

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.
@@ -174,12 +174,13 @@ restart)
174
174
  need_sudo
175
175
  banner "Restarting services"
176
176
  echo ""
177
- spin_start "Restarting plc_watcher and plc_web"
178
- sudo systemctl restart plc_watcher plc_web
177
+ spin_start "Restarting plc_watcher, plc_web and plc_selfheal"
178
+ sudo systemctl restart plc_watcher plc_web plc_selfheal 2>/dev/null \
179
+ || sudo systemctl restart plc_watcher plc_web
179
180
  sleep 2
180
181
  spin_ok
181
182
  echo ""
182
- systemctl status plc_watcher plc_web --no-pager | grep -E 'Active|Main PID'
183
+ systemctl status plc_watcher plc_web plc_selfheal --no-pager 2>/dev/null | grep -E 'Active|Main PID'
183
184
  echo ""
184
185
  ;;
185
186
 
@@ -187,8 +188,9 @@ start)
187
188
  need_sudo
188
189
  banner "Starting services"
189
190
  echo ""
190
- spin_start "Starting plc_watcher and plc_web"
191
- sudo systemctl start plc_watcher plc_web
191
+ spin_start "Starting plc_watcher, plc_web and plc_selfheal"
192
+ sudo systemctl start plc_watcher plc_web plc_selfheal 2>/dev/null \
193
+ || sudo systemctl start plc_watcher plc_web
192
194
  sleep 1
193
195
  spin_ok
194
196
  echo ""
@@ -198,12 +200,15 @@ stop)
198
200
  need_sudo
199
201
  banner "Stopping services"
200
202
  echo ""
201
- spin_start "Stopping plc_watcher and plc_web"
203
+ # Stop the self-healing daemon FIRST — otherwise it would detect the
204
+ # watcher/web going down and immediately restart them, fighting this stop.
205
+ spin_start "Stopping plc_selfheal, plc_watcher and plc_web"
206
+ sudo systemctl stop plc_selfheal 2>/dev/null || true
202
207
  sudo systemctl stop plc_watcher plc_web
203
208
  spin_ok
204
209
  echo ""
205
210
  warn "Services stopped — will restart automatically on next boot"
206
- warn "To disable auto-start: sudo systemctl disable plc_watcher plc_web"
211
+ warn "To disable auto-start: sudo systemctl disable plc_watcher plc_web plc_selfheal"
207
212
  echo ""
208
213
  ;;
209
214
 
@@ -235,6 +240,87 @@ queue)
235
240
  echo ""
236
241
  ;;
237
242
 
243
+ # ── Self-healing daemon ────────────────────────────────────────────────────────
244
+ selfheal)
245
+ SUBCMD="${1:-status}"
246
+ shift || true
247
+ case "$SUBCMD" in
248
+ status)
249
+ banner "Self-Healing Daemon"
250
+ echo ""
251
+ ACTIVE=$(systemctl is-active plc_selfheal 2>/dev/null || true)
252
+ ENABLED=$(systemctl is-enabled plc_selfheal 2>/dev/null || true)
253
+ if [[ "$ACTIVE" == "active" ]]; then
254
+ ok "plc_selfheal: RUNNING (auto-start: ${ENABLED})"
255
+ else
256
+ warn "plc_selfheal: ${ACTIVE} (auto-start: ${ENABLED})"
257
+ info "Start it: sudo systemctl start plc_selfheal"
258
+ fi
259
+ echo ""
260
+ # Last-cycle / throttle state
261
+ _ST="/home/pi/reports/health/.selfheal_state.json"
262
+ if [[ -f "$_ST" ]]; then
263
+ "${PYTHON}" - "$_ST" << 'PYEOF' 2>/dev/null || true
264
+ import json, sys, time
265
+ try:
266
+ d = json.load(open(sys.argv[1]))
267
+ except Exception:
268
+ sys.exit(0)
269
+ lc = d.get("last_cycle", 0)
270
+ if lc:
271
+ age = int(time.time() - lc)
272
+ print(f" Last self-heal sweep: {age}s ago")
273
+ rep = d.get("reported", {})
274
+ if rep:
275
+ print(f" Active unresolved problems: {len(rep)}")
276
+ for k in rep: print(f" • {k}")
277
+ else:
278
+ print(" No unresolved problems")
279
+ PYEOF
280
+ else
281
+ info "No sweep has run yet (daemon may have just started)"
282
+ fi
283
+ echo ""
284
+ # Pending health reports on disk
285
+ _HD="/home/pi/reports/health"
286
+ if [[ -d "$_HD" ]]; then
287
+ _N=$(find "$_HD" -maxdepth 1 -name 'health_*.txt' 2>/dev/null | wc -l)
288
+ [[ "${_N:-0}" -gt 0 ]] && info "${_N} health report(s) in ${_HD}"
289
+ fi
290
+ echo ""
291
+ info "Recent activity: plc_checkweigher selfheal logs"
292
+ echo ""
293
+ ;;
294
+ logs)
295
+ banner "Self-Heal Logs"
296
+ echo ""
297
+ exec journalctl -u plc_selfheal -n 50 -f --no-pager
298
+ ;;
299
+ now|run)
300
+ banner "Self-Heal — Immediate Sweep"
301
+ need_sudo
302
+ echo ""
303
+ spin_start "Running one self-heal cycle"
304
+ sudo "${PYTHON}" -c "
305
+ import sys; sys.path.insert(0, '${INSTALL_DIR}')
306
+ import selfheal
307
+ healed, failed, env = selfheal.run_cycle()
308
+ selfheal.maybe_report(healed, failed, env)
309
+ print('HEALED:', len(healed), '| UNRESOLVED:', len(failed))
310
+ for k,d in healed: print(' [HEALED]', k, '-', d)
311
+ for k,d in failed: print(' [FAIL] ', k, '-', d)
312
+ " && spin_ok || spin_warn "Cycle completed with issues"
313
+ echo ""
314
+ ;;
315
+ *)
316
+ echo "Usage: plc_checkweigher selfheal [status|logs|now]"
317
+ echo " status — daemon state, last sweep, unresolved problems (default)"
318
+ echo " logs — follow the self-heal journal"
319
+ echo " now — run one self-heal sweep immediately"
320
+ ;;
321
+ esac
322
+ ;;
323
+
238
324
  # ── Push test ─────────────────────────────────────────────────────────────────
239
325
  push-test)
240
326
  banner "SMB Push Test"
@@ -612,20 +698,23 @@ console-passwd)
612
698
 
613
699
  # ─────────────────────────────────────────────────────────────────────────────
614
700
  # FIX — auto-detect and repair common issues
615
- # Usage: fix [-wifi] [-health] [-programs] [-errors] (no flags = run all)
701
+ # Usage: fix [-wifi] [-health] [-programs] [-errors] [-registers] (no flags = all but registers)
616
702
  # ─────────────────────────────────────────────────────────────────────────────
617
703
  fix)
618
- FIX_WIFI=0; FIX_HEALTH=0; FIX_PROGRAMS=0; FIX_ERRORS=0
704
+ FIX_WIFI=0; FIX_HEALTH=0; FIX_PROGRAMS=0; FIX_ERRORS=0; FIX_REGISTERS=0
619
705
  if [[ $# -eq 0 ]]; then
706
+ # -registers is opt-in only: it talks to the PLC and may rewrite the
707
+ # register map, so it is excluded from the default "fix everything" run.
620
708
  FIX_WIFI=1; FIX_HEALTH=1; FIX_PROGRAMS=1; FIX_ERRORS=1
621
709
  else
622
710
  for _flag in "$@"; do
623
711
  case "$_flag" in
624
- -wifi) FIX_WIFI=1 ;;
625
- -health) FIX_HEALTH=1 ;;
626
- -programs) FIX_PROGRAMS=1 ;;
627
- -errors) FIX_ERRORS=1 ;;
628
- *) warn "Unknown flag: $_flag (valid: -wifi -health -programs -errors)" ;;
712
+ -wifi) FIX_WIFI=1 ;;
713
+ -health) FIX_HEALTH=1 ;;
714
+ -programs) FIX_PROGRAMS=1 ;;
715
+ -errors) FIX_ERRORS=1 ;;
716
+ -registers) FIX_REGISTERS=1 ;;
717
+ *) warn "Unknown flag: $_flag (valid: -wifi -health -programs -errors -registers)" ;;
629
718
  esac
630
719
  done
631
720
  fi
@@ -634,10 +723,11 @@ fix)
634
723
  mkdir -p "$LOG_DIR" 2>/dev/null || true
635
724
  # Build mode tag: "all" or hyphen-joined active scopes
636
725
  _LOG_MODES=""
637
- [[ $FIX_WIFI -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}wifi"
638
- [[ $FIX_HEALTH -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}health"
639
- [[ $FIX_PROGRAMS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}programs"
640
- [[ $FIX_ERRORS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}errors"
726
+ [[ $FIX_WIFI -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}wifi"
727
+ [[ $FIX_HEALTH -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}health"
728
+ [[ $FIX_PROGRAMS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}programs"
729
+ [[ $FIX_ERRORS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}errors"
730
+ [[ $FIX_REGISTERS -eq 1 ]] && _LOG_MODES="${_LOG_MODES:+${_LOG_MODES}-}registers"
641
731
  [[ "$_LOG_MODES" == "wifi-health-programs-errors" ]] && _LOG_MODES="all"
642
732
  LOG_FILE="${LOG_DIR}/fix_${_LOG_MODES}_$(date '+%Y%m%d_%H%M%S').log"
643
733
  flog() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
@@ -647,7 +737,7 @@ fix)
647
737
  ffix_err() { spin_err "$*"; flog "ERROR : $*"; }
648
738
 
649
739
  FIX_COUNT=0
650
- flog "=== Run started | wifi=${FIX_WIFI} health=${FIX_HEALTH} programs=${FIX_PROGRAMS} errors=${FIX_ERRORS} ==="
740
+ flog "=== Run started | wifi=${FIX_WIFI} health=${FIX_HEALTH} programs=${FIX_PROGRAMS} errors=${FIX_ERRORS} registers=${FIX_REGISTERS} ==="
651
741
 
652
742
  banner "Auto Fix"
653
743
  echo ""
@@ -1152,6 +1242,37 @@ SMBC
1152
1242
  echo ""
1153
1243
  fi
1154
1244
 
1245
+ # ─────────────────────────────────────────────────────────────────────────
1246
+ # REGISTERS — confirm the live PLC register layout and auto-correct the map
1247
+ # ─────────────────────────────────────────────────────────────────────────
1248
+ if [[ $FIX_REGISTERS -eq 1 ]]; then
1249
+ echo -e " ${B}▸ PLC Registers${NC}"; hr; echo ""
1250
+ flog "=== Register scan ==="
1251
+ # The scanner connects to the PLC, decodes known + candidate registers,
1252
+ # and — if the read weight reads 0 while an item is on the scale —
1253
+ # detects the real register and writes data/register_map.json so the
1254
+ # code self-corrects without a source edit.
1255
+ _REG_OUT=$("${PYTHON}" -c "
1256
+ import sys; sys.path.insert(0, '${INSTALL_DIR}')
1257
+ import regmap
1258
+ changed, _ = regmap.scan(write=True)
1259
+ sys.exit(10 if changed else 0)
1260
+ " 2>&1)
1261
+ _REG_RC=$?
1262
+ echo "$_REG_OUT"
1263
+ while IFS= read -r _rl; do flog "REG: $_rl"; done <<< "$_REG_OUT"
1264
+ echo ""
1265
+ if [[ $_REG_RC -eq 10 ]]; then
1266
+ ffix_ok "Register map auto-corrected — read weight remapped"
1267
+ # New map takes effect on the reader's next item; restart to apply now.
1268
+ sudo systemctl restart plc_watcher 2>/dev/null || true
1269
+ ffix_info "plc_watcher restarted to apply the new register map"
1270
+ else
1271
+ ffix_info "Register scan complete — no remap needed"
1272
+ fi
1273
+ echo ""
1274
+ fi
1275
+
1155
1276
  # ── Summary ───────────────────────────────────────────────────────────────
1156
1277
  flog "=== Complete: ${FIX_COUNT} fix(es) applied ==="
1157
1278
  hr
@@ -1296,7 +1417,8 @@ update)
1296
1417
  # ── Update systemd unit files if they changed ─────────────────────────────
1297
1418
  _UNITS_UPDATED=0
1298
1419
  for _svc_src in "${INSTALL_DIR}/plc_watcher.service" \
1299
- "${INSTALL_DIR}/web/plc_web.service"; do
1420
+ "${INSTALL_DIR}/web/plc_web.service" \
1421
+ "${INSTALL_DIR}/plc_selfheal.service"; do
1300
1422
  [[ ! -f "$_svc_src" ]] && continue
1301
1423
  _svc_dst="/etc/systemd/system/$(basename "$_svc_src")"
1302
1424
  if [[ ! -f "$_svc_dst" ]] || ! diff -q "$_svc_src" "$_svc_dst" &>/dev/null; then
@@ -1309,6 +1431,22 @@ update)
1309
1431
  spin_ok "systemd reloaded"
1310
1432
  fi
1311
1433
 
1434
+ # ── Self-healing daemon present + enabled (retrofit older installs) ────────
1435
+ spin_start "Self-healing daemon"
1436
+ if [[ -f "${INSTALL_DIR}/selfheal.py" && -f /etc/systemd/system/plc_selfheal.service ]]; then
1437
+ if ! systemctl is-enabled --quiet plc_selfheal 2>/dev/null; then
1438
+ sudo systemctl enable plc_selfheal 2>/dev/null || true
1439
+ spin_ok "plc_selfheal enabled"
1440
+ elif ! systemctl is-active --quiet plc_selfheal 2>/dev/null; then
1441
+ sudo systemctl start plc_selfheal 2>/dev/null || true
1442
+ spin_ok "plc_selfheal started"
1443
+ else
1444
+ spin_ok "plc_selfheal active"
1445
+ fi
1446
+ else
1447
+ spin_warn "selfheal.py or unit missing — will be installed on next full setup"
1448
+ fi
1449
+
1312
1450
  # ── Update Plymouth boot splash ───────────────────────────────────────────
1313
1451
  _THEME_DIR="/usr/share/plymouth/themes/saismruth"
1314
1452
  _LOGO_SRC="${INSTALL_DIR}/assets/logo.png"
@@ -1566,6 +1704,7 @@ SUDOEOF
1566
1704
  if [[ $_CODE_CHANGED -eq 1 || $_UNITS_UPDATED -eq 1 ]]; then
1567
1705
  spin_start "Restarting services"
1568
1706
  sudo systemctl restart plc_watcher plc_web 2>/dev/null
1707
+ sudo systemctl restart plc_selfheal 2>/dev/null || true
1569
1708
  sleep 2
1570
1709
  spin_ok "Services restarted"
1571
1710
  else
@@ -1699,12 +1838,13 @@ uninstall)
1699
1838
  # ── 1. Stop and disable services ─────────────────────────────────────────
1700
1839
  echo ""
1701
1840
  ustep "Stopping and disabling services"
1702
- for SVC in plc_watcher plc_web; do
1841
+ for SVC in plc_selfheal plc_watcher plc_web; do
1703
1842
  systemctl is-active --quiet "$SVC" 2>/dev/null && sudo systemctl stop "$SVC" 2>/dev/null || true
1704
1843
  systemctl is-enabled --quiet "$SVC" 2>/dev/null && sudo systemctl disable "$SVC" 2>/dev/null || true
1705
1844
  done
1706
1845
  sudo rm -f /etc/systemd/system/plc_watcher.service \
1707
- /etc/systemd/system/plc_web.service
1846
+ /etc/systemd/system/plc_web.service \
1847
+ /etc/systemd/system/plc_selfheal.service
1708
1848
  spin_ok "Services removed"
1709
1849
 
1710
1850
  # ── 2. System drop-ins ───────────────────────────────────────────────────
@@ -1856,13 +1996,17 @@ help|--help|-h)
1856
1996
  echo " fix -health Fix disk, memory, time, dirs"
1857
1997
  echo " fix -programs Fix services, packages, queue, unit files"
1858
1998
  echo " fix -errors Scan journal, verify RT/permissions/PLC/config"
1999
+ echo " fix -registers Confirm live PLC register layout + auto-correct map"
1859
2000
  echo " logs Stream live logs (plc_watcher + plc_web)"
1860
2001
  echo " queue Show SMB pending queue and delivery ledger"
2002
+ echo " selfheal status Self-healing daemon state + unresolved problems"
2003
+ echo " selfheal logs Follow the self-heal journal"
2004
+ echo " selfheal now Run one self-heal sweep immediately"
1861
2005
  echo ""
1862
2006
  echo -e " ${W}Services${NC}"
1863
- echo " start Start plc_watcher and plc_web"
1864
- echo " stop Stop both services"
1865
- echo " restart Restart both services"
2007
+ echo " start Start watcher, web and self-heal"
2008
+ echo " stop Stop all services"
2009
+ echo " restart Restart all services"
1866
2010
  echo ""
1867
2011
  echo -e " ${W}Network${NC}"
1868
2012
  echo " wifi Scan and switch WiFi network"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.32.3",
3
+ "version": "1.33.1",
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
@@ -432,9 +432,37 @@ WantedBy=multi-user.target
432
432
  EOF
433
433
  ok "plc_web.service (Nice=-10)"
434
434
 
435
+ # ── Self-healing daemon — auto-repairs services/files/configs at runtime ──
436
+ cat > /etc/systemd/system/plc_selfheal.service << EOF
437
+ [Unit]
438
+ Description=PLC Check-Weigher Self-Healing Daemon
439
+ After=network.target plc_watcher.service plc_web.service
440
+ Wants=plc_watcher.service plc_web.service
441
+
442
+ [Service]
443
+ Type=simple
444
+ User=root
445
+ WorkingDirectory=${INSTALL_DIR}
446
+ Environment=PYTHONUNBUFFERED=1
447
+ ExecStart=${VENV_DIR}/bin/python3 -u ${INSTALL_DIR}/selfheal.py
448
+ Restart=always
449
+ RestartSec=10
450
+ Nice=10
451
+ CPUAffinity=0 1 2
452
+ IOSchedulingClass=idle
453
+ StandardOutput=journal
454
+ StandardError=journal
455
+
456
+ [Install]
457
+ WantedBy=multi-user.target
458
+ EOF
459
+ cp /etc/systemd/system/plc_selfheal.service "${INSTALL_DIR}/plc_selfheal.service"
460
+ chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/plc_selfheal.service"
461
+ ok "plc_selfheal.service (auto-repair · cores 0-2 · Nice=10)"
462
+
435
463
  systemctl daemon-reload
436
- systemctl enable plc_watcher.service plc_web.service
437
- ok "Both services enabled — start automatically after reboot"
464
+ systemctl enable plc_watcher.service plc_web.service plc_selfheal.service
465
+ ok "All services enabled — start automatically after reboot"
438
466
 
439
467
  cp /etc/systemd/system/plc_watcher.service "${INSTALL_DIR}/plc_watcher.service"
440
468
  chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/plc_watcher.service"