diff --git a/data/device-ui.html b/data/device-ui.html new file mode 100644 index 00000000..1385876c --- /dev/null +++ b/data/device-ui.html @@ -0,0 +1,69 @@ + + + + + Device UI + + + + + + + +
+ + + diff --git a/scripts/connect.ts b/scripts/connect.ts index 95107b1b..f53f0d20 100644 --- a/scripts/connect.ts +++ b/scripts/connect.ts @@ -1,13 +1,15 @@ import * as path from 'path' import { Iot } from 'aws-sdk' -import { device } from 'aws-iot-device-sdk' +import { thingShadow } from 'aws-iot-device-sdk' import { deviceFileLocations } from './jitp/deviceFileLocations' import chalk from 'chalk' +import { uiServer } from './device/ui-server' /** * Connect to the AWS IoT broker using a generated device certificate */ const main = async (args: { deviceId: string }) => { + const clientId = args.deviceId const { endpointAddress } = await new Iot({ region: process.env.AWS_DEFAULT_REGION, }) @@ -21,10 +23,10 @@ const main = async (args: { deviceId: string }) => { console.log( chalk.blue(`IoT broker hostname: ${chalk.yellow(endpointAddress)}`), ) - console.log(chalk.blue(`Device ID: ${chalk.yellow(args.deviceId)}`)) + console.log(chalk.blue(`Device ID: ${chalk.yellow(clientId)}`)) const certsDir = path.resolve(process.cwd(), 'certificates') - const deviceFiles = deviceFileLocations(certsDir, args.deviceId) + const deviceFiles = deviceFileLocations(certsDir, clientId) console.time(chalk.green(chalk.inverse(' connected '))) @@ -36,30 +38,65 @@ const main = async (args: { deviceId: string }) => { console.timeLog(note) }, 5000) - const connection = new device({ + const connection = new thingShadow({ privateKey: deviceFiles.key, clientCert: deviceFiles.certWithCA, caCert: path.resolve(process.cwd(), 'data', 'AmazonRootCA1.pem'), - clientId: args.deviceId.trim(), + clientId, host: endpointAddress, region: endpointAddress.split('.')[2], + debug: true, }) connection.on('connect', async () => { console.timeEnd(chalk.green(chalk.inverse(' connected '))) clearInterval(connectingNote) - }) - connection.on('close', () => { - console.error(chalk.red(chalk.inverse(' disconnected! '))) - }) + connection.register(clientId, {}, async () => { + await uiServer({ + onUpdate: update => { + console.log({ clientId, state: { state: { reported: update } } }) + connection.update(clientId, { state: { reported: update } }) + }, + }) + }) + + connection.on('close', () => { + console.error(chalk.red(chalk.inverse(' disconnected! '))) + }) + + connection.on('reconnect', () => { + console.log(chalk.magenta('reconnecting...')) + }) + + connection.on('status', function(thingName, stat, _, stateObject) { + console.log( + 'received ' + + stat + + ' on ' + + thingName + + ': ' + + JSON.stringify(stateObject), + ) + }) + + connection.on('delta', function(thingName, stateObject) { + console.log( + 'received delta on ' + thingName + ': ' + JSON.stringify(stateObject), + ) + }) - connection.on('reconnect', () => { - console.log(chalk.magenta('reconnecting...')) + connection.on('timeout', function(thingName, clientToken) { + console.log( + 'received timeout on ' + thingName + ' with token: ' + clientToken, + ) + }) }) } -main({ deviceId: process.argv[process.argv.length - 1] }).catch(error => { - console.error(chalk.red(error)) - process.exit(1) -}) +main({ deviceId: process.argv[process.argv.length - 1].trim() }).catch( + error => { + console.error(chalk.red(error)) + process.exit(1) + }, +) diff --git a/scripts/device/ui-server.ts b/scripts/device/ui-server.ts new file mode 100644 index 00000000..5221d45d --- /dev/null +++ b/scripts/device/ui-server.ts @@ -0,0 +1,62 @@ +import * as path from 'path' +import * as http from 'http' +import { promises as fs } from 'fs' +import chalk from 'chalk' + +export const uiServer = async (args: { + onUpdate: (update: object) => void +}) => { + const port = 1024 + Math.round(Math.random() * (65535 - 1024)) + const uiPage = await fs.readFile( + path.resolve(process.cwd(), 'data', 'device-ui.html'), + 'utf-8', + ) + + const requestHandler: http.RequestListener = async (request, response) => { + let body = '' + switch (request.url) { + case '/': + case '/index.html': + response.writeHead(200, { + 'Content-Length': Buffer.byteLength(uiPage), + 'Content-Type': 'text/html', + }) + response.end(uiPage) + break + case '/update': + request.on('data', chunk => { + body += chunk.toString() // convert Buffer to string + }) + request.on('end', () => { + try { + const update = JSON.parse(body) + args.onUpdate(update) + response.statusCode = 202 + response.end() + } catch (err) { + console.log(err) + const errData = JSON.stringify(err) + response.writeHead(400, { + 'Content-Length': Buffer.byteLength(errData), + 'Content-Type': 'application/json', + }) + response.end(errData) + } + }) + break + default: + response.statusCode = 404 + response.end() + } + } + + const server = http.createServer(requestHandler) + + server.listen(port, () => { + console.log( + chalk.green( + `To control this device open your browser on http://localhost:${port}`, + ), + ) + }) +}