frugal-iot-server 2.0.8 → 2.0.10

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.
@@ -1,25 +1,22 @@
1
1
  # Installing OpenSearch, Grafana and Dashboards
2
2
 
3
3
  This is for **browsing and charting your readings**. All of it is optional - the server and logger work
4
- without any of it, and the readings are always safe in the CSV files under `data/`.
4
+ without any of it, and the readings are always safe in the CSV files under `data/`,
5
+ and the client has its own graphing.
5
6
 
6
7
  ## What the pieces are
7
8
 
8
- OpenSearch is **two programs**, and it matters which is which:
9
+ OpenSearch is **two programs**:
9
10
 
10
11
  - **OpenSearch** is the database. It stores and searches the readings, and listens on port 9200.
11
- - **OpenSearch Dashboards** is a separate web app for searching and charting what is in OpenSearch. It
12
- holds no readings: it asks OpenSearch for them, over port 9200, using one login in its own
12
+ - **OpenSearch Dashboards** is a separate web app for searching and charting what is in OpenSearch.
13
+ It access readings from OpenSearch at port 9200, using one login in its own
13
14
  configuration file. (It also keeps its own saved dashboards *inside* OpenSearch, in an index.)
14
15
 
15
- **Grafana** is a third, unrelated program that makes charts. It can read the server directly
16
- ([GRAFANA.md](GRAFANA.md)), needing no OpenSearch at all.
17
-
18
16
  ## The ways to use it
19
17
 
20
18
  | | Readings are kept in | You look at them with | Set up |
21
19
  |---|---|---|---|
22
- | **Grafana, reading the server** *(start here)* | the server's CSV files, all of them | Grafana on your computer | [GRAFANA.md](GRAFANA.md); needs no OpenSearch |
23
20
  | **(a) OpenSearch on the server** | OpenSearch on the production server | OpenSearch Dashboards on **your own computer** | sections 2, 3, 4, 5 |
24
21
  | **(b) OpenSearch on your own machine** | OpenSearch on a machine you run, which the server sends readings to | whatever you like - you run it all | section 6 |
25
22
 
@@ -28,9 +25,8 @@ Either of (a) and (b) can be used with Grafana too.
28
25
  **None of this is for the Raspberry Pi bridge.**
29
26
 
30
27
  > **Status:** the Grafana and Mac steps were followed on macOS with Homebrew (OpenSearch 3.9.0,
31
- > Dashboards 3.9.0, Grafana 13.3.0). The Linux steps, the logins for Dashboards, and the Apache setup
32
- > are written from OpenSearch's documentation and have **not been run on a Linux server** - correct them
33
- > where they differ. [Open questions](#open-questions) lists what to check first.
28
+ > Dashboards 3.9.0, Grafana 13.3.0). The Linux steps, the logins for Dashboards, and the Apache setup
29
+ > have been run on the production server at frugaliot.naturalinnovation.org
34
30
 
35
31
  We use **OpenSearch**, not Elasticsearch: it is Apache-licensed, installs with Homebrew and apt, and
36
32
  Grafana can read it. OpenSearch Dashboards is its fork of Kibana, so Kibana guides are close but not
@@ -38,117 +34,122 @@ identical. (The reasoning is also in a comment above the `OpenSearch` class in f
38
34
  `index.js`, in case the decision is revisited.)
39
35
 
40
36
  How heavy is each? OpenSearch is the heavy one: a Java program with its own 1 GB heap by default.
