Skip to content

Commit 173d9c3

Browse files
authored
Merge pull request #1 from balena-io-examples/add_azure_iot
Add support for Azure IoT
2 parents 8402e2e + e1b3ba8 commit 173d9c3

File tree

6 files changed

+103
-22
lines changed

6 files changed

+103
-22
lines changed

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
![Overview](doc/overview.png)
66

7-
Cloud Relay accepts application data via MQTT and relays it to a cloud provider's IoT Core facility. You only need to provide the data, and Cloud Relay takes care of messaging with the cloud provider. Cloud Relay works with AWS IoT Core and Google Cloud (GCP) IoT Core.
7+
Cloud Relay accepts application data via MQTT and relays it to a cloud provider's IoT Core facility. You only need to provide the data, and Cloud Relay takes care of messaging with the cloud provider. Cloud Relay works with AWS IoT, Azure IoT, and Google Cloud (GCP) IoT.
88

99
## Getting Started
1010

11-
You must install the Cloud Relay container on your device as well as set up the cloud provider's IoT service. Balena also provides cloud functions for AWS and Google Cloud that expose an HTTP endpoint to initially provision each device. See the _Cloud Provisioning_ section below.
11+
You must install the Cloud Relay container on your device as well as set up the cloud provider's IoT service. Balena also provides cloud functions for AWS, Azure and GCP that expose an HTTP endpoint to initially provision each device. See the _Cloud Provisioning_ section below.
1212

