plc-checkweigher 1.7.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
+ }
@@ -1,17 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # plc_checkweigher — CLI for the PLC Check-Weigher system
3
3
  # Installed to /usr/local/bin/ by setup.sh
4
- #
5
- # Commands:
6
- # status Full system diagnostic
7
- # logs Live journalctl stream
8
- # restart / start / stop Service control
9
- # push-test Push latest PDF to SMB target
10
- # queue Show SMB delivery queue + ledger
11
- # wifi Scan and switch WiFi network (prompts SMB IP update)
12
- # hotspot on/off/status/scan WiFi hotspot (AP mode) — direct PC connection
13
- # display on/off/status Enable/disable the display (LightDM)
14
- # smb-config Interactively update SMB delivery target
15
4
 
16
5
  set -euo pipefail
17
6
 
@@ -19,6 +8,9 @@ INSTALL_DIR="/home/pi/plc_checkweigher"
19
8
  PYTHON="/home/pi/plc_env/bin/python3"
20
9
  SMB_CFG="${INSTALL_DIR}/smb_config.py"
21
10
 
11
+ # ── TTY detection — animations only when connected to a real terminal ─────────
12
+ [[ -t 1 ]] && _TTY=1 || _TTY=0
13
+
22
14
  # ── Colours ───────────────────────────────────────────────────────────────────
23
15
  B='\033[1;34m'; G='\033[0;32m'; R='\033[1;31m'; Y='\033[1;33m'
24
16
  C='\033[0;36m'; D='\033[2m'; W='\033[1m'; NC='\033[0m'
@@ -28,33 +20,72 @@ err() { echo -e " ${R}✗${NC} $*" >&2; }
28
20
  warn() { echo -e " ${Y}!${NC} $*"; }
29
21
  info() { echo -e " ${C}i${NC} $*"; }
30
22
  hr() { echo -e " ${D}$(printf '─%.0s' {1..56})${NC}"; }
31
- banner() { echo -e "\n${B} ▸ $*${NC}"; }
32
23
 
33
- need_sudo() {
34
- if [[ $EUID -ne 0 ]]; then
35
- if ! sudo -n true 2>/dev/null; then
36
- echo -e " ${Y}sudo password required${NC}"
37
- fi
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}"
38
37
  fi
39
38
  }
40
39
 
41
- # ── Read current SMB config ───────────────────────────────────────────────────
42
- smb_get() {
43
- local key="$1"
44
- grep "^${key}" "${SMB_CFG}" 2>/dev/null \
45
- | head -1 | sed 's/.*= *"//;s/".*//'
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=$!
46
69
  }
47
70
 
48
- smb_set() {
49
- local key="$1" val="$2"
50
- if grep -q "^${key}" "${SMB_CFG}" 2>/dev/null; then
51
- sed -i "s|^${key}.*|${key} = \"${val}\"|" "${SMB_CFG}"
52
- else
53
- echo "${key} = \"${val}\"" >> "${SMB_CFG}"
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
54
80
  fi
55
81
  }
56
82
 
57
- # ── SMB config update prompt (reused by wifi + smb-config) ───────────────────
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) ────
58
89
  prompt_smb_config() {
59
90
  local context="${1:-}"
60
91
  echo ""
@@ -62,34 +93,26 @@ prompt_smb_config() {
62
93
  [[ -n "$context" ]] && info "$context"
63
94
  echo ""
64
95
 
96
+ local CUR_HOST CUR_SHARE CUR_USER CUR_PASS
65
97
  CUR_HOST=$(smb_get "SMB_HOST")
66
98
  CUR_SHARE=$(smb_get "SMB_SHARE")
67
99
  CUR_USER=$(smb_get "SMB_USERNAME")
68
100
  CUR_PASS=$(smb_get "SMB_PASSWORD")
69
- CUR_ENABLED=$(grep "^SMB_ENABLED" "${SMB_CFG}" 2>/dev/null | grep -o "True\|False" || echo "True")
70
101
 
71
102
  hr
72
- printf " ${B}%-22s${NC} ${D}current: %s${NC}\n" "Host IP" "${CUR_HOST:-(not set)}"
73
- printf " ${B}%-22s${NC} ${D}current: %s${NC}\n" "Share name" "${CUR_SHARE:-(not set)}"
74
- printf " ${B}%-22s${NC} ${D}current: %s${NC}\n" "Username" "${CUR_USER:-(not set)}"
75
- printf " ${B}%-22s${NC} ${D}current: %s${NC}\n" "Password" "${CUR_PASS:-(not set)}"
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)}"
76
107
  hr
