plc-checkweigher 1.0.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 +327 -348
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-checkweigher",
3
- "version": "1.0.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,57 +1,40 @@
1
1
  #!/usr/bin/env bash
2
2
  # =============================================================================
3
- # PLC Check-Weigher — Full Stack Bootstrap Installer
3
+ # PLC Check-Weigher — Full Stack Installer v1.2
4
4
  # =============================================================================
5
+ # Run on any fresh Raspberry Pi:
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
+ # Steps (everything first, ONE reboot at the very end):
10
+ # 1. Pre-flight checks
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
24
21
  # =============================================================================
25
22
 
26
23
  set -euo pipefail
27
24
  IFS=$'\n\t'
28
25
 
29
- # ── Configurable defaults ─────────────────────────────────────────────────────
26
+ # ── Configurable ──────────────────────────────────────────────────────────────
30
27
  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
28
  REPO_URL="https://github.com/Bibin-VR/plc-checkweigher.git"
38
29
  REPO_BRANCH="main"
39
-
40
- # ── Derived paths ─────────────────────────────────────────────────────────────
41
30
  HOME_DIR="/home/${PI_USER}"
42
31
  INSTALL_DIR="${HOME_DIR}/plc_checkweigher"
43
32
  VENV_DIR="${HOME_DIR}/plc_env"
44
33
  REPORTS_DIR="${HOME_DIR}/reports"
45
34
  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
35
  RT_PKG="linux-image-6.12.86+deb13-rt-arm64"
52
36
  RT_HDR="linux-headers-6.12.86+deb13-rt-arm64"
53
37
 
54
- # ── Python packages (pinned) ──────────────────────────────────────────────────
55
38
  PY_PKGS=(
56
39
  "Flask==3.1.3"
57
40
  "pymcprotocol==0.3.0"
@@ -63,254 +46,250 @@ PY_PKGS=(
63
46
  "scapy==2.7.0"
64
47
  )
65
48
 
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."
49
+ # ── Colours ───────────────────────────────────────────────────────────────────
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
+ }
85
73
 
86
- mkdir -p "${STATE_DIR}"
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
+ }
87
81
 
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; }
82
+ # ── 0. Pre-flight ─────────────────────────────────────────────────────────────
83
+ preflight() {
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."
89
+ info "Host : $(hostname)"
90
+ info "Kernel : $(uname -r)"
91
+ info "User : ${PI_USER}"
92
+ }
92
93
 