1313
### Device
1414
We will use the docker-compose [example script](docker-compose.yml), which provides WiFi metrics data. First create a multi-container fleet in balenaCloud and provision a device with balenaOS. See the [online docs](https://www.balena.io/docs/learn/getting-started/raspberrypi3/nodejs/) for details. Next define the fleet variables from the cloud provider's setup, as described in the *Configuration* section below. Finally push the docker-compose script to the balena builders, substituting your fleet's name for `<myFleet>` in the commands below.
@@ -38,6 +38,7 @@ We have developed projects that automate this provisioning, including use of the
3838
| Provider / Cloud Function | GitHub project |
3939
|----------|-------------------|
4040
| AWS Lambda | [aws-iot-provision](https://github.com/balena-io-examples/aws-iot-provision) |
41+
| Azure Functions | [azure-iot-provision](https://github.com/balena-io-examples/azure-iot-provision) |
4142
| GCP Cloud Functions | [gcp-iot-provision](https://github.com/balena-io-examples/gcp-iot-provision) |
4243

4344
## Configuration
@@ -46,10 +47,10 @@ Environment variables, probably common to all devices so may be defined as balen
4647

4748
| Name | Value | Notes |
4849
|-------|-------|-------|
49-
| CLOUD_PROVIDER | default `AWS`<br><br>or `GCP` | |
50-
| PROVISION_URL | AWS Lambda like<br>`https://xxxxxxxx.execute-api.<region>.amazonaws.com/default/provision`<br><br>GCP Cloud Functions like<br>`https://<region>-<projectID>.cloudfunctions.net/provision` | URL to trigger the provisioning cloud function. See the README for the cloud provisioning projects above for specifics.|
50+
| CLOUD_PROVIDER | default `AWS`<br><br>`AZURE` or `GCP` | |
51+
| PROVISION_URL | AWS Lambda like<br>`https://xxxxxxxx.execute-api.<region>.amazonaws.com/default/provision`<br><br>Azure Functions like<br>`https://<function-app>.azurewebsites.net/api/provision`<br><br>GCP Cloud Functions like<br>`https://<region>-<projectID>.cloudfunctions.net/provision` | URL to trigger the provisioning cloud function. See the README for the cloud provisioning projects above for specifics.|
5152
| PRODUCER_TOPIC| default `sensors` | Message topic from data producer |
52-
| CLOUD_CONSUMER_TOPIC| AWS default `sensors`<br><br>GCP default `events` | Message topic expected by cloud consumer. For GCP, `events` is the default *telemetry* topic. As the docs [describe](https://cloud.google.com/iot/docs/how-tos/mqtt-bridge#publishing_telemetry_events_to_additional_cloud_pubsub_topics), you also may publish to a subfolder like `events/alerts`. |
53+
| CLOUD_CONSUMER_TOPIC| AWS, Azure default `sensors`<br><br>GCP default `events` | Message topic expected by cloud consumer. For Azure `sensors` is the value for the `topic` entry in the `properties` map.<br><br>For GCP, `events` is the default *telemetry* topic. As the docs [describe](https://cloud.google.com/iot/docs/how-tos/mqtt-bridge#publishing_telemetry_events_to_additional_cloud_pubsub_topics), you also may publish to a subfolder like `events/alerts`. |
5354

5455
**AWS** specific variables
5556

@@ -59,6 +60,14 @@ Environment variables, probably common to all devices so may be defined as balen
5960

6061
The provisioning tool generates AWS_CERT and AWS_PRIVATE_KEY.
6162

63+
**Azure** specific variables
64+
65+
| Name | Value | Notes |
66+
|-------|-------|-------|
67+
| AZURE_HUB_HOST | like `<iot-hub-name>.azure-devices.net` | Host name to receive data. See *Overview* for the IoT Hub in the Azure portal. |
68+
69+
The provisioning tool generates AZURE_CERT and AZURE_PRIVATE_KEY.
70+
6271
**GCP** specific variables
6372

6473
No GCP specific variables for configuration. However, the provisioning tool generates GCP_CLIENT_PATH, GCP_DATA_TOPIC_ROOT, GCP_PRIVATE_KEY, and GCP_PROJECT_ID.

doc/overview.png

4.38 KB
Loading

doc/provision-send.png

698 Bytes
Loading

index.js

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ async function provision(uuid) {
3838
let bodyJson = null
3939
switch (process.env.CLOUD_PROVIDER) {
4040
case 'AWS':
41+
case 'AZURE':
4142
bodyJson = `{ "uuid": "${uuid}", "method": "POST" }`
4243
break
4344
case 'GCP':
@@ -63,24 +64,32 @@ async function provision(uuid) {
6364
// vars yet and thus tried to provision again. So force Supervisor to update
6465
// and refresh environment variables. If successful, this service will
6566
// not attempt to provision on the next invocation.
66-
if (process.env.CLOUD_PROVIDER == 'AWS') {
67-
if (text == "thing already exists") {
68-
console.warn("AWS thing already exists; updating environment vars")
69-
updateEnvironmentVars()
70-
}
71-
} else if (process.env.CLOUD_PROVIDER == 'GCP') {
72-
let respJson = {}
73-
try {
74-
respJson = JSON.parse(text)
75-
} catch(e) {
76-
// just use empty respJson
77-
}
78-
const alreadyExistsCode = 6
67+
let alreadyExists = false
68+
switch (process.env.CLOUD_PROVIDER) {
69+
case 'AWS':
70+
alreadyExists = (text == "thing already exists")
71+
break
72+
case 'AZURE':
73+
alreadyExists = text.startsWith("DeviceAlreadyExistsError")
74+
break
75+
case 'GCP':
76+
let respJson = {}
77+
try {
78+
respJson = JSON.parse(text)
79+
} catch(e) {
80+
// just use empty respJson
81+
}
82+
const alreadyExistsCode = 6
7983

80-
if (respJson.code && respJson.code == alreadyExistsCode) {
81-
console.warn("GCP device already exists; updating environment vars")
82-
updateEnvironmentVars()
83-
}
84+
alreadyExists = (respJson.code && respJson.code == alreadyExistsCode)
85+
break
86+
default:
87+
// not possible at this point
88+
break
89+
}
90+
if (alreadyExists) {
91+
console.warn(`Device already exists on ${process.env.CLOUD_PROVIDER}; updating environment vars`)
92+
updateEnvironmentVars()
8493
}
8594
}
8695
return response.ok

lib/messenger.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import awsIot from 'aws-iot-device-sdk'
22
import jwt from 'jsonwebtoken'
33
import mqtt from 'async-mqtt'
4+
import azureIot from 'azure-iot-device'
5+
import { clientFromConnectionString } from 'azure-iot-device-mqtt'
46

57
/**
68
* Abstract superclass for cloud provider's IoT data messaging.
@@ -112,6 +114,63 @@ class AwsMessenger extends Messenger {
112114
}
113115
}
114116

117+
/** Messenger for MS Azure IoT. */
118+
class AzureMessenger extends Messenger {
119+
connectSync() {
120+
console.log(`Connecting to host ${process.env.AZURE_HUB_HOST}`)
121+
//console.debug("connstr:", `HostName=${process.env.AZURE_HUB_HOST};DeviceId=${process.env.RESIN_DEVICE_UUID};x509=true`)
122+
this.mqtt = clientFromConnectionString(
123+
`HostName=${process.env.AZURE_HUB_HOST};DeviceId=${process.env.RESIN_DEVICE_UUID};x509=true`)
124+
//console.debug("cert:", Buffer.from(process.env.AZURE_CERT, 'base64').toString())
125+
//console.debug("private key:", Buffer.from(process.env.AZURE_PRIVATE_KEY, 'base64').toString())
126+
const options = {
127+
cert: Buffer.from(process.env.AZURE_CERT, 'base64').toString(),
128+
key: Buffer.from(process.env.AZURE_PRIVATE_KEY, 'base64').toString()
129+
}
130+
this.mqtt.setOptions(options)
131+
132+
this.mqtt.open(function (err) {
133+
if (err) {
134+
console.warn("Cannot connect to Azure IoT:", err.toString())
135+
} else {
136+
console.log("Connected to Azure IoT messaging")
137+
}
138+
})
139+
}
140+
141+
isRegistrationComplete() {
142+
return process.env.AZURE_PRIVATE_KEY
143+
&& process.env.AZURE_CERT
144+
}
145+
146+
isSyncConnect() {
147+
return true
148+
}
149+
150+
isUnregistered() {
151+
return !process.env.AZURE_PRIVATE_KEY
152+
&& !process.env.AZURE_CERT
153+
}
154+
155+
publish(topic, message) {
156+
//console.debug(`Messenger pub: ${message.toString()}`)
157+
let msg = new azureIot.Message(message)
158+
msg.contentEncoding = 'utf-8'
159+
msg.contentType = 'application/json'
160+
msg.properties.add('topic', topic)
161+
162+
this.mqtt.sendEvent(msg, function (err) {
163+
if (err) {
164+
console.warn("Error sending message:", err.toString())
165+
}
166+
})
167+
}
168+
169+
toString() {
170+
return "Azure cloud messenger"
171+
}
172+
}
173+
115174
/**
116175
* Messenger for GCP IoT Core.
117176
*
@@ -257,6 +316,8 @@ Messenger.create = function(cloudProvider) {
257316
switch (cloudProvider) {
258317
case "AWS":
259318
return new AwsMessenger()
319+
case "AZURE":
320+
return new AzureMessenger()
260321
case "GCP":
261322
return new GcpMessenger()
262323
default:

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"dependencies": {
88
"aws-iot-device-sdk": "^2.2.11",
99
"async-mqtt": "^2.6.1",
10+
"azure-iot-device": "^1.17.8",
11+
"azure-iot-device-mqtt": "^1.15.8",
1012
"jsonwebtoken": "^8.5.1",
1113
"node-fetch": "^3.1.0"
1214
},

0 commit comments

Comments
 (0)