-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from kasperl/add-service
Add Qubitro service
- Loading branch information
Showing
9 changed files
with
257 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,60 @@ | ||
# qubitro | ||
# Qubitro connector for the ESP32 | ||
|
||
Connect your devices to Qubitro and visualize your data in the Qubitro Portal. | ||
Connect your devices to [Qubitro](https://www.qubitro.com/) and visualize your data in the | ||
[Qubitro Portal](https://portal.qubitro.com/). | ||
|
||
This [Toit](https://toitlang.org) package provides an easy and convenient way to | ||
connect to Qubitro via MQTT from devices running on the ESP32-family of chips. | ||
|
||
## Architecture | ||
|
||
The Qubitro connector runs as a separate micro-service isolated from the rest of the | ||
system through [Toit containers](https://github.com/toitlang/toit/discussions/869). | ||
|
||
## Installing the Qubitro connector | ||
|
||
To install the Qubitro connector service on your device, we recommend that you | ||
use [Jaguar](https://github.com/toitlang/jaguar). Jaguar makes it easy to experiment | ||
with the Qubitro services because it allows you to upload new services and | ||
applications via WiFi without having to restart your device. | ||
|
||
The Qubitro credentials easily be provided to the service at install time, so you | ||
don't have to write it into your source code: | ||
|
||
```sh | ||
jag container install qubitro src/service.toit \ | ||
-D qubitro.device.id=<PASTE_DEVICE_ID> \ | ||
-D qubitro.device.token=<PASTE_DEVICE_TOKEN> | ||
``` | ||
|
||
This install the Qubitro connector service in a separate container and it sticks | ||
around across device restarts: | ||
|
||
``` | ||
$ jag container list | ||
DEVICE IMAGE NAME | ||
lunar-bet 3fb76dd5-5842-57ff-b19c-857669906b04 jaguar | ||
lunar-bet d04371a2-bb38-54cb-9124-5e48d06ff3d1 qubitro | ||
``` | ||
|
||
## Publishing data | ||
|
||
Once the service is installed, you do not need to provide credentials to publish | ||
data from individual applications, although you still can by providing arguments | ||
to `qubitro.connect`. The code for publishing data is reasonably straight forward: | ||
|
||
``` | ||
import qubitro | ||
main: | ||
client ::= qubitro.connect | ||
10.repeat: | ||
client.publish { "MyData": random 1000 } | ||
sleep (Duration --s=2) | ||
client.close | ||
``` | ||
|
||
To run code like the above, you can use `jag run`: | ||
|
||
```sh | ||
jag run examples/publish.toit | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
sdk: ^2.0.0-alpha.35 | ||
prefixes: | ||
certificate_roots: toit-cert-roots | ||
mqtt: mqtt | ||
packages: | ||
mqtt: | ||
url: github.com/toitware/mqtt | ||
version: 2.0.2 | ||
hash: b5440a523baa8d696ad3899c34241e30d4ed1a6a | ||
version: 2.1.1 | ||
hash: 49eab960bb1c27245bfc52e72f6f1ec840492abb | ||
toit-cert-roots: | ||
url: github.com/toitware/toit-cert-roots | ||
version: 1.3.2 | ||
hash: 288547039d8a3797330064e91d8c79ad16313545 | ||
version: 1.4.0 | ||
hash: bf41ee60ebba65ba01bc9ba1aa6d697e4cc4a8c7 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright (C) 2022 Kasper Lund. | ||
// Use of this source code is governed by an MIT-style license that can be | ||
// found in the LICENSE file. | ||
import system.services | ||
|
||
interface QubitroService: | ||
static SELECTOR ::= services.ServiceSelector | ||
--uuid="4590d299-5c62-46f7-a3f3-3ccac3d67994" | ||
--major=1 | ||
--minor=0 | ||
|
||
connect config/Map -> int | ||
static CONNECT_INDEX ::= 0 | ||
|
||
publish handle/int data/Map -> none | ||
static PUBLISH_INDEX ::= 1 | ||
|
||
class QubitroServiceClient extends services.ServiceClient implements QubitroService: | ||
static SELECTOR ::= QubitroService.SELECTOR | ||
constructor selector/services.ServiceSelector=SELECTOR: | ||
assert: selector.matches SELECTOR | ||
super selector | ||
|
||
connect config/Map -> int: | ||
return invoke_ QubitroService.CONNECT_INDEX config | ||
|
||
publish handle/int data/Map -> none: | ||
invoke_ QubitroService.PUBLISH_INDEX [handle, data] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
// Copyright (C) 2022 Kasper Lund. | ||
// Use of this source code is governed by an MIT-style license that can be | ||
// found in the LICENSE file. | ||
import log | ||
import monitor | ||
import net | ||
|
||
import encoding.json | ||
import encoding.tison | ||
|
||
import certificate_roots | ||
import mqtt | ||
import mqtt.packets as mqtt | ||
|
||
import .internal.api show QubitroService | ||
|
||
import system.assets | ||
import system.services show ServiceHandler ServiceProvider ServiceResource | ||
import system.base.network show NetworkModule NetworkState NetworkResource | ||
|
||
HOST ::= "broker.qubitro.com" | ||
PORT ::= 8883 | ||
|
||
CONFIG_DEVICE_ID ::= "qubitro.device.id" | ||
CONFIG_DEVICE_TOKEN ::= "qubitro.device.token" | ||
|
||
main: | ||
logger ::= log.Logger log.DEBUG_LEVEL log.DefaultTarget --name="qubitro" | ||
logger.info "service starting" | ||
defines := assets.decode.get "jag.defines" | ||
--if_present=: tison.decode it | ||
--if_absent=: {:} | ||
service := QubitroServiceProvider logger defines | ||
service.install | ||
logger.info "service running" | ||
|
||
class QubitroServiceProvider extends ServiceProvider implements ServiceHandler: | ||
logger_/log.Logger | ||
defines_/Map | ||
state_ ::= NetworkState | ||
|
||
constructor .logger_ .defines_: | ||
super "qubitro" --major=1 --minor=0 | ||
provides QubitroService.SELECTOR --handler=this | ||
|
||
handle pid/int client/int index/int arguments/any -> any: | ||
if index == QubitroService.CONNECT_INDEX: | ||
return connect arguments client | ||
if index == QubitroService.PUBLISH_INDEX: | ||
resource := (resource client arguments[0]) as QubitroClient | ||
return resource.module.publish arguments[1] | ||
unreachable | ||
|
||
connect config/Map client/int -> ServiceResource: | ||
device_id ::= config.get CONFIG_DEVICE_ID or defines_.get CONFIG_DEVICE_ID | ||
device_token := config.get CONFIG_DEVICE_TOKEN or defines_.get CONFIG_DEVICE_TOKEN | ||
if not device_id: throw "ILLEGAL_ARGUMENT: No device id provided" | ||
if not device_token: throw "ILLEGAL_ARGUMENT: No device token provided" | ||
module := state_.up: QubitroMqttModule logger_ device_id device_token | ||
if module.device_id != device_id: | ||
unreachable | ||
if module.device_token != device_token: | ||
unreachable | ||
return QubitroClient this client state_ | ||
|
||
class QubitroMqttModule implements NetworkModule: | ||
logger_/log.Logger | ||
device_id/string | ||
device_token/string | ||
client_/mqtt.FullClient? := null | ||
|
||
task_/Task? := null | ||
done_/monitor.Latch? := null | ||
|
||
constructor logger/log.Logger .device_id .device_token: | ||
logger_ = logger.with_name "mqtt" | ||
|
||
connect -> none: | ||
connected := monitor.Latch | ||
done := monitor.Latch | ||
done_ = done | ||
task_ = task:: | ||
try: | ||
connect_ connected | ||
finally: | ||
client_ = task_ = done_ = null | ||
critical_do: done.set true | ||
// Wait until the MQTT task has connected and is running. | ||
client_ = connected.get | ||
client_.when_running: null | ||
|
||
disconnect -> none: | ||
if not task_: return | ||
// Cancel the MQTT task and wait until it has disconnected. | ||
task_.cancel | ||
done_.get | ||
|
||
connect_ connected/monitor.Latch -> none: | ||
network := net.open | ||
transport/mqtt.TcpTransport? := null | ||
client/mqtt.FullClient? := null | ||
try: | ||
transport = mqtt.TcpTransport.tls network | ||
--host=HOST | ||
--port=PORT | ||
--root_certificates=[ certificate_roots.BALTIMORE_CYBERTRUST_ROOT ] | ||
client = mqtt.FullClient --transport=transport | ||
options := mqtt.SessionOptions | ||
--client_id=device_id | ||
--username=device_id | ||
--password=device_token | ||
client.connect --options=options | ||
logger_.info "connected" --tags={"host": HOST, "port": PORT, "device": device_id} | ||
connected.set client | ||
client.handle: | packet/mqtt.Packet | | ||
logger_.warn "packet received (ignored)" --tags={"type": packet.type} | ||
finally: | is_exception exception | | ||
if client: client.close | ||
else if transport: transport.close | ||
network.close | ||
// We need to call monitor operations to send exceptions | ||
// to the task that initiated the connection attempt, so | ||
// we have to do this in a critical section if we're being | ||
// canceled as part of a disconnect. | ||
critical_do: | ||
if connected.has_value: | ||
logger_.info "disconnected" --tags={"host": HOST, "port": PORT, "device": device_id} | ||
if is_exception: | ||
connected.set --exception exception | ||
return | ||
|
||
publish data/Map -> none: | ||
payload ::= json.encode data | ||
client_.publish device_id payload | ||
logger_.info "packet published" --tags={"device": device_id, "data": data} | ||
|
||
class QubitroClient extends NetworkResource: | ||
module/QubitroMqttModule | ||
constructor provider/QubitroServiceProvider client/int state/NetworkState: | ||
module = state.module as QubitroMqttModule | ||
super provider client state |