93
- install_rt_kernel() {
94
- banner "Phase 1 — Installing PREEMPT_RT Kernel"
94
+ # ── 1. System packages ────────────────────────────────────────────────────────
95
+ install_system_packages() {
96
+ step "System packages ..."
97
+ DEBIAN_FRONTEND=noninteractive apt-get update -qq
98
+ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
99
+ git python3-venv python3-pip python3-dev \
100
+ samba-client cifs-utils network-manager curl build-essential
101
+ ok "git python3-venv samba-client cifs-utils build-essential"
102
+ }
95
103
 
96
- step "1a" "Backing up current kernel image ..."
97
- if [[ ! -f "${BOOT_FW}/kernel8-stock.img" ]]; then
98
- cp "${BOOT_FW}/kernel8.img" "${BOOT_FW}/kernel8-stock.img"
99
- ok "Backup ${BOOT_FW}/kernel8-stock.img"
104
+ # ── 2. Clone / update repo ────────────────────────────────────────────────────
105
+ setup_repo() {
106
+ step "Repository ..."
107
+ if [[ -d "${INSTALL_DIR}/.git" ]]; then
108
+ sudo -u "${PI_USER}" git -C "${INSTALL_DIR}" pull --ff-only origin "${REPO_BRANCH}" \
109
+ && ok "Repo updated → ${INSTALL_DIR}" \
110
+ || warn "git pull failed — using existing files"
100
111
  else
101
- ok "Backup already exists skipping"
112
+ sudo -u "${PI_USER}" git clone --branch "${REPO_BRANCH}" \
113
+ "${REPO_URL}" "${INSTALL_DIR}"
114
+ ok "Repo cloned → ${INSTALL_DIR}"
102
115
  fi
116
+ }
103
117
 
104
- CHKSUM_BEFORE="$(md5sum "${BOOT_FW}/kernel8.img" | cut -d' ' -f1)"
118
+ # ── 3. Python venv ────────────────────────────────────────────────────────────
119
+ setup_venv() {
120
+ step "Python environment ..."
121
+ [[ -d "${VENV_DIR}" ]] \
122
+ && ok "venv exists — skipping creation" \
123
+ || sudo -u "${PI_USER}" python3 -m venv "${VENV_DIR}"
124
+ sudo -u "${PI_USER}" "${VENV_DIR}/bin/pip" install -q --upgrade pip
125
+ sudo -u "${PI_USER}" "${VENV_DIR}/bin/pip" install -q "${PY_PKGS[@]}"
126
+ ok "Packages installed → ${VENV_DIR}"
127
+ }
105
128
 
106
- step "1b" "Installing ${RT_PKG} ..."
107
- DEBIAN_FRONTEND=noninteractive apt-get install -y "${RT_PKG}" "${RT_HDR}"
108
- ok "RT kernel package installed"
129
+ # ── 4. Directories ────────────────────────────────────────────────────────────
130
+ setup_dirs() {
131
+ step "Runtime directories ..."
132
+ mkdir -p "${REPORTS_DIR}"
133
+ chown "${PI_USER}:${PI_USER}" "${REPORTS_DIR}"
134
+ ok "${REPORTS_DIR}"
135
+ }
109
136
 
110
- # Detect whether post-install hook overwrote kernel8.img
111
- CHKSUM_AFTER="$(md5sum "${BOOT_FW}/kernel8.img" | cut -d' ' -f1)"
112
- if [[ "${CHKSUM_BEFORE}" != "${CHKSUM_AFTER}" ]]; then
113
- # Hook replaced kernel8.img → move to kernel8-rt.img, restore stock
114
- cp "${BOOT_FW}/kernel8.img" "${BOOT_FW}/kernel8-rt.img"
115
- cp "${BOOT_FW}/kernel8-stock.img" "${BOOT_FW}/kernel8.img"
116
- ok "RT kernel → kernel8-rt.img | stock kernel restored as default"
117
- else
118
- # Hook did NOT copy — do it manually
119
- 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/"
121
- if file "${RT_VMLINUZ}" | grep -q gzip; then
122
- zcat "${RT_VMLINUZ}" > "${BOOT_FW}/kernel8-rt.img"
123
- else
124
- cp "${RT_VMLINUZ}" "${BOOT_FW}/kernel8-rt.img"
125
- fi
126
- ok "RT kernel manually copied → ${BOOT_FW}/kernel8-rt.img"
137
+ # ── 5. WiFi scan → pick → password ─────────────────────────────────────────
138
+ setup_wifi() {
139
+ step "WiFi Setup"
140
+
141
+ if ! ip link show wlan0 &>/dev/null; then
142
+ warn "No wlan0 found — skipping WiFi setup."
143
+ return
127
144
  fi
128
145
 
129
- # Copy RT initramfs if one was generated
130
- 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"
146
+ echo -e "\n ${C}Scanning for networks ...${NC}"
147
+ nmcli dev wifi rescan ifname wlan0 2>/dev/null || true
148
+ sleep 3
149
+
150
+ mapfile -t RAW < <(
151
+ nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list ifname wlan0 2>/dev/null \
152
+ | grep -v '^:' \
153
+ | awk -F: '$1!=""' \
154
+ | sort -t: -k2 -rn \
155
+ | awk -F: '!seen[$1]++'
156
+ )
157
+
158
+ if [[ ${#RAW[@]} -eq 0 ]]; then
159
+ warn "No networks found — skipping WiFi setup."
160
+ return
134
161
  fi
135
162
 
136
- step "1c" "Activating RT kernel in ${BOOT_FW}/config.txt ..."
137
- # Remove any previous RT block we wrote
138
- sed -i '/### PLC-RT-BLOCK-START ###/,/### PLC-RT-BLOCK-END ###/d' \
139
- "${BOOT_FW}/config.txt"
163
+ echo ""
164
+ hr
165
+ printf " ${B}%-4s %-28s %-10s %s${NC}\n" "#" "SSID" "Signal" "Security"
166
+ hr
167
+ declare -a SSIDS SIGNALS SECURITIES
168
+ for i in "${!RAW[@]}"; do
169
+ IFS=':' read -r SSID SIGNAL SECURITY <<< "${RAW[$i]}"
170
+ SSIDS[$i]="${SSID}"; SIGNALS[$i]="${SIGNAL}"; SECURITIES[$i]="${SECURITY}"
171
+ SIG="${SIGNAL:-0}"
172
+ if [[ $SIG -ge 80 ]]; then BAR="${G}▂▄▆█${NC}"
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:---}"
178
+ done
179
+ hr
180
+ printf " %-4s %s\n" "0)" "Skip WiFi setup"
181
+ echo ""
140
182
 
141
- cat >> "${BOOT_FW}/config.txt" << 'CFGEOF'
183
+ while true; do
184
+ read -r -p " Choose network [1-${#RAW[@]}] or 0 to skip: " CHOICE </dev/tty
185
+ [[ "$CHOICE" =~ ^[0-9]+$ ]] && \
186
+ [[ "$CHOICE" -ge 0 && "$CHOICE" -le "${#RAW[@]}" ]] && break
187
+ echo -e " ${R}Enter a number between 0 and ${#RAW[@]}${NC}"
188
+ done
142
189
 
143
- ### PLC-RT-BLOCK-START ###
144
- # PREEMPT_RT kernel — installed by plc-checkweigher setup.sh
145
- kernel=kernel8-rt.img
146
- initramfs initramfs8-rt followkernel
147
- ### PLC-RT-BLOCK-END ###
148
- CFGEOF
149
- ok "config.txt updated — system will boot RT kernel after reboot"
190
+ [[ "$CHOICE" -eq 0 ]] && { warn "WiFi setup skipped."; return; }
150
191
 
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
192
+ IDX=$((CHOICE - 1))
193
+ SEL_SSID="${SSIDS[$IDX]}"
194
+ SEL_SEC="${SECURITIES[$IDX]}"
195
+
196
+ WIFI_PASS=""
197
+ if [[ "${SEL_SEC}" != "--" && -n "${SEL_SEC}" ]]; then
198
+ while true; do
199
+ prompt_secret WIFI_PASS "WiFi password"
200
+ [[ -n "${WIFI_PASS}" ]] && break
201
+ echo -e " ${R}Password cannot be empty for a secured network.${NC}"
202
+ done
155
203
  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
204
+ info "Open network no password needed."
165
205
  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
206
 
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
212
- }
213
-
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} ..."
237
- if [[ -d "${INSTALL_DIR}/.git" ]]; then
238
- 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"
207
+ nmcli connection delete "${SEL_SSID}" 2>/dev/null || true
208
+ if [[ -n "${WIFI_PASS}" ]]; then
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
240
212
  else
241
- sudo -u "${PI_USER}" git clone --branch "${REPO_BRANCH}" \
242
- "${REPO_URL}" "${INSTALL_DIR}"
243
- ok "Repo cloned → ${INSTALL_DIR}"
213
+ nmcli connection add type wifi ifname wlan0 con-name "${SEL_SSID}" \
214
+ ssid "${SEL_SSID}" connection.autoconnect yes \
215
+ connection.autoconnect-priority 200 2>/dev/null
244
216
  fi
245
217
 
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"
218
+ echo -n " Connecting ..."
219
+ if nmcli connection up "${SEL_SSID}" 2>/dev/null; then
220
+ echo ""; ok "Connected to '${SEL_SSID}'"
251
221
  else
252
- ok "venv already exists"
222
+ echo ""; warn "Will connect automatically after reboot."
253
223
  fi
224
+ }
254
225
 
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[*]}"
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
259
251
 
