frugal-iot-server 0.3.4 → 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 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:** first draft, written 2026-07. The Pi 4 path has been written against the current
17
- > code but not yet walked through end to end on hardware. Items still to confirm are collected
18
- > under [Open questions](#open-questions) please add your findings there as you go.
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. 2 GB RAM or more is comfortable; 1 GB should work.
29
- * A microSD card, 16 GB or larger, Class 10 / A1 or better. (8 GB works but leaves little room for data.)
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
- * A micro-HDMI to HDMI cable plus a monitor, and a USB keyboard lets you log in directly, see boot
52
- errors, and finish Wi-Fi setup by hand. Wi-Fi configured through Imager does not always connect on
53
- the first boot (see step A2), so do not count on not needing these.
54
- * An Ethernet cable from the Pi to your router — bypasses all Wi-Fi problems.
58
+ * An Ethernet cable from the Pi to your routerthe 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
- * Do not be surprised if the Pi still does not join the network - this has been seen on a plain
76
- WPA2 network with correct details, and is quick to fix at the console with `nmtui` (step A2).
77
- A WPA3 network cannot use Imager's derived key at all, so there expect to use `nmtui`.
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 !
@@ -170,12 +179,12 @@ address does not change. Sensor nodes and phones then have something stable to t
170
179
  ```
171
180
  sudo apt install -y nodejs npm sqlite3 zsh
172
181
  node -v
173
- npm -v
174
182
  ```
175
183
 
176
- * `nodejs` — the server needs **Node 18 or later**. Raspberry Pi OS currently provides 20.19.2,
177
- which is fine. If `node -v` ever reports something older on your image, install a current
178
- version from [NodeSource](https://github.com/nodesource/distributions) instead.
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.
179
188
  * `npm` — installs the server; it is a separate package from `nodejs` on Debian.
180
189
  * `sqlite3` — the database the server keeps its accounts in.
181
190
  * `zsh` — the setup commands in step A6 are zsh scripts.
@@ -257,15 +266,22 @@ sudo cp extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf
257
266
  `/etc/mosquitto/mosquitto.conf`, which keeps its own settings for logging and persistence.)
258
267
 
259
268
  That configuration names a password file, and Mosquitto will not start if the file is missing, so
260
- create an empty one. The accounts inside it get created for you in the next step, by
261
- `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:
262
270
 
263
271
  ```
264
- sudo touch /etc/mosquitto/mosquitto_passwords
265
- sudo chown ${USER}:mosquitto /etc/mosquitto/mosquitto_passwords
266
- sudo chmod 640 /etc/mosquitto/mosquitto_passwords
272
+ sudo install -o mosquitto -g mosquitto -m 600 /dev/null /var/lib/mosquitto/passwords
267
273
  ```
268
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
+
269
285
  Start the broker and have it start at every boot:
270
286
 
271
287
  ```
@@ -525,6 +541,16 @@ network and everything is correct — which is what you will see during this ins
525
541
  cannot: Raspberry Pi OS saves the time periodically and restores that value at boot, so after a
526
542
  power cut the Pi comes up believing it is whenever it last saved, and the gap never gets made up.
527
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
+
528
554
  What that affects:
529
555
 
530
556
  * **Logged data is stamped with the wrong time**, so graphs and history drift after each power cut.
@@ -539,6 +565,54 @@ but close enough for sensor data.
539
565
 
540
566
  ---
541
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
+
614
+ ---
615
+
542
616
  ## Part B — Raspberry Pi Zero W
543
617
 
544
618
  **Not yet tested — do not follow this section expecting it to work.** It records what is known
@@ -547,8 +621,9 @@ and what has to be checked.
547
621
  First, work out which board you have, because they are very different:
548
622
 
549
623
  * **Raspberry Pi Zero 2 W** — 64-bit ARM (Cortex-A53), 512 MB RAM. Should follow **Part A**
550
- almost unchanged: choose *Raspberry Pi Zero 2 W* in Imager, and everything else applies.
551
- The concern is memory, not architecture.
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.
552
627
  * **Raspberry Pi Zero W** (the original) — 32-bit ARMv6 (BCM2835), 512 MB RAM. This is the
553
628
  awkward one: it needs the 32-bit Raspberry Pi OS, and **the official Node.js builds no longer
554
629
  support ARMv6**. Unofficial ARMv6 builds exist at
@@ -565,8 +640,9 @@ Differences to expect on either Zero:
565
640
  * Everything will be slow. `npm install` may take a long time, especially if `sqlite3` has to be
566
641
  compiled from source — which is likely on ARMv6, since no ready-made binaries are published.
567
642
 
568
- **Before this section can be written, I need to know:** which Zero you intend to support (original
569
- W, 2 W, or both), and the results of trying a Node install on it.
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.
570
646
 
571
647
  ---
572
648
 
@@ -584,20 +660,18 @@ they get settled.
584
660
 
585
661
  **Needs information I do not have**
586
662
 
587
- 2. **Which Pi Zero** (Part B) — original Zero W, Zero 2 W, or both.
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.
588
665
  3. **Bridging to the shared server** — the local broker could optionally bridge to
589
666
  naturalinnovation.org so data also reaches the shared server. Not covered here; a later task.
590
667
  4. **Organization naming** — this guide sets up exactly one organization named `dev`, because that
591
668
  is the node firmware's default. Is that the right default for a farm installation, or should the
592
669
  guide encourage a meaningful organization id (which then has to be set in the node firmware too)?
593
- 5. **Upgrading an existing installation** — `npm update frugal-iot-server` followed by
594
- `npx frugal-iot-init` should be all it takes, since init adds missing configuration without
595
- touching what is there. Not yet tried on a server that has been running for a while.
596
670
 
597
- **Tested on**
671
+ ## Tested on
598
672
 
599
- Fill this in as you go, so we know what the guide has actually been proven against:
673
+ What the guide has actually been proven against add a row for each run:
600
674
 
601
- | Date | Board | OS image | Node version | Result |
602
- | --- | --- | --- | --- | --- |
603
- | | Pi 4 Model B | | | |
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. |
@@ -1,10 +1,10 @@
1
1
  port: 8080
2
2
  #Production mode
3
- #nodemodulesdir: ./node_modules
4
- #htmldir: ./node_modules/frugal-iot-client
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
@@ -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
- password_file /etc/mosquitto/mosquitto_passwords
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
@@ -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
  */
@@ -920,6 +921,24 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
920
921
  });