77
108
  echo ""
78
109
 
79
- read -r -p " Host IP [${CUR_HOST}]: " NEW_HOST </dev/tty
80
- NEW_HOST="${NEW_HOST:-$CUR_HOST}"
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}"
81
115
 
82
- read -r -p " Share name [${CUR_SHARE:-Reports}]: " NEW_SHARE </dev/tty
83
- NEW_SHARE="${NEW_SHARE:-${CUR_SHARE:-Reports}}"
84
-
85
- read -r -p " Username [${CUR_USER:-plcreport}]: " NEW_USER </dev/tty
86
- NEW_USER="${NEW_USER:-${CUR_USER:-plcreport}}"
87
-
88
- read -r -s -p " Password (leave blank to keep current): " NEW_PASS </dev/tty
89
- echo ""
90
- NEW_PASS="${NEW_PASS:-$CUR_PASS}"
91
-
92
- # Write updated config
93
116
  cat > "${SMB_CFG}" << EOF
94
117
  SMB_ENABLED = True
95
118
  SMB_HOST = "${NEW_HOST}"
@@ -101,22 +124,21 @@ EOF
101
124
 
102
125
  echo ""
103
126
  ok "smb_config.py updated"
104
- info "Host: ${NEW_HOST} | Share: //${NEW_HOST}/${NEW_SHARE} | User: ${NEW_USER}"
105
-
106
- # Quick connectivity test
127
+ info "Target: //${NEW_HOST}/${NEW_SHARE} as ${NEW_USER}"
107
128
  echo ""
108
- echo -n " Testing connection ..."
129
+
130
+ spin_start "Pinging ${NEW_HOST}"
109
131
  if ping -c 2 -W 2 "${NEW_HOST}" &>/dev/null; then
110
- echo ""
111
- ok "${NEW_HOST} reachable"
112
- if smbclient "//${NEW_HOST}/${NEW_SHARE}" -U "${NEW_USER}%${NEW_PASS}" -c "ls" &>/dev/null 2>&1; then
113
- ok "SMB auth OK PDF delivery is ready"
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"
114
137
  else
115
- warn "SMB auth failed — verify share name and credentials on the PC"
138
+ spin_warn "Auth failed — verify share name and credentials on the PC"
116
139
  fi
117
140
  else
118
- echo ""
119
- warn "${NEW_HOST} not reachable right now — config saved, will retry at runtime"
141
+ spin_warn "${NEW_HOST} not reachable — config saved, will retry at runtime"
120
142
  fi
121
143
  echo ""
122
144
  }
@@ -141,22 +163,38 @@ logs)
141
163
  restart)
142
164
  need_sudo
143
165
  banner "Restarting services"
166
+ echo ""
167
+ spin_start "Restarting plc_watcher and plc_web"
144
168
  sudo systemctl restart plc_watcher plc_web
145
169
  sleep 2
170
+ spin_ok
171
+ echo ""
146
172
  systemctl status plc_watcher plc_web --no-pager | grep -E 'Active|Main PID'
173
+ echo ""
147
174
  ;;
175
+
148
176
  start)
149
177
  need_sudo
150
178
  banner "Starting services"
179
+ echo ""
180
+ spin_start "Starting plc_watcher and plc_web"
151
181
  sudo systemctl start plc_watcher plc_web
152
- ok "plc_watcher and plc_web started"
182
+ sleep 1
183
+ spin_ok
184
+ echo ""
153
185
  ;;
186
+
154
187
  stop)
155
188
  need_sudo
156
189
  banner "Stopping services"
190
+ echo ""
191
+ spin_start "Stopping plc_watcher and plc_web"
157
192
  sudo systemctl stop plc_watcher plc_web
158
- warn "Services stopped — they will restart automatically on next boot"
193
+ spin_ok
194
+ echo ""
195
+ warn "Services stopped — will restart automatically on next boot"
159
196
  warn "To disable auto-start: sudo systemctl disable plc_watcher plc_web"
197
+ echo ""
160
198
  ;;
161
199
 
162
200
  # ── SMB queue ─────────────────────────────────────────────────────────────────
