plc-checkweigher 1.0.0 → 1.1.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 +270 -331
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.0.0",
3
+ "version": "1.1.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,57 +1,41 @@
1
1
  #!/usr/bin/env bash
2
2
  # =============================================================================
3
- # PLC Check-Weigher — Full Stack Bootstrap Installer
3
+ # PLC Check-Weigher — Full Stack Installer v1.1
4
4
  # =============================================================================
5
+ # Run on any fresh Raspberry Pi with ONE command:
5
6
  #
6
- # One-liner (run this on any fresh Raspberry Pi):
7
+ # npx plc-checkweigher
7
8
  #
8
- # sudo bash -c "$(curl -sSL https://raw.githubusercontent.com/Bibin-VR/plc-checkweigher/main/setup.sh)"
9
- #
10
- # What it does (automatically, in order):
11
- # 1. Validates hardware / OS / arch
12
- # 2. Installs the PREEMPT_RT kernel (reboots once if needed, then resumes)
13
- # 3. Installs all system & Python dependencies
14
- # 4. Clones / updates this repo
15
- # 5. Configures WiFi (sai @samarth) with highest autoconnect priority
16
- # 6. Enables network-online guarantee before services start
17
- # 7. Creates /home/pi/reports directory
18
- # 8. Installs both systemd services with SCHED_FIFO real-time scheduling
19
- # 9. Starts & enables everything
20
- # 10. Verifies the full stack
21
- #
22
- # Override defaults via env vars before the command:
23
- # WIFI_PASS="secret" PI_USER="pi" sudo bash -c "$(curl -sSL ...)"
9
+ # What happens (all in one go, single reboot at the end):
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)
24
22
  # =============================================================================
25
23
 
26
24
  set -euo pipefail
27
25
  IFS=$'\n\t'
28
26
 
29
- # ── Configurable defaults ─────────────────────────────────────────────────────
27
+ # ── Configurable ──────────────────────────────────────────────────────────────
30
28
  PI_USER="${PI_USER:-pi}"
31
- WIFI_SSID="${WIFI_SSID:-sai @samarth}"
32
- WIFI_PASS="${WIFI_PASS:-}" # prompted if empty and not connected
33
- SMB_HOST="${SMB_HOST:-192.168.0.140}"
34
- SMB_SHARE="${SMB_SHARE:-Reports}"
35
- SMB_USER="${SMB_USER:-plcreport}"
36
- SMB_PASS="${SMB_PASS:-plcreport}"
37
29
  REPO_URL="https://github.com/Bibin-VR/plc-checkweigher.git"
38
30
  REPO_BRANCH="main"
39
-
40
- # ── Derived paths ─────────────────────────────────────────────────────────────
41
31
  HOME_DIR="/home/${PI_USER}"
42
32
  INSTALL_DIR="${HOME_DIR}/plc_checkweigher"
43
33
  VENV_DIR="${HOME_DIR}/plc_env"
44
34
  REPORTS_DIR="${HOME_DIR}/reports"
45
35
  BOOT_FW="/boot/firmware"
46
- STATE_DIR="/var/lib/plc-setup"
47
- SELF_COPY="${STATE_DIR}/setup.sh"
48
- CONT_SVC="plc-setup-continue.service"
49
-
50
- # ── RT kernel package ─────────────────────────────────────────────────────────
51
36
  RT_PKG="linux-image-6.12.86+deb13-rt-arm64"
52
37
  RT_HDR="linux-headers-6.12.86+deb13-rt-arm64"
53
38
 
