initial commit
This commit is contained in:
Родитель
4aa3dcbecd
Коммит
3b6144cde0
46
README.md
46
README.md
|
@ -1,2 +1,48 @@
|
|||
# iot-central-web-mqtt-device
|
||||
Create a simple IoT device in a web browser that communicates with Azure IoT Central
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
This project is based in large part on the work done by Github user [ridomin](https://github.com/ridomin) and the repository [iothub-webclient](https://github.com/ridomin/iothub-webclient). In this project I have focused the sample on connecting to an IoT Central application and it exclusively uses the Device Provisioning Service (DPS) for the provisioning and connection of the device. IoT Central does not support connection strings and requires the device to use DPS to obtain it the location of it's IoT hub, this allows a device to be moved between hubs and in the near futrure for IoT Central to support IoT Hub redundancy. The device running in the browser is also setup to run as an autonomous device very much like if the device was running in the real world. The devices two way communication between IoT Central and itsel;f can be observed in the console output on the web page once sending telemetry has been started.
|
||||
|
||||
The device supports the following functionality:
|
||||
|
||||
* Connection to IoT Central using DPS. The device does not need to be registered in the application prior to connecting
|
||||
* The device can be authenticated via either the device SAS token or the Group SAS token for the application (in this case the device SAS token is automatically generated)
|
||||
* The device is automatically associated with the model identity provided by the user in the form
|
||||
* Sending the telemetry values for temperature and humidity (randomly generated values in a range) ever ~5 seconds
|
||||
* Sending the reported property of fan speed to IoT Central every ~10 seconds
|
||||
* Accepting a desired (writable) property setTemp to set the temperature of a thermostat on the device
|
||||
* Accepting the direct method call sendTextMessage from IoT Central with a text message and responding with a boolean return value if the message is read
|
||||
* Accepting a cloud to device message startVacuumCleaner that takes a time value as a parameter (this is additional functionality over iothub-webclient version)
|
||||
|
||||
All communication and payloads are displayed on the web browser console output with the last 200 transmissions held in history.
|
||||
|
||||
|
||||
## Hosting locally
|
||||
|
||||
To host this locally clone this repository to your computer and run the following:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
The following should be seen after running the last command
|
||||
|
||||
```
|
||||
> webmqtt@1.0.0 start D:\github\iot-central-web-mqtt-device
|
||||
> node server.js
|
||||
|
||||
Listening at http//localhost:8080
|
||||
```
|
||||
|
||||
Open your browser of choice and go to the URL http//localhost:8080. You should see the following in you browser:
|
||||
|
||||
![Initial browser screen](https://github.com/iot-for-all/iot-central-web-mqtt-device/raw/master/assets/initialscreen.png "Initial browser screen")
|
||||
|
||||
|
||||
## Hosting on Azure
|
||||
|
||||
## Running the sample
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 24 KiB |
|
@ -0,0 +1,45 @@
|
|||
const REGISTRATION_TOPIC = '$dps/registrations/res/#'
|
||||
const REGISTER_TOPIC = '$dps/registrations/PUT/iotdps-register/?$rid='
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {String} msg
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export const createHmac = async (key, msg) => {
|
||||
const keyBytes = Uint8Array.from(window.atob(key), c => c.charCodeAt(0))
|
||||
const msgBytes = Uint8Array.from(msg, c => c.charCodeAt(0))
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' },
|
||||
true, ['sign']
|
||||
)
|
||||
const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, msgBytes)
|
||||
return window.btoa(String.fromCharCode(...new Uint8Array(signature)))
|
||||
}
|
||||
|
||||
export class AzDpsClient {
|
||||
constructor (scopeId, deviceId, deviceKey, modelId) {
|
||||
this.host = 'global.azure-devices-provisioning.net'
|
||||
this.scopeId = scopeId
|
||||
this.deviceId = deviceId
|
||||
this.deviceKey = deviceKey
|
||||
this.modelId = modelId
|
||||
}
|
||||
|
||||
async registerDevice () {
|
||||
const endpoint = 'https://dps-proxy.azurewebsites.net/register'
|
||||
const url = `${endpoint}?scopeId=${this.scopeId}&deviceId=${this.deviceId}&deviceKey=${encodeURIComponent(this.deviceKey)}&modelId=${this.modelId}`
|
||||
console.log(url)
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Encoding': 'utf-8'
|
||||
}
|
||||
})
|
||||
const resp = await response.json()
|
||||
console.log(resp)
|
||||
return resp
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
const WEB_SOCKET = '/$iothub/websocket?iothub-no-client-cert=true'
|
||||
const DEVICE_TWIN_RES_TOPIC = '$iothub/twin/res/#'
|
||||
const DEVICE_TWIN_GET_TOPIC = '$iothub/twin/GET/?$rid='
|
||||
const DEVICE_TWIN_PUBLISH_TOPIC = '$iothub/twin/PATCH/properties/reported/?$rid='
|
||||
const DIRECT_METHOD_TOPIC = '$iothub/methods/POST/#'
|
||||
const DEVICE_TWIN_DESIRED_PROP_RES_TOPIC = '$iothub/twin/PATCH/properties/desired/#'
|
||||
const DIRECT_METHOD_RESPONSE_TOPIC = '$iothub/methods/res/{status}/?$rid='
|
||||
const DEVICE_C2D_COMMAND_TOPIC = 'devices/{deviceid}/messages/devicebound/#'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {String} msg
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const createHmac = async (key, msg) => {
|
||||
const keyBytes = Uint8Array.from(window.atob(key), c => c.charCodeAt(0))
|
||||
const msgBytes = Uint8Array.from(msg, c => c.charCodeAt(0))
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' },
|
||||
true, ['sign']
|
||||
)
|
||||
const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, msgBytes)
|
||||
return window.btoa(String.fromCharCode(...new Uint8Array(signature)))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} resourceUri
|
||||
* @param {string} signingKey
|
||||
* @param {string | null} policyName
|
||||
* @param {number} expiresInMins
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function generateSasToken (resourceUri, signingKey, policyName, expiresInMins) {
|
||||
resourceUri = encodeURIComponent(resourceUri)
|
||||
let expires = (Date.now() / 1000) + expiresInMins * 60
|
||||
expires = Math.ceil(expires)
|
||||
const toSign = resourceUri + '\n' + expires
|
||||
const hmac = await createHmac(signingKey, toSign)
|
||||
const base64UriEncoded = encodeURIComponent(hmac)
|
||||
let token = 'SharedAccessSignature sr=' + resourceUri + '&sig=' + base64UriEncoded + '&se=' + expires
|
||||
if (policyName) token += '&skn=' + policyName
|
||||
return token
|
||||
}
|
||||
|
||||
export class AzIoTHubClient {
|
||||
/**
|
||||
* @param {string} host
|
||||
* @param {string} deviceId
|
||||
* @param {string} key
|
||||
* @param {string} [modelId]
|
||||
*/
|
||||
constructor (host, deviceId, key, modelId) {
|
||||
this.connected = false
|
||||
this.host = host
|
||||
this.deviceId = deviceId
|
||||
this.key = key
|
||||
this.modelId = modelId
|
||||
this.rid = 0
|
||||
this.client = new Paho.MQTT.Client(this.host, Number(443), WEB_SOCKET, this.deviceId)
|
||||
|
||||
/**
|
||||
* @description Callback when a direct method invocation is received
|
||||
* @param {string} method
|
||||
* @param {string} payload
|
||||
* @param {number} rid
|
||||
*/
|
||||
this.directMethodCallback = (method, payload, rid) => { }
|
||||
|
||||
/**
|
||||
* @description Callback when a C2D command invocation is received
|
||||
* @param {string} methodName
|
||||
* @param {string} payload
|
||||
*/
|
||||
this.c2dCallback = (methodName, payload) => { }
|
||||
|
||||
/**
|
||||
* @description Callback for desired properties upadtes
|
||||
* @param {string} desired
|
||||
*/
|
||||
this.desiredPropCallback = (desired) => { }
|
||||
/**
|
||||
* @param {any} err
|
||||
*/
|
||||
this.disconnectCallback = (err) => { console.log(err) }
|
||||
/**
|
||||
* @param {any} twin
|
||||
*/
|
||||
this._onReadTwinCompleted = (twin) => { }
|
||||
this._onUpdateTwinCompleted = () => { }
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Connects to Azure IoT Hub using MQTT over websockets
|
||||
*/
|
||||
async connect () {
|
||||
let userName = `${this.host}/${this.deviceId}/?api-version=2020-05-31-preview`
|
||||
if (this.modelId) userName += `&model-id=${this.modelId}`
|
||||
const password = await generateSasToken(`${this.host}/devices/${this.deviceId}`, this.key, null, 60)
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.onConnectionLost = (err) => {
|
||||
console.log(err)
|
||||
this.connected = false
|
||||
this.disconnectCallback(err)
|
||||
reject(err)
|
||||
}
|
||||
|
||||
const willMsg = new Paho.MQTT.Message('')
|
||||
willMsg.destinationName = 'willMessage'
|
||||
|
||||
this.client.onMessageArrived = (/** @type {Paho.MQTT.Message} */ m) => {
|
||||
const destinationName = m.destinationName
|
||||
const payloadString = m.payloadString
|
||||
// console.log('On Msg Arrived to ' + destinationName)
|
||||
// console.log(payloadString)
|
||||
if (destinationName === '$iothub/twin/res/200/?$rid=' + this.rid) {
|
||||
this._onReadTwinCompleted(payloadString)
|
||||
}
|
||||
if (destinationName.startsWith('$iothub/twin/res/204/?$rid=' + this.rid)) {
|
||||
this._onUpdateTwinCompleted()
|
||||
}
|
||||
if (destinationName.indexOf('methods/POST') > 1) {
|
||||
const destParts = destinationName.split('/') // $iothub/methods/POST/myCommand/?$rid=2
|
||||
const methodName = destParts[3]
|
||||
const ridPart = destParts[4]
|
||||
const rid = parseInt(ridPart.split('=')[1])
|
||||
this.directMethodCallback(methodName, payloadString, rid)
|
||||
}
|
||||
if (destinationName.indexOf('twin/PATCH/properties/desired') > 1) {
|
||||
this.desiredPropCallback(payloadString)
|
||||
}
|
||||
if (destinationName.startsWith(DEVICE_C2D_COMMAND_TOPIC.slice(0, -1).replace('{deviceid}', this.deviceId))) {
|
||||
const url = decodeURIComponent(destinationName)
|
||||
const methodName = url.substring(url.indexOf("&method-name=")+13)
|
||||
this.c2dCallback(methodName, payloadString)
|
||||
}
|
||||
}
|
||||
this.client.connect({
|
||||
useSSL: true,
|
||||
userName: userName,
|
||||
timeout: 120,
|
||||
cleanSession: true,
|
||||
invocationContext: {},
|
||||
keepAliveInterval: 120,
|
||||
willMessage: willMsg,
|
||||
password: password,
|
||||
onSuccess: () => {
|
||||
this.connected = true
|
||||
console.log('Connected !!')
|
||||
this.client.subscribe(DEVICE_TWIN_RES_TOPIC, {
|
||||
qos: 0,
|
||||
invocationContext: {},
|
||||
onSuccess: () => { },
|
||||
onFailure: (err) => { throw err },
|
||||
timeout: 120
|
||||
})
|
||||
this.client.subscribe(DIRECT_METHOD_TOPIC, {
|
||||
qos: 0,
|
||||
invocationContext: {},
|
||||
onSuccess: () => { },
|
||||
onFailure: (err) => { throw err },
|
||||
timeout: 120
|
||||
})
|
||||
this.client.subscribe(DEVICE_TWIN_DESIRED_PROP_RES_TOPIC, {
|
||||
qos: 0,
|
||||
invocationContext: {},
|
||||
onSuccess: () => { },
|
||||
onFailure: (err) => { throw err },
|
||||
timeout: 120
|
||||
})
|
||||
this.client.subscribe(DEVICE_C2D_COMMAND_TOPIC.replace('{deviceid}', this.deviceId), {
|
||||
qos: 0,
|
||||
invocationContext: {},
|
||||
onSuccess: () => { },
|
||||
onFailure: (err) => { throw err },
|
||||
timeout: 120
|
||||
})
|
||||
resolve(this.deviceId)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<DeviceTwin>}
|
||||
*/
|
||||
getTwin () {
|
||||
this.rid = Date.now()
|
||||
// console.log(this.rid)
|
||||
const readTwinMessage = new Paho.MQTT.Message('')
|
||||
readTwinMessage.destinationName = DEVICE_TWIN_GET_TOPIC + this.rid
|
||||
this.client.send(readTwinMessage)
|
||||
return new Promise((resolve, reject) => {
|
||||
/**
|
||||
* @param {string} twin
|
||||
*/
|
||||
this._onReadTwinCompleted = (twin) => {
|
||||
resolve(JSON.parse(twin))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} reportedProperties
|
||||
*/
|
||||
updateTwin (reportedProperties) {
|
||||
this.rid = Date.now()
|
||||
// console.log(this.rid)
|
||||
const reportedTwinMessage = new Paho.MQTT.Message(reportedProperties)
|
||||
reportedTwinMessage.destinationName = DEVICE_TWIN_PUBLISH_TOPIC + this.rid
|
||||
this.client.send(reportedTwinMessage)
|
||||
return new Promise((resolve, reject) => {
|
||||
this._onUpdateTwinCompleted = () => {
|
||||
resolve(204)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} payload
|
||||
*/
|
||||
sendTelemetry (payload) {
|
||||
const telemetryMessage = new Paho.MQTT.Message(payload)
|
||||
telemetryMessage.destinationName = `devices/${this.deviceId}/messages/events/`
|
||||
this.client.send(telemetryMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ (methodName: string, payload:string, rid:number): void}} directMethodCallback
|
||||
*/
|
||||
setDirectMethodCallback (directMethodCallback) {
|
||||
this.directMethodCallback = directMethodCallback
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ (methodName: string, payload:string, rid:number): void}} c2dCallback
|
||||
*/
|
||||
setCloudToDeviceCallback (c2dCallback) {
|
||||
this.c2dCallback = c2dCallback
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} methodName
|
||||
* @param {string} payload
|
||||
* @param {number} rid
|
||||
* @param {number} status
|
||||
*/
|
||||
commandResponse (methodName, payload, rid, status) {
|
||||
const response = new Paho.MQTT.Message(payload)
|
||||
response.destinationName = DIRECT_METHOD_RESPONSE_TOPIC.replace('{status}', status.toString()) + rid.toString()
|
||||
this.client.send(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ (desired: string): void}} desiredPropCallback
|
||||
*/
|
||||
setDesiredPropertyCallback (desiredPropCallback) {
|
||||
this.desiredPropCallback = desiredPropCallback
|
||||
}
|
||||
}
|
||||
|
||||
export /**
|
||||
* @param {{ [x: string]: any; }} propValues
|
||||
* @param {number} ac
|
||||
* @param {number} av
|
||||
*/
|
||||
const ackPayload = (propValues, ac, av) => {
|
||||
const isObject = o => o === Object(o)
|
||||
const payload = {}
|
||||
Object.keys(propValues).filter(k => k !== '$version').forEach(k => {
|
||||
const value = propValues[k]
|
||||
if (isObject(value)) {
|
||||
Object.keys(value).filter(k => k !== '__t').forEach(p => {
|
||||
const desiredValue = value[p]
|
||||
propValues[k][p] = { ac, av, value: desiredValue }
|
||||
payload[k] = propValues[k]
|
||||
})
|
||||
} else {
|
||||
payload[k] = { ac, av, value }
|
||||
}
|
||||
})
|
||||
return payload
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Azure IoT Central Device Example</title>
|
||||
<!-- This is a development version of Vue.js! -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||
<script src="lib/paho-mqtt.js"></script>
|
||||
<link rel="stylesheet" href="s.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Device in a browser connecting to IoT Central using MQTT</h1>
|
||||
<div v-show="!connectionInfo.connected">
|
||||
<div class="header">
|
||||
Enter in the connection information for your IoT Central application and device
|
||||
</div>
|
||||
<div class="header">
|
||||
Get the model used by this device <a href="simple_device_model.json" download>here</a>
|
||||
then head to your Azure IoT Central applications by clicking <a href="https://apps.azureiotcentral.com/myapps" target="_blank">here</a>
|
||||
</div>
|
||||
<p>
|
||||
<label for="input Id Scope">Scope Identity</label>
|
||||
<input type="text" id="inputIdScope" size="55" v-model='connectionInfo.scopeId' />
|
||||
</p>
|
||||
<p>
|
||||
<label for="inputDeviceId">Device Identity</label>
|
||||
<input type="text" id="inputDeviceId" size="55"
|
||||
v-model='connectionInfo.deviceId'
|
||||
@change='updateDeviceKey()'>
|
||||
|
||||
<a v-show="viewDpsForm" href="#" @click="refreshDeviceId()">refresh</a>
|
||||
</p>
|
||||
<p>
|
||||
<label for="inputDeviceKey">Device SAS Token</label>
|
||||
<input type="text" id="inputDeviceKey" size="55"
|
||||
:disabled='disableDeviceKey'
|
||||
v-model='connectionInfo.deviceKey'> <span style="font-size: smaller;">(Auto calculated if Group SAS Token provided)</span>
|
||||
</p>
|
||||
<p>
|
||||
<label for="inputMasterKey">Group SAS Token</label>
|
||||
<input type="text" id="inputMasterKey" size="55"
|
||||
@change='updateDeviceKey()'
|
||||
v-model='connectionInfo.masterKey'>
|
||||
</p>
|
||||
<p>
|
||||
<label for="inputModelId">Model Identity</label>
|
||||
<input type="text" id="inputModelId" size="55" v-model='connectionInfo.modelId'>
|
||||
</p>
|
||||
|
||||
<div class="right" v-show="!runningProvision">
|
||||
<input type="button" value="Clear Form" @click="clearForm()">
|
||||
<input type="button" id="btnDPS" value="Provision and Connect" @click="provision()">
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="connectionInfo.connected" class="connected">
|
||||
Device <strong>{{ connectionInfo.deviceId }}</strong> connected to IoT Central
|
||||
</div>
|
||||
<div class="transport" v-show="connectionInfo.connected">
|
||||
<button @click="startTelemetry()" v-show="!isTelemetryRunning">Start Sending Telemetry</button>
|
||||
<button @click="stopTelemetry()" v-show="isTelemetryRunning">Stop Sending Telemetry</button>
|
||||
<button @click="fetchTwin()">Fetch Full Twin</button>
|
||||
<button @click="clearConsole()">Clear Console</button>
|
||||
</div>
|
||||
<div id="console" v-show="connectionInfo.connected" class="console">
|
||||
<span v-html="statusConsole"></span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="device.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,271 @@
|
|||
import {
|
||||
AzDpsClient,
|
||||
createHmac
|
||||
} from './AzDpsClient.js'
|
||||
import {
|
||||
AzIoTHubClient,
|
||||
ackPayload
|
||||
} from './AzIoTHubClient.js'
|
||||
|
||||
const createApp = () => {
|
||||
let telemetryInterval
|
||||
let reportedInterval
|
||||
let consoleEntries = 0
|
||||
const maxConsoleEntries = 200
|
||||
|
||||
/** @type {AzIoTHubClient} client */
|
||||
let client
|
||||
// @ts-ignore
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
|
||||
|
||||
data: {
|
||||
saveConfig: true,
|
||||
viewDpsForm: false,
|
||||
disableDeviceKey: false,
|
||||
runningProvision: false,
|
||||
|
||||
/** @type {ConnectionInfo} */
|
||||
connectionInfo: {
|
||||
scopeId: '',
|
||||
hubName: '',
|
||||
deviceId: 'SimpleDevice01',
|
||||
deviceKey: '',
|
||||
modelId: 'dtmi:simpleModel:simplesample;1',
|
||||
status: 'Disconnected',
|
||||
connected: false
|
||||
},
|
||||
|
||||
/** @type {Array<CommandInfo>} */
|
||||
isTelemetryRunning: false,
|
||||
statusConsole:''
|
||||
},
|
||||
|
||||
|
||||
async created() {
|
||||
/** @type { ConnectionInfo } connInfo */
|
||||
const connInfo = JSON.parse(window.localStorage.getItem('connectionInfo') || '{}')
|
||||
|
||||
connInfo.deviceId = connInfo.deviceId
|
||||
|
||||
if (connInfo.scopeId) {
|
||||
this.connectionInfo.scopeId = connInfo.scopeId
|
||||
if (connInfo.masterKey) {
|
||||
this.connectionInfo.masterKey = connInfo.masterKey
|
||||
this.connectionInfo.deviceKey = await createHmac(this.connectionInfo.masterKey, this.connectionInfo.deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
if (connInfo.hubName) {
|
||||
this.connectionInfo.hubName = connInfo.hubName
|
||||
this.connectionInfo.deviceId = connInfo.deviceId
|
||||
this.connectionInfo.deviceKey = connInfo.deviceKey
|
||||
this.connectionInfo.modelId = connInfo.modelId
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
writeToConsole(content, color) {
|
||||
if (consoleEntries >= maxConsoleEntries) {
|
||||
this.statusConsole = this.statusConsole.substring(this.statusConsole.indexOf('</div>')+6)
|
||||
consoleEntries--
|
||||
}
|
||||
this.statusConsole += '<div style="color: ' + color + ';">' + content + '</div>'
|
||||
consoleEntries++
|
||||
},
|
||||
|
||||
async provision() {
|
||||
window.localStorage.setItem('connectionInfo',
|
||||
JSON.stringify({
|
||||
scopeId: this.connectionInfo.scopeId,
|
||||
hubName: this.connectionInfo.hubName,
|
||||
deviceId: this.connectionInfo.deviceId,
|
||||
deviceKey: this.connectionInfo.deviceKey,
|
||||
masterKey: this.connectionInfo.masterKey,
|
||||
modelId: this.connectionInfo.modelId
|
||||
}))
|
||||
const dpsClient = new AzDpsClient(this.connectionInfo.scopeId, this.connectionInfo.deviceId, this.connectionInfo.deviceKey, this.connectionInfo.modelId)
|
||||
this.runningProvision = true
|
||||
const result = await dpsClient.registerDevice()
|
||||
this.runningProvision = false
|
||||
if (result.status === 'assigned') {
|
||||
this.connectionInfo.hubName = result.registrationState.assignedHub
|
||||
this.connect()
|
||||
} else {
|
||||
console.log(result)
|
||||
this.connectionInfo.hubName = result.status
|
||||
}
|
||||
this.viewDpsForm = false
|
||||
},
|
||||
|
||||
|
||||
async refreshDeviceId() {
|
||||
this.com
|
||||
this.connectionInfo.deviceId = 'device' + Date.now()
|
||||
await this.updateDeviceKey()
|
||||
},
|
||||
|
||||
async desiredPropertyAck(patch, status, statusMsg) {
|
||||
// acknowledge the desired property back to IoT Central
|
||||
const patchJSON = JSON.parse(patch)
|
||||
const keyName = Object.keys(patchJSON)[0]
|
||||
let reported_payload = {}
|
||||
reported_payload[keyName] = {"value": patchJSON[keyName], "ac":status,"ad":statusMsg,"av":patchJSON['$version']}
|
||||
await client.updateTwin(JSON.stringify(reported_payload))
|
||||
this.writeToConsole("Desired property patch acknowledged: " + "<pre>" + this.syntaxHighlight(patch) + "</pre>", "cyan")
|
||||
},
|
||||
|
||||
async processDirectMethods(method, payload, rid) {
|
||||
this.writeToConsole("Direct Method received: <pre>" + method + "(" + payload + ")</pre>", "green")
|
||||
let response = 'unknown command'
|
||||
let status = 404
|
||||
if (method == 'sendTextMessage') {
|
||||
response = {"messageRead": true}
|
||||
status = 200
|
||||
}
|
||||
this.writeToConsole("Direct Method response: <pre>(status: " + status + ", payload: " + "<pre>" + this.syntaxHighlight(response) + "</pre>" + ")</pre>", "green")
|
||||
await client.commandResponse(method, JSON.stringify(response), rid, status)
|
||||
},
|
||||
|
||||
async processDesiredPropertyPatch(patch) {
|
||||
this.writeToConsole("Desired property patch received: " + "<pre>" + this.syntaxHighlight(patch) + "</pre>", "cyan")
|
||||
await this.desiredPropertyAck(patch, 200, "completed")
|
||||
},
|
||||
|
||||
async processCloudToDeviceMessage(methodName, payload) {
|
||||
this.writeToConsole("Cloud to Device message received: <pre>" + methodName + "(" + payload + ")</pre>", "red")
|
||||
},
|
||||
|
||||
async connect() {
|
||||
if (this.saveConfig) {
|
||||
window.localStorage.setItem('connectionInfo',
|
||||
JSON.stringify({
|
||||
scopeId: this.connectionInfo.scopeId,
|
||||
hubName: this.connectionInfo.hubName,
|
||||
deviceId: this.connectionInfo.deviceId,
|
||||
deviceKey: this.connectionInfo.deviceKey,
|
||||
masterKey: this.connectionInfo.masterKey,
|
||||
modelId: this.connectionInfo.modelId
|
||||
}))
|
||||
}
|
||||
let host = this.connectionInfo.hubName
|
||||
if (host.indexOf('.azure-devices.net') === -1) {
|
||||
host += '.azure-devices.net'
|
||||
}
|
||||
client = new AzIoTHubClient(host,
|
||||
this.connectionInfo.deviceId,
|
||||
this.connectionInfo.deviceKey,
|
||||
this.connectionInfo.modelId)
|
||||
|
||||
client.setDirectMethodCallback(this.processDirectMethods)
|
||||
|
||||
client.setDesiredPropertyCallback(this.processDesiredPropertyPatch)
|
||||
|
||||
client.setCloudToDeviceCallback(this.processCloudToDeviceMessage)
|
||||
|
||||
client.disconnectCallback = (err) => {
|
||||
console.log(err)
|
||||
this.connectionInfo.connected = false
|
||||
this.connectionInfo.status = 'Disconnected'
|
||||
}
|
||||
|
||||
await client.connect()
|
||||
this.connectionInfo.status = 'Connected'
|
||||
this.connectionInfo.connected = true
|
||||
},
|
||||
|
||||
getRandomArbitrary(min, max, decimals) {
|
||||
return +((Math.random() * (max - min) + min).toFixed(decimals));
|
||||
},
|
||||
|
||||
startTelemetry() {
|
||||
telemetryInterval = setInterval(() => {
|
||||
const telemetryMessage = {"temp": this.getRandomArbitrary(32, 110, 2), "humidity": this.getRandomArbitrary(0, 100, 2)}
|
||||
this.writeToConsole("Sending telemetry: " + "<pre>" + this.syntaxHighlight(telemetryMessage) + "</pre>", "#DEFFFF")
|
||||
client.sendTelemetry(JSON.stringify(telemetryMessage))
|
||||
}, this.getRandomArbitrary(4500, 5500, 0))
|
||||
|
||||
reportedInterval = setInterval(() => {
|
||||
|
||||
const reportedMessage = {"fanspeed": this.getRandomArbitrary(0, 1000, 0)}
|
||||
this.writeToConsole("Sending reported property: " + "<pre>" + this.syntaxHighlight(reportedMessage) + "</pre>", "orange")
|
||||
client.updateTwin(JSON.stringify(reportedMessage))
|
||||
}, this.getRandomArbitrary(9500, 10500, 0))
|
||||
|
||||
this.isTelemetryRunning = true
|
||||
},
|
||||
|
||||
stopTelemetry() {
|
||||
clearInterval(telemetryInterval)
|
||||
clearInterval(reportedInterval)
|
||||
this.isTelemetryRunning = false
|
||||
},
|
||||
|
||||
clearConsole() {
|
||||
this.statusConsole = ''
|
||||
consoleEntries = 0
|
||||
},
|
||||
|
||||
clearForm() {
|
||||
window.localStorage.removeItem('connectionInfo')
|
||||
this.connectionInfo = {
|
||||
scopeId: '',
|
||||
hubName: '',
|
||||
deviceId: 'SimpleDevice01',
|
||||
deviceKey: '',
|
||||
modelId: 'dtmi:simpleModel:simplesample;1',
|
||||
status: 'Disconnected',
|
||||
connected: false
|
||||
}
|
||||
},
|
||||
|
||||
syntaxHighlight(json) {
|
||||
if (typeof json != 'string') {
|
||||
json = JSON.stringify(json, undefined, 2)
|
||||
}
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
|
||||
var cls = 'number'
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
cls = 'key'
|
||||
} else {
|
||||
cls = 'string'
|
||||
}
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'boolean'
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'null'
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>'
|
||||
})
|
||||
},
|
||||
|
||||
async fetchTwin() {
|
||||
if (client.connected) {
|
||||
const twin = await client.getTwin()
|
||||
this.writeToConsole('<div style="color: white;">Current full twin:<pre>' + this.syntaxHighlight(twin) + '</pre></div>', 'white')
|
||||
}
|
||||
},
|
||||
|
||||
async updateDeviceKey() {
|
||||
this.disableDeviceKey = true
|
||||
this.connectionInfo.deviceKey = await createHmac(this.connectionInfo.masterKey, this.connectionInfo.deviceId)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
connectionString() {
|
||||
return `HostName=${this.connectionInfo.hubName}.azure-devices.net;DeviceId=${this.connectionInfo.deviceId};SharedAccessKey=${this.connectionInfo.deviceKey}`
|
||||
}
|
||||
},
|
||||
})
|
||||
return app
|
||||
}
|
||||
|
||||
(() => {
|
||||
createApp()
|
||||
})()
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,56 @@
|
|||
body {
|
||||
font-family: Arial, Helvetica, sans-serif
|
||||
}
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 140px;
|
||||
text-align: right
|
||||
}
|
||||
table,tr,td {
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.right {
|
||||
margin-left: 330px;
|
||||
}
|
||||
.footer {
|
||||
padding:20px;
|
||||
width: 100%;
|
||||
}
|
||||
.connected {
|
||||
background-color: lightgreen;
|
||||
top:0px;
|
||||
left:0px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
.console {
|
||||
background-color: #1E1E1E;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding-top: 10px;
|
||||
border-width: 2px;
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
resize: vertical;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.transport {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.header {
|
||||
display: inline-block;
|
||||
margin-left: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
.string { color: #3B8334; }
|
||||
.number { color: darkorange; }
|
||||
.boolean { color: #6BCFFE; }
|
||||
.null { color: whitesmoke; }
|
||||
.key { color: #C586C0; }
|
|
@ -0,0 +1,122 @@
|
|||
[
|
||||
{
|
||||
"@id": "dtmi:simpleModel:simplesample;1",
|
||||
"@type": "Interface",
|
||||
"contents": [
|
||||
{
|
||||
"@id": "dtmi:simpleModel:simplesample:temp;1",
|
||||
"@type": "Telemetry",
|
||||
"comment": "This is a telemetry element",
|
||||
"description": {
|
||||
"en": "This is a telemetry element"
|
||||
},
|
||||
"displayName": {
|
||||
"en": "Temperature"
|
||||
},
|
||||
"name": "temp",
|
||||
"schema": "double",
|
||||
"unit": "farad"
|
||||
},
|
||||
{
|
||||
"@id": "dtmi:simpleModel:simplesample:Humidity;1",
|
||||
"@type": "Telemetry",
|
||||
"comment": "This is a telemetry element",
|
||||
"description": {
|
||||
"en": "This is a telemetry element"
|
||||
},
|
||||
"displayName": {
|
||||
"en": "Humidity"
|
||||
},
|
||||
"name": "humidity",
|
||||
"schema": "double",
|
||||
"unit": "percent"
|
||||
},
|
||||
{
|
||||
"@id": "dtmi:simpleModel:simplesample:fanspeed;1",
|
||||
"@type": "Property",
|
||||
"comment": "This is a reported property",
|
||||
"description": {
|
||||
"en": "This is a reported property"
|
||||
},
|
||||
"displayName": {
|
||||
"en": "Fan Speed"
|
||||
},
|
||||
"name": "fanspeed",
|
||||
"schema": "integer",
|
||||
"unit": "revolutionPerMinute"
|
||||
},
|
||||
{
|
||||
"@id": "dtmi:simpleModel:simplesample:setTemp;1",
|
||||
"@type": "Property",
|
||||
"comment": "This is a desired property",
|
||||
"description": {
|
||||
"en": "This is a desired property"
|
||||
},
|
||||
"displayName": {
|
||||
"en": "Set Thermostat Temperature"
|
||||
},
|
||||
"name": "setTemp",
|
||||
"schema": "double",
|
||||
"unit": "farad",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"@id": "dtmi:simpleModel:simplesample:sendTextMessage;1",
|
||||
"@type": "Command",
|
||||
"commandType": "synchronous",
|
||||
"comment": "This is a direct Method",
|
||||
"description": {
|
||||
"en": "This is a direct Method"
|
||||
},
|
||||
"displayName": {
|
||||
"en": "Send Text Message"
|
||||
},
|
||||
"name": "sendTextMessage",
|
||||
"request": {
|
||||
"@type": "CommandPayload",
|
||||
"displayName": {
|
||||
"en": "Message"
|
||||
},
|
||||
"name": "message",
|
||||
"schema": "string"
|
||||
},
|
||||
"response": {
|
||||
"@type": "CommandPayload",
|
||||
"displayName": {
|
||||
"en": "Message Read"
|
||||
},
|
||||
"name": "messageRead",
|
||||
"schema": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@id": "dtmi:simpleModel:simplesample:startVacuumCleaner;1",
|
||||
"@type": "Command",
|
||||
"durable": true,
|
||||
"comment": "This is a cloud to device message",
|
||||
"description": {
|
||||
"en": "This is a cloud to device message"
|
||||
},
|
||||
"displayName": {
|
||||
"en": "Start Vacuum cleaner"
|
||||
},
|
||||
"name": "startVacuumCleaner",
|
||||
"request": {
|
||||
"@type": "CommandPayload",
|
||||
"displayName": {
|
||||
"en": "Time to start"
|
||||
},
|
||||
"name": "timeToStart",
|
||||
"schema": "time"
|
||||
}
|
||||
}
|
||||
],
|
||||
"displayName": {
|
||||
"en": "simple_sample"
|
||||
},
|
||||
"@context": [
|
||||
"dtmi:iotcentral:context;2",
|
||||
"dtmi:dtdl:context;2"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "webmqtt",
|
||||
"version": "1.0.0",
|
||||
"description": "Run an Azure IoT device in a browser using MQTT over websockets",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"mime-types": "^2.1.28"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "node server.js",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "Ian Hollier",
|
||||
"license": "ISC"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
var fs = require('fs'),
|
||||
http = require('http');
|
||||
var mime = require('mime-types')
|
||||
|
||||
http.createServer(function (req, res) {
|
||||
if (fs.existsSync(__dirname + '/content/' + req.url)) {
|
||||
fs.readFile(__dirname + '/content/' + req.url, function (err,data) {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify(err));
|
||||
return;
|
||||
}
|
||||
var contentType = mime.lookup(req.url)
|
||||
var headers = {};
|
||||
res.setHeader('content-type', contentType);
|
||||
res.writeHead(200);
|
||||
res.end(data);
|
||||
});
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
}).listen(8080);
|
||||
console.log('Listening at http//localhost:8080');
|
Загрузка…
Ссылка в новой задаче