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.
- package/package.json +1 -1
- package/setup.sh +270 -331
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plc-checkweigher",
|
|
3
|
-
"version": "1.
|
|
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
|
|
3
|
+
# PLC Check-Weigher — Full Stack Installer v1.1
|
|
4
4
|
# =============================================================================
|
|
5
|
+
# Run on any fresh Raspberry Pi with ONE command:
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
+
# npx plc-checkweigher
|
|
7
8
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
|
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
|
-
# ──
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
echo -e "${B}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# ──
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
[[ -
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
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 "
|
|
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 →
|
|
119
|
+
ok "RT kernel manually copied → kernel8-rt.img"
|
|
127
120
|
fi
|
|
128
121
|
|
|
129
|
-
# Copy RT initramfs if
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
ok "config.txt updated —
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
#
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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 network — no password needed."
|
|
302
264
|
fi
|
|
303
265
|
|
|
304
|
-
#
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
309
|
-
ok "Autoconnect priority set to 200 for '${EXISTING_PROFILE}'"
|
|
285
|
+
connection.autoconnect-priority 200 2>/dev/null
|
|
310
286
|
fi
|
|
311
287
|
|
|
312
|
-
#
|
|
313
|
-
|
|
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 "
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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 "
|
|
455
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
echo "
|
|
475
|
-
echo "
|
|
476
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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 "$@"
|