54
- # ── Python packages (pinned) ──────────────────────────────────────────────────
55
39
  PY_PKGS=(
56
40
  "Flask==3.1.3"
57
41
  "pymcprotocol==0.3.0"
@@ -63,254 +47,258 @@ PY_PKGS=(
63
47
  "scapy==2.7.0"
64
48
  )
65
49
 
66
- # ── Terminal colours ──────────────────────────────────────────────────────────
67
- R='\033[1;31m'; G='\033[0;32m'; B='\033[1;34m'; Y='\033[1;33m'; NC='\033[0m'
68
- banner() { echo -e "\n${B}══════════════════════════════════════════════${NC}"; \
69
- echo -e "${B} $*${NC}"; \
70
- echo -e "${B}══════════════════════════════════════════════${NC}"; }
71
- step() { echo -e "\n${Y}[${1}]${NC} ${2}"; }
72
- ok() { echo -e " ${G}✓${NC} ${1}"; }
73
- warn() { echo -e " ${Y}!${NC} ${1}"; }
74
- die() { echo -e "\n${R}FATAL:${NC} ${1}" >&2; exit 1; }
75
-
76
- # ── Guard: must be root ───────────────────────────────────────────────────────
77
- [[ "${EUID}" -eq 0 ]] || die "Run with sudo: sudo bash -c \"\$(curl -sSL ...)\" "
78
-
79
- # ── Guard: must be aarch64 ────────────────────────────────────────────────────
80
- ARCH="$(uname -m)"
81
- [[ "${ARCH}" == "aarch64" ]] || die "This installer targets 64-bit Raspberry Pi (aarch64). Got: ${ARCH}"
82
-
83
- # ── Guard: home dir must exist ────────────────────────────────────────────────
84
- [[ -d "${HOME_DIR}" ]] || die "User home ${HOME_DIR} not found. Set PI_USER= before running."
85
-
86
- mkdir -p "${STATE_DIR}"
50
+ # ── 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}"; }
62
+
63
+ # ── 0. Pre-flight ─────────────────────────────────────────────────────────────
64
+ 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
+
72
+ info "Host : $(hostname)"
73
+ info "Kernel : $(uname -r)"
74
+ info "User : ${PI_USER}"
75
+ info "Repo : ${REPO_URL}"
76
+ }
87
77
 
88
- # =============================================================================
89
- # PHASE 1 — Real-Time Kernel (skipped when already running PREEMPT_RT)
90
- # =============================================================================
91
- is_rt_kernel() { grep -q "PREEMPT_RT" /proc/version 2>/dev/null; }
78
+ # ── 1. System packages ────────────────────────────────────────────────────────
79
+ install_system_packages() {
80
+ step "Installing system packages ..."
81
+ DEBIAN_FRONTEND=noninteractive apt-get update -qq
82
+ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
83
+ git python3-venv python3-pip python3-dev \
84
+ samba-client cifs-utils network-manager curl build-essential
85
+ ok "git, python3-venv, python3-dev, samba-client, cifs-utils"
86
+ }
92
87
 
