plc-checkweigher 1.6.0 → 1.11.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
@@ -6,61 +6,185 @@ const { platform, arch } = require('os');
6
6
  const path = require('path');
7
7
  const fs = require('fs');
8
8
 
9
- // ── Colour helpers ────────────────────────────────────────────────────────────
9
+ // ── Colours ───────────────────────────────────────────────────────────────────
10
10
  const B = '\x1b[1;34m'; // bold blue
11
11
  const G = '\x1b[0;32m'; // green
12
12
  const R = '\x1b[1;31m'; // red
13
13
  const Y = '\x1b[1;33m'; // yellow
14
- const NC = '\x1b[0m';
14
+ const D = '\x1b[2m'; // dim
15
+ const NC = '\x1b[0m'; // reset
15
16
 
16
17
  function die(msg) {
17
18
  console.error(`\n${R}Error:${NC} ${msg}`);
18
19
  process.exit(1);
19
20
  }
20
21
 
21
- // ── Platform guards ───────────────────────────────────────────────────────────
22
- if (platform() !== 'linux') {
23
- die('This installer only runs on Raspberry Pi (Linux). Got: ' + platform());
22
+ // ── Dot-matrix font (5 px wide × 5 px tall, █ = lit, space = dark) ──────────
23
+ const GLYPHS = {
24
+ 'T': ['█████', ' █ ', ' █ ', ' █ ', ' █ '],
25
+ 'Ø': [' ███ ', '█ /█', '█ / █', '█/ █', ' ███ '],
26
+ 'V': ['█ █', '█ █', ' █ █ ', ' █ █ ', ' █ '],
27
+ 'E': ['█████', '█ ', '████ ', '█ ', '█████'],
28
+ 'X': ['█ █', ' █ █ ', ' █ ', ' █ █ ', '█ █'],
29
+ 'S': [' ████', '█ ', ' ███ ', ' █', '████ '],
30
+ 'Y': ['█ █', ' █ █ ', ' █ ', ' █ ', ' █ '],
31
+ 'M': ['█ █', '██ ██', '█ █ █', '█ █', '█ █'],
32
+ ' ': [' ', ' ', ' ', ' ', ' '],
33
+ };
34
+
35
+ /**
36
+ * Returns 5 equal-length strings representing the dot-matrix rows of `word`.
37
+ * Each character glyph is 5 wide; glyphs are separated by a single space.
38
+ */
39
+ function dotRows(word) {
40
+ const rows = ['', '', '', '', ''];
41
+ for (const ch of word.toUpperCase()) {
42
+ const g = GLYPHS[ch] || GLYPHS[' '];
43
+ for (let i = 0; i < 5; i++) rows[i] += g[i] + ' ';
44
+ }
45
+ // Remove the one trailing separator space added after the last glyph
46
+ return rows.map(r => r.slice(0, -1));
24
47
  }
25
- if (arch() !== 'arm64') {
26
- die('Requires 64-bit ARM (arm64). Got: ' + arch() +
27
- '\nMake sure you are running 64-bit Raspberry Pi OS.');
48
+
49
+ // ── TØVEX-SYSTEMS access banner ───────────────────────────────────────────────
50
+ function showAccessDenied() {
51
+ const INNER = 52; // characters between the ║ borders
52
+
53
+ // Center a plain-text string inside INNER
54
+ function cen(str) {
55
+ const len = str.length;
56
+ const lpad = Math.floor((INNER - len) / 2);
57
+ const rpad = INNER - len - lpad;
58
+ return ' '.repeat(Math.max(0, lpad)) + str + ' '.repeat(Math.max(0, rpad));
59
+ }
60
+
61
+ const bar = '═'.repeat(INNER);
62
+ const blank = `${B}║${' '.repeat(INNER)}║${NC}`;
63
+
64
+ function boxRow(str, color) {
65
+ return `${B}║${NC}${color}${cen(str)}${NC}${B}║${NC}`;
66
+ }
67
+
68
+ const tRow = dotRows('TØVEX');
69
+ const sRow = dotRows('SYSTEMS');
70
+
71
+ // Dot-separator exactly as wide as SYSTEMS
72
+ const sep = Array.from({ length: sRow[0].length }, (_, i) => i % 2 ? ' ' : '·').join('');
73
+
74
+ // ── Print box ─────────────────────────────────────────────────────────────
75
+ console.log('');
76
+ console.log(`${B}╔${bar}╗${NC}`);
77
+ console.log(blank);
78
+ for (const r of tRow) console.log(boxRow(r, B));
79
+ console.log(blank);
80
+ console.log(boxRow(sep, D));
81
+ console.log(blank);
82
+ for (const r of sRow) console.log(boxRow(r, B));
83
+ console.log(blank);
84
+ console.log(`${B}╚${bar}╝${NC}`);
85
+
86
+ // ── Access-denied message ─────────────────────────────────────────────────
87
+ console.log('');
88
+ console.log(` ${R}⚠ Please contact administrator for access${NC}`);
89
+ console.log('');
90
+ console.log(` ${D}Install:${NC} npx plc-checkweigher ${Y}-tov${NC}`);
91
+ console.log(` ${D}Uninstall:${NC} npx plc-checkweigher ${Y}-ex${NC}`);
92
+ console.log(` ${D}Help:${NC} npx plc-checkweigher ${Y}--help${NC}`);
93
+ console.log('');
94
+
95
+ process.exit(1);
28
96
  }
29
97
 
30
- // ── Banner ────────────────────────────────────────────────────────────────────
31
- console.log(`
32
- ${B}╔════════════════════════════════════════════╗
33
- PLC Check-Weigher — Full Stack Installer ║
34
- ╚════════════════════════════════════════════╝${NC}
98
+ // ── Argument parsing ──────────────────────────────────────────────────────────
99
+ const arg = (process.argv[2] || '').trim();
100
+
101
+ const INSTALL_FLAGS = ['-tov', '--install', 'install'];
102
+ const UNINSTALL_FLAGS = ['-ex', '--uninstall', 'uninstall', 'remove'];
103
+ const HELP_FLAGS = ['-h', '--help', 'help'];
104
+
105
+ if (HELP_FLAGS.includes(arg)) {
106
+ console.log(`
107
+ ${B}npx plc-checkweigher${NC} — PLC Check-Weigher setup utility
108
+
109
+ ${Y}npx plc-checkweigher -tov${NC} Install ${D}(set up stack, services, RT kernel)${NC}
110
+ ${Y}npx plc-checkweigher -ex${NC} Uninstall ${D}(remove everything)${NC}
111
+ ${Y}npx plc-checkweigher --help${NC} Help
112
+
113
+ ${D}Aliases:${NC}
114
+ ${D}-tov / --install / install${NC}
115
+ ${D}-ex / --uninstall / uninstall / remove${NC}
35
116
  `);
117
+ process.exit(0);
118
+ }
119
+
120
+ let mode = 'access'; // default: show brand banner + access denied
121
+ if (INSTALL_FLAGS.includes(arg)) mode = 'install';
122
+ else if (UNINSTALL_FLAGS.includes(arg)) mode = 'uninstall';
123
+ else if (arg !== '') die(`Unknown argument: ${arg}\nRun: npx plc-checkweigher --help`);
124
+
125
+ // ── Platform guards (skip for help / access-denied) ───────────────────────────
126
+ if (mode !== 'access') {
127
+ if (platform() !== 'linux')
128
+ die('This installer only runs on Raspberry Pi (Linux). Got: ' + platform());
129
+ if (arch() !== 'arm64')
130
+ die('Requires 64-bit ARM (arm64). Got: ' + arch() +
131
+ '\nMake sure you are running 64-bit Raspberry Pi OS.');
132
+ }
133
+
134
+ // ── Locate scripts ────────────────────────────────────────────────────────────
135
+ const pkgRoot = path.resolve(__dirname, '..');
136
+ const setupScript = path.join(pkgRoot, 'setup.sh');
137
+ const uninstallScript = path.join(pkgRoot, 'uninstall.sh');
138
+
139
+ // ── Dispatch ──────────────────────────────────────────────────────────────────
140
+ if (mode === 'access') {
141
+ showAccessDenied(); // exits with code 1
142
+ }
36
143
 
37
- // ── Locate setup.sh (bundled inside this npm package) ────────────────────────
38
- const setupScript = path.resolve(__dirname, '..', 'setup.sh');
39
- if (!fs.existsSync(setupScript)) {
40
- die('setup.sh not found inside the package — try: npm install -g plc-checkweigher@latest');
144
+ if (mode === 'install') {
145
+ if (!fs.existsSync(setupScript))
146
+ die('setup.sh not found — try: npx plc-checkweigher@latest -tov');
147
+
148
+ console.log(`
149
+ ${B}╔══════════════════════════════════════════════╗
150
+ ║ PLC Check-Weigher — Full Stack Installer ║
151
+ ╚══════════════════════════════════════════════╝${NC}
152
+ `);
153
+ console.log(`${Y}This will:${NC}`);
154
+ console.log(' 1. Install the PREEMPT_RT real-time kernel (reboots once)');
155
+ console.log(' 2. Install all Python dependencies');
156
+ console.log(' 3. Clone / update the plc-checkweigher repo');
157
+ console.log(' 4. Configure WiFi, SMB file sharing (credentials → smb_config.py)');
158
+ console.log(' 5. Install systemd services with RT scheduling priority');
159
+ console.log(' 6. Set up live dashboard → http://<pi-ip>:8080/live');
160
+ console.log(' 7. Set up PDF report viewer with instant auto-refresh');
161
+ console.log('');
162
+ console.log(`${Y}Sudo password required to make system-level changes.${NC}`);
163
+ console.log('');
164
+
165
+ const result = spawnSync('sudo', ['bash', setupScript], {
166
+ stdio: 'inherit',
167
+ env: process.env,
168
+ });
169
+ process.exit(result.status ?? 0);
41
170
  }
42
171
 
43
- // ── Inform the user ───────────────────────────────────────────────────────────
44
- console.log(`${Y}This will:${NC}`);
45
- console.log(' 1. Install the PREEMPT_RT real-time kernel (reboots once)');
46
- console.log(' 2. Install all Python dependencies');
47
- console.log(' 3. Clone / update the plc-checkweigher repo');
48
- console.log(' 4. Configure WiFi, SMB file sharing (credentials → smb_config.py)');
49
- console.log(' 5. Install systemd services with RT scheduling priority');
50
- console.log(' 6. Set up live dashboard → http://<pi-ip>:8080/live');
51
- console.log(' 7. Set up PDF report viewer with instant auto-refresh');
52
- console.log('');
53
- console.log(`${Y}Sudo password required to make system-level changes.${NC}`);
54
- console.log('');
55
-
56
- // ── Run setup.sh via sudo ─────────────────────────────────────────────────────
57
- // stdio:'inherit' keeps the terminal fully interactive:
58
- // • sudo password prompt works
59
- // • WiFi password prompt works
60
- // • All colour output passes through
61
- const result = spawnSync('sudo', ['bash', setupScript], {
62
- stdio: 'inherit',
63
- env: process.env,
64
- });
65
-
66
- process.exit(result.status ?? 0);
172
+ if (mode === 'uninstall') {
173
+ if (!fs.existsSync(uninstallScript))
174
+ die('uninstall.sh not found try: npx plc-checkweigher@latest -ex');
175
+
176
+ console.log(`
177
+ ${R}╔══════════════════════════════════════════════╗
178
+ ║ PLC Check-Weigher Uninstaller ║
179
+ ╚══════════════════════════════════════════════╝${NC}
180
+ `);
181
+ console.log(`${D}Removes all services, code, venv, kernel config, and CLI tools.${NC}`);
182
+ console.log(`${D}You will be asked whether to keep your PDF reports.${NC}`);
183
+ console.log('');
184
+
185
+ const result = spawnSync('sudo', ['bash', uninstallScript], {
186
+ stdio: 'inherit',
187
+ env: process.env,
188
+ });
189
+ process.exit(result.status ?? 0);
190
+ }
@@ -0,0 +1,783 @@
1
+ #!/usr/bin/env bash
2
+ # plc_checkweigher — CLI for the PLC Check-Weigher system
3
+ # Installed to /usr/local/bin/ by setup.sh
4
+
5
+ set -euo pipefail
6
+
7
+ INSTALL_DIR="/home/pi/plc_checkweigher"
8
+ PYTHON="/home/pi/plc_env/bin/python3"
9
+ SMB_CFG="${INSTALL_DIR}/smb_config.py"
10
+
11
+ # ── TTY detection — animations only when connected to a real terminal ─────────
12
+ [[ -t 1 ]] && _TTY=1 || _TTY=0
13
+
14
+ # ── Colours ───────────────────────────────────────────────────────────────────
15
+ B='\033[1;34m'; G='\033[0;32m'; R='\033[1;31m'; Y='\033[1;33m'
16
+ C='\033[0;36m'; D='\033[2m'; W='\033[1m'; NC='\033[0m'
17
+
18
+ ok() { echo -e " ${G}✓${NC} $*"; }
19
+ err() { echo -e " ${R}✗${NC} $*" >&2; }
20
+ warn() { echo -e " ${Y}!${NC} $*"; }
21
+ info() { echo -e " ${C}i${NC} $*"; }
22
+ hr() { echo -e " ${D}$(printf '─%.0s' {1..56})${NC}"; }
23
+
24
+ # ── Typewrite banner ──────────────────────────────────────────────────────────
25
+ banner() {
26
+ echo ""
27
+ if [[ $_TTY -eq 1 ]]; then
28
+ printf " ${B}▸ ${NC}"
29
+ local txt="$*" i
30
+ for ((i=0; i<${#txt}; i++)); do
31
+ printf "${B}%s${NC}" "${txt:$i:1}"
32
+ sleep 0.022
33
+ done
34
+ echo ""
35
+ else
36
+ echo -e "${B} ▸ $*${NC}"
37
+ fi
38
+ }
39
+
40
+ # ── Spinner ───────────────────────────────────────────────────────────────────
41
+ _SP_PID=""
42
+ _SP_MSG=""
43
+ _SP_FRAMES=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
44
+
45
+ _spin_kill() {
46
+ if [[ -n "$_SP_PID" ]]; then
47
+ kill "$_SP_PID" 2>/dev/null || true
48
+ wait "$_SP_PID" 2>/dev/null || true
49
+ _SP_PID=""
50
+ fi
51
+ if [[ $_TTY -eq 1 ]]; then printf '\r\033[K'; fi
52
+ return 0
53
+ }
54
+ trap '_spin_kill' EXIT INT TERM
55
+
56
+ spin_start() {
57
+ _SP_MSG="${1:-}"
58
+ [[ $_TTY -eq 0 ]] && { echo -e " ${C}…${NC} ${_SP_MSG}"; return; }
59
+ _spin_kill
60
+ (
61
+ local i=0
62
+ while true; do
63
+ printf "\r ${C}%s${NC} %s " "${_SP_FRAMES[$((i % 10))]}" "${_SP_MSG}"
64
+ sleep 0.08
65
+ i=$((i + 1))
66
+ done
67
+ ) &
68
+ _SP_PID=$!
69
+ }
70
+
71
+ spin_ok() { local m="${_SP_MSG}"; _spin_kill; ok "${m}${1:+ ${D}$1${NC}}"; return 0; }
72
+ spin_warn() { local m="${_SP_MSG}"; _spin_kill; warn "${m}${1:+ ${D}$1${NC}}"; return 0; }
73
+ spin_err() { local m="${_SP_MSG}"; _spin_kill; err "${m}${1:+ ${D}$1${NC}}"; return 0; }
74
+
75
+ # ── sudo helper ───────────────────────────────────────────────────────────────
76
+ need_sudo() {
77
+ if [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then
78
+ echo -e " ${Y}sudo password required${NC}"
79
+ sudo -v
80
+ fi
81
+ }
82
+
83
+ # ── Read / write smb_config.py ────────────────────────────────────────────────
84
+ smb_get() {
85
+ grep "^${1}" "${SMB_CFG}" 2>/dev/null | head -1 | sed 's/.*= *"//;s/".*//'
86
+ }
87
+
88
+ # ── SMB config update prompt (reused by wifi + smb-config + hotspot scan) ────
89
+ prompt_smb_config() {
90
+ local context="${1:-}"
91
+ echo ""
92
+ banner "Update SMB Delivery Config"
93
+ [[ -n "$context" ]] && info "$context"
94
+ echo ""
95
+
96
+ local CUR_HOST CUR_SHARE CUR_USER CUR_PASS
97
+ CUR_HOST=$(smb_get "SMB_HOST")
98
+ CUR_SHARE=$(smb_get "SMB_SHARE")
99
+ CUR_USER=$(smb_get "SMB_USERNAME")
100
+ CUR_PASS=$(smb_get "SMB_PASSWORD")
101
+
102
+ hr
103
+ printf " ${B}%-22s${NC} ${D}current: %s${NC}\n" "Host IP" "${CUR_HOST:-(not set)}"
104
+ printf " ${B}%-22s${NC} ${D}current: %s${NC}\n" "Share name" "${CUR_SHARE:-(not set)}"
105
+ printf " ${B}%-22s${NC} ${D}current: %s${NC}\n" "Username" "${CUR_USER:-(not set)}"
106
+ printf " ${B}%-22s${NC} ${D}current: %s${NC}\n" "Password" "${CUR_PASS:-(not set)}"
107
+ hr
108
+ echo ""
109
+
110
+ local NEW_HOST NEW_SHARE NEW_USER NEW_PASS
111
+ read -r -p " Host IP [${CUR_HOST}]: " NEW_HOST </dev/tty; NEW_HOST="${NEW_HOST:-$CUR_HOST}"
112
+ read -r -p " Share name [${CUR_SHARE:-Reports}]: " NEW_SHARE </dev/tty; NEW_SHARE="${NEW_SHARE:-${CUR_SHARE:-Reports}}"
113
+ read -r -p " Username [${CUR_USER:-plcreport}]: " NEW_USER </dev/tty; NEW_USER="${NEW_USER:-${CUR_USER:-plcreport}}"
114
+ read -r -s -p " Password (blank = keep current): " NEW_PASS </dev/tty; echo ""; NEW_PASS="${NEW_PASS:-$CUR_PASS}"
115
+
116
+ cat > "${SMB_CFG}" << EOF
117
+ SMB_ENABLED = True
118
+ SMB_HOST = "${NEW_HOST}"
119
+ SMB_SHARE = "${NEW_SHARE}"
120
+ SMB_USERNAME = "${NEW_USER}"
121
+ SMB_PASSWORD = "${NEW_PASS}"
122
+ SMB_SUBDIR = ""
123
+ EOF
124
+
125
+ echo ""
126
+ ok "smb_config.py updated"
127
+ info "Target: //${NEW_HOST}/${NEW_SHARE} as ${NEW_USER}"
128
+ echo ""
129
+
130
+ spin_start "Pinging ${NEW_HOST}"
131
+ if ping -c 2 -W 2 "${NEW_HOST}" &>/dev/null; then
132
+ spin_ok "${NEW_HOST} reachable"
133
+ spin_start "Authenticating with //${NEW_HOST}/${NEW_SHARE}"
134
+ if smbclient "//${NEW_HOST}/${NEW_SHARE}" \
135
+ -U "${NEW_USER}%${NEW_PASS}" -c "ls" &>/dev/null 2>&1; then
136
+ spin_ok "SMB auth OK — PDF delivery ready"
137
+ else
138
+ spin_warn "Auth failed — verify share name and credentials on the PC"
139
+ fi
140
+ else
141
+ spin_warn "${NEW_HOST} not reachable — config saved, will retry at runtime"
142
+ fi
143
+ echo ""
144
+ }
145
+
146
+ # ─────────────────────────────────────────────────────────────────────────────
147
+ CMD="${1:-help}"
148
+ shift || true
149
+
150
+ case "$CMD" in
151
+
152
+ # ── System diagnostic ─────────────────────────────────────────────────────────
153
+ status|check|diag)
154
+ exec "${PYTHON}" "${INSTALL_DIR}/debugger.py" "$@"
155
+ ;;
156
+
157
+ # ── Live logs ─────────────────────────────────────────────────────────────────
158
+ logs)
159
+ exec journalctl -u plc_watcher -u plc_web -f --no-pager
160
+ ;;
161
+
162
+ # ── Service control ───────────────────────────────────────────────────────────
163
+ restart)
164
+ need_sudo
165
+ banner "Restarting services"
166
+ echo ""
167
+ spin_start "Restarting plc_watcher and plc_web"
168
+ sudo systemctl restart plc_watcher plc_web
169
+ sleep 2
170
+ spin_ok
171
+ echo ""
172
+ systemctl status plc_watcher plc_web --no-pager | grep -E 'Active|Main PID'
173
+ echo ""
174
+ ;;
175
+
176
+ start)
177
+ need_sudo
178
+ banner "Starting services"
179
+ echo ""
180
+ spin_start "Starting plc_watcher and plc_web"
181
+ sudo systemctl start plc_watcher plc_web
182
+ sleep 1
183
+ spin_ok
184
+ echo ""
185
+ ;;
186
+
187
+ stop)
188
+ need_sudo
189
+ banner "Stopping services"
190
+ echo ""
191
+ spin_start "Stopping plc_watcher and plc_web"
192
+ sudo systemctl stop plc_watcher plc_web
193
+ spin_ok
194
+ echo ""
195
+ warn "Services stopped — will restart automatically on next boot"
196
+ warn "To disable auto-start: sudo systemctl disable plc_watcher plc_web"
197
+ echo ""
198
+ ;;
199
+
200
+ # ── SMB queue ─────────────────────────────────────────────────────────────────
201
+ queue)
202
+ banner "SMB Delivery Queue"
203
+ echo ""
204
+ QUEUE="${INSTALL_DIR}/delivery_queue.json"
205
+ LEDGER="${INSTALL_DIR}/delivery_sent.log"
206
+ if [[ -f "$QUEUE" ]]; then
207
+ COUNT=$(python3 -c "import json; d=json.load(open('$QUEUE')); print(len(d))" 2>/dev/null || echo 0)
208
+ if [[ "$COUNT" -eq 0 ]]; then
209
+ ok "Queue empty — all reports delivered"
210
+ else
211
+ warn "${COUNT} file(s) pending delivery:"
212
+ python3 -m json.tool "$QUEUE" 2>/dev/null
213
+ fi
214
+ else
215
+ ok "Queue empty"
216
+ fi
217
+ echo ""
218
+ if [[ -f "$LEDGER" ]]; then
219
+ SENT=$(wc -l < "$LEDGER")
220
+ ok "Delivery ledger: ${SENT} file(s) sent"
221
+ sed 's/^/ /' "$LEDGER"
222
+ else
223
+ info "No deliveries recorded yet"
224
+ fi
225
+ echo ""
226
+ ;;
227
+
228
+ # ── Push test ─────────────────────────────────────────────────────────────────
229
+ push-test)
230
+ banner "SMB Push Test"
231
+ echo ""
232
+ LATEST=$(ls -t /home/pi/reports/*.pdf 2>/dev/null | head -1 || true)
233
+ if [[ -z "$LATEST" ]]; then
234
+ info "No PDFs found — generating one from PLC..."
235
+ "${PYTHON}" "${INSTALL_DIR}/plc_report.py"
236
+ LATEST=$(ls -t /home/pi/reports/*.pdf 2>/dev/null | head -1 || true)
237
+ fi
238
+ [[ -z "$LATEST" ]] && { err "Could not generate PDF — is PLC connected?"; exit 1; }
239
+ info "File: $(basename "$LATEST")"
240
+ echo ""
241
+ "${PYTHON}" -c "
242
+ import sys, os
243
+ sys.path.insert(0, '${INSTALL_DIR}')
244
+ from pdf_push import _push_smb, _already_sent, _record_sent
245
+ fname = os.path.basename('$LATEST')
246
+ if _already_sent(fname):
247
+ print(f' Already delivered: {fname}')
248
+ print(' Delete the ledger entry to re-test:')
249
+ print(f' sed -i \"/{fname}/d\" ${INSTALL_DIR}/delivery_sent.log')
250
+ sys.exit(0)
251
+ ok = _push_smb('$LATEST')
252
+ if ok:
253
+ _record_sent(fname)
254
+ sys.exit(0 if ok else 1)
255
+ "
256
+ ;;
257
+
258
+ # ─────────────────────────────────────────────────────────────────────────────
259
+ # WIFI — scan + switch network
260
+ # ─────────────────────────────────────────────────────────────────────────────
261
+ wifi)
262
+ SUBCMD="${1:-scan}"
263
+ shift || true
264
+
265
+ case "$SUBCMD" in
266
+ scan|"")
267
+ banner "WiFi Networks"
268
+ echo ""
269
+
270
+ spin_start "Scanning for networks"
271
+ nmcli dev wifi rescan ifname wlan0 2>/dev/null || true
272
+ sleep 2
273
+ spin_ok
274
+
275
+ mapfile -t RAW < <(
276
+ nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list ifname wlan0 2>/dev/null \
277
+ | awk -F: '$1!=""' \
278
+ | sort -t: -k2 -rn \
279
+ | awk -F: '!seen[$1]++'
280
+ )
281
+
282
+ [[ ${#RAW[@]} -eq 0 ]] && { warn "No networks found."; exit 0; }
283
+
284
+ echo ""
285
+ hr
286
+ printf " ${B}%-4s %-28s %-8s %-10s %s${NC}\n" "#" "SSID" "Signal" "Security" ""
287
+ hr
288
+ declare -a SSIDS
289
+ for i in "${!RAW[@]}"; do
290
+ IFS=':' read -r SSID SIGNAL SECURITY INUSE <<< "${RAW[$i]}"
291
+ SSIDS[$i]="$SSID"
292
+ SIG="${SIGNAL:-0}"
293
+ if [[ $SIG -ge 80 ]]; then BAR="${G}▂▄▆█${NC}"
294
+ elif [[ $SIG -ge 60 ]]; then BAR="${G}▂▄▆ ${NC}"
295
+ elif [[ $SIG -ge 40 ]]; then BAR="${Y}▂▄ ${NC}"
296
+ else BAR="${R}▂ ${NC}"; fi
297
+ ACTIVE=""
298
+ [[ "$INUSE" == "*" ]] && ACTIVE="${G} ← connected${NC}"
299
+ printf " %-4s %-28s %b %3s%% %-10s%b\n" \
300
+ "$((i+1)))" "$SSID" "$BAR" "$SIG" "${SECURITY:---}" "$ACTIVE"
301
+ done
302
+ hr
303
+ printf " %-4s %s\n" "0)" "Cancel"
304
+ echo ""
305
+
306
+ while true; do
307
+ read -r -p " Choose network [1-${#RAW[@]}] or 0 to cancel: " CHOICE </dev/tty
308
+ [[ "$CHOICE" =~ ^[0-9]+$ ]] && \
309
+ [[ "$CHOICE" -ge 0 && "$CHOICE" -le "${#RAW[@]}" ]] && break
310
+ echo -e " ${R}Enter a number 0–${#RAW[@]}${NC}"
311
+ done
312
+ [[ "$CHOICE" -eq 0 ]] && exit 0
313
+
314
+ SEL="${SSIDS[$((CHOICE-1))]}"
315
+ echo ""
316
+ read -r -s -p " Password for '${SEL}' (blank if open): " WIFI_PASS </dev/tty
317
+ echo ""; echo ""
318
+
319
+ need_sudo
320
+ sudo nmcli connection delete "$SEL" 2>/dev/null || true
321
+ if [[ -n "$WIFI_PASS" ]]; then
322
+ sudo nmcli connection add type wifi ifname wlan0 con-name "$SEL" \
323
+ ssid "$SEL" wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$WIFI_PASS" \
324
+ connection.autoconnect yes connection.autoconnect-priority 200 \
325
+ 2>/dev/null
326
+ else
327
+ sudo nmcli connection add type wifi ifname wlan0 con-name "$SEL" \
328
+ ssid "$SEL" connection.autoconnect yes \
329
+ connection.autoconnect-priority 200 2>/dev/null
330
+ fi
331
+
332
+ spin_start "Connecting to '${SEL}'"
333
+ if sudo nmcli connection up "$SEL" 2>/dev/null; then
334
+ sleep 2
335
+ NEW_IP=$(ip -4 addr show wlan0 2>/dev/null | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "")
336
+ spin_ok "Connected"
337
+ [[ -n "$NEW_IP" ]] && ok "Pi IP: ${NEW_IP} → http://${NEW_IP}:8080"
338
+
339
+ echo ""
340
+ echo -e " ${Y}Network changed — SMB delivery target may need updating.${NC}"
341
+ read -r -p " Update SMB host IP? [Y/n]: " CONFIRM </dev/tty
342
+ CONFIRM="${CONFIRM:-Y}"
343
+ [[ "${CONFIRM^^}" == "Y" ]] && \
344
+ prompt_smb_config "Network changed to '${SEL}' (Pi IP: ${NEW_IP:-unknown})"
345
+ else
346
+ spin_warn "Could not connect — check password and try again"
347
+ fi
348
+ ;;
349
+
350
+ *)
351
+ echo "Usage: plc_checkweigher wifi [scan]"
352
+ ;;
353
+ esac
354
+ ;;
355
+
356
+ # ─────────────────────────────────────────────────────────────────────────────
357
+ # HOTSPOT — AP mode for direct PC connection + SMB push
358
+ # ─────────────────────────────────────────────────────────────────────────────
359
+ hotspot)
360
+ SUBCMD="${1:-status}"
361
+ shift || true
362
+ HOTSPOT_SSID="${HOTSPOT_SSID:-PLC-Reports}"
363
+ HOTSPOT_PASS="${HOTSPOT_PASS:-plcreport}"
364
+ HOTSPOT_CON="plc-hotspot"
365
+
366
+ case "$SUBCMD" in
367
+ on)
368
+ banner "Enable Hotspot"
369
+ need_sudo
370
+ echo ""
371
+ [[ -n "${1:-}" ]] && HOTSPOT_SSID="$1"
372
+ [[ -n "${2:-}" ]] && HOTSPOT_PASS="$2"
373
+
374
+ sudo nmcli connection delete "$HOTSPOT_CON" 2>/dev/null || true
375
+
376
+ spin_start "Starting hotspot '${HOTSPOT_SSID}'"
377
+ sudo nmcli device wifi hotspot \
378
+ ifname wlan0 con-name "$HOTSPOT_CON" \
379
+ ssid "$HOTSPOT_SSID" password "$HOTSPOT_PASS" &>/dev/null || true
380
+ sleep 2
381
+ spin_ok "Hotspot active"
382
+
383
+ HOTSPOT_IP=$(ip -4 addr show wlan0 2>/dev/null \
384
+ | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "10.42.0.1")
385
+
386
+ echo ""
387
+ echo -e " ${W}┌────────────────────────────────────────────────┐${NC}"
388
+ echo -e " ${W}│ Connect your PC to this WiFi: │${NC}"
389
+ echo -e " ${W}│ │${NC}"
390
+ printf " ${W}│ %-14s ${G}%-30s${W} │${NC}\n" "SSID:" "$HOTSPOT_SSID"
391
+ printf " ${W}│ %-14s ${G}%-30s${W} │${NC}\n" "Password:" "$HOTSPOT_PASS"
392
+ printf " ${W}│ %-14s ${C}%-30s${W} │${NC}\n" "Pi IP:" "$HOTSPOT_IP"
393
+ echo -e " ${W}└────────────────────────────────────────────────┘${NC}"
394
+ echo ""
395
+ info "After PC connects, run: plc_checkweigher hotspot scan"
396
+ info "Web UI still accessible at: http://${HOTSPOT_IP}:8080"
397
+ echo ""
398
+ ;;
399
+
400
+ off)
401
+ banner "Disable Hotspot"
402
+ need_sudo
403
+ echo ""
404
+ spin_start "Stopping hotspot"
405
+ sudo nmcli connection delete "$HOTSPOT_CON" 2>/dev/null \
406
+ && spin_ok \
407
+ || spin_warn "No active hotspot found"
408
+ echo ""
409
+ info "To reconnect to WiFi: plc_checkweigher wifi"
410
+ echo ""
411
+ ;;
412
+
413
+ status)
414
+ banner "Hotspot Status"
415
+ echo ""
416
+ if nmcli connection show --active 2>/dev/null | grep -q "$HOTSPOT_CON"; then
417
+ HOTSPOT_IP=$(ip -4 addr show wlan0 2>/dev/null \
418
+ | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "?")
419
+ ok "Hotspot is ON"
420
+ info "SSID: ${HOTSPOT_SSID} | Pi IP: ${HOTSPOT_IP}"
421
+ info "Run: plc_checkweigher hotspot scan — to find connected PCs"
422
+ else
423
+ info "Hotspot is OFF"
424
+ info "Run: plc_checkweigher hotspot on — to start"
425
+ fi
426
+ echo ""
427
+ ;;
428
+
429
+ scan)
430
+ banner "Scanning for connected devices"
431
+ echo ""
432
+
433
+ HOTSPOT_IP=$(ip -4 addr show wlan0 2>/dev/null \
434
+ | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "")
435
+ if [[ -z "$HOTSPOT_IP" ]]; then
436
+ err "Hotspot not active — run: plc_checkweigher hotspot on"
437
+ exit 1
438
+ fi
439
+
440
+ SUBNET="${HOTSPOT_IP%.*}.0/24"
441
+ info "Sweeping ${SUBNET} ..."
442
+ echo ""
443
+
444
+ # Parallel ping sweep with live counter
445
+ DONE=0
446
+ for i in $(seq 1 254); do
447
+ ping -c 1 -W 1 "${HOTSPOT_IP%.*}.${i}" &>/dev/null &
448
+ DONE=$((DONE + 1))
449
+ if [[ $_TTY -eq 1 && $((DONE % 16)) -eq 0 ]]; then
450
+ printf "\r ${C}⠹${NC} Pinging hosts ... ${D}%d / 254${NC} " "$DONE"
451
+ fi
452
+ done
453
+ wait
454
+ [[ $_TTY -eq 1 ]] && printf '\r\033[K'
455
+ sleep 1
456
+ ok "Subnet sweep complete"
457
+
458
+ mapfile -t CLIENTS < <(
459
+ ip neigh show 2>/dev/null \
460
+ | grep "REACHABLE\|STALE\|DELAY" \
461
+ | grep "${HOTSPOT_IP%.*}\." \
462
+ | grep -v "^${HOTSPOT_IP}" \
463
+ | awk '{print $1}' \
464
+ | sort -t. -k4 -n
465
+ )
466
+
467
+ if [[ ${#CLIENTS[@]} -eq 0 ]]; then
468
+ echo ""
469
+ warn "No devices found — make sure the PC is connected to '${HOTSPOT_SSID}'"
470
+ info "Try again: plc_checkweigher hotspot scan"
471
+ echo ""; exit 0
472
+ fi
473
+
474
+ echo ""
475
+ hr
476
+ printf " ${B}%-4s %-18s %s${NC}\n" "#" "IP Address" "Hostname"
477
+ hr
478
+ declare -a CLIENT_IPS
479
+ for i in "${!CLIENTS[@]}"; do
480
+ IP="${CLIENTS[$i]}"
481
+ CLIENT_IPS[$i]="$IP"
482
+ HOST=$(host "$IP" 2>/dev/null | grep "domain name pointer" \
483
+ | awk '{print $NF}' | sed 's/\.$//' || echo "")
484
+ printf " %-4s %-18s %s\n" "$((i+1)))" "$IP" "${HOST:-(unknown)}"
485
+ done
486
+ hr
487
+ printf " %-4s %s\n" "0)" "Cancel"
488
+ echo ""
489
+
490
+ while true; do
491
+ read -r -p " Set device as SMB target [1-${#CLIENTS[@]}] or 0 to cancel: " CHOICE </dev/tty
492
+ [[ "$CHOICE" =~ ^[0-9]+$ ]] && \
493
+ [[ "$CHOICE" -ge 0 && "$CHOICE" -le "${#CLIENTS[@]}" ]] && break
494
+ done
495
+ [[ "$CHOICE" -eq 0 ]] && exit 0
496
+
497
+ TARGET_IP="${CLIENT_IPS[$((CHOICE-1))]}"
498
+ echo ""
499
+ info "Selected: ${TARGET_IP}"
500
+ prompt_smb_config "Setting SMB target to hotspot client ${TARGET_IP}"
501
+ ;;
502
+
503
+ *)
504
+ echo "Usage: plc_checkweigher hotspot [on|off|status|scan]"
505
+ echo " on [SSID] [PASSWORD] — start hotspot (default: PLC-Reports / plcreport)"
506
+ echo " off — stop hotspot"
507
+ echo " status — show hotspot state"
508
+ echo " scan — find connected PCs and set as SMB target"
509
+ ;;
510
+ esac
511
+ ;;
512
+
513
+ # ─────────────────────────────────────────────────────────────────────────────
514
+ # DISPLAY — enable / disable LightDM
515
+ # ─────────────────────────────────────────────────────────────────────────────
516
+ display)
517
+ SUBCMD="${1:-status}"
518
+ shift || true
519
+
520
+ case "$SUBCMD" in
521
+ on)
522
+ banner "Enable Display"
523
+ need_sudo
524
+ echo ""
525
+ spin_start "Starting LightDM"
526
+ if sudo systemctl start lightdm 2>/dev/null; then
527
+ spin_ok "Display enabled"
528
+ else
529
+ spin_warn "LightDM failed to start (may not be installed)"
530
+ fi
531
+ sudo systemctl enable lightdm 2>/dev/null && ok "Auto-start enabled" || true
532
+ echo ""
533
+ ;;
534
+ off)
535
+ banner "Disable Display"
536
+ need_sudo
537
+ echo ""
538
+ spin_start "Stopping LightDM"
539
+ if sudo systemctl stop lightdm 2>/dev/null; then
540
+ spin_ok "Display disabled"
541
+ else
542
+ spin_warn "LightDM was not running"
543
+ fi
544
+ read -r -p " Disable auto-start on boot too? [y/N]: " DIS </dev/tty
545
+ DIS="${DIS:-N}"
546
+ if [[ "${DIS^^}" == "Y" ]]; then
547
+ sudo systemctl disable lightdm 2>/dev/null && ok "Auto-start disabled" || true
548
+ fi
549
+ echo ""
550
+ ;;
551
+ status)
552
+ banner "Display Status"
553
+ echo ""
554
+ ACTIVE=$(systemctl is-active lightdm 2>/dev/null || true)
555
+ ENABLED=$(systemctl is-enabled lightdm 2>/dev/null || true)
556
+ if [[ "$ACTIVE" == "active" ]]; then
557
+ ok "LightDM: RUNNING (auto-start: ${ENABLED})"
558
+ else
559
+ info "LightDM: ${ACTIVE} (auto-start: ${ENABLED})"
560
+ fi
561
+ if command -v tvservice &>/dev/null; then
562
+ info "HDMI: $(tvservice -s 2>/dev/null | head -1)"
563
+ fi
564
+ echo ""
565
+ ;;
566
+ *)
567
+ echo "Usage: plc_checkweigher display [on|off|status]"
568
+ ;;
569
+ esac
570
+ ;;
571
+
572
+ # ─────────────────────────────────────────────────────────────────────────────
573
+ # SMB CONFIG — interactive update of delivery target
574
+ # ─────────────────────────────────────────────────────────────────────────────
575
+ smb-config)
576
+ prompt_smb_config
577
+ ;;
578
+
579
+ # ─────────────────────────────────────────────────────────────────────────────
580
+ # UNINSTALL — remove everything setup.sh installed
581
+ # ─────────────────────────────────────────────────────────────────────────────
582
+ uninstall)
583
+ PI_USER="${PI_USER:-pi}"
584
+ HOME_DIR="/home/${PI_USER}"
585
+ VENV_DIR="${HOME_DIR}/plc_env"
586
+ REPORTS_DIR="${HOME_DIR}/reports"
587
+ BOOT_FW="/boot/firmware"
588
+
589
+ echo ""
590
+ echo -e "${R} ╔══════════════════════════════════════════════════════════╗${NC}"
591
+ echo -e "${R} ║ PLC CHECK-WEIGHER UNINSTALLER ║${NC}"
592
+ echo -e "${R} ╚══════════════════════════════════════════════════════════╝${NC}"
593
+ echo ""
594
+ echo -e " This will permanently remove:"
595
+ 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)"
606
+ echo ""
607
+ echo -e " ${D}System packages (git, python3-venv, samba-client) are NOT removed.${NC}"
608
+ echo ""
609
+ hr
610
+ echo ""
611
+
612
+ read -r -p " Type YES to confirm full uninstall: " CONFIRM </dev/tty
613
+ [[ "$CONFIRM" == "YES" ]] || { echo " Aborted."; exit 0; }
614
+
615
+ echo ""
616
+ read -r -p " Keep report PDFs in ${REPORTS_DIR}? [Y/n]: " KEEP_REPORTS </dev/tty
617
+ KEEP_REPORTS="${KEEP_REPORTS:-Y}"
618
+
619
+ echo ""
620
+ need_sudo
621
+
622
+ # Step counter
623
+ _US=0; _UT=9
624
+ ustep() { _US=$((_US + 1)); spin_start "[${_US}/${_UT}] $*"; }
625
+
626
+ # ── 1. Stop and disable services ─────────────────────────────────────────
627
+ echo ""
628
+ ustep "Stopping and disabling services"
629
+ 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
634
+ done
635
+ sudo rm -f /etc/systemd/system/plc_watcher.service \
636
+ /etc/systemd/system/plc_web.service
637
+ spin_ok "Services removed"
638
+
639
+ # ── 2. System drop-ins ───────────────────────────────────────────────────
640
+ ustep "Removing system drop-ins"
641
+ sudo rm -f /etc/systemd/system/lightdm.service.d/display-priority.conf
642
+ sudo rm -f /etc/systemd/system/NetworkManager-wait-online.service.d/timeout.conf
643
+ sudo rm -f /etc/tmpfiles.d/utmp-fix.conf
644
+ sudo systemctl reenable lightdm 2>/dev/null || true
645
+ spin_ok
646
+
647
+ # ── 3. Plymouth theme ────────────────────────────────────────────────────
648
+ ustep "Removing Plymouth theme"
649
+ THEME_DIR="/usr/share/plymouth/themes/saismruth"
650
+ [[ -d "$THEME_DIR" ]] && sudo rm -rf "$THEME_DIR"
651
+ DEFAULT_THEME=$(sudo plymouth-set-default-theme --list 2>/dev/null \
652
+ | grep -E "^pix$|^bgrt$|^spinner$" | head -1 || echo "")
653
+ if [[ -n "$DEFAULT_THEME" ]]; then
654
+ sudo plymouth-set-default-theme "$DEFAULT_THEME" 2>/dev/null || true
655
+ else
656
+ sudo plymouth-set-default-theme --reset 2>/dev/null || true
657
+ fi
658
+ spin_ok "Reverted to '${DEFAULT_THEME:-default}'"
659
+
660
+ # ── 4. Rebuild initramfs (slowest step — keep spinner alive) ─────────────
661
+ ustep "Rebuilding initramfs ${D}(~30 s)${NC}"
662
+ sudo update-initramfs -u > /tmp/uninstall_initramfs.log 2>&1 \
663
+ && spin_ok \
664
+ || spin_warn "Warnings — see /tmp/uninstall_initramfs.log"
665
+
666
+ # ── 5. RT kernel revert ──────────────────────────────────────────────────
667
+ ustep "Reverting RT kernel config"
668
+ 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"
671
+ 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"
676
+ else
677
+ spin_warn "config.txt not found — skipped"
678
+ fi
679
+
680
+ # ── 6. Hotspot + nmcli cleanup ───────────────────────────────────────────
681
+ ustep "Cleaning up network connections"
682
+ sudo nmcli connection delete "plc-hotspot" 2>/dev/null || true
683
+ sudo systemctl daemon-reload
684
+ spin_ok
685
+
686
+ # ── 7. Python venv ───────────────────────────────────────────────────────
687
+ 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
694
+
695
+ # ── 8. Reports (optional) + temp files ───────────────────────────────────
696
+ ustep "Cleaning up runtime files"
697
+ rm -f /tmp/plc_live.json 2>/dev/null || true
698
+ BASHRC="${HOME_DIR}/.bashrc"
699
+ [[ -f "$BASHRC" ]] && sed -i '/export PATH.*\.local\/bin/d' "$BASHRC" 2>/dev/null || true
700
+ sudo rm -f /usr/local/bin/plc_checkweigher
701
+ rm -f "${HOME_DIR}/.local/bin/plc_checkweigher" 2>/dev/null || true
702
+ if [[ "${KEEP_REPORTS^^}" != "Y" && -d "$REPORTS_DIR" ]]; then
703
+ rm -rf "$REPORTS_DIR"
704
+ fi
705
+ spin_ok "CLI and temp files removed"
706
+
707
+ # ── 9. Remove project directory (self-deletes last) ───────────────────────
708
+ ustep "Removing project code"
709
+ if [[ -d "$INSTALL_DIR" ]]; then
710
+ rm -rf "$INSTALL_DIR"
711
+ spin_ok "Removed ${INSTALL_DIR}"
712
+ else
713
+ spin_ok "Already gone"
714
+ fi
715
+
716
+ # ── Done ─────────────────────────────────────────────────────────────────
717
+ echo ""
718
+ echo -e "${G}"
719
+ echo " ╔══════════════════════════════════════════════════════════╗"
720
+ echo " ║ Uninstall complete. ║"
721
+ echo " ║ ║"
722
+ echo " ║ A reboot is needed to apply kernel revert. ║"
723
+ echo " ╚══════════════════════════════════════════════════════════╝"
724
+ echo -e "${NC}"
725
+ [[ "${KEEP_REPORTS^^}" == "Y" ]] && info "PDFs still at: ${REPORTS_DIR} (remove manually if needed)"
726
+ echo ""
727
+
728
+ read -r -p " Reboot now? [Y/n]: " DO_REBOOT </dev/tty
729
+ DO_REBOOT="${DO_REBOOT:-Y}"
730
+ if [[ "${DO_REBOOT^^}" == "Y" ]]; then
731
+ spin_start "Rebooting"
732
+ sleep 1
733
+ sudo reboot
734
+ else
735
+ warn "Remember to reboot for kernel changes to take effect."
736
+ echo ""
737
+ fi
738
+ ;;
739
+
740
+ # ─────────────────────────────────────────────────────────────────────────────
741
+ # HELP
742
+ # ─────────────────────────────────────────────────────────────────────────────
743
+ help|--help|-h)
744
+ echo ""
745
+ echo -e "${B} plc_checkweigher${NC} — PLC Check-Weigher system CLI"
746
+ echo ""
747
+ echo -e " ${W}Diagnostics${NC}"
748
+ echo " status Full system diagnostic — all checks + fix hints"
749
+ echo " logs Stream live logs (plc_watcher + plc_web)"
750
+ echo " queue Show SMB pending queue and delivery ledger"
751
+ echo ""
752
+ echo -e " ${W}Services${NC}"
753
+ echo " start Start plc_watcher and plc_web"
754
+ echo " stop Stop both services"
755
+ echo " restart Restart both services"
756
+ echo ""
757
+ echo -e " ${W}Network${NC}"
758
+ echo " wifi Scan and switch WiFi network"
759
+ echo " hotspot on [SSID] [PASS] Start WiFi hotspot (direct PC connection)"
760
+ echo " hotspot off Stop hotspot"
761
+ echo " hotspot status Show hotspot state"
762
+ echo " hotspot scan Detect connected PCs → set as SMB target"
763
+ echo ""
764
+ echo -e " ${W}Configuration${NC}"
765
+ echo " smb-config Interactively update SMB delivery target"
766
+ echo " push-test Push latest PDF to SMB target now"
767
+ echo ""
768
+ echo -e " ${W}Display${NC}"
769
+ echo " display on Enable display (start LightDM)"
770
+ echo " display off Disable display (stop LightDM)"
771
+ echo " display status Show display state"
772
+ echo ""
773
+ echo -e " ${R} uninstall${NC} Remove everything setup.sh installed"
774
+ echo ""
775
+ ;;
776
+
777
+ *)
778
+ err "Unknown command: $CMD"
779
+ echo " Run: plc_checkweigher help"
780
+ exit 1
781
+ ;;
782
+
783
+ esac
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.6.0",
3
+ "version": "1.11.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
  "bin": {
6
6
  "plc-checkweigher": "bin/cli.js"
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "bin/",
10
10
  "setup.sh",
11
+ "uninstall.sh",
11
12
  "assets/"
12
13
  ],
13
14
  "keywords": [
package/setup.sh CHANGED
@@ -136,6 +136,21 @@ setup_dirs() {
136
136
  ok "${REPORTS_DIR}"
137
137
  }
138
138
 
139
+ # ── CLI tool — install plc_checkweigher command ───────────────────────────────
140
+ install_cli() {
141
+ step "CLI tool ..."
142
+ CLI_SRC="${INSTALL_DIR}/bin/plc_checkweigher"
143
+ CLI_DEST="/usr/local/bin/plc_checkweigher"
144
+ if [[ -f "${CLI_SRC}" ]]; then
145
+ chmod +x "${CLI_SRC}"
146
+ cp "${CLI_SRC}" "${CLI_DEST}"
147
+ ok "plc_checkweigher → ${CLI_DEST}"
148
+ ok "Run: plc_checkweigher status (full system diagnostic)"
149
+ else
150
+ warn "bin/plc_checkweigher not found — skipping CLI install"
151
+ fi
152
+ }
153
+
139
154
  # ── 5. WiFi — scan → pick → password ─────────────────────────────────────────
140
155
  setup_wifi() {
141
156
  step "WiFi Setup"
@@ -601,10 +616,11 @@ main() {
601
616
  setup_repo # 2
602
617
  setup_venv # 3
603
618
  setup_dirs # 4
604
- setup_wifi # 5 — interactive WiFi picker
605
- setup_smb # 6 — interactive SMB config → smb_config.py
606
- setup_network_online # 7
607
- install_services # 8
619
+ install_cli # 5 — plc_checkweigher status command
620
+ setup_wifi # 6 — interactive WiFi picker
621
+ setup_smb # 7 — interactive SMB config → smb_config.py
622
+ setup_network_online # 8
623
+ install_services # 9
608
624
  setup_boot_logo # 9 — Plymouth: logo + "SAI SAMARTH ENGG"
609
625
  setup_display # 10 — LightDM priority, CPU isolation, utmpx, GPU
610
626
  install_rt_kernel # 11 — LAST, so only one reboot needed
package/uninstall.sh ADDED
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # PLC Check-Weigher — Full Uninstaller
4
+ # =============================================================================
5
+ # Run directly: sudo bash uninstall.sh
6
+ # Via npx: npx plc-checkweigher -ex
7
+ # =============================================================================
8
+
9
+ set -euo pipefail
10
+
11
+ PI_USER="${PI_USER:-pi}"
12
+ HOME_DIR="/home/${PI_USER}"
13
+ INSTALL_DIR="${HOME_DIR}/plc_checkweigher"
14
+ VENV_DIR="${HOME_DIR}/plc_env"
15
+ REPORTS_DIR="${HOME_DIR}/reports"
16
+ BOOT_FW="/boot/firmware"
17
+
18
+ B='\033[1;34m'; G='\033[0;32m'; R='\033[1;31m'; Y='\033[1;33m'
19
+ C='\033[0;36m'; D='\033[2m'; NC='\033[0m'
20
+
21
+ ok() { echo -e " ${G}✓${NC} $*"; }
22
+ warn() { echo -e " ${Y}!${NC} $*"; }
23
+ info() { echo -e " ${C}i${NC} $*"; }
24
+ hr() { echo -e " ${D}$(printf '─%.0s' {1..56})${NC}"; }
25
+ banner() {
26
+ echo ""
27
+ echo -e "${B} ▸ $*${NC}"
28
+ }
29
+
30
+ # ── Spinner ───────────────────────────────────────────────────────────────────
31
+ _SP_PID=""; _SP_MSG=""
32
+ _SP_FRAMES=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
33
+ _TTY=0; [[ -t 1 ]] && _TTY=1
34
+
35
+ _spin_kill() {
36
+ if [[ -n "$_SP_PID" ]]; then
37
+ kill "$_SP_PID" 2>/dev/null || true
38
+ wait "$_SP_PID" 2>/dev/null || true
39
+ _SP_PID=""
40
+ fi
41
+ if [[ $_TTY -eq 1 ]]; then printf '\r\033[K'; fi
42
+ return 0
43
+ }
44
+ trap '_spin_kill' EXIT INT TERM
45
+
46
+ spin_start() {
47
+ _SP_MSG="${1:-}"
48
+ [[ $_TTY -eq 0 ]] && { echo -e " ${C}…${NC} ${_SP_MSG}"; return; }
49
+ _spin_kill
50
+ ( local i=0
51
+ while true; do
52
+ printf "\r ${C}%s${NC} %s " "${_SP_FRAMES[$((i % 10))]}" "${_SP_MSG}"
53
+ sleep 0.08; i=$((i+1))
54
+ done ) &
55
+ _SP_PID=$!
56
+ }
57
+ spin_ok() { local m="${_SP_MSG}"; _spin_kill; ok "${m}${1:+ ${D}$1${NC}}"; return 0; }
58
+ spin_warn() { local m="${_SP_MSG}"; _spin_kill; warn "${m}${1:+ ${D}$1${NC}}"; return 0; }
59
+
60
+ # ─────────────────────────────────────────────────────────────────────────────
61
+ [[ "${EUID}" -eq 0 ]] || { echo -e "${R}Run as root:${NC} sudo bash uninstall.sh"; exit 1; }
62
+
63
+ echo ""
64
+ echo -e "${R} ╔══════════════════════════════════════════════════════════╗${NC}"
65
+ echo -e "${R} ║ PLC CHECK-WEIGHER UNINSTALLER ║${NC}"
66
+ echo -e "${R} ╚══════════════════════════════════════════════════════════╝${NC}"
67
+ echo ""
68
+ echo -e " This will permanently remove:"
69
+ echo ""
70
+ echo -e " ${R}✗${NC} systemd services plc_watcher plc_web"
71
+ echo -e " ${R}✗${NC} project code ${INSTALL_DIR}"
72
+ echo -e " ${R}✗${NC} Python venv ${VENV_DIR}"
73
+ echo -e " ${R}✗${NC} CLI tool /usr/local/bin/plc_checkweigher"
74
+ echo -e " ${R}✗${NC} Plymouth theme saismruth (reverts to default)"
75
+ echo -e " ${R}✗${NC} RT kernel config (reverts /boot/firmware/config.txt)"
76
+ echo -e " ${R}✗${NC} LightDM drop-in /etc/systemd/system/lightdm.service.d/"
77
+ echo -e " ${R}✗${NC} NetworkManager cfg /etc/systemd/system/NetworkManager-wait-online.service.d/"
78
+ echo -e " ${R}✗${NC} Hotspot connection plc-hotspot (nmcli)"
79
+ echo -e " ${Y}!${NC} reports folder ${REPORTS_DIR} (you will be asked)"
80
+ echo ""
81
+ echo -e " ${D}System packages (git, python3-venv, samba-client) are NOT removed.${NC}"
82
+ echo ""
83
+ hr
84
+ echo ""
85
+
86
+ read -r -p " Type YES to confirm full uninstall: " CONFIRM </dev/tty
87
+ [[ "$CONFIRM" == "YES" ]] || { echo " Aborted."; exit 0; }
88
+
89
+ echo ""
90
+ read -r -p " Keep report PDFs in ${REPORTS_DIR}? [Y/n]: " KEEP_REPORTS </dev/tty
91
+ KEEP_REPORTS="${KEEP_REPORTS:-Y}"
92
+ echo ""
93
+
94
+ _US=0; _UT=9
95
+ ustep() { _US=$((_US + 1)); spin_start "[${_US}/${_UT}] $*"; }
96
+
97
+ # ── 1. Stop and disable services ─────────────────────────────────────────────
98
+ ustep "Stopping and disabling services"
99
+ for SVC in plc_watcher plc_web; do
100
+ systemctl is-active --quiet "$SVC" 2>/dev/null && systemctl stop "$SVC" 2>/dev/null || true
101
+ systemctl is-enabled --quiet "$SVC" 2>/dev/null && systemctl disable "$SVC" 2>/dev/null || true
102
+ done
103
+ rm -f /etc/systemd/system/plc_watcher.service \
104
+ /etc/systemd/system/plc_web.service
105
+ spin_ok "Services removed"
106
+
107
+ # ── 2. System drop-ins ────────────────────────────────────────────────────────
108
+ ustep "Removing system drop-ins"
109
+ rm -f /etc/systemd/system/lightdm.service.d/display-priority.conf
110
+ rm -f /etc/systemd/system/NetworkManager-wait-online.service.d/timeout.conf
111
+ rm -f /etc/tmpfiles.d/utmp-fix.conf
112
+ systemctl reenable lightdm 2>/dev/null || true
113
+ spin_ok
114
+
115
+ # ── 3. Plymouth theme ────────────────────────────────────────────────────────
116
+ ustep "Removing Plymouth theme"
117
+ THEME_DIR="/usr/share/plymouth/themes/saismruth"
118
+ [[ -d "$THEME_DIR" ]] && rm -rf "$THEME_DIR"
119
+ DEFAULT_THEME=$(plymouth-set-default-theme --list 2>/dev/null \
120
+ | grep -E "^pix$|^bgrt$|^spinner$" | head -1 || echo "")
121
+ if [[ -n "$DEFAULT_THEME" ]]; then
122
+ plymouth-set-default-theme "$DEFAULT_THEME" 2>/dev/null || true
123
+ else
124
+ plymouth-set-default-theme --reset 2>/dev/null || true
125
+ fi
126
+ spin_ok "Reverted to '${DEFAULT_THEME:-default}'"
127
+
128
+ # ── 4. Rebuild initramfs ──────────────────────────────────────────────────────
129
+ ustep "Rebuilding initramfs (${D}~30 s${NC})"
130
+ update-initramfs -u > /tmp/uninstall_initramfs.log 2>&1 \
131
+ && spin_ok \
132
+ || spin_warn "Warnings — see /tmp/uninstall_initramfs.log"
133
+
134
+ # ── 5. RT kernel revert ───────────────────────────────────────────────────────
135
+ ustep "Reverting RT kernel config"
136
+ if [[ -f "${BOOT_FW}/config.txt" ]]; then
137
+ sed -i '/### PLC-RT-BLOCK-START ###/,/### PLC-RT-BLOCK-END ###/d' "${BOOT_FW}/config.txt"
138
+ sed -i '/^gpu_mem=128$/d' "${BOOT_FW}/config.txt"
139
+ rm -f "${BOOT_FW}/kernel8-rt.img" \
140
+ "${BOOT_FW}/initramfs8-rt" \
141
+ "${BOOT_FW}/kernel8-stock.img"
142
+ spin_ok "Stock kernel will boot after reboot"
143
+ else
144
+ spin_warn "config.txt not found — skipped"
145
+ fi
146
+
147
+ # ── 6. Network cleanup ────────────────────────────────────────────────────────
148
+ ustep "Cleaning up network connections"
149
+ nmcli connection delete "plc-hotspot" 2>/dev/null || true
150
+ systemctl daemon-reload
151
+ spin_ok
152
+
153
+ # ── 7. Python venv ────────────────────────────────────────────────────────────
154
+ ustep "Removing Python environment"
155
+ if [[ -d "$VENV_DIR" ]]; then
156
+ rm -rf "$VENV_DIR"
157
+ spin_ok "Removed ${VENV_DIR}"
158
+ else
159
+ spin_ok "Already gone"
160
+ fi
161
+
162
+ # ── 8. Runtime files + CLI ────────────────────────────────────────────────────
163
+ ustep "Cleaning up runtime files and CLI"
164
+ rm -f /tmp/plc_live.json 2>/dev/null || true
165
+ BASHRC="${HOME_DIR}/.bashrc"
166
+ [[ -f "$BASHRC" ]] && sed -i '/export PATH.*\.local\/bin/d' "$BASHRC" 2>/dev/null || true
167
+ rm -f /usr/local/bin/plc_checkweigher
168
+ rm -f "${HOME_DIR}/.local/bin/plc_checkweigher" 2>/dev/null || true
169
+ if [[ "${KEEP_REPORTS^^}" != "Y" && -d "$REPORTS_DIR" ]]; then
170
+ rm -rf "$REPORTS_DIR"
171
+ fi
172
+ spin_ok "CLI and temp files removed"
173
+
174
+ # ── 9. Project code ───────────────────────────────────────────────────────────
175
+ ustep "Removing project code"
176
+ if [[ -d "$INSTALL_DIR" ]]; then
177
+ rm -rf "$INSTALL_DIR"
178
+ spin_ok "Removed ${INSTALL_DIR}"
179
+ else
180
+ spin_ok "Already gone"
181
+ fi
182
+
183
+ # ── Done ──────────────────────────────────────────────────────────────────────
184
+ echo ""
185
+ echo -e "${G}"
186
+ echo " ╔══════════════════════════════════════════════════════════╗"
187
+ echo " ║ Uninstall complete. ║"
188
+ echo " ║ ║"
189
+ echo " ║ A reboot is needed to apply kernel revert. ║"
190
+ echo " ╚══════════════════════════════════════════════════════════╝"
191
+ echo -e "${NC}"
192
+ [[ "${KEEP_REPORTS^^}" == "Y" ]] && info "PDFs still at: ${REPORTS_DIR} (remove manually if needed)"
193
+ echo ""
194
+
195
+ read -r -p " Reboot now? [Y/n]: " DO_REBOOT </dev/tty
196
+ DO_REBOOT="${DO_REBOOT:-Y}"
197
+ if [[ "${DO_REBOOT^^}" == "Y" ]]; then
198
+ echo ""; info "Rebooting ..."
199
+ reboot
200
+ else
201
+ warn "Remember to reboot for kernel changes to take effect."
202
+ echo ""
203
+ fi