frugal-iot-server 0.3.3 → 0.3.4

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
@@ -101,8 +101,14 @@ Say `yes` to the fingerprint question, then give the password you set in Imager.
101
101
  * Wait another minute and try again — the Pi may still be on its first boot.
102
102
  * Log in to your Wi-Fi router's admin page and look for a device called `frugaliot` in its
103
103
  list of connected clients; note its IP address and use that instead: `ssh pi@192.168.1.42`.
104
+ * **Plug an Ethernet cable from the Pi into your router.** Nothing needs configuring — the Pi picks
105
+ up an address, and `ssh pi@frugaliot.local` then works over the cable. This is the least effort
106
+ way in if the Pi is within reach of the router, and once you are logged in you can sort the Wi-Fi
107
+ 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.
104
109
  * 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.
110
+ and `sudo journalctl -b | grep -i wpa` to see why Wi-Fi failed. Use this when the Pi is nowhere
111
+ near the router, or when you want to see boot messages.
106
112
 
107
113
  **If Wi-Fi did not connect at all** — `ip addr` shows no address on `wlan0`, and the journal has
108
114
  `WPA: 4-Way Handshake failed - pre-shared key may be incorrect` — then the Pi found your network
@@ -145,8 +151,19 @@ sudo reboot
145
151
 
146
152
  Wait a minute, then `ssh pi@frugaliot.local` again.
147
153
 
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.
154
+ **Find the Pi's IP address**, because later steps can need it for the broker URL if you will view
155
+ the dashboard on a phone, and for your sensor nodes. From your laptop:
156
+
157
+ ```
158
+ ping -c1 frugaliot.local
159
+ ```
160
+
161
+ The address it prints is the Pi's. That is easier than hunting through your router's admin pages,
162
+ which you may not have the password for.
163
+
164
+ **Recommended:** if you can get into your router, give the Pi a fixed (reserved) IP address, so that
165
+ address does not change. Sensor nodes and phones then have something stable to talk to even where
166
+ `.local` names do not work.
150
167
 
151
168
  ### A3. Install the prerequisites
152
169
 
@@ -186,9 +203,10 @@ This copies in the configuration files, creates the `data`, `ota` and `config.d/
186
203
  directories, and creates the database. It never overwrites anything already there, so it is also
187
204
  what you run after an upgrade to pick up newly added configuration.
188
205
 
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`.
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`.
192
210
 
193
211
  Everything from here on is run from `~/frugal-iot`, and `npx` is how you run the server's commands
194
212
  without having to know where npm put them.
@@ -210,9 +228,11 @@ This one URL is used both by the server's own logger and by the browser UI, so i
210
228
  WebSocket (`ws://`) address that **your phone or laptop browser** can reach, not just one that
211
229
  works on the Pi.
212
230
 
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.
231
+ > `frugaliot.local` is known to work from a laptop and from an iPhone. Android phones generally
232
+ > cannot resolve `.local` names, so if you will view the dashboard on Android, put the Pi's IP
233
+ > 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
235
+ > does not change under you.
216
236
 
217
237
  ### A5. Install and configure the MQTT broker (Mosquitto)
218
238
 
@@ -254,7 +274,8 @@ sudo systemctl restart mosquitto
254
274
  systemctl status mosquitto
255
275
  ```
256
276
 
257
- `systemctl status` should say `active (running)`. Press `q` to exit. If it is not running,
277
+ `systemctl status` should say `active (running)`, and returns you to the prompt. (If the output is
278
+ long enough that it opens a pager instead, `q` gets you out.) If it is not running,
258
279
  `sudo journalctl -u mosquitto -n 50` will show what it objected to.
259
280
 
260
281
  **Check both listeners are open:**
@@ -349,45 +370,71 @@ If you get `Connection Refused: not authorised`, the password does not match the
349
370
 
350
371
  ### A7. Start the server by hand and check it
351
372
 
373
+ From your install directory — `npx` looks for the server in the current directory's `node_modules`,
374
+ so this only works there:
375
+
352
376
  ```
377
+ cd ~/frugal-iot
353
378
  npx frugal-iot-server
354
379
  ```
355
380
 
356
- You should see the configuration echoed back, then something like:
381
+ > If npx answers with `Need to install the following packages: frugal-iot-server` and asks to
382
+ > continue, you are in the wrong directory. Say no, `cd ~/frugal-iot`, and try again — otherwise
383
+ > npx fetches a throwaway copy that has none of your configuration.
384
+
385
+ It lists each configuration file as it reads it, then:
357
386
 
358
387
  ```
388
+ readYamlConfigFile ./config.yaml
389
+ readYamlConfigDir ./config.d
390
+ ... one line per configuration file ...
391
+ Broker ws://frugaliot.local:9012 - organizations: dev
359
392
  Doing OTA updates at /ota_update from /home/pi/frugal-iot/ota
360
393
  Serving /node_modules from ./node_modules
361
394
  User Database exists
362
395
  Opened user database
