diff --git a/samples/pubsub.md b/samples/pubsub.md new file mode 100644 index 00000000..0ba8ac91 --- /dev/null +++ b/samples/pubsub.md @@ -0,0 +1,73 @@ +# PubSub + +[**Return to main sample list**](./README.md) + +This sample uses the +[Message Broker](https://docs.aws.amazon.com/iot/latest/developerguide/iot-message-broker.html) +for AWS IoT to send and receive messages through an MQTT connection. + +On startup, the device connects to the server, subscribes to a topic, and begins publishing messages to that topic. The device should receive those same messages back from the message broker, since it is subscribed to that same topic. Status updates are continually printed to the console. This sample demonstrates how to send and receive messages on designated IoT Core topics, an essential task that is the backbone of many IoT applications that need to send data over the internet. This sample simply subscribes and publishes to a topic, printing the messages it just sent as it is received from AWS IoT Core, but this can be used as a reference point for more complex Pub-Sub applications. + +Your IoT Core Thing's [Policy](https://docs.aws.amazon.com/iot/latest/developerguide/iot-policies.html) must provide privileges for this sample to connect, subscribe, publish, and receive. Below is a sample policy that can be used on your IoT Core Thing that will allow this sample to run as intended. + +
+(see sample policy) +
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Publish",
+        "iot:Receive"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:topic/test/topic"
+      ]
+    },
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Subscribe"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:topicfilter/test/topic"
+      ]
+    },
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Connect"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:client/test-*"
+      ]
+    }
+  ]
+}
+
+ +Replace with the following with the data from your AWS account: +* ``: The AWS IoT Core region where you created your AWS IoT Core thing you wish to use with this sample. For example `us-east-1`. +* ``: Your AWS IoT Core account ID. This is the set of numbers in the top right next to your AWS account name when using the AWS IoT Core website. + +Note that in a real application, you may want to avoid the use of wildcards in your ClientID or use them selectively. Please follow best practices when working with AWS on production applications using the SDK. Also, for the purposes of this sample, please make sure your policy allows a client ID of `test-*` to connect or use `--client_id ` to send the client ID your policy supports. + +
+ +## How to run + +To Run this sample from the `samples` folder, use the following command: + +```sh +# For Windows: replace 'python3' with 'python' and '/' with '\' +python3 pubsub.py --endpoint --cert --key +``` + +You can also pass a Certificate Authority file (CA) if your certificate and key combination requires it: + +```sh +# For Windows: replace 'python3' with 'python' and '/' with '\' +python3 pubsub.py --endpoint --cert --key --ca_file +``` diff --git a/samples/pubsub.py b/samples/pubsub.py new file mode 100644 index 00000000..924cb140 --- /dev/null +++ b/samples/pubsub.py @@ -0,0 +1,183 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0. + +from awsiot import mqtt5_client_builder +from awscrt import mqtt5 +import threading +import time +# This sample uses the Message Broker for AWS IoT to send and receive messages +# through an MQTT connection. On startup, the device connects to the server, +# subscribes to a topic, and begins publishing messages to that topic. +# The device should receive those same messages back from the message broker, +# since it is subscribed to that same topic. + +# --------------------------------- ARGUMENT PARSING ----------------------------------------- +import argparse +import uuid + +parser = argparse.ArgumentParser( + description="MQTT5 X509 Sample (mTLS)", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, +) +required = parser.add_argument_group("required arguments") +optional = parser.add_argument_group("optional arguments") + +# Required Arguments +required.add_argument("--endpoint", required=True, metavar="", dest="input_endpoint", + help="IoT endpoint hostname") +required.add_argument("--cert", required=True, metavar="", dest="input_cert", + help="Path to the certificate file to use during mTLS connection establishment") +required.add_argument("--key", required=True, metavar="", dest="input_key", + help="Path to the private key file to use during mTLS connection establishment") + +# Optional Arguments +optional.add_argument("--client_id", metavar="", dest="input_clientId", default=f"mqtt5-sample-{uuid.uuid4().hex[:8]}", + help="Client ID") +optional.add_argument("--topic", metavar="", default="test/topic", dest="input_topic", + help="Topic") +optional.add_argument("--message", metavar="", default="Hello from mqtt5 sample", dest="input_message", + help="Message payload") +optional.add_argument("--count", type=int, metavar="", default=5, dest="input_count", + help="Messages to publish (0 = infinite)") +optional.add_argument("--ca_file", metavar="", dest="input_ca", default=None, + help="Path to root CA file") + +# args contains all the parsed commandline arguments used by the sample +args = parser.parse_args() +# --------------------------------- ARGUMENT PARSING END ----------------------------------------- + +TIMEOUT = 100 +message_count = args.input_count +message_topic = args.input_topic +message_string = args.input_message +# Events used within callbacks to progress sample +connection_success_event = threading.Event() +stopped_event = threading.Event() +received_all_event = threading.Event() +received_count = 0 + + +# Callback when any publish is received +def on_publish_received(publish_packet_data): + publish_packet = publish_packet_data.publish_packet + print("==== Received message from topic '{}': {} ====\n".format( + publish_packet.topic, publish_packet.payload.decode('utf-8'))) + + # Track number of publishes received + global received_count + received_count += 1 + if received_count == args.input_count: + received_all_event.set() + + +# Callback for the lifecycle event Stopped +def on_lifecycle_stopped(lifecycle_stopped_data: mqtt5.LifecycleStoppedData): + print("Lifecycle Stopped\n") + stopped_event.set() + + +# Callback for lifecycle event Attempting Connect +def on_lifecycle_attempting_connect(lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData): + print("Lifecycle Connection Attempt\nConnecting to endpoint: '{}' with client ID'{}'".format( + args.input_endpoint, args.input_clientId)) + + +# Callback for the lifecycle event Connection Success +def on_lifecycle_connection_success(lifecycle_connect_success_data: mqtt5.LifecycleConnectSuccessData): + connack_packet = lifecycle_connect_success_data.connack_packet + print("Lifecycle Connection Success with reason code:{}\n".format( + repr(connack_packet.reason_code))) + connection_success_event.set() + + +# Callback for the lifecycle event Connection Failure +def on_lifecycle_connection_failure(lifecycle_connection_failure: mqtt5.LifecycleConnectFailureData): + print("Lifecycle Connection Failure with exception:{}".format( + lifecycle_connection_failure.exception)) + + +# Callback for the lifecycle event Disconnection +def on_lifecycle_disconnection(lifecycle_disconnect_data: mqtt5.LifecycleDisconnectData): + print("Lifecycle Disconnected with reason code:{}".format( + lifecycle_disconnect_data.disconnect_packet.reason_code if lifecycle_disconnect_data.disconnect_packet else "None")) + + +if __name__ == '__main__': + print("\nStarting MQTT5 X509 PubSub Sample\n") + message_count = args.input_count + message_topic = args.input_topic + message_string = args.input_message + + # Create MQTT5 client using mutual TLS via X509 Certificate and Private Key + print("==== Creating MQTT5 Client ====\n") + client = mqtt5_client_builder.mtls_from_path( + endpoint=args.input_endpoint, + cert_filepath=args.input_cert, + pri_key_filepath=args.input_key, + on_publish_received=on_publish_received, + on_lifecycle_stopped=on_lifecycle_stopped, + on_lifecycle_attempting_connect=on_lifecycle_attempting_connect, + on_lifecycle_connection_success=on_lifecycle_connection_success, + on_lifecycle_connection_failure=on_lifecycle_connection_failure, + on_lifecycle_disconnection=on_lifecycle_disconnection, + client_id=args.input_clientId, + ca_filepath=args.input_ca) + + # Start the client, instructing the client to desire a connected state. The client will try to + # establish a connection with the provided settings. If the client is disconnected while in this + # state it will attempt to reconnect automatically. + print("==== Starting client ====") + client.start() + + # We await the `on_lifecycle_connection_success` callback to be invoked. + if not connection_success_event.wait(TIMEOUT): + raise TimeoutError("Connection timeout") + + # Subscribe + print("==== Subscribing to topic '{}' ====".format(message_topic)) + subscribe_future = client.subscribe(subscribe_packet=mqtt5.SubscribePacket( + subscriptions=[mqtt5.Subscription( + topic_filter=message_topic, + qos=mqtt5.QoS.AT_LEAST_ONCE)] + )) + suback = subscribe_future.result(TIMEOUT) + print("Suback received with reason code:{}\n".format(suback.reason_codes)) + + # Publish + if message_count == 0: + print("==== Sending messages until program killed ====\n") + else: + print("==== Sending {} message(s) ====\n".format(message_count)) + + publish_count = 1 + while (publish_count <= message_count) or (message_count == 0): + message = f"{message_string} [{publish_count}]" + print(f"Publishing message to topic '{message_topic}': {message}") + publish_future = client.publish(mqtt5.PublishPacket( + topic=message_topic, + payload=message, + qos=mqtt5.QoS.AT_LEAST_ONCE + )) + publish_completion_data = publish_future.result(TIMEOUT) + print("PubAck received with {}\n".format(repr(publish_completion_data.puback.reason_code))) + time.sleep(1.5) + publish_count += 1 + + received_all_event.wait(TIMEOUT) + print("{} message(s) received.\n".format(received_count)) + + # Unsubscribe + print("==== Unsubscribing from topic '{}' ====".format(message_topic)) + unsubscribe_future = client.unsubscribe(unsubscribe_packet=mqtt5.UnsubscribePacket( + topic_filters=[message_topic])) + unsuback = unsubscribe_future.result(TIMEOUT) + print("Unsubscribed with {}\n".format(unsuback.reason_codes)) + + # Stop the client. Instructs the client to disconnect and remain in a disconnected state. + print("==== Stopping Client ====") + client.stop() + + if not stopped_event.wait(TIMEOUT): + raise TimeoutError("Stop timeout") + + print("==== Client Stopped! ====")