260
- # ── 2.4 Runtime directories ──────────────────────────────────────────────
261
- step "2.4" "Creating runtime directories ..."
262
- mkdir -p "${REPORTS_DIR}"
263
- chown "${PI_USER}:${PI_USER}" "${REPORTS_DIR}"
264
- ok "${REPORTS_DIR}"
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)" ""
265
256
 
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"
278
- else
279
- # Need password — use 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
302
- fi
257
+ hr
258
+ echo ""
303
259
 
304
- # Set high autoconnect priority on whichever profile is active
305
- if [[ -n "${EXISTING_PROFILE}" ]]; then
306
- nmcli connection modify "${EXISTING_PROFILE}" \
307
- connection.autoconnect yes \
308
- connection.autoconnect-priority 200 2>/dev/null || true
309
- ok "Autoconnect priority set to 200 for '${EXISTING_PROFILE}'"
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
276
+ echo ""
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."
310
287
  fi
288
+ }
311
289
 
312
- # ── 2.6 NetworkManager-wait-online (guarantees IP before services start) ─
313
- step "2.6" "Enabling NetworkManager-wait-online ..."
290
+ # ── 7. Network-online guarantee ───────────────────────────────────────────────
291
+ setup_network_online() {
292
+ step "NetworkManager-wait-online ..."
314
293
  mkdir -p /etc/systemd/system/NetworkManager-wait-online.service.d/
315
294
  cat > /etc/systemd/system/NetworkManager-wait-online.service.d/timeout.conf << 'EOF'
316
295
  [Service]
@@ -318,10 +297,13 @@ ExecStart=
318
297
  ExecStart=/usr/lib/NetworkManager/nm-online -s -q --timeout=60
319
298
  EOF
320
299
  systemctl enable NetworkManager-wait-online.service 2>/dev/null || true
321
- ok "NetworkManager-wait-online enabled (60 s timeout)"
300
+ ok "Services will wait up to 60 s for network on each boot"
301
+ }
302
+
303
+ # ── 8. systemd services ───────────────────────────────────────────────────────
304
+ install_services() {
305
+ step "systemd services ..."
322
306
 
323
- # ── 2.7 systemd service: plc_watcher (RT) ───────────────────────────────
324
- step "2.7" "Installing plc_watcher.service (SCHED_FIFO:50, IOClass=realtime) ..."
325
307
  cat > /etc/systemd/system/plc_watcher.service << EOF
326
308
  [Unit]
327
309
  Description=PLC Check-Weigher Start Watcher
@@ -338,31 +320,18 @@ Restart=always
338
320
  RestartSec=5
339
321
  StandardOutput=journal
340
322
  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
323
  CPUSchedulingPolicy=fifo
347
324
  CPUSchedulingPriority=50
348
325
  IOSchedulingClass=realtime
349
326
  IOSchedulingPriority=0
350
- # Pin to core 3 — isolated from general OS scheduling noise
351
327
  CPUAffinity=3
352
328
  Nice=-15
353
329
 
354
330
  [Install]
355
331
  WantedBy=multi-user.target
356
332
  EOF
333
+ ok "plc_watcher.service (SCHED_FIFO:50 · IOClass=realtime · CPU core 3)"
357
334
 
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 ..."
366
335
  cat > /etc/systemd/system/plc_web.service << EOF
367
336
  [Unit]
368
337
  Description=PLC Check-Weigher Report Viewer
@@ -385,106 +354,116 @@ Nice=-10
385
354
  [Install]
386
355
  WantedBy=multi-user.target
387
356
  EOF
388
- ok "plc_web.service installed"
357
+ ok "plc_web.service (Nice=-10)"
389
358
 
390
- # ── 2.9 Enable + start services ──────────────────────────────────────────
391
- step "2.9" "Enabling and starting services ..."
392
359
  systemctl daemon-reload
393
360
  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"
361
+ ok "Both services enabled — start automatically after reboot"
362
+
363
+ cp /etc/systemd/system/plc_watcher.service "${INSTALL_DIR}/plc_watcher.service"
364
+ chown "${PI_USER}:${PI_USER}" "${INSTALL_DIR}/plc_watcher.service"
365
+ }
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
405
374
  fi