396
+ Exec-ed starting SQL
397
+ Created logger client for API integration
398
+ Created push manager for Farm-Platform data push
399
+ Mounted API routes at /api
400
+ Added API error handler
363
401
  Serving /data from ./data
364
402
  Server starting on port 8080
365
- Serving from ./node_modules/frugal-iot-client
366
403
  mqtt dev connecting
367
404
  mqtt dev connect
405
+ Subscribing topic dev/# 0
368
406
  ```
369
407
 
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
408
+ 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
373
412
  `config.d/mqtt.yaml` and that the password in `config.d/organizations/dev.yaml` matches the `dev`
374
413
  broker account.
375
414
 
415
+ Once nodes are reporting, each reading is logged as it arrives, so this output keeps scrolling.
416
+
376
417
  Now open a browser on your laptop or phone at:
377
418
 
378
419
  ```
379
420
  http://frugaliot.local:8080
380
421
  ```
381
422
 
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.)
423
+ (or `http://<the Pi's IP>:8080`).
424
+
425
+ > On a Mac, the browser will ask something like *"Allow Google Chrome Helper to find devices on
426
+ > local networks"* the first time. **Allow it** — without that permission the browser cannot look up
427
+ > `frugaliot.local`, nor reach the broker at that name, so the page and the live data both fail.
385
428
 
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.
429
+ You land on the Frugal IoT home page. Click **Dashboard**, and log in as username `dev` with the
430
+ login password you set with `frugal-iot-setpassword` not the broker password. (`superuser` and its
431
+ password work too.)
432
+
433
+ Once you are through to the dashboard and have selected your organization, its MQTT status should
434
+ show **connected**. That is the browser authenticating to the broker as your organization, over the
435
+ WebSocket listener on port 9012, from a different machine — the last untested piece of the chain.
436
+ If it does not connect, check that the `broker:` URL in `config.d/mqtt.yaml` is one this browser can
437
+ actually resolve: a phone that cannot look up `.local` names needs the Pi's IP address there instead.
391
438
 
392
439
  Until a sensor node reports in there will be no data to look at, but the dashboard should load.
393
440
 
@@ -395,32 +442,26 @@ Stop the server with `Ctrl-C` before continuing.
395
442
 
396
443
  ### A8. Run the server as a service
397
444
 
398
- So that it starts automatically at boot and restarts if it crashes:
445
+ 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
447
+ into `/home/pi/frugal-iot` — so it needs no editing:
399
448
 
400
449
  ```
401
450
  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
451
  sudo systemctl daemon-reload
419
- sudo systemctl enable frugaliot
420
- sudo systemctl start frugaliot
452
+ sudo systemctl enable --now frugaliot
421
453
  systemctl status frugaliot
422
454
  ```
423
455
 
456
+ `enable --now` both starts it and sets it to start at boot. `status` should report
457
+ `active (running)`.
458
+
459
+ > Only if you departed from this guide — a different username, or a directory other than
460
+ > `~/frugal-iot` — edit `User`, `WorkingDirectory` and `ExecStart` in
461
+ > `/etc/systemd/system/frugaliot.service` to match, then `sudo systemctl daemon-reload` and
462
+ > `sudo systemctl restart frugaliot`. `WorkingDirectory` is the important one: it is where the
463
+ > server looks for its configuration and database.
464
+
424
465
  To watch its log output, which is where the `mqtt dev connect` and incoming-reading messages now go:
425
466
 
426
467
  ```
@@ -432,21 +473,37 @@ still answers. Your server is now installed.
432
473
 
433
474
  ### A9. Point your sensor nodes at the Pi
434
475
 
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.
476
+ Your ESP8266/ESP32 nodes are told which broker to use in their sketch`main.cpp`, or the `.ino`
477
+ file if you build in the Arduino IDE. Look for a line like:
478
+
479
+ ```cpp
480
+ frugal_iot.configure_mqtt("frugaliot.naturalinnovation.org", "dev", "public");
481
+ ```
482
+
483
+ and point it at your Pi instead:
438
484
 
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).
485
+ ```cpp
486
+ frugal_iot.configure_mqtt("frugaliot.local", "dev", "<broker-password>");
487
+ ```
488
+
489
+ 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
491
+ every topic the node publishes to, and the password is the *broker* password from that step — not
492
+ the login password. Then rebuild and flash the node as usual.
442
493
 
443
- You can confirm nodes are reporting without the UI at all, using the subscriber from step A6:
494
+ > If the node does not connect, try the Pi's IP address in place of `frugaliot.local`. Resolving
495
+ > `.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
497
+ > for the Pi in your router.
498
+
499
+ You can confirm nodes are reporting without involving the UI, using the subscriber from step A6:
444
500
 
445
501
  ```
446
502
  mosquitto_sub -h localhost -u dev -P '<broker-password>' -t '#' -v
447
503
  ```
448
504
 
