Skip to content

Commit

Permalink
Merge pull request #5 from kasperl/add-service
Browse files Browse the repository at this point in the history
Add Qubitro service
  • Loading branch information
Kasper Lund authored Feb 3, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 79274bd + 4729b60 commit 51fbb3a
Showing 9 changed files with 257 additions and 58 deletions.
61 changes: 59 additions & 2 deletions README.md
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
```
4 changes: 2 additions & 2 deletions examples/package.lock
Original file line number Diff line number Diff line change
@@ -8,8 +8,8 @@ packages:
mqtt: mqtt
mqtt:
url: github.com/toitware/mqtt
version: 2.0.2
hash: b5440a523baa8d696ad3899c34241e30d4ed1a6a
version: 2.0.4
hash: 81cd43e7383eccdbf07360cda3d03e9b862fa280
toit-cert-roots:
url: github.com/toitware/toit-cert-roots
version: 1.3.2
3 changes: 0 additions & 3 deletions examples/publish.toit
Original file line number Diff line number Diff line change
@@ -6,7 +6,4 @@ import qubitro

main:
client ::= qubitro.connect
--id="<PASTE_DEVICE_ID>"
--token="<PASTE_DEVICE_TOKEN>"
client.publish { "MyData": random 1000 }
print "Published data to Qubitro!"
9 changes: 5 additions & 4 deletions package.lock
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
4 changes: 2 additions & 2 deletions package.yaml
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ name: qubitro
dependencies:
certificate_roots:
url: github.com/toitware/toit-cert-roots
version: ^1.3.2
version: ^1.4.0
mqtt:
url: github.com/toitware/mqtt
version: ^2.0.2
version: ^2.1.1
33 changes: 8 additions & 25 deletions src/client.toit
Original file line number Diff line number Diff line change
@@ -2,35 +2,18 @@
// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file.
import mqtt as mqttx // Avoid issues with mqtt getter.
import encoding.json
import system.services show ServiceResourceProxy
import .internal.api

class Client:
device_id_ /string
mqtt_ /mqttx.Client? := null
_client_/QubitroServiceClient? ::= (QubitroServiceClient).open
--if_absent=: null

constructor .device_id_ .mqtt_:
add_finalizer this:: close
class Client extends ServiceResourceProxy:
constructor handle/int:
super _client_ handle

/**
Publishes the $data key-value mapping to Qubitro.
*/
publish data/Map -> none:
encoded := json.encode data
mqtt_.publish device_id_ encoded

/**
Closes the connection to Qubitro.
*/
close:
if mqtt_:
mqtt_.close
remove_finalizer this
mqtt_ = null

/**
Returns the underlying MQTT client, which allows publishing and subscribing
to non-default topics.
*/
mqtt -> mqttx.Client:
return mqtt_
_client_.publish handle_ data
30 changes: 10 additions & 20 deletions src/connect.toit
Original file line number Diff line number Diff line change
@@ -8,26 +8,16 @@ import tls
import certificate_roots

import .client

QUBITRO_HOST ::= "broker.qubitro.com"
QUBITRO_PORT ::= 8883
import .service show CONFIG_DEVICE_ID CONFIG_DEVICE_TOKEN

/**
Connects to the Qubitro MQTT broker. If no $network is provided, the default
system network is used.
Connects to Qubitro.
*/
connect --id/string --token/string --network/net.Interface?=null -> Client:
if not network: network = net.open

transport := mqtt.TcpTransport.tls network --host=QUBITRO_HOST --port=QUBITRO_PORT
--root_certificates=[certificate_roots.BALTIMORE_CYBERTRUST_ROOT]

options := mqtt.SessionOptions
--client_id=id
--username=id
--password=token

mqtt_client := mqtt.Client --transport=transport
mqtt_client.start --options=options

return Client id mqtt_client
connect --id/string?=null --token/string?=null -> Client:
client := _client_
if not client: throw "Cannot find Qubitro service"
config := {:}
if id: config[CONFIG_DEVICE_ID] = id
if token: config[CONFIG_DEVICE_TOKEN] = token
handle ::= client.connect config
return Client handle
29 changes: 29 additions & 0 deletions src/internal/api.toit
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]
142 changes: 142 additions & 0 deletions src/service.toit
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

0 comments on commit 51fbb3a

Please sign in to comment.