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 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 !
@@ -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
- **Recommended:** in your router, give the Pi a fixed (reserved) IP address. Sensor nodes and
149
- phones then have a stable address to talk to even where `.local` names do not work.
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 currently provides 20.19.2,
160
- which is fine. If `node -v` ever reports something older on your image, install a current
161
- 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.
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
- > If `npm install` fails while building `sqlite3`, it could not find a ready-made binary for this
190
- > platform and needs to compile one. Install the compiler toolchain and try again:
191
- > `sudo apt install -y build-essential python3` then `npm install frugal-iot-server`.
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
- > Many Android phones cannot resolve `.local` names. If you will view the dashboard from a phone,
214
- > use the Pi's IP address instead for example `broker: ws://192.168.1.42:9012` and give the Pi
215
- > a reserved address in your router so it does not change.
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, by
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 touch /etc/mosquitto/mosquitto_passwords
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)`. Press `q` to exit. If it is not 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
- You should see the configuration echoed back, then something like:
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
- The two lines that matter most are `Server starting on port 8080` and `mqtt dev connect`.
371
- `mqtt dev connect` means the server reached your broker and authenticated. If instead you see
372
- repeated `mqtt dev close` or `offline`, the broker URL or the password is wrong — recheck
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`). You should get the Frugal IoT UI, and be able to log in as
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
- Once logged in and with your organization selected, the UI's MQTT status should show *connected*:
387
- that is the browser using the organization's broker credentials over the WebSocket listener on
388
- port 9012, which is the last untested piece of the chain. If it does not connect, check that the
389
- `broker:` URL in `config.d/mqtt.yaml` is one this browser can actually resolve — a phone that
390
- cannot look up `.local` names needs the Pi's IP address there instead.
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 need to be told to use the Pi's brokerhostname or IP of the Pi,
436
- port `1883`, username and password of the organization (`dev` / the broker password from step A6),
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
- > **To be written.** The node firmware lives in a separate repo
440
- > ([mitra42/frugal-iot](https://github.com/mitra42/frugal-iot)) and its exact configuration
441
- > settings are not documented here yet. See [Open questions](#open-questions).
495
+ ```cpp
496
+ frugal_iot.configure_mqtt("frugaliot.naturalinnovation.org", "dev", "public");
497
+ ```
442
498
 
443
- You can confirm nodes are reporting without the UI at all, using the subscriber from step A6:
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
- 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.
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
- **Before this section can be written, I need to know:** which Zero you intend to support (original
493
- 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.
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. **`sqlite3` native build** (step A4) does `npm install frugal-iot-server` find a prebuilt
506
- ARM64 binary, or does it need `build-essential`? If it always needs compiling, that should move
507
- up into step A3 as a normal prerequisite rather than a troubleshooting note.
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
- 8. **Sensor node configuration** (step A9) — what exactly does one set in the node firmware to
530
- point it at a local broker (broker host/port, credentials, organization, project)? Once you tell
531
- me, or point me at the right file in the `frugal-iot` repo, I can write that section properly.
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
- 11. **Organization naming** — this guide sets up exactly one organization named `dev`, because that
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
- **Tested on**
671
+ ## Tested on
543
672
 
544
- 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:
545
674
 
546
- | Date | Board | OS image | Node version | Result |
547
- | --- | --- | --- | --- | --- |
548
- | | 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. |
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
- Config= {
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 config is reported back,
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
 
@@ -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
@@ -1,22 +1,33 @@
1
- # Copy to /etc/systemd/system/frugaliot.service (or /usr/lib/systemd/system on some distributions)
2
- # and edit User, WorkingDirectory and ExecStart for this machine.
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
- After=network-online.target
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=mitra
20
+ User=pi
10
21
  # The directory holding this server's config.d, database and data - where npm install was run
11
- WorkingDirectory=/var/www/frugal-iot
22
+ WorkingDirectory=/home/pi/frugal-iot
12
23
  # Installed with npm, this is the command npm put in that directory:
13
- ExecStart=/var/www/frugal-iot/node_modules/.bin/frugal-iot-server
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=syslog
19
- StandardError=syslog
29
+ StandardOutput=journal
30
+ StandardError=journal
20
31
  SyslogIdentifier=frugaliot
21
32
 
22
33
  [Install]
@@ -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
  */
@@ -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
- console.log("Config=", config);
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.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.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
- # 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"