-
-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·513 lines (432 loc) · 20 KB
/
install.sh
File metadata and controls
executable file
·513 lines (432 loc) · 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
#!/usr/bin/env bash
#
# Meshpoint Installer
#
# Prepares a fresh Raspberry Pi for Meshpoint operation:
# 1. System packages and build tools
# 2. SPI / UART / GPS kernel config
# 3. SX1302 HAL (libloragw) compilation
# 4. Python virtual-env and pip dependencies
# 5. systemd service installation
#
# Usage:
# sudo ./scripts/install.sh
#
# After completion, reboot then run: meshpoint setup
#
set -euo pipefail
MESHPOINT_DIR="/opt/meshpoint"
HAL_BUILD_DIR="/opt/sx1302_hal"
BOOT_CONFIG="/boot/firmware/config.txt"
SERVICE_FILE="scripts/meshpoint.service"
WATCHDOG_SERVICE_FILE="scripts/network-watchdog.service"
CLI_SCRIPT="scripts/meshpoint"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
fail() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; }
# ── Pre-flight checks ──────────────────────────────────────────────
if [[ $EUID -ne 0 ]]; then
fail "This script must be run as root. Use: sudo ./scripts/install.sh"
fi
if ! grep -qi "raspberry\|raspbian\|debian" /etc/os-release 2>/dev/null; then
warn "This doesn't look like Raspberry Pi OS. Proceeding anyway."
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
info "Source directory: ${SCRIPT_DIR}"
# Detect upgrade vs fresh install for the post-install banner.
# An existing local.yaml or an enabled meshpoint service is the
# clearest signal that the previous install completed at least once.
IS_UPGRADE=0
if [ -f "${MESHPOINT_DIR}/config/local.yaml" ] \
|| systemctl is-enabled meshpoint &>/dev/null; then
IS_UPGRADE=1
info "Existing installation detected: running in upgrade mode"
fi
# Read the version we're installing for the post-install banner.
INSTALL_VERSION="$(
grep -oP '__version__ = "\K[^"]+' "${SCRIPT_DIR}/src/version.py" \
2>/dev/null || echo "unknown"
)"
# ── 1. System packages ─────────────────────────────────────────────
info "Updating system packages..."
apt-get update -qq
apt-get upgrade -y -qq
info "Installing build tools and dependencies..."
apt-get install -y -qq \
build-essential \
git \
python3 \
python3-venv \
python3-pip \
libsqlite3-dev \
i2c-tools
# ── 2. Enable SPI ──────────────────────────────────────────────────
info "Enabling SPI interface..."
raspi-config nonint do_spi 0 2>/dev/null || warn "raspi-config SPI failed (may already be enabled)"
# ── 2b. Enable I2C ────────────────────────────────────────────────
info "Enabling I2C interface..."
raspi-config nonint do_i2c 0 2>/dev/null || warn "raspi-config I2C failed (may already be enabled)"
# ── 3. Enable UART for GPS ─────────────────────────────────────────
info "Enabling UART hardware..."
raspi-config nonint do_serial_hw 0 2>/dev/null || warn "raspi-config UART failed"
info "Disabling serial console (needed for GPS on /dev/ttyAMA0)..."
raspi-config nonint do_serial_cons 1 2>/dev/null || warn "raspi-config serial console failed"
# Disable Bluetooth on primary UART so GPS gets /dev/ttyAMA0
if [ -f "$BOOT_CONFIG" ]; then
if ! grep -q "dtoverlay=disable-bt" "$BOOT_CONFIG"; then
info "Adding dtoverlay=disable-bt to ${BOOT_CONFIG}"
echo "" >> "$BOOT_CONFIG"
echo "# Meshpoint: free primary UART for GPS" >> "$BOOT_CONFIG"
echo "dtoverlay=disable-bt" >> "$BOOT_CONFIG"
else
info "dtoverlay=disable-bt already present"
fi
fi
# ── 4. Build SX1302 HAL ───────────────────────────────────────────
if [ -f "/usr/local/lib/libloragw.so" ]; then
info "libloragw.so already installed, skipping HAL build"
else
info "Cloning SX1302 HAL..."
rm -rf "$HAL_BUILD_DIR"
git clone --depth 1 https://github.com/Lora-net/sx1302_hal.git "$HAL_BUILD_DIR"
info "Configuring HAL source..."
python3 - "${HAL_BUILD_DIR}/libloragw/src/loragw_sx1302.c" \
"${HAL_BUILD_DIR}/libloragw/src/loragw_hal.c" <<'_HALCFG'
import sys
from pathlib import Path
def _rd(p):
f = Path(p)
if not f.is_file():
print("FAIL: " + p); sys.exit(1)
return f, f.read_text().replace("\r\n", "\n")
f1, s1 = _rd(sys.argv[1])
f2, s2 = _rd(sys.argv[2])
_A = """\
int err = LGW_REG_SUCCESS;
/* Multi-SF modem configuration */
DEBUG_MSG("INFO: configuring LoRa (Multi-SF) SF5->SF6 with syncword PRIVATE (0x12)\\n");
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF5_PEAK1_POS_SF5, 2);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH1_SF5_PEAK2_POS_SF5, 4);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF6_PEAK1_POS_SF6, 2);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH1_SF6_PEAK2_POS_SF6, 4);
if (public == true) {
DEBUG_MSG("INFO: configuring LoRa (Multi-SF) SF7->SF12 with syncword PUBLIC (0x34)\\n");
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF7TO12_PEAK1_POS_SF7TO12, 6);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH1_SF7TO12_PEAK2_POS_SF7TO12, 8);
} else {
DEBUG_MSG("INFO: configuring LoRa (Multi-SF) SF7->SF12 with syncword PRIVATE (0x12)\\n");
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF7TO12_PEAK1_POS_SF7TO12, 2);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH1_SF7TO12_PEAK2_POS_SF7TO12, 4);
}
/* LoRa Service modem configuration */
if ((public == false) || (lora_service_sf == DR_LORA_SF5) || (lora_service_sf == DR_LORA_SF6)) {
DEBUG_PRINTF("INFO: configuring LoRa (Service) SF%u with syncword PRIVATE (0x12)\\n", lora_service_sf);
err |= lgw_reg_w(SX1302_REG_RX_TOP_LORA_SERVICE_FSK_FRAME_SYNCH0_PEAK1_POS, 2);
err |= lgw_reg_w(SX1302_REG_RX_TOP_LORA_SERVICE_FSK_FRAME_SYNCH1_PEAK2_POS, 4);
} else {
DEBUG_PRINTF("INFO: configuring LoRa (Service) SF%u with syncword PUBLIC (0x34)\\n", lora_service_sf);
err |= lgw_reg_w(SX1302_REG_RX_TOP_LORA_SERVICE_FSK_FRAME_SYNCH0_PEAK1_POS, 6);
err |= lgw_reg_w(SX1302_REG_RX_TOP_LORA_SERVICE_FSK_FRAME_SYNCH1_PEAK2_POS, 8);
}
return err;"""
_B = """\
int err = LGW_REG_SUCCESS;
uint8_t sw_reg1, sw_reg2;
if (public == true) {
sw_reg1 = 6;
sw_reg2 = 8;
} else if (lora_service_sf > 12) {
sw_reg1 = ((lora_service_sf >> 4) & 0x0F) * 2;
sw_reg2 = (lora_service_sf & 0x0F) * 2;
DEBUG_PRINTF("INFO: sync cfg 0x%02X -> %u, %u\\n", lora_service_sf, sw_reg1, sw_reg2);
} else {
sw_reg1 = 2;
sw_reg2 = 4;
}
sx1302_tx_sw_peak1 = sw_reg1;
sx1302_tx_sw_peak2 = sw_reg2;
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF5_PEAK1_POS_SF5, sw_reg1);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH1_SF5_PEAK2_POS_SF5, sw_reg2);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF6_PEAK1_POS_SF6, sw_reg1);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH1_SF6_PEAK2_POS_SF6, sw_reg2);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF7TO12_PEAK1_POS_SF7TO12, sw_reg1);
err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH1_SF7TO12_PEAK2_POS_SF7TO12, sw_reg2);
err |= lgw_reg_w(SX1302_REG_RX_TOP_LORA_SERVICE_FSK_FRAME_SYNCH0_PEAK1_POS, sw_reg1);
err |= lgw_reg_w(SX1302_REG_RX_TOP_LORA_SERVICE_FSK_FRAME_SYNCH1_PEAK2_POS, sw_reg2);
return err;"""
if "sw_reg1" in s1:
pass
elif _A in s1:
s1 = s1.replace(_A, _B, 1)
else:
print("FAIL: source mismatch in " + str(f1)); sys.exit(1)
_TX_A = """\
/* Syncword */
if ((lwan_public == false) || (pkt_data->datarate == DR_LORA_SF5) || (pkt_data->datarate == DR_LORA_SF6)) {
DEBUG_MSG("Setting LoRa syncword 0x12\\n");
err = lgw_reg_w(SX1302_REG_TX_TOP_FRAME_SYNCH_0_PEAK1_POS(pkt_data->rf_chain), 2);
CHECK_ERR(err);
err = lgw_reg_w(SX1302_REG_TX_TOP_FRAME_SYNCH_1_PEAK2_POS(pkt_data->rf_chain), 4);
CHECK_ERR(err);
} else {
DEBUG_MSG("Setting LoRa syncword 0x34\\n");
err = lgw_reg_w(SX1302_REG_TX_TOP_FRAME_SYNCH_0_PEAK1_POS(pkt_data->rf_chain), 6);
CHECK_ERR(err);
err = lgw_reg_w(SX1302_REG_TX_TOP_FRAME_SYNCH_1_PEAK2_POS(pkt_data->rf_chain), 8);
CHECK_ERR(err);
}"""
_TX_B = """\
/* Syncword */
err = lgw_reg_w(SX1302_REG_TX_TOP_FRAME_SYNCH_0_PEAK1_POS(pkt_data->rf_chain), sx1302_tx_sw_peak1);
CHECK_ERR(err);
err = lgw_reg_w(SX1302_REG_TX_TOP_FRAME_SYNCH_1_PEAK2_POS(pkt_data->rf_chain), sx1302_tx_sw_peak2);
CHECK_ERR(err);"""
if "static uint8_t sx1302_tx_sw_peak1" not in s1:
s1 = s1.replace("int sx1302_lora_syncword(", "static uint8_t sx1302_tx_sw_peak1 = 2;\nstatic uint8_t sx1302_tx_sw_peak2 = 4;\n\nint sx1302_lora_syncword(", 1)
if "sx1302_tx_sw_peak1 = sw_reg1" not in s1:
s1 = s1.replace(" sw_reg2 = 4;\n }\n\n err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF5_PEAK1_POS_SF5", " sw_reg2 = 4;\n }\n\n sx1302_tx_sw_peak1 = sw_reg1;\n sx1302_tx_sw_peak2 = sw_reg2;\n\n err |= lgw_reg_w(SX1302_REG_RX_TOP_FRAME_SYNCH0_SF5_PEAK1_POS_SF5", 1)
if _TX_A in s1:
s1 = s1.replace(_TX_A, _TX_B, 1)
f1.write_text(s1, newline="\n")
_C = [
("""\
/* Find the temperature sensor on the known supported ports */
for (i = 0; i < (int)(sizeof I2C_PORT_TEMP_SENSOR); i++) {
ts_addr = I2C_PORT_TEMP_SENSOR[i];
err = i2c_linuxdev_open(I2C_DEVICE, ts_addr, &ts_fd);
if (err != LGW_I2C_SUCCESS) {
printf("ERROR: failed to open I2C for temperature sensor on port 0x%02X\\n", ts_addr);
return LGW_HAL_ERROR;
}
err = stts751_configure(ts_fd, ts_addr);
if (err != LGW_I2C_SUCCESS) {
printf("INFO: no temperature sensor found on port 0x%02X\\n", ts_addr);
i2c_linuxdev_close(ts_fd);
ts_fd = -1;
} else {
printf("INFO: found temperature sensor on port 0x%02X\\n", ts_addr);
break;
}
}
if (i == sizeof I2C_PORT_TEMP_SENSOR) {
printf("ERROR: no temperature sensor found.\\n");
return LGW_HAL_ERROR;
}""",
"""\
/* Find the temperature sensor on the known supported ports */
for (i = 0; i < (int)(sizeof I2C_PORT_TEMP_SENSOR); i++) {
ts_addr = I2C_PORT_TEMP_SENSOR[i];
err = i2c_linuxdev_open(I2C_DEVICE, ts_addr, &ts_fd);
if (err != LGW_I2C_SUCCESS) {
printf("WARNING: could not open I2C on port 0x%02X\\n", ts_addr);
ts_fd = -1;
continue;
}
err = stts751_configure(ts_fd, ts_addr);
if (err != LGW_I2C_SUCCESS) {
printf("INFO: no temperature sensor found on port 0x%02X\\n", ts_addr);
i2c_linuxdev_close(ts_fd);
ts_fd = -1;
} else {
printf("INFO: found temperature sensor on port 0x%02X\\n", ts_addr);
break;
}
}
if (ts_fd < 0) {
printf("WARNING: sensor not available, using default\\n");
}"""),
("""\
case LGW_COM_SPI:
err = stts751_get_temperature(ts_fd, ts_addr, temperature);
break;""",
"""\
case LGW_COM_SPI:
if (ts_fd > 0) {
err = stts751_get_temperature(ts_fd, ts_addr, temperature);
} else {
*temperature = 25.0;
err = LGW_HAL_SUCCESS;
}
break;"""),
("""\
DEBUG_MSG("INFO: Closing I2C for temperature sensor\\n");
x = i2c_linuxdev_close(ts_fd);
if (x != 0) {
printf("ERROR: failed to close I2C temperature sensor device (err=%i)\\n", x);
err = LGW_HAL_ERROR;
}""",
"""\
if (ts_fd > 0) {
DEBUG_MSG("INFO: Closing I2C for temperature sensor\\n");
x = i2c_linuxdev_close(ts_fd);
if (x != 0) {
printf("ERROR: failed to close I2C temperature sensor device (err=%i)\\n", x);
err = LGW_HAL_ERROR;
}
}"""),
]
ok = True
for o, n in _C:
if n in s2:
continue
if o not in s2:
ok = False; break
s2 = s2.replace(o, n, 1)
if ok:
f2.write_text(s2, newline="\n")
else:
print("FAIL: source mismatch in " + str(f2)); sys.exit(1)
_HALCFG
info "Compiling libloragw (this takes a few minutes)..."
cd "$HAL_BUILD_DIR"
make clean 2>/dev/null || true
make -j"$(nproc)"
info "Recompiling with -fPIC for shared library..."
mkdir -p pic_obj
for src in libtools/src/*.c; do
gcc -c -O2 -fPIC -Wall -Wextra -std=c99 \
-Ilibtools/inc -Ilibtools \
"$src" -o "pic_obj/$(basename "${src%.c}.o")"
done
for src in libloragw/src/*.c; do
gcc -c -O2 -fPIC -Wall -Wextra -std=c99 \
-Ilibloragw/inc -Ilibloragw -Ilibtools/inc \
"$src" -o "pic_obj/$(basename "${src%.c}.o")"
done
info "Linking libloragw.so..."
gcc -shared -o libloragw/libloragw.so pic_obj/*.o -lrt -lm -lpthread
info "Installing libloragw.so..."
cp libloragw/libloragw.so /usr/local/lib/
ldconfig
info "libloragw.so installed to /usr/local/lib/"
fi
# ── 4b. Apply TX sync word patch ─────────────────────────────────
HAL_SRC="${HAL_BUILD_DIR}/libloragw/src/loragw_sx1302.c"
if [ -f "$HAL_SRC" ]; then
info "Applying TX sync word patch..."
bash "${SCRIPT_DIR}/scripts/patch_hal.sh"
fi
# ── 5. Install Meshpoint application ──────────────────────────────
info "Installing Meshpoint to ${MESHPOINT_DIR}..."
mkdir -p "$MESHPOINT_DIR"
rsync -a --exclude='venv' \
--exclude='.git' \
--exclude='__pycache__' \
--exclude='cdk.out' \
--exclude='cloud/build' \
--exclude='data' \
--exclude='*.pyc' \
"${SCRIPT_DIR}/" "$MESHPOINT_DIR/"
# ── 5b. Remove stale compiled core modules from prior installs ─────
# Releases before 0.7.0 shipped .cpython-*.so files alongside the
# .py source. Python prefers the .so at import time, so any leftover
# binary would silently shadow the current source. rsync above does
# not delete files that are absent from the source tree, so we
# explicitly clean them up here.
if find "${MESHPOINT_DIR}/src" -name '*.cpython-*.so' -print -quit | grep -q .; then
info "Removing stale compiled modules from previous installation..."
find "${MESHPOINT_DIR}/src" -name '*.cpython-*.so' -delete
fi
# ── 6. Python virtual environment ──────────────────────────────────
info "Setting up Python virtual environment..."
python3 -m venv "${MESHPOINT_DIR}/venv"
source "${MESHPOINT_DIR}/venv/bin/activate"
pip install --upgrade pip -q
pip install -r "${MESHPOINT_DIR}/requirements.txt" -q
pip install pyserial -q
deactivate
# ── 7. Create data directory ───────────────────────────────────────
mkdir -p "${MESHPOINT_DIR}/data"
# ── 8. Create meshpoint system user ────────────────────────────────
if ! id -u meshpoint &>/dev/null; then
info "Creating system user 'meshpoint'..."
useradd --system --no-create-home --shell /usr/sbin/nologin meshpoint
fi
# Grant access to SPI, UART, GPIO, and I2C
usermod -a -G spi,gpio,dialout,i2c meshpoint 2>/dev/null || true
# Grant the service user read access to its own systemd journal so the
# dashboard's `meshpoint logs` button (and `journalctl -u meshpoint`
# inside the web terminal) work without sudo. This is the same group
# Raspberry Pi OS uses to gate journal access for the `pi` user.
usermod -a -G systemd-journal,adm meshpoint 2>/dev/null || true
chown -R meshpoint:meshpoint "${MESHPOINT_DIR}/data"
chown -R meshpoint:meshpoint "${MESHPOINT_DIR}/config"
# Espressif USB serial devices (Heltec V3/V4, T-Beam ESP32-S3) may not
# default to dialout group on all Pi OS versions. Add a udev rule so
# the meshpoint service user can access them for relay and MeshCore.
UDEV_RULE='SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", MODE="0666"'
UDEV_FILE="/etc/udev/rules.d/99-meshpoint-esp.rules"
if [ ! -f "$UDEV_FILE" ]; then
info "Installing udev rule for Espressif USB serial devices..."
echo "$UDEV_RULE" > "$UDEV_FILE"
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger 2>/dev/null || true
fi
# Allow service user to restart/stop its own service (dashboard + remote commands)
info "Installing sudoers rule for service management..."
cp "${MESHPOINT_DIR}/config/sudoers-meshpoint" /etc/sudoers.d/meshpoint
chmod 440 /etc/sudoers.d/meshpoint
# ── 9. Configure journald log rotation ─────────────────────────────
info "Configuring journald log limits (100M, 7-day retention)..."
mkdir -p /etc/systemd/journald.conf.d
cp "${MESHPOINT_DIR}/config/journald-meshpoint.conf" /etc/systemd/journald.conf.d/meshpoint.conf
systemctl restart systemd-journald 2>/dev/null || warn "Could not restart journald"
# ── 10. Install systemd service ────────────────────────────────────
info "Installing systemd service..."
cp "${MESHPOINT_DIR}/${SERVICE_FILE}" /etc/systemd/system/meshpoint.service
systemctl daemon-reload
systemctl enable meshpoint
info "Service enabled (will start after 'meshpoint setup')"
# ── 11. Install network watchdog ───────────────────────────────────
info "Installing WiFi network watchdog..."
cp "${MESHPOINT_DIR}/${WATCHDOG_SERVICE_FILE}" /etc/systemd/system/network-watchdog.service
systemctl daemon-reload
systemctl enable network-watchdog
systemctl start network-watchdog 2>/dev/null || warn "Could not start network-watchdog (will start on next boot)"
info "Network watchdog enabled"
# ── 12. Install CLI tool ───────────────────────────────────────────
info "Installing meshpoint CLI..."
chmod +x "${MESHPOINT_DIR}/${CLI_SCRIPT}"
ln -sf "${MESHPOINT_DIR}/${CLI_SCRIPT}" /usr/local/bin/meshpoint
# ── Done ────────────────────────────────────────────────────────────
echo ""
echo "==========================================="
if [ "$IS_UPGRADE" = "1" ]; then
echo " Meshpoint upgrade to v${INSTALL_VERSION} complete!"
echo "==========================================="
echo ""
echo " Restart the service to apply changes:"
echo " sudo systemctl restart meshpoint"
echo ""
echo " A reboot is NOT required: SPI/UART/I2C are"
echo " already configured from the original install."
echo ""
else
echo " Meshpoint installation complete!"
echo "==========================================="
echo ""
echo " Next steps:"
echo ""
echo " 1. Reboot to apply SPI/UART changes:"
echo " sudo reboot"
echo ""
echo " 2. After reboot, run the setup wizard:"
echo " sudo meshpoint setup"
echo ""
echo " 3. The wizard will walk you through:"
echo " - Hardware detection"
echo " - API key configuration"
echo " - Device naming and GPS"
echo " - Starting the service"
echo ""
echo " IMPORTANT: Never yank the power cable"
echo " without shutting down first. Always run:"
echo " sudo poweroff"
echo " and wait for the LED to go dark."
echo ""
fi
echo "==========================================="