plc-checkweigher 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/setup.sh +223 -183
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.1.0",
3
+ "version": "1.2.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"
package/setup.sh CHANGED
@@ -1,24 +1,23 @@
1
1
  #!/usr/bin/env bash
2
2
  # =============================================================================
3
- # PLC Check-Weigher — Full Stack Installer v1.1
3
+ # PLC Check-Weigher — Full Stack Installer v1.2
4
4
  # =============================================================================
5
- # Run on any fresh Raspberry Pi with ONE command:
5
+ # Run on any fresh Raspberry Pi:
6
6
  #
7
7
  # npx plc-checkweigher
8
8
  #
9
- # What happens (all in one go, single reboot at the end):
9
+ # Steps (everything first, ONE reboot at the very end):
10
10
  # 1. Pre-flight checks
11
- # 2. System packages (git, python3-venv, samba-client …)
12
- # 3. PREEMPT_RT kernel install + config (no reboot yet)
13
- # 4. Clone / update the plc-checkweigher repo
14
- # 5. Python venv + pip install (pinned versions)
15
- # 6. Create /home/pi/reports directory
16
- # 7. WiFi interactive scan pick from list enter password
17
- # 8. NetworkManager-wait-online (guarantees IP before services start)
18
- # 9. plc_watcher.service — SCHED_FIFO:50, IOClass=realtime, Core 3
19
- # 10. plc_web.service — Nice=-10
20
- # 11. Enable + arm both services
21
- # 12. REBOOT (one-time, applies RT kernel + all config)
11
+ # 2. System packages
12
+ # 3. Clone / update repo
13
+ # 4. Python venv + pip install
14
+ # 5. Create /home/<user>/reports
15
+ # 6. WiFi — scan → pick from list → password
16
+ # 7. SMB enter host IP, share name, credentials smb_config.py
17
+ # 8. NetworkManager-wait-online
18
+ # 9. systemd services (plc_watcher + plc_web)
19
+ # 10. PREEMPT_RT kernel ← installed last so only one reboot is needed
20
+ # 11. REBOOT
22
21
  # =============================================================================
23
22
 
24
23
  set -euo pipefail
@@ -48,100 +47,63 @@ PY_PKGS=(
48
47
  )
49
48
 
50
49
  # ── Colours ───────────────────────────────────────────────────────────────────
