frugal-iot-server 2.0.1 → 2.0.3
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 +180 -142
- package/README.md +3 -0
- package/config.d/schema/modules.yaml +15 -0
- package/frugal-iot-createdb.sql +34 -1
- package/frugal-iot-server.js +117 -5
- package/lib/dynsec-plan.js +75 -1
- package/lib/dynsec-server.js +41 -1
- package/lib/dynsec-sync.js +35 -4
- package/lib/enrol.js +132 -29
- package/package.json +7 -4
- package/public/service-worker.js +1 -1
- package/scripts/addbridge-prod.zsh +81 -22
- package/scripts/addorganization.zsh +25 -5
- package/scripts/diagnostic.zsh +149 -27
- package/scripts/init.zsh +106 -6
- package/scripts/install-pi.sh +269 -44
- package/scripts/rebuild-dynsec.js +41 -4
package/INSTALLATION.md
CHANGED
|
@@ -25,6 +25,12 @@ and go to [Upgrading](#upgrading).
|
|
|
25
25
|
`npx --no frugal-iot-diagnostic` from your install directory — it inspects the whole installation and
|
|
26
26
|
reports what looks wrong. See [When something does not work](#when-something-does-not-work).
|
|
27
27
|
|
|
28
|
+
Adding `quiet` makes it say nothing at all unless something is wrong, and exit non-zero when it
|
|
29
|
+
does, so it can be run periodically rather than only when you already have a problem:
|
|
30
|
+
`npx --no frugal-iot-diagnostic quiet`. The word has no dash on purpose — npm parses the command
|
|
31
|
+
line itself and never passes a flag through to the script, and `-q` is worse still, because it is
|
|
32
|
+
npm's own abbreviation for `--quiet`.
|
|
33
|
+
|
|
28
34
|
---
|
|
29
35
|
|
|
30
36
|
## Which Raspberry Pi
|
|
@@ -189,17 +195,32 @@ bash install-pi.sh --org myfarm --name "My Farm" --email you@example.com --phone
|
|
|
189
195
|
```
|
|
190
196
|
|
|
191
197
|
It asks for anything it needs that you did not pass, says what it is doing as it goes, and stops at
|
|
192
|
-
the first thing that fails - leaving a log, and telling you to run `frugal-iot-diagnostic`.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
the first thing that fails - leaving a log, and telling you to run `frugal-iot-diagnostic`.
|
|
199
|
+
`--dry-run` checks the arguments and shows the plan without touching the machine.
|
|
200
|
+
|
|
201
|
+
**It is safe to run again, as many times as you like.** Every step that changes something checks
|
|
202
|
+
first and says `already done:` instead, so a run that stopped half way carries on from where it got
|
|
203
|
+
to rather than starting over. That matters most on a Pi Zero W, where compiling `sqlite3` takes
|
|
204
|
+
about 40 minutes and is not repeated once it has succeeded. It is also how to pick up a fix: re-run
|
|
205
|
+
it after upgrading the package and it will put right anything the newer version installs differently.
|
|
206
|
+
|
|
207
|
+
Two things it will tell you it cannot mend by itself, rather than looping:
|
|
208
|
+
|
|
209
|
+
* a `sqlite3` that still will not load after being rebuilt;
|
|
210
|
+
* a `/var/lib/mosquitto/dynamic-security.json` that exists while `config.d/secrets.yaml` has no
|
|
211
|
+
`dynsec_admin_password` - a previous run stopped between creating the one and recording the other,
|
|
212
|
+
and the password it chose was never written down. It tells you to delete that file and run again.
|
|
213
|
+
|
|
214
|
+
**Passwords.** You are asked for the two you will actually type - the `superuser` login and the
|
|
215
|
+
organization's web login - twice each, not echoed, and pressing Enter has one generated instead.
|
|
216
|
+
`--superuser-password` and `--login-password` skip the prompts, `--random-passwords` generates both,
|
|
217
|
+
and `--yes` asks nothing at all, which is what to use over ssh with no terminal. A password you
|
|
218
|
+
chose is not written to the log; a generated one has to be, or you would have no way of knowing it.
|
|
219
|
+
|
|
220
|
+
The organization's *broker* credential is no longer among them: it is generated and never shown,
|
|
221
|
+
because nothing on the Pi needs you to know it. Nodes are issued their own credential when they
|
|
222
|
+
enrol, and browsers derive their own when they log in. `--broker-password` still sets it, for an
|
|
223
|
+
organization whose nodes were flashed before enrolment existed.
|
|
203
224
|
|
|
204
225
|
**It tells you whether a reboot is needed**, rather than leaving you to wonder — it notices a kernel
|
|
205
226
|
or boot-firmware package in the upgrade, the images in `/boot` changing underneath it, and
|
|
@@ -209,8 +230,10 @@ Fetch it with `curl -O` and then run it, rather than piping curl into bash - pip
|
|
|
209
230
|
terminal to ask questions at.
|
|
210
231
|
|
|
211
232
|
It finishes with a live server and a tested broker. It cannot do step 9, pointing your nodes at it,
|
|
212
|
-
because a node learns its broker by being flashed with it - so it prints
|
|
213
|
-
|
|
233
|
+
because a node learns its broker by being flashed with it - so it prints what to build into them:
|
|
234
|
+
the broker host, the organization, and the organization's **enrolment secret**. A node presents that
|
|
235
|
+
secret once, is issued a broker credential of its own, and stores it; the secret grants nothing else,
|
|
236
|
+
no read and no write. Nor can it do step 10, HTTPS.
|
|
214
237
|
|
|
215
238
|
The steps below are what it does, in the same order, if you would rather do it by hand or need to
|
|
216
239
|
understand what went wrong, otherwise skip to Step 9.
|
|
@@ -242,23 +265,38 @@ which you may not have the password for.
|
|
|
242
265
|
address does not change. Sensor nodes and phones then have something stable to talk to even where
|
|
243
266
|
`.local` names do not work.
|
|
244
267
|
|
|
245
|
-
Now the packages the server needs.
|
|
268
|
+
Now the packages the server needs. Which set depends on whether this is a **32-bit or a 64-bit
|
|
269
|
+
installation** — and that is not the same question as which board it is, so check rather than assume:
|
|
246
270
|
|
|
247
|
-
|
|
271
|
+
```
|
|
272
|
+
dpkg --print-architecture
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`arm64` is a 64-bit installation; `armhf` is a 32-bit one. **Do not use `uname -m` for this.** It
|
|
276
|
+
reports the *kernel*, and Raspberry Pi OS 32-bit boots the 64-bit kernel by default on any board
|
|
277
|
+
that can run it — so a Pi 4 running the 32-bit image answers `armv8l` there while every binary on
|
|
278
|
+
it is 32-bit.
|
|
279
|
+
|
|
280
|
+
**If it said `arm64`:**
|
|
248
281
|
|
|
249
282
|
```
|
|
250
283
|
sudo apt install -y nodejs npm sqlite3 zsh
|
|
251
284
|
node -v
|
|
252
285
|
```
|
|
253
286
|
|
|
254
|
-
**
|
|
255
|
-
|
|
287
|
+
**If it said `armhf`** (any 32-bit installation, on a Pi Zero W or a Pi 4 alike) — three more
|
|
288
|
+
packages, because no ready-made binary is published for 32-bit ARM, so part of the server is
|
|
289
|
+
compiled in step 4 and a Lite image has no compiler:
|
|
256
290
|
|
|
257
291
|
```
|
|
258
292
|
sudo apt install -y nodejs npm sqlite3 zsh build-essential python3-dev python3-setuptools
|
|
259
293
|
node -v
|
|
260
294
|
```
|
|
261
295
|
|
|
296
|
+
> Skipping those three on a 32-bit installation does not fail until step 4, and then it fails deep
|
|
297
|
+
> inside pages of `gyp` output, ending in `ModuleNotFoundError: No module named 'distutils'`. If you
|
|
298
|
+
> are reading this after seeing that, install them and run step 4 again — nothing is lost.
|
|
299
|
+
|
|
262
300
|
What each is for:
|
|
263
301
|
|
|
264
302
|
* `nodejs` — the server needs **Node 18 or later**. Current Raspberry Pi OS (Debian 13, trixie)
|
|
@@ -321,8 +359,8 @@ The server is an npm package. Make a directory for this server to live in and in
|
|
|
321
359
|
that directory will hold your configuration, your data, and your database, while npm looks after
|
|
322
360
|
the software itself underneath it in `node_modules`.
|
|
323
361
|
|
|
324
|
-
**On a
|
|
325
|
-
nothing is compiled:
|
|
362
|
+
**On a 64-bit installation** (`dpkg --print-architecture` said `arm64`) — a few minutes, everything
|
|
363
|
+
arrives as ready-made binaries and nothing is compiled:
|
|
326
364
|
|
|
327
365
|
```
|
|
328
366
|
mkdir ~/frugal-iot
|
|
@@ -330,8 +368,10 @@ cd ~/frugal-iot
|
|
|
330
368
|
npm install frugal-iot-server
|
|
331
369
|
```
|
|
332
370
|
|
|
333
|
-
**On a
|
|
334
|
-
|
|
371
|
+
**On a 32-bit installation** (`armhf`) `sqlite3` is compiled here — a few minutes on a Pi 4, about
|
|
372
|
+
40 on a Pi Zero W. On a board with under about a gigabyte of memory add `--maxsockets 1 --no-audit
|
|
373
|
+
--no-fund`, which makes npm do one thing at a time: that lowers its peak memory as well as being
|
|
374
|
+
kinder to the SD card. This is the slow part; leave it running:
|
|
335
375
|
|
|
336
376
|
```
|
|
337
377
|
mkdir ~/frugal-iot
|
|
@@ -663,30 +703,50 @@ still answers. Your server is now installed.
|
|
|
663
703
|
|
|
664
704
|
### 9. Point your sensor nodes at the Pi
|
|
665
705
|
|
|
666
|
-
|
|
667
|
-
|
|
706
|
+
A node is flashed with an **enrolment secret**,
|
|
707
|
+
presents that once, and is issued a broker credential of its own which it stores. The secret grants only one thing — "create a node in this organization" — so it is not the thing to guard, and a node that is stolen cannot be used to read anyone else's data.
|
|
668
708
|
|
|
669
|
-
|
|
670
|
-
|
|
709
|
+
**First, get the enrolment secret.** On the dashboard on the Pi: **gear icon → Nodes → Enrolment secret**. It prints the line ready to paste.
|
|
710
|
+
(It is also in `config.d/secrets.yaml` as `enrolment_myfarm`.)
|
|
711
|
+
|
|
712
|
+
**Second, check the project exists.** A node states which project it is in, and enrolling into one the server does not know is refused. The organization you made in step 6 starts with none, so create one on the dashboard before flashing.
|
|
713
|
+
|
|
714
|
+
Then, on your workstation, in the PlatformIO project's `platformio-secrets-local.ini` for example ...:
|
|
715
|
+
|
|
716
|
+
```ini
|
|
717
|
+
[common]
|
|
718
|
+
|
|
719
|
+
build_flags_local =
|
|
720
|
+
'-D SYSTEM_FRUGAL_ORG="myfarm"'
|
|
721
|
+
'-D SYSTEM_FRUGAL_PROJECT="lotus"'
|
|
722
|
+
'-D SYSTEM_MQTT_HOST="frugaliot.local"'
|
|
723
|
+
'-D SYSTEM_MQTT_ENROL_URL="http://frugaliot.local:8080/enrol"'
|
|
724
|
+
'-D SYSTEM_MQTT_ENROL_SECRET="<the secret from the dashboard>"'
|
|
671
725
|
```
|
|
672
726
|
|
|
673
|
-
and point
|
|
727
|
+
Note `http`, not `https`, in the enrol URL: a Pi on your own network has no certificate, and step 10 is where that may change in the future. The defaults in `platformio.ini` point at the production server, so all five
|
|
728
|
+
lines are needed to aim a build at your Pi.
|
|
729
|
+
|
|
730
|
+
The sketch itself calls:
|
|
674
731
|
|
|
675
732
|
```cpp
|
|
676
|
-
frugal_iot.
|
|
733
|
+
frugal_iot.configure_mqtt_enrolled(SYSTEM_MQTT_HOST, SYSTEM_MQTT_ENROL_SECRET);
|
|
677
734
|
```
|
|
678
735
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
the
|
|
736
|
+
which is already what the examples do. Then rebuild and flash as usual. The node enrols on first boot and appears on the dashboard by itself.
|
|
737
|
+
|
|
738
|
+
> If the node does not connect, try the Pi's IP address in place of `frugaliot.local`, in both the
|
|
739
|
+
> host and the enrol URL. Resolving `.local` names needs mDNS support in the firmware, which is not
|
|
740
|
+
> something this guide has confirmed; an IP address avoids the question entirely, which is why step
|
|
741
|
+
> 3 suggests reserving one for the Pi in your router.
|
|
742
|
+
|
|
743
|
+
> A node whose enrolment is refused — wrong secret, unknown project, or awaiting approval — retries
|
|
744
|
+
> every half hour rather than giving up, because nobody can reach a node in a field to reboot it.
|
|
683
745
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
> confirmed; an IP address avoids the question entirely, which is why step 3 suggests reserving one
|
|
687
|
-
> for the Pi in your router.
|
|
746
|
+
**To rotate the secret**, add a new line above the old one in `config.d/secrets.yaml` and leave the
|
|
747
|
+
old one there until every node flashed with it has enrolled. Deleting it first strands them.
|
|
688
748
|
|
|
689
|
-
You can
|
|
749
|
+
You can watch nodes arriving without involving the UI, using the subscriber from step 6:
|
|
690
750
|
|
|
691
751
|
```
|
|
692
752
|
mosquitto_sub -h localhost -u myfarm -P '<broker-password>' -t '#' -v
|
|
@@ -698,8 +758,10 @@ port 1883 is reachable from off the Pi, which is what the nodes need.
|
|
|
698
758
|
### 9a. Optional: outgoing mail, so people can reset a forgotten password
|
|
699
759
|
|
|
700
760
|
Without this, a forgotten password can only be fixed by you, on the Pi, with
|
|
701
|
-
`npx --no frugal-iot-setpassword <username> <new-password>`.
|
|
702
|
-
|
|
761
|
+
`npx --no frugal-iot-setpassword <username> <new-password>`.
|
|
762
|
+
|
|
763
|
+
Until you set this up, the login page's "Forgot password?"
|
|
764
|
+
link says "Password reset is not available on this server".
|
|
703
765
|
|
|
704
766
|
An offline Pi cannot send mail at all, so skip this unless it has internet access.
|
|
705
767
|
|
|
@@ -819,10 +881,9 @@ surrounding work.
|
|
|
819
881
|
It comes in three parts, and only the first is a one-off:
|
|
820
882
|
|
|
821
883
|
* **11a — preparing a production server to accept bridges.** A TLS listener, a certificate, and an
|
|
822
|
-
access-control file. Done once for a given production server
|
|
823
|
-
|
|
824
|
-
* **
|
|
825
|
-
* **11c — pointing this Pi at it.** One command, run here, once per Pi.
|
|
884
|
+
access-control file. Done once for a given production server.
|
|
885
|
+
* **11b — authorizing this Pi on the production server.** One command, run on that server, once per Pi.
|
|
886
|
+
* **11c — pointing this Pi at it.** One command, run on the Pi, once per Pi.
|
|
826
887
|
|
|
827
888
|
**What you get, and what you do not.** While the link is up, readings appear on production within
|
|
828
889
|
a second or so. While it is down, the Pi records everything as usual and production simply has a
|
|
@@ -839,7 +900,7 @@ asks for it; with it, this Pi pulls that organization's logins and permissions f
|
|
|
839
900
|
fifteen minutes, so the same people can use this Pi's dashboard — including while production is
|
|
840
901
|
unreachable, which is the point of the Pi. It is optional: leave the token blank and the bridge
|
|
841
902
|
relays readings as before. Nothing derived from a password travels either way, and each server
|
|
842
|
-
issues its own broker credentials, so the same person has different ones
|
|
903
|
+
issues its own broker credentials, so the same person has different ones on the Pi and on production. Revoking a
|
|
843
904
|
permission on production removes it here on the next pull. See SECURITY.md.
|
|
844
905
|
|
|
845
906
|
#### 11a. Preparing a production server to accept bridges
|
|
@@ -917,118 +978,68 @@ installed by hand, the written procedure for doing that.
|
|
|
917
978
|
> Note the asymmetry with the Pi end below: a *certificate* change needs only `reload`, but adding
|
|
918
979
|
> or changing a *bridge* needs a full `restart`, because Mosquitto does not reload bridges.
|
|
919
980
|
|
|
920
|
-
|
|
921
|
-
`extras/aclfile` became part of the base install, a Pi set up with `install-pi.sh` already has this
|
|
922
|
-
and there is nothing to do here; the steps below are for a broker built before that, or one whose
|
|
923
|
-
configuration was written by hand.
|
|
924
|
-
|
|
925
|
-
Without it, any account that can log in to the broker can publish and subscribe anywhere on it, so
|
|
926
|
-
a per-Pi account would be no more confined than the organization's own. This is the part to plan
|
|
927
|
-
carefully, because Mosquitto's ACL file is **deny-by-default**: the moment the broker names an
|
|
928
|
-
`acl_file`, every existing account with no entry in it stops working. So the file that restricts new
|
|
929
|
-
bridge accounts has to grant the existing organizations what they already have, in the same change.
|
|
981
|
+
#### 11b. Authorizing this Pi (on the production server)
|
|
930
982
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
# Deny-by-default: an account with no rule here can connect but reach no topic at all.
|
|
983
|
+
Once per Pi, and there are two ways. **Use the dashboard** unless that server's broker has no
|
|
984
|
+
dynamic security plugin.
|
|
934
985
|
|
|
935
|
-
|
|
936
|
-
# first element of every Frugal IoT topic is the organization id - so this is what the nodes,
|
|
937
|
-
# dashboards and the server's own logger already do. It just stops being optional.
|
|
938
|
-
pattern readwrite %u/#
|
|
986
|
+
**From the dashboard** — no shell account on the production server needed, which is the point:
|
|
939
987
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
sudo chown mosquitto:mosquitto /etc/mosquitto/aclfile
|
|
947
|
-
sudo chmod 600 /etc/mosquitto/aclfile
|
|
948
|
-
```
|
|
988
|
+
1. Log in to production as someone with ADMIN on the organization.
|
|
989
|
+
2. Open the organization, gear icon → **Bridges**.
|
|
990
|
+
3. Under "Add a bridge", give the Pi a short name — `northfield`, `shed`, `village2`. It only
|
|
991
|
+
distinguishes one Pi from another within an organization, and becomes part of the broker account
|
|
992
|
+
name, so it cannot be changed afterwards without reconfiguring that Pi.
|
|
993
|
+
4. Copy the **broker password** and the **replica token** it shows. Both are shown once.
|
|
949
994
|
|
|
950
|
-
|
|
951
|
-
dropping to the `mosquitto` user, so a root-only file stops it starting. Mode 600 owned by
|
|
952
|
-
`mosquitto` rather than 644 owned by root — 2.0.21 accepts the latter but warns on every start that
|
|
953
|
-
a file it does not own, or one that is world readable, "will be refused by a future version". Then name it in the configuration
|
|
954
|
-
and restart:
|
|
955
|
-
|
|
956
|
-
```
|
|
957
|
-
echo 'acl_file /etc/mosquitto/aclfile' | sudo tee /etc/mosquitto/conf.d/zy-frugal-iot-acl.conf
|
|
958
|
-
sudo systemctl restart mosquitto
|
|
959
|
-
```
|
|
960
|
-
|
|
961
|
-
Check every existing organization can still log in **before** connecting any Pi — a wrong ACL file
|
|
962
|
-
locks out every node and dashboard at once, and the reason appears only in Mosquitto's own log. On a
|
|
963
|
-
Frugal IoT server the quickest check is its journal, which logs one line per organization:
|
|
964
|
-
|
|
965
|
-
```
|
|
966
|
-
sudo journalctl -u frugaliot -n 40 --no-pager | grep -iE "mqtt |not authoris"
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
Every organization should show `connect` and none should show `not authorized`. To back the change
|
|
970
|
-
out, delete `/etc/mosquitto/conf.d/zy-frugal-iot-acl.conf` and restart.
|
|
971
|
-
|
|
972
|
-
Nothing needs adding here per Pi: `frugal-iot-addbridge-prod` in 11b appends each bridge's own rule.
|
|
973
|
-
|
|
974
|
-
> `pattern readwrite %u/#` on its own denies `$SYS`, which would stop `frugal-iot-diagnostic`
|
|
975
|
-
> reporting whether a bridge is connected — it reads `$SYS/broker/connection/+/state` as the
|
|
976
|
-
> organization. The second rule above restores exactly that one topic and nothing else. Reading all
|
|
977
|
-
> of `$SYS` would also work and would hand every account the broker's client counts, subscription
|
|
978
|
-
> counts and traffic totals, which on a multi-organization broker tells each organization about the
|
|
979
|
-
> others.
|
|
980
|
-
|
|
981
|
-
#### 11b. Authorizing this Pi (run on the production server)
|
|
982
|
-
|
|
983
|
-
Once per Pi. On the production server, from its own directory:
|
|
995
|
+
**From a shell on the production server**, which is what to use if its broker predates the plugin:
|
|
984
996
|
|
|
985
997
|
```
|
|
986
998
|
npx --no frugal-iot-addbridge-prod <org-id> <site-name>
|
|
987
999
|
```
|
|
988
1000
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
adds its access-control rule, reloads the broker, and prints both the password and the exact
|
|
992
|
-
command to run in 11c.
|
|
1001
|
+
Either way you end up with a broker account `bridge-<site-name>`, its access-control rules, a
|
|
1002
|
+
replica token, and the command to run in 11c.
|
|
993
1003
|
|
|
994
|
-
Three things
|
|
1004
|
+
Three things worth knowing, because each is a way this quietly fails:
|
|
995
1005
|
|
|
996
1006
|
* **The organization must already exist on the production server**, or the readings will arrive at
|
|
997
1007
|
its broker and nothing will record them — that server's logger subscribes per organization,
|
|
998
1008
|
driven by the files in its `config.d/organizations/`. If it is missing, add it there first with
|
|
999
1009
|
`frugal-iot-addorganization`, using the *same* organization id as this Pi.
|
|
1000
|
-
* **Each Pi gets its own account**, rather than sharing the organization's
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
touching it.
|
|
1010
|
+
* **Each Pi gets its own account**, rather than sharing the organization's, so it can be confined to
|
|
1011
|
+
one site and revoked on its own.
|
|
1012
|
+
* **Doing it again for a site that already exists re-issues both credentials**, and the ones that Pi
|
|
1013
|
+
is using stop working — from the dashboard it asks first. That is how to recover from having lost
|
|
1014
|
+
them, and how to revoke a Pi you no longer trust; it is not a way to look them up again.
|
|
1006
1015
|
|
|
1007
|
-
|
|
1008
|
-
the output.
|
|
1016
|
+
Neither credential is stored anywhere you can read back, so copy both before you lose the output.
|
|
1009
1017
|
|
|
1010
1018
|
#### 11c. Bridging this Pi (run on the Pi)
|
|
1011
1019
|
|
|
1012
|
-
Once per Pi.
|
|
1020
|
+
Once per Pi. Have both credentials from 11b to hand first — it asks for each in turn:
|
|
1021
|
+
|
|
1022
|
+
* the **broker password** for `bridge-<site-name>`, which the MQTT bridge authenticates with;
|
|
1023
|
+
* the **replica token**, which lets this Pi pull production's logins so the same people can log in
|
|
1024
|
+
here. Press Enter to skip it: a bridge relays readings perfectly well without one, and you can
|
|
1025
|
+
re-run this later to add it.
|
|
1026
|
+
|
|
1027
|
+
They are two different credentials for two different connections — MQTT on port 8883 for the first,
|
|
1028
|
+
HTTPS for the second — so neither substitutes for the other.
|
|
1013
1029
|
|
|
1014
1030
|
```
|
|
1015
1031
|
cd ~/frugal-iot
|
|
1016
1032
|
npx --no frugal-iot-addbridge-pi <org-id> <prod-host> bridge-<site-name>
|
|
1017
1033
|
```
|
|
1018
1034
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
bridge actually connected.
|
|
1023
|
-
|
|
1024
|
-
The restart is unavoidable: Mosquitto does not pick up bridges on a reload signal, so `reload`
|
|
1025
|
-
appears to succeed and changes nothing. It briefly disconnects every node, which they recover from
|
|
1026
|
-
on their own.
|
|
1035
|
+
Having asked for those, it checks the far end is reachable and that its certificate is valid for
|
|
1036
|
+
that name, writes `/etc/mosquitto/conf.d/frugal-iot-bridge.conf` and — if you gave a token —
|
|
1037
|
+
`config.d/replica.yaml` with the token in `config.d/secrets.yaml`, restarts the broker, and reports
|
|
1038
|
+
whether the bridge actually connected. It briefly disconnects every node, which they recover from on
|
|
1039
|
+
their own.
|
|
1027
1040
|
|
|
1028
1041
|
> The configuration goes into `/etc/mosquitto/conf.d/` and not into this installation's
|
|
1029
|
-
> `config.d/`,
|
|
1030
|
-
> is served to any logged-in browser by `/config.json`, so a credential put there would not stay
|
|
1031
|
-
> private. The file is written mode 600 for the same reason.
|
|
1042
|
+
> `config.d/`, to keep the password out of `/config.json`. The file is written mode 600 for the same reason.
|
|
1032
1043
|
|
|
1033
1044
|
To undo it: `sudo rm /etc/mosquitto/conf.d/frugal-iot-bridge.conf && sudo systemctl restart
|
|
1034
1045
|
mosquitto`. The Pi goes back to being self-contained and keeps everything it has recorded.
|
|
@@ -1222,24 +1233,51 @@ journalctl -u frugaliot -n 30
|
|
|
1222
1233
|
npm ls frugal-iot-server
|
|
1223
1234
|
```
|
|
1224
1235
|
|
|
1225
|
-
`frugal-iot-init` leaves your files alone
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
`
|
|
1236
|
+
`frugal-iot-init` leaves your own files alone. Two sections of its output are worth reading.
|
|
1237
|
+
|
|
1238
|
+
**"These files were left as you have them, but this release ships a different version"** covers the
|
|
1239
|
+
sensor schema under `config.d/schema/`, and gives you a `diff` command for each. If you added a
|
|
1240
|
+
sensor type of your own, that is exactly what it should say; if you did not, this release changed
|
|
1241
|
+
the schema and the difference is worth reading.
|
|
1242
|
+
|
|
1243
|
+
**"Installed elsewhere from extras/, and older than this release"** covers the files that live
|
|
1244
|
+
somewhere else — the broker configuration, its ACL, the systemd unit. It compares what is actually
|
|
1245
|
+
in `/etc`, not the copy in `extras/`, because that copy is never overwritten and so differs for ever
|
|
1246
|
+
once a release changes it. To bring anything it names up to date, re-run the installer:
|
|
1229
1247
|
|
|
1230
1248
|
```
|
|
1231
|
-
|
|
1232
|
-
sudo cp node_modules/frugal-iot-server/extras/aclfile /etc/mosquitto/aclfile
|
|
1233
|
-
sudo chown mosquitto:mosquitto /etc/mosquitto/aclfile && sudo chmod 600 /etc/mosquitto/aclfile
|
|
1234
|
-
sudo systemctl restart mosquitto
|
|
1235
|
-
sudo cp node_modules/frugal-iot-server/extras/frugaliot.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart frugaliot
|
|
1249
|
+
bash node_modules/frugal-iot-server/scripts/install-pi.sh
|
|
1236
1250
|
```
|
|
1237
1251
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1252
|
+
It is safe to re-run: it changes only what is out of date, and it is the *only* correct way to
|
|
1253
|
+
install these three, because none of them can simply be copied into place:
|
|
1254
|
+
|
|
1255
|
+
| file | why a plain `cp` is wrong |
|
|
1256
|
+
|------|---------------------------|
|
|
1257
|
+
| `extras/mosquitto.conf` | carries `plugin PLUGIN_PATH_SET_BY_INSTALLER`. The path differs by distribution, so the installer substitutes it. Copied as-is, **the broker will not start.** |
|
|
1258
|
+
| `extras/aclfile` | `frugal-iot-addbridge-prod` appends a rule per Pi. Copying over it deletes them, and a mosquitto `acl_file` is deny-by-default — so every bridge goes on logging in and reaches nothing at all. |
|
|
1259
|
+
| `extras/frugaliot.service` | written for user `pi` in `/home/pi/frugal-iot`; the installer rewrites `User`, `WorkingDirectory` and `ExecStart` for wherever this actually went. |
|
|
1260
|
+
|
|
1261
|
+
The installer will not overwrite an ACL file that has per-bridge rules in it. If this release adds a
|
|
1262
|
+
rule to a broker that has them, it says which lines are missing and leaves the file alone for you to
|
|
1263
|
+
add them by hand, then `sudo systemctl reload mosquitto`.
|
|
1264
|
+
|
|
1265
|
+
> **If your broker's configuration was assembled by hand** — several small files in
|
|
1266
|
+
> `/etc/mosquitto/conf.d/` rather than the single `frugal-iot.conf` the installer writes — then
|
|
1267
|
+
> `frugal-iot-init` reports those files under "Nothing at the usual place, so these were not
|
|
1268
|
+
> compared", and re-running the installer is *not* what you want: it would add `frugal-iot.conf`
|
|
1269
|
+
> alongside what you already have, declaring the same listeners and the same plugin twice. Compare
|
|
1270
|
+
> by hand instead, and add only what is missing:
|
|
1271
|
+
>
|
|
1272
|
+
> ```
|
|
1273
|
+
> sudo grep -hvE '^\s*(#|$)' /etc/mosquitto/conf.d/*.conf | sort -u > /tmp/running.txt
|
|
1274
|
+
> grep -hvE '^\s*(#|$)' node_modules/frugal-iot-server/extras/mosquitto.conf | sort -u > /tmp/shipped.txt
|
|
1275
|
+
> diff /tmp/running.txt /tmp/shipped.txt
|
|
1276
|
+
> ```
|
|
1277
|
+
>
|
|
1278
|
+
> Lines only in the running config are usually this machine's own — TLS `certfile`/`keyfile`, a
|
|
1279
|
+
> bridge `listener`, a `password_file` in a different place. Lines only in the shipped one are what
|
|
1280
|
+
> this release added.
|
|
1243
1281
|
|
|
1244
1282
|
> It does not compare `config.yaml`, `config.d/mqtt.yaml`, `config.d/logger.yaml` or
|
|
1245
1283
|
> `config.d/server.yaml`, because those hold your own settings and would differ every time. If a
|
package/README.md
CHANGED
|
@@ -100,6 +100,9 @@ broker logins, what has been logged - and changes nothing:
|
|
|
100
100
|
```
|
|
101
101
|
npx --no frugal-iot-diagnostic
|
|
102
102
|
```
|
|
103
|
+
Add `quiet` and it says nothing unless something is wrong, exiting non-zero when it does, which is
|
|
104
|
+
what makes it usable as a periodic check: `npx --no frugal-iot-diagnostic quiet`. No dash, because
|
|
105
|
+
npm consumes flags before they reach the script - and `-q` is npm's own `--quiet`.
|
|
103
106
|
|
|
104
107
|
To upgrade later: `npm update frugal-iot-server` then `npx --no frugal-iot-init`.
|
|
105
108
|
|
|
@@ -249,6 +249,21 @@ gps:
|
|
|
249
249
|
- leaf: hdop
|
|
250
250
|
- leaf: position
|
|
251
251
|
- leaf: utc_time
|
|
252
|
+
# HC-SR04 ultrasonic module, timed trigger/echo - Sensor_HCSR04. The distance is the same shape
|
|
253
|
+
# as the Modbus `ultrasonic` module's, but the module is only rated to 4m. The temperature is a
|
|
254
|
+
# compensation input for the speed of sound, not a reading - so controlfloat, as for ens160.
|
|
255
|
+
hcsr04:
|
|
256
|
+
name: "HC-SR04 Distance"
|
|
257
|
+
topics:
|
|
258
|
+
- leaf: hcsr04
|
|
259
|
+
leaf_from: ultrasonic
|
|
260
|
+
max: 4000
|
|
261
|
+
- leaf: temperature
|
|
262
|
+
leaf_from: controlfloat
|
|
263
|
+
name: Air Temperature
|
|
264
|
+
min: -20
|
|
265
|
+
max: 50
|
|
266
|
+
color: "#ff0000"
|
|
252
267
|
health:
|
|
253
268
|
name: System
|
|
254
269
|
insidefrugaliot: true
|
package/frugal-iot-createdb.sql
CHANGED
|
@@ -96,7 +96,8 @@ CREATE TABLE IF NOT EXISTS `bridges` (
|
|
|
96
96
|
-- Keyed by (org, nodeid), NOT by project: a node states its own project, so keying on it would let
|
|
97
97
|
-- a denied node return by claiming a different one.
|
|
98
98
|
--
|
|
99
|
-
-- Persisted,
|
|
99
|
+
-- Persisted, because a restart must not un-deny a node. So is node_attempts below, for a
|
|
100
|
+
-- different reason - see the note there.
|
|
100
101
|
CREATE TABLE IF NOT EXISTS `node_grants` (
|
|
101
102
|
`org` TEXT NOT NULL,
|
|
102
103
|
`project` TEXT NOT NULL DEFAULT '',
|
|
@@ -193,3 +194,35 @@ CREATE TABLE IF NOT EXISTS notification_queue (
|
|
|
193
194
|
-- silently becoming the superuser.
|
|
194
195
|
INSERT OR IGNORE INTO `users` (`id`, `username`, `name`, `organization`) VALUES (0, 'everyone', 'Everyone', '');
|
|
195
196
|
INSERT OR IGNORE INTO `users` (`id`, `username`, `name`, `organization`) VALUES (1, 'superuser', 'Superuser', '');
|
|
197
|
+
|
|
198
|
+
-- Every node that asked to enrol and was refused, for the Nodes card on the dashboard.
|
|
199
|
+
--
|
|
200
|
+
-- This is the ONLY way an administrator learns that a node exists: a node id is not discoverable
|
|
201
|
+
-- from the server, so a node nobody can reach physically can only announce itself by asking and
|
|
202
|
+
-- being refused. The rows are therefore UNTRUSTED - anyone can make an id appear by attempting
|
|
203
|
+
-- enrolment - so each keeps when it asked, from where, and why it was refused, for an administrator
|
|
204
|
+
-- to recognise rather than to trust.
|
|
205
|
+
--
|
|
206
|
+
-- Persisted, although it was in memory first and the reasoning for that was not wrong: the nodes
|
|
207
|
+
-- table has no last_seen column because that would be a database write per reading. But an
|
|
208
|
+
-- enrolment attempt is not a reading. It is capped by the rate limiter at 20 per organization per
|
|
209
|
+
-- hour, repeats from one node are throttled to one write a minute, and a rate-limited request is
|
|
210
|
+
-- not written at all - so the write rate here is a few rows an hour, not one per reading.
|
|
211
|
+
--
|
|
212
|
+
-- What losing them on a restart cost was the thing this table exists to give: a node retries only
|
|
213
|
+
-- every half hour, so a server restart made a node that was asking invisible for up to thirty
|
|
214
|
+
-- minutes, which is indistinguishable from a node that is not asking at all. That is precisely the
|
|
215
|
+
-- case an administrator is trying to diagnose.
|
|
216
|
+
--
|
|
217
|
+
-- Keyed by (org, nodeid) like node_grants, and for the same reason: a node states its own project,
|
|
218
|
+
-- so keying on it would let one node occupy several rows by varying what it claims.
|
|
219
|
+
CREATE TABLE IF NOT EXISTS `node_attempts` (
|
|
220
|
+
`org` TEXT NOT NULL,
|
|
221
|
+
`project` TEXT NOT NULL DEFAULT '',
|
|
222
|
+
`nodeid` TEXT NOT NULL,
|
|
223
|
+
`reason` TEXT NOT NULL DEFAULT '',
|
|
224
|
+
`from_addr` TEXT NOT NULL DEFAULT '',
|
|
225
|
+
`at` INTEGER NOT NULL,
|
|
226
|
+
`count` INTEGER NOT NULL DEFAULT 1,
|
|
227
|
+
UNIQUE(`org`, `nodeid`)
|
|
228
|
+
);
|