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.
- package/package.json +1 -1
- package/setup.sh +327 -348
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plc-checkweigher",
|
|
3
|
-
"version": "1.
|
|
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
|
|
3
|
+
# PLC Check-Weigher — Full Stack Installer v1.2
|
|
4
4
|
# =============================================================================
|
|
5
|
+
# Run on any fresh Raspberry Pi:
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
+
# npx plc-checkweigher
|
|
7
8
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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
|
|
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
|
-
# ──
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
[[ -
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
242
|
-
"${
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
222
|
+
echo ""; warn "Will connect automatically after reboot."
|
|
253
223
|
fi
|
|
224
|
+
}
|
|
254
225
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
#
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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 "
|
|
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
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
#
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
ok "
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
396
|
+
cp "${RT_VMLINUZ}" "${BOOT_FW}/kernel8-rt.img"
|
|
416
397
|
fi
|
|
417
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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 "
|
|
460
|
-
echo "
|
|
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
|
-
|
|
463
|
-
echo
|
|
464
|
-
echo "
|
|
465
|
-
echo "
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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 "$@"
|