diff --git a/iot/http_example/README.md b/iot/http_example/README.md new file mode 100644 index 0000000000..a4e2ed9b55 --- /dev/null +++ b/iot/http_example/README.md @@ -0,0 +1,63 @@ +Google Cloud Platform logo + +# Google Cloud IoT Core NodeJS HTTP example + +This sample app publishes messages to [Google Cloud Pub/Sub](pubsub) or updates +device states using the HTTP bridge provided as part of Google Cloud IoT Core. + +Note that before you can run this sample, you must register a device as +described in the parent README. + +[pubsub]: https://cloud.google.com/pubsub/docs +# Setup + +Run the following command to install the library dependencies for NodeJS: + + npm install + +# Running the sample + +The following command summarizes the sample usage: + +Usage: cloudiot_http_example_nodejs [options] + +Example Google Cloud IoT Core HTTP device connection code. + +Options: + + -h, --help output usage information + --project_id GCP cloud project name. + --registry_id Cloud IoT Core registry id. + --device_id Cloud IoT Core device id. + --private_key_file Path to private key file. + --algorithm Encryption algorithm to generate the JWT. + Either RS256 (RSA) or ES256 (Eliptic Curve) + --cloud_region [region] GCP cloud region + --num_messages [num] Number of messages to publish. + --http_bridge_address [address] HTTP bridge address. + --message_type [events|state] The message type to publish. + +For example, if your project ID is `blue-jet-123`, your service account +credentials are stored in your home folder in creds.json and you have generated +your credentials using the shell script provided in the parent folder, you can +run the sample as: + + node cloudiot_http_example_nodejs.js \ + --project_id=blue-jet-123 \ + --registry_id=my-registry \ + --device_id=my-node-device \ + --private_key_file=../rsa_private.pem \ + --algorithm=RS256 + +# Reading Cloud Pub/Sub messages written by the sample client + +1. Create a subscription to your topic. + + gcloud beta pubsub subscriptions create \ + projects/your-project-id/subscriptions/my-subscription \ + --topic device-events + +2. Read messages published to the topic + + gcloud beta pubsub subscriptions pull --auto-ack \ + projects/my-iot-project/subscriptions/my-subscription diff --git a/iot/http_example/cloudiot_http_example_nodejs.js b/iot/http_example/cloudiot_http_example_nodejs.js new file mode 100644 index 0000000000..101676cb27 --- /dev/null +++ b/iot/http_example/cloudiot_http_example_nodejs.js @@ -0,0 +1,163 @@ +/** + * Copyright 2017, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +// [START iot_http_includes] +const fs = require('fs'); +const jwt = require('jsonwebtoken'); +const request = require('request'); +// [END iot_http_includes] + +console.log('Google Cloud IoT Core HTTP example.'); +var argv = require(`yargs`) + .options({ + project_id: { + default: process.env.GCLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT, + description: 'The Project ID to use. Defaults to the value of the GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT environment variables.', + requiresArg: true, + type: 'string' + }, + cloud_region: { + default: 'us-central1', + description: 'GCP cloud region.', + requiresArg: true, + type: 'string' + }, + registry_id: { + description: 'Cloud IoT registry ID.', + requiresArg: true, + demandOption: true, + type: 'string' + }, + device_id: { + description: 'Cloud IoT device ID.', + requiresArg: true, + demandOption: true, + type: 'string' + }, + private_key_file: { + description: 'Path to private key file.', + requiresArg: true, + demandOption: true, + type: 'string' + }, + algorithm: { + description: 'Encryption algorithm to generate the RSA or EC JWT.', + requiresArg: true, + demandOption: true, + choices: ['RS256', 'ES256'], + type: 'string' + }, + num_messages: { + default: 100, + description: 'Number of messages to publish.', + requiresArg: true, + type: 'number' + }, + http_bridge_address: { + default: 'cloudiot-device.googleapis.com', + description: 'HTTP bridge address.', + requiresArg: true, + type: 'string' + }, + message_type: { + default: 'events', + description: 'Message type to publish.', + requiresArg: true, + choices: ['events', 'state'], + type: 'string' + } + }) + .example(`node $0 cloudiot_http_example_nodejs.js --project_id=blue-jet-123 --registry_id=my-registry --device_id=my-node-device --private_key_file=../rsa_private.pem --algorithm=RS256`) + .wrap(120) + .recommendCommands() + .epilogue(`For more information, see https://cloud.google.com/iot-core/docs`) + .help() + .strict() + .argv; + +// Create a Cloud IoT Core JWT for the given project ID, signed with the given +// private key. +// [START iot_http_jwt] +function createJwt (projectId, privateKeyFile, algorithm) { + // Create a JWT to authenticate this device. The device will be disconnected + // after the token expires, and will have to reconnect with a new token. The + // audience field should always be set to the GCP project ID. + const token = { + 'iat': parseInt(Date.now() / 1000), + 'exp': parseInt(Date.now() / 1000) + 20 * 60, // 20 minutes + 'aud': projectId + }; + const privateKey = fs.readFileSync(privateKeyFile); + return jwt.sign(token, privateKey, { algorithm: algorithm }); +} +// [END iot_http_jwt] + +// Publish numMessages message asynchronously, starting from message +// messageCount. Telemetry events are published at a rate of 1 per second and +// states at a rate of 1 every 2 seconds. +// [START iot_http_publish] +function publishAsync (messageCount, numMessages) { + const payload = `${argv.registry_id}/${argv.device_id}-payload-${messageCount}`; + console.log('Publishing message:', payload); + const binaryData = Buffer.from(payload).toString('base64'); + const postData = argv.message_type === 'events' ? { + binary_data: binaryData + } : { + state: { + binary_data: binaryData + } + }; + const options = { + url: url, + headers: { + 'Authorization': 'Bearer ' + authToken + }, + json: true, + body: postData + }; + // Send events for high-frequency updates, update state only occasionally. + const delayMs = argv.message_type === 'events' ? 1000 : 2000; + request.post(options, function (error, response, body) { + if (error) { + return console.error('Received error: ', error); + } + console.log('Received response: '); + console.dir(response); + if (messageCount < numMessages) { + // If we have published fewer than numMessage messages, publish payload + // messageCount + 1. + setTimeout(function () { + publishAsync(messageCount + 1, numMessages); + }, delayMs); + } + }); +} +// [END iot_http_publish] + +// [START iot_run_http] +// A unique string that identifies this device. For Google Cloud IoT Core, it +// must be in the format below. +const devicePath = `projects/${argv.project_id}/locations/${argv.cloud_region}/registries/${argv.registry_id}/devices/${argv.device_id}`; + +// The request path, set accordingly depending on the message type. +const pathSuffix = argv.message_type === 'events' + ? ':publishEvent' : ':setState'; +const url = `https://${argv.http_bridge_address}/v1beta1/${devicePath}${pathSuffix}`; +const authToken = createJwt(argv.project_id, argv.private_key_file, argv.algorithm); + +// Publish messages. +publishAsync(1, argv.num_messages); +// [END iot_run_http] diff --git a/iot/http_example/package.json b/iot/http_example/package.json new file mode 100644 index 0000000000..89c6b77985 --- /dev/null +++ b/iot/http_example/package.json @@ -0,0 +1,14 @@ +{ + "name": "nodejs-docs-samples-iot-http-example", + "version": "0.0.1", + "description": "HTTP Example for Google Cloud IoT Core using NodeJS.", + "license": "Apache-2.0", + "author": "Google Inc.", + "main": "cloudiot_http_example_nodejs.js", + "dependencies": { + "yargs": "8.0.2", + "jsonwebtoken": "7.4.1", + "request": "2.82.0" + }, + "devDependencies": {} +} diff --git a/iot/manager/manager.js b/iot/manager/manager.js index 732bb0f69c..53dad96b9d 100644 --- a/iot/manager/manager.js +++ b/iot/manager/manager.js @@ -18,7 +18,7 @@ const fs = require('fs'); const google = require('googleapis'); -const API_VERSION = 'v1beta1'; +const API_VERSION = 'v1'; const DISCOVERY_API = 'https://cloudiot.googleapis.com/$discovery/rest'; // Configures the topic for Cloud IoT Core. @@ -125,9 +125,9 @@ function createRegistry (client, registryId, projectId, cloudRegion, const request = { parent: parentName, resource: { - eventNotificationConfig: { - pubsubTopicName: pubsubTopic - }, + eventNotificationConfigs: [{ + 'pubsubTopicName': pubsubTopic + }], 'id': registryId } }; @@ -545,6 +545,34 @@ function getDevice (client, deviceId, registryId, projectId, cloudRegion) { // [END iot_get_device] } +// Retrieve the given device's state from the registry. +function getDeviceState (client, deviceId, registryId, projectId, + cloudRegion) { + // [START iot_get_device_state] + // Client retrieved in callback + // getClient(apiKey, serviceAccountJson, function(client) {...}); + // const cloudRegion = 'us-central1'; + // const deviceId = 'my-device'; + // const projectId = 'adjective-noun-123'; + // const registryId = 'my-registry'; + const parentName = `projects/${projectId}/locations/${cloudRegion}`; + const registryName = `${parentName}/registries/${registryId}`; + const request = { + name: `${registryName}/devices/${deviceId}` + }; + + client.projects.locations.registries.devices.states.list(request, + (err, data) => { + if (err) { + console.log('Could not find device:', deviceId); + console.log(err); + } else { + console.log('State:', data); + } + }); + // [END iot_get_device_state] +} + // Retrieve the given device from the registry. function getRegistry (client, registryId, projectId, cloudRegion) { // [START iot_get_registry] @@ -733,6 +761,18 @@ require(`yargs`) // eslint-disable-line getClient(opts.apiKey, opts.serviceAccount, cb); } ) + .command( + `getDeviceState `, + `Retrieves device state given a device ID.`, + {}, + (opts) => { + const cb = function (client) { + getDeviceState(client, opts.deviceId, opts.registryId, opts.projectId, + opts.cloudRegion); + }; + getClient(opts.apiKey, opts.serviceAccount, cb); + } + ) .command( `getRegistry `, `Retrieves a registry.`, @@ -797,6 +837,7 @@ require(`yargs`) // eslint-disable-line .example(`node $0 deleteDevice my-device my-registry`) .example(`node $0 deleteRegistry my-device my-registry`) .example(`node $0 getDevice my-device my-registry`) + .example(`node $0 getDeviceState my-device my-registry`) .example(`node $0 getRegistry my-registry`) .example(`node $0 listDevices my-node-registry`) .example(`node $0 listRegistries`) diff --git a/iot/manager/system-test/manager.test.js b/iot/manager/system-test/manager.test.js index 3b2d04d795..e797d24992 100644 --- a/iot/manager/system-test/manager.test.js +++ b/iot/manager/system-test/manager.test.js @@ -70,6 +70,9 @@ test(`should create and delete an RSA256 device`, async (t) => { output = await tools.runAsync( `${cmd} createRsa256Device ${localDevice} ${localRegName} resources/rsa_cert.pem`, cwd); t.regex(output, new RegExp(`Created device`)); + output = await tools.runAsync( + `${cmd} getDeviceState ${localDevice} ${localRegName}`, cwd); + t.regex(output, new RegExp(`State`)); output = await tools.runAsync( `${cmd} deleteDevice ${localDevice} ${localRegName}`, cwd); t.regex(output, new RegExp(`Successfully deleted device`)); @@ -85,6 +88,9 @@ test(`should create and delete an EC256 device`, async (t) => { output = await tools.runAsync( `${cmd} createEs256Device ${localDevice} ${localRegName} resources/ec_public.pem`, cwd); t.regex(output, new RegExp(`Created device`)); + output = await tools.runAsync( + `${cmd} getDeviceState ${localDevice} ${localRegName}`, cwd); + t.regex(output, new RegExp(`State`)); output = await tools.runAsync( `${cmd} deleteDevice ${localDevice} ${localRegName}`, cwd); t.regex(output, new RegExp(`Successfully deleted device`)); diff --git a/iot/mqtt_example/README.md b/iot/mqtt_example/README.md index 342da97943..848681af22 100644 --- a/iot/mqtt_example/README.md +++ b/iot/mqtt_example/README.md @@ -34,6 +34,7 @@ The following command summarizes the sample usage: --num_messages [num] Number of messages to publish. --mqtt_bridge_hostname [hostname] MQTT bridge hostname. --mqtt_bridge_port [port] MQTT bridge port. + --message_type [events|state] The message type to publish. For example, if your project ID is `blue-jet-123`, your service account credentials are stored in your home folder in creds.json and you have generated diff --git a/iot/mqtt_example/cloudiot_mqtt_example_nodejs.js b/iot/mqtt_example/cloudiot_mqtt_example_nodejs.js index 7b73d4b128..8bdb60450e 100644 --- a/iot/mqtt_example/cloudiot_mqtt_example_nodejs.js +++ b/iot/mqtt_example/cloudiot_mqtt_example_nodejs.js @@ -13,55 +13,91 @@ * limitations under the License. */ -/** - * NodeJS sample of connecting to Google Cloud IoT Core via MQTT, using JWT. - * - *