449
- Every reading from every node should scroll past.
505
+ Every reading from every node should scroll past. Seeing anything here also proves the broker's
506
+ port 1883 is reachable from off the Pi, which is what the nodes need.
450
507
 
451
508
  ### A10. HTTPS and over-the-air firmware updates
452
509
 
@@ -461,6 +518,25 @@ That section will cover: a DNS name for the Pi, port forwarding on the router, n
461
518
  proxy in front of port 8080, and certificates from Let's Encrypt via certbot. Both require the Pi
462
519
  to have internet access, which is the opposite of the offline case this guide is aimed at.
463
520
 
521
+ ### Known limitation: the clock on an offline Pi
522
+
523
+ A Raspberry Pi has no battery-backed clock. While it has internet access it sets its time from the
524
+ network and everything is correct — which is what you will see during this install. Fully offline it
525
+ cannot: Raspberry Pi OS saves the time periodically and restores that value at boot, so after a
526
+ power cut the Pi comes up believing it is whenever it last saved, and the gap never gets made up.
527
+
528
+ What that affects:
529
+
530
+ * **Logged data is stamped with the wrong time**, so graphs and history drift after each power cut.
531
+ * **Relaying MQTT and watching devices live are unaffected** — the dashboard shows current values
532
+ whatever the Pi believes the date to be.
533
+
534
+ If timestamps matter to you on an installation with no internet, the fix today is a hardware RTC
535
+ module on the Pi's GPIO header. A possible future fix within Frugal IoT itself: the dashboard knows
536
+ the time of the phone or laptop viewing it, so it could hand that to the server, which could adopt
537
+ it whenever its own clock looks implausible (a date far in the past). Not accurate to the second,
538
+ but close enough for sensor data.
539
+
464
540
  ---
465
541
 
466
542
  ## Part B — Raspberry Pi Zero W
@@ -502,40 +578,19 @@ they get settled.
502
578
 
503
579
  **Part A, to check while installing**
504
580
 
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?
581
+ 1. **`.local` from an Android phone** (step A2, A4) - confirmed working from a laptop (with the
582
+ 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.
526
584
 
527
585
  **Needs information I do not have**
528
586
 
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
587
+ 2. **Which Pi Zero** (Part B) — original Zero W, Zero 2 W, or both.
588
+ 3. **Bridging to the shared server** the local broker could optionally bridge to
534
589
  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
590
+ 4. **Organization naming** — this guide sets up exactly one organization named `dev`, because that
536
591
  is the node firmware's default. Is that the right default for a farm installation, or should the
537
592
  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
593
+ 5. **Upgrading an existing installation** — `npm update frugal-iot-server` followed by
539
594
  `npx frugal-iot-init` should be all it takes, since init adds missing configuration without
540
595
  touching what is there. Not yet tried on a server that has been running for a while.
541
596
 
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,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]
@@ -313,7 +313,6 @@ function addProjectToConfig(org, id, name) {
313
313
  }
314
314
  // Read the projects table for every configured organization and add them to config.organizations
315
315
  function loadProjectsIntoConfig(cb) {
316
- console.log("XXX loadProjectsIntoConfig");
317
316
  each(Object.keys(config.organizations), (org, cb) => {
318
317
  get_projects_list(org, (err, projects) => {
319
318
  if (err) {
@@ -659,13 +658,25 @@ function debugRoutes(req, res, next) {
659
658
  next();
660
659
  }
661
660
  // Main for server
661
+ // Everything is read relative to the working directory, so being in the wrong one is the most
662
+ // common way to fail to start - say so plainly rather than reporting ENOENT on ./config.yaml.
663
+ access('./config.yaml', constants.R_OK, (err) => {
664
+ if (err) {
665
+ console.error(`No config.yaml in ${process.cwd()}`);
666
+ console.error("Run this from the directory this server was installed into - the one holding");
667
+ console.error("config.d and frugal-iot.db. If it is a new directory, set it up with: npx frugal-iot-init");
668
+ process.exit(1);
669
+ }
670
+ });
662
671
  mqttLogger.readYamlConfig('.', (err, configobj) => {
663
672
  // Note side effect leaves copy of config in the mqttLogger
664
673
  if (err) {
665
674
  console.error(err);
666
675
  } else {
667
676
  /* global */ config = configobj;
668
- console.log("Config=", config);
677
+ // Summarize rather than dumping the whole config: it is mostly the sensor schema, and it holds
678
+ // each organization's mqtt_password, which should not be going to the console and the journal.
679
+ console.log("Broker", config.mqtt.broker, "- organizations:", Object.keys(config.organizations).join(", ") || "(none)");
669
680
  // Could genericize config defaults
670
681
  if (!config.morgan) {
671
682
  config.morgan = ':method :url :req[range] :status :res[content-length] :response-time ms :req[referer]'
package/package.json CHANGED
@@ -66,5 +66,5 @@
66
66
  "test:coverage": "vitest run --coverage"
67
67
  },
68
68
  "type": "module",
69
- "version": "0.3.3"
69
+ "version": "0.3.4"
70
70
  }