88
+ # ── 2. RT kernel ──────────────────────────────────────────────────────────────
93
89
  install_rt_kernel() {
94
- banner "Phase 1 Installing PREEMPT_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
95
96
 
96
- step "1a" "Backing up current kernel image ..."
97
+ # Backup
97
98
  if [[ ! -f "${BOOT_FW}/kernel8-stock.img" ]]; then
98
99
  cp "${BOOT_FW}/kernel8.img" "${BOOT_FW}/kernel8-stock.img"
99
- ok "Backup${BOOT_FW}/kernel8-stock.img"
100
- else
101
- ok "Backup already exists — skipping"
100
+ ok "Stock kernel backed up → kernel8-stock.img"
102
101
  fi
103
102
 
104
103
  CHKSUM_BEFORE="$(md5sum "${BOOT_FW}/kernel8.img" | cut -d' ' -f1)"
105
-
106
- step "1b" "Installing ${RT_PKG} ..."
107
- DEBIAN_FRONTEND=noninteractive apt-get install -y "${RT_PKG}" "${RT_HDR}"
108
- ok "RT kernel package installed"
109
-
110
- # Detect whether post-install hook overwrote kernel8.img
104
+ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${RT_PKG}" "${RT_HDR}"
111
105
  CHKSUM_AFTER="$(md5sum "${BOOT_FW}/kernel8.img" | cut -d' ' -f1)"
106
+
112
107
  if [[ "${CHKSUM_BEFORE}" != "${CHKSUM_AFTER}" ]]; then
113
- # Hook replaced kernel8.img → move to kernel8-rt.img, restore stock
114
108
  cp "${BOOT_FW}/kernel8.img" "${BOOT_FW}/kernel8-rt.img"
115
109
  cp "${BOOT_FW}/kernel8-stock.img" "${BOOT_FW}/kernel8.img"
116
- ok "RT kernel → kernel8-rt.img | stock kernel restored as default"
110
+ ok "RT kernel → kernel8-rt.img | stock restored as fallback"
117
111
  else
118
- # Hook did NOT copy — do it manually
119
112
  RT_VMLINUZ="$(ls /boot/vmlinuz-*rt-arm64 2>/dev/null | sort -V | tail -1)"
120
- [[ -n "${RT_VMLINUZ}" ]] || die "Cannot find RT vmlinuz in /boot/"
113
+ [[ -n "${RT_VMLINUZ}" ]] || die "RT vmlinuz not found in /boot/"
121
114
  if file "${RT_VMLINUZ}" | grep -q gzip; then
122
115
  zcat "${RT_VMLINUZ}" > "${BOOT_FW}/kernel8-rt.img"
123
116
  else
124
117
  cp "${RT_VMLINUZ}" "${BOOT_FW}/kernel8-rt.img"
125
118
  fi
126
- ok "RT kernel manually copied → ${BOOT_FW}/kernel8-rt.img"
119
+ ok "RT kernel manually copied → kernel8-rt.img"
127
120
  fi
128
121
 
129
- # Copy RT initramfs if one was generated
122
+ # Copy RT initramfs if present
130
123
  RT_INITRD="$(ls /boot/initrd.img-*rt-arm64 2>/dev/null | sort -V | tail -1 || true)"
131
- if [[ -n "${RT_INITRD}" ]]; then
132
- cp "${RT_INITRD}" "${BOOT_FW}/initramfs8-rt"
133
- ok "RT initramfs → ${BOOT_FW}/initramfs8-rt"
134
- fi
124
+ [[ -n "${RT_INITRD}" ]] && cp "${RT_INITRD}" "${BOOT_FW}/initramfs8-rt" \
125
+ && ok "RT initramfs → initramfs8-rt"
135
126
 
136
- step "1c" "Activating RT kernel in ${BOOT_FW}/config.txt ..."
137
- # Remove any previous RT block we wrote
127
+ # Activate in config.txt (idempotent — removes any previous block first)
138
128
  sed -i '/### PLC-RT-BLOCK-START ###/,/### PLC-RT-BLOCK-END ###/d' \
139
129
  "${BOOT_FW}/config.txt"
140
-
141
- cat >> "${BOOT_FW}/config.txt" << 'CFGEOF'
130
+ cat >> "${BOOT_FW}/config.txt" << 'EOF'
142
131
 
143
132
  ### PLC-RT-BLOCK-START ###
144
133
  # PREEMPT_RT kernel — installed by plc-checkweigher setup.sh
134
+ # To revert: comment the two lines below and reboot.
145
135
  kernel=kernel8-rt.img
146
136
  initramfs initramfs8-rt followkernel
147
137
  ### PLC-RT-BLOCK-END ###
148
- CFGEOF
149
- ok "config.txt updated — system will boot RT kernel after reboot"
150
-
151
- step "1d" "Saving installer for post-reboot continuation ..."
152
- # Save this very script so the continuation service can re-run it
153
- if [[ -f "${SELF_COPY}" && "${BASH_SOURCE[0]}" != "${SELF_COPY}" ]]; then
154
- : # already saved by a previous run
155
- else
156
- # If running via bash -c "$(curl ...)", BASH_SOURCE[0] is empty
157
- # In that case, re-download from GitHub so we have a saved copy
158
- if [[ -f "${BASH_SOURCE[0]:-}" ]]; then
159
- cp "${BASH_SOURCE[0]}" "${SELF_COPY}"
160
- else
161
- curl -sSL \
162
- "https://raw.githubusercontent.com/Bibin-VR/plc-checkweigher/${REPO_BRANCH}/setup.sh" \
163
- -o "${SELF_COPY}"
164
- fi
165
- fi
166
- chmod +x "${SELF_COPY}"
167
-
168
- # Persist any env overrides so they survive across reboot
169
- cat > "${STATE_DIR}/env" << ENVEOF
170
- PI_USER="${PI_USER}"
171
- WIFI_SSID="${WIFI_SSID}"
172
- WIFI_PASS="${WIFI_PASS}"
173
- SMB_HOST="${SMB_HOST}"
174
- SMB_SHARE="${SMB_SHARE}"
175
- SMB_USER="${SMB_USER}"
176
- SMB_PASS="${SMB_PASS}"
177
- ENVEOF
178
- chmod 600 "${STATE_DIR}/env"
179
-
180
- step "1e" "Creating post-reboot continuation service ..."
181
- cat > "/etc/systemd/system/${CONT_SVC}" << SVCEOF
182
- [Unit]
183
- Description=PLC Check-Weigher Setup Continuation (post-RT-kernel reboot)
184
- After=network.target
185
- ConditionPathExists=${STATE_DIR}/env
186
-
187
- [Service]
188
- Type=oneshot
189
- EnvironmentFile=${STATE_DIR}/env
190
- ExecStart=/usr/bin/bash ${SELF_COPY}
191
- ExecStartPost=/bin/rm -f ${STATE_DIR}/env
192
- StandardOutput=journal+console
193
- StandardError=journal+console
194
- RemainAfterExit=yes
195
-
196
- [Install]
197
- WantedBy=multi-user.target
198
- SVCEOF
199
-
200
- systemctl daemon-reload
201
- systemctl enable "${CONT_SVC}"
202
- ok "Continuation service enabled — will auto-run phase 2 after reboot"
203
-
204
- echo ""
205
- echo -e "${G}╔═══════════════════════════════════════════════════╗${NC}"
206
- echo -e "${G}║ RT kernel ready. Rebooting in 5 seconds ... ║${NC}"
207
- echo -e "${G}║ Phase 2 will complete automatically on boot. ║${NC}"
208
- echo -e "${G}║ Watch progress: journalctl -u ${CONT_SVC} -f ║${NC}"
209
- echo -e "${G}╚═══════════════════════════════════════════════════╝${NC}"
210
- sleep 5
211
- reboot
138
+ EOF
139
+ ok "config.txt updated — RT kernel activates after reboot"
212
140
  }
