frugal-iot-server 0.3.4 → 0.3.6

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
@@ -5,33 +5,61 @@ an MQTT broker (Mosquitto), the Frugal IoT server, the logger that writes sensor
5
5
  and the web UI. Once installed, the Pi works **offline** — your sensor nodes talk to the Pi,
6
6
  and you view the data from a phone or laptop on the same Wi-Fi. No internet needed after setup.
7
7
 
8
- Two hardware paths are covered:
9
-
10
- * [Raspberry Pi 4](#part-a--raspberry-pi-4) — the recommended, tested path.
11
- * [Raspberry Pi Zero W](#part-b--raspberry-pi-zero-w) — smaller and cheaper, **not yet tested**, see the open questions in that section.
12
-
13
8
  If you already have a working Linux server (not a Pi), you do not need this document —
14
9
  see [README.md](https://github.com/mitra42/frugal-iot-server/blob/main/README.md) instead.
15
10
 
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.
11
+ **Read the steps in order.** Anything specific to one board is marked in the step it belongs to,
12
+ like this:
13
+
14
+ > **Pi Zero W:** extra detail that only applies to a Zero W. Skip these on a Pi 4.
15
+
16
+ > **Status:** followed on hardware — a Raspberry Pi 4 through step 9, and a Pi Zero W through step 8
17
+ > (see [Tested on](#tested-on)) — and corrected from what those runs found. HTTPS/OTA in step 10 is
18
+ > not written yet. Remaining uncertainties are under [Open questions](#open-questions) — please add
19
+ > 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).
23
+
24
+ **If a step does not do what it says here**, once you reach step 4 you can run
25
+ `npx --no frugal-iot-diagnostic` from your install directory — it inspects the whole installation and
26
+ reports what looks wrong. See [When something does not work](#when-something-does-not-work).
19
27
 
20
28
  ---
21
29
 
22
- ## Part A — Raspberry Pi 4
30
+ ## Which Raspberry Pi
31
+
32
+ Any of these work. What differs is only how long the install takes, and how much of a fight it is:
33
+
34
+ | Board | Install | Notes |
35
+ | --- | --- | --- |
36
+ | **Pi 4** (or Pi 5, Pi 3) | About half an hour | The straightforward path. 64-bit, everything installs as ready-made binaries. |
37
+ | **Pi Zero 2 W** | Expect longer | 64-bit, so it should avoid the long compile below, but shares the Zero's 512 MB. Not yet tested. |
38
+ | **Pi Zero W** (the original) | Allow an afternoon | 32-bit ARMv6. One dependency has to be compiled — about 40 minutes — and 512 MB of RAM is not enough without adding swap. Tested and works. |
23
39
 
24
- ### A0. What you need before you start
40
+ A Zero W runs the server perfectly well once installed. It is *installing* that is slow, because a
41
+ single 1 GHz core has to unpack several hundred packages and compile one of them.
42
+
43
+ ### 0. What you need before you start
25
44
 
26
45
  **Hardware**
27
46
 
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.)
30
- * The official Raspberry Pi USB-C power supply (5 V / 3 A). Phone chargers frequently cause
31
- random reboots and corrupted SD cards this is the single most common cause of "it doesn't work".
47
+ * A Raspberry Pi, per the table above. Any RAM size a running server uses around 250 MB.
48
+ (Measured on a 4 GB Pi 4: 231 MB in use with the server running.)
49
+ * A microSD card. 8 GB is enough for the system and software a working install occupies about
50
+ 5 GB but sensor data accumulates on this card for as long as the server runs, so 16 GB or larger
51
+ is the safer choice. Class 10 / A1 or better.
52
+ * The official Raspberry Pi power supply for your board — USB-C on a Pi 4, micro-USB on a Zero.
53
+ Phone chargers frequently cause random reboots and corrupted SD cards — this is the single most
54
+ common cause of "it doesn't work".
32
55
  * A way to write the SD card from your laptop: a built-in SD slot or a USB card reader.
33
56
  * Note that you will often need an adapter from the SD format the Pi uses to the SD format of most laptop readers.
34
57
 
58
+ > **Pi Zero W:** its Wi-Fi is 2.4 GHz only, so it cannot see a 5 GHz-only network. It has no
59
+ > Ethernet socket either, so the cable trick in step 2 is unavailable — have the micro-HDMI cable
60
+ > and a keyboard to hand instead. Note micro-USB for power and a micro-USB OTG adapter for the
61
+ > keyboard: different cables from a Pi 4.
62
+
35
63
  **Software and information**
36
64
 
37
65
  * A laptop or desktop (Mac, Windows or Linux) with [Raspberry Pi Imager](https://www.raspberrypi.com/software/) installed.
@@ -48,19 +76,21 @@ see [README.md](https://github.com/mitra42/frugal-iot-server/blob/main/README.md
48
76
 
49
77
  **Have these to hand in case the headless setup does not come up**
50
78
 
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.
79
+ * An Ethernet cable from the Pi to your routerthe simplest way in if the Pi does not appear on
80
+ the Wi-Fi, and you can then sort the Wi-Fi out over SSH (step 2).
81
+ * A micro-HDMI to HDMI cable plus a monitor, and a USB keyboard for when the Pi is nowhere near the
82
+ router, or you want to see boot messages.
55
83
 
56
- ### A1. Write the operating system to the SD card
84
+ ### 1. Write the operating system to the SD card
57
85
 
58
- We use **Raspberry Pi OS Lite (64-bit)** — the version with no desktop. The Pi is a server;
86
+ We use **Raspberry Pi OS Lite** — the version with no desktop. The Pi is a server;
59
87
  a desktop would only consume memory and SD card space.
60
88
 
61
89
  1. Insert the SD card into your laptop and start Raspberry Pi Imager.
62
- 2. **Choose Device** → *Raspberry Pi 4*.
90
+ 2. **Choose Device** → your board.
63
91
  3. **Choose OS** → *Raspberry Pi OS (other)* → *Raspberry Pi OS Lite (64-bit)*.
92
+ * **On a Pi Zero W choose *Raspberry Pi OS Lite (32-bit)*** — the 64-bit images will not boot on
93
+ its ARMv6 processor. (A Pi Zero **2** W is 64-bit, so it takes the 64-bit image like a Pi 4.)
64
94
  4. **Choose Storage** → your SD card. Check the size shown matches your card — this erases it.
65
95
  5. Click **Next**. When asked *"Would you like to apply OS customisation settings?"*, choose
66
96
  **Edit Settings** — everything below depends on it.
@@ -72,9 +102,11 @@ a desktop would only consume memory and SD card space.
72
102
  * Type the SSID exactly as the network broadcasts it, including capitals. Imager does not store
73
103
  your Wi-Fi password; it converts it into a 64-character key using the SSID, so a mistyped SSID
74
104
  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`.
105
+ * If the Pi does not join the network, a mistyped SSID or password is much the likeliest cause -
106
+ neither is echoed back to you here, and Imager combines the two into a key, so a slip in either
107
+ looks the same later. Step 2 fixes it in a couple of minutes with `nmtui`.
108
+ * A WPA3 network cannot use the key Imager derives at all, so on one of those expect to finish the
109
+ Wi-Fi setup with `nmtui` regardless.
78
110
  10. Enable SSH, either choose *Use password authentication* or paste your public key if you already use SSH keys.
79
111
  11. Leave Raspberry Pi Connect off for now - feel free to experiment with this, as we haven't yet.
80
112
  12. Confirm that you want to save settings and write to the card, and click through operating system prompts wanting to stop you !
@@ -86,7 +118,7 @@ Eject the card, put it in the Pi, connect power.
86
118
  The first boot resizes the filesystem and reboots itself. Give it **two to three minutes** before
87
119
  expecting it to answer.
88
120
 
89
- ### A2. Log in over the network
121
+ ### 2. Log in over the network
90
122
 
91
123
  From your laptop's terminal:
92
124
 
@@ -96,6 +128,8 @@ ssh pi@frugaliot.local
96
128
 
97
129
  Say `yes` to the fingerprint question, then give the password you set in Imager.
98
130
 
131
+ **If that logged you in, go straight to step 3.** The rest of this step is for when it did not.
132
+
99
133
  **If `frugaliot.local` is not found**, the `.local` (mDNS) name is not reaching you. In order of ease:
100
134
 
101
135
  * Wait another minute and try again — the Pi may still be on its first boot.
@@ -105,7 +139,7 @@ Say `yes` to the fingerprint question, then give the password you set in Imager.
105
139
  up an address, and `ssh pi@frugaliot.local` then works over the cable. This is the least effort
106
140
  way in if the Pi is within reach of the router, and once you are logged in you can sort the Wi-Fi
107
141
  out over SSH with `nmtui` as below, no monitor or keyboard needed. Unplug the cable afterwards and
108
- check that Wi-Fi alone still gets you in.
142
+ check that Wi-Fi alone still gets you in. (Not an option on a Pi Zero — no Ethernet socket.)
109
143
  * Plug in the HDMI and keyboard, log in at the console, and run `ip addr` to read the IP address,
110
144
  and `sudo journalctl -b | grep -i wpa` to see why Wi-Fi failed. Use this when the Pi is nowhere
111
145
  near the router, or when you want to see boot messages.
@@ -141,7 +175,9 @@ the password — either way `nmtui` is the fix, and it is not worth more time th
141
175
  (`sudo nmcli device wifi list` shows each nearby network's SSID and whether it is WPA2 or WPA3;
142
176
  WPA3 cannot use a derived key at all.)
143
177
 
144
- Once logged in, bring the system up to date and reboot:
178
+ ### 3. Update the operating system and install the prerequisites
179
+
180
+ Logged in, bring the system up to date and reboot:
145
181
 
146
182
  ```
147
183
  sudo apt update
@@ -165,49 +201,138 @@ which you may not have the password for.
165
201
  address does not change. Sensor nodes and phones then have something stable to talk to even where
166
202
  `.local` names do not work.
167
203
 
168
- ### A3. Install the prerequisites
204
+ Now the packages the server needs.
205
+
206
+ **On a Pi 4** (or any 64-bit board):
169
207
 
170
208
  ```
171
209
  sudo apt install -y nodejs npm sqlite3 zsh
172
210
  node -v
173
- npm -v
174
211
  ```
175
212
 
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.
213
+ **On a Pi Zero W**, three more packages, because a 32-bit machine has to compile part of the server
214
+ in step 4 and a Lite image has no compiler:
215
+
216
+ ```
217
+ sudo apt install -y nodejs npm sqlite3 zsh build-essential python3-dev python3-setuptools
218
+ node -v
219
+ ```
220
+
221
+ What each is for:
222
+
223
+ * `nodejs` — the server needs **Node 18 or later**. Current Raspberry Pi OS (Debian 13, trixie)
224
+ provides 20.19.2, which is fine, on 32-bit as well as 64-bit. Older images shipped Node 18, also
225
+ fine. If `node -v` reports anything below 18, install a current version from
226
+ [NodeSource](https://github.com/nodesource/distributions) instead.
179
227
  * `npm` — installs the server; it is a separate package from `nodejs` on Debian.
180
228
  * `sqlite3` — the database the server keeps its accounts in.
181
- * `zsh` — the setup commands in step A6 are zsh scripts.
182
-
183
- ### A4. Install the Frugal IoT server
229
+ * `zsh` — the setup commands in step 6 are zsh scripts.
230
+ * `build-essential`, `python3-dev`, `python3-setuptools` — only needed where something has to be
231
+ compiled. `python3-setuptools` is the non-obvious one: the build uses node-gyp 8, which imports
232
+ Python's `distutils`, removed in Python 3.12, and setuptools puts an importable `distutils` back.
233
+ Without it step 4 ends in `ModuleNotFoundError: No module named 'distutils'`.
234
+
235
+ Now two settings that make the SD card last longer. A card wears out from being written to, and
236
+ these are the two places the system writes constantly without being asked to:
237
+
238
+ ```
239
+ # Cap the systemd journal, which by default is allowed to grow to a tenth of the card
240
+ sudo mkdir -p /etc/systemd/journald.conf.d
241
+ printf '[Journal]\nSystemMaxUse=16M\nSystemMaxFileSize=4M\n' | sudo tee /etc/systemd/journald.conf.d/frugal-iot.conf
242
+ sudo systemctl restart systemd-journald
243
+
244
+ # Swap out to the card only when there is genuinely no alternative
245
+ echo 'vm.swappiness=1' | sudo tee /etc/sysctl.d/99-frugal-iot-swappiness.conf
246
+ sudo sysctl --system | grep swappiness
247
+ ```
248
+
249
+ The `grep` should print `vm.swappiness = 1`. Both survive a reboot. There is more about what wears a
250
+ card out, and how to see whether yours is being written to hard, under
251
+ [Wear and tear on the SD card](#wear-and-tear-on-the-sd-card) — but nothing else there needs doing
252
+ during the install.
253
+
254
+ > **Pi Zero W: add swap before going on.** 512 MB is not enough to unpack what step 4 downloads, and
255
+ > running out does not fail cleanly — the board stops answering SSH and ping, and has to have its
256
+ > power pulled. Raspberry Pi OS enables zram, which is **not** sufficient here, because compressed
257
+ > RAM does not help when the working set is genuinely large. Add a real swap file (`dphys-swapfile`
258
+ > is not on the Trixie Lite image, so make it directly):
259
+ >
260
+ > ```
261
+ > sudo fallocate -l 2G /swapfile
262
+ > sudo chmod 600 /swapfile
263
+ > sudo mkswap /swapfile
264
+ > sudo swapon /swapfile
265
+ > free -h # should show 2.0Gi of swap
266
+ > echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
267
+ > ```
268
+ >
269
+ > **No reboot needed** — `swapon` takes effect at once, which the `free -h` line confirms. The
270
+ > `/etc/fstab` entry only matters later, so that the swap comes back after a reboot rather than
271
+ > having to be turned on by hand. Go straight on to step 4.
272
+ >
273
+ > Swapping to an SD card is slow, and wears it. That is the trade: the install takes longer, but it
274
+ > finishes instead of hanging. The `vm.swappiness=1` set above keeps the swap file available for
275
+ > emergencies like this one without it being used routinely afterwards.
276
+
277
+ ### 4. Install the Frugal IoT server
184
278
 
185
279
  The server is an npm package. Make a directory for this server to live in and install it there —
186
280
  that directory will hold your configuration, your data, and your database, while npm looks after
187
281
  the software itself underneath it in `node_modules`.
188
282
 
283
+ **On a Pi 4** (or any 64-bit board) — a few minutes, everything arrives as ready-made binaries and
284
+ nothing is compiled:
285
+
189
286
  ```
190
287
  mkdir ~/frugal-iot
191
288
  cd ~/frugal-iot
192
289
  npm install frugal-iot-server
193
290
  ```
194
291
 
195
- That pulls in the web UI (`frugal-iot-client`) and the logger (`frugal-iot-logger`) as well.
196
- Expect a few minutes on a Pi. Then set the directory up:
292
+ **On a Pi Zero W** — the extra flags make npm do one thing at a time, which lowers the peak memory
293
+ as well as being kinder to the SD card. This is the slow part; leave it running:
197
294
 
198
295
  ```
199
- npx frugal-iot-init
296
+ mkdir ~/frugal-iot
297
+ cd ~/frugal-iot
298
+ npm install --maxsockets 1 --no-audit --no-fund frugal-iot-server
299
+ ```
300
+
301
+ That pulls in the web UI (`frugal-iot-client`) and the logger (`frugal-iot-logger`) as well.
302
+
303
+ > **Pi Zero W: check that the compiled part actually built.** npm can report success while leaving
304
+ > it out, and nothing else looks wrong when that happens — the server just refuses to start later.
305
+ >
306
+ > ```
307
+ > node -e "require('sqlite3'); console.log('sqlite3 native module loads OK')"
308
+ > ```
309
+ >
310
+ > If that throws `Could not locate the bindings file`, build just that package:
311
+ >
312
+ > ```
313
+ > time npm rebuild sqlite3 --foreground-scripts
314
+ > ```
315
+ >
316
+ > **This takes around 40 minutes on a Zero W** — it is compiling SQLite itself on one slow core. As
317
+ > long as it is producing output it is working; `--foreground-scripts` is what lets you see that. If
318
+ > the board locks up, pull the power, boot it, and run the same command again: work already done is
319
+ > kept, so each attempt gets further.
320
+ >
321
+ > Most of what is being installed is not Frugal IoT. The logger depends on `firebase-admin`, which
322
+ > brings in the Google Cloud SDK — around 40 packages and 30 MB — and that is what the board
323
+ > struggles with. It is only used by organizations that configure a `firebase:` section.
324
+ > TODO make that dependency optional in frugal-iot-logger, so small boards can skip it.
325
+
326
+ Then set the directory up:
327
+
328
+ ```
329
+ npx --no frugal-iot-init
200
330
  ```
201
331
 
202
332
  This copies in the configuration files, creates the `data`, `ota` and `config.d/organizations`
203
333
  directories, and creates the database. It never overwrites anything already there, so it is also
204
334
  what you run after an upgrade to pick up newly added configuration.
205
335
 
206
- > On a 64-bit Raspberry Pi OS this installs ready-made binaries and compiles nothing. If it ever
207
- > does stop while building `sqlite3`, no binary was available for your platform, so install the
208
- > compiler toolchain and try again: `sudo apt install -y build-essential python3` then
209
- > `npm install frugal-iot-server`.
210
-
211
336
  Everything from here on is run from `~/frugal-iot`, and `npx` is how you run the server's commands
212
337
  without having to know where npm put them.
213
338
 
@@ -231,10 +356,10 @@ works on the Pi.
231
356
  > `frugaliot.local` is known to work from a laptop and from an iPhone. Android phones generally
232
357
  > cannot resolve `.local` names, so if you will view the dashboard on Android, put the Pi's IP
233
358
  > address here instead — `broker: ws://192.168.1.42:9012`, using the address `ping -c1
234
- > frugaliot.local` reported in step A2 — and reserve that address in your router if you can, so it
359
+ > frugaliot.local` reported in step 3 — and reserve that address in your router if you can, so it
235
360
  > does not change under you.
236
361
 
237
- ### A5. Install and configure the MQTT broker (Mosquitto)
362
+ ### 5. Install and configure the MQTT broker (Mosquitto)
238
363
 
239
364
  Sensor nodes publish their readings to an MQTT broker; the Frugal IoT logger subscribes to it and
240
365
  writes the readings to disk; the web UI subscribes to it to show live values. On an offline Pi,
@@ -256,16 +381,29 @@ sudo cp extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf
256
381
  (Everything in `/etc/mosquitto/conf.d/` is read in addition to the packaged
257
382
  `/etc/mosquitto/mosquitto.conf`, which keeps its own settings for logging and persistence.)
258
383
 
384
+ That file also turns off the broker's per-connection logging, because a node on a weak signal
385
+ reconnects constantly and each of those lines is a write to the SD card. Nothing is lost — the
386
+ readings themselves are recorded by the server, and everything else the broker says, including why
387
+ it refused to start, still goes to `/var/log/mosquitto/mosquitto.log`. If you are chasing a node
388
+ that keeps dropping off the network, comment out `connection_messages false` and restart the broker.
389
+
259
390
  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:
391
+ create an empty one. The accounts inside it get created for you in the next step:
262
392
 
263
393
  ```
264
- sudo touch /etc/mosquitto/mosquitto_passwords
265
- sudo chown ${USER}:mosquitto /etc/mosquitto/mosquitto_passwords
266
- sudo chmod 640 /etc/mosquitto/mosquitto_passwords
394
+ sudo install -o mosquitto -g mosquitto -m 600 /dev/null /var/lib/mosquitto/passwords
267
395
  ```
268
396
 
397
+ That makes an empty file belonging to the `mosquitto` user, readable by nobody else — one command
398
+ instead of a `touch`, a `chown` and a `chmod`.
399
+
400
+ > The ownership matters, and is easy to get wrong. Mosquitto warns unless the password file belongs
401
+ > to whoever opened it, and the broker runs as the `mosquitto` user — so the file belongs to
402
+ > `mosquitto`, and it lives under `/var/lib/mosquitto` (which that user owns) rather than
403
+ > `/etc/mosquitto` (which root owns). `mosquitto_passwd` also writes a temporary backup file
404
+ > alongside it, so it needs to write to that directory too, not just to the file. Step 6 runs it as
405
+ > the right user for you.
406
+
269
407
  Start the broker and have it start at every boot:
270
408
 
271
409
  ```
@@ -293,19 +431,24 @@ mosquitto_sub -h localhost -u nobody -P wrong -t '#'
293
431
  ```
294
432
 
295
433
  `Connection Refused: not authorised` is the **success** case here — the broker answered and
296
- requires an account. There are no accounts yet; step A6 creates the first one, and there is a
434
+ requires an account. There are no accounts yet; step 6 creates the first one, and there is a
297
435
  fuller test at the end of it.
298
436
 
299
- ### A6. Create your accounts and your organization
437
+ > If Mosquitto did not start, `npx --no frugal-iot-diagnostic` will tell you why in one step — most often
438
+ > that the password file its configuration names does not exist. Note that `systemctl status` and
439
+ > the journal only show an exit code; the actual reason is in Mosquitto's own log, which the
440
+ > diagnostic reads for you. See [When something does not work](#when-something-does-not-work).
300
441
 
301
- The database was created by `npx frugal-iot-init` in step A4, holding two accounts. One is
442
+ ### 6. Create your accounts and your organization
443
+
444
+ The database was created by `npx --no frugal-iot-init` in step 4, holding two accounts. One is
302
445
  `everyone`, which nobody logs in as — it exists so that permissions granted to all logged-in users
303
446
  have somewhere to live. The other is `superuser`, this server's administrator, which is given admin
304
447
  rights over every organization you create. It starts with no password and cannot be logged into
305
448
  until you give it one:
306
449
 
307
450
  ```
308
- npx frugal-iot-setpassword superuser "<a-good-password>"
451
+ npx --no frugal-iot-setpassword superuser "<a-good-password>"
309
452
  ```
310
453
 
311
454
  Use the same command later if you ever need to reset a password — for `superuser` or for any
@@ -314,19 +457,18 @@ other account.
314
457
  Frugal IoT groups devices as **organization → project → device**. Create yours:
315
458
 
316
459
  ```
317
- npx frugal-iot-addorganization dev "My Farm" you@example.com +61123456789 "<broker-password>"
460
+ npx --no frugal-iot-addorganization myfarm "My Farm" you@example.com +61123456789 "<broker-password>"
318
461
  ```
319
462
 
320
463
  The arguments are: organization id, display name, your email, your phone (`+` and digits only),
321
- and a password. That one command writes `config.d/organizations/dev.yaml`, creates a login account
322
- named after the organization (`dev`), grants it admin rights, creates its OTA directory, **and adds
464
+ and a password. That one command writes `config.d/organizations/myfarm.yaml`, creates a login account
465
+ named after the organization (`myfarm`), grants it admin rights, creates its OTA directory, **and adds
323
466
  the organization's account to the broker's password file** — which is why the broker had to be
324
467
  installed first.
325
468
 
326
- * **Organization id `dev`** — must be 1–10 lower-case letters or digits. It becomes the first part
469
+ * **Organization id `myfarm`** — must be 1–10 lower-case letters or digits. It becomes the first part
327
470
  of every MQTT topic, so it must match what your sensor nodes are configured to publish to.
328
- `dev` is the default in the Frugal IoT node firmware, so it is the safe choice for a first install.
329
- * **The password** goes into `config.d/organizations/dev.yaml` as `mqtt_password` and into the
471
+ * **The password** goes into `config.d/organizations/myfarm.yaml` as `mqtt_password` and into the
330
472
  broker's password file, so it is a *machine* credential: the server's logger, your sensor nodes,
331
473
  and any browser showing this organization's dashboard all authenticate to the broker with it.
332
474
  Treat it as shared, not personal.
@@ -337,45 +479,47 @@ Tell Mosquitto to re-read the password file, so the new account works:
337
479
  sudo systemctl restart mosquitto
338
480
  ```
339
481
 
340
- **Give your login its own password.** That command set the `dev` *web login* password to the same
482
+ **Give your login its own password.** That command set the `myfarm` *web login* password to the same
341
483
  string as the broker password. They serve completely different purposes, so change the login one
342
484
  now to something only you know — the broker credential is unaffected, and nothing needs to be kept
343
485
  in step:
344
486
 
345
487
  ```
346
- npx frugal-iot-setpassword dev "<your-own-login-password>"
488
+ npx --no frugal-iot-setpassword myfarm "<your-own-login-password>"
347
489
  ```
348
490
 
349
491
  **Now the full broker test.** In your SSH session subscribe as the organization, using the broker
350
492
  password you chose above:
351
493
 
352
494
  ```
353
- mosquitto_sub -h localhost -u dev -P '<broker-password>' -t '#' -v
495
+ mosquitto_sub -h localhost -u myfarm -P '<broker-password>' -t '#' -v
354
496
  ```
355
497
 
356
498
  It should sit there silently — no error, no exit. Open a **second** SSH session to the Pi and
357
499
  publish something:
358
500
 
359
501
  ```
360
- mosquitto_pub -h localhost -u dev -P '<broker-password>' -t 'dev/test/hello' -m '42'
502
+ mosquitto_pub -h localhost -u myfarm -P '<broker-password>' -t 'myfarm/test/hello' -m '42'
361
503
  ```
362
504
 
363
- `dev/test/hello 42` appearing in the first window proves the account, the password file and the
505
+ `myfarm/test/hello 42` appearing in the first window proves the account, the password file and the
364
506
  port 1883 listener your sensor nodes use are all working. The WebSocket listener on 9012 gets
365
- exercised by the browser in step A7. Leave the subscriber running if you like — it is a useful
507
+ exercised by the browser in step 7. Leave the subscriber running if you like — it is a useful
366
508
  window onto what your nodes are doing. (`Ctrl-C` stops it.)
367
509
 
368
510
  If you get `Connection Refused: not authorised`, the password does not match the one in
369
- `config.d/organizations/dev.yaml`, or Mosquitto has not re-read the file since it changed.
511
+ `config.d/organizations/myfarm.yaml`, or Mosquitto has not re-read the file since it changed.
512
+ `npx --no frugal-iot-diagnostic` checks this for every organization you have, and reports which ones the
513
+ broker actually accepts.
370
514
 
371
- ### A7. Start the server by hand and check it
515
+ ### 7. Start the server by hand and check it
372
516
 
373
517
  From your install directory — `npx` looks for the server in the current directory's `node_modules`,
374
518
  so this only works there:
375
519
 
376
520
  ```
377
521
  cd ~/frugal-iot
378
- npx frugal-iot-server
522
+ npx --no frugal-iot-server
379
523
  ```
380
524
 
381
525
  > If npx answers with `Need to install the following packages: frugal-iot-server` and asks to
@@ -388,7 +532,7 @@ It lists each configuration file as it reads it, then:
388
532
  readYamlConfigFile ./config.yaml
389
533
  readYamlConfigDir ./config.d
390
534
  ... one line per configuration file ...
391
- Broker ws://frugaliot.local:9012 - organizations: dev
535
+ Broker ws://frugaliot.local:9012 - organizations: myfarm
392
536
  Doing OTA updates at /ota_update from /home/pi/frugal-iot/ota
393
537
  Serving /node_modules from ./node_modules
394
538
  User Database exists
@@ -400,16 +544,16 @@ Mounted API routes at /api
400
544
  Added API error handler
401
545
  Serving /data from ./data
402
546
  Server starting on port 8080
403
- mqtt dev connecting
404
- mqtt dev connect
405
- Subscribing topic dev/# 0
547
+ mqtt myfarm connecting
548
+ mqtt myfarm connect
549
+ Subscribing topic myfarm/# 0
406
550
  ```
407
551
 
408
552
  Check the `Broker` line names your own broker and your organization. The lines that matter most are
409
- the last three: `mqtt dev connect` means the server reached the broker and authenticated, and
410
- `Subscribing topic dev/#` means it is listening for your nodes. If instead you see repeated
411
- `mqtt dev close`, `offline`, or `Not authorized`, the broker URL or the password is wrong — recheck
412
- `config.d/mqtt.yaml` and that the password in `config.d/organizations/dev.yaml` matches the `dev`
553
+ the last three: `mqtt myfarm connect` means the server reached the broker and authenticated, and
554
+ `Subscribing topic myfarm/#` means it is listening for your nodes. If instead you see repeated
555
+ `mqtt myfarm close`, `offline`, or `Not authorized`, the broker URL or the password is wrong — recheck
556
+ `config.d/mqtt.yaml` and that the password in `config.d/organizations/myfarm.yaml` matches the `myfarm`
413
557
  broker account.
414
558
 
415
559
  Once nodes are reporting, each reading is logged as it arrives, so this output keeps scrolling.
@@ -426,7 +570,7 @@ http://frugaliot.local:8080
426
570
  > local networks"* the first time. **Allow it** — without that permission the browser cannot look up
427
571
  > `frugaliot.local`, nor reach the broker at that name, so the page and the live data both fail.
428
572
 
429
- You land on the Frugal IoT home page. Click **Dashboard**, and log in as username `dev` with the
573
+ You land on the Frugal IoT home page. Click **Dashboard**, and log in as username `myfarm` with the
430
574
  login password you set with `frugal-iot-setpassword` — not the broker password. (`superuser` and its
431
575
  password work too.)
432
576
 
@@ -438,12 +582,17 @@ actually resolve: a phone that cannot look up `.local` names needs the Pi's IP a
438
582
 
439
583
  Until a sensor node reports in there will be no data to look at, but the dashboard should load.
440
584
 
585
+ > Anything wrong here — the server not starting, `mqtt myfarm close` instead of `connect`, the page not
586
+ > loading, the MQTT status not reaching *connected* — is worth a `npx --no frugal-iot-diagnostic` in
587
+ > another terminal before digging in by hand. It tests the same chain from the Pi's side: broker
588
+ > logins, the three ports, and the web server.
589
+
441
590
  Stop the server with `Ctrl-C` before continuing.
442
591
 
443
- ### A8. Run the server as a service
592
+ ### 8. Run the server as a service
444
593
 
445
594
  So that it starts automatically at boot and restarts if it crashes. The file that
446
- `npx frugal-iot-init` put in `extras/` already describes this installation — user `pi`, installed
595
+ `npx --no frugal-iot-init` put in `extras/` already describes this installation — user `pi`, installed
447
596
  into `/home/pi/frugal-iot` — so it needs no editing:
448
597
 
449
598
  ```
@@ -462,7 +611,7 @@ systemctl status frugaliot
462
611
  > `sudo systemctl restart frugaliot`. `WorkingDirectory` is the important one: it is where the
463
612
  > server looks for its configuration and database.
464
613
 
465
- To watch its log output, which is where the `mqtt dev connect` and incoming-reading messages now go:
614
+ To watch its log output, which is where the `mqtt myfarm connect` and incoming-reading messages now go:
466
615
 
467
616
  ```
468
617
  journalctl -u frugaliot -f
@@ -471,7 +620,7 @@ journalctl -u frugaliot -f
471
620
  Reboot the Pi (`sudo reboot`), wait a couple of minutes, and check `http://frugaliot.local:8080`
472
621
  still answers. Your server is now installed.
473
622
 
474
- ### A9. Point your sensor nodes at the Pi
623
+ ### 9. Point your sensor nodes at the Pi
475
624
 
476
625
  Your ESP8266/ESP32 nodes are told which broker to use in their sketch — `main.cpp`, or the `.ino`
477
626
  file if you build in the Arduino IDE. Look for a line like:
@@ -483,29 +632,29 @@ frugal_iot.configure_mqtt("frugaliot.naturalinnovation.org", "dev", "public");
483
632
  and point it at your Pi instead:
484
633
 
485
634
  ```cpp
486
- frugal_iot.configure_mqtt("frugaliot.local", "dev", "<broker-password>");
635
+ frugal_iot.configure_mqtt("frugaliot.local", "myfarm", "<broker-password>");
487
636
  ```
488
637
 
489
638
  The three arguments are the broker's host, the organization, and that organization's broker
490
- password. The organization must be the one you created in step A6, because it is the first part of
639
+ password. The organization must be the one you created in step 6, because it is the first part of
491
640
  every topic the node publishes to, and the password is the *broker* password from that step — not
492
641
  the login password. Then rebuild and flash the node as usual.
493
642
 
494
643
  > If the node does not connect, try the Pi's IP address in place of `frugaliot.local`. Resolving
495
644
  > `.local` names needs mDNS support in the firmware, which is not something this guide has
496
- > confirmed; an IP address avoids the question entirely, which is why step A2 suggests reserving one
645
+ > confirmed; an IP address avoids the question entirely, which is why step 3 suggests reserving one
497
646
  > for the Pi in your router.
498
647
 
499
- You can confirm nodes are reporting without involving the UI, using the subscriber from step A6:
648
+ You can confirm nodes are reporting without involving the UI, using the subscriber from step 6:
500
649
 
501
650
  ```
502
- mosquitto_sub -h localhost -u dev -P '<broker-password>' -t '#' -v
651
+ mosquitto_sub -h localhost -u myfarm -P '<broker-password>' -t '#' -v
503
652
  ```
504
653
 
505
654
  Every reading from every node should scroll past. Seeing anything here also proves the broker's
506
655
  port 1883 is reachable from off the Pi, which is what the nodes need.
507
656
 
508
- ### A10. HTTPS and over-the-air firmware updates
657
+ ### 10. HTTPS and over-the-air firmware updates
509
658
 
510
659
  **To be written.** Everything above gives you a plain HTTP server on your local network, which is
511
660
  all an offline installation needs. HTTPS matters for two things:
@@ -525,6 +674,16 @@ network and everything is correct — which is what you will see during this ins
525
674
  cannot: Raspberry Pi OS saves the time periodically and restores that value at boot, so after a
526
675
  power cut the Pi comes up believing it is whenever it last saved, and the gap never gets made up.
527
676
 
677
+ To see whether the Pi's clock is actually right at any moment:
678
+
679
+ ```
680
+ timedatectl
681
+ ```
682
+
683
+ `System clock synchronized: yes` means it has reached a time server and the clock is trustworthy.
684
+ `no`, with `NTP service: active`, means it is trying but has not succeeded — normal on a Pi with no
685
+ route to the internet, and the point at which the timestamps below become a concern.
686
+
528
687
  What that affects:
529
688
 
530
689
  * **Logged data is stamped with the wrong time**, so graphs and history drift after each power cut.
@@ -538,35 +697,133 @@ it whenever its own clock looks implausible (a date far in the past). Not accura
538
697
  but close enough for sensor data.
539
698
 
540
699
  ---
700
+ ### Wear and tear on the SD card
701
+
702
+ SD cards wear out from being written to, and a server recording sensor readings writes all the time.
703
+ We have not yet seen a card fail, but plan for one eventually: keep a copy of `config.d/` and
704
+ `frugal-iot.db` somewhere else, and treat the readings in `data/` as valuable but not irreplaceable.
705
+
706
+ A new installation is set up to write as little as it reasonably can, and the settings are worth
707
+ knowing about, because turning one of them back on for debugging and forgetting is easy to do:
708
+
709
+ | What | Where | Shipped as |
710
+ | ---- | ----- | ---------- |
711
+ | A line logged for every MQTT message received | `verbose:` in `config.d/logger.yaml` | `false` |
712
+ | Readings held in memory and written out periodically instead of one at a time | `flushseconds:` in `config.d/logger.yaml` | `300` (5 minutes) |
713
+ | A line logged for every web request | `morgan:` in `config.d/server.yaml` | `false` |
714
+ | Old readings compressed, and deleted if the disk fills | `housekeeping:` in `config.d/server.yaml` | compress after 2 days, never delete, keep 10% free |
715
+ | A line logged for every device connect and disconnect | `connection_messages` in `extras/mosquitto.conf` | `false` |
716
+ | A capped systemd journal, and swapping only as a last resort | `/etc/systemd/journald.conf.d/` and `/etc/sysctl.d/`, both set in step 3 | 16 MB journal, `vm.swappiness=1` |
717
+
718
+ Nothing in that table needs doing — step 3 and step 5 set all of it up. It is here because turning
719
+ one of them back on for debugging and then forgetting is easy to do.
720
+
721
+ `flushseconds` is the one with a cost attached: readings that have not been written out yet are only
722
+ in memory, so pulling the power loses up to five minutes of them. Stopping the server properly
723
+ (`sudo service frugaliot stop`, or a restart) writes them out first, and so does looking at a graph.
724
+
725
+ On a Pi 4 there should be very little swapping in any case — the server and Mosquitto together are a
726
+ small load for 1 GB or more. The Pi Zero W is the one to watch, since step 3 adds a 2 GB swap file to
727
+ get through the install. To see what is actually happening on your board:
728
+
729
+ ```
730
+ cd ~/frugal-iot
731
+ npx --no frugal-iot-diagnostic
732
+ ```
733
+
734
+ and read the **Wear on the SD card** section, which reports how much has been written since boot,
735
+ whether the machine is swapping, and whether each of the settings above is in force. Run it twice a
736
+ few days apart — the interesting number is the rate, not the total.
737
+
738
+ ## When something does not work
739
+
740
+ From your install directory:
741
+
742
+ ```
743
+ cd ~/frugal-iot
744
+ npx --no frugal-iot-diagnostic
745
+ ```
746
+
747
+ It only looks — it changes nothing — and it ends with a **Summary** of anything it recognises as
748
+ broken. It covers most of the checks scattered through this guide, in one pass:
749
+
750
+ * the machine, the OS, free memory and disk, and whether the clock is synchronized
751
+ * the versions of node, npm, sqlite3, mosquitto, and of the three Frugal IoT packages
752
+ * what this directory holds: configuration, database, accounts, organizations
753
+ * Mosquitto's configuration, **which password file it names and whether that file exists**, who owns
754
+ it, and the accounts in it
755
+ * whether Mosquitto and the `frugaliot` service are running, with the last lines of the journal
756
+ **and of Mosquitto's own log**, which is where its startup errors actually appear
757
+ * whether the broker refuses a wrong password, and accepts each organization's real one
758
+ * whether ports 1883, 9012 and 8080 are listening, and whether the web server answers
759
+ * whether the broker host in `config.d/mqtt.yaml` resolves from this machine
760
+ * how much data the logger has written
761
+
762
+ Two things to know:
763
+
764
+ * Mosquitto's log belongs to the `mosquitto` user. The script reads it with `sudo` where it can; if
765
+ it says it could not, run the whole thing as `sudo npx --no frugal-iot-diagnostic`.
766
+ * The output is safe to paste into a bug report. Passwords are deliberately not printed — the
767
+ password file is listed by account name only, and organizations by name.
768
+
769
+ If you have not reached step 4 yet, the command does not exist. Copy
770
+ [scripts/diagnostic.zsh](https://github.com/mitra42/frugal-iot-server/blob/main/scripts/diagnostic.zsh)
771
+ to the Pi and run `zsh diagnostic.zsh` instead.
772
+
773
+ ---
774
+
775
+ ## Upgrading
541
776
 
542
- ## Part B Raspberry Pi Zero W
777
+ To move an existing server to a newer release, from the directory you installed into:
543
778
 
544
- **Not yet tested — do not follow this section expecting it to work.** It records what is known
545
- and what has to be checked.
779
+ ```
780
+ cd ~/frugal-iot
781
+ npm update frugal-iot-server
782
+ npx --no frugal-iot-init
783
+ sudo systemctl restart frugaliot
784
+ ```
546
785
 
547
- First, work out which board you have, because they are very different:
786
+ Taking those in turn:
548
787
 
549
- * **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.
552
- * **Raspberry Pi Zero W** (the original) 32-bit ARMv6 (BCM2835), 512 MB RAM. This is the
553
- awkward one: it needs the 32-bit Raspberry Pi OS, and **the official Node.js builds no longer
554
- support ARMv6**. Unofficial ARMv6 builds exist at
555
- [unofficial-builds.nodejs.org](https://unofficial-builds.nodejs.org/download/release/), but
556
- which versions are available, and whether one recent enough for this server exists, needs checking.
788
+ * `npm update` fetches the new version of the server, and of the client and logger it depends on.
789
+ * `frugal-iot-init` adds any configuration file or directory the new version expects and you do not
790
+ have yet. It never changes files you already have, so your settings, database and data are safe.
791
+ * `systemctl restart frugaliot` is what actually puts the new version into service without it the
792
+ old one keeps running from memory, and nothing appears to have changed. (Skip this if you are
793
+ running the server by hand rather than as a service; stop it with `Ctrl-C` and start it again.)
794
+
795
+ Check it came back up, and is running the version you expect:
796
+
797
+ ```
798
+ systemctl status frugaliot
799
+ journalctl -u frugaliot -n 30
800
+ npm ls frugal-iot-server
801
+ ```
802
+
803
+ `frugal-iot-init` leaves your files alone, so watch its output for a section headed **"These files
804
+ were left as you have them, but this release ships a different version"**. It gives you the `diff`
805
+ command for each. That matters most for the files you copy somewhere else — a changed
806
+ `extras/mosquitto.conf` or `extras/frugaliot.service` does nothing until you install it again:
807
+
808
+ ```
809
+ sudo cp extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf && sudo systemctl restart mosquitto
810
+ sudo cp extras/frugaliot.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart frugaliot
811
+ ```
557
812
 
558
- Differences to expect on either Zero:
813
+ > It does not compare `config.yaml`, `config.d/mqtt.yaml`, `config.d/logger.yaml` or
814
+ > `config.d/server.yaml`, because those hold your own settings and would differ every time. If a
815
+ > release note mentions a new setting in one of them, compare it yourself:
816
+ > `diff config.d/server.yaml node_modules/frugal-iot-server/config.d/server.yaml`
559
817
 
560
- * Wi-Fi is 2.4 GHz only on the original Zero W it cannot see a 5 GHz-only network.
561
- * 512 MB of RAM is tight for `npm install`. Increasing the swap file is likely to be necessary.
562
- * Micro-USB power and micro-USB OTG, not USB-C and USB-A different cables from the Pi 4.
563
- * No Ethernet socket, so the Ethernet fallback in Part A is unavailable. (The original Zero W can
564
- be reached as a USB gadget over the data port, which is an alternative worth documenting.)
565
- * Everything will be slow. `npm install` may take a long time, especially if `sqlite3` has to be
566
- compiled from source — which is likely on ARMv6, since no ready-made binaries are published.
818
+ Because your own configuration is left alone, an upgraded server goes on behaving exactly as it did,
819
+ which for the settings under [Wear and tear on the SD card](#wear-and-tear-on-the-sd-card) means it
820
+ goes on writing as often as it did. A server that has been running since before those settings
821
+ existed keeps logging every message and every web request, and writing every reading as it arrives
822
+ deliberately, since quietening somebody's server without being asked is not a thing an upgrade should
823
+ do. On a Pi, compare the two files above and copy across the settings you want.
567
824
 
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.
825
+ Your organizations, accounts, database and logged data are untouched by an upgrade they live in
826
+ this directory, not in `node_modules`.
570
827
 
571
828
  ---
572
829
 
@@ -576,28 +833,28 @@ Things this guide currently states with less confidence than the rest, to be res
576
833
  following it on real hardware. Please correct them in place, and delete them from this list, as
577
834
  they get settled.
578
835
 
579
- **Part A, to check while installing**
836
+ **To check while installing**
580
837
 
581
- 1. **`.local` from an Android phone** (step A2, A4) - confirmed working from a laptop (with the
838
+ 1. **`.local` from an Android phone** (steps 3, 4) - confirmed working from a laptop (with the
582
839
  local-network permission granted on a Mac) and from an iPhone. Android is expected to fail, which
583
- is what the note in A4 assumes; worth confirming on a real Android phone.
840
+ is what the note in step 4 assumes; worth confirming on a real Android phone.
584
841
 
585
842
  **Needs information I do not have**
586
843
 
587
- 2. **Which Pi Zero** (Part B) original Zero W, Zero 2 W, or both.
844
+ 2. **Pi Zero 2 W** - the Zero W notes come from a real install; the Zero 2 W is untested. Being
845
+ 64-bit it should take the 64-bit image and avoid the `sqlite3` compile entirely, which is the slow
846
+ part, but it has the same 512 MB and so probably still needs the swap file in step 3.
588
847
  3. **Bridging to the shared server** — the local broker could optionally bridge to
589
848
  naturalinnovation.org so data also reaches the shared server. Not covered here; a later task.
590
- 4. **Organization naming** — this guide sets up exactly one organization named `dev`, because that
849
+ 4. **Organization naming** — this guide sets up exactly one organization named `myfarm`, because that
591
850
  is the node firmware's default. Is that the right default for a farm installation, or should the
592
851
  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
852
 
597
- **Tested on**
853
+ ## Tested on
598
854
 
599
- Fill this in as you go, so we know what the guide has actually been proven against:
855
+ What the guide has actually been proven against add a row for each run:
600
856
 
601
- | Date | Board | OS image | Node version | Result |
602
- | --- | --- | --- | --- | --- |
603
- | | Pi 4 Model B | | | |
857
+ | Date | Board | OS image | Node | Server | Result |
858
+ | --- | --- | --- | --- | --- | --- |
859
+ | 2026-08-17 | Pi Zero W (original), 512 MB | Raspberry Pi OS Lite **32-bit**, Debian 13 (trixie), kernel 6.18.39+rpt-rpi-v6 | 20.19.2 and npm 9.2.0, both from `apt` — they run on ARMv6 | frugal-iot-server 0.3.5 from npm | Steps 1-8 completed. `sqlite3` had to be compiled: about 40 minutes, after `build-essential python3-dev python3-setuptools` (without setuptools it fails on `distutils`). A 2 GB swap file was required — zram alone was not enough and the board locked up hard without it — and `npm install` was run with `--maxsockets 1`. Step 9 (sensor nodes) not exercised on this board, but the broker publish/subscribe tests in step 6 passed. |
860
+ | 2026-08-16 | Pi 4 Model B, 4GB | Raspberryy 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.5 from npm | Steps 1-9 completed, plus upgrade over top. Dashboard reached over `frugaliot.local` from a Mac (Chrome, after allowing local network access) and from an iPhone, MQTT status *connected*. 10 (HTTPS/OTA) not exercised.