@@ -180,7 +218,7 @@ queue)
180
218
  if [[ -f "$LEDGER" ]]; then
181
219
  SENT=$(wc -l < "$LEDGER")
182
220
  ok "Delivery ledger: ${SENT} file(s) sent"
183
- cat "$LEDGER" | sed 's/^/ /'
221
+ sed 's/^/ /' "$LEDGER"
184
222
  else
185
223
  info "No deliveries recorded yet"
186
224
  fi
@@ -198,7 +236,7 @@ push-test)
198
236
  LATEST=$(ls -t /home/pi/reports/*.pdf 2>/dev/null | head -1 || true)
199
237
  fi
200
238
  [[ -z "$LATEST" ]] && { err "Could not generate PDF — is PLC connected?"; exit 1; }
201
- info "File: $(basename $LATEST)"
239
+ info "File: $(basename "$LATEST")"
202
240
  echo ""
203
241
  "${PYTHON}" -c "
204
242
  import sys, os
@@ -228,9 +266,11 @@ wifi)
228
266
  scan|"")
229
267
  banner "WiFi Networks"
230
268
  echo ""
231
- info "Scanning ..."
269
+
270
+ spin_start "Scanning for networks"
232
271
  nmcli dev wifi rescan ifname wlan0 2>/dev/null || true
233
272
  sleep 2
273
+ spin_ok
234
274
 
235
275
  mapfile -t RAW < <(
236
276
  nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list ifname wlan0 2>/dev/null \
@@ -274,11 +314,9 @@ wifi)
274
314
  SEL="${SSIDS[$((CHOICE-1))]}"
275
315
  echo ""
276
316
  read -r -s -p " Password for '${SEL}' (blank if open): " WIFI_PASS </dev/tty
277
- echo ""
278
- echo ""
317
+ echo ""; echo ""
279
318
 
280
319
  need_sudo
281
- # Remove existing connection if any, then connect
282
320
  sudo nmcli connection delete "$SEL" 2>/dev/null || true
283
321
  if [[ -n "$WIFI_PASS" ]]; then
284
322
  sudo nmcli connection add type wifi ifname wlan0 con-name "$SEL" \
@@ -291,25 +329,21 @@ wifi)
291
329
  connection.autoconnect-priority 200 2>/dev/null
292
330
  fi
293
331
 
294
- echo -n " Connecting to '$SEL' ..."
332
+ spin_start "Connecting to '${SEL}'"
295
333
  if sudo nmcli connection up "$SEL" 2>/dev/null; then
296
- echo ""
297
334
  sleep 2
298
335
  NEW_IP=$(ip -4 addr show wlan0 2>/dev/null | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "")
299
- ok "Connected to '${SEL}'"
300
- [[ -n "$NEW_IP" ]] && ok "New IP: ${NEW_IP} → web UI at http://${NEW_IP}:8080"
336
+ spin_ok "Connected"
337
+ [[ -n "$NEW_IP" ]] && ok "Pi IP: ${NEW_IP} → http://${NEW_IP}:8080"
301
338
 
302
- # Prompt to update SMB host IP
303
339
  echo ""
304
340
  echo -e " ${Y}Network changed — SMB delivery target may need updating.${NC}"
305
341
  read -r -p " Update SMB host IP? [Y/n]: " CONFIRM </dev/tty
306
342
  CONFIRM="${CONFIRM:-Y}"
307
- if [[ "${CONFIRM^^}" == "Y" ]]; then
343
+ [[ "${CONFIRM^^}" == "Y" ]] && \
308
344
  prompt_smb_config "Network changed to '${SEL}' (Pi IP: ${NEW_IP:-unknown})"
309
- fi
310
345
  else
311
- echo ""
312
- warn "Could not connect — check password and try again"
346
+ spin_warn "Could not connect — check password and try again"
313
347
  fi
314
348
  ;;
315
349
 
@@ -334,41 +368,31 @@ hotspot)
334
368
  banner "Enable Hotspot"
335
369
  need_sudo
336
370
  echo ""
337
-
338
- # Allow custom SSID/pass as args
339
371
  [[ -n "${1:-}" ]] && HOTSPOT_SSID="$1"
340
372
  [[ -n "${2:-}" ]] && HOTSPOT_PASS="$2"
341
373
 
342
- # Bring down existing hotspot connection if any
343
374
  sudo nmcli connection delete "$HOTSPOT_CON" 2>/dev/null || true
344
375
 
345
- info "Creating hotspot '${HOTSPOT_SSID}' on wlan0 ..."
376
+ spin_start "Starting hotspot '${HOTSPOT_SSID}'"
346
377
  sudo nmcli device wifi hotspot \
347
- ifname wlan0 \
348
- con-name "$HOTSPOT_CON" \
349
- ssid "$HOTSPOT_SSID" \
350
- password "$HOTSPOT_PASS" 2>&1 | grep -v "^$" || true
351
-
378
+ ifname wlan0 con-name "$HOTSPOT_CON" \
379
+ ssid "$HOTSPOT_SSID" password "$HOTSPOT_PASS" &>/dev/null || true
352
380
  sleep 2
381
+ spin_ok "Hotspot active"
353
382
 
354
- # Get Pi's hotspot IP (usually 10.42.0.1)
355
383
  HOTSPOT_IP=$(ip -4 addr show wlan0 2>/dev/null \
356
384
  | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "10.42.0.1")
357
385
 
358
- echo ""
359
- ok "Hotspot active"
360
386
  echo ""
361
387
  echo -e " ${W}┌────────────────────────────────────────────────┐${NC}"
362
388
  echo -e " ${W}│ Connect your PC to this WiFi: │${NC}"
363
389
  echo -e " ${W}│ │${NC}"
364
- printf " ${W}│ %-14s ${G}%-30s${W} │${NC}\n" "SSID:" "$HOTSPOT_SSID"
390
+ printf " ${W}│ %-14s ${G}%-30s${W} │${NC}\n" "SSID:" "$HOTSPOT_SSID"
365
391
  printf " ${W}│ %-14s ${G}%-30s${W} │${NC}\n" "Password:" "$HOTSPOT_PASS"
366
- printf " ${W}│ %-14s ${C}%-30s${W} │${NC}\n" "Pi IP:" "$HOTSPOT_IP"
392
+ printf " ${W}│ %-14s ${C}%-30s${W} │${NC}\n" "Pi IP:" "$HOTSPOT_IP"
367
393
  echo -e " ${W}└────────────────────────────────────────────────┘${NC}"
368
394
  echo ""
369
395
  info "After PC connects, run: plc_checkweigher hotspot scan"
370
- info " — detects PC's IP and updates SMB config automatically"
371
- echo ""
372
396
  info "Web UI still accessible at: http://${HOTSPOT_IP}:8080"
373
397
  echo ""
374
398
  ;;
@@ -376,9 +400,11 @@ hotspot)
376
400
  off)
377
401
  banner "Disable Hotspot"
378
402
  need_sudo
403
+ echo ""
404
+ spin_start "Stopping hotspot"
379
405
  sudo nmcli connection delete "$HOTSPOT_CON" 2>/dev/null \
380
- && ok "Hotspot stopped" \
381
- || warn "No active hotspot found"
406
+ && spin_ok \
407
+ || spin_warn "No active hotspot found"
382
408
  echo ""
383
409
  info "To reconnect to WiFi: plc_checkweigher wifi"
384
410
  echo ""
@@ -401,29 +427,34 @@ hotspot)
401
427
  ;;
402
428
 
403
429
  scan)
404
- # Find devices connected to the hotspot subnet and offer to set as SMB target
405
430
  banner "Scanning for connected devices"
406
431
  echo ""
407
432
 
408
433
  HOTSPOT_IP=$(ip -4 addr show wlan0 2>/dev/null \
409
434
  | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' || echo "")
410
-
411
435
  if [[ -z "$HOTSPOT_IP" ]]; then
412
436
  err "Hotspot not active — run: plc_checkweigher hotspot on"
413
437
  exit 1
414
438
  fi
415
439
 
416
440
  SUBNET="${HOTSPOT_IP%.*}.0/24"
417
- info "Scanning ${SUBNET} ..."
441
+ info "Sweeping ${SUBNET} ..."
442
+ echo ""
418
443
 
419
- # Refresh ARP table by pinging the subnet
444
+ # Parallel ping sweep with live counter
445
+ DONE=0
420
446
  for i in $(seq 1 254); do
421
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
422
452
  done
423
453
  wait
454
+ [[ $_TTY -eq 1 ]] && printf '\r\033[K'
424
455
  sleep 1
456
+ ok "Subnet sweep complete"
425
457
 
426
- # Collect clients (exclude Pi itself)
427
458
  mapfile -t CLIENTS < <(
428
459
  ip neigh show 2>/dev/null \
429
460
  | grep "REACHABLE\|STALE\|DELAY" \
@@ -434,10 +465,10 @@ hotspot)
434
465
  )
435
466
 
436
467
  if [[ ${#CLIENTS[@]} -eq 0 ]]; then
437
- warn "No devices found yet — make sure the PC is connected to '${HOTSPOT_SSID}'"
438
- info "Try again in a few seconds: plc_checkweigher hotspot scan"
439
468
  echo ""
440
- exit 0
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
441
472
  fi
442
473
 
443
474
  echo ""
@@ -448,7 +479,8 @@ hotspot)
448
479
  for i in "${!CLIENTS[@]}"; do
449
480
  IP="${CLIENTS[$i]}"
450
481
  CLIENT_IPS[$i]="$IP"
451
- HOST=$(host "$IP" 2>/dev/null | grep "domain name pointer" | awk '{print $NF}' | sed 's/\.$//' || echo "")
482
+ HOST=$(host "$IP" 2>/dev/null | grep "domain name pointer" \
483
+ | awk '{print $NF}' | sed 's/\.$//' || echo "")
452
484
  printf " %-4s %-18s %s\n" "$((i+1)))" "$IP" "${HOST:-(unknown)}"
453
485
  done
454
486
  hr
@@ -489,18 +521,26 @@ display)
489
521
  on)
490
522
  banner "Enable Display"
491
523
  need_sudo
492
- sudo systemctl start lightdm 2>/dev/null \
493
- && ok "Display enabled — LightDM started" \
494
- || warn "LightDM failed to start (may not be installed)"
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
495
531
  sudo systemctl enable lightdm 2>/dev/null && ok "Auto-start enabled" || true
496
532
  echo ""
497
533
  ;;
498
534
  off)
499
535
  banner "Disable Display"
500
536
  need_sudo
501
- sudo systemctl stop lightdm 2>/dev/null \
502
- && ok "Display disabled — LightDM stopped" \
503
- || warn "LightDM was not running"
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
504
544
  read -r -p " Disable auto-start on boot too? [y/N]: " DIS </dev/tty
505
545
  DIS="${DIS:-N}"
506
546
  if [[ "${DIS^^}" == "Y" ]]; then
@@ -511,17 +551,15 @@ display)
511
551
  status)
512
552
  banner "Display Status"
513
553
  echo ""
514
- ACTIVE=$(systemctl is-active lightdm 2>/dev/null || echo "inactive")
515
- ENABLED=$(systemctl is-enabled lightdm 2>/dev/null || echo "disabled")
554
+ ACTIVE=$(systemctl is-active lightdm 2>/dev/null || true)
555
+ ENABLED=$(systemctl is-enabled lightdm 2>/dev/null || true)
516
556
  if [[ "$ACTIVE" == "active" ]]; then
517
557
  ok "LightDM: RUNNING (auto-start: ${ENABLED})"
518
558
  else
519
559
  info "LightDM: ${ACTIVE} (auto-start: ${ENABLED})"
520
560
  fi
521
- # HDMI / display connected
522
561
  if command -v tvservice &>/dev/null; then
523
- HDMI=$(tvservice -s 2>/dev/null | head -1)
524
- info "HDMI: ${HDMI}"
562
+ info "HDMI: $(tvservice -s 2>/dev/null | head -1)"
525
563
  fi
526
564
  echo ""
527
565
  ;;
@@ -538,6 +576,167 @@ smb-config)
538
576
  prompt_smb_config
539
577
  ;;
540
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
+
541
740
  # ─────────────────────────────────────────────────────────────────────────────
542
741
  # HELP
543
742
  # ─────────────────────────────────────────────────────────────────────────────
@@ -571,6 +770,8 @@ help|--help|-h)
571
770
  echo " display off Disable display (stop LightDM)"
572
771
  echo " display status Show display state"
573
772
  echo ""
773
+ echo -e " ${R} uninstall${NC} Remove everything setup.sh installed"
774
+ echo ""
574
775
  ;;
575
776
 
576
777
  *)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.7.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/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