51
- B='\033[1;34m'; G='\033[0;32m'; R='\033[1;31m'; Y='\033[1;33m'; C='\033[0;36m'; NC='\033[0m'
52
-
53
- banner() { echo -e "\n${B}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
54
- echo -e "${B} $*${NC}"
55
- echo -e "${B}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; }
56
- step() { echo -e "\n${Y}▶ $*${NC}"; }
57
- ok() { echo -e " ${G}✓${NC} $*"; }
58
- warn() { echo -e " ${Y}!${NC} $*"; }
59
- info() { echo -e " ${C}i${NC} $*"; }
60
- die() { echo -e "\n${R}FATAL:${NC} $*" >&2; exit 1; }
61
- hr() { echo -e " ${B}$(printf '─%.0s' {1..48})${NC}"; }
50
+ B='\033[1;34m'; G='\033[0;32m'; R='\033[1;31m'; Y='\033[1;33m'
51
+ C='\033[0;36m'; D='\033[2m'; NC='\033[0m'
52
+
53
+ banner() { echo -e "\n${B}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
54
+ echo -e "${B} $*${NC}"
55
+ echo -e "${B}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; }
56
+ step() { echo -e "\n${Y}▶ $*${NC}"; }
57
+ ok() { echo -e " ${G}✓${NC} $*"; }
58
+ warn() { echo -e " ${Y}!${NC} $*"; }
59
+ info() { echo -e " ${C}i${NC} $*"; }
60
+ hr() { echo -e " ${D}$(printf '─%.0s' {1..50})${NC}"; }
61
+ die() { echo -e "\n${R}FATAL:${NC} $*" >&2; exit 1; }
62
+
63
+ prompt() {
64
+ # prompt <varname> <display-label> [default]
65
+ local var="$1" label="$2" default="${3:-}"
66
+ local hint=""
67
+ [[ -n "$default" ]] && hint=" ${D}[${default}]${NC}"
68
+ printf " %-28s%b: " "${label}" "${hint}"
69
+ read -r value </dev/tty
70
+ [[ -z "$value" && -n "$default" ]] && value="$default"
71
+ printf -v "$var" '%s' "$value"
72
+ }
73
+
74
+ prompt_secret() {
75
+ local var="$1" label="$2"
76
+ printf " %-28s: " "${label}"
77
+ read -r -s value </dev/tty
78
+ echo ""
79
+ printf -v "$var" '%s' "$value"
80
+ }
62
81
 
63
82
  # ── 0. Pre-flight ─────────────────────────────────────────────────────────────
64
83
  preflight() {
65
- banner "PLC Check-Weigher Installer v1.1"
66
-
67
- [[ "${EUID}" -eq 0 ]] || die "Run via: npx plc-checkweigher (asks for sudo password)"
68
- [[ "$(uname -m)" == "aarch64" ]] || die "Requires 64-bit Raspberry Pi (aarch64). Got: $(uname -m)"
69
- [[ -d "${HOME_DIR}" ]] || die "Home dir ${HOME_DIR} not found. Set PI_USER= to override."
70
- command -v nmcli &>/dev/null || die "NetworkManager not found — install Raspberry Pi OS first."
71
-
84
+ banner "PLC Check-Weigher Installer v1.2"
85
+ [[ "${EUID}" -eq 0 ]] || die "Run via npx plc-checkweigher (asks for sudo password)"
86
+ [[ "$(uname -m)" == "aarch64" ]] || die "Requires 64-bit Raspberry Pi (aarch64). Got: $(uname -m)"
87
+ [[ -d "${HOME_DIR}" ]] || die "Home ${HOME_DIR} not found. Set PI_USER= to override."
88
+ command -v nmcli &>/dev/null || die "NetworkManager not found install Raspberry Pi OS first."
72
89
  info "Host : $(hostname)"
73
90
  info "Kernel : $(uname -r)"
74
91
  info "User : ${PI_USER}"
75
- info "Repo : ${REPO_URL}"
76
92
  }
77
93
 
78
94
  # ── 1. System packages ────────────────────────────────────────────────────────
79
95
  install_system_packages() {
80
- step "Installing system packages ..."
96
+ step "System packages ..."
81
97
  DEBIAN_FRONTEND=noninteractive apt-get update -qq
82
98
  DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
83
99
  git python3-venv python3-pip python3-dev \
84
100
  samba-client cifs-utils network-manager curl build-essential
85
- ok "git, python3-venv, python3-dev, samba-client, cifs-utils"
86
- }
87
-
88
- # ── 2. RT kernel ──────────────────────────────────────────────────────────────
89
- install_rt_kernel() {
90
- step "Setting up PREEMPT_RT kernel ..."
91
-
92
- if grep -q "PREEMPT_RT" /proc/version 2>/dev/null; then
93
- ok "Already running PREEMPT_RT kernel — skipping install"
94
- return
95
- fi
96
-
97
- # Backup
98
- if [[ ! -f "${BOOT_FW}/kernel8-stock.img" ]]; then
99
- cp "${BOOT_FW}/kernel8.img" "${BOOT_FW}/kernel8-stock.img"
100
- ok "Stock kernel backed up → kernel8-stock.img"
101
- fi
102
-
103
- CHKSUM_BEFORE="$(md5sum "${BOOT_FW}/kernel8.img" | cut -d' ' -f1)"
104
- DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${RT_PKG}" "${RT_HDR}"
105
- CHKSUM_AFTER="$(md5sum "${BOOT_FW}/kernel8.img" | cut -d' ' -f1)"
106
-
107
- if [[ "${CHKSUM_BEFORE}" != "${CHKSUM_AFTER}" ]]; then
108
- cp "${BOOT_FW}/kernel8.img" "${BOOT_FW}/kernel8-rt.img"
109
- cp "${BOOT_FW}/kernel8-stock.img" "${BOOT_FW}/kernel8.img"
110
- ok "RT kernel → kernel8-rt.img | stock restored as fallback"
111
- else
112
- RT_VMLINUZ="$(ls /boot/vmlinuz-*rt-arm64 2>/dev/null | sort -V | tail -1)"
113
- [[ -n "${RT_VMLINUZ}" ]] || die "RT vmlinuz not found in /boot/"
114
- if file "${RT_VMLINUZ}" | grep -q gzip; then
115
- zcat "${RT_VMLINUZ}" > "${BOOT_FW}/kernel8-rt.img"
116
- else
117
- cp "${RT_VMLINUZ}" "${BOOT_FW}/kernel8-rt.img"
118
- fi
119
- ok "RT kernel manually copied → kernel8-rt.img"
120
- fi
121
-
122
- # Copy RT initramfs if present
123
- RT_INITRD="$(ls /boot/initrd.img-*rt-arm64 2>/dev/null | sort -V | tail -1 || true)"
124
- [[ -n "${RT_INITRD}" ]] && cp "${RT_INITRD}" "${BOOT_FW}/initramfs8-rt" \
125
- && ok "RT initramfs → initramfs8-rt"
126
-
127
- # Activate in config.txt (idempotent — removes any previous block first)
128
- sed -i '/### PLC-RT-BLOCK-START ###/,/### PLC-RT-BLOCK-END ###/d' \
129
- "${BOOT_FW}/config.txt"
130
- cat >> "${BOOT_FW}/config.txt" << 'EOF'
131
-
132
- ### PLC-RT-BLOCK-START ###
133
- # PREEMPT_RT kernel — installed by plc-checkweigher setup.sh
134
- # To revert: comment the two lines below and reboot.
135
- kernel=kernel8-rt.img
136
- initramfs initramfs8-rt followkernel
137
- ### PLC-RT-BLOCK-END ###
138
- EOF
139
- ok "config.txt updated — RT kernel activates after reboot"
101
+ ok "git python3-venv samba-client cifs-utils build-essential"
140
102
  }
141
103
 
142
- # ── 3. Clone / update repo ────────────────────────────────────────────────────
104
+ # ── 2. Clone / update repo ────────────────────────────────────────────────────
143
105
  setup_repo() {
144
- step "Setting up repository ..."
106
+ step "Repository ..."
145
107
  if [[ -d "${INSTALL_DIR}/.git" ]]; then
146
108
  sudo -u "${PI_USER}" git -C "${INSTALL_DIR}" pull --ff-only origin "${REPO_BRANCH}" \
147
109
  && ok "Repo updated → ${INSTALL_DIR}" \
@@ -153,41 +115,38 @@ setup_repo() {
153
115
  fi
154
116
  }
155
117
 
156
- # ── 4. Python venv ────────────────────────────────────────────────────────────
118
+ # ── 3. Python venv ────────────────────────────────────────────────────────────
157
119
  setup_venv() {
158
- step "Setting up Python environment ..."
120
+ step "Python environment ..."
159
121
  [[ -d "${VENV_DIR}" ]] \
160
122
  && ok "venv exists — skipping creation" \
161
123
  || sudo -u "${PI_USER}" python3 -m venv "${VENV_DIR}"
162
124
  sudo -u "${PI_USER}" "${VENV_DIR}/bin/pip" install -q --upgrade pip
163
125
  sudo -u "${PI_USER}" "${VENV_DIR}/bin/pip" install -q "${PY_PKGS[@]}"
164
- ok "Packages installed in ${VENV_DIR}"
126
+ ok "Packages installed${VENV_DIR}"
165
127
  }
166
128
 
167
- # ── 5. Directories ────────────────────────────────────────────────────────────
129
+ # ── 4. Directories ────────────────────────────────────────────────────────────
168
130
  setup_dirs() {
169
- step "Creating runtime directories ..."
131
+ step "Runtime directories ..."
170
132
  mkdir -p "${REPORTS_DIR}"
171
133
  chown "${PI_USER}:${PI_USER}" "${REPORTS_DIR}"
172
134
  ok "${REPORTS_DIR}"
173
135
  }
174
136
 
175
- # ── 6. WiFi — interactive scan + pick ────────────────────────────────────────
137
+ # ── 5. WiFi — scan pick → password ─────────────────────────────────────────
176
138
  setup_wifi() {
177
139
  step "WiFi Setup"
178
140
 
179
- # Check if wlan0 exists
180
141
  if ! ip link show wlan0 &>/dev/null; then
181
- warn "No wlan0 interface found — skipping WiFi setup."
142
+ warn "No wlan0 found — skipping WiFi setup."
182
143
  return
183
144
  fi
184
145
 
185
- echo ""
186
- echo -e " ${C}Scanning for nearby networks ...${NC}"
146
+ echo -e "\n ${C}Scanning for networks ...${NC}"
187
147
  nmcli dev wifi rescan ifname wlan0 2>/dev/null || true
188
148
  sleep 3
189
149
 
190
- # Build deduplicated, signal-sorted list [SSID, SIGNAL, SECURITY]
191
150
  mapfile -t RAW < <(
192
151
  nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list ifname wlan0 2>/dev/null \
193
152
  | grep -v '^:' \
@@ -197,65 +156,47 @@ setup_wifi() {
197
156
  )
198
157
 
199
158
  if [[ ${#RAW[@]} -eq 0 ]]; then
200
- warn "No networks found. Skipping WiFi setup."
159
+ warn "No networks found skipping WiFi setup."
201
160
  return
202
161
  fi
203
162
 
204
- # ── Print menu ────────────────────────────────────────────────
205
163
  echo ""
206
164
  hr
207
165
  printf " ${B}%-4s %-28s %-10s %s${NC}\n" "#" "SSID" "Signal" "Security"
208
166
  hr
209
-
210
167
  declare -a SSIDS SIGNALS SECURITIES
211
168
  for i in "${!RAW[@]}"; do
212
169
  IFS=':' read -r SSID SIGNAL SECURITY <<< "${RAW[$i]}"
213
- SSIDS[$i]="${SSID}"
214
- SIGNALS[$i]="${SIGNAL}"
215
- SECURITIES[$i]="${SECURITY}"
216
-
217
- # Visual signal bar
170
+ SSIDS[$i]="${SSID}"; SIGNALS[$i]="${SIGNAL}"; SECURITIES[$i]="${SECURITY}"
218
171
  SIG="${SIGNAL:-0}"
219
172
  if [[ $SIG -ge 80 ]]; then BAR="${G}▂▄▆█${NC}"
220
- elif [[ $SIG -ge 60 ]]; then BAR="${G}▂▄▆${NC} "
221
- elif [[ $SIG -ge 40 ]]; then BAR="${Y}▂▄${NC} "
222
- else BAR="${R}▂${NC} "; fi
223
-
224
- SEC="${SECURITY:---}"
225
- printf " %-4s %-28s %b %-4s%% %s\n" \
226
- "$((i+1)))" "${SSID}" "${BAR}" "${SIG}" "${SEC}"
173
+ elif [[ $SIG -ge 60 ]]; then BAR="${G}▂▄▆ ${NC}"
174
+ elif [[ $SIG -ge 40 ]]; then BAR="${Y}▂▄ ${NC}"
175
+ else BAR="${R}▂ ${NC}"; fi
176
+ printf " %-4s %-28s %b %3s%% %s\n" \
177
+ "$((i+1)))" "${SSID}" "${BAR}" "${SIG}" "${SECURITY:---}"
227
178
  done
228
-
229
179
  hr
230
180
  printf " %-4s %s\n" "0)" "Skip WiFi setup"
231
181
  echo ""
232
182
 
233
- # ── Read choice ───────────────────────────────────────────────
234
183
  while true; do
235
184
  read -r -p " Choose network [1-${#RAW[@]}] or 0 to skip: " CHOICE </dev/tty
236
185
  [[ "$CHOICE" =~ ^[0-9]+$ ]] && \
237
- [[ "$CHOICE" -ge 0 ]] && [[ "$CHOICE" -le "${#RAW[@]}" ]] && break
238
- echo -e " ${R}Invalid choice — enter a number between 0 and ${#RAW[@]}${NC}"
186
+ [[ "$CHOICE" -ge 0 && "$CHOICE" -le "${#RAW[@]}" ]] && break
187
+ echo -e " ${R}Enter a number between 0 and ${#RAW[@]}${NC}"
239
188
  done
240
189
 
241
- if [[ "$CHOICE" -eq 0 ]]; then
242
- warn "WiFi setup skipped."
243
- return
244
- fi
190
+ [[ "$CHOICE" -eq 0 ]] && { warn "WiFi setup skipped."; return; }
245
191
 
246
192
  IDX=$((CHOICE - 1))
247
193
  SEL_SSID="${SSIDS[$IDX]}"
248
194
  SEL_SEC="${SECURITIES[$IDX]}"
249
195
 
250
- echo ""
251
- ok "Selected: ${SEL_SSID}"
252
-
253
- # ── Password prompt (skip for open networks) ──────────────────
254
196
  WIFI_PASS=""
255
197
  if [[ "${SEL_SEC}" != "--" && -n "${SEL_SEC}" ]]; then
256
198
  while true; do
257
- read -r -s -p " Enter WiFi password: " WIFI_PASS </dev/tty
258
- echo ""
199
+ prompt_secret WIFI_PASS "WiFi password"
259
200
  [[ -n "${WIFI_PASS}" ]] && break
260
201
  echo -e " ${R}Password cannot be empty for a secured network.${NC}"
261
202
  done
@@ -263,42 +204,92 @@ setup_wifi() {
263
204
  info "Open network — no password needed."
264
205
  fi
265
206
 
266
- # ── Create / update the NM connection profile ─────────────────
267
- # Remove any existing profile with this SSID to avoid conflicts
268
207
  nmcli connection delete "${SEL_SSID}" 2>/dev/null || true
269
-
270
208
  if [[ -n "${WIFI_PASS}" ]]; then
271
- nmcli connection add \
272
- type wifi ifname wlan0 \
273
- con-name "${SEL_SSID}" \
274
- ssid "${SEL_SSID}" \
275
- wifi-sec.key-mgmt wpa-psk \
276
- wifi-sec.psk "${WIFI_PASS}" \
277
- connection.autoconnect yes \
278
- connection.autoconnect-priority 200 2>/dev/null
209
+ nmcli connection add type wifi ifname wlan0 con-name "${SEL_SSID}" \
210
+ ssid "${SEL_SSID}" wifi-sec.key-mgmt wpa-psk wifi-sec.psk "${WIFI_PASS}" \
211
+ connection.autoconnect yes connection.autoconnect-priority 200 2>/dev/null
279
212
  else
280
- nmcli connection add \
281
- type wifi ifname wlan0 \
282
- con-name "${SEL_SSID}" \
283
- ssid "${SEL_SSID}" \
284
- connection.autoconnect yes \
213
+ nmcli connection add type wifi ifname wlan0 con-name "${SEL_SSID}" \
214
+ ssid "${SEL_SSID}" connection.autoconnect yes \
285
215
  connection.autoconnect-priority 200 2>/dev/null
286
216
  fi
287
217
 
288
- # Connect now so the rest of the setup (git pull, pip) works
289
218
  echo -n " Connecting ..."
290
219
  if nmcli connection up "${SEL_SSID}" 2>/dev/null; then
291
- echo ""
292
- ok "Connected to '${SEL_SSID}'"
220
+ echo ""; ok "Connected to '${SEL_SSID}'"
293
221
  else
222
+ echo ""; warn "Will connect automatically after reboot."
223
+ fi
224
+ }
225
+
226
+ # ── 6. SMB file sharing — interactive ────────────────────────────────────────
227
+ setup_smb() {
228
+ step "SMB File Sharing Setup"
229
+ echo ""
230
+ echo -e " ${C}PDF reports will be pushed to a shared folder on another PC.${NC}"
231
+ echo -e " ${D}Leave blank to disable SMB push.${NC}"
232
+ echo ""
233
+ hr
234
+
235
+ prompt SMB_HOST "Host IP address" ""
236
+ if [[ -z "${SMB_HOST}" ]]; then
237
+ warn "SMB push disabled — no host entered."
238
+ # Write a disabled smb_config.py
239
+ cat > "${INSTALL_DIR}/smb_config.py" << 'EOF'
240
+ # SMB push disabled during setup
241
+ SMB_ENABLED = False
242
+ SMB_HOST = ""
243
+ SMB_SHARE = ""
244
+ SMB_USERNAME = ""
245
+ SMB_PASSWORD = ""
246
+ SMB_SUBDIR = ""
247
+ EOF
248
+ chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/smb_config.py"
249
+ return
250
+ fi
251
+
252
+ prompt SMB_SHARE "Share name (folder)" "Reports"
253
+ prompt SMB_USERNAME "Username" ""
254
+ prompt_secret SMB_PASSWORD "Password"
255
+ prompt SMB_SUBDIR "Subfolder (optional)" ""
256
+
257
+ hr
258
+ echo ""
259
+
260
+ # Write smb_config.py (gitignored — credentials stay off GitHub)
261
+ cat > "${INSTALL_DIR}/smb_config.py" << EOF
262
+ # SMB configuration — written by setup.sh, NOT committed to git.
263
+ SMB_ENABLED = True
264
+ SMB_HOST = "${SMB_HOST}"
265
+ SMB_SHARE = "${SMB_SHARE}"
266
+ SMB_USERNAME = "${SMB_USERNAME}"
267
+ SMB_PASSWORD = "${SMB_PASSWORD}"
268
+ SMB_SUBDIR = "${SMB_SUBDIR}"
269
+ EOF
270
+ chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/smb_config.py"
271
+ ok "SMB config saved → ${INSTALL_DIR}/smb_config.py"
272
+
273
+ # Test connectivity
274
+ echo -n " Testing connection to ${SMB_HOST} ..."
275
+ if ping -c 2 -W 2 "${SMB_HOST}" &>/dev/null; then
294
276
  echo ""
295
- warn "Could not connect now — will auto-connect on reboot."
277
+ ok "Host ${SMB_HOST} reachable"
278
+ echo -n " Authenticating with share //${SMB_HOST}/${SMB_SHARE} ..."
279
+ if smbclient "//${SMB_HOST}/${SMB_SHARE}" \
280
+ -U "${SMB_USERNAME}%${SMB_PASSWORD}" -c "ls" &>/dev/null 2>&1; then
281
+ echo ""; ok "SMB share authenticated — PDF push is ready"
282
+ else
283
+ echo ""; warn "Auth failed — verify credentials and share name after reboot."
284
+ fi
285
+ else
286
+ echo ""; warn "${SMB_HOST} not reachable now — will retry at runtime."
296
287
  fi
297
288
  }
298
289
 
299
290
  # ── 7. Network-online guarantee ───────────────────────────────────────────────
300
291
  setup_network_online() {
301
- step "Enabling NetworkManager-wait-online (60 s timeout) ..."
292
+ step "NetworkManager-wait-online ..."
302
293
  mkdir -p /etc/systemd/system/NetworkManager-wait-online.service.d/
303
294
  cat > /etc/systemd/system/NetworkManager-wait-online.service.d/timeout.conf << 'EOF'
304
295
  [Service]
@@ -306,14 +297,13 @@ ExecStart=
306
297
  ExecStart=/usr/lib/NetworkManager/nm-online -s -q --timeout=60
307
298
  EOF
308
299
  systemctl enable NetworkManager-wait-online.service 2>/dev/null || true
309
- ok "Services will wait up to 60 s for a network connection after boot"
300
+ ok "Services will wait up to 60 s for network on each boot"
310
301
  }
311
302
 
312
- # ── 8 & 9. systemd services ───────────────────────────────────────────────────
303
+ # ── 8. systemd services ───────────────────────────────────────────────────────
313
304
  install_services() {
314
- step "Installing systemd services ..."
305
+ step "systemd services ..."
315
306
 
316
- # plc_watcher — SCHED_FIFO:50, IOClass=realtime, pinned to CPU core 3
317
307
  cat > /etc/systemd/system/plc_watcher.service << EOF
318
308
  [Unit]
319
309
  Description=PLC Check-Weigher Start Watcher
@@ -342,7 +332,6 @@ WantedBy=multi-user.target
342
332
  EOF
343
333
  ok "plc_watcher.service (SCHED_FIFO:50 · IOClass=realtime · CPU core 3)"
344
334
 
345
- # plc_web — elevated but not real-time
346
335
  cat > /etc/systemd/system/plc_web.service << EOF
347
336
  [Unit]
348
337
  Description=PLC Check-Weigher Report Viewer
@@ -369,38 +358,88 @@ EOF
369
358
 
370
359
  systemctl daemon-reload
371
360
  systemctl enable plc_watcher.service plc_web.service
372
- ok "Both services enabled — will start automatically after reboot"
361
+ ok "Both services enabled — start automatically after reboot"
373
362
 
374
- # Mirror updated service file back into the repo
375
363
  cp /etc/systemd/system/plc_watcher.service "${INSTALL_DIR}/plc_watcher.service"
376
364
  chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/plc_watcher.service"
377
365
  }
378
366
 
367
+ # ── 9. RT kernel — installed LAST so only one reboot is needed ────────────────
368
+ install_rt_kernel() {
369
+ step "PREEMPT_RT kernel (final step before reboot) ..."
370
+
371
+ if grep -q "PREEMPT_RT" /proc/version 2>/dev/null; then
372
+ ok "Already running PREEMPT_RT — skipping kernel install"
373
+ return
374
+ fi
375
+
376
+ # Backup stock kernel
377
+ if [[ ! -f "${BOOT_FW}/kernel8-stock.img" ]]; then
378
+ cp "${BOOT_FW}/kernel8.img" "${BOOT_FW}/kernel8-stock.img"
379
+ ok "Stock kernel backed up → kernel8-stock.img"
380
+ fi
381
+
382
+ CHKSUM_BEFORE="$(md5sum "${BOOT_FW}/kernel8.img" | cut -d' ' -f1)"
383
+ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${RT_PKG}" "${RT_HDR}"
384
+ CHKSUM_AFTER="$(md5sum "${BOOT_FW}/kernel8.img" | cut -d' ' -f1)"
385
+
386
+ if [[ "${CHKSUM_BEFORE}" != "${CHKSUM_AFTER}" ]]; then
387
+ cp "${BOOT_FW}/kernel8.img" "${BOOT_FW}/kernel8-rt.img"
388
+ cp "${BOOT_FW}/kernel8-stock.img" "${BOOT_FW}/kernel8.img"
389
+ ok "RT kernel → kernel8-rt.img | stock restored as boot default"
390
+ else
391
+ RT_VMLINUZ="$(ls /boot/vmlinuz-*rt-arm64 2>/dev/null | sort -V | tail -1)"
392
+ [[ -n "${RT_VMLINUZ}" ]] || die "RT vmlinuz not found in /boot/"
393
+ if file "${RT_VMLINUZ}" | grep -q gzip; then
394
+ zcat "${RT_VMLINUZ}" > "${BOOT_FW}/kernel8-rt.img"
395
+ else
396
+ cp "${RT_VMLINUZ}" "${BOOT_FW}/kernel8-rt.img"
397
+ fi
398
+ ok "RT kernel manually copied → kernel8-rt.img"
399
+ fi
400
+
401
+ RT_INITRD="$(ls /boot/initrd.img-*rt-arm64 2>/dev/null | sort -V | tail -1 || true)"
402
+ [[ -n "${RT_INITRD}" ]] && cp "${RT_INITRD}" "${BOOT_FW}/initramfs8-rt" \
403
+ && ok "RT initramfs → initramfs8-rt"
404
+
405
+ # Activate in config.txt (idempotent)
406
+ sed -i '/### PLC-RT-BLOCK-START ###/,/### PLC-RT-BLOCK-END ###/d' \
407
+ "${BOOT_FW}/config.txt"
408
+ cat >> "${BOOT_FW}/config.txt" << 'EOF'
409
+
410
+ ### PLC-RT-BLOCK-START ###
411
+ # PREEMPT_RT kernel — installed by plc-checkweigher setup.sh
412
+ # To revert to stock: comment the two lines below and reboot.
413
+ kernel=kernel8-rt.img
414
+ initramfs initramfs8-rt followkernel
415
+ ### PLC-RT-BLOCK-END ###
416
+ EOF
417
+ ok "config.txt updated — RT kernel activates on reboot"
418
+ }
419
+
379
420
  # ── Summary + countdown reboot ────────────────────────────────────────────────
380
421
  do_reboot() {
381
422
  echo ""
382
- banner "All Done — Summary"
423
+ banner "Setup Complete"
383
424
  echo ""
384
-
385
- printf " %-30s %s\n" "RT kernel ready:" "kernel8-rt.img (activates on reboot)"
386
- printf " %-30s %s\n" "Stock kernel fallback:" "kernel8-stock.img"
387
- printf " %-30s %s\n" "Repo:" "${INSTALL_DIR}"
388
- printf " %-30s %s\n" "Python venv:" "${VENV_DIR}"
389
- printf " %-30s %s\n" "Reports output:" "${REPORTS_DIR}"
390
- printf " %-30s %s\n" "Web dashboard (after reboot):" \
425
+ printf " ${G}%-32s${NC} %s\n" "Repo:" "${INSTALL_DIR}"
426
+ printf " ${G}%-32s${NC} %s\n" "Python venv:" "${VENV_DIR}"
427
+ printf " ${G}%-32s${NC} %s\n" "Reports output:" "${REPORTS_DIR}"
428
+ printf " ${G}%-32s${NC} %s\n" "RT kernel:" "kernel8-rt.img (active after reboot)"
429
+ printf " ${G}%-32s${NC} %s\n" "Stock kernel fallback:" "kernel8-stock.img"
430
+ printf " ${G}%-32s${NC} %s\n" "Web dashboard (after reboot):" \
391
431
  "http://$(hostname -I | awk '{print $1}' 2>/dev/null || echo '<pi-ip>'):8080"
392
-
393
432
  echo ""
394
- echo -e " ${Y}After reboot, check services with:${NC}"
395
- echo " journalctl -u plc_watcher -f"
396
- echo " sudo chrt -p \$(systemctl show -p MainPID --value plc_watcher)"
433
+ echo -e " ${Y}After reboot:${NC}"
434
+ echo " journalctl -u plc_watcher -f # live logs"
435
+ echo " sudo chrt -p \$(systemctl show -p MainPID --value plc_watcher) # verify RT"
397
436
  echo ""
398
437
 
399
438
  echo -e "${G}"
400
439
  echo " ╔══════════════════════════════════════════════════════╗"
401
- echo " ║ Rebooting in 10 seconds to apply all changes. ║"
402
- echo " ║ Press Ctrl+C to cancel — reboot manually later: ║"
403
- echo " ║ sudo reboot ║"
440
+ echo " ║ All done. Rebooting in 10 seconds to apply all ║"
441
+ echo " ║ changes including the RT kernel. ║"
442
+ echo " ║ Press Ctrl+C to cancel — then: sudo reboot ║"
404
443
  echo " ╚══════════════════════════════════════════════════════╝"
405
444
  echo -e "${NC}"
406
445
 
@@ -415,15 +454,16 @@ do_reboot() {
415
454
  # ── Main ──────────────────────────────────────────────────────────────────────
416
455
  main() {
417
456
  preflight
418
- install_system_packages
419
- install_rt_kernel
420
- setup_repo
421
- setup_venv
422
- setup_dirs
423
- setup_wifi
424
- setup_network_online
425
- install_services
426
- do_reboot
457
+ install_system_packages # 1
458
+ setup_repo # 2
459
+ setup_venv # 3
460
+ setup_dirs # 4
461
+ setup_wifi # 5 — interactive WiFi picker
462
+ setup_smb # 6 — interactive SMB config → smb_config.py
463
+ setup_network_online # 7
464
+ install_services # 8
465
+ install_rt_kernel # 9 — LAST, so only one reboot needed
466
+ do_reboot # 10 — single reboot applies everything
427
467
  }
428
468
 
429
469
  main "$@"