Skip to content

Commit

Permalink
Merge branch 'master' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
ridomin committed Dec 4, 2023
2 parents 0e0346a + b999d9c commit c453f77
Show file tree
Hide file tree
Showing 11 changed files with 9,889 additions and 4,017 deletions.
45 changes: 45 additions & 0 deletions app/AzDpsClient.js
Original file line number Diff line number Diff line change
@@ -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
}
}
255 changes: 255 additions & 0 deletions app/AzIoTHubClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
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='

/**
*
* @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 commnand invocation is received
* @param {string} method
* @param {string} payload
* @param {number} rid
*/
this.c2dCallback = (method, payload, rid) => { }

/**
* @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.c2dCallback(methodName, payloadString, rid)
}
if (destinationName.indexOf('twin/PATCH/properties/desired') > 1) {
this.desiredPropCallback(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
})
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}} c2dCallback
*/
setDirectMehodCallback (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 payload = {}
Object.keys(propValues).filter(k => k !== '$version').forEach(k => {
const value = propValues[k]
if (value.__t === 'c') { // is Component
delete value.__t
payload[k] = { __t: 'c' }
Object.keys(value).forEach(v => {
payload[k][v] = { ac, av, value: value[v] }
})
} else {
payload[k] = { ac, av, value }
}
})
return payload
}
Binary file added app/favicon.ico
Binary file not shown.
23 changes: 23 additions & 0 deletions app/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
type ConnectionInfo = {
scopeId: string,
deviceId: string,
hubName: string,
deviceKey: string,
masterKey?: string,
modelId?: string,
status: string,
connected: bool
}

type DeviceTwin = {
reported: any,
desired: any
}

type CommandInfo = {
method: string,
payload: string,
response: string,
rid: number,
dirty: boolean
}
Loading

0 comments on commit c453f77

Please sign in to comment.