frugal-iot-server 0.3.7 → 0.3.8
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/INSTALLATION.md +299 -0
- package/README.md +94 -19
- package/config.d/schema/devices.yaml +63 -0
- package/config.d/schema/modules.yaml +8 -37
- package/config.d/schema/topics.yaml +39 -6
- package/extras/mosquitto-bridge.conf.example +130 -0
- package/frugal-iot-server.js +50 -2
- package/lib/housekeeping.js +10 -4
- package/package.json +10 -5
- package/public/index.html +8 -0
- package/public/service-worker.js +4 -3
- package/scripts/addbridge-pi.zsh +164 -0
- package/scripts/addbridge-prod.zsh +215 -0
- package/scripts/check-published-files.js +0 -0
- package/scripts/check-schema.js +126 -15
- package/scripts/clearretained.zsh +153 -0
- package/scripts/copy-schema-to-examples.zsh +9 -5
- package/scripts/diagnostic.zsh +47 -0
- package/scripts/install-pi.sh +646 -0
- package/scripts/not-published.txt +27 -0
- package/scripts/prerelease.zsh +91 -17
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Install a complete Frugal IoT server - broker, server, service - on a freshly flashed Raspberry Pi.
|
|
4
|
+
#
|
|
5
|
+
# This does automatically what INSTALLATION.md walks through by hand, steps 3 to 8. Read that if you
|
|
6
|
+
# want to know why any of it is being done; this only says what it is doing as it goes.
|
|
7
|
+
#
|
|
8
|
+
# curl -fsSLO https://raw.githubusercontent.com/mitra42/frugal-iot-server/main/scripts/install-pi.sh
|
|
9
|
+
# bash install-pi.sh --org myfarm --name "My Farm" --email you@example.com --phone +61123456789
|
|
10
|
+
#
|
|
11
|
+
# Written in bash on purpose: a fresh Raspberry Pi OS Lite image has no zsh, which the server's own
|
|
12
|
+
# setup commands need, so this cannot be a zsh script - it is what installs zsh.
|
|
13
|
+
#
|
|
14
|
+
# Anything not given on the command line is asked for, or made up where that is reasonable
|
|
15
|
+
# (passwords). With --yes it never asks and invents whatever is missing, for an unattended run.
|
|
16
|
+
#
|
|
17
|
+
# Safe to run again. Every step checks whether it has already been done, so a run that stopped
|
|
18
|
+
# half way - or was interrupted by the board running out of memory, which a Pi Zero W will do -
|
|
19
|
+
# carries on from where it got to rather than starting over. That matters most for the one slow
|
|
20
|
+
# step: compiling sqlite3 on a Zero W takes about 40 minutes and is not repeated once it is done.
|
|
21
|
+
#
|
|
22
|
+
# What it does NOT do:
|
|
23
|
+
# * flash the SD card, or set up wifi - INSTALLATION.md steps 1 and 2, before you can ssh in
|
|
24
|
+
# * point your sensor nodes at this server - that means flashing the nodes, which happens on your
|
|
25
|
+
# workstation, not here. It prints the settings to give them.
|
|
26
|
+
# * HTTPS, which over-the-air firmware updates to ESP32 need - INSTALLATION.md step 10
|
|
27
|
+
# * reboot. A reboot is worth doing afterwards if the upgrade brought a new kernel.
|
|
28
|
+
|
|
29
|
+
set -Eeuo pipefail
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------- settings and arguments
|
|
32
|
+
|
|
33
|
+
ORG_ID=""; ORG_NAME=""; ORG_EMAIL=""; ORG_PHONE=""
|
|
34
|
+
BROKER_PW=""; SUPER_PW=""; LOGIN_PW=""; BROKER_HOST=""
|
|
35
|
+
INSTALL_DIR=""; RUN_USER=""; ASSUME_YES=0; DRY_RUN=0; RANDOM_PASSWORDS=0
|
|
36
|
+
|
|
37
|
+
usage() {
|
|
38
|
+
cat <<USAGE
|
|
39
|
+
Usage: bash install-pi.sh [options]
|
|
40
|
+
|
|
41
|
+
--org <id> organization id, 1-10 lower-case letters or digits. Becomes the first part
|
|
42
|
+
of every MQTT topic, so it must match what your nodes publish to.
|
|
43
|
+
--name "<name>" organization display name
|
|
44
|
+
--email <address> contact address for the organization
|
|
45
|
+
--phone <number> contact phone, "+" and digits only
|
|
46
|
+
--broker-password the machine credential shared by the server, nodes and dashboards.
|
|
47
|
+
--superuser-password password for this server's administrator login.
|
|
48
|
+
--login-password web login password for the organization's own account, which is a different
|
|
49
|
+
thing from the broker credential above and should not be the same string.
|
|
50
|
+
Any of these three not given is asked for, and generated if you just press
|
|
51
|
+
Enter, or if there is no terminal to ask at.
|
|
52
|
+
--broker-host <host> what the browser and the nodes should call this Pi.
|
|
53
|
+
Default: this Pi's hostname with ".local". Use an IP address if you will
|
|
54
|
+
view the dashboard on Android, which cannot resolve ".local" names.
|
|
55
|
+
--dir <path> where to install. Default: ~/frugal-iot of the user running this.
|
|
56
|
+
--user <name> account to own and run the server. Default: whoever invoked this.
|
|
57
|
+
--yes never ask anything; invent whatever was not given.
|
|
58
|
+
--random-passwords generate the passwords instead of asking, even at a terminal.
|
|
59
|
+
--dry-run work out what would be done, check the arguments, and stop before
|
|
60
|
+
changing anything. Also the way to see the generated passwords first.
|
|
61
|
+
--help
|
|
62
|
+
USAGE
|
|
63
|
+
exit "${1:-0}"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
while (( $# )); do
|
|
67
|
+
case "$1" in
|
|
68
|
+
--org) ORG_ID="${2:-}"; shift 2 ;;
|
|
69
|
+
--name) ORG_NAME="${2:-}"; shift 2 ;;
|
|
70
|
+
--email) ORG_EMAIL="${2:-}"; shift 2 ;;
|
|
71
|
+
--phone) ORG_PHONE="${2:-}"; shift 2 ;;
|
|
72
|
+
--broker-password) BROKER_PW="${2:-}"; shift 2 ;;
|
|
73
|
+
--superuser-password) SUPER_PW="${2:-}"; shift 2 ;;
|
|
74
|
+
--login-password) LOGIN_PW="${2:-}"; shift 2 ;;
|
|
75
|
+
--broker-host) BROKER_HOST="${2:-}"; shift 2 ;;
|
|
76
|
+
--dir) INSTALL_DIR="${2:-}"; shift 2 ;;
|
|
77
|
+
--user) RUN_USER="${2:-}"; shift 2 ;;
|
|
78
|
+
--yes|-y) ASSUME_YES=1; shift ;;
|
|
79
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
80
|
+
--random-passwords) RANDOM_PASSWORDS=1; shift ;;
|
|
81
|
+
--help|-h) usage 0 ;;
|
|
82
|
+
*) echo "Unknown option: $1" >&2; usage 1 ;;
|
|
83
|
+
esac
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------- who this runs as
|
|
87
|
+
# npm must not run as root: it would leave root-owned files in the install directory, and the
|
|
88
|
+
# service runs as an ordinary user, so the server would then be unable to write its own database.
|
|
89
|
+
# Being invoked with sudo is fine and expected - the user-level parts are handed back to the
|
|
90
|
+
# account that called it.
|
|
91
|
+
|
|
92
|
+
if [[ -z "$RUN_USER" ]]; then
|
|
93
|
+
if [[ ${EUID} -eq 0 ]]; then
|
|
94
|
+
RUN_USER="${SUDO_USER:-}"
|
|
95
|
+
if [[ -z "$RUN_USER" ]]; then
|
|
96
|
+
echo "Running as root with no SUDO_USER to hand back to." >&2
|
|
97
|
+
echo "Either run this as your ordinary account (it will use sudo where it needs to)," >&2
|
|
98
|
+
echo "or say which account should own the server: --user pi" >&2
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
else
|
|
102
|
+
RUN_USER="$(id -un)"
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
id "$RUN_USER" >/dev/null 2>&1 || { echo "No such user: $RUN_USER" >&2; exit 1; }
|
|
106
|
+
if command -v getent >/dev/null 2>&1; then
|
|
107
|
+
RUN_HOME="$(getent passwd "$RUN_USER" | cut -d: -f6)"
|
|
108
|
+
else
|
|
109
|
+
RUN_HOME="$(eval echo "~${RUN_USER}")" # no getent outside linux; only reached on the wrong OS
|
|
110
|
+
fi
|
|
111
|
+
[[ -n "$RUN_HOME" ]] || { echo "Could not find the home directory of ${RUN_USER}" >&2; exit 1; }
|
|
112
|
+
[[ -n "$INSTALL_DIR" ]] || INSTALL_DIR="${RUN_HOME}/frugal-iot"
|
|
113
|
+
|
|
114
|
+
# Run a command as the owning account, or straight through if that is already us
|
|
115
|
+
as_user() {
|
|
116
|
+
if [[ "$(id -un)" == "$RUN_USER" ]]; then
|
|
117
|
+
"$@"
|
|
118
|
+
else
|
|
119
|
+
sudo -u "$RUN_USER" -H "$@"
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
# Same, but for something that has to be a shell line (cd, pipes, redirection)
|
|
123
|
+
as_user_sh() {
|
|
124
|
+
if [[ "$(id -un)" == "$RUN_USER" ]]; then
|
|
125
|
+
bash -c "$1"
|
|
126
|
+
else
|
|
127
|
+
sudo -u "$RUN_USER" -H bash -c "$1"
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
# sudo, or nothing if we are already root
|
|
131
|
+
sudo_() { if [[ ${EUID} -eq 0 ]]; then "$@"; else sudo "$@"; fi; }
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------- logging
|
|
134
|
+
|
|
135
|
+
if (( DRY_RUN )); then
|
|
136
|
+
LOG=/dev/null # --dry-run changes nothing, and that includes not leaving a log behind
|
|
137
|
+
else
|
|
138
|
+
LOG="${RUN_HOME}/frugal-iot-install-$(date +%Y%m%d-%H%M%S).log"
|
|
139
|
+
touch "$LOG"; chmod 600 "$LOG"; chown "$RUN_USER" "$LOG" 2>/dev/null || true
|
|
140
|
+
# Everything from here on goes to the terminal and to the log. The log holds the passwords, which
|
|
141
|
+
# is why it is readable only by its owner.
|
|
142
|
+
exec > >(tee -a "$LOG") 2>&1
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
STEP_N=0
|
|
146
|
+
step() { STEP_N=$((STEP_N+1)); echo; echo "=== ${STEP_N}. $* ==="; }
|
|
147
|
+
info() { echo " $*"; }
|
|
148
|
+
ok() { echo " ok: $*"; }
|
|
149
|
+
skip() { echo " already done: $*"; }
|
|
150
|
+
warn() { echo " WARNING: $*"; }
|
|
151
|
+
|
|
152
|
+
on_error() {
|
|
153
|
+
local line=$1
|
|
154
|
+
echo
|
|
155
|
+
echo "=== Stopped at line ${line} ==="
|
|
156
|
+
echo "Nothing after this point has been done."
|
|
157
|
+
if [[ "$LOG" != /dev/null ]]; then
|
|
158
|
+
echo "The log of this run is:"
|
|
159
|
+
echo " ${LOG}"
|
|
160
|
+
fi
|
|
161
|
+
echo
|
|
162
|
+
echo "Run it again once the cause is dealt with - the steps that succeeded are not repeated."
|
|
163
|
+
if [[ -x "${INSTALL_DIR}/node_modules/.bin/frugal-iot-diagnostic" ]]; then
|
|
164
|
+
echo "For what state the installation is in now:"
|
|
165
|
+
echo " cd ${INSTALL_DIR} && npx --no frugal-iot-diagnostic"
|
|
166
|
+
fi
|
|
167
|
+
exit 1
|
|
168
|
+
}
|
|
169
|
+
trap 'on_error $LINENO' ERR
|
|
170
|
+
|
|
171
|
+
randpw() { ( set +o pipefail; LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 ); }
|
|
172
|
+
|
|
173
|
+
REBOOT_REASONS=()
|
|
174
|
+
|
|
175
|
+
# Size and modification time of every kernel and initrd image, so that the upgrade replacing one can
|
|
176
|
+
# be noticed. /boot/firmware is where Raspberry Pi OS keeps them from Debian 12 on, /boot before that.
|
|
177
|
+
boot_fingerprint() {
|
|
178
|
+
local f
|
|
179
|
+
for f in /boot/*.img /boot/vmlinuz* /boot/initrd* /boot/firmware/*.img /boot/firmware/vmlinuz* /boot/firmware/initrd*; do
|
|
180
|
+
[[ -e "$f" ]] || continue
|
|
181
|
+
printf '%s %s\n' "$f" "$(stat -c '%Y:%s' "$f" 2>/dev/null || echo '?')"
|
|
182
|
+
done | sort
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Which passwords were made up rather than given, so the summary can print those and leave the ones
|
|
186
|
+
# you already know out of the log
|
|
187
|
+
GENERATED=()
|
|
188
|
+
|
|
189
|
+
ask_password() { # ask_password <variable name> <what it is for>
|
|
190
|
+
local var=$1 what=$2
|
|
191
|
+
if [[ -n "${!var}" ]]; then
|
|
192
|
+
return 0 # given on the command line
|
|
193
|
+
fi
|
|
194
|
+
if (( RANDOM_PASSWORDS )) || (( ASSUME_YES )) || [[ ! -t 0 ]]; then
|
|
195
|
+
printf -v "$var" '%s' "$(randpw)"
|
|
196
|
+
GENERATED+=("$var")
|
|
197
|
+
info "generated a password for ${what}$( [[ -t 0 ]] || echo ' (no terminal to ask at)' )"
|
|
198
|
+
return 0
|
|
199
|
+
fi
|
|
200
|
+
# Prompt and reply both go straight to the terminal rather than through the log: nothing typed
|
|
201
|
+
# here is echoed, and nothing typed here is written to the log either. Going via stdout would also
|
|
202
|
+
# mean the prompt queued behind tee. Empty means "make one up", which is the quickest answer for
|
|
203
|
+
# the broker credential - a machine password nobody wants to type twice.
|
|
204
|
+
local first second
|
|
205
|
+
while true; do
|
|
206
|
+
printf ' Password for %s\n (or press Enter to have one generated): ' "$what" > /dev/tty
|
|
207
|
+
read -rs first < /dev/tty; printf '\n' > /dev/tty
|
|
208
|
+
if [[ -z "$first" ]]; then
|
|
209
|
+
printf -v "$var" '%s' "$(randpw)"
|
|
210
|
+
GENERATED+=("$var")
|
|
211
|
+
info "generated a password for ${what}"
|
|
212
|
+
return 0
|
|
213
|
+
fi
|
|
214
|
+
printf ' Again, to be sure: ' > /dev/tty
|
|
215
|
+
read -rs second < /dev/tty; printf '\n' > /dev/tty
|
|
216
|
+
if [[ "$first" == "$second" ]]; then
|
|
217
|
+
printf -v "$var" '%s' "$first"
|
|
218
|
+
info "took the password you typed for ${what}"
|
|
219
|
+
return 0
|
|
220
|
+
fi
|
|
221
|
+
printf ' Those did not match - try again.\n' > /dev/tty
|
|
222
|
+
done
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
ask() { # ask <variable name> <prompt> [generated]
|
|
226
|
+
local var=$1 prompt=$2 generate=${3:-}
|
|
227
|
+
local current="${!var}"
|
|
228
|
+
[[ -n "$current" ]] && return 0
|
|
229
|
+
if [[ -n "$generate" ]]; then
|
|
230
|
+
printf -v "$var" '%s' "$(randpw)"
|
|
231
|
+
return 0
|
|
232
|
+
fi
|
|
233
|
+
if (( ASSUME_YES )) || [[ ! -t 0 ]]; then
|
|
234
|
+
echo "Nothing given for '${prompt}', and there is no terminal to ask at (or --yes was given)." >&2
|
|
235
|
+
echo "Pass it on the command line - see --help." >&2
|
|
236
|
+
return 1
|
|
237
|
+
fi
|
|
238
|
+
local reply
|
|
239
|
+
read -r -p " ${prompt}: " reply
|
|
240
|
+
printf -v "$var" '%s' "$reply"
|
|
241
|
+
[[ -n "${!var}" ]] || { echo "That cannot be empty" >&2; return 1; }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
echo "Frugal IoT server installation"
|
|
245
|
+
(( DRY_RUN )) && echo "Dry run - nothing will be changed" || echo "Log: ${LOG}"
|
|
246
|
+
|
|
247
|
+
# Everything below assumes a Debian with systemd - Raspberry Pi OS, or Debian/Ubuntu on anything
|
|
248
|
+
# else. Say so now rather than failing obscurely four steps in.
|
|
249
|
+
MISSING_PLATFORM=()
|
|
250
|
+
[[ "$(uname -s)" == "Linux" ]] || MISSING_PLATFORM+=("this is $(uname -s), not Linux")
|
|
251
|
+
command -v apt-get >/dev/null 2>&1 || MISSING_PLATFORM+=("no apt-get")
|
|
252
|
+
command -v systemctl >/dev/null 2>&1 || MISSING_PLATFORM+=("no systemctl")
|
|
253
|
+
if (( ${#MISSING_PLATFORM[@]} )); then
|
|
254
|
+
if (( DRY_RUN )); then
|
|
255
|
+
warn "not a Debian with systemd (${MISSING_PLATFORM[*]}) - checking the arguments anyway"
|
|
256
|
+
else
|
|
257
|
+
echo "This installs onto a Debian system with systemd - Raspberry Pi OS, or Debian or Ubuntu." >&2
|
|
258
|
+
printf ' %s\n' "${MISSING_PLATFORM[@]}" >&2
|
|
259
|
+
exit 1
|
|
260
|
+
fi
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# ---------------------------------------------------------------- what we are installing
|
|
264
|
+
|
|
265
|
+
step "Working out what to install"
|
|
266
|
+
|
|
267
|
+
if [[ -r /etc/os-release ]]; then
|
|
268
|
+
info "os: $(sed -n 's/^PRETTY_NAME="\(.*\)"$/\1/p' /etc/os-release)"
|
|
269
|
+
fi
|
|
270
|
+
[[ -r /proc/device-tree/model ]] && info "board: $(tr -d '\0' < /proc/device-tree/model)"
|
|
271
|
+
ARCH="$(uname -m)"
|
|
272
|
+
MEM_KB="$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)"
|
|
273
|
+
info "arch: ${ARCH}, memory: $((MEM_KB / 1024)) MB"
|
|
274
|
+
|
|
275
|
+
# A 32-bit ARMv6 board (Pi Zero W, Pi 1) has no ready-made sqlite3 binary, so it has to compile it,
|
|
276
|
+
# which needs a compiler and more memory than the board has.
|
|
277
|
+
NEEDS_BUILD_TOOLS=0
|
|
278
|
+
NPM_SLOW_FLAGS=()
|
|
279
|
+
case "$ARCH" in
|
|
280
|
+
armv6l|armv7l) NEEDS_BUILD_TOOLS=1; NPM_SLOW_FLAGS=(--maxsockets 1 --no-audit --no-fund) ;;
|
|
281
|
+
esac
|
|
282
|
+
if (( MEM_KB > 0 && MEM_KB < 700000 )); then NEEDS_SWAP=1; else NEEDS_SWAP=0; fi
|
|
283
|
+
(( NEEDS_BUILD_TOOLS )) && info "32-bit ARM: sqlite3 will be compiled, which is slow (allow 40 minutes on a Zero W)"
|
|
284
|
+
(( NEEDS_SWAP )) && info "under 700 MB of memory: a swap file will be added so the install can finish"
|
|
285
|
+
|
|
286
|
+
ask ORG_ID "Organization id (1-10 lower-case letters or digits, e.g. myfarm)" || exit 1
|
|
287
|
+
[[ "$ORG_ID" =~ ^[a-z0-9]{1,10}$ ]] || { echo "Organization id must be 1-10 lower-case letters or digits: '${ORG_ID}'" >&2; exit 1; }
|
|
288
|
+
ask ORG_NAME "Organization display name (e.g. My Farm)" || exit 1
|
|
289
|
+
ask ORG_EMAIL "Contact email" || exit 1
|
|
290
|
+
ask ORG_PHONE "Contact phone ('+' and digits only)" || exit 1
|
|
291
|
+
[[ "$ORG_PHONE" =~ ^[+]?[0-9]+$ ]] || { echo "Phone must be '+' and digits only: '${ORG_PHONE}'" >&2; exit 1; }
|
|
292
|
+
# Asked for in the order they matter. Each may be typed, passed as an option, or generated.
|
|
293
|
+
ask_password SUPER_PW "the superuser login (this server's administrator)"
|
|
294
|
+
ask_password LOGIN_PW "the ${ORG_ID} web login"
|
|
295
|
+
ask_password BROKER_PW "the ${ORG_ID} broker credential, shared by the nodes and dashboards"
|
|
296
|
+
[[ -n "$BROKER_HOST" ]] || BROKER_HOST="$(hostname).local"
|
|
297
|
+
|
|
298
|
+
info "install into: ${INSTALL_DIR} (owned by ${RUN_USER})"
|
|
299
|
+
info "organization: ${ORG_ID} (${ORG_NAME})"
|
|
300
|
+
info "broker URL: ws://${BROKER_HOST}:9012"
|
|
301
|
+
|
|
302
|
+
if (( DRY_RUN )); then
|
|
303
|
+
cat <<PLAN
|
|
304
|
+
|
|
305
|
+
Nothing has been changed. With the same arguments and without --dry-run, this would:
|
|
306
|
+
* apt update, full-upgrade, and install: nodejs npm sqlite3 zsh curl$( (( NEEDS_BUILD_TOOLS )) && echo " build-essential python3-dev python3-setuptools" )
|
|
307
|
+
* cap the systemd journal at 16M and set vm.swappiness=1
|
|
308
|
+
$( (( NEEDS_SWAP )) && echo " * add a 2G swap file, because this board has under 700 MB of memory" )
|
|
309
|
+
* npm install frugal-iot-server into ${INSTALL_DIR} as ${RUN_USER}$( (( NEEDS_BUILD_TOOLS )) && echo ", compiling sqlite3 (slow)" )
|
|
310
|
+
* set the broker to ws://${BROKER_HOST}:9012
|
|
311
|
+
* install and configure mosquitto, with listeners on 1883 and 9012
|
|
312
|
+
* create the superuser login and the ${ORG_ID} organization, and test the broker
|
|
313
|
+
* install and start the frugaliot service, running as ${RUN_USER}
|
|
314
|
+
|
|
315
|
+
Passwords it would use. A generated one is different on every run, so pass it or type it if you
|
|
316
|
+
want this exact set. All of them are shown here because a dry run writes no log; a real run prints
|
|
317
|
+
only the generated ones, so the ones you chose stay out of the log:
|
|
318
|
+
$(printf ' %-22s %s\n' "superuser login" "${SUPER_PW}" "${ORG_ID} web login" "${LOGIN_PW}" "${ORG_ID} broker" "${BROKER_PW}")
|
|
319
|
+
PLAN
|
|
320
|
+
exit 0
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
# ---------------------------------------------------------------- the system
|
|
324
|
+
|
|
325
|
+
step "Updating the operating system"
|
|
326
|
+
sudo_ apt-get update -qq
|
|
327
|
+
# What is about to be upgraded, asked for before doing it: "-s" simulates, and each package it would
|
|
328
|
+
# install shows up as an "Inst" line. Doing it this way rather than reading the real output means the
|
|
329
|
+
# upgrade itself can stay quiet.
|
|
330
|
+
UPGRADE_PLAN="$(sudo_ apt-get -s full-upgrade 2>/dev/null | awk '/^Inst /{print $2}' | sort -u || true)"
|
|
331
|
+
BOOT_BEFORE="$(boot_fingerprint)"
|
|
332
|
+
# No reboot here even if this brings a new kernel, so that the run can continue unattended. Whether
|
|
333
|
+
# one is needed is worked out below and said plainly in the summary.
|
|
334
|
+
sudo_ env DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y -qq
|
|
335
|
+
if [[ -n "$UPGRADE_PLAN" ]]; then
|
|
336
|
+
info "upgraded: $(echo "$UPGRADE_PLAN" | wc -l | tr -d ' ') package(s)"
|
|
337
|
+
else
|
|
338
|
+
info "nothing needed upgrading"
|
|
339
|
+
fi
|
|
340
|
+
ok "system up to date"
|
|
341
|
+
|
|
342
|
+
# The running kernel is whatever was loaded at boot; replacing the file on disk does not change it.
|
|
343
|
+
# Three ways to notice, because no single one is reliable on every image:
|
|
344
|
+
# * a kernel or firmware package in the upgrade list
|
|
345
|
+
# * the boot images on disk having changed underneath us
|
|
346
|
+
# * /var/run/reboot-required, which only appears if update-notifier-common is installed - it is
|
|
347
|
+
# not on a Lite image, which is why the other two matter
|
|
348
|
+
if [[ -n "$UPGRADE_PLAN" ]]; then
|
|
349
|
+
KERNEL_PKGS="$(echo "$UPGRADE_PLAN" | grep -E '^(linux-image|linux-headers|linux-firmware|raspberrypi-kernel|raspberrypi-bootloader|raspi-firmware)' || true)"
|
|
350
|
+
[[ -n "$KERNEL_PKGS" ]] && REBOOT_REASONS+=("a new kernel or boot firmware was installed: $(echo "$KERNEL_PKGS" | tr '\n' ' ')")
|
|
351
|
+
CORE_PKGS="$(echo "$UPGRADE_PLAN" | grep -E '^(libc6|libssl[0-9]|systemd)$' || true)"
|
|
352
|
+
[[ -n "$CORE_PKGS" ]] && REBOOT_REASONS+=("core libraries were replaced, and running programs still have the old ones in memory: $(echo "$CORE_PKGS" | tr '\n' ' ')")
|
|
353
|
+
fi
|
|
354
|
+
if [[ "$(boot_fingerprint)" != "$BOOT_BEFORE" ]]; then
|
|
355
|
+
REBOOT_REASONS+=("the kernel images in /boot changed, so the kernel running now is not the one on disk")
|
|
356
|
+
fi
|
|
357
|
+
[[ -f /var/run/reboot-required ]] && REBOOT_REASONS+=("the system itself asked for one (/var/run/reboot-required)")
|
|
358
|
+
if (( ${#REBOOT_REASONS[@]} )); then
|
|
359
|
+
warn "a reboot will be needed when this finishes - the summary says so again"
|
|
360
|
+
else
|
|
361
|
+
ok "no reboot needed: the kernel running now is the one on disk"
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
step "Installing the packages the server needs"
|
|
365
|
+
PACKAGES=(nodejs npm sqlite3 zsh curl)
|
|
366
|
+
(( NEEDS_BUILD_TOOLS )) && PACKAGES+=(build-essential python3-dev python3-setuptools)
|
|
367
|
+
sudo_ env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${PACKAGES[@]}"
|
|
368
|
+
NODE_VERSION="$(node -v)"
|
|
369
|
+
info "node ${NODE_VERSION}, npm $(npm -v), sqlite3 $(sqlite3 --version | awk '{print $1}')"
|
|
370
|
+
# The server needs node 18 or later
|
|
371
|
+
NODE_MAJOR="$(echo "${NODE_VERSION#v}" | cut -d. -f1)"
|
|
372
|
+
if (( NODE_MAJOR < 18 )); then
|
|
373
|
+
echo "node ${NODE_VERSION} is too old - the server needs 18 or later." >&2
|
|
374
|
+
echo "Install a current version from https://github.com/nodesource/distributions and run this again." >&2
|
|
375
|
+
exit 1
|
|
376
|
+
fi
|
|
377
|
+
ok "packages installed"
|
|
378
|
+
|
|
379
|
+
step "Settings that make the SD card last longer"
|
|
380
|
+
# The journal would otherwise be allowed to grow to a tenth of the card, and every line in it is a
|
|
381
|
+
# write; swapping to an SD card wears it out fastest of all.
|
|
382
|
+
if [[ -f /etc/systemd/journald.conf.d/frugal-iot.conf ]]; then
|
|
383
|
+
skip "journal size already capped"
|
|
384
|
+
else
|
|
385
|
+
sudo_ mkdir -p /etc/systemd/journald.conf.d
|
|
386
|
+
printf '[Journal]\nSystemMaxUse=16M\nSystemMaxFileSize=4M\n' | sudo_ tee /etc/systemd/journald.conf.d/frugal-iot.conf >/dev/null
|
|
387
|
+
sudo_ systemctl restart systemd-journald
|
|
388
|
+
ok "journal capped at 16M"
|
|
389
|
+
fi
|
|
390
|
+
if [[ -f /etc/sysctl.d/99-frugal-iot-swappiness.conf ]]; then
|
|
391
|
+
skip "swappiness already set"
|
|
392
|
+
else
|
|
393
|
+
echo 'vm.swappiness=1' | sudo_ tee /etc/sysctl.d/99-frugal-iot-swappiness.conf >/dev/null
|
|
394
|
+
sudo_ sysctl -q -w vm.swappiness=1
|
|
395
|
+
ok "swappiness set to 1 (swap only as a last resort)"
|
|
396
|
+
fi
|
|
397
|
+
|
|
398
|
+
if (( NEEDS_SWAP )); then
|
|
399
|
+
step "Adding swap, so the install can finish on a small board"
|
|
400
|
+
if [[ -f /swapfile ]]; then
|
|
401
|
+
skip "/swapfile exists"
|
|
402
|
+
swapon --show=NAME --noheadings 2>/dev/null | grep -qx /swapfile || sudo_ swapon /swapfile
|
|
403
|
+
else
|
|
404
|
+
# zram alone is not enough here: compressed RAM does not help when the working set is genuinely
|
|
405
|
+
# large, and without this the board stops answering ssh and has to have its power pulled.
|
|
406
|
+
sudo_ fallocate -l 2G /swapfile
|
|
407
|
+
sudo_ chmod 600 /swapfile
|
|
408
|
+
sudo_ mkswap -q /swapfile
|
|
409
|
+
sudo_ swapon /swapfile
|
|
410
|
+
grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo_ tee -a /etc/fstab >/dev/null
|
|
411
|
+
ok "2G swap file added and enabled"
|
|
412
|
+
fi
|
|
413
|
+
info "swap now: $(free -h | awk '/^Swap:/ {print $3 " used of " $2}')"
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------- the server
|
|
417
|
+
|
|
418
|
+
step "Installing the Frugal IoT server"
|
|
419
|
+
as_user mkdir -p "$INSTALL_DIR"
|
|
420
|
+
if [[ -d "${INSTALL_DIR}/node_modules/frugal-iot-server" ]]; then
|
|
421
|
+
skip "already installed in ${INSTALL_DIR}"
|
|
422
|
+
else
|
|
423
|
+
if (( NEEDS_BUILD_TOOLS )); then
|
|
424
|
+
info "this is the slow part - sqlite3 is compiled here. Leave it running."
|
|
425
|
+
fi
|
|
426
|
+
as_user_sh "cd '${INSTALL_DIR}' && npm install ${NPM_SLOW_FLAGS[*]:-} frugal-iot-server"
|
|
427
|
+
ok "installed"
|
|
428
|
+
fi
|
|
429
|
+
info "server $(as_user_sh "node -e \"console.log(require('${INSTALL_DIR}/node_modules/frugal-iot-server/package.json').version)\"")"
|
|
430
|
+
|
|
431
|
+
step "Checking the compiled part of sqlite3 actually built"
|
|
432
|
+
# npm can report success while leaving this out, and nothing else looks wrong when it does - the
|
|
433
|
+
# server simply refuses to start later.
|
|
434
|
+
if as_user_sh "cd '${INSTALL_DIR}' && node -e \"require('sqlite3')\"" >/dev/null 2>&1; then
|
|
435
|
+
ok "sqlite3 loads"
|
|
436
|
+
else
|
|
437
|
+
warn "sqlite3 did not build on the first attempt - rebuilding it"
|
|
438
|
+
(( NEEDS_BUILD_TOOLS )) && info "expect around 40 minutes on a Pi Zero W; it is working as long as output keeps appearing"
|
|
439
|
+
as_user_sh "cd '${INSTALL_DIR}' && npm rebuild sqlite3 --foreground-scripts"
|
|
440
|
+
as_user_sh "cd '${INSTALL_DIR}' && node -e \"require('sqlite3')\"" >/dev/null 2>&1 \
|
|
441
|
+
|| { echo "sqlite3 still will not load - the server cannot run without it." >&2; exit 1; }
|
|
442
|
+
ok "sqlite3 loads after rebuilding"
|
|
443
|
+
fi
|
|
444
|
+
|
|
445
|
+
step "Setting up the server's directory"
|
|
446
|
+
as_user_sh "cd '${INSTALL_DIR}' && npx --no frugal-iot-init"
|
|
447
|
+
ok "configuration, directories and database in place"
|
|
448
|
+
|
|
449
|
+
step "Pointing the server at this Pi's own broker"
|
|
450
|
+
# One URL, used by the server's own logger and by the browser, so it has to be an address the phone
|
|
451
|
+
# or laptop showing the dashboard can reach - not localhost.
|
|
452
|
+
MQTT_YAML="${INSTALL_DIR}/config.d/mqtt.yaml"
|
|
453
|
+
CURRENT_BROKER="$(sed -n 's/^broker:[[:space:]]*//p' "$MQTT_YAML" 2>/dev/null | head -1 || true)"
|
|
454
|
+
WANTED_BROKER="ws://${BROKER_HOST}:9012"
|
|
455
|
+
if [[ "$CURRENT_BROKER" == "$WANTED_BROKER" ]]; then
|
|
456
|
+
skip "broker already ${WANTED_BROKER}"
|
|
457
|
+
else
|
|
458
|
+
as_user_sh "printf '# This Pi is its own broker - see INSTALLATION.md step 4\nbroker: %s\n' '${WANTED_BROKER}' > '${MQTT_YAML}'"
|
|
459
|
+
ok "broker set to ${WANTED_BROKER} (was ${CURRENT_BROKER:-unset})"
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
# ---------------------------------------------------------------- the broker
|
|
463
|
+
|
|
464
|
+
step "Installing the MQTT broker"
|
|
465
|
+
sudo_ env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq mosquitto mosquitto-clients
|
|
466
|
+
ok "mosquitto $(mosquitto -h 2>&1 | head -1 | awk '{print $3}' || true)"
|
|
467
|
+
|
|
468
|
+
step "Configuring the broker"
|
|
469
|
+
# Two listeners: 1883 for the sensor nodes, 9012 websockets for the browser. Shipped with the
|
|
470
|
+
# server, so it is copied rather than written here.
|
|
471
|
+
SRC_CONF="${INSTALL_DIR}/extras/mosquitto.conf"
|
|
472
|
+
[[ -f "$SRC_CONF" ]] || { echo "Expected ${SRC_CONF} to exist after frugal-iot-init" >&2; exit 1; }
|
|
473
|
+
if cmp -s "$SRC_CONF" /etc/mosquitto/conf.d/frugal-iot.conf 2>/dev/null; then
|
|
474
|
+
skip "/etc/mosquitto/conf.d/frugal-iot.conf already current"
|
|
475
|
+
else
|
|
476
|
+
sudo_ cp "$SRC_CONF" /etc/mosquitto/conf.d/frugal-iot.conf
|
|
477
|
+
ok "configuration copied to /etc/mosquitto/conf.d/frugal-iot.conf"
|
|
478
|
+
fi
|
|
479
|
+
# The broker refuses to start if the password file it is told about does not exist. It has to belong
|
|
480
|
+
# to the mosquitto user, because both the broker and mosquitto_passwd warn unless the file belongs to
|
|
481
|
+
# whoever opened it - and mosquitto_passwd writes a temporary file beside it, so the directory has to
|
|
482
|
+
# be writable by that user too.
|
|
483
|
+
PWFILE="$(grep -hE '^[[:space:]]*password_file[[:space:]]' /etc/mosquitto/conf.d/frugal-iot.conf | tail -1 | awk '{print $2}' || true)"
|
|
484
|
+
[[ -n "$PWFILE" ]] || { echo "No password_file line in /etc/mosquitto/conf.d/frugal-iot.conf" >&2; exit 1; }
|
|
485
|
+
if [[ -f "$PWFILE" ]]; then
|
|
486
|
+
skip "password file ${PWFILE} exists"
|
|
487
|
+
else
|
|
488
|
+
sudo_ install -o mosquitto -g mosquitto -m 600 /dev/null "$PWFILE"
|
|
489
|
+
ok "empty password file created at ${PWFILE}"
|
|
490
|
+
fi
|
|
491
|
+
sudo_ systemctl restart mosquitto
|
|
492
|
+
sleep 2
|
|
493
|
+
systemctl is-active --quiet mosquitto \
|
|
494
|
+
|| { echo "Mosquitto did not start. Its own log says why - the journal usually does not:" >&2
|
|
495
|
+
sudo_ tail -20 /var/log/mosquitto/mosquitto.log >&2 || true
|
|
496
|
+
exit 1; }
|
|
497
|
+
ok "broker running"
|
|
498
|
+
|
|
499
|
+
# ---------------------------------------------------------------- accounts
|
|
500
|
+
|
|
501
|
+
step "Creating the administrator login"
|
|
502
|
+
if as_user_sh "cd '${INSTALL_DIR}' && sqlite3 frugal-iot.db \"SELECT 1 FROM users WHERE username='superuser' AND hashed_password IS NOT NULL;\"" | grep -q 1; then
|
|
503
|
+
skip "superuser already has a password"
|
|
504
|
+
else
|
|
505
|
+
as_user_sh "cd '${INSTALL_DIR}' && npx --no frugal-iot-setpassword superuser '${SUPER_PW}'" >/dev/null
|
|
506
|
+
ok "superuser password set"
|
|
507
|
+
fi
|
|
508
|
+
|
|
509
|
+
step "Creating the organization"
|
|
510
|
+
if [[ -f "${INSTALL_DIR}/config.d/organizations/${ORG_ID}.yaml" ]]; then
|
|
511
|
+
skip "organization ${ORG_ID} already exists"
|
|
512
|
+
BROKER_PW="$(sed -n 's/^mqtt_password:[[:space:]]*//p' "${INSTALL_DIR}/config.d/organizations/${ORG_ID}.yaml" | head -1 | tr -d "\"'" || true)"
|
|
513
|
+
info "using the broker password already in its configuration"
|
|
514
|
+
else
|
|
515
|
+
# Writes the organization's yaml, creates a login account of the same name, grants it admin, makes
|
|
516
|
+
# its OTA directory, and adds it to the broker's password file
|
|
517
|
+
as_user_sh "cd '${INSTALL_DIR}' && npx --no frugal-iot-addorganization '${ORG_ID}' '${ORG_NAME}' '${ORG_EMAIL}' '${ORG_PHONE}' '${BROKER_PW}'"
|
|
518
|
+
ok "organization ${ORG_ID} created"
|
|
519
|
+
# addorganization set the web login password to the broker password as a side effect. They are for
|
|
520
|
+
# entirely different things, so give the login its own.
|
|
521
|
+
as_user_sh "cd '${INSTALL_DIR}' && npx --no frugal-iot-setpassword '${ORG_ID}' '${LOGIN_PW}'" >/dev/null
|
|
522
|
+
ok "web login password for ${ORG_ID} set, different from the broker password"
|
|
523
|
+
fi
|
|
524
|
+
sudo_ systemctl restart mosquitto # so it re-reads the password file
|
|
525
|
+
sleep 2
|
|
526
|
+
|
|
527
|
+
step "Testing the broker end to end"
|
|
528
|
+
# Proves the account, the password file and the port 1883 listener the nodes use all work together.
|
|
529
|
+
TEST_TOPIC="${ORG_ID}/installtest/hello"
|
|
530
|
+
SUB_OUT="$(mktemp)"
|
|
531
|
+
mosquitto_sub -h localhost -u "$ORG_ID" -P "$BROKER_PW" -t "$TEST_TOPIC" -C 1 -W 10 > "$SUB_OUT" 2>&1 &
|
|
532
|
+
SUB_PID=$!
|
|
533
|
+
sleep 2
|
|
534
|
+
mosquitto_pub -h localhost -u "$ORG_ID" -P "$BROKER_PW" -t "$TEST_TOPIC" -m '42'
|
|
535
|
+
wait "$SUB_PID" 2>/dev/null || true
|
|
536
|
+
if grep -qx '42' "$SUB_OUT"; then
|
|
537
|
+
ok "published and received on ${TEST_TOPIC}"
|
|
538
|
+
else
|
|
539
|
+
echo "The broker did not deliver a test message. It said:" >&2
|
|
540
|
+
cat "$SUB_OUT" >&2
|
|
541
|
+
rm -f "$SUB_OUT"
|
|
542
|
+
exit 1
|
|
543
|
+
fi
|
|
544
|
+
rm -f "$SUB_OUT"
|
|
545
|
+
|
|
546
|
+
# ---------------------------------------------------------------- the service
|
|
547
|
+
|
|
548
|
+
step "Running the server as a service"
|
|
549
|
+
# The shipped unit is written for user "pi" installing into /home/pi/frugal-iot, so the three lines
|
|
550
|
+
# that depend on that are rewritten for wherever this actually went.
|
|
551
|
+
SRC_SERVICE="${INSTALL_DIR}/extras/frugaliot.service"
|
|
552
|
+
[[ -f "$SRC_SERVICE" ]] || { echo "Expected ${SRC_SERVICE} to exist after frugal-iot-init" >&2; exit 1; }
|
|
553
|
+
TMP_SERVICE="$(mktemp)"
|
|
554
|
+
sed -e "s|^User=.*|User=${RUN_USER}|" \
|
|
555
|
+
-e "s|^WorkingDirectory=.*|WorkingDirectory=${INSTALL_DIR}|" \
|
|
556
|
+
-e "s|^ExecStart=/home/pi/frugal-iot/node_modules/.bin/frugal-iot-server|ExecStart=${INSTALL_DIR}/node_modules/.bin/frugal-iot-server|" \
|
|
557
|
+
"$SRC_SERVICE" > "$TMP_SERVICE"
|
|
558
|
+
if cmp -s "$TMP_SERVICE" /etc/systemd/system/frugaliot.service 2>/dev/null; then
|
|
559
|
+
skip "service file already current"
|
|
560
|
+
else
|
|
561
|
+
sudo_ cp "$TMP_SERVICE" /etc/systemd/system/frugaliot.service
|
|
562
|
+
ok "service installed, running as ${RUN_USER} from ${INSTALL_DIR}"
|
|
563
|
+
fi
|
|
564
|
+
rm -f "$TMP_SERVICE"
|
|
565
|
+
sudo_ systemctl daemon-reload
|
|
566
|
+
sudo_ systemctl enable -q frugaliot
|
|
567
|
+
sudo_ systemctl restart frugaliot
|
|
568
|
+
|
|
569
|
+
step "Checking the server answers"
|
|
570
|
+
PORT="$(sed -n 's/^port:[[:space:]]*//p' "${INSTALL_DIR}/config.d/server.yaml" | head -1 || true)"
|
|
571
|
+
PORT="${PORT:-8080}"
|
|
572
|
+
for attempt in $(seq 1 20); do
|
|
573
|
+
CODE="$(curl -s -o /dev/null -w '%{http_code}' -m 5 "http://localhost:${PORT}/" || true)"
|
|
574
|
+
[[ "$CODE" == "200" ]] && break
|
|
575
|
+
sleep 2
|
|
576
|
+
done
|
|
577
|
+
if [[ "${CODE:-}" == "200" ]]; then
|
|
578
|
+
ok "http://localhost:${PORT}/ answered 200"
|
|
579
|
+
else
|
|
580
|
+
echo "The server is not answering on port ${PORT} (last response: ${CODE:-none})." >&2
|
|
581
|
+
echo "What it logged:" >&2
|
|
582
|
+
sudo_ journalctl -u frugaliot -n 30 --no-pager >&2 || true
|
|
583
|
+
exit 1
|
|
584
|
+
fi
|
|
585
|
+
systemctl is-enabled --quiet frugaliot && ok "will start again at boot"
|
|
586
|
+
|
|
587
|
+
# ---------------------------------------------------------------- what is left to do
|
|
588
|
+
|
|
589
|
+
IP_ADDR="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
|
590
|
+
|
|
591
|
+
# Only the generated ones are worth printing - and printing a password you typed would put it in the
|
|
592
|
+
# log for no reason
|
|
593
|
+
was_generated() { local v; for v in ${GENERATED[@]+"${GENERATED[@]}"}; do [[ "$v" == "$1" ]] && return 0; done; return 1; }
|
|
594
|
+
shown() { if was_generated "$1"; then echo "${!1}"; else echo "(the one you gave)"; fi; }
|
|
595
|
+
|
|
596
|
+
if (( ${#REBOOT_REASONS[@]} )); then
|
|
597
|
+
REBOOT_TEXT=" 2. REBOOT NEEDED - $(printf '%s' "${REBOOT_REASONS[0]}")."
|
|
598
|
+
for r in "${REBOOT_REASONS[@]:1}"; do REBOOT_TEXT="${REBOOT_TEXT}"$'\n'" Also: ${r}."; done
|
|
599
|
+
REBOOT_TEXT="${REBOOT_TEXT}"$'\n'" sudo reboot"$'\n'" The server and broker both come back by themselves afterwards."
|
|
600
|
+
else
|
|
601
|
+
REBOOT_TEXT=" 2. No reboot needed. The kernel running now is the one on disk, and everything
|
|
602
|
+
installed here is already running."
|
|
603
|
+
fi
|
|
604
|
+
|
|
605
|
+
cat <<SUMMARY
|
|
606
|
+
|
|
607
|
+
=======================================================================
|
|
608
|
+
Frugal IoT is installed and running.
|
|
609
|
+
=======================================================================
|
|
610
|
+
|
|
611
|
+
Dashboard http://${BROKER_HOST}:${PORT}/
|
|
612
|
+
http://${IP_ADDR:-this-pi}:${PORT}/ (if .local does not resolve for you)
|
|
613
|
+
|
|
614
|
+
Logins superuser / $(shown SUPER_PW)
|
|
615
|
+
${ORG_ID} / $(shown LOGIN_PW)
|
|
616
|
+
|
|
617
|
+
Broker ws://${BROKER_HOST}:9012 for browsers
|
|
618
|
+
${BROKER_HOST}:1883 for sensor nodes
|
|
619
|
+
user ${ORG_ID}, password $(shown BROKER_PW)
|
|
620
|
+
|
|
621
|
+
Installed in ${INSTALL_DIR}, running as ${RUN_USER}
|
|
622
|
+
Log of this run ${LOG} (readable only by you; any generated password above is in it)
|
|
623
|
+
|
|
624
|
+
Still to do:
|
|
625
|
+
|
|
626
|
+
1. Your sensor nodes. This cannot be done from here - a node learns the broker by being
|
|
627
|
+
flashed with it, which happens on your workstation. Build each node's firmware with:
|
|
628
|
+
broker ${BROKER_HOST}
|
|
629
|
+
org ${ORG_ID}
|
|
630
|
+
password $(shown BROKER_PW)
|
|
631
|
+
They appear on the dashboard by themselves once they connect.
|
|
632
|
+
|
|
633
|
+
${REBOOT_TEXT}
|
|
634
|
+
|
|
635
|
+
3. HTTPS, if you want over-the-air firmware updates - ESP32 requires it for those.
|
|
636
|
+
See INSTALLATION.md step 10.
|
|
637
|
+
|
|
638
|
+
If anything looks wrong:
|
|
639
|
+
cd ${INSTALL_DIR} && npx --no frugal-iot-diagnostic
|
|
640
|
+
|
|
641
|
+
Note the server is deliberately quiet - it does not log every message or web request,
|
|
642
|
+
because writes wear out SD cards. To watch messages while setting nodes up, set
|
|
643
|
+
"verbose: true" in ${INSTALL_DIR}/config.d/logger.yaml and restart with
|
|
644
|
+
"sudo service frugaliot restart".
|
|
645
|
+
|
|
646
|
+
SUMMARY
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Files in this repository that are deliberately left out of the npm package.
|
|
2
|
+
#
|
|
3
|
+
# "files" in package.json is an allow-list, so anything not named there is left out whether or not
|
|
4
|
+
# that was intended. scripts/check-published-files.js compares the two and reports anything left
|
|
5
|
+
# out that is not listed here - so this file is what lets that report stay short enough to read.
|
|
6
|
+
#
|
|
7
|
+
# When it names something new, either add it to "files" in package.json (if installers need it) or
|
|
8
|
+
# add it here with a line saying why. Blank lines and #comments are ignored; "*" matches within a
|
|
9
|
+
# path segment and "**" across segments.
|
|
10
|
+
|
|
11
|
+
# --- Never publish: this installation's own secrets ---
|
|
12
|
+
# Organization configs hold mqtt_password, and the broker password file holds its hashes. Both are
|
|
13
|
+
# in git, which is its own problem, but they must certainly not go to npm as well.
|
|
14
|
+
config.d/organizations/*.yaml
|
|
15
|
+
extras/mosquitto_passwords
|
|
16
|
+
|
|
17
|
+
# --- Development-only: no use to somebody installing the server ---
|
|
18
|
+
.gitignore
|
|
19
|
+
claude.md
|
|
20
|
+
create-favicon.js
|
|
21
|
+
jest.config.js
|
|
22
|
+
vitest.config.js
|
|
23
|
+
test/**
|
|
24
|
+
|
|
25
|
+
# --- Placeholders ---
|
|
26
|
+
# Keeps the ota directory present in a clone; a real installation gets one from frugal-iot-init
|
|
27
|
+
ota/README.md
|