406
375
 
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"
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"
414
395
  else
415
- warn "SMB host reachable but auth failed — ensure the share is set up on the host"
396
+ cp "${RT_VMLINUZ}" "${BOOT_FW}/kernel8-rt.img"
416
397
  fi
417
- else
418
- warn "SMB host ${SMB_HOST} not reachable — will retry at runtime"
398
+ ok "RT kernel manually copied → kernel8-rt.img"
419
399
  fi
420
400
 
421
- # ── Final verification report ─────────────────────────────────────────────
422
- banner "Setup Complete Verification"
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"
423
404
 
424
- 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}"
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'
430
409
 
431
- 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
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
+ }
441
419
 
420
+ # ── Summary + countdown reboot ────────────────────────────────────────────────
421
+ do_reboot() {
442
422
  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
452
-
423
+ banner "Setup Complete"
453
424
  echo ""
454
- echo " WiFi:"
455
- nmcli -t -f NAME,DEVICE,STATE con show --active | grep wifi | sed 's/^/ /' \
456
- || echo " (no active WiFi)"
457
-
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):" \
431
+ "http://$(hostname -I | awk '{print $1}' 2>/dev/null || echo '<pi-ip>'):8080"
458
432
  echo ""
459
- echo " Reports directory: ${REPORTS_DIR}"
460
- echo " Web dashboard: http://$(hostname -I | awk '{print $1}'):8080"
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"
461
436
  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"
465
- echo " sudo chrt -p \$(systemctl show -p MainPID --value plc_watcher)"
437
+
438
+ echo -e "${G}"
439
+ echo " ╔══════════════════════════════════════════════════════╗"
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 ║"
443
+ echo " ╚══════════════════════════════════════════════════════╝"
444
+ echo -e "${NC}"
445
+
446
+ for i in $(seq 10 -1 1); do
447
+ printf "\r Rebooting in %2d seconds ... " "$i"
448
+ sleep 1
449
+ done
466
450
  echo ""
451
+ reboot
467
452
  }
468
453
 
469
- # =============================================================================
470
- # Entry point — decide which phase to run
471
- # =============================================================================
454
+ # ── Main ──────────────────────────────────────────────────────────────────────
472
455
  main() {
473
- banner "PLC Check-Weigher Installer"
474
- echo " Repo : ${REPO_URL}"
475
- echo " User : ${PI_USER}"
476
- echo " Kernel: $(uname -r)"
477
- echo ""
478
-
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
456
+ preflight
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
488
467
  }
489
468
 
490
469
  main "$@"