41
- Grafana and Dashboards are small by comparison (about 60 MB and 20 MB resident on a Mac, which
37
+ Dashboards are small by comparison (about 60 MB and 20 MB resident on a Mac, which
42
38
  under-reports, so read that loosely).
43
39
 
44
- ---
45
-
46
- ## 1. On your own computer: Grafana
40
+ ## 1. (a) On the server: OpenSearch
47
41
 
48
- On a Mac:
42
+ **Debian or Ubuntu.**
49
43
 
44
+ **First, check there is room.** OpenSearch with its default 1 GB heap takes about 1.4 GB of RAM in
45
+ total (measured on a 2.4 GB server, where it became by far the biggest process). Run this *before*
46
+ installing:
50
47
  ```sh
51
- brew install grafana
52
- brew services start grafana
48
+ free -h
53
49
  ```
50
+ The `available` column is what is left for OpenSearch; you want at least 1.5 GB. If you have less, either add
51
+ memory or give OpenSearch a smaller heap (the step before starting it, below). Run it again after
52
+ OpenSearch is started and `available` will, of course, be much lower; `top` (press `M` to sort by
53
+ memory, `q` to quit) shows what is using it.
54
54
 
55
- Open <http://localhost:3000>. Grafana's default login is `admin` / `admin`, and it asks you to change it.
56
- Then add the Infinity plugin - in Grafana, Administration → Plugins → "Infinity" - or from the shell:
57
-
55
+ Install the prerequisites
58
56
  ```sh
59
- /opt/homebrew/opt/grafana/bin/grafana cli --homepath /opt/homebrew/opt/grafana/share/grafana \
60
- --pluginsDir /opt/homebrew/var/lib/grafana/plugins plugins install yesoreyeram-infinity-datasource
61
- brew services restart grafana
57
+ sudo apt-get update && sudo apt-get -y install lsb-release ca-certificates curl gnupg2
62
58
  ```
63
-
64
- For other systems, Grafana's own download page has installers for [Windows, Linux and others](https://grafana.com/grafana/download).
65
-
66
- Then continue at [GRAFANA.md](GRAFANA.md) to make a token and chart your readings.
67
-
68
- To stop it: `brew services stop grafana` (Homebrew restarts it at login until you do).
69
-
70
- ---
71
-
72
- ---
73
-
74
- ## 2. (a) On the server: OpenSearch
75
-
76
- Debian or Ubuntu.
77
-
78
- `apt-get` does the installing. The `curl` line just downloads the signing key for OpenSearch's
79
- repository, which apt needs in order to trust it:
80
-
59
+ Fetch the signing key for OpenSearch's repository
81
60
  ```sh
82
- sudo apt-get update && sudo apt-get -y install lsb-release ca-certificates curl gnupg2
83
61
  sudo mkdir -p /etc/apt/keyrings
84
62
  curl -fsSL https://artifacts.opensearch.org/publickeys/opensearch-release.pgp \
85
63
  | sudo gpg --dearmor -o /etc/apt/keyrings/opensearch.gpg
86
64
  ```
65
+ Add OpenSearch's apt repository
66
+ ```sh
67
+ echo "deb [signed-by=/etc/apt/keyrings/opensearch.gpg] https://artifacts.opensearch.org/releases/bundle/opensearch/3.x/apt stable main" \
68
+ | sudo tee /etc/apt/sources.list.d/opensearch-3.x.list
69
+ ```
87
70
 
88
- Then add OpenSearch's apt repository - the `3.x/apt` line from
89
- [their Debian guide](https://docs.opensearch.org/latest/install-and-configure/install-opensearch/debian/),
90
- which is printed there for your architecture - and install, choosing an admin password now:
91
-
71
+ Choose a strong password to replace `<strong password>` with below. Installing does not start it yet.
92
72
  ```sh
93
73
  sudo apt-get update
94
- sudo env OPENSEARCH_INITIAL_ADMIN_PASSWORD='<choose-a-strong-password>' apt-get install opensearch
74
+ sudo env OPENSEARCH_INITIAL_ADMIN_PASSWORD='<strong password>' apt-get install opensearch
95
75
  sudo systemctl enable opensearch
96
- sudo systemctl start opensearch
97
- curl https://localhost:9200 -u 'admin:<that password>' --insecure
98
76
  ```
99
77
 
100
- (That last `curl` only checks that it is answering. `--insecure` here means "do not check the
101
- certificate", because the one it ships with is self-signed.)
78
+ **Set the memory before starting.** It is set in `/etc/opensearch/jvm.options`, which ships with `-Xms1g`
79
+ and `-Xmx1g` (a 1 GB heap). If `free -h` above showed less than 1.5 GB available, change both lines to
80
+ `512m`, which is enough for a little data: all 598,000 readings of the `dev` organization came to about
81
+ 35 MB. More heap helps with a lot of history, but the machine needs the whole amount free *beyond* what
82
+ the server, broker, database and logger already use. You can change it later, then
83
+ `sudo systemctl restart opensearch`.
84
+
85
+ The other settings, in `/etc/opensearch/opensearch.yml`, need no change.
86
+
87
+ Now start it. This can take a couple of minutes.
88
+ ```sh
89
+ sudo systemctl start opensearch
90
+ ```
91
+ Check it is answering (`--insecure` means "do not check the certificate", because the one it ships with
92
+ is self-signed):
93
+ ```sh
94
+ curl https://localhost:9200 -u 'admin:<strong password>' --insecure
95
+ ```
102
96
 
103
97
  This install **has the security plugin switched on**: HTTPS, a login, and that self-signed certificate.
104
- The password is only used to set up that demo security configuration. Config is in
105
- `/etc/opensearch/opensearch.yml` and the memory setting in `/etc/opensearch/jvm.options`.
98
+ The password is only used to set up that demo security configuration.
106
99
 
107
- - **Memory:** the file ships with `-Xms1g` and `-Xmx1g` (a 1 GB heap). Give both lines the same value;
108
- more helps with a lot of history, and the machine needs that much free *beyond* what the server,
109
- broker and logger use. The data is small: all 598,000 readings of the `dev` organization came to
110
- about 35 MB.
111
100
  - **If it will not start**, look at `journalctl -u opensearch`. A common cause on Linux is the kernel
112
101
  setting `vm.max_map_count` being below 262144; `sudo sysctl -w vm.max_map_count=262144` fixes it until
113
102
  reboot, and a line in `/etc/sysctl.d/` makes it permanent.
114
103
  - **Leave OpenSearch listening on the machine itself** (the default). Only Apache, below, is open to the
115
104
  internet - never port 9200 directly. Check under [Keeping it safe](#keeping-it-safe).
116
105
 
117
- ### Tell the server about it
106
+ ### Tell the Frugal IoT Server about it
118
107
 
119
- Make `config.d/opensearch.yaml` in the server's directory (a commented example ships there), and restart
120
- the server:
108
+ Make `config.d/opensearch.yaml` in the server's directory (a commented example ships there),
121
109
 
122
110
  ```yaml
123
- opensearch:
124
- node: https://localhost:9200 # where THIS server reaches OpenSearch: on this machine
125
- username: admin # what the forwarder writes readings as
126
- password: <the password you chose>
127
- insecure: true # OpenSearch's demo certificate cannot be checked; fine on this machine
128
- admin: # the account that makes the per-organization logins (section 4)
129
- username: admin
130
- password: <the same>
131
- public: https://search.example.org # the address people outside reach it at - section 3
111
+ node: https://localhost:9200 # where THIS server reaches OpenSearch: on this machine
112
+ username: admin # what the forwarder writes readings as
113
+ password: <the password you chose>
114
+ insecure: true # OpenSearch's demo certificate cannot be checked; fine on this machine
115
+ admin: # the account that makes the per-organization logins (section 4)
116
+ username: admin
117
+ password: <the same>
118
+ public: https://search.example.org # the address people outside reach it at - section 3
132
119
  ```
120
+ and restart the Frugal IoT server:
121
+
122
+ ```sh
123
+ sudo service frugaliot restart
124
+ ```
125
+
126
+ The lines go at the top level of the file - the file's name already says they are about `opensearch`.
133
127
 
134
- This file is never sent to a browser. Using `admin` for everything is the simplest thing that works, not
128
+ This file is never sent to a browser. `username:` and `password:` at the top must be an account OpenSearch
129
+ knows - the `admin` one you chose when installing - or every send fails with "Response Error" and the
130
+ logger's journal says so. Using `admin` for everything is the simplest thing that works, not
135
131
  the best: a role that can only write `frugaliot-*` for the forwarder would be better, and is not yet
136
132
  written. Leave `admin:` and `public:` out until you have done sections 3 and 4.
137
133
 
138
134
  ### Send readings to it
139
135
 
140
- In the dashboard, open the project's back, then the **Forwarders** card, and add **OpenSearch on this
136
+ In the dashboard, open the project's back, then the **Forwarders** card (not the **OpenSearch** card), and add **OpenSearch on this
141
137
  server**. There is no address or password to type: they come from the file above. Optionally set *Only
142
138
  these topics*, e.g. `dev/lotus/#`; leave it empty to send everything that is logged.
143
139
 
144
- It applies at once, with no restart, and creates what it needs in OpenSearch (an index template that
140
+ Readings are sent in batches every few minutes, so the first ones take a few minutes to show up. It applies at once, with no restart, and creates what it needs in OpenSearch (an index template that
145
141
  makes `value` a number) the first time it sends. An organization's administrator can add it for their own
146
142
  organization, so only do this if you are happy for them to have their readings in OpenSearch.
147
143
 
148
144
  ### Loading the history you already have
149
145
 
150
- A forwarder only sends readings from the moment it is switched on. To bring in everything already on
151
- disk, from the server's directory (safe to repeat, and safe while the server is running):
146
+ A forwarder only sends readings from the moment it is switched on. To bring in what is already recorded,
147
+ open the **Forwarders** card, and on the OpenSearch forwarder's row press **Send earlier readings...**.
148
+ Pick the day to start from (leave it empty for everything), optionally tick *Only count them* to see how
149
+ many there are first, and press **Start**. It runs in the background and the card shows how many it has
150
+ sent so far. It is safe to repeat, and safe while the server is running.
151
+
152
+ The same thing from a terminal, in the server's directory, if you prefer:
152
153
 
153
154
  ```sh
154
155
  npx --no frugal-iot-backfill-opensearch dev dry-run # count what would be sent
@@ -158,12 +159,22 @@ npx --no frugal-iot-backfill-opensearch dev 2026-09-01 # only files from that d
158
159
 
159
160
  ### Looking after it
160
161
 
162
+ Run these on the server, in a terminal. `admin:<strong password>` is the login and password you chose when
163
+ installing OpenSearch; `--insecure` is the same "don't check the certificate" as before.
164
+
165
+ - **See what is there.** One line per index, with its size:
166
+ ```sh
167
+ curl -s 'https://localhost:9200/_cat/indices/frugaliot-*?v' -u 'admin:<strong password>' --insecure
168
+ ```
161
169
  - **Old months.** Readings go into one index per organization per month, named like
162
- `frugaliot-dev-2026.09`. To drop a month: `curl -X DELETE https://localhost:9200/frugaliot-dev-2026.02 -u admin:... --insecure`.
163
- The CSV files are untouched.
170
+ `frugaliot-dev-2026.09` (that name is in the first column above). To drop one:
171
+ ```sh
172
+ curl -X DELETE 'https://localhost:9200/frugaliot-dev-2026.02' -u 'admin:<strong password>' --insecure
173
+ ```
174
+ The CSV files are untouched, so nothing is lost for good.
164
175
  - **A gap while OpenSearch was down.** The forwarder does not retry a batch it could not send; the CSV
165
- files have everything, and the backfill above fills the gap.
166
- - **Starting afresh.** Delete the indices (`frugaliot-*`) and run the backfill again.
176
+ files have everything, and *Send earlier readings* (above) fills the gap.
177
+ - **Starting afresh.** Delete each index listed above, as for an old month, and send the earlier readings again.
167
178
 
168
179
  ---
169
180
 
@@ -171,15 +182,25 @@ npx --no frugal-iot-backfill-opensearch dev 2026-09-01 # only files from that d
171
182
 
172
183
  ## 3. (a) Apache: HTTPS in front of OpenSearch
173
184
 
174
- Dashboards on someone's computer reaches OpenSearch across the internet, so its login and the readings
175
- must travel over HTTPS. OpenSearch itself stays on the machine (port 9200, loopback); **Apache is put
176
- in front of OpenSearch**, not in front of Dashboards. It is the same Apache that already sits in front
177
- of this server - add one more site to it, on its own name, e.g. `search.example.org`.
185
+ Dashboards on someone's computer reach OpenSearch across the internet, it needs HTTPS.
186
+
187
+ While opensearch ships with HTTP and some self-signed certificates, for most practical installs it will be behind an Apache proxy.
178
188
 
189
+ We'll add a domain name to it - e.g. `opensearch.naturalinnovation.org`
190
+
191
+ Add components needed for Apache
179
192
  ```sh
180
193
  sudo a2enmod ssl proxy proxy_http headers
181
194
  ```
182
195
 
196
+ You need a DNS record pointing `search.example.org` (use your own name) at this server, and a
197
+ certificate for it, made the way you made the server's own, e.g. `sudo certbot certonly --apache -d search.example.org`.
198
+
199
+ Your certificate may be a wildcard one (e.g. `*.example.org`) you already have; then use its files in
200
+ the two `SSLCertificate...` lines below.
201
+
202
+ Make a new file for this site, `/etc/apache2/sites-available/opensearch.conf` (Debian and Ubuntu keep
203
+ every Apache site in that folder; it is separate from your existing site's file), with:
183
204
  ```apache
184
205
  <VirtualHost *:443>
185
206
  ServerName search.example.org
@@ -205,10 +226,17 @@ sudo a2enmod ssl proxy proxy_http headers
205
226
  </LocationMatch>
206
227
  </VirtualHost>
207
228
  ```
229
+ Switch the site on, then test and reload Apache:
230
+ ```sh
231
+ sudo a2ensite opensearch
232
+ sudo apachectl configtest && sudo systemctl reload apache2
233
+ ```
234
+ Check from your own computer:
235
+ `curl https://search.example.org` should answer 401 (it wants a login) - not a block of JSON, or HTML, and not a
236
+ timeout.
208
237
 
209
- Then `sudo apachectl configtest && sudo systemctl reload apache2`. Check from your own computer:
210
- `curl https://search.example.org` should answer 401 (it wants a login) - not a block of JSON, and not a
211
- timeout. Put the address in `public:` in `config.d/opensearch.yaml`.
238
+ Put the address (with the `https://`) in `public:` in `config.d/opensearch.yaml`, with the `admin:` lines
239
+ from section 1, then restart the server so it reads the file: `sudo systemctl restart frugaliot`.
212
240
 
213
241
  Port 9200 itself stays closed to the network (firewall) - only Apache's 443 is open.
214
242
 
@@ -256,17 +284,80 @@ on a Mac), start it (`brew services start opensearch-dashboards`) and open <http
256
284
 
257
285
  ```yaml
258
286
  opensearch.hosts: [ "https://search.example.org" ]
259
- opensearch.username: "frugaliot_dev"
287
+ opensearch.username: "frugaliot_dev_mitra"
260
288
  opensearch.password: "..."
261
289
  opensearchDashboards.index: ".kibana_dev"
262
- opensearch.requestHeadersAllowlist: [ authorization ]
290
+ opensearch.requestHeadersAllowlist: []
291
+ opensearch.customHeaders: { authorization: "Basic <the login, encoded - the card writes this line for you>" }
263
292
  ```
264
293
 
265
294
  The `opensearchDashboards.index` line is what gives the organization its own place to keep saved
266
- dashboards; without it Dashboards uses `.kibana`, which this login cannot touch. Then, in Dashboards:
267
- **Dashboards Management → Index patterns → Create**, enter `frugaliot-*`, and choose `@timestamp` as the
295
+ dashboards; without it Dashboards uses `.kibana`, which this login cannot touch. The last two lines stop Dashboards asking your browser for a login: Dashboards uses the login above
296
+ only for its own housekeeping, so they add the same login to every request. Copy the whole block from
297
+ the card - the `Basic ...` line is the username and password encoded, which you cannot easily write by
298
+ hand. Nothing is typed when you open Dashboards. Keep Dashboards on your own computer: it listens on
299
+ `localhost` by default, and anyone who can reach its port acts as this login.
300
+
301
+ Open opensearch - by default it will be on `http://localhost:5601`
302
+
303
+ Then, in Dashboards:
304
+ **Dashboards Management → Index patterns → Create**, enter `frugaliot-<org>-*` (e.g. `frugaliot-dev-*`; your login can read only your own organization's), and choose `@timestamp` as the
268
305
  time field.
269
306
 
307
+ If it says the pattern **"doesn't match any indices"**, OpenSearch has no readings for your organization yet:
308
+ the forwarder (section 2, "Send readings to it") is not added, or nothing has been sent. Readings only
309
+ arrive once the forwarder is on, so send the earlier ones as in "Loading the history you already have".
310
+ You can check from your own computer with the login that is in the Dashboards file - the generated one the card
311
+ showed (not the OpenSearch `admin` password). This reads it from the file on a Mac; use your own address
312
+ and organization:
313
+ ```sh
314
+ curl -s -u "$(awk -F'"' '/^opensearch.username/{u=$2} /^opensearch.password/{p=$2} END{print u":"p}' /opt/homebrew/etc/opensearch-dashboards/opensearch_dashboards.yml)" 'https://search.example.org/frugaliot-dev-*/_count'
315
+ ```
316
+ `"count":0` with `"total":0` shards means none are there; a number above 0 means they are.
317
+
318
+ If it says **"OpenSearch Dashboards server is not ready yet"**, look at its log
319
+ (`/opt/homebrew/var/log/opensearch-dashboards.log` on a Mac): it names what it could not do. A
320
+ `security_exception ... no permissions for [...]` line means the organization's role on the server is
321
+ missing that permission - press **Replace the password** on the OpenSearch card once (the server rewrites
322
+ the role), then restart Dashboards. A stalled "Another OpenSearch Dashboards instance appears to be
323
+ migrating" means a first start was interrupted; as the message says, delete the index it names
324
+ (`.kibana_<org>_1`, see "Looking after it" for the command) and restart it.
325
+
326
+ ### Seeing your readings as a chart
327
+
328
+ Checked step by step against OpenSearch Dashboards 3.9 in a browser. Each reading has these fields:
329
+ `value` (the number), `@timestamp`, and `topic` split into `node` (the device), `module` and `leaf`
330
+ (e.g. `temperature`). First look at **Discover** (☰ menu, top left): with `frugaliot-<org>-*` chosen you
331
+ see a bar chart of how many readings arrived, and the readings below it.
332
+
333
+ Now a chart of one kind of reading, a line per device:
334
+
335
+ 1. Go to **Visualize** (<http://localhost:5601/app/visualize>) and press **Create new visualization**.
336
+ Choose **Line**, then your `frugaliot-<org>-*` index pattern.
337
+ 2. At the top right, click the calendar icon beside the time and choose **Last 24 hours**. The default
338
+ is the last 15 minutes, which is nearly always empty.
339
+ 3. In the right-hand panel, under **Metrics**, open **Y-axis**. Set **Aggregation** to **Average**
340
+ (type "Average" and pick it). **Field** fills itself in with `value`.
341
+ 4. Under **Buckets** press **Add**, choose **X-axis**, and set **Aggregation** to **Date Histogram**
342
+ (**Field** fills itself in with `@timestamp`). Press **Update** at the bottom right.
343
+ You now have one line, but it averages every kind of reading together (temperature, humidity,
344
+ wifi...), so the numbers mean nothing.
345
+ 5. In the search bar above the chart type `leaf:temperature` and press Enter. Now only temperatures
346
+ are charted.
347
+ 6. Under **Buckets** press **Add** again, choose **Split series**, set **Sub aggregation** to **Terms**
348
+ and **Field** to `node`. Press **Update**. You get one line per device, with its name in the legend.
349
+ 7. Press **Save** (top right), give it a title such as `Temperature by device`, and **Save**.
350
+
351
+ Put it on a dashboard:
352
+
353
+ 1. Go to **Dashboards** (<http://localhost:5601/app/dashboards>) and **Create new dashboard**.
354
+ 2. Press **Add** (top right), click your chart in the list, and close the list with the **X**.
355
+ 3. Press **Save**, give it a title, switch on **Store time with dashboard** (otherwise it opens on the
356
+ last 15 minutes and looks empty), and **Save**.
357
+
358
+ To chart something else, change `temperature` in step 5 to `humidity` or another `leaf`, or leave the
359
+ search empty and split by `topic` instead of `node`.
360
+
270
361
  You can use the same login in **Grafana** instead, with its OpenSearch data source and the same address.
271
362
 
272
363
  ---
@@ -368,8 +459,7 @@ and only your own machine can reach it. Make `config.d/opensearch.yaml` in your
368
459
  just:
369
460
 
370
461
  ```yaml
371
- opensearch:
372
- node: http://localhost:9200
462
+ node: http://localhost:9200
373
463
  ```
374
464
 
375
465
  and restart the server. The Forwarders card then offers **OpenSearch on this server**. The Dashboards
@@ -397,11 +487,7 @@ To stop them: `brew services stop opensearch-dashboards opensearch`.
397
487
 
398
488
  Please add what you find:
399
489
 
400
- - **The Linux steps have not been run**: the exact `apt` repository line, and the Apache site above.
401
- - **The role in section 4 is untried.** Its actions are the ones OpenSearch's documentation names for
402
- Dashboards, and may be missing one - symptoms are Dashboards failing to start, or an index pattern that
403
- cannot be created. If so, OpenSearch's log names the refused action; add it to `roleFor` in
404
- `lib/opensearch-access.js`.
490
+ - **The Linux steps have not been run for the opensource-dashboards**: the exact `apt` repository line, and the Apache site above.
405
491
  - **`opensearchDashboards.index`** is meant to give each organization its own saved-objects index. Check
406
492
  that two organizations' Dashboards do not see each other's saved dashboards, and that neither can open
407
493
  the other's `.kibana_<org>`.
@@ -0,0 +1,208 @@
1
+ # Running a Frugal IoT server on an OpenWrt router
2
+
3
+ **Status: worked once, on one router, after fixes.** The Raspberry Pi installation
4
+ ([INSTALLATION.md](INSTALLATION.md)) is proven; this is the same thing re-done for OpenWrt. The first
5
+ real run - an OpenWrt One, OpenWrt 25.12.5, see [Tested on](#tested-on) - needed five attempts and
6
+ found ten problems, which are fixed or described below. Expect the next router to find more. This
7
+ document says what a router needs, how to prepare it, gives two scripts, and says what to send back.
8
+
9
+ ## What you are installing
10
+
11
+ A Frugal IoT server is three programs on one machine:
12
+
13
+ * **mosquitto**, an MQTT broker. Small sensor boards (ESP32s) publish readings to it, and the
14
+ dashboard in your browser listens to it.
15
+ * **the Frugal IoT server**, a Node.js program. It serves the dashboard on port 8080, keeps the
16
+ user logins and settings in an SQLite database, and writes every reading to disk.
17
+ * nothing else. No cloud, no internet needed once it is installed.
18
+
19
+ The router is simply where they run. It does not stop being a router.
20
+
21
+ ## What the router needs
22
+
23
+ Run the preflight script (below) and it checks every line of this table for you.
24
+
25
+ | | Needed | Why |
26
+ | --- | --- | --- |
27
+ | **CPU** | **aarch64 or x86_64.** 32-bit ARM might work. MIPS will not. | OpenWrt's `node` package is only built for some CPUs, and the database module (`sqlite3`) is a compiled binary that has to exist for that CPU and for OpenWrt's C library (musl). Both are most likely on 64-bit. |
28
+ | **Memory** | 512 MB or more (256 MB at the very least) | The server uses about 250 MB once running. |
29
+ | **Storage** | **About 400 MB free** on ext4, f2fs or btrfs | The software is ~100 MB; npm needs room to work; readings accumulate. **Most routers' own flash is far too small**, so this means a USB stick or SSD, set up as extroot or just mounted, and `--dir` pointing at it. Not FAT, exFAT or NTFS: the install needs symlinks and Unix permissions. The image may also lack the USB storage driver - see [Preparing the router](#preparing-the-router). |
30
+ | **Node.js** | **20.17 or later**, and npm | From OpenWrt's feed if it has one - **OpenWrt 25.12 has none for aarch64**, in which case it is unpacked by hand from a musl build ([how](#if-the-feed-has-no-node)). The preflight and the installer accept either. |
31
+ | **mosquitto** | A build with **websockets** | The browser dashboard connects over websockets. If the router's mosquitto has none, the dashboard cannot work. |
32
+ | | Ideally the **dynamic security plugin** | It gives every node and every user its own broker password. Without it the server falls back to one shared password per organization: it works, but it is the older and weaker scheme. |
33
+ | **Network** | Internet access while installing | opkg and npm download from their own servers. |
34
+ | **Free ports** | 1883 (nodes), 9012 (dashboard websocket), 8080 (dashboard) | Nothing on OpenWrt uses these by default; LuCI is on 80/443. |
35
+
36
+ Better to have, not required: a fixed LAN address (a router has one already), and a drive that
37
+ tolerates constant small writes - the server logs readings all day, which is hard on a cheap USB stick.
38
+
39
+ ## Preparing the router
40
+
41
+ Two things a router often lacks, both found on the first real run. Commands are shown for `apk`
42
+ (OpenWrt 25.12 and later); on older releases use `opkg install` instead of `apk add`.
43
+
44
+ ### A drive the router can see
45
+
46
+ The preflight tells you when none is visible. A USB drive formatted ext4 was invisible on the test
47
+ router because the image carried no USB storage driver:
48
+
49
+ ```sh
50
+ apk add kmod-usb-storage kmod-fs-ext4 block-mount
51
+ block info # is the drive listed now? what is it called?
52
+ block detect > /etc/config/fstab # NOTE: replaces your mount configuration - if you already have
53
+ block mount # mounts there, add the new one by hand (LuCI: System > Mount Points)
54
+ df -h /mnt/sda1
55
+ ```
56
+
57
+ Check that the drive is mounted again after a reboot (`uci show fstab`, or LuCI's Mount Points), or
58
+ the server will not find its files and will not start.
59
+
60
+ ### If the feed has no node
61
+
62
+ The preflight's feed check says `no node package in this feed` when this applies. Node.js publishes
63
+ musl builds, which is what OpenWrt uses, at <https://unofficial-builds.nodejs.org/download/release/>.
64
+ Pick a version of 20.17 or later (24.9.0 worked) and the file for your CPU: `linux-arm64-musl` for
65
+ aarch64, `linux-x64-musl` for x86_64. **Take the `.tar.gz`, not the `.tar.xz`** - BusyBox `tar` cannot
66
+ unpack xz. Put it on the drive, not the router's flash:
67
+
68
+ ```sh
69
+ cd /mnt/sda1
70
+ wget https://unofficial-builds.nodejs.org/download/release/v24.9.0/node-v24.9.0-linux-arm64-musl.tar.gz
71
+ tar xzf node-v24.9.0-linux-arm64-musl.tar.gz
72
+ for t in node npm npx; do ln -sf /mnt/sda1/node-v24.9.0-linux-arm64-musl/bin/$t /usr/bin/$t; done
73
+ apk add libstdcpp6 libatomic1 # the musl build needs both
74
+ node -v; npm -v
75
+ ```
76
+
77
+ If `node -v` says a library is missing, that is the `apk add` line. The preflight and installer then
78
+ see node on the router and do not look for a package.
79
+
80
+ ## Doing it
81
+
82
+ You need `ssh` access to the router as root. On the router, fetch the two scripts from the `main` branch:
83
+
84
+ ```sh
85
+ B=https://raw.githubusercontent.com/mitra42/frugal-iot-server/main/scripts
86
+ wget -O openwrt-preflight.sh $B/openwrt-preflight.sh
87
+ wget -O install-openwrt.sh $B/install-openwrt.sh
88
+ ```
89
+
90
+ If `wget` complains about https, the router has no TLS support for it yet
91
+ (`apk add ca-bundle libustream-mbedtls`, or `opkg update && opkg install ...` on older releases), or download the files on your computer
92
+ and copy them across with `scp`.
93
+
94
+ ### 1. Preflight - on each candidate router
95
+
96
+ ```sh
97
+ sh openwrt-preflight.sh --dir /mnt/sda1/frugal-iot --install 2>&1 | tee preflight.txt
98
+ ```
99
+
100
+ * `--dir` is where you intend to put the server: a path on the drive. The default is `/opt/frugal-iot`.
101
+ * Without `--install` it only reads things, and refreshes the package list. **With `--install` it
102
+ installs node, mosquitto and the other packages**, after asking, so that it can try mosquitto and
103
+ sqlite3 for real rather than guess. That is the same set the installer would put there anyway, but it
104
+ does use the router's own flash: it prints how much is free first. If the flash is nearly full, run
105
+ it without `--install` first.
106
+ * It ends with `PASS`/`WARN`/`FAIL` counts and a verdict. **Please send the whole of `preflight.txt`
107
+ for every router you try**, including the ones that fail - which ones fail, and why, is the most
108
+ useful thing this stage can tell us.
109
+
110
+ Which router to choose is then mostly: one with no `FAIL`, a USB port with a decent drive in it, and
111
+ 512 MB or more.
112
+
113
+ ### 2. Install - on the one you pick
114
+
115
+ ```sh
116
+ sh install-openwrt.sh --org myfarm --name "My Farm" --email you@example.com \
117
+ --phone +61123456789 --dir /mnt/sda1/frugal-iot
118
+ ```
119
+
120
+ * `--org` is 1-10 lower-case letters or digits. It becomes the first part of every MQTT topic, and
121
+ the sensor boards must be flashed with the same one. Pick something you will keep.
122
+ * Passwords you do not give are generated and printed at the end. The whole run is also logged to
123
+ `/root/frugal-iot-install-<date>.log` (readable only by root; the generated passwords are in it).
124
+ * `--dry-run` checks the arguments and the router and says what it would do, changing nothing.
125
+ * It is safe to run again after a failure: steps already done are skipped. Give it the **same**
126
+ `--org` and `--dir` as before - a different `--org` creates a second organization. Passwords already
127
+ set are left alone, and the summary says "unchanged" rather than printing a made-up one.
128
+ * It runs the server as root, as most OpenWrt services do. `--user frugal` creates and uses a
129
+ separate account instead, which is tidier but is another thing that has not been tried.
130
+
131
+ When it finishes it prints the dashboard address and the settings the sensor boards need.
132
+
133
+ ### 3. If something goes wrong
134
+
135
+ Send back:
136
+
137
+ 1. `preflight.txt`
138
+ 2. the install log (`/root/frugal-iot-install-*.log`) - or just the end of the terminal output
139
+ 3. `logread -e frugaliot | tail -50` and `logread -e mosquitto | tail -50`
140
+ 4. the output of `apk list -I | grep -E 'mosquitto|node|sqlite|zsh'` (`opkg list-installed | grep ...`
141
+ on older releases)
142
+
143
+ ## What is different from a Raspberry Pi, and why it might fail
144
+
145
+ The installer is the Pi one, redone, so these are the places it can be wrong:
146
+
147
+ * **No systemd.** The service is a procd init script, `/etc/init.d/frugaliot`. Control it with
148
+ `service frugaliot restart`. Logs go to the system log: `logread -e frugaliot`.
149
+ * **`/var` is RAM.** On OpenWrt `/var` is `/tmp`, emptied at every reboot. The broker's password file
150
+ and account database would normally live in `/var/lib/mosquitto`, which would lose every account on
151
+ restart, so the installer keeps them in `/etc/mosquitto/state` and points the configuration there.
152
+ It also moves mosquitto's own persistence (the retained values) there if it was set to a
153
+ RAM path; the original configuration is kept as `/etc/mosquitto/mosquitto.conf.pre-frugal`.
154
+ * **No bash, no zsh, no sudo, no apt.** The installer is plain `sh` and installs zsh itself (the
155
+ server's own commands are zsh scripts). BusyBox lacks `install(1)` (it is in `coreutils-install`),
156
+ which stopped the first run, so the installer places files with `cp`, `chown`, `chmod` and `mv`
157
+ instead; and its `tar` cannot unpack `.xz`. `stat`, `sed`, `grep` and `su` worked.
158
+ * **The sqlite3 module.** The most likely single failure. If a binary does not exist for your CPU
159
+ and musl, `npm install` will try to compile it, and there is no compiler. The installer stops there
160
+ with the error. A fix exists (compile it on another machine, or use a build environment) but it
161
+ is a different project.
162
+ * **The mosquitto package.** Which parts OpenWrt builds in - websockets, the dynamic security plugin,
163
+ `mosquitto_ctrl`, `mosquitto_passwd` - is the main thing nobody here knows. The preflight tries each
164
+ one, and the installer lists every mosquitto package the feed offers into the log.
165
+ * **No WiFi fallback network.** On the Pi, a setup network is raised if its WiFi is lost. A router is
166
+ the access point, so this is skipped.
167
+ * **Time.** Routers without a battery clock start at an old date until NTP runs. The server stamps
168
+ readings when they arrive, so check the router has the right time.
169
+
170
+ ## After it is running
171
+
172
+ * **Do not open the ports to the internet.** OpenWrt accepts LAN connections and refuses WAN ones by
173
+ default; leave 1883, 9012 and 8080 that way. HTTPS (needed for over-the-air firmware updates to the
174
+ boards) is not covered here.
175
+ * **Upgrading:**
176
+ ```sh
177
+ cd /mnt/sda1/frugal-iot && npm update frugal-iot-server && service frugaliot restart
178
+ ```
179
+ (and re-run `install-openwrt.sh` with the same arguments afterwards, which brings the broker
180
+ configuration up to date.)
181
+ * **If `http://<router>:8080/` answers 404** (the dashboard at `/dashboard` may still work), the
182
+ server cannot find its own web pages. An install whose `frugal-iot-init` failed part-way and was
183
+ re-run - as on the first OpenWrt run, before `init` was fixed - keeps a `config.d/server.yaml`
184
+ with `publicdir: ./public`. Set these in `<install dir>/config.d/server.yaml`, then
185
+ `service frugaliot restart`:
186
+ ```yaml
187
+ htmldir: ./node_modules/frugal-iot-client
188
+ nodemodulesdir: ./node_modules
189
+ publicdir: ./node_modules/frugal-iot-server/public
190
+ privatedir: ./node_modules/frugal-iot-server/private
191
+ ```
192
+ Without `publicdir` the home page, `/about.html` and the PWA's `service-worker.js` are missing too.
193
+ * **`npx --no frugal-iot-diagnostic`** was written for systemd and will report some things that do
194
+ not apply here. Treat it as a hint, not a verdict.
195
+ * **Removing it:**
196
+ ```sh
197
+ service frugaliot stop; /etc/init.d/frugaliot disable; rm /etc/init.d/frugaliot
198
+ rm -rf /mnt/sda1/frugal-iot # your install directory: this deletes the readings too
199
+ rm /etc/mosquitto/conf.d/frugal-iot.conf /etc/mosquitto/aclfile; rm -rf /etc/mosquitto/state
200
+ cp /etc/mosquitto/mosquitto.conf.pre-frugal /etc/mosquitto/mosquitto.conf # if it exists
201
+ /etc/init.d/mosquitto restart
202
+ ```
203
+
204
+ ## Tested on
205
+
206
+ | Date | Router | OpenWrt | Node | Server | Result |
207
+ | --- | --- | --- | --- | --- | --- |
208
+ | 2026-11 | OpenWrt One (Banana Pi), MediaTek Filogic, aarch64_cortex-a53, 986 MB; USB drive, ext4, mounted at `/mnt/sda1` | 25.12.5 with LibreMesh, `apk`, kernel 6.12.94 | v24.9.0 and npm 11.6.0, unpacked by hand (no node in the aarch64 feed) | 2.0.8 | Installed and running: broker (websockets and dynamic security both present), organization, procd service, dashboard. Took five runs: no USB storage driver; no node package; no `install` command; `frugal-iot-init` failing on `extras/grafana`, which also left `server.yaml` unconfigured and made `/` answer 404. All now handled by the scripts or described above, except the `init` fix, which needs a server release to reach a router. |
@@ -1,6 +1,6 @@
1
1
  # Systemd unit for the Frugal IoT server.
2
2
  #
3
- # As shipped this matches the Raspberry Pi installation in INSTALLATION.md: user "pi", installed
3
+ # As shipped this matches the Raspberry Pi installation in docs/INSTALLATION.md: user "pi", installed
4
4
  # with npm into /home/pi/frugal-iot. If that is what you have, install it unedited:
5
5
  # sudo cp extras/frugaliot.service /etc/systemd/system/frugaliot.service
6
6
  # sudo systemctl daemon-reload
@@ -6,7 +6,7 @@ protocol websockets
6
6
  # Kept under /var/lib/mosquitto, which belongs to the mosquitto user, rather than /etc/mosquitto,
7
7
  # which belongs to root. Both the broker and mosquitto_passwd warn unless the file belongs to
8
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.
9
+ # directory has to be writable by that same user. See docs/INSTALLATION.md step A5.
10
10
  password_file /var/lib/mosquitto/passwords
11
11
 
12
12
  # Which topics each account may reach. Without this, every account that can log in can publish and