This example connects to Google Cloud IoT Core via MQTT, using a JWT for - * device authentication. After connecting, by default the device publishes 100 - * messages to the device's MQTT topic at a rate of one per second, and then - * exits. - * - *

Before you can run this sample, you must register a device as described - * in the parent README. - * - *

Usage example: - * - *

- *   $ npm install
- *   $ nodejs cloudiot_mqtt_example_nodejs.js \
- *       --project_id=my-project-id \
- *       --registry_id=my-registry-id \
- *       --device_id=my-device-id \
- *       --private_key_file=rsa_private.pem \
- *       --algorithm=RS256"
- * 
- */ - 'use strict'; +// [START iot_mqtt_include] const fs = require('fs'); const jwt = require('jsonwebtoken'); const mqtt = require('mqtt'); -const program = require('commander'); - -program.description('Google Cloud IoT Core MQTT example.') - .option('--project_id ', 'GCP cloud project name.') - .option('--registry_id ', 'Cloud IoT registry id.') - .option('--device_id ', 'Cloud IoT device id.') - .option('--private_key_file ', 'Path to private key file.') - .option( - '--algorithm ', - 'Encryption algorithm to generate the JWT. Either RS256 or ES256') - .option('--cloud_region [region]', 'GCP cloud region', 'us-central1') - .option('--num_messages [num]', 'Number of messages to publish.', 100) - .option( - '--mqtt_bridge_hostname [hostname]', 'MQTT bridge hostname.', - 'mqtt.googleapis.com') - .option('--mqtt_bridge_port [port]', 'MQTT bridge port.', 8883) - .parse(process.argv); +// [END iot_mqtt_include] + +console.log('Google Cloud IoT Core MQTT example.'); +var argv = require(`yargs`) + .options({ + project_id: { + default: process.env.GCLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT, + description: 'The Project ID to use. Defaults to the value of the GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT environment variables.', + requiresArg: true, + type: 'string' + }, + cloud_region: { + default: 'us-central1', + description: 'GCP cloud region.', + requiresArg: true, + type: 'string' + }, + registry_id: { + description: 'Cloud IoT registry ID.', + requiresArg: true, + demandOption: true, + type: 'string' + }, + device_id: { + description: 'Cloud IoT device ID.', + requiresArg: true, + demandOption: true, + type: 'string' + }, + private_key_file: { + description: 'Path to private key file.', + requiresArg: true, + demandOption: true, + type: 'string' + }, + algorithm: { + description: 'Encryption algorithm to generate the JWT.', + requiresArg: true, + demandOption: true, + choices: ['RS256', 'ES256'], + type: 'string' + }, + num_messages: { + default: 100, + description: 'Number of messages to publish.', + requiresArg: true, + type: 'number' + }, + mqtt_bridge_hostname: { + default: 'mqtt.googleapis.com', + description: 'MQTT bridge hostname.', + requiresArg: true, + type: 'string' + }, + mqtt_bridge_port: { + default: 8883, + description: 'MQTT bridge port.', + requiresArg: true, + type: 'number' + }, + message_type: { + default: 'events', + description: 'Message type to publish.', + requiresArg: true, + choices: ['events', 'state'], + type: 'string' + } + }) + .example(`node $0 cloudiot_mqtt_example_nodejs.js --project_id=blue-jet-123 --registry_id=my-registry --device_id=my-node-device --private_key_file=../rsa_private.pem --algorithm=RS256`) + .wrap(120) + .recommendCommands() + .epilogue(`For more information, see https://cloud.google.com/iot-core/docs`) + .help() + .strict() + .argv; // Create a Cloud IoT Core JWT for the given project id, signed with the given // private key. +// [START iot_mqtt_jwt] function createJwt (projectId, privateKeyFile, algorithm) { // Create a JWT to authenticate this device. The device will be disconnected // after the token expires, and will have to reconnect with a new token. The @@ -74,59 +110,65 @@ function createJwt (projectId, privateKeyFile, algorithm) { const privateKey = fs.readFileSync(privateKeyFile); return jwt.sign(token, privateKey, { algorithm: algorithm }); } +// [END iot_mqtt_jwt] // Publish numMessages messages asynchronously, starting from message // messageCount. +// [START iot_mqtt_publish] function publishAsync (messageCount, numMessages) { - const payload = `${program.registry_id}/${program.device_id}-payload-${messageCount}`; + const payload = `${argv.registry_id}/${argv.device_id}-payload-${messageCount}`; // Publish "payload" to the MQTT topic. qos=1 means at least once delivery. // Cloud IoT Core also supports qos=0 for at most once delivery. console.log('Publishing message:', payload); client.publish(mqttTopic, payload, { qos: 1 }); + const delayMs = argv.message_type === 'events' ? 1000 : 2000; if (messageCount < numMessages) { // If we have published fewer than numMessage messages, publish payload // messageCount + 1 in 1 second. setTimeout(function () { publishAsync(messageCount + 1, numMessages); - }, 1000); + }, delayMs); } else { // Otherwise, close the connection. console.log('Closing connection to MQTT. Goodbye!'); client.end(); } } +// [END iot_mqtt_publish] +// [START iot_mqtt_run] // The mqttClientId is a unique string that identifies this device. For Google // Cloud IoT Core, it must be in the format below. -const mqttClientId = `projects/${program.project_id}/locations/${program.cloud_region}/registries/${program.registry_id}/devices/${program.device_id}`; +const mqttClientId = `projects/${argv.project_id}/locations/${argv.cloud_region}/registries/${argv.registry_id}/devices/${argv.device_id}`; // With Google Cloud IoT Core, the username field is ignored, however it must be // non-empty. The password field is used to transmit a JWT to authorize the // device. The "mqtts" protocol causes the library to connect using SSL, which // is required for Cloud IoT Core. const connectionArgs = { - host: program.mqtt_bridge_hostname, - port: program.mqtt_bridge_port, + host: argv.mqtt_bridge_hostname, + port: argv.mqtt_bridge_port, clientId: mqttClientId, username: 'unused', - password: createJwt(program.project_id, program.private_key_file, program.algorithm), + password: createJwt(argv.project_id, argv.private_key_file, argv.algorithm), protocol: 'mqtts' }; // Create a client, and connect to the Google MQTT bridge. const client = mqtt.connect(connectionArgs); -// The MQTT topic that this device will publish telemetry data to. The MQTT -// topic name is required to be in the format below. Note that this is not the -// same as the device registry's Cloud Pub/Sub topic. -const mqttTopic = `/devices/${program.device_id}/events`; +// The MQTT topic that this device will publish data to. The MQTT +// topic name is required to be in the format below. The topic name must end in +// 'state' to publish state and 'events' to publish telemetry. Note that this is +// not the same as the device registry's Cloud Pub/Sub topic. +const mqttTopic = `/devices/${argv.device_id}/${argv.message_type}`; client.on('connect', () => { console.log('connect', arguments); // After connecting, publish 'num_messages' messagse asynchronously, at a rate - // of 1 per second. - publishAsync(1, program.num_messages); + // of 1 per second for telemetry events and 1 every 2 seconds for states. + publishAsync(1, argv.num_messages); }); client.on('close', () => { @@ -143,3 +185,4 @@ client.on('packetsend', () => { // Once all of the messages have been published, the connection to Google Cloud // IoT will be closed and the process will exit. See the publishAsync method. +// [END iot_mqtt_run] diff --git a/iot/mqtt_example/package.json b/iot/mqtt_example/package.json index 137a303854..6a8c79eaf5 100644 --- a/iot/mqtt_example/package.json +++ b/iot/mqtt_example/package.json @@ -3,8 +3,10 @@ "version": "0.0.1", "description": "MQTT Example for Google Cloud IoT Core using NodeJS.", "main": "cloudiot_mqtt_example_nodejs.js", + "license": "Apache-2.0", + "author": "Google Inc.", "dependencies": { - "commander": "2.9.0", + "yargs": "8.0.2", "jsonwebtoken": "7.4.1", "mqtt": "2.7.2" },