frugal-iot-server 0.3.3 → 0.3.5
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 +239 -110
- package/README.md +4 -9
- package/config.d/server.yaml +4 -4
- package/extras/frugaliot.service +19 -8
- package/extras/mosquitto.conf +5 -1
- package/frugal-iot-server.js +32 -2
- package/package.json +4 -3
- package/scripts/addorganization.zsh +55 -11
- package/scripts/diagnostic.zsh +289 -0
- package/scripts/init.zsh +33 -6
package/INSTALLATION.md
CHANGED
|
@@ -13,9 +13,13 @@ Two hardware paths are covered:
|
|
|
13
13
|
If you already have a working Linux server (not a Pi), you do not need this document —
|
|
14
14
|
see [README.md](https://github.com/mitra42/frugal-iot-server/blob/main/README.md) instead.
|
|
15
15
|
|
|
16
|
-
> **Status:**
|
|
17
|
-
>
|
|
18
|
-
>
|
|
16
|
+
> **Status:** the Raspberry Pi 4 path has been followed end to end on hardware (see
|
|
17
|
+
> [Tested on](#tested-on)), and corrected from what that run found. Part B, and HTTPS/OTA in step
|
|
18
|
+
> A10, are not written yet. Remaining uncertainties are listed under
|
|
19
|
+
> [Open questions](#open-questions) — please add your findings there as you go.
|
|
20
|
+
|
|
21
|
+
**Already have a Frugal IoT server running and just want a newer version?** Skip everything below
|
|
22
|
+
and go to [Upgrading](#upgrading).
|
|
19
23
|
|
|
20
24
|
---
|
|
21
25
|
|
|
@@ -25,8 +29,11 @@ see [README.md](https://github.com/mitra42/frugal-iot-server/blob/main/README.md
|
|
|
25
29
|
|
|
26
30
|
**Hardware**
|
|
27
31
|
|
|
28
|
-
* Raspberry Pi 4 Model B.
|
|
29
|
-
|
|
32
|
+
* Raspberry Pi 4 Model B. Any RAM size — a running server uses around 250 MB, so even the 1 GB
|
|
33
|
+
model has plenty of room. (Measured on a 4 GB Pi 4: 231 MB in use with the server running.)
|
|
34
|
+
* A microSD card. 8 GB is enough for the system and software — a working install occupies about
|
|
35
|
+
5 GB — but sensor data accumulates on this card for as long as the server runs, so 16 GB or larger
|
|
36
|
+
is the safer choice. Class 10 / A1 or better.
|
|
30
37
|
* The official Raspberry Pi USB-C power supply (5 V / 3 A). Phone chargers frequently cause
|
|
31
38
|
random reboots and corrupted SD cards — this is the single most common cause of "it doesn't work".
|
|
32
39
|
* A way to write the SD card from your laptop: a built-in SD slot or a USB card reader.
|
|
@@ -48,10 +55,10 @@ see [README.md](https://github.com/mitra42/frugal-iot-server/blob/main/README.md
|
|
|
48
55
|
|
|
49
56
|
**Have these to hand in case the headless setup does not come up**
|
|
50
57
|
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
* An Ethernet cable from the Pi to your router — the simplest way in if the Pi does not appear on
|
|
59
|
+
the Wi-Fi, and you can then sort the Wi-Fi out over SSH (step A2).
|
|
60
|
+
* A micro-HDMI to HDMI cable plus a monitor, and a USB keyboard — for when the Pi is nowhere near the
|
|
61
|
+
router, or you want to see boot messages.
|
|
55
62
|
|
|
56
63
|
### A1. Write the operating system to the SD card
|
|
57
64
|
|
|
@@ -72,9 +79,11 @@ a desktop would only consume memory and SD card space.
|
|
|
72
79
|
* Type the SSID exactly as the network broadcasts it, including capitals. Imager does not store
|
|
73
80
|
your Wi-Fi password; it converts it into a 64-character key using the SSID, so a mistyped SSID
|
|
74
81
|
produces a key that fails even though the password was right.
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
* If the Pi does not join the network, a mistyped SSID or password is much the likeliest cause -
|
|
83
|
+
neither is echoed back to you here, and Imager combines the two into a key, so a slip in either
|
|
84
|
+
looks the same later. Step A2 fixes it in a couple of minutes with `nmtui`.
|
|
85
|
+
* A WPA3 network cannot use the key Imager derives at all, so on one of those expect to finish the
|
|
86
|
+
Wi-Fi setup with `nmtui` regardless.
|
|
78
87
|
10. Enable SSH, either choose *Use password authentication* or paste your public key if you already use SSH keys.
|
|
79
88
|
11. Leave Raspberry Pi Connect off for now - feel free to experiment with this, as we haven't yet.
|
|
80
89
|
12. Confirm that you want to save settings and write to the card, and click through operating system prompts wanting to stop you !
|
|
@@ -101,8 +110,14 @@ Say `yes` to the fingerprint question, then give the password you set in Imager.
|
|
|
101
110
|
* Wait another minute and try again — the Pi may still be on its first boot.
|
|
102
111
|
* Log in to your Wi-Fi router's admin page and look for a device called `frugaliot` in its
|
|
103
112
|
list of connected clients; note its IP address and use that instead: `ssh pi@192.168.1.42`.
|
|
113
|
+
* **Plug an Ethernet cable from the Pi into your router.** Nothing needs configuring — the Pi picks
|
|
114
|
+
up an address, and `ssh pi@frugaliot.local` then works over the cable. This is the least effort
|
|
115
|
+
way in if the Pi is within reach of the router, and once you are logged in you can sort the Wi-Fi
|
|
116
|
+
out over SSH with `nmtui` as below, no monitor or keyboard needed. Unplug the cable afterwards and
|
|
117
|
+
check that Wi-Fi alone still gets you in.
|
|
104
118
|
* Plug in the HDMI and keyboard, log in at the console, and run `ip addr` to read the IP address,
|
|
105
|
-
and `sudo journalctl -b | grep -i wpa` to see why Wi-Fi failed.
|
|
119
|
+
and `sudo journalctl -b | grep -i wpa` to see why Wi-Fi failed. Use this when the Pi is nowhere
|
|
120
|
+
near the router, or when you want to see boot messages.
|
|
106
121
|
|
|
107
122
|
**If Wi-Fi did not connect at all** — `ip addr` shows no address on `wlan0`, and the journal has
|
|
108
123
|
`WPA: 4-Way Handshake failed - pre-shared key may be incorrect` — then the Pi found your network
|
|
@@ -145,20 +160,31 @@ sudo reboot
|
|
|
145
160
|
|
|
146
161
|
Wait a minute, then `ssh pi@frugaliot.local` again.
|
|
147
162
|
|
|
148
|
-
**
|
|
149
|
-
|
|
163
|
+
**Find the Pi's IP address**, because later steps can need it — for the broker URL if you will view
|
|
164
|
+
the dashboard on a phone, and for your sensor nodes. From your laptop:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
ping -c1 frugaliot.local
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The address it prints is the Pi's. That is easier than hunting through your router's admin pages,
|
|
171
|
+
which you may not have the password for.
|
|
172
|
+
|
|
173
|
+
**Recommended:** if you can get into your router, give the Pi a fixed (reserved) IP address, so that
|
|
174
|
+
address does not change. Sensor nodes and phones then have something stable to talk to even where
|
|
175
|
+
`.local` names do not work.
|
|
150
176
|
|
|
151
177
|
### A3. Install the prerequisites
|
|
152
178
|
|
|
153
179
|
```
|
|
154
180
|
sudo apt install -y nodejs npm sqlite3 zsh
|
|
155
181
|
node -v
|
|
156
|
-
npm -v
|
|
157
182
|
```
|
|
158
183
|
|
|
159
|
-
* `nodejs` — the server needs **Node 18 or later**. Raspberry Pi OS
|
|
160
|
-
which is fine. If `node -v`
|
|
161
|
-
version from
|
|
184
|
+
* `nodejs` — the server needs **Node 18 or later**. Current Raspberry Pi OS (Debian 13, trixie)
|
|
185
|
+
provides 20.19.2, which is fine. Older images shipped Node 18, also fine. If `node -v` reports
|
|
186
|
+
anything below 18, install a current version from
|
|
187
|
+
[NodeSource](https://github.com/nodesource/distributions) instead.
|
|
162
188
|
* `npm` — installs the server; it is a separate package from `nodejs` on Debian.
|
|
163
189
|
* `sqlite3` — the database the server keeps its accounts in.
|
|
164
190
|
* `zsh` — the setup commands in step A6 are zsh scripts.
|
|
@@ -186,9 +212,10 @@ This copies in the configuration files, creates the `data`, `ota` and `config.d/
|
|
|
186
212
|
directories, and creates the database. It never overwrites anything already there, so it is also
|
|
187
213
|
what you run after an upgrade to pick up newly added configuration.
|
|
188
214
|
|
|
189
|
-
>
|
|
190
|
-
>
|
|
191
|
-
> `sudo apt install -y build-essential python3` then
|
|
215
|
+
> On a 64-bit Raspberry Pi OS this installs ready-made binaries and compiles nothing. If it ever
|
|
216
|
+
> does stop while building `sqlite3`, no binary was available for your platform, so install the
|
|
217
|
+
> compiler toolchain and try again: `sudo apt install -y build-essential python3` then
|
|
218
|
+
> `npm install frugal-iot-server`.
|
|
192
219
|
|
|
193
220
|
Everything from here on is run from `~/frugal-iot`, and `npx` is how you run the server's commands
|
|
194
221
|
without having to know where npm put them.
|
|
@@ -210,9 +237,11 @@ This one URL is used both by the server's own logger and by the browser UI, so i
|
|
|
210
237
|
WebSocket (`ws://`) address that **your phone or laptop browser** can reach, not just one that
|
|
211
238
|
works on the Pi.
|
|
212
239
|
|
|
213
|
-
>
|
|
214
|
-
>
|
|
215
|
-
>
|
|
240
|
+
> `frugaliot.local` is known to work from a laptop and from an iPhone. Android phones generally
|
|
241
|
+
> cannot resolve `.local` names, so if you will view the dashboard on Android, put the Pi's IP
|
|
242
|
+
> address here instead — `broker: ws://192.168.1.42:9012`, using the address `ping -c1
|
|
243
|
+
> frugaliot.local` reported in step A2 — and reserve that address in your router if you can, so it
|
|
244
|
+
> does not change under you.
|
|
216
245
|
|
|
217
246
|
### A5. Install and configure the MQTT broker (Mosquitto)
|
|
218
247
|
|
|
@@ -237,15 +266,22 @@ sudo cp extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf
|
|
|
237
266
|
`/etc/mosquitto/mosquitto.conf`, which keeps its own settings for logging and persistence.)
|
|
238
267
|
|
|
239
268
|
That configuration names a password file, and Mosquitto will not start if the file is missing, so
|
|
240
|
-
create an empty one. The accounts inside it get created for you in the next step
|
|
241
|
-
`addorganization.zsh` — which runs as you rather than as root, hence the ownership:
|
|
269
|
+
create an empty one. The accounts inside it get created for you in the next step:
|
|
242
270
|
|
|
243
271
|
```
|
|
244
|
-
sudo
|
|
245
|
-
sudo chown ${USER}:mosquitto /etc/mosquitto/mosquitto_passwords
|
|
246
|
-
sudo chmod 640 /etc/mosquitto/mosquitto_passwords
|
|
272
|
+
sudo install -o mosquitto -g mosquitto -m 600 /dev/null /var/lib/mosquitto/passwords
|
|
247
273
|
```
|
|
248
274
|
|
|
275
|
+
That makes an empty file belonging to the `mosquitto` user, readable by nobody else — one command
|
|
276
|
+
instead of a `touch`, a `chown` and a `chmod`.
|
|
277
|
+
|
|
278
|
+
> The ownership matters, and is easy to get wrong. Mosquitto warns unless the password file belongs
|
|
279
|
+
> to whoever opened it, and the broker runs as the `mosquitto` user — so the file belongs to
|
|
280
|
+
> `mosquitto`, and it lives under `/var/lib/mosquitto` (which that user owns) rather than
|
|
281
|
+
> `/etc/mosquitto` (which root owns). `mosquitto_passwd` also writes a temporary backup file
|
|
282
|
+
> alongside it, so it needs to write to that directory too, not just to the file. Step A6 runs it as
|
|
283
|
+
> the right user for you.
|
|
284
|
+
|
|
249
285
|
Start the broker and have it start at every boot:
|
|
250
286
|
|
|
251
287
|
```
|
|
@@ -254,7 +290,8 @@ sudo systemctl restart mosquitto
|
|
|
254
290
|
systemctl status mosquitto
|
|
255
291
|
```
|
|
256
292
|
|
|
257
|
-
`systemctl status` should say `active (running)
|
|
293
|
+
`systemctl status` should say `active (running)`, and returns you to the prompt. (If the output is
|
|
294
|
+
long enough that it opens a pager instead, `q` gets you out.) If it is not running,
|
|
258
295
|
`sudo journalctl -u mosquitto -n 50` will show what it objected to.
|
|
259
296
|
|
|
260
297
|
**Check both listeners are open:**
|
|
@@ -349,45 +386,71 @@ If you get `Connection Refused: not authorised`, the password does not match the
|
|
|
349
386
|
|
|
350
387
|
### A7. Start the server by hand and check it
|
|
351
388
|
|
|
389
|
+
From your install directory — `npx` looks for the server in the current directory's `node_modules`,
|
|
390
|
+
so this only works there:
|
|
391
|
+
|
|
352
392
|
```
|
|
393
|
+
cd ~/frugal-iot
|
|
353
394
|
npx frugal-iot-server
|
|
354
395
|
```
|
|
355
396
|
|
|
356
|
-
|
|
397
|
+
> If npx answers with `Need to install the following packages: frugal-iot-server` and asks to
|
|
398
|
+
> continue, you are in the wrong directory. Say no, `cd ~/frugal-iot`, and try again — otherwise
|
|
399
|
+
> npx fetches a throwaway copy that has none of your configuration.
|
|
400
|
+
|
|
401
|
+
It lists each configuration file as it reads it, then:
|
|
357
402
|
|
|
358
403
|
```
|
|
404
|
+
readYamlConfigFile ./config.yaml
|
|
405
|
+
readYamlConfigDir ./config.d
|
|
406
|
+
... one line per configuration file ...
|
|
407
|
+
Broker ws://frugaliot.local:9012 - organizations: dev
|
|
359
408
|
Doing OTA updates at /ota_update from /home/pi/frugal-iot/ota
|
|
360
409
|
Serving /node_modules from ./node_modules
|
|
361
410
|
User Database exists
|
|
362
411
|
Opened user database
|
|
412
|
+
Exec-ed starting SQL
|
|
413
|
+
Created logger client for API integration
|
|
414
|
+
Created push manager for Farm-Platform data push
|
|
415
|
+
Mounted API routes at /api
|
|
416
|
+
Added API error handler
|
|
363
417
|
Serving /data from ./data
|
|
364
418
|
Server starting on port 8080
|
|
365
|
-
Serving from ./node_modules/frugal-iot-client
|
|
366
419
|
mqtt dev connecting
|
|
367
420
|
mqtt dev connect
|
|
421
|
+
Subscribing topic dev/# 0
|
|
368
422
|
```
|
|
369
423
|
|
|
370
|
-
|
|
371
|
-
`mqtt dev connect` means the server reached
|
|
372
|
-
|
|
424
|
+
Check the `Broker` line names your own broker and your organization. The lines that matter most are
|
|
425
|
+
the last three: `mqtt dev connect` means the server reached the broker and authenticated, and
|
|
426
|
+
`Subscribing topic dev/#` means it is listening for your nodes. If instead you see repeated
|
|
427
|
+
`mqtt dev close`, `offline`, or `Not authorized`, the broker URL or the password is wrong — recheck
|
|
373
428
|
`config.d/mqtt.yaml` and that the password in `config.d/organizations/dev.yaml` matches the `dev`
|
|
374
429
|
broker account.
|
|
375
430
|
|
|
431
|
+
Once nodes are reporting, each reading is logged as it arrives, so this output keeps scrolling.
|
|
432
|
+
|
|
376
433
|
Now open a browser on your laptop or phone at:
|
|
377
434
|
|
|
378
435
|
```
|
|
379
436
|
http://frugaliot.local:8080
|
|
380
437
|
```
|
|
381
438
|
|
|
382
|
-
(or `http://<the Pi's IP>:8080`).
|
|
383
|
-
username `dev` with the login password you set with `frugal-iot-setpassword` — not the broker
|
|
384
|
-
password. (`superuser` and its password work too.)
|
|
439
|
+
(or `http://<the Pi's IP>:8080`).
|
|
385
440
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
441
|
+
> On a Mac, the browser will ask something like *"Allow Google Chrome Helper to find devices on
|
|
442
|
+
> local networks"* the first time. **Allow it** — without that permission the browser cannot look up
|
|
443
|
+
> `frugaliot.local`, nor reach the broker at that name, so the page and the live data both fail.
|
|
444
|
+
|
|
445
|
+
You land on the Frugal IoT home page. Click **Dashboard**, and log in as username `dev` with the
|
|
446
|
+
login password you set with `frugal-iot-setpassword` — not the broker password. (`superuser` and its
|
|
447
|
+
password work too.)
|
|
448
|
+
|
|
449
|
+
Once you are through to the dashboard and have selected your organization, its MQTT status should
|
|
450
|
+
show **connected**. That is the browser authenticating to the broker as your organization, over the
|
|
451
|
+
WebSocket listener on port 9012, from a different machine — the last untested piece of the chain.
|
|
452
|
+
If it does not connect, check that the `broker:` URL in `config.d/mqtt.yaml` is one this browser can
|
|
453
|
+
actually resolve: a phone that cannot look up `.local` names needs the Pi's IP address there instead.
|
|
391
454
|
|
|
392
455
|
Until a sensor node reports in there will be no data to look at, but the dashboard should load.
|
|
393
456
|
|
|
@@ -395,32 +458,26 @@ Stop the server with `Ctrl-C` before continuing.
|
|
|
395
458
|
|
|
396
459
|
### A8. Run the server as a service
|
|
397
460
|
|
|
398
|
-
So that it starts automatically at boot and restarts if it crashes
|
|
461
|
+
So that it starts automatically at boot and restarts if it crashes. The file that
|
|
462
|
+
`npx frugal-iot-init` put in `extras/` already describes this installation — user `pi`, installed
|
|
463
|
+
into `/home/pi/frugal-iot` — so it needs no editing:
|
|
399
464
|
|
|
400
465
|
```
|
|
401
466
|
sudo cp extras/frugaliot.service /etc/systemd/system/frugaliot.service
|
|
402
|
-
sudo nano /etc/systemd/system/frugaliot.service
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
Three lines need to match your Pi:
|
|
406
|
-
|
|
407
|
-
```
|
|
408
|
-
User=pi
|
|
409
|
-
WorkingDirectory=/home/pi/frugal-iot
|
|
410
|
-
ExecStart=/home/pi/frugal-iot/node_modules/.bin/frugal-iot-server
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
`WorkingDirectory` must be the directory you installed into, because that is where the server
|
|
414
|
-
finds its configuration and database. `ExecStart` is the command npm created for you there — check
|
|
415
|
-
it exists with `ls node_modules/.bin/frugal-iot-server`. Then:
|
|
416
|
-
|
|
417
|
-
```
|
|
418
467
|
sudo systemctl daemon-reload
|
|
419
|
-
sudo systemctl enable frugaliot
|
|
420
|
-
sudo systemctl start frugaliot
|
|
468
|
+
sudo systemctl enable --now frugaliot
|
|
421
469
|
systemctl status frugaliot
|
|
422
470
|
```
|
|
423
471
|
|
|
472
|
+
`enable --now` both starts it and sets it to start at boot. `status` should report
|
|
473
|
+
`active (running)`.
|
|
474
|
+
|
|
475
|
+
> Only if you departed from this guide — a different username, or a directory other than
|
|
476
|
+
> `~/frugal-iot` — edit `User`, `WorkingDirectory` and `ExecStart` in
|
|
477
|
+
> `/etc/systemd/system/frugaliot.service` to match, then `sudo systemctl daemon-reload` and
|
|
478
|
+
> `sudo systemctl restart frugaliot`. `WorkingDirectory` is the important one: it is where the
|
|
479
|
+
> server looks for its configuration and database.
|
|
480
|
+
|
|
424
481
|
To watch its log output, which is where the `mqtt dev connect` and incoming-reading messages now go:
|
|
425
482
|
|
|
426
483
|
```
|
|
@@ -432,21 +489,37 @@ still answers. Your server is now installed.
|
|
|
432
489
|
|
|
433
490
|
### A9. Point your sensor nodes at the Pi
|
|
434
491
|
|
|
435
|
-
Your ESP8266/ESP32 nodes
|
|
436
|
-
|
|
437
|
-
and the organization and project names to publish under.
|
|
492
|
+
Your ESP8266/ESP32 nodes are told which broker to use in their sketch — `main.cpp`, or the `.ino`
|
|
493
|
+
file if you build in the Arduino IDE. Look for a line like:
|
|
438
494
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
495
|
+
```cpp
|
|
496
|
+
frugal_iot.configure_mqtt("frugaliot.naturalinnovation.org", "dev", "public");
|
|
497
|
+
```
|
|
442
498
|
|
|
443
|
-
|
|
499
|
+
and point it at your Pi instead:
|
|
500
|
+
|
|
501
|
+
```cpp
|
|
502
|
+
frugal_iot.configure_mqtt("frugaliot.local", "dev", "<broker-password>");
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
The three arguments are the broker's host, the organization, and that organization's broker
|
|
506
|
+
password. The organization must be the one you created in step A6, because it is the first part of
|
|
507
|
+
every topic the node publishes to, and the password is the *broker* password from that step — not
|
|
508
|
+
the login password. Then rebuild and flash the node as usual.
|
|
509
|
+
|
|
510
|
+
> If the node does not connect, try the Pi's IP address in place of `frugaliot.local`. Resolving
|
|
511
|
+
> `.local` names needs mDNS support in the firmware, which is not something this guide has
|
|
512
|
+
> confirmed; an IP address avoids the question entirely, which is why step A2 suggests reserving one
|
|
513
|
+
> for the Pi in your router.
|
|
514
|
+
|
|
515
|
+
You can confirm nodes are reporting without involving the UI, using the subscriber from step A6:
|
|
444
516
|
|
|
445
517
|
```
|
|
446
518
|
mosquitto_sub -h localhost -u dev -P '<broker-password>' -t '#' -v
|
|
447
519
|
```
|
|
448
520
|
|
|
449
|
-
Every reading from every node should scroll past.
|
|
521
|
+
Every reading from every node should scroll past. Seeing anything here also proves the broker's
|
|
522
|
+
port 1883 is reachable from off the Pi, which is what the nodes need.
|
|
450
523
|
|
|
451
524
|
### A10. HTTPS and over-the-air firmware updates
|
|
452
525
|
|
|
@@ -461,6 +534,83 @@ That section will cover: a DNS name for the Pi, port forwarding on the router, n
|
|
|
461
534
|
proxy in front of port 8080, and certificates from Let's Encrypt via certbot. Both require the Pi
|
|
462
535
|
to have internet access, which is the opposite of the offline case this guide is aimed at.
|
|
463
536
|
|
|
537
|
+
### Known limitation: the clock on an offline Pi
|
|
538
|
+
|
|
539
|
+
A Raspberry Pi has no battery-backed clock. While it has internet access it sets its time from the
|
|
540
|
+
network and everything is correct — which is what you will see during this install. Fully offline it
|
|
541
|
+
cannot: Raspberry Pi OS saves the time periodically and restores that value at boot, so after a
|
|
542
|
+
power cut the Pi comes up believing it is whenever it last saved, and the gap never gets made up.
|
|
543
|
+
|
|
544
|
+
To see whether the Pi's clock is actually right at any moment:
|
|
545
|
+
|
|
546
|
+
```
|
|
547
|
+
timedatectl
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
`System clock synchronized: yes` means it has reached a time server and the clock is trustworthy.
|
|
551
|
+
`no`, with `NTP service: active`, means it is trying but has not succeeded — normal on a Pi with no
|
|
552
|
+
route to the internet, and the point at which the timestamps below become a concern.
|
|
553
|
+
|
|
554
|
+
What that affects:
|
|
555
|
+
|
|
556
|
+
* **Logged data is stamped with the wrong time**, so graphs and history drift after each power cut.
|
|
557
|
+
* **Relaying MQTT and watching devices live are unaffected** — the dashboard shows current values
|
|
558
|
+
whatever the Pi believes the date to be.
|
|
559
|
+
|
|
560
|
+
If timestamps matter to you on an installation with no internet, the fix today is a hardware RTC
|
|
561
|
+
module on the Pi's GPIO header. A possible future fix within Frugal IoT itself: the dashboard knows
|
|
562
|
+
the time of the phone or laptop viewing it, so it could hand that to the server, which could adopt
|
|
563
|
+
it whenever its own clock looks implausible (a date far in the past). Not accurate to the second,
|
|
564
|
+
but close enough for sensor data.
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Upgrading
|
|
569
|
+
|
|
570
|
+
To move an existing server to a newer release, from the directory you installed into:
|
|
571
|
+
|
|
572
|
+
```
|
|
573
|
+
cd ~/frugal-iot
|
|
574
|
+
npm update frugal-iot-server
|
|
575
|
+
npx frugal-iot-init
|
|
576
|
+
sudo systemctl restart frugaliot
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Taking those in turn:
|
|
580
|
+
|
|
581
|
+
* `npm update` fetches the new version of the server, and of the client and logger it depends on.
|
|
582
|
+
* `frugal-iot-init` adds any configuration file or directory the new version expects and you do not
|
|
583
|
+
have yet. It never changes files you already have, so your settings, database and data are safe.
|
|
584
|
+
* `systemctl restart frugaliot` is what actually puts the new version into service — without it the
|
|
585
|
+
old one keeps running from memory, and nothing appears to have changed. (Skip this if you are
|
|
586
|
+
running the server by hand rather than as a service; stop it with `Ctrl-C` and start it again.)
|
|
587
|
+
|
|
588
|
+
Check it came back up, and is running the version you expect:
|
|
589
|
+
|
|
590
|
+
```
|
|
591
|
+
systemctl status frugaliot
|
|
592
|
+
journalctl -u frugaliot -n 30
|
|
593
|
+
npm ls frugal-iot-server
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
`frugal-iot-init` leaves your files alone, so watch its output for a section headed **"These files
|
|
597
|
+
were left as you have them, but this release ships a different version"**. It gives you the `diff`
|
|
598
|
+
command for each. That matters most for the files you copy somewhere else — a changed
|
|
599
|
+
`extras/mosquitto.conf` or `extras/frugaliot.service` does nothing until you install it again:
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
sudo cp extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf && sudo systemctl restart mosquitto
|
|
603
|
+
sudo cp extras/frugaliot.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart frugaliot
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
> It does not compare `config.yaml`, `config.d/mqtt.yaml` or `config.d/server.yaml`, because those
|
|
607
|
+
> hold your own settings and would differ every time. If a release note mentions a new setting in
|
|
608
|
+
> one of them, compare it yourself:
|
|
609
|
+
> `diff config.d/server.yaml node_modules/frugal-iot-server/config.d/server.yaml`
|
|
610
|
+
|
|
611
|
+
Your organizations, accounts, database and logged data are untouched by an upgrade — they live in
|
|
612
|
+
this directory, not in `node_modules`.
|
|
613
|
+
|
|
464
614
|
---
|
|
465
615
|
|
|
466
616
|
## Part B — Raspberry Pi Zero W
|
|
@@ -471,8 +621,9 @@ and what has to be checked.
|
|
|
471
621
|
First, work out which board you have, because they are very different:
|
|
472
622
|
|
|
473
623
|
* **Raspberry Pi Zero 2 W** — 64-bit ARM (Cortex-A53), 512 MB RAM. Should follow **Part A**
|
|
474
|
-
almost unchanged: choose *Raspberry Pi Zero 2 W* in Imager, and everything else applies.
|
|
475
|
-
|
|
624
|
+
almost unchanged: choose *Raspberry Pi Zero 2 W* in Imager, and everything else applies. Running
|
|
625
|
+
the server needs about 250 MB, so 512 MB should be enough; the pinch point is more likely to be
|
|
626
|
+
`npm install`, which is far hungrier than the running server.
|
|
476
627
|
* **Raspberry Pi Zero W** (the original) — 32-bit ARMv6 (BCM2835), 512 MB RAM. This is the
|
|
477
628
|
awkward one: it needs the 32-bit Raspberry Pi OS, and **the official Node.js builds no longer
|
|
478
629
|
support ARMv6**. Unofficial ARMv6 builds exist at
|
|
@@ -489,8 +640,9 @@ Differences to expect on either Zero:
|
|
|
489
640
|
* Everything will be slow. `npm install` may take a long time, especially if `sqlite3` has to be
|
|
490
641
|
compiled from source — which is likely on ARMv6, since no ready-made binaries are published.
|
|
491
642
|
|
|
492
|
-
**
|
|
493
|
-
|
|
643
|
+
**Both boards are to be tested**, one of each having been ordered for the purpose. This section gets
|
|
644
|
+
written once we know how they behave — in particular whether a usable Node exists for the original
|
|
645
|
+
Zero W's ARMv6.
|
|
494
646
|
|
|
495
647
|
---
|
|
496
648
|
|
|
@@ -502,47 +654,24 @@ they get settled.
|
|
|
502
654
|
|
|
503
655
|
**Part A, to check while installing**
|
|
504
656
|
|
|
505
|
-
1.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
2. **Mosquitto listeners** (step A5) — confirm that copying `extras/mosquitto.conf` into `conf.d`
|
|
509
|
-
does not collide with the packaged default configuration, and that both 1883 and 9012 are
|
|
510
|
-
reachable from another machine (`mosquitto_sub -h frugaliot.local ...` from your laptop).
|
|
511
|
-
3. **Empty password file** (step A5) - Mosquitto is started with a `password_file` that exists but
|
|
512
|
-
is empty, because the first account is not created until step A6. Confirm
|
|
513
|
-
it starts happily like that. If it refuses, the fix is to create a throwaway account with
|
|
514
|
-
`sudo mosquitto_passwd -c -b /etc/mosquitto/mosquitto_passwords unused unused` before starting it.
|
|
515
|
-
4. **`frugal-iot-addorganization` writing the broker password** (step A6) — step A5 chowns
|
|
516
|
-
`/etc/mosquitto/mosquitto_passwords` to your user so the script can add the account without
|
|
517
|
-
sudo. Confirm it does, and that it reports "Set mosquitto password" rather than a warning.
|
|
518
|
-
5. **`.local` name resolution** (step A2) — does `frugaliot.local` work from your laptop, and from
|
|
519
|
-
an Android phone? If Android fails as expected, the guide should recommend IP addresses more strongly.
|
|
520
|
-
6. **First-run output** (step A7) — the expected startup output above is adapted from README.md and
|
|
521
|
-
a run on a development machine; replace it with the actual output from the Pi.
|
|
522
|
-
7. **Time and dates while offline** — an offline Pi has no internet clock to sync with, and no
|
|
523
|
-
battery-backed clock, so after a power cut it starts with a wrong date until something corrects
|
|
524
|
-
it. For a data logger writing timestamped files, that matters. Does the logger cope? Should this
|
|
525
|
-
guide recommend a hardware RTC module, or a way for a phone or laptop on the LAN to set the time?
|
|
657
|
+
1. **`.local` from an Android phone** (step A2, A4) - confirmed working from a laptop (with the
|
|
658
|
+
local-network permission granted on a Mac) and from an iPhone. Android is expected to fail, which
|
|
659
|
+
is what the note in A4 assumes; worth confirming on a real Android phone.
|
|
526
660
|
|
|
527
661
|
**Needs information I do not have**
|
|
528
662
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
9. **Which Pi Zero** (Part B) — original Zero W, Zero 2 W, or both.
|
|
533
|
-
10. **Bridging to the shared server** — the local broker could optionally bridge to
|
|
663
|
+
2. **Pi Zero W and Zero 2 W** (Part B) — both boards are on order; Part B stays unwritten until they
|
|
664
|
+
have been tried. The open technical question is Node on the original Zero W's ARMv6.
|
|
665
|
+
3. **Bridging to the shared server** — the local broker could optionally bridge to
|
|
534
666
|
naturalinnovation.org so data also reaches the shared server. Not covered here; a later task.
|
|
535
|
-
|
|
667
|
+
4. **Organization naming** — this guide sets up exactly one organization named `dev`, because that
|
|
536
668
|
is the node firmware's default. Is that the right default for a farm installation, or should the
|
|
537
669
|
guide encourage a meaningful organization id (which then has to be set in the node firmware too)?
|
|
538
|
-
12. **Upgrading an existing installation** — `npm update frugal-iot-server` followed by
|
|
539
|
-
`npx frugal-iot-init` should be all it takes, since init adds missing configuration without
|
|
540
|
-
touching what is there. Not yet tried on a server that has been running for a while.
|
|
541
670
|
|
|
542
|
-
|
|
671
|
+
## Tested on
|
|
543
672
|
|
|
544
|
-
|
|
673
|
+
What the guide has actually been proven against — add a row for each run:
|
|
545
674
|
|
|
546
|
-
| Date | Board | OS image | Node
|
|
547
|
-
| --- | --- | --- | --- | --- |
|
|
548
|
-
|
|
|
675
|
+
| Date | Board | OS image | Node | Server | Result |
|
|
676
|
+
| --- | --- | --- | --- | --- | --- |
|
|
677
|
+
| 2026-07-31 | Pi 4 Model B, 4 GB | Raspberry Pi OS Lite 64-bit, Debian 13.6 (trixie) | 20.19.2 and npm 9.2.0, both from `apt` | frugal-iot-server 0.3.3 from npm | Steps A1–A8 completed, plus an upgrade over the top of it. Wi-Fi from Imager did not connect and was fixed with `nmtui` (A2). `sqlite3` installed as a prebuilt binary, nothing compiled. Mosquitto started with an empty password file, and `frugal-iot-addorganization` wrote the broker account without sudo. Dashboard reached over `frugaliot.local` from a Mac (Chrome, after allowing local network access) and from an iPhone, MQTT status *connected*. A9 (sensor nodes) and A10 (HTTPS/OTA) not exercised. |
|
package/README.md
CHANGED
|
@@ -51,25 +51,20 @@ If its working correctly you should see something like
|
|
|
51
51
|
readYamlConfigFile ./config.yaml
|
|
52
52
|
... and then reading each of the other files
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
server: { port: 8080 },
|
|
56
|
-
mqtt: { broker: 'wss://frugaliot.naturalinnovation.org/wss' },
|
|
57
|
-
organizations: {
|
|
58
|
-
dev: { mqtt_password: 'public', projects: [Array] }
|
|
59
|
-
}
|
|
60
|
-
}
|
|
54
|
+
Broker wss://frugaliot.naturalinnovation.org/wss - organizations: dev
|
|
61
55
|
Doing OTA updates at /ota_update from ...some path.../ota
|
|
62
56
|
Serving /node_modules from ./node_modules
|
|
63
57
|
User Database exists
|
|
64
58
|
Opened user database
|
|
59
|
+
Exec-ed starting SQL
|
|
65
60
|
Serving /data from ./data
|
|
66
61
|
Server starting on port 8080
|
|
67
|
-
Serving from ./node_modules/frugal-iot-client
|
|
68
62
|
mqtt dev connecting
|
|
69
63
|
mqtt dev connect
|
|
64
|
+
Subscribing topic dev/# 0
|
|
70
65
|
Received dev/lotus/esp8266-85ea2b/humidity 71.8
|
|
71
66
|
```
|
|
72
|
-
Where the
|
|
67
|
+
Where the broker and organizations are reported back,
|
|
73
68
|
then it successfully connects to the mqtt server
|
|
74
69
|
and receives data from nodes attached to it.
|
|
75
70
|
|
package/config.d/server.yaml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
port: 8080
|
|
2
2
|
#Production mode
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
nodemodulesdir: ./node_modules
|
|
4
|
+
htmldir: ./node_modules/frugal-iot-client
|
|
5
5
|
# Development mode (uncomment these two, and comment out the two above, when working on the client)
|
|
6
|
-
htmldir: ../frugal-iot-client
|
|
7
|
-
nodemodulesdir: ../frugal-iot-client/node_modules
|
|
6
|
+
#htmldir: ../frugal-iot-client
|
|
7
|
+
#nodemodulesdir: ../frugal-iot-client/node_modules
|
|
8
8
|
# The path to the private directory is relative to the server's working directory.
|
|
9
9
|
privatedir: ./private
|
|
10
10
|
publicdir: ./public
|
package/extras/frugaliot.service
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
1
|
+
# Systemd unit for the Frugal IoT server.
|
|
2
|
+
#
|
|
3
|
+
# As shipped this matches the Raspberry Pi installation in INSTALLATION.md: user "pi", installed
|
|
4
|
+
# with npm into /home/pi/frugal-iot. If that is what you have, install it unedited:
|
|
5
|
+
# sudo cp extras/frugaliot.service /etc/systemd/system/frugaliot.service
|
|
6
|
+
# sudo systemctl daemon-reload
|
|
7
|
+
# sudo systemctl enable --now frugaliot
|
|
8
|
+
#
|
|
9
|
+
# On any other machine, change User and WorkingDirectory to the account and the directory you
|
|
10
|
+
# installed into, and ExecStart to match (see the git clone alternative below).
|
|
3
11
|
[Unit]
|
|
4
12
|
Description=frugal-iot server
|
|
5
|
-
|
|
13
|
+
# mosquitto is listed for the common case of the broker running on this same machine; harmless if
|
|
14
|
+
# it is not installed, because systemd ignores ordering against units that do not exist.
|
|
15
|
+
After=network-online.target mosquitto.service
|
|
16
|
+
Wants=network-online.target
|
|
6
17
|
|
|
7
18
|
[Service]
|
|
8
19
|
Type=simple
|
|
9
|
-
User=
|
|
20
|
+
User=pi
|
|
10
21
|
# The directory holding this server's config.d, database and data - where npm install was run
|
|
11
|
-
WorkingDirectory=/
|
|
22
|
+
WorkingDirectory=/home/pi/frugal-iot
|
|
12
23
|
# Installed with npm, this is the command npm put in that directory:
|
|
13
|
-
ExecStart=/
|
|
24
|
+
ExecStart=/home/pi/frugal-iot/node_modules/.bin/frugal-iot-server
|
|
14
25
|
# From a git clone instead, run the file directly (check the node path with "which node"):
|
|
15
26
|
#ExecStart=/usr/bin/node ./frugal-iot-server.js
|
|
16
27
|
Restart=always
|
|
17
28
|
RestartSec=10
|
|
18
|
-
StandardOutput=
|
|
19
|
-
StandardError=
|
|
29
|
+
StandardOutput=journal
|
|
30
|
+
StandardError=journal
|
|
20
31
|
SyslogIdentifier=frugaliot
|
|
21
32
|
|
|
22
33
|
[Install]
|
package/extras/mosquitto.conf
CHANGED
|
@@ -3,4 +3,8 @@ protocol mqtt
|
|
|
3
3
|
listener 9012
|
|
4
4
|
protocol websockets
|
|
5
5
|
#http_dir /home/mitra/github_mitra42/frugal-iot/html
|
|
6
|
-
|
|
6
|
+
# Kept under /var/lib/mosquitto, which belongs to the mosquitto user, rather than /etc/mosquitto,
|
|
7
|
+
# which belongs to root. Both the broker and mosquitto_passwd warn unless the file belongs to
|
|
8
|
+
# whoever opened it, and mosquitto_passwd also writes a temporary backup beside it, so the
|
|
9
|
+
# directory has to be writable by that same user. See INSTALLATION.md step A5.
|
|
10
|
+
password_file /var/lib/mosquitto/passwords
|
package/frugal-iot-server.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*Δ /admin (get) dashboard for administrators - includes OTA and will include permission management
|
|
31
31
|
P /ota_update (post) protected place to upload new binaries (OTAUPDATE)
|
|
32
32
|
XP /ota_list/:org list all ota files for an organization
|
|
33
|
+
XP /ota_get/:org/*remainingpath download a binary so the client can flash it over USB
|
|
33
34
|
O /private Serve up private files under authentication - currently unused
|
|
34
35
|
* /register (post) register a new user
|
|
35
36
|
*/
|
|
@@ -313,7 +314,6 @@ function addProjectToConfig(org, id, name) {
|
|
|
313
314
|
}
|
|
314
315
|
// Read the projects table for every configured organization and add them to config.organizations
|
|
315
316
|
function loadProjectsIntoConfig(cb) {
|
|
316
|
-
console.log("XXX loadProjectsIntoConfig");
|
|
317
317
|
each(Object.keys(config.organizations), (org, cb) => {
|
|
318
318
|
get_projects_list(org, (err, projects) => {
|
|
319
319
|
if (err) {
|
|
@@ -659,13 +659,25 @@ function debugRoutes(req, res, next) {
|
|
|
659
659
|
next();
|
|
660
660
|
}
|
|
661
661
|
// Main for server
|
|
662
|
+
// Everything is read relative to the working directory, so being in the wrong one is the most
|
|
663
|
+
// common way to fail to start - say so plainly rather than reporting ENOENT on ./config.yaml.
|
|
664
|
+
access('./config.yaml', constants.R_OK, (err) => {
|
|
665
|
+
if (err) {
|
|
666
|
+
console.error(`No config.yaml in ${process.cwd()}`);
|
|
667
|
+
console.error("Run this from the directory this server was installed into - the one holding");
|
|
668
|
+
console.error("config.d and frugal-iot.db. If it is a new directory, set it up with: npx frugal-iot-init");
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
662
672
|
mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
663
673
|
// Note side effect leaves copy of config in the mqttLogger
|
|
664
674
|
if (err) {
|
|
665
675
|
console.error(err);
|
|
666
676
|
} else {
|
|
667
677
|
/* global */ config = configobj;
|
|
668
|
-
|
|
678
|
+
// Summarize rather than dumping the whole config: it is mostly the sensor schema, and it holds
|
|
679
|
+
// each organization's mqtt_password, which should not be going to the console and the journal.
|
|
680
|
+
console.log("Broker", config.mqtt.broker, "- organizations:", Object.keys(config.organizations).join(", ") || "(none)");
|
|
669
681
|
// Could genericize config defaults
|
|
670
682
|
if (!config.morgan) {
|
|
671
683
|
config.morgan = ':method :url :req[range] :status :res[content-length] :response-time ms :req[referer]'
|
|
@@ -909,6 +921,24 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
909
921
|
});
|
|
910
922
|
}
|
|
911
923
|
);
|
|
924
|
+
// Download an OTA binary so the client can flash it over USB (see FLASH_PLAN.md in
|
|
925
|
+
// frugal-iot-client). Deliberately not /ota_update, which is unauthenticated for devices
|
|
926
|
+
// and answers 304 rather than sending bytes.
|
|
927
|
+
app.get('/ota_get/:org/*remainingpath',
|
|
928
|
+
loggedInOrFail,
|
|
929
|
+
can_OTAUPDATE,
|
|
930
|
+
(req,res) => {
|
|
931
|
+
let remainingpath = req.params.remainingpath.join('/');
|
|
932
|
+
let filepath = `${config.server.otadir}/${req.params.org}/${sanitize(remainingpath)}/firmware.bin`;
|
|
933
|
+
console.log("Sending OTA file", filepath);
|
|
934
|
+
res.sendFile(filepath, {}, (err) => {
|
|
935
|
+
if (err) {
|
|
936
|
+
console.error("Error sending ota file:", filepath, err);
|
|
937
|
+
if (!res.headersSent) res.status(404).send(err.message);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
);
|
|
912
942
|
app.get('/people_list/:org',
|
|
913
943
|
loggedInOrFail,
|
|
914
944
|
can_ADMIN, // Gets org from URL
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"express": "^5.2.1",
|
|
13
13
|
"express-session": "^1.19.0",
|
|
14
14
|
"frugal-iot-client": "^1.3.11",
|
|
15
|
-
"frugal-iot-logger": "^1.1.
|
|
15
|
+
"frugal-iot-logger": "^1.1.18",
|
|
16
16
|
"hash-wasm": "^4.12.0",
|
|
17
17
|
"morgan": "^1.11.0",
|
|
18
18
|
"multer": "^2.1.1",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"frugal-iot-server": "frugal-iot-server.js",
|
|
25
25
|
"frugal-iot-init": "scripts/init.zsh",
|
|
26
26
|
"frugal-iot-addorganization": "scripts/addorganization.zsh",
|
|
27
|
-
"frugal-iot-setpassword": "scripts/setpassword.zsh"
|
|
27
|
+
"frugal-iot-setpassword": "scripts/setpassword.zsh",
|
|
28
|
+
"frugal-iot-diagnostic": "scripts/diagnostic.zsh"
|
|
28
29
|
},
|
|
29
30
|
"description": "Frugal IoT client and server",
|
|
30
31
|
"//files": "An allowlist, not an ignore list - anything not named here is never published, so a stray file (a database backup, a credential) cannot leak by default. Check with: npm pack --dry-run",
|
|
@@ -66,5 +67,5 @@
|
|
|
66
67
|
"test:coverage": "vitest run --coverage"
|
|
67
68
|
},
|
|
68
69
|
"type": "module",
|
|
69
|
-
"version": "0.3.
|
|
70
|
+
"version": "0.3.5"
|
|
70
71
|
}
|
|
@@ -75,13 +75,50 @@ if ! command -v mosquitto_passwd >/dev/null; then
|
|
|
75
75
|
MOSQUITTO_PASSWD_MISSING=1
|
|
76
76
|
fi
|
|
77
77
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
78
|
+
# Ask the broker's own configuration where its password file is, rather than guessing - that way
|
|
79
|
+
# this works whatever layout a machine uses, including installations older than this script.
|
|
80
|
+
# Mosquitto reads /etc/mosquitto/mosquitto.conf and then the .conf files in conf.d, last one wins.
|
|
81
|
+
MOSQUITTO_PASSWD_FILE=$(grep -hE '^[[:space:]]*password_file[[:space:]]' \
|
|
82
|
+
/etc/mosquitto/mosquitto.conf /etc/mosquitto/conf.d/*.conf(N) 2>/dev/null | tail -1 | awk '{print $2}')
|
|
81
83
|
USED_LOCAL_MOSQUITTO_FILE=0
|
|
82
|
-
if [[
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
if [[ -n "$MOSQUITTO_PASSWD_FILE" ]]; then
|
|
85
|
+
MOSQUITTO_PASSWD_FROM_CONFIG=1
|
|
86
|
+
else
|
|
87
|
+
MOSQUITTO_PASSWD_FROM_CONFIG=0
|
|
88
|
+
# No mosquitto configuration here - fall back to the usual locations, and then to the copy under
|
|
89
|
+
# extras/ so that a machine without mosquitto installed still gets something useful.
|
|
90
|
+
for f in /var/lib/mosquitto/passwords /etc/mosquitto/mosquitto_passwords; do
|
|
91
|
+
if [[ -e "$f" ]]; then
|
|
92
|
+
MOSQUITTO_PASSWD_FILE="$f"
|
|
93
|
+
break
|
|
94
|
+
fi
|
|
95
|
+
done
|
|
96
|
+
if [[ -z "$MOSQUITTO_PASSWD_FILE" ]]; then
|
|
97
|
+
if [[ -e "extras/mosquitto_passwords" ]]; then
|
|
98
|
+
MOSQUITTO_PASSWD_FILE="extras/mosquitto_passwords"
|
|
99
|
+
USED_LOCAL_MOSQUITTO_FILE=1
|
|
100
|
+
else
|
|
101
|
+
MOSQUITTO_PASSWD_FILE="/var/lib/mosquitto/passwords"
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Run mosquitto_passwd as whoever owns that file. Mosquitto warns unless the file belongs to the user
|
|
107
|
+
# opening it, and mosquitto_passwd writes a temporary backup beside it, so it needs the directory too
|
|
108
|
+
# - neither of which we get by running as ourselves against a file owned by mosquitto or by root.
|
|
109
|
+
# On Raspberry Pi OS the first user has passwordless sudo, so this is invisible.
|
|
110
|
+
MOSQUITTO_PASSWD_CMD=(mosquitto_passwd)
|
|
111
|
+
if [[ ! -w "${MOSQUITTO_PASSWD_FILE:h}" || ( -e "$MOSQUITTO_PASSWD_FILE" && ! -w "$MOSQUITTO_PASSWD_FILE" ) ]]; then
|
|
112
|
+
if command -v sudo >/dev/null; then
|
|
113
|
+
# stat's spelling differs between Linux (-c) and BSD/macOS (-f)
|
|
114
|
+
MOSQUITTO_PASSWD_OWNER=$(stat -c '%U' "$MOSQUITTO_PASSWD_FILE" 2>/dev/null \
|
|
115
|
+
|| stat -f '%Su' "$MOSQUITTO_PASSWD_FILE" 2>/dev/null || true)
|
|
116
|
+
if [[ -n "$MOSQUITTO_PASSWD_OWNER" && "$MOSQUITTO_PASSWD_OWNER" != "$(id -un)" && "$MOSQUITTO_PASSWD_OWNER" != "root" ]]; then
|
|
117
|
+
MOSQUITTO_PASSWD_CMD=(sudo -u "$MOSQUITTO_PASSWD_OWNER" mosquitto_passwd)
|
|
118
|
+
else
|
|
119
|
+
MOSQUITTO_PASSWD_CMD=(sudo mosquitto_passwd)
|
|
120
|
+
fi
|
|
121
|
+
fi
|
|
85
122
|
fi
|
|
86
123
|
|
|
87
124
|
# ---- Escaping helpers ----
|
|
@@ -168,17 +205,24 @@ MOSQUITTO_PASSWD_FAILED=0
|
|
|
168
205
|
MOSQUITTO_PASSWD_ERROR=""
|
|
169
206
|
if [[ "$MOSQUITTO_PASSWD_MISSING" -eq 1 ]]; then
|
|
170
207
|
echo "Warning: mosquitto_passwd command not found - skipped setting MQTT broker password" >&2
|
|
208
|
+
elif [[ "$MOSQUITTO_PASSWD_FROM_CONFIG" -eq 1 && ! -e "$MOSQUITTO_PASSWD_FILE" ]]; then
|
|
209
|
+
# Nothing to add an account to - and mosquitto will not be running either, for the same reason.
|
|
210
|
+
echo "Warning: mosquitto is configured to use ${MOSQUITTO_PASSWD_FILE}, but that file does not exist," >&2
|
|
211
|
+
echo "so the MQTT broker password was NOT set. Create it, then add this organization to it:" >&2
|
|
212
|
+
echo " sudo install -o mosquitto -g mosquitto -m 600 /dev/null \"${MOSQUITTO_PASSWD_FILE}\"" >&2
|
|
213
|
+
echo " sudo -u mosquitto mosquitto_passwd -b \"${MOSQUITTO_PASSWD_FILE}\" \"${ORG_ID}\" \"${PASSWORD}\"" >&2
|
|
214
|
+
MOSQUITTO_PASSWD_FAILED=1
|
|
171
215
|
else
|
|
172
216
|
# Capture stderr without letting `set -e` abort the whole script on failure - the org's config file
|
|
173
217
|
# and DB rows are already written by this point, so a failure here should be reported, not fatal.
|
|
174
|
-
MOSQUITTO_PASSWD_ERROR=$(
|
|
218
|
+
MOSQUITTO_PASSWD_ERROR=$("${MOSQUITTO_PASSWD_CMD[@]}" -b "$MOSQUITTO_PASSWD_FILE" "${ORG_ID}" "${PASSWORD}" 2>&1 >/dev/null) || MOSQUITTO_PASSWD_FAILED=1
|
|
175
219
|
if [[ "$MOSQUITTO_PASSWD_FAILED" -eq 1 ]]; then
|
|
176
220
|
echo "Warning: mosquitto_passwd failed to set the MQTT broker password:" >&2
|
|
177
221
|
echo " ${MOSQUITTO_PASSWD_ERROR}" >&2
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
222
|
+
echo "Everything else was done, so set just the broker password by hand:" >&2
|
|
223
|
+
echo " sudo mosquitto_passwd -b \"${MOSQUITTO_PASSWD_FILE}\" \"${ORG_ID}\" \"${PASSWORD}\"" >&2
|
|
224
|
+
echo "If it complains about creating a backup file, the directory holding that file is not" >&2
|
|
225
|
+
echo "writable by you - which is why the command above uses sudo." >&2
|
|
182
226
|
else
|
|
183
227
|
echo "Set mosquitto password for ${ORG_ID} in ${MOSQUITTO_PASSWD_FILE}"
|
|
184
228
|
fi
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
#
|
|
3
|
+
# Report the state of a Frugal IoT installation, for working out why something in INSTALLATION.md
|
|
4
|
+
# did not do what it said it would. It only looks - it changes nothing.
|
|
5
|
+
#
|
|
6
|
+
# Run it from the directory the server is installed in (the one holding frugal-iot.db):
|
|
7
|
+
# zsh diagnostic.zsh
|
|
8
|
+
# or, once installed from npm:
|
|
9
|
+
# npx frugal-iot-diagnostic
|
|
10
|
+
#
|
|
11
|
+
# Paste the whole output into a bug report. Passwords are deliberately not printed: the password
|
|
12
|
+
# file is reported by account name only, and organization configs by name only.
|
|
13
|
+
|
|
14
|
+
# No "set -e": a failing check should report and carry on, not stop the script.
|
|
15
|
+
setopt no_unset 2>/dev/null
|
|
16
|
+
|
|
17
|
+
section() { print -r -- ""; print -r -- "===== $* ====="; }
|
|
18
|
+
item() { print -r -- " $*"; }
|
|
19
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
20
|
+
# Ownership and mode of a file, spelled for either Linux or BSD/macOS stat
|
|
21
|
+
fileinfo() {
|
|
22
|
+
local f=$1
|
|
23
|
+
if [[ ! -e "$f" ]]; then print -r -- "missing"; return; fi
|
|
24
|
+
stat -c '%U:%G %a %s bytes' "$f" 2>/dev/null || stat -f '%Su:%Sg %Lp %z bytes' "$f" 2>/dev/null || print -r -- "(cannot stat)"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
PROBLEMS=()
|
|
28
|
+
problem() { PROBLEMS+=("$1"); }
|
|
29
|
+
|
|
30
|
+
section "When and where"
|
|
31
|
+
item "date: $(date 2>/dev/null)"
|
|
32
|
+
item "host: $(hostname 2>/dev/null)"
|
|
33
|
+
item "user: $(id -un 2>/dev/null) (groups: $(id -Gn 2>/dev/null))"
|
|
34
|
+
item "directory: $PWD"
|
|
35
|
+
|
|
36
|
+
section "The machine"
|
|
37
|
+
have uname && item "kernel: $(uname -srm)"
|
|
38
|
+
[[ -r /etc/os-release ]] && item "os: $(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)"
|
|
39
|
+
[[ -r /proc/device-tree/model ]] && item "board: $(tr -d '\0' < /proc/device-tree/model)"
|
|
40
|
+
have free && item "memory: $(free -h | awk '/^Mem:/ {print $3 " used of " $2 ", " $7 " available"}')"
|
|
41
|
+
have df && item "disk (/): $(df -h / | awk 'NR==2 {print $3 " used of " $2 ", " $4 " free"}')"
|
|
42
|
+
if have timedatectl; then
|
|
43
|
+
item "clock: $(timedatectl show -p NTPSynchronized --value 2>/dev/null | sed 's/^yes$/synchronized with a time server/; s/^no$/NOT synchronized - timestamps may be wrong/')"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
section "Software versions"
|
|
47
|
+
for c in node npm sqlite3 zsh mosquitto mosquitto_passwd mosquitto_sub; do
|
|
48
|
+
if have $c; then
|
|
49
|
+
case $c in
|
|
50
|
+
node|npm) item "$c: $($c -v 2>&1 | head -1)" ;;
|
|
51
|
+
sqlite3) item "$c: $(sqlite3 --version 2>&1 | awk '{print $1}')" ;;
|
|
52
|
+
zsh) item "$c: $ZSH_VERSION" ;;
|
|
53
|
+
mosquitto) item "$c: $(mosquitto -h 2>&1 | head -1)" ;;
|
|
54
|
+
*) item "$c: present" ;;
|
|
55
|
+
esac
|
|
56
|
+
else
|
|
57
|
+
item "$c: NOT INSTALLED"
|
|
58
|
+
[[ $c == (node|npm|sqlite3) ]] && problem "$c is not installed - see INSTALLATION.md step A3"
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
section "This installation"
|
|
63
|
+
for f in config.yaml config.d frugal-iot.db data ota node_modules; do
|
|
64
|
+
if [[ -e $f ]]; then item "$f: present"; else item "$f: MISSING"; fi
|
|
65
|
+
done
|
|
66
|
+
[[ ! -e frugal-iot.db ]] && problem "No frugal-iot.db here - either the wrong directory, or 'npx frugal-iot-init' has not been run"
|
|
67
|
+
if [[ -d node_modules/frugal-iot-server ]]; then
|
|
68
|
+
item "frugal-iot-server: $(node -e 'console.log(require("./node_modules/frugal-iot-server/package.json").version)' 2>/dev/null)"
|
|
69
|
+
item "frugal-iot-client: $(node -e 'console.log(require("./node_modules/frugal-iot-client/package.json").version)' 2>/dev/null)"
|
|
70
|
+
item "frugal-iot-logger: $(node -e 'console.log(require("./node_modules/frugal-iot-logger/package.json").version)' 2>/dev/null)"
|
|
71
|
+
item "commands: $(ls node_modules/.bin 2>/dev/null | grep frugal | tr '\n' ' ')"
|
|
72
|
+
else
|
|
73
|
+
item "frugal-iot-server is not installed in this directory"
|
|
74
|
+
fi
|
|
75
|
+
if [[ -f config.d/mqtt.yaml ]]; then item "broker configured as: $(grep -h '^broker:' config.d/mqtt.yaml 2>/dev/null)"; fi
|
|
76
|
+
if [[ -d config.d/organizations ]]; then
|
|
77
|
+
# Names only - these files contain the broker password.
|
|
78
|
+
# (N) makes a non-matching glob expand to nothing rather than erroring - and the list has to be
|
|
79
|
+
# tested before use, because "ls" with no arguments would list the current directory instead.
|
|
80
|
+
ORGS=(config.d/organizations/*.yaml(N:t:r))
|
|
81
|
+
if (( ${#ORGS} )); then
|
|
82
|
+
item "organizations: ${ORGS}"
|
|
83
|
+
else
|
|
84
|
+
item "organizations: none defined yet"
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
if [[ -f frugal-iot.db ]] && have sqlite3; then
|
|
88
|
+
item "accounts: $(sqlite3 frugal-iot.db 'SELECT group_concat(username, ", ") FROM users;' 2>/dev/null)"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
section "Mosquitto configuration"
|
|
92
|
+
if [[ -f /etc/mosquitto/mosquitto.conf ]]; then
|
|
93
|
+
item "/etc/mosquitto/mosquitto.conf: $(fileinfo /etc/mosquitto/mosquitto.conf)"
|
|
94
|
+
else
|
|
95
|
+
item "/etc/mosquitto/mosquitto.conf: missing - is mosquitto installed?"
|
|
96
|
+
fi
|
|
97
|
+
item "files in /etc/mosquitto/conf.d:"
|
|
98
|
+
for f in /etc/mosquitto/conf.d/*(N); do
|
|
99
|
+
item " ${f}: $(fileinfo $f)"
|
|
100
|
+
grep -nE '^[[:space:]]*(listener|protocol|password_file|allow_anonymous)' $f 2>/dev/null | sed 's/^/ /'
|
|
101
|
+
done
|
|
102
|
+
[[ -z "$(print -r -- /etc/mosquitto/conf.d/*(N))" ]] && problem "Nothing in /etc/mosquitto/conf.d - the Frugal IoT config was never copied there (step A5)"
|
|
103
|
+
|
|
104
|
+
# Which password file does the running configuration actually name?
|
|
105
|
+
# mosquitto's include_dir only loads files ending in .conf, so look at exactly those, last one wins
|
|
106
|
+
CONFIGURED_PWFILE=$(grep -hE '^[[:space:]]*password_file[[:space:]]' /etc/mosquitto/mosquitto.conf /etc/mosquitto/conf.d/*.conf(N) 2>/dev/null | tail -1 | awk '{print $2}')
|
|
107
|
+
section "Mosquitto password file"
|
|
108
|
+
if [[ -n "$CONFIGURED_PWFILE" ]]; then
|
|
109
|
+
item "configuration names: $CONFIGURED_PWFILE"
|
|
110
|
+
item " that file: $(fileinfo $CONFIGURED_PWFILE)"
|
|
111
|
+
if [[ ! -e "$CONFIGURED_PWFILE" ]]; then
|
|
112
|
+
problem "Mosquitto is configured to use $CONFIGURED_PWFILE but that file does not exist - it will refuse to start"
|
|
113
|
+
else
|
|
114
|
+
# Account names only, never the hashes
|
|
115
|
+
item " accounts in it: $(cut -d: -f1 "$CONFIGURED_PWFILE" 2>/dev/null | tr '\n' ' ')"
|
|
116
|
+
[[ ! -s "$CONFIGURED_PWFILE" ]] && item " (the file is empty - no accounts yet)"
|
|
117
|
+
fi
|
|
118
|
+
else
|
|
119
|
+
item "no password_file line found in any mosquitto configuration"
|
|
120
|
+
problem "No password_file configured - the broker would allow anonymous access or reject everything"
|
|
121
|
+
fi
|
|
122
|
+
# The copy in this installation is what step A5 tells you to put into /etc - if the two disagree,
|
|
123
|
+
# either the copy never happened or the installed package is older than the instructions.
|
|
124
|
+
if [[ -f extras/mosquitto.conf ]]; then
|
|
125
|
+
item "this installation's extras/mosquitto.conf names: $(grep -hE '^[[:space:]]*password_file' extras/mosquitto.conf 2>/dev/null | awk '{print $2}')"
|
|
126
|
+
if [[ -n "$CONFIGURED_PWFILE" ]] && ! diff -q extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf >/dev/null 2>&1; then
|
|
127
|
+
problem "extras/mosquitto.conf here differs from /etc/mosquitto/conf.d/frugal-iot.conf - if you followed step A5 with an older installed package, copy it again after upgrading"
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
item "candidate locations, whether or not configured:"
|
|
131
|
+
for f in /var/lib/mosquitto/passwords /etc/mosquitto/mosquitto_passwords; do
|
|
132
|
+
item " ${f}: $(fileinfo $f)"
|
|
133
|
+
done
|
|
134
|
+
item "directories they live in:"
|
|
135
|
+
for d in /var/lib/mosquitto /etc/mosquitto; do
|
|
136
|
+
item " ${d}: $(fileinfo $d)"
|
|
137
|
+
done
|
|
138
|
+
|
|
139
|
+
section "Mosquitto service"
|
|
140
|
+
if have systemctl; then
|
|
141
|
+
item "enabled: $(systemctl is-enabled mosquitto 2>&1)"
|
|
142
|
+
item "active: $(systemctl is-active mosquitto 2>&1)"
|
|
143
|
+
[[ "$(systemctl is-active mosquitto 2>/dev/null)" != "active" ]] && problem "Mosquitto is not running - see its own log, quoted above under 'Mosquitto service'"
|
|
144
|
+
print -r -- " --- last 12 journal lines (systemd's view: usually only an exit code) ---"
|
|
145
|
+
journalctl -u mosquitto -n 12 --no-pager 2>&1 | sed 's/^/ /'
|
|
146
|
+
else
|
|
147
|
+
item "systemctl not available on this machine"
|
|
148
|
+
fi
|
|
149
|
+
# Debian's mosquitto logs to a file rather than the journal, so this is where the real reason is.
|
|
150
|
+
MOSQUITTO_LOG=$(grep -hE '^[[:space:]]*log_dest[[:space:]]+file' /etc/mosquitto/mosquitto.conf /etc/mosquitto/conf.d/*(N) 2>/dev/null | tail -1 | awk '{print $3}')
|
|
151
|
+
[[ -z "$MOSQUITTO_LOG" ]] && MOSQUITTO_LOG=/var/log/mosquitto/mosquitto.log
|
|
152
|
+
item "its own log file: ${MOSQUITTO_LOG} ($(fileinfo $MOSQUITTO_LOG))"
|
|
153
|
+
if [[ -r "$MOSQUITTO_LOG" ]]; then
|
|
154
|
+
print -r -- " --- last 12 lines of that log (this is where startup errors appear) ---"
|
|
155
|
+
tail -12 "$MOSQUITTO_LOG" 2>&1 | sed 's/^/ /'
|
|
156
|
+
elif [[ -e "$MOSQUITTO_LOG" ]]; then
|
|
157
|
+
# The log belongs to the mosquitto user, so reading it needs privilege. Try without prompting -
|
|
158
|
+
# on Raspberry Pi OS the first user has passwordless sudo, so this usually just works.
|
|
159
|
+
if have sudo && MOSQUITTO_LOG_TAIL=$(sudo -n tail -12 "$MOSQUITTO_LOG" 2>/dev/null); then
|
|
160
|
+
print -r -- " --- last 12 lines of that log, read with sudo (startup errors appear here) ---"
|
|
161
|
+
print -r -- "$MOSQUITTO_LOG_TAIL" | sed 's/^/ /'
|
|
162
|
+
else
|
|
163
|
+
item " it belongs to $(fileinfo $MOSQUITTO_LOG | awk '{print $1}') so it cannot be read as $(id -un)."
|
|
164
|
+
item " Re-run this whole script with sudo to include it: sudo zsh $0"
|
|
165
|
+
problem "Could not read ${MOSQUITTO_LOG}, which is where Mosquitto explains itself - re-run with sudo"
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
section "Listening ports"
|
|
170
|
+
PORTS=""
|
|
171
|
+
if have ss; then
|
|
172
|
+
PORTS=$(ss -tln 2>/dev/null | grep -E ':(1883|9012|8080)\b')
|
|
173
|
+
elif have netstat; then
|
|
174
|
+
PORTS=$(netstat -an 2>/dev/null | grep -E '[.:](1883|9012|8080) ')
|
|
175
|
+
else
|
|
176
|
+
item "neither ss nor netstat available"
|
|
177
|
+
fi
|
|
178
|
+
if [[ -n "$PORTS" ]]; then
|
|
179
|
+
print -r -- "$PORTS" | sed 's/^/ /'
|
|
180
|
+
else
|
|
181
|
+
item "none of 1883 (mqtt), 9012 (websockets), 8080 (web) are listening"
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
section "Frugal IoT server service"
|
|
185
|
+
if have systemctl; then
|
|
186
|
+
if systemctl list-unit-files 2>/dev/null | grep -q '^frugaliot.service'; then
|
|
187
|
+
item "enabled: $(systemctl is-enabled frugaliot 2>&1)"
|
|
188
|
+
item "active: $(systemctl is-active frugaliot 2>&1)"
|
|
189
|
+
print -r -- " --- last 20 journal lines ---"
|
|
190
|
+
journalctl -u frugaliot -n 20 --no-pager 2>&1 | sed 's/^/ /'
|
|
191
|
+
else
|
|
192
|
+
item "no frugaliot service installed yet (step A8 not reached)"
|
|
193
|
+
fi
|
|
194
|
+
else
|
|
195
|
+
item "systemctl not available on this machine"
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
section "Name resolution (step A2, A4)"
|
|
199
|
+
# The broker URL in config.d/mqtt.yaml has to resolve from every machine that uses it - this one,
|
|
200
|
+
# and whatever browser or node talks to it.
|
|
201
|
+
BROKER_URL=$(grep -h '^broker:' config.d/mqtt.yaml 2>/dev/null | awk '{print $2}')
|
|
202
|
+
BROKER_HOST=${${BROKER_URL#*://}%%:*}
|
|
203
|
+
if [[ -n "$BROKER_HOST" ]]; then
|
|
204
|
+
item "broker host: $BROKER_HOST"
|
|
205
|
+
if have getent && BROKER_IP=$(getent hosts "$BROKER_HOST" 2>/dev/null | head -1 | awk '{print $1}') && [[ -n "$BROKER_IP" ]]; then
|
|
206
|
+
item " resolves to $BROKER_IP from this machine"
|
|
207
|
+
elif have ping && ping -c1 -W2 "$BROKER_HOST" >/dev/null 2>&1; then
|
|
208
|
+
item " answers ping from this machine"
|
|
209
|
+
else
|
|
210
|
+
item " DOES NOT RESOLVE from this machine"
|
|
211
|
+
problem "The broker host '$BROKER_HOST' does not resolve here - the server's own logger cannot connect either"
|
|
212
|
+
fi
|
|
213
|
+
[[ "$BROKER_HOST" == *.local ]] && item " note: .local names do not resolve on most Android phones - use an IP address there"
|
|
214
|
+
fi
|
|
215
|
+
MYADDRS=$( (have ip && ip -4 -o addr show scope global | awk '{print $2"="$4}') 2>/dev/null | tr '\n' ' ')
|
|
216
|
+
item "this machine's addresses: ${MYADDRS:-(could not determine)}"
|
|
217
|
+
|
|
218
|
+
section "Broker authentication (steps A5, A6)"
|
|
219
|
+
if have mosquitto_sub; then
|
|
220
|
+
# A wrong password must be refused - that is the check in step A5
|
|
221
|
+
WRONGOUT=$(mosquitto_sub -h localhost -u nobody -P wrong -t '#' -W 2 2>&1)
|
|
222
|
+
if [[ "$WRONGOUT" == *"not authorised"* ]]; then
|
|
223
|
+
item "wrong password: correctly refused"
|
|
224
|
+
elif [[ "$WRONGOUT" == *"Connection refused"* || "$WRONGOUT" == *"Error"* ]]; then
|
|
225
|
+
item "wrong password: broker did not answer - $WRONGOUT"
|
|
226
|
+
problem "The broker is not answering on localhost:1883"
|
|
227
|
+
else
|
|
228
|
+
item "wrong password: ACCEPTED - the broker is not requiring credentials"
|
|
229
|
+
problem "A wrong password was not refused - check password_file is set in the mosquitto config"
|
|
230
|
+
fi
|
|
231
|
+
# Then each configured organization should be able to connect with its own credentials.
|
|
232
|
+
# The password is read from the config and used, never printed.
|
|
233
|
+
for f in config.d/organizations/*.yaml(N); do
|
|
234
|
+
ORG=${f:t:r}
|
|
235
|
+
ORG_PW=$(sed -n 's/^mqtt_password:[[:space:]]*//p' "$f" 2>/dev/null | head -1 | tr -d '"'"'"'')
|
|
236
|
+
if [[ -z "$ORG_PW" ]]; then
|
|
237
|
+
item "organization $ORG: no mqtt_password in $f"
|
|
238
|
+
continue
|
|
239
|
+
fi
|
|
240
|
+
ORGOUT=$(mosquitto_sub -h localhost -u "$ORG" -P "$ORG_PW" -t '#' -W 2 2>&1)
|
|
241
|
+
if [[ "$ORGOUT" == *"not authorised"* ]]; then
|
|
242
|
+
item "organization $ORG: REFUSED by the broker"
|
|
243
|
+
problem "Organization '$ORG' cannot log in to the broker - its password in $f does not match the broker's password file (step A6)"
|
|
244
|
+
elif [[ "$ORGOUT" == *"Connection refused"* ]]; then
|
|
245
|
+
item "organization $ORG: broker not answering"
|
|
246
|
+
else
|
|
247
|
+
item "organization $ORG: authenticates, and saw $(print -r -- "$ORGOUT" | grep -c . ) message(s) in a 2 second sample"
|
|
248
|
+
fi
|
|
249
|
+
done
|
|
250
|
+
else
|
|
251
|
+
item "mosquitto_sub not installed - cannot test broker logins (sudo apt install mosquitto-clients)"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
section "Web server (step A7)"
|
|
255
|
+
WEBPORT=$(grep -h '^port:' config.d/server.yaml 2>/dev/null | awk '{print $2}')
|
|
256
|
+
[[ -z "$WEBPORT" ]] && WEBPORT=8080
|
|
257
|
+
if have curl; then
|
|
258
|
+
HOMECODE=$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://localhost:${WEBPORT}/ 2>/dev/null)
|
|
259
|
+
if [[ "$HOMECODE" == "000" ]]; then
|
|
260
|
+
item "nothing is answering on port ${WEBPORT} - the server is not running"
|
|
261
|
+
item " (expected if you have not reached step A7 yet; otherwise start it, or see the frugaliot service below)"
|
|
262
|
+
else
|
|
263
|
+
item "GET / -> ${HOMECODE} (expect 200)"
|
|
264
|
+
item "GET /config.json -> $(curl -s -o /dev/null -w '%{http_code}' -m 5 http://localhost:${WEBPORT}/config.json 2>/dev/null) (expect 401 when not logged in)"
|
|
265
|
+
[[ "$HOMECODE" != "200" ]] && problem "The web server answered ${HOMECODE} rather than 200 on port ${WEBPORT}"
|
|
266
|
+
fi
|
|
267
|
+
else
|
|
268
|
+
item "curl not installed - cannot test the web server"
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
section "Logged data (step A9)"
|
|
272
|
+
if [[ -d data ]]; then
|
|
273
|
+
DATADIRS=(data/*(N/))
|
|
274
|
+
if (( ${#DATADIRS} )); then
|
|
275
|
+
for d in $DATADIRS; do
|
|
276
|
+
item "${d}: $(find $d -type f 2>/dev/null | wc -l | tr -d ' ') files, newest $(ls -t $d/**/*(N.om[1]) 2>/dev/null | head -1)"
|
|
277
|
+
done
|
|
278
|
+
else
|
|
279
|
+
item "data/ is empty - no readings logged yet, which is expected until a node reports (step A9)"
|
|
280
|
+
fi
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
section "Summary"
|
|
284
|
+
if (( ${#PROBLEMS} == 0 )); then
|
|
285
|
+
item "No problems detected by these checks."
|
|
286
|
+
else
|
|
287
|
+
for p in $PROBLEMS; do item "PROBLEM: $p"; done
|
|
288
|
+
fi
|
|
289
|
+
print -r -- ""
|
package/scripts/init.zsh
CHANGED
|
@@ -28,11 +28,24 @@ if [[ ! -f "${PKG}/frugal-iot-createdb.sql" ]]; then
|
|
|
28
28
|
fi
|
|
29
29
|
|
|
30
30
|
# ---- 1. Configuration files, copied only if not already here ----
|
|
31
|
-
COPIED=()
|
|
31
|
+
COPIED=() # which files this run actually created, so existing ones are never rewritten below
|
|
32
|
+
DIFFER_TO=() # files kept that no longer match what this release ships, and where to compare them
|
|
33
|
+
DIFFER_FROM=()
|
|
34
|
+
# copy_if_missing <packaged file> <local file> [compare]
|
|
35
|
+
# "compare" asks for a warning when the file exists but differs from the one this release ships.
|
|
36
|
+
# Only pass it for files nobody is expected to edit - config.yaml, mqtt.yaml and server.yaml are
|
|
37
|
+
# this installation's own settings, so of course they differ, and warning about them every time
|
|
38
|
+
# would train you to ignore the warning that matters.
|
|
32
39
|
copy_if_missing() {
|
|
33
|
-
local from=$1 to=$2
|
|
40
|
+
local from=$1 to=$2 compare=${3:-}
|
|
34
41
|
if [[ -e "$to" ]]; then
|
|
35
|
-
|
|
42
|
+
if [[ "$compare" == compare && -e "$from" ]] && ! cmp -s "$from" "$to"; then
|
|
43
|
+
DIFFER_TO+=("$to")
|
|
44
|
+
DIFFER_FROM+=("$from")
|
|
45
|
+
echo " kept ${to} (DIFFERS from this release - see below)"
|
|
46
|
+
else
|
|
47
|
+
echo " kept ${to} (already present)"
|
|
48
|
+
fi
|
|
36
49
|
elif [[ -e "$from" ]]; then
|
|
37
50
|
mkdir -p "${to:h}"
|
|
38
51
|
cp "$from" "$to"
|
|
@@ -46,12 +59,14 @@ copy_if_missing "${PKG}/config.yaml" "./config.yaml"
|
|
|
46
59
|
for f in logger.yaml mqtt.yaml server.yaml; do
|
|
47
60
|
copy_if_missing "${PKG}/config.d/${f}" "./config.d/${f}"
|
|
48
61
|
done
|
|
62
|
+
# The schema describes the sensor types the software understands, so a release changing it matters
|
|
49
63
|
for f in "${PKG}"/config.d/schema/*.yaml(N); do
|
|
50
|
-
copy_if_missing "$f" "./config.d/schema/${f:t}"
|
|
64
|
+
copy_if_missing "$f" "./config.d/schema/${f:t}" compare
|
|
51
65
|
done
|
|
52
|
-
# Copied so that mosquitto.conf and frugaliot.service can be edited and installed from here
|
|
66
|
+
# Copied so that mosquitto.conf and frugaliot.service can be edited and installed from here. Worth
|
|
67
|
+
# comparing: these get copied on somewhere else (/etc/...), where an old version lingers unnoticed.
|
|
53
68
|
for f in "${PKG}"/extras/*(N); do
|
|
54
|
-
copy_if_missing "$f" "./extras/${f:t}"
|
|
69
|
+
copy_if_missing "$f" "./extras/${f:t}" compare
|
|
55
70
|
done
|
|
56
71
|
|
|
57
72
|
# ---- 1a. Point server.yaml at the web client this instance actually has ----
|
|
@@ -104,6 +119,18 @@ else
|
|
|
104
119
|
echo " created ${DB}"
|
|
105
120
|
fi
|
|
106
121
|
|
|
122
|
+
if (( ${#DIFFER_TO} )); then
|
|
123
|
+
echo
|
|
124
|
+
echo "These files were left as you have them, but this release ships a different version:"
|
|
125
|
+
for i in {1..${#DIFFER_TO}}; do
|
|
126
|
+
echo " ${DIFFER_TO[$i]}"
|
|
127
|
+
echo " compare with: diff ${DIFFER_TO[$i]} ${DIFFER_FROM[$i]}"
|
|
128
|
+
done
|
|
129
|
+
echo "Usually that just means you edited it, and there is nothing to do. But a release can also"
|
|
130
|
+
echo "change one of these files - and anything installed elsewhere from it, such as"
|
|
131
|
+
echo "/etc/mosquitto/conf.d/frugal-iot.conf, keeps the old content until you copy it again."
|
|
132
|
+
fi
|
|
133
|
+
|
|
107
134
|
echo
|
|
108
135
|
echo "Ready. Next:"
|
|
109
136
|
echo " npx frugal-iot-setpassword superuser <password> # so you can log in as the administrator"
|