921
922
  }
922
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
+ );
923
942
  app.get('/people_list/:org',
924
943
  loggedInOrFail,
925
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.17",
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.4"
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
- # Prefer the real system password file; if it's not present (e.g. mosquitto isn't installed on this
79
- # machine), fall back to editing the local copy under extras/ so the script still does something useful.
80
- MOSQUITTO_PASSWD_FILE="/etc/mosquitto/mosquitto_passwords"
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 [[ ! -e "$MOSQUITTO_PASSWD_FILE" && -e "extras/mosquitto_passwords" ]]; then
83
- MOSQUITTO_PASSWD_FILE="extras/mosquitto_passwords"
84
- USED_LOCAL_MOSQUITTO_FILE=1
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=$(mosquitto_passwd -b "$MOSQUITTO_PASSWD_FILE" "${ORG_ID}" "${PASSWORD}" 2>&1 >/dev/null) || MOSQUITTO_PASSWD_FAILED=1
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
- if [[ "$MOSQUITTO_PASSWD_ERROR" == *"Permission denied"* || ! -w "$MOSQUITTO_PASSWD_FILE" ]]; then
179
- echo "This looks like a permissions problem - re-run that command with sudo:" >&2
180
- echo " sudo mosquitto_passwd -b \"${MOSQUITTO_PASSWD_FILE}\" \"${ORG_ID}\" \"${PASSWORD}\"" >&2
181
- fi
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=() # which files this run actually created, so existing ones are never rewritten below
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
- echo " kept ${to} (already present)"
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"