plc-checkweigher 1.0.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/README.md +71 -0
- package/bin/cli.js +64 -0
- package/package.json +31 -0
- package/setup.sh +490 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# PLC Check-Weigher System
|
|
2
|
+
|
|
3
|
+
> **Hardware:** Raspberry Pi 4B · Mitsubishi PLC (Type3E) · Check-weigher line
|
|
4
|
+
> **Author:** Bibin VR
|
|
5
|
+
|
|
6
|
+
Real-time check-weigher data logger and report system for a Mitsubishi PLC production line. Monitors each item, logs weight and status, generates PDF batch reports, and instantly pushes them to a network PC via SMB.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Live PLC polling at 50 ms — captures every item (accept/reject/weight)
|
|
11
|
+
- PDF batch report generated automatically at end of each production run
|
|
12
|
+
- Instant PDF push to Windows/Mac shared folder via SMB (no software needed on the receiving PC)
|
|
13
|
+
- Live operations dashboard — weight gauge, batch stats, item feed in real time
|
|
14
|
+
- PDF report viewer with live auto-refresh (new reports appear without page reload)
|
|
15
|
+
- Systemd service — starts at boot, reconnects on PLC disconnect
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Install dependencies
|
|
21
|
+
python3 -m venv /home/pi/plc_env
|
|
22
|
+
source /home/pi/plc_env/bin/activate
|
|
23
|
+
pip install pymcprotocol flask reportlab
|
|
24
|
+
sudo apt install samba-client
|
|
25
|
+
|
|
26
|
+
# Start watcher (or install as systemd service — see procedure.md)
|
|
27
|
+
cd /home/pi/plc_checkweigher
|
|
28
|
+
python3 plc_watcher.py
|
|
29
|
+
|
|
30
|
+
# Start web interface
|
|
31
|
+
python3 web/app.py
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Open `http://<pi-ip>:8080` for the report viewer, `/live` for the dashboard.
|
|
35
|
+
|
|
36
|
+
## PDF Push Setup
|
|
37
|
+
|
|
38
|
+
See **[procedure.md](procedure.md)** for full setup instructions including:
|
|
39
|
+
- Windows local user creation (avoids Microsoft account credential issues)
|
|
40
|
+
- macOS File Sharing configuration
|
|
41
|
+
- Email delivery via Gmail
|
|
42
|
+
- HTTP push using `pdf_receiver.py`
|
|
43
|
+
|
|
44
|
+
## Project Layout
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
plc_checkweigher/
|
|
48
|
+
├── plc_watcher.py # systemd entry — waits for PLC START
|
|
49
|
+
├── plc_reader.py # per-item data collection + PDF trigger
|
|
50
|
+
├── plc_report.py # PDF generation (ReportLab)
|
|
51
|
+
├── pdf_push.py # instant PDF delivery to network PC
|
|
52
|
+
├── pdf_receiver.py # optional HTTP receiver for target PC
|
|
53
|
+
├── plc_watcher.service # systemd unit
|
|
54
|
+
├── procedure.md # full setup & operating procedure
|
|
55
|
+
└── web/
|
|
56
|
+
├── app.py # Flask server (port 8080)
|
|
57
|
+
└── templates/
|
|
58
|
+
├── index.html # report list with live SSE refresh
|
|
59
|
+
└── live.html # live operations dashboard
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Edit the top of each file:
|
|
65
|
+
|
|
66
|
+
| File | Key settings |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `plc_reader.py` | `PLC_IP`, `PLC_PORT` |
|
|
69
|
+
| `plc_watcher.py` | `PLC_IP`, `PLC_PORT` |
|
|
70
|
+
| `pdf_push.py` | `SMB_HOST`, `SMB_SHARE`, `SMB_USERNAME`, `SMB_PASSWORD` |
|
|
71
|
+
| `web/app.py` | `REPORTS_DIR`, `PORT` |
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const { platform, arch } = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
// ── Colour helpers ────────────────────────────────────────────────────────────
|
|
10
|
+
const B = '\x1b[1;34m'; // bold blue
|
|
11
|
+
const G = '\x1b[0;32m'; // green
|
|
12
|
+
const R = '\x1b[1;31m'; // red
|
|
13
|
+
const Y = '\x1b[1;33m'; // yellow
|
|
14
|
+
const NC = '\x1b[0m';
|
|
15
|
+
|
|
16
|
+
function die(msg) {
|
|
17
|
+
console.error(`\n${R}Error:${NC} ${msg}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── Platform guards ───────────────────────────────────────────────────────────
|
|
22
|
+
if (platform() !== 'linux') {
|
|
23
|
+
die('This installer only runs on Raspberry Pi (Linux). Got: ' + platform());
|
|
24
|
+
}
|
|
25
|
+
if (arch() !== 'arm64') {
|
|
26
|
+
die('Requires 64-bit ARM (arm64). Got: ' + arch() +
|
|
27
|
+
'\nMake sure you are running 64-bit Raspberry Pi OS.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Banner ────────────────────────────────────────────────────────────────────
|
|
31
|
+
console.log(`
|
|
32
|
+
${B}╔════════════════════════════════════════════╗
|
|
33
|
+
║ PLC Check-Weigher — Full Stack Installer ║
|
|
34
|
+
╚════════════════════════════════════════════╝${NC}
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
// ── Locate setup.sh (bundled inside this npm package) ────────────────────────
|
|
38
|
+
const setupScript = path.resolve(__dirname, '..', 'setup.sh');
|
|
39
|
+
if (!fs.existsSync(setupScript)) {
|
|
40
|
+
die('setup.sh not found inside the package — try: npm install -g plc-checkweigher@latest');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Inform the user ───────────────────────────────────────────────────────────
|
|
44
|
+
console.log(`${Y}This will:${NC}`);
|
|
45
|
+
console.log(' 1. Install the PREEMPT_RT real-time kernel (reboots once)');
|
|
46
|
+
console.log(' 2. Install all Python dependencies');
|
|
47
|
+
console.log(' 3. Clone / update the plc-checkweigher repo');
|
|
48
|
+
console.log(' 4. Configure WiFi, SMB file sharing');
|
|
49
|
+
console.log(' 5. Install systemd services with RT scheduling priority');
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(`${Y}Sudo password required to make system-level changes.${NC}`);
|
|
52
|
+
console.log('');
|
|
53
|
+
|
|
54
|
+
// ── Run setup.sh via sudo ─────────────────────────────────────────────────────
|
|
55
|
+
// stdio:'inherit' keeps the terminal fully interactive:
|
|
56
|
+
// • sudo password prompt works
|
|
57
|
+
// • WiFi password prompt works
|
|
58
|
+
// • All colour output passes through
|
|
59
|
+
const result = spawnSync('sudo', ['bash', setupScript], {
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
env: process.env,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
process.exit(result.status ?? 0);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plc-checkweigher",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"bin": {
|
|
6
|
+
"plc-checkweigher": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"setup.sh"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"plc",
|
|
14
|
+
"checkweigher",
|
|
15
|
+
"raspberry-pi",
|
|
16
|
+
"installer",
|
|
17
|
+
"preempt-rt",
|
|
18
|
+
"real-time",
|
|
19
|
+
"mitsubishi",
|
|
20
|
+
"iot"
|
|
21
|
+
],
|
|
22
|
+
"author": "Bibin-VR",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=16"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/Bibin-VR/plc-checkweigher.git"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/setup.sh
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# PLC Check-Weigher — Full Stack Bootstrap Installer
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# One-liner (run this on any fresh Raspberry Pi):
|
|
7
|
+
#
|
|
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 ...)"
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
IFS=$'\n\t'
|
|
28
|
+
|
|
29
|
+
# ── Configurable defaults ─────────────────────────────────────────────────────
|
|
30
|
+
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
|
+
REPO_URL="https://github.com/Bibin-VR/plc-checkweigher.git"
|
|
38
|
+
REPO_BRANCH="main"
|
|
39
|
+
|
|
40
|
+
# ── Derived paths ─────────────────────────────────────────────────────────────
|
|
41
|
+
HOME_DIR="/home/${PI_USER}"
|
|
42
|
+
INSTALL_DIR="${HOME_DIR}/plc_checkweigher"
|
|
43
|
+
VENV_DIR="${HOME_DIR}/plc_env"
|
|
44
|
+
REPORTS_DIR="${HOME_DIR}/reports"
|
|
45
|
+
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
|
+
RT_PKG="linux-image-6.12.86+deb13-rt-arm64"
|
|
52
|
+
RT_HDR="linux-headers-6.12.86+deb13-rt-arm64"
|
|
53
|
+
|
|
54
|
+
# ── Python packages (pinned) ──────────────────────────────────────────────────
|
|
55
|
+
PY_PKGS=(
|
|
56
|
+
"Flask==3.1.3"
|
|
57
|
+
"pymcprotocol==0.3.0"
|
|
58
|
+
"reportlab==4.5.1"
|
|
59
|
+
"pillow==12.2.0"
|
|
60
|
+
"pyserial==3.5"
|
|
61
|
+
"pymodbus==2.5.3"
|
|
62
|
+
"websockets==16.0"
|
|
63
|
+
"scapy==2.7.0"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# ── Terminal colours ──────────────────────────────────────────────────────────
|
|
67
|
+
R='\033[1;31m'; G='\033[0;32m'; B='\033[1;34m'; Y='\033[1;33m'; NC='\033[0m'
|
|
68
|
+
banner() { echo -e "\n${B}══════════════════════════════════════════════${NC}"; \
|
|
69
|
+
echo -e "${B} $*${NC}"; \
|
|
70
|
+
echo -e "${B}══════════════════════════════════════════════${NC}"; }
|
|
71
|
+
step() { echo -e "\n${Y}[${1}]${NC} ${2}"; }
|
|
72
|
+
ok() { echo -e " ${G}✓${NC} ${1}"; }
|
|
73
|
+
warn() { echo -e " ${Y}!${NC} ${1}"; }
|
|
74
|
+
die() { echo -e "\n${R}FATAL:${NC} ${1}" >&2; exit 1; }
|
|
75
|
+
|
|
76
|
+
# ── Guard: must be root ───────────────────────────────────────────────────────
|
|
77
|
+
[[ "${EUID}" -eq 0 ]] || die "Run with sudo: sudo bash -c \"\$(curl -sSL ...)\" "
|
|
78
|
+
|
|
79
|
+
# ── Guard: must be aarch64 ────────────────────────────────────────────────────
|
|
80
|
+
ARCH="$(uname -m)"
|
|
81
|
+
[[ "${ARCH}" == "aarch64" ]] || die "This installer targets 64-bit Raspberry Pi (aarch64). Got: ${ARCH}"
|
|
82
|
+
|
|
83
|
+
# ── Guard: home dir must exist ────────────────────────────────────────────────
|
|
84
|
+
[[ -d "${HOME_DIR}" ]] || die "User home ${HOME_DIR} not found. Set PI_USER= before running."
|
|
85
|
+
|
|
86
|
+
mkdir -p "${STATE_DIR}"
|
|
87
|
+
|
|
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; }
|
|
92
|
+
|
|
93
|
+
install_rt_kernel() {
|
|
94
|
+
banner "Phase 1 — Installing PREEMPT_RT Kernel"
|
|
95
|
+
|
|
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"
|
|
100
|
+
else
|
|
101
|
+
ok "Backup already exists — skipping"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
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
|
|
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"
|
|
127
|
+
fi
|
|
128
|
+
|
|
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"
|
|
134
|
+
fi
|
|
135
|
+
|
|
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"
|
|
140
|
+
|
|
141
|
+
cat >> "${BOOT_FW}/config.txt" << 'CFGEOF'
|
|
142
|
+
|
|
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"
|
|
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
|
|
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"
|
|
240
|
+
else
|
|
241
|
+
sudo -u "${PI_USER}" git clone --branch "${REPO_BRANCH}" \
|
|
242
|
+
"${REPO_URL}" "${INSTALL_DIR}"
|
|
243
|
+
ok "Repo cloned → ${INSTALL_DIR}"
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# ── 2.3 Python virtual environment ───────────────────────────────────────
|
|
247
|
+
step "2.3" "Setting up Python venv at ${VENV_DIR} ..."
|
|
248
|
+
if [[ ! -d "${VENV_DIR}" ]]; then
|
|
249
|
+
sudo -u "${PI_USER}" python3 -m venv "${VENV_DIR}"
|
|
250
|
+
ok "venv created"
|
|
251
|
+
else
|
|
252
|
+
ok "venv already exists"
|
|
253
|
+
fi
|
|
254
|
+
|
|
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[*]}"
|
|
259
|
+
|
|
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}"
|
|
265
|
+
|
|
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
|
|
303
|
+
|
|
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}'"
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
# ── 2.6 NetworkManager-wait-online (guarantees IP before services start) ─
|
|
313
|
+
step "2.6" "Enabling NetworkManager-wait-online ..."
|
|
314
|
+
mkdir -p /etc/systemd/system/NetworkManager-wait-online.service.d/
|
|
315
|
+
cat > /etc/systemd/system/NetworkManager-wait-online.service.d/timeout.conf << 'EOF'
|
|
316
|
+
[Service]
|
|
317
|
+
ExecStart=
|
|
318
|
+
ExecStart=/usr/lib/NetworkManager/nm-online -s -q --timeout=60
|
|
319
|
+
EOF
|
|
320
|
+
systemctl enable NetworkManager-wait-online.service 2>/dev/null || true
|
|
321
|
+
ok "NetworkManager-wait-online enabled (60 s timeout)"
|
|
322
|
+
|
|
323
|
+
# ── 2.7 systemd service: plc_watcher (RT) ───────────────────────────────
|
|
324
|
+
step "2.7" "Installing plc_watcher.service (SCHED_FIFO:50, IOClass=realtime) ..."
|
|
325
|
+
cat > /etc/systemd/system/plc_watcher.service << EOF
|
|
326
|
+
[Unit]
|
|
327
|
+
Description=PLC Check-Weigher Start Watcher
|
|
328
|
+
After=network-online.target time-sync.target
|
|
329
|
+
Wants=network-online.target
|
|
330
|
+
|
|
331
|
+
[Service]
|
|
332
|
+
Type=simple
|
|
333
|
+
User=${PI_USER}
|
|
334
|
+
WorkingDirectory=${INSTALL_DIR}
|
|
335
|
+
Environment=PYTHONUNBUFFERED=1
|
|
336
|
+
ExecStart=${VENV_DIR}/bin/python3 -u ${INSTALL_DIR}/plc_watcher.py
|
|
337
|
+
Restart=always
|
|
338
|
+
RestartSec=5
|
|
339
|
+
StandardOutput=journal
|
|
340
|
+
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
|
+
CPUSchedulingPolicy=fifo
|
|
347
|
+
CPUSchedulingPriority=50
|
|
348
|
+
IOSchedulingClass=realtime
|
|
349
|
+
IOSchedulingPriority=0
|
|
350
|
+
# Pin to core 3 — isolated from general OS scheduling noise
|
|
351
|
+
CPUAffinity=3
|
|
352
|
+
Nice=-15
|
|
353
|
+
|
|
354
|
+
[Install]
|
|
355
|
+
WantedBy=multi-user.target
|
|
356
|
+
EOF
|
|
357
|
+
|
|
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
|
+
cat > /etc/systemd/system/plc_web.service << EOF
|
|
367
|
+
[Unit]
|
|
368
|
+
Description=PLC Check-Weigher Report Viewer
|
|
369
|
+
After=network-online.target plc_watcher.service
|
|
370
|
+
Wants=network-online.target
|
|
371
|
+
BindsTo=plc_watcher.service
|
|
372
|
+
|
|
373
|
+
[Service]
|
|
374
|
+
Type=simple
|
|
375
|
+
User=${PI_USER}
|
|
376
|
+
WorkingDirectory=${INSTALL_DIR}/web
|
|
377
|
+
Environment=PYTHONUNBUFFERED=1
|
|
378
|
+
ExecStart=${VENV_DIR}/bin/python3 -u ${INSTALL_DIR}/web/app.py
|
|
379
|
+
Restart=always
|
|
380
|
+
RestartSec=5
|
|
381
|
+
StandardOutput=journal
|
|
382
|
+
StandardError=journal
|
|
383
|
+
Nice=-10
|
|
384
|
+
|
|
385
|
+
[Install]
|
|
386
|
+
WantedBy=multi-user.target
|
|
387
|
+
EOF
|
|
388
|
+
ok "plc_web.service installed"
|
|
389
|
+
|
|
390
|
+
# ── 2.9 Enable + start services ──────────────────────────────────────────
|
|
391
|
+
step "2.9" "Enabling and starting services ..."
|
|
392
|
+
systemctl daemon-reload
|
|
393
|
+
systemctl enable plc_watcher.service plc_web.service
|
|
394
|
+
systemctl restart plc_watcher.service || true
|
|
395
|
+
sleep 2
|
|
396
|
+
systemctl restart plc_web.service || true
|
|
397
|
+
ok "Services enabled and started"
|
|
398
|
+
|
|
399
|
+
# ── 2.10 Disable continuation service (we're done) ──────────────────────
|
|
400
|
+
if systemctl is-enabled "${CONT_SVC}" &>/dev/null 2>&1; then
|
|
401
|
+
systemctl disable "${CONT_SVC}" 2>/dev/null || true
|
|
402
|
+
rm -f "/etc/systemd/system/${CONT_SVC}"
|
|
403
|
+
systemctl daemon-reload
|
|
404
|
+
ok "Continuation service cleaned up"
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# ── 2.11 SMB connectivity check ─────────────────────────────────────────
|
|
408
|
+
step "2.11" "Verifying SMB share at //${SMB_HOST}/${SMB_SHARE} ..."
|
|
409
|
+
if ping -c 2 -W 2 "${SMB_HOST}" &>/dev/null; then
|
|
410
|
+
ok "Host ${SMB_HOST} reachable"
|
|
411
|
+
if smbclient "//${SMB_HOST}/${SMB_SHARE}" \
|
|
412
|
+
-U "${SMB_USER}%${SMB_PASS}" -c "ls" &>/dev/null 2>&1; then
|
|
413
|
+
ok "SMB auth success — PDF push is ready"
|
|
414
|
+
else
|
|
415
|
+
warn "SMB host reachable but auth failed — ensure the share is set up on the host"
|
|
416
|
+
fi
|
|
417
|
+
else
|
|
418
|
+
warn "SMB host ${SMB_HOST} not reachable — will retry at runtime"
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
# ── Final verification report ─────────────────────────────────────────────
|
|
422
|
+
banner "Setup Complete — Verification"
|
|
423
|
+
|
|
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}"
|
|
430
|
+
|
|
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
|
|
441
|
+
|
|
442
|
+
echo ""
|
|
443
|
+
echo " RT scheduling (plc_watcher):"
|
|
444
|
+
PID="$(systemctl show -p MainPID --value plc_watcher.service 2>/dev/null || echo '')"
|
|
445
|
+
if [[ -n "${PID}" && "${PID}" != "0" ]]; then
|
|
446
|
+
chrt -p "${PID}" 2>/dev/null | sed 's/^/ /' || true
|
|
447
|
+
ionice -p "${PID}" 2>/dev/null | sed 's/^/ /' || true
|
|
448
|
+
taskset -cp "${PID}" 2>/dev/null | sed 's/^/ /' || true
|
|
449
|
+
else
|
|
450
|
+
echo " (PID not yet available)"
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
echo ""
|
|
454
|
+
echo " WiFi:"
|
|
455
|
+
nmcli -t -f NAME,DEVICE,STATE con show --active | grep wifi | sed 's/^/ /' \
|
|
456
|
+
|| echo " (no active WiFi)"
|
|
457
|
+
|
|
458
|
+
echo ""
|
|
459
|
+
echo " Reports directory: ${REPORTS_DIR}"
|
|
460
|
+
echo " Web dashboard: http://$(hostname -I | awk '{print $1}'):8080"
|
|
461
|
+
echo ""
|
|
462
|
+
echo " Useful commands:"
|
|
463
|
+
echo " journalctl -u plc_watcher -f # live watcher log"
|
|
464
|
+
echo " journalctl -u plc_web -f # live web log"
|
|
465
|
+
echo " sudo chrt -p \$(systemctl show -p MainPID --value plc_watcher)"
|
|
466
|
+
echo ""
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# =============================================================================
|
|
470
|
+
# Entry point — decide which phase to run
|
|
471
|
+
# =============================================================================
|
|
472
|
+
main() {
|
|
473
|
+
banner "PLC Check-Weigher Installer"
|
|
474
|
+
echo " Repo : ${REPO_URL}"
|
|
475
|
+
echo " User : ${PI_USER}"
|
|
476
|
+
echo " Kernel: $(uname -r)"
|
|
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
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
main "$@"
|