213
141
 
214
- # =============================================================================
215
- # PHASE 2 — Full Application Setup (runs on RT kernel)
216
- # =============================================================================
217
- setup_full() {
218
- banner "Phase 2 — Full Application Setup (PREEMPT_RT kernel confirmed)"
219
-
220
- # ── 2.1 Update package list & install system deps ────────────────────────
221
- step "2.1" "Installing system packages ..."
222
- DEBIAN_FRONTEND=noninteractive apt-get update -q
223
- DEBIAN_FRONTEND=noninteractive apt-get install -y \
224
- git \
225
- python3-venv \
226
- python3-pip \
227
- python3-dev \
228
- samba-client \
229
- cifs-utils \
230
- network-manager \
231
- curl \
232
- build-essential
233
- ok "System packages installed"
234
-
235
- # ── 2.2 Clone or update repo ─────────────────────────────────────────────
236
- step "2.2" "Setting up repository at ${INSTALL_DIR} ..."
142
+ # ── 3. Clone / update repo ────────────────────────────────────────────────────
143
+ setup_repo() {
144
+ step "Setting up repository ..."
237
145
  if [[ -d "${INSTALL_DIR}/.git" ]]; then
238
146
  sudo -u "${PI_USER}" git -C "${INSTALL_DIR}" pull --ff-only origin "${REPO_BRANCH}" \
239
- && ok "Repo updated" || warn "git pull failed — using existing files"
147
+ && ok "Repo updated → ${INSTALL_DIR}" \
148
+ || warn "git pull failed — using existing files"
240
149
  else
241
150
  sudo -u "${PI_USER}" git clone --branch "${REPO_BRANCH}" \
242
151
  "${REPO_URL}" "${INSTALL_DIR}"
243
- ok "Repo cloned ${INSTALL_DIR}"
244
- fi
245
-
246
- # ── 2.3 Python virtual environment ───────────────────────────────────────
247
- step "2.3" "Setting up Python venv at ${VENV_DIR} ..."
248
- if [[ ! -d "${VENV_DIR}" ]]; then
249
- sudo -u "${PI_USER}" python3 -m venv "${VENV_DIR}"
250
- ok "venv created"
251
- else
252
- ok "venv already exists"
152
+ ok "Repo cloned ${INSTALL_DIR}"
253
153
  fi
154
+ }
254
155
 
255
- # Upgrade pip silently, then install pinned packages
256
- sudo -u "${PI_USER}" "${VENV_DIR}/bin/pip" install --quiet --upgrade pip
257
- sudo -u "${PI_USER}" "${VENV_DIR}/bin/pip" install --quiet "${PY_PKGS[@]}"
258
- ok "Python packages installed: ${PY_PKGS[*]}"
156
+ # ── 4. Python venv ────────────────────────────────────────────────────────────
157
+ setup_venv() {
158
+ step "Setting up Python environment ..."
159
+ [[ -d "${VENV_DIR}" ]] \
160
+ && ok "venv exists — skipping creation" \
161
+ || sudo -u "${PI_USER}" python3 -m venv "${VENV_DIR}"
162
+ sudo -u "${PI_USER}" "${VENV_DIR}/bin/pip" install -q --upgrade pip
163
+ sudo -u "${PI_USER}" "${VENV_DIR}/bin/pip" install -q "${PY_PKGS[@]}"
164
+ ok "Packages installed in ${VENV_DIR}"
165
+ }
259
166
 
260
- # ── 2.4 Runtime directories ──────────────────────────────────────────────
261
- step "2.4" "Creating runtime directories ..."
167
+ # ── 5. Directories ────────────────────────────────────────────────────────────
168
+ setup_dirs() {
169
+ step "Creating runtime directories ..."
262
170
  mkdir -p "${REPORTS_DIR}"
263
171
  chown "${PI_USER}:${PI_USER}" "${REPORTS_DIR}"
264
172
  ok "${REPORTS_DIR}"
173
+ }
174
+
175
+ # ── 6. WiFi — interactive scan + pick ────────────────────────────────────────
176
+ setup_wifi() {
177
+ step "WiFi Setup"
265
178
 
266
- # ── 2.5 WiFi ensure saved profile with autoconnect ────────────────────
267
- step "2.5" "Configuring WiFi (SSID: ${WIFI_SSID}) ..."
268
- # Find any existing profile with this SSID
269
- EXISTING_PROFILE="$(nmcli -t -f NAME,TYPE con show | \
270
- awk -F: '$2=="802-11-wireless"{print $1}' | head -1 || true)"
271
-
272
- if nmcli device status | awk '$2=="wifi" && $3=="connected"' | grep -q .; then
273
- ok "WiFi already connected skipping profile creation"
274
- EXISTING_PROFILE="$(nmcli -t -f NAME,DEVICE con show --active | \
275
- grep ":wlan0" | cut -d: -f1 || true)"
276
- elif [[ -n "${EXISTING_PROFILE}" ]]; then
277
- ok "WiFi profile '${EXISTING_PROFILE}' already saved in NetworkManager"
179
+ # Check if wlan0 exists
180
+ if ! ip link show wlan0 &>/dev/null; then
181
+ warn "No wlan0 interface found skipping WiFi setup."
182
+ return
183
+ fi
184
+
185
+ echo ""
186
+ echo -e " ${C}Scanning for nearby networks ...${NC}"
187
+ nmcli dev wifi rescan ifname wlan0 2>/dev/null || true
188
+ sleep 3
189
+
190
+ # Build deduplicated, signal-sorted list [SSID, SIGNAL, SECURITY]
191
+ mapfile -t RAW < <(
192
+ nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list ifname wlan0 2>/dev/null \
193
+ | grep -v '^:' \
194
+ | awk -F: '$1!=""' \
195
+ | sort -t: -k2 -rn \
196
+ | awk -F: '!seen[$1]++'
197
+ )
198
+
199
+ if [[ ${#RAW[@]} -eq 0 ]]; then
200
+ warn "No networks found. Skipping WiFi setup."
201
+ return
202
+ fi
203
+
204
+ # ── Print menu ────────────────────────────────────────────────
205
+ echo ""
206
+ hr
207
+ printf " ${B}%-4s %-28s %-10s %s${NC}\n" "#" "SSID" "Signal" "Security"
208
+ hr
209
+
210
+ declare -a SSIDS SIGNALS SECURITIES
211
+ for i in "${!RAW[@]}"; do
212
+ 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
218
+ SIG="${SIGNAL:-0}"
219
+ 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}"
227
+ done
228
+
229
+ hr
230
+ printf " %-4s %s\n" "0)" "Skip WiFi setup"
231
+ echo ""
232
+
233
+ # ── Read choice ───────────────────────────────────────────────
234
+ while true; do
235
+ read -r -p " Choose network [1-${#RAW[@]}] or 0 to skip: " CHOICE </dev/tty
236
+ [[ "$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}"
239
+ done
240
+
241
+ if [[ "$CHOICE" -eq 0 ]]; then
242
+ warn "WiFi setup skipped."
243
+ return
244
+ fi
245
+
246
+ IDX=$((CHOICE - 1))
247
+ SEL_SSID="${SSIDS[$IDX]}"
248
+ SEL_SEC="${SECURITIES[$IDX]}"
249
+
250
+ echo ""
251
+ ok "Selected: ${SEL_SSID}"
252
+
253
+ # ── Password prompt (skip for open networks) ──────────────────
254
+ WIFI_PASS=""
255
+ if [[ "${SEL_SEC}" != "--" && -n "${SEL_SEC}" ]]; then
256
+ while true; do
257
+ read -r -s -p " Enter WiFi password: " WIFI_PASS </dev/tty
258
+ echo ""
259
+ [[ -n "${WIFI_PASS}" ]] && break
260
+ echo -e " ${R}Password cannot be empty for a secured network.${NC}"
261
+ done
278
262
  else
279
- # Need passworduse WIFI_PASS env var or prompt
280
- if [[ -z "${WIFI_PASS}" ]]; then
281
- if [[ -t 0 ]]; then
282
- read -r -s -p " Enter WiFi password for '${WIFI_SSID}': " WIFI_PASS < /dev/tty
283
- echo ""
284
- else
285
- warn "WiFi password not provided. Set WIFI_PASS= env var or configure manually."
286
- warn "Skipping WiFi profile creation."
287
- WIFI_PASS=""
288
- fi
289
- fi
290
- if [[ -n "${WIFI_PASS}" ]]; then
291
- nmcli connection add \
292
- type wifi \
293
- ifname wlan0 \
294
- con-name "${WIFI_SSID}" \
295
- ssid "${WIFI_SSID}" \
296
- wifi-sec.key-mgmt wpa-psk \
297
- wifi-sec.psk "${WIFI_PASS}" \
298
- connection.autoconnect yes
299
- ok "WiFi profile '${WIFI_SSID}' created"
300
- EXISTING_PROFILE="${WIFI_SSID}"
301
- fi
263
+ info "Open networkno password needed."
302
264
  fi
303
265
 
304
- # Set high autoconnect priority on whichever profile is active
305
- if [[ -n "${EXISTING_PROFILE}" ]]; then
306
- nmcli connection modify "${EXISTING_PROFILE}" \
266
+ # ── Create / update the NM connection profile ─────────────────
267
+ # Remove any existing profile with this SSID to avoid conflicts
268
+ nmcli connection delete "${SEL_SSID}" 2>/dev/null || true
269
+
270
+ 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
279
+ else
280
+ nmcli connection add \
281
+ type wifi ifname wlan0 \
282
+ con-name "${SEL_SSID}" \
283
+ ssid "${SEL_SSID}" \
307
284
  connection.autoconnect yes \
308
- connection.autoconnect-priority 200 2>/dev/null || true
309
- ok "Autoconnect priority set to 200 for '${EXISTING_PROFILE}'"
285
+ connection.autoconnect-priority 200 2>/dev/null
310
286
  fi
311
287
 
312
- # ── 2.6 NetworkManager-wait-online (guarantees IP before services start)
313
- step "2.6" "Enabling NetworkManager-wait-online ..."
288
+ # Connect now so the rest of the setup (git pull, pip) works
289
+ echo -n " Connecting ..."
290
+ if nmcli connection up "${SEL_SSID}" 2>/dev/null; then
291
+ echo ""
292
+ ok "Connected to '${SEL_SSID}'"
293
+ else
294
+ echo ""
295
+ warn "Could not connect now — will auto-connect on reboot."
296
+ fi
297
+ }
298
+
299
+ # ── 7. Network-online guarantee ───────────────────────────────────────────────
300
+ setup_network_online() {
301
+ step "Enabling NetworkManager-wait-online (60 s timeout) ..."
314
302
  mkdir -p /etc/systemd/system/NetworkManager-wait-online.service.d/
315
303
  cat > /etc/systemd/system/NetworkManager-wait-online.service.d/timeout.conf << 'EOF'
316
304
  [Service]
@@ -318,10 +306,14 @@ ExecStart=
318
306
  ExecStart=/usr/lib/NetworkManager/nm-online -s -q --timeout=60
319
307
  EOF
320
308
  systemctl enable NetworkManager-wait-online.service 2>/dev/null || true
321
- ok "NetworkManager-wait-online enabled (60 s timeout)"
309
+ ok "Services will wait up to 60 s for a network connection after boot"
310
+ }
311
+
312
+ # ── 8 & 9. systemd services ───────────────────────────────────────────────────
313
+ install_services() {
314
+ step "Installing systemd services ..."
322
315
 
323
- # ── 2.7 systemd service: plc_watcher (RT) ───────────────────────────────
324
- step "2.7" "Installing plc_watcher.service (SCHED_FIFO:50, IOClass=realtime) ..."
316
+ # plc_watcher SCHED_FIFO:50, IOClass=realtime, pinned to CPU core 3
325
317
  cat > /etc/systemd/system/plc_watcher.service << EOF
326
318
  [Unit]
327
319
  Description=PLC Check-Weigher Start Watcher
@@ -338,31 +330,19 @@ Restart=always
338
330
  RestartSec=5
339
331
  StandardOutput=journal
340
332
  StandardError=journal
341
-
342
- # ── Real-time scheduling ──────────────────────────────
343
- # SCHED_FIFO priority 50 preempts all SCHED_OTHER tasks.
344
- # CPUSchedulingResetOnFork=no (default) means plc_reader.py
345
- # subprocess inherits this RT policy via Popen automatically.
346
333
  CPUSchedulingPolicy=fifo
347
334
  CPUSchedulingPriority=50
348
335
  IOSchedulingClass=realtime
349
336
  IOSchedulingPriority=0
350
- # Pin to core 3 — isolated from general OS scheduling noise
351
337
  CPUAffinity=3
352
338
  Nice=-15
353
339
 
354
340
  [Install]
355
341
  WantedBy=multi-user.target
356
342
  EOF
343
+ ok "plc_watcher.service (SCHED_FIFO:50 · IOClass=realtime · CPU core 3)"
357
344
 
358
- # Mirror the service file back into the repo so git tracks it
359
- cp /etc/systemd/system/plc_watcher.service \
360
- "${INSTALL_DIR}/plc_watcher.service"
361
- chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/plc_watcher.service"
362
- ok "plc_watcher.service installed"
363
-
364
- # ── 2.8 systemd service: plc_web ─────────────────────────────────────────
365
- step "2.8" "Installing plc_web.service ..."
345
+ # plc_web elevated but not real-time
366
346
  cat > /etc/systemd/system/plc_web.service << EOF
367
347
  [Unit]
368
348
  Description=PLC Check-Weigher Report Viewer
@@ -385,106 +365,65 @@ Nice=-10
385
365
  [Install]
386
366
  WantedBy=multi-user.target
387
367
  EOF
388
- ok "plc_web.service installed"
368
+ ok "plc_web.service (Nice=-10)"
389
369
 
390
- # ── 2.9 Enable + start services ──────────────────────────────────────────
391
- step "2.9" "Enabling and starting services ..."
392
370
  systemctl daemon-reload
393
371
  systemctl enable plc_watcher.service plc_web.service
394
- systemctl restart plc_watcher.service || true
395
- sleep 2
396
- systemctl restart plc_web.service || true
397
- ok "Services enabled and started"
398
-
399
- # ── 2.10 Disable continuation service (we're done) ──────────────────────
400
- if systemctl is-enabled "${CONT_SVC}" &>/dev/null 2>&1; then
401
- systemctl disable "${CONT_SVC}" 2>/dev/null || true
402
- rm -f "/etc/systemd/system/${CONT_SVC}"
403
- systemctl daemon-reload
404
- ok "Continuation service cleaned up"
405
- fi
372
+ ok "Both services enabled — will start automatically after reboot"
406
373
 
407
- # ── 2.11 SMB connectivity check ─────────────────────────────────────────
408
- step "2.11" "Verifying SMB share at //${SMB_HOST}/${SMB_SHARE} ..."
409
- if ping -c 2 -W 2 "${SMB_HOST}" &>/dev/null; then
410
- ok "Host ${SMB_HOST} reachable"
411
- if smbclient "//${SMB_HOST}/${SMB_SHARE}" \
412
- -U "${SMB_USER}%${SMB_PASS}" -c "ls" &>/dev/null 2>&1; then
413
- ok "SMB auth success — PDF push is ready"
414
- else
415
- warn "SMB host reachable but auth failed — ensure the share is set up on the host"
416
- fi
417
- else
418
- warn "SMB host ${SMB_HOST} not reachable — will retry at runtime"
419
- fi
420
-
421
- # ── Final verification report ─────────────────────────────────────────────
422
- banner "Setup Complete — Verification"
374
+ # Mirror updated service file back into the repo
375
+ cp /etc/systemd/system/plc_watcher.service "${INSTALL_DIR}/plc_watcher.service"
376
+ chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/plc_watcher.service"
377
+ }
423
378
 
379
+ # ── Summary + countdown reboot ────────────────────────────────────────────────
380
+ do_reboot() {
424
381
  echo ""
425
- echo " Kernel:"
426
- uname -r | sed 's/^/ /'
427
- grep -o 'PREEMPT_RT' /proc/version 2>/dev/null \
428
- && echo -e " ${G}✓ PREEMPT_RT confirmed${NC}" \
429
- || echo -e " ${R}✗ PREEMPT_RT NOT detected${NC}"
430
-
382
+ banner "All Done — Summary"
431
383
  echo ""
432
- echo " Services:"
433
- for svc in plc_watcher plc_web; do
434
- STATE="$(systemctl is-active ${svc}.service 2>/dev/null || echo 'inactive')"
435
- if [[ "${STATE}" == "active" ]]; then
436
- echo -e " ${G}✓${NC} ${svc} (${STATE})"
437
- else
438
- echo -e " ${R}✗${NC} ${svc} (${STATE})"
439
- fi
440
- done
441
384
 
442
- echo ""
443
- echo " RT scheduling (plc_watcher):"
444
- PID="$(systemctl show -p MainPID --value plc_watcher.service 2>/dev/null || echo '')"
445
- if [[ -n "${PID}" && "${PID}" != "0" ]]; then
446
- chrt -p "${PID}" 2>/dev/null | sed 's/^/ /' || true
447
- ionice -p "${PID}" 2>/dev/null | sed 's/^/ /' || true
448
- taskset -cp "${PID}" 2>/dev/null | sed 's/^/ /' || true
449
- else
450
- echo " (PID not yet available)"
451
- fi
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):" \
391
+ "http://$(hostname -I | awk '{print $1}' 2>/dev/null || echo '<pi-ip>'):8080"
452
392
 
453
393
  echo ""
454
- echo " WiFi:"
455
- nmcli -t -f NAME,DEVICE,STATE con show --active | grep wifi | sed 's/^/ /' \
456
- || echo " (no active WiFi)"
457
-
458
- echo ""
459
- echo " Reports directory: ${REPORTS_DIR}"
460
- echo " Web dashboard: http://$(hostname -I | awk '{print $1}'):8080"
461
- echo ""
462
- echo " Useful commands:"
463
- echo " journalctl -u plc_watcher -f # live watcher log"
464
- echo " journalctl -u plc_web -f # live web log"
394
+ echo -e " ${Y}After reboot, check services with:${NC}"
395
+ echo " journalctl -u plc_watcher -f"
465
396
  echo " sudo chrt -p \$(systemctl show -p MainPID --value plc_watcher)"
466
397
  echo ""
467
- }
468
398
 
469
- # =============================================================================
470
- # Entry point — decide which phase to run
471
- # =============================================================================
472
- main() {
473
- banner "PLC Check-Weigher Installer"
474
- echo " Repo : ${REPO_URL}"
475
- echo " User : ${PI_USER}"
476
- echo " Kernel: $(uname -r)"
399
+ echo -e "${G}"
400
+ 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 ║"
404
+ echo " ╚══════════════════════════════════════════════════════╝"
405
+ echo -e "${NC}"
406
+
407
+ for i in $(seq 10 -1 1); do
408
+ printf "\r Rebooting in %2d seconds ... " "$i"
409
+ sleep 1
410
+ done
477
411
  echo ""
412
+ reboot
413
+ }
478
414
 
479
- if is_rt_kernel; then
480
- ok "PREEMPT_RT kernel already running — proceeding directly to phase 2"
481
- setup_full
482
- else
483
- warn "Standard kernel detected ($(uname -r))"
484
- warn "Installing RT kernel — system will reboot once, then auto-complete setup."
485
- install_rt_kernel
486
- # install_rt_kernel reboots — execution never reaches here
487
- fi
415
+ # ── Main ──────────────────────────────────────────────────────────────────────
416
+ main() {
417
+ 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
488
427
  }
489
428
 
490
429
  main "$@"