diff --git a/august7_mqtt/README.md b/august7_mqtt/README.md new file mode 100644 index 0000000..fe42c61 --- /dev/null +++ b/august7_mqtt/README.md @@ -0,0 +1,164 @@ +# MQTT for Fun + +In this workshop, we will learn how to use MQTT for connected devices. + +* Install Mosquitto Broker and Client +* Adjust ports/firewalls + +On the hardware side, we will learn to install and use an MQTT client on diverse devices. + +* Install MicroPython client on RP2040 +* Install GDScript client on Godot Game Engine + +## Software Required + +* Git (for downloading project repo) +* Thonny (python code editor with RP2040 support) + +### Optional +* Raspberry Pi running Raspbian +* Godot Game Engine + +## Intro + +What is MQTT? MQTT is a very lightweight protocol for sending messages around a network. It is simple and easy to work with, involving the use of a "broker" that centralizes the messages, and "clients" that can receive and send the messages. + +Because MQTT is so lightweight, it works well on devices with limited resources, such as microcontrollers. It has become one of the most popular protocols for internet-of-things and edge devices. + +# The MQTT Broker + +While there are many software packages for installing an MQTT broker, probably the most popular is Mosquitto. It is available for most operating systems, and comes with both broker and client software. + +Installation on Linux (debian): + +```sudo apt install mosquitto mosquitto-clients``` + +Once you have installed the broker, it will automatically start as a service that will run in the background, and clients can connect to it. + +In Mosquitto version 2+ extra security was implemented requiring clients to connect with authentication. This is overkill for many purposes, so we will allow clients to connect anonymously by editing the configuration file. + +On Ubuntu: + +Locate `/etc/mosquitto/mosquitto.conf` + +Open the file for editing: + +`sudo nano mosquitto.conf` + +Add the following line: + +`allow_anonymous true` + +However, there may be other factors in your OS setup that can affect the clients from connecting - most importantly your firewall settings. + +In Ubuntu Linux, the most common firewall software is UFW. This comes by default with that distribution. The firewall can be configured to allow or disallow traffic on specific ports. + +The port used by default for the Mosquitto broker is 1883. We must ensure that this is available for clients to connect to. + +The command to see if UFW is running and what ports it is monitoring looks like this: + +`sudo ufw status` + +To allow connections through the firewall on port 1883 (the MQTT port) you can write a rule for UFW + +`sudo ufw allow 1883/tcp` + +Note that you can get a lot more specific with UFW rules, including limiting access to certain applications or IP addresses. + +# Using the MQTT Client + +Once the broker is up and running, and the firewall is configured to allow connections, clients can connect. This is very easy to test on the local machine, because the Mosquitto installation come with a client you can use from the command line. + +MQTT organizes messaging by topics that clients can both publish to and subscribe to for updates. The topics can include anything. + +On the machine where we installed the broker we can test by subscribing and publishing to the "godot" topic. + +`mosquitto_sub -v -h 127.0.0.1 -t "godot/#"` + +This subscribes the mosquitto client to hear updates from the broker running on 127.0.0.1 (the local machine) for any message in the "godot" topic. + +In a new window, lets test publishing a message. + +`mosquitto_pub -h 127.0.0.1 -t "godot/abcd" -m "Bingo!"` + +In the first window, you'll see the message appear. + +From this you can see that devices can be either publishers, subscribers, or both. Any devices on the same network can use their client to communicate back and forth with this messaging. + +### What you can do with this: + +MQTT clients are pretty ubiquitous since they are just based on TCP, a very common protocol. + +If the client can be made to run on something like a microcontroller, messaging can be used to do things such boards are great at - controlling hardware. + +In the next section, we will get an MQTT client working on the Raspberry Pi Pico W, a popular microcontroller built on the RP2040 chip. + +To achieve this, we will need to: + +* Flash the board with the language of choice (we are using MicroPython) +* Install an MQTT client written in that language +* Write code that connects to the network and uses the client code to connect to the broker +* Interpret the MQTT messaging into control of hardware + +# MQTT on MicroPython + +Let's begin by prepping the board for programming + +In the 'resources' folder you will find specific instructions, along with the UF2 file that is needed to flash the board with MicroPython. Once that is done you'll need to have a code editor that has support for connecting to the board. + +Thonny is the code editor commonly used for this. + +If you do not have Thonny installed, you can follow these instructions. + +Ubuntu: + +`sudo apt install Thonny` + +Raspbian: + +If you are using a Raspberry Pi, Thonny should already be installed. (You will be limited to Python 3.9 but this should not cause any problems) + +Once completed you should be able to connect to the RP2040 with Thonny. + +In the bottom-right corner of the software you will see the currently configured Python interpreter. Click on that to edit it. + +You will be taken to options, where you can find "MicroPython for Rpi Pico". When you select this option, the code your write in Thonny will be executed on the Pico board rather than locally. You're programming the board! + +In the "rp2040_client" folder you will find a folder "lib". Inside of that you will find the folder "mqtt", and a python file called `simple.py` + +The lib folder is important. It is where the pico will look for any libraries you have installed on the board. You will need to move the lib folder onto the Pico using Thonny. The easiest way to do this save-as. Create a new file, and save it into "lib/mqtt" as `simple.py`. You can then just copy the code into that file. + +You are now ready to write the code that will use the MQTT client. Take a look in the `main.py` for an example. + +The example uses the MQTT client to subscribe to the "godot" topic. Note that you will need to edit the code to fill in the details about your wifi network. + +## The Godot Example + +Once you have the MQTT client subscribed on the Pico board, you could consider another interesting use of MQTT. + +In the `godot_client` folder you will find a Godot project you can import into the game engine to test your MQTT connection. + +Like the project we put on the Pico board, there is a lightweight client that connects when you run the application. + +If both the Godot client and the Pico client are connected to the same broker on the same network, you should be able to send messages between them. + +This opens up a whole world of possibilities! + +# Suggested Projects + +**simple** + +Send a message from Godot to the Pico that causes the pico to toggle an LED light. + +**intermediate** + +Collect sensor data from the Pico and provide this in messages to Godot. Create a visualisation for the data using Godot's animation capability. + +## Resources + +* Mosquitto man pages https://mosquitto.org/man/ +* Godot MQTT client https://github.com/goatchurchprime/godot-mqtt +* Install broker/client on Linux http://www.steves-internet-guide.com/install-mosquitto-linux/ +* Install broker/client on RPI https://randomnerdtutorials.com/how-to-install-mosquitto-broker-on-raspberry-pi/ +* Core Electronics RP2040 guide https://core-electronics.com.au/guides/getting-started-with-mqtt-on-raspberry-pi-pico-w-connect-to-the-internet-of-things/ +* RP2040 client https://github.com/micropython/micropython-lib/tree/master/micropython/umqtt.simple diff --git a/august7_mqtt/godot_client/HighScoreExample.gd b/august7_mqtt/godot_client/HighScoreExample.gd new file mode 100644 index 0000000..c4dae2d --- /dev/null +++ b/august7_mqtt/godot_client/HighScoreExample.gd @@ -0,0 +1,68 @@ +extends Control + +var brokerurl = "wss://test.mosquitto.org:8081" +var possibleusernames = ["Alice", "Beth", "Cath", "Dan", "Earl", "Fred", "George", "Harry", "Ivan", + "John", "Kevin", "Larry", "Martin", "Oliver", "Peter", "Quentin", "Robert", + "Samuel", "Thomas", "Ulrik", "Victor", "Wayne", "Xavier", "Youngs", "Zephir"] + +var fetchingscores = true +var highscores = [ ] +var regexscores = RegEx.new() + +func _ready(): + randomize() + $VBox/HBoxPlayername/LineEdit.text = possibleusernames[randi()%len(possibleusernames)] + $VBox/HBoxHighScore/LineEdit.text = "%d" % (randi()%1000) + regexscores.compile("^\\w+/(\\w+)/score") + #$MQTT.verbose_level = 0 + _on_fetch_scores_pressed() + +func _on_mqtt_broker_connected(): + # this quality of service means we get an acknowledgement message back from the broker + # which we can use to disconnect from the broker when the work is done + var qos = 1 + + if fetchingscores: + $MQTT.subscribe("%s/+/score" % $VBox/HBoxGamename/LineEdit.text) + + # this follow-on message is acknowledged after all subscribed retained messages have arrived + $MQTT.publish("%s" % $VBox/HBoxGamename/LineEdit.text, "acknowledgemessage", false, qos) + + else: + var topic = "%s/%s/score" % [$VBox/HBoxGamename/LineEdit.text, $VBox/HBoxPlayername/LineEdit.text] + $MQTT.publish(topic, $VBox/HBoxHighScore/LineEdit.text, true, qos) + +func _on_mqtt_broker_connection_failed(): + print("Connection failed") + +func _on_send_score_pressed(): + fetchingscores = false + $MQTT.connect_to_broker(brokerurl) + +func _on_fetch_scores_pressed(): + fetchingscores = true + highscores = [ ] + $MQTT.connect_to_broker(brokerurl) + +func _on_mqtt_broker_disconnected(): + print("disconnected") + +func _on_mqtt_publish_acknowledge(pid): + print("Publish message acknowledged ", pid) + $MQTT.disconnect_from_server() + if fetchingscores: + highscores.sort() + highscores.reverse() + $VBox/HBoxTopscores/RichTextLabel.clear() + $VBox/HBoxTopscores/RichTextLabel.append_text("[u]Top ten scores[/u]\n") + while len(highscores) < 10: + highscores.append([0, " --- "]) + for i in range(10): + $VBox/HBoxTopscores/RichTextLabel.append_text("%d. [b]%s:[/b] %d\n" % [i+1, highscores[i][1], highscores[i][0]]) + print(highscores) + +func _on_mqtt_received_message(topic, message): + var rem = regexscores.search(topic) + if rem: + var playername = rem.get_string(1) + highscores.append([int(message), playername]) diff --git a/august7_mqtt/godot_client/LICENSE.md b/august7_mqtt/godot_client/LICENSE.md new file mode 100644 index 0000000..5b3960d --- /dev/null +++ b/august7_mqtt/godot_client/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Julian Todd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/august7_mqtt/godot_client/README.md b/august7_mqtt/godot_client/README.md new file mode 100644 index 0000000..7928518 --- /dev/null +++ b/august7_mqtt/godot_client/README.md @@ -0,0 +1,154 @@ +# MQTT + +Godot 4 addon - to implement MQTT + +## Installation + +Clone the repo and copy the `addons/mqtt` folder into your project's `addons` folder +(or use the AssetLib to install when + +## Applications + +**MQTT** is probably the simplest known networking protocol you can use +to communicate between systems, which is why it is often used by +low power IoT (Internet of Things) devices. + +In spite of its simplicity, MQTT is extremely flexible and has the +advantage of being standardized enough that you don't need to write +your own server-side software. You can often get by with "borrowing " the use of a public server on the internet (see [test.mosquitto.org](https://test.mosquitto.org/)). + +Uses for MQTT in Godot are chat rooms, receiving and sending messages to physical devices, high score tables, remote monitoring of runtime metrics (eg framerate) of an alpha release for quality control, and as a +[signalling server](https://docs.godotengine.org/en/stable/tutorials/networking/webrtc.html#id1) +for the powerful WebRTC peer-to-peer networking protocol. + +Here is a [talk](https://www.youtube.com/watch?v=en9fMP4g9y8) from +GodotCon 2021 that included demo of controlling a wheeled robot from +the Dodge the Creeps game. + +[![image](https://github.com/goatchurchprime/godot-mqtt/assets/677254/593b8a53-801e-480f-9812-cfc4e9ae360b)](https://www.youtube.com/watch?v=en9fMP4g9y8) + + + +The **mqttexample** project in this repo is good for +experimenting and exploring the features. + +![image](https://github.com/goatchurchprime/godot-mqtt/assets/677254/264473c6-6ad1-4a87-8bb5-49fd28789bed) + +There's also an even simpler **HighScoreExample** scene that demonstrates how to implement a list of +global high scores in a game. + +## The protocol + +The easiest way to understand this protocol (without using Godot or this plugin) is to [install mosquitto](https://mosquitto.org/download/), +and open two command line consoles on your computer. + +Run the following command in the first console: + +* `mosquitto_sub -v -h test.mosquitto.org -t "godot/#"` + +and then run this other command in the second console: + +* `mosquitto_pub -h test.mosquitto.org -t "godot/abcd" -m "Bingo!"` + +The first console should have now printed: + +* `godot/abcd Bingo!` + +*What's going on here?* + +The first command connected to the broker `test.mosquitto.org` +and subscribed to the topic `"godot/#"` where the `'#'` is a wild-card +that matches the remained of the topic string. + +The second command publishes the message `"Bingo!"` to the topic `"godot/abcd"` +on the broker at the address `test.mosquitto.org`, which is picked up by the first +command since it matches the pattern. + +Now you understand the basics, you can do the same thing on a webpage +version of the mqtt client at [hivemq.com](https://www.hivemq.com/demos/websocket-client/). +(The ClientID is a unique identifier for the client, often randomly generated.) + +### Advanced features + +To make this protocol insanely useful, there are two further features: + +When the **`retain`** flag is set to true, the broker not only sends the message to +all the subscribers, it also keeps a copy and sends it to any new subscriber that matches the topic. +This allows for persistent data (eg high scores) to be recorded and updated on the +broker. To delete a retained message, publish an empty message to its topic. + +When a new connection is made to the broker, a **`lastwill`** topic and message can optionally be included. +This message will be published automatically on disconnect. The lastwill message +can also have its `retain` flag set. + +These two features can be combined to provide a connection status feature for a player instance +by first executing: +* `set_last_will( topic="playerstates/myplayerid", msg="disconnected", retain=true )` + +before connecting to the broker. + +Then the first message you send after successfully connecting to the broker is: +* `publish( topic="playerstates/myplayerid", msg="readytoplay", retain=true )` + +This persistent message will be automatically over-written at disconnect. + +At any time when a player elsewhere connects to this broker and subscribes +to the topic `"playerstates/+"` the the messages returned will +correctly give their online statuses. + +There is final setting in the publish fuction, **`qos`** for "Quality of Service". +This tells the broker whether you want an acknowledgement that the message +has gotten through to it (qos=1) as well as enforce confirmation +and de-duplication feature (qos=2). This has not been fully implemented in this +library. + + +## Usage + +* `$MQTT.set_last_will(topic, msg, retain=false, qos=0)` + +Must be set before connection is made. + +* `$MQTT.connect_to_broker(brokerurl)` + +This URL should contain protocol and port, eg +`"tcp://test.mosquitto.org:1883/"` for the basic default setting +or for secure websocket `"wss://test.mosquitto.org:8081/"`. + +Some MQTT brokers do not have the WebSocket interface enabled +since the primary interface is TCP. WebSockets are, however, +necessary to get round restrictions in HTML5 not having direct access +to TCP sockets. + +* `$MQTT.subscribe(topic, qos=0)` + +This subscribes to a topic. All subscribed messages go to the +same `received_message`. + +* `$MQTT.unsubscribe(topic)` + +Little used since all topics are unsubscribed on disconnect. +The topic has to match exactly to one that was subscribed. +(You cannot use this for include/exclude rules.) + +* `$MQTT.publish(topic, msg, retain=false, qos=0)` + +Publish a message to the broker. + +### @export parameters + +* @export var client_id = "" +* @export var verbose_level = 2 # 0 quiet, 1 connections and subscriptions, 2 all messages +* @export var binarymessages = false +* @export var pinginterval = 30 + +### Signals + +* received_message(topic, message) +* signal broker_connected() +* signal broker_disconnected() +* signal broker_connection_failed() +* signal broker_connection_failed() +* signal publish_acknowledge(id) + + diff --git a/august7_mqtt/godot_client/addons/mqtt/mqtt.gd b/august7_mqtt/godot_client/addons/mqtt/mqtt.gd new file mode 100644 index 0000000..6c1adc7 --- /dev/null +++ b/august7_mqtt/godot_client/addons/mqtt/mqtt.gd @@ -0,0 +1,478 @@ +extends Node + +# MQTT client implementation in GDScript +# Loosely based on https://github.com/pycom/pycom-libraries/blob/master/lib/mqtt/mqtt.py +# and initial work by Alex J Lennon +# but then heavily rewritten to follow https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html + +# mosquitto_sub -h test.mosquitto.org -v -t "metest/#" +# mosquitto_pub -h test.mosquitto.org -t "metest/retain" -m "retained message" -r + +@export var client_id = "" +@export var verbose_level = 2 # 0 quiet, 1 connections and subscriptions, 2 all messages +@export var binarymessages = false +@export var pinginterval = 30 + +var socket = null +var sslsocket = null +var websocket = null + +const BCM_NOCONNECTION = 0 +const BCM_WAITING_WEBSOCKET_CONNECTION = 1 +const BCM_WAITING_SOCKET_CONNECTION = 2 +const BCM_WAITING_SSL_SOCKET_CONNECTION = 3 +const BCM_FAILED_CONNECTION = 5 +const BCM_WAITING_CONNMESSAGE = 10 +const BCM_WAITING_CONNACK = 19 +const BCM_CONNECTED = 20 + +var brokerconnectmode = BCM_NOCONNECTION + +var regexbrokerurl = RegEx.new() + +const DEFAULTBROKERPORT_TCP = 1883 +const DEFAULTBROKERPORT_SSL = 8886 +const DEFAULTBROKERPORT_WS = 8080 +const DEFAULTBROKERPORT_WSS = 8081 + +const CP_PINGREQ = 0xc0 +const CP_PINGRESP = 0xd0 +const CP_CONNACK = 0x20 +const CP_CONNECT = 0x10 +const CP_PUBLISH = 0x30 +const CP_SUBSCRIBE = 0x82 +const CP_UNSUBSCRIBE = 0xa2 +const CP_PUBREC = 0x40 +const CP_SUBACK = 0x90 +const CP_UNSUBACK = 0xb0 + +var pid = 0 +var user = null +var pswd = null +var keepalive = 120 +var lw_topic = null +var lw_msg = null +var lw_qos = 0 +var lw_retain = false + +signal received_message(topic, message) +signal broker_connected() +signal broker_disconnected() +signal broker_connection_failed() +signal publish_acknowledge(pid) + +var receivedbuffer : PackedByteArray = PackedByteArray() + +var common_name = null + +func senddata(data): + var E = 0 + if sslsocket != null: + E = sslsocket.put_data(data) + elif socket != null: + E = socket.put_data(data) + elif websocket != null: + E = websocket.put_packet(data) + if E != 0: + print("bad senddata packet E=", E) + +func receiveintobuffer(): + if sslsocket != null: + var sslsocketstatus = sslsocket.get_status() + if sslsocketstatus == StreamPeerTLS.STATUS_CONNECTED or sslsocketstatus == StreamPeerTLS.STATUS_HANDSHAKING: + sslsocket.poll() + var n = sslsocket.get_available_bytes() + if n != 0: + var sv = sslsocket.get_data(n) + assert (sv[0] == 0) # error code + receivedbuffer.append_array(sv[1]) + + elif socket != null and socket.get_status() == StreamPeerTCP.STATUS_CONNECTED: + socket.poll() + var n = socket.get_available_bytes() + if n != 0: + var sv = socket.get_data(n) + assert (sv[0] == 0) # error code + receivedbuffer.append_array(sv[1]) + + + elif websocket != null: + websocket.poll() + while websocket.get_available_packet_count() != 0: + receivedbuffer.append_array(websocket.get_packet()) + +var pingticksnext0 = 0 + +func _process(delta): + if brokerconnectmode == BCM_NOCONNECTION: + pass + elif brokerconnectmode == BCM_WAITING_WEBSOCKET_CONNECTION: + websocket.poll() + var websocketstate = websocket.get_ready_state() + if websocketstate == WebSocketPeer.STATE_CLOSED: + if verbose_level: + print("WebSocket closed with code: %d, reason %s." % [websocket.get_close_code(), websocket.get_close_reason()]) + brokerconnectmode = BCM_FAILED_CONNECTION + emit_signal("broker_connection_failed") + elif websocketstate == WebSocketPeer.STATE_OPEN: + brokerconnectmode = BCM_WAITING_CONNMESSAGE + if verbose_level: + print("Websocket connection now open") + + elif brokerconnectmode == BCM_WAITING_SOCKET_CONNECTION: + socket.poll() + var socketstatus = socket.get_status() + if socketstatus == StreamPeerTCP.STATUS_ERROR: + if verbose_level: + print("TCP socket error") + brokerconnectmode = BCM_FAILED_CONNECTION + emit_signal("broker_connection_failed") + if socketstatus == StreamPeerTCP.STATUS_CONNECTED: + brokerconnectmode = BCM_WAITING_CONNMESSAGE + + elif brokerconnectmode == BCM_WAITING_SSL_SOCKET_CONNECTION: + socket.poll() + var socketstatus = socket.get_status() + if socketstatus == StreamPeerTCP.STATUS_ERROR: + if verbose_level: + print("TCP socket error before SSL") + brokerconnectmode = BCM_FAILED_CONNECTION + emit_signal("broker_connection_failed") + if socketstatus == StreamPeerTCP.STATUS_CONNECTED: + if sslsocket == null: + sslsocket = StreamPeerTLS.new() + if verbose_level: + print("Connecting socket to SSL with common_name=", common_name) + var E3 = sslsocket.connect_to_stream(socket, common_name) + if E3 != 0: + print("bad sslsocket.connect_to_stream E=", E3) + brokerconnectmode = BCM_FAILED_CONNECTION + emit_signal("broker_connection_failed") + sslsocket = null + if sslsocket != null: + sslsocket.poll() + var sslsocketstatus = sslsocket.get_status() + if sslsocketstatus == StreamPeerTLS.STATUS_CONNECTED: + brokerconnectmode = BCM_WAITING_CONNMESSAGE + elif sslsocketstatus >= StreamPeerTLS.STATUS_ERROR: + print("bad sslsocket.connect_to_stream") + emit_signal("broker_connection_failed") + + elif brokerconnectmode == BCM_WAITING_CONNMESSAGE: + senddata(firstmessagetoserver()) + brokerconnectmode = BCM_WAITING_CONNACK + + elif brokerconnectmode == BCM_WAITING_CONNACK or brokerconnectmode == BCM_CONNECTED: + receiveintobuffer() + while wait_msg(): + pass + if brokerconnectmode == BCM_CONNECTED and pingticksnext0 < Time.get_ticks_msec(): + pingreq() + pingticksnext0 = Time.get_ticks_msec() + pinginterval*1000 + + elif brokerconnectmode == BCM_FAILED_CONNECTION: + cleanupsockets() + +func _ready(): + regexbrokerurl.compile('^(tcp://|wss://|ws://|ssl://)?([^:\\s]+)(:\\d+)?(/\\S*)?$') + if client_id == "": + randomize() + client_id = "rr%d" % randi() + +func set_last_will(stopic, smsg, retain=false, qos=0): + assert((0 <= qos) and (qos <= 2)) + assert(stopic) + self.lw_topic = stopic.to_ascii_buffer() + self.lw_msg = smsg if binarymessages else smsg.to_ascii_buffer() + self.lw_qos = qos + self.lw_retain = retain + if verbose_level: + print("LASTWILL%s topic=%s msg=%s" % [ " " if retain else "", stopic, smsg]) + +func set_user_pass(suser, spswd): + if suser != null: + self.user = suser.to_ascii_buffer() + self.pswd = spswd.to_ascii_buffer() + else: + self.user = null + self.pswd = null + +func firstmessagetoserver(): + var clean_session = true + var msg = PackedByteArray() + msg.append(CP_CONNECT); + msg.append(0x00); + msg.append(0x00); + msg.append(0x04); + msg.append_array("MQTT".to_ascii_buffer()); + msg.append(0x04); + msg.append(0x02); + msg.append(0x00); + msg.append(0x3C); + + msg[1] = 10 + 2 + len(self.client_id) + msg[9] = (1<<1) if clean_session else 0 + if self.user != null: + msg[1] += 2 + len(self.user) + 2 + len(self.pswd) + msg[9] |= 0xC0 + if self.keepalive: + assert(self.keepalive < 65536) + msg[10] |= self.keepalive >> 8 + msg[11] |= self.keepalive & 0x00FF + if self.lw_topic: + msg[1] += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) + msg[9] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 + msg[9] |= 1<<5 if self.lw_retain else 0 + + msg.append(len(self.client_id) >> 8) + msg.append(self.client_id.length() & 0xFF) + msg.append_array(self.client_id.to_ascii_buffer()) + if self.lw_topic: + msg.append(len(self.lw_topic) >> 8) + msg.append(len(self.lw_topic) & 0xFF) + msg.append_array(self.lw_topic) + msg.append(len(self.lw_msg) >> 8) + msg.append(len(self.lw_msg) & 0xFF) + msg.append_array(self.lw_msg) + if self.user != null: + msg.append(len(self.user) >> 8) + msg.append(len(self.user) & 0xFF) + msg.append_array(self.user) + msg.append(len(self.pswd) >> 8) + msg.append(len(self.pswd) & 0xFF) + msg.append_array(self.pswd) + return msg + +func cleanupsockets(retval=false): + if verbose_level: + print("cleanupsockets") + if socket: + if sslsocket: + sslsocket = null + socket.disconnect_from_host() + socket = null + else: + assert (sslsocket == null) + + if websocket: + websocket.close() + websocket = null + brokerconnectmode = BCM_NOCONNECTION + return retval + +func connect_to_broker(brokerurl): + assert (brokerconnectmode == BCM_NOCONNECTION) + var brokermatch = regexbrokerurl.search(brokerurl) + if brokermatch == null: + print("ERROR: unrecognized brokerurl pattern:", brokerurl) + return cleanupsockets(false) + var brokercomponents = brokermatch.strings + var brokerprotocol = brokercomponents[1] + var brokerserver = brokercomponents[2] + var iswebsocket = (brokerprotocol == "ws://" or brokerprotocol == "wss://") + var isssl = (brokerprotocol == "ssl://" or brokerprotocol == "wss://") + var brokerport = ((DEFAULTBROKERPORT_WSS if isssl else DEFAULTBROKERPORT_WS) if iswebsocket else (DEFAULTBROKERPORT_SSL if isssl else DEFAULTBROKERPORT_TCP)) + if brokercomponents[3]: + brokerport = int(brokercomponents[3].substr(1)) + var brokerpath = brokercomponents[4] if brokercomponents[4] else "" + + common_name = null + if iswebsocket: + websocket = WebSocketPeer.new() + websocket.supported_protocols = PackedStringArray(["mqttv3.1"]) + var websocketurl = ("wss://" if isssl else "ws://") + brokerserver + ":" + str(brokerport) + brokerpath + if verbose_level: + print("Connecting to websocketurl: ", websocketurl) + var E = websocket.connect_to_url(websocketurl) + if E != 0: + print("ERROR: websocketclient.connect_to_url Err: ", E) + return cleanupsockets(false) + print("Websocket get_requested_url ", websocket.get_requested_url()) + brokerconnectmode = BCM_WAITING_WEBSOCKET_CONNECTION + + else: + socket = StreamPeerTCP.new() + if verbose_level: + print("Connecting to %s:%s" % [brokerserver, brokerport]) + var E = socket.connect_to_host(brokerserver, brokerport) + if E != 0: + print("ERROR: socketclient.connect_to_url Err: ", E) + return cleanupsockets(false) + if isssl: + brokerconnectmode = BCM_WAITING_SSL_SOCKET_CONNECTION + common_name = brokerserver + else: + brokerconnectmode = BCM_WAITING_SOCKET_CONNECTION + + return true + + +func disconnect_from_server(): + if brokerconnectmode == BCM_CONNECTED: + senddata(PackedByteArray([0xE0, 0x00])) + emit_signal("broker_disconnected") + cleanupsockets() + +func publish(stopic, smsg, retain=false, qos=0): + var msg = smsg.to_ascii_buffer() if not binarymessages else smsg + var topic = stopic.to_ascii_buffer() + + var pkt = PackedByteArray() + pkt.append(CP_PUBLISH | (2 if qos else 0) | (1 if retain else 0)); + pkt.append(0x00); + + var sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + assert(sz < 2097152) + var i = 1 + while sz > 0x7f: + pkt[i] = (sz & 0x7f) | 0x80 + sz >>= 7 + i += 1 + if i + 1 > len(pkt): + pkt.append(0x00); + pkt[i] = sz + + pkt.append(len(topic) >> 8) + pkt.append(len(topic) & 0xFF) + pkt.append_array(topic) + + if qos > 0: + pid += 1 + pkt.append(pid >> 8) + pkt.append(pid & 0xFF) + pkt.append_array(msg) + senddata(pkt) + if verbose_level >= 2: + print("CP_PUBLISH%s%s topic=%s msg=%s" % [ "[%d]"%pid if qos else "", " " if retain else "", stopic, smsg]) + return pid + +func subscribe(stopic, qos=0): + pid += 1 + var topic = stopic.to_ascii_buffer() + var length = 2 + 2 + len(topic) + 1 + var msg = PackedByteArray() + msg.append(CP_SUBSCRIBE); + msg.append(length) + msg.append(pid >> 8) + msg.append(pid & 0xFF) + msg.append(len(topic) >> 8) + msg.append(len(topic) & 0xFF) + msg.append_array(topic) + msg.append(qos); + if verbose_level: + print("SUBSCRIBE[%d] topic=%s" % [pid, stopic]) + senddata(msg) + +func pingreq(): + if verbose_level >= 2: + print("PINGREQ") + senddata(PackedByteArray([CP_PINGREQ, 0x00])) + +func unsubscribe(stopic): + pid += 1 + var topic = stopic.to_ascii_buffer() + var length = 2 + 2 + len(topic) + var msg = PackedByteArray() + msg.append(CP_UNSUBSCRIBE); + msg.append(length) + msg.append(pid >> 8) + msg.append(pid & 0xFF) + msg.append(len(topic) >> 8) + msg.append(len(topic) & 0xFF) + msg.append_array(topic) + if verbose_level: + print("UNSUBSCRIBE[%d] topic=%s" % [pid, stopic]) + senddata(msg) + + +func wait_msg(): + var n = receivedbuffer.size() + if n < 2: + return false + var op = receivedbuffer[0] + var i = 1 + var sz = receivedbuffer[i] & 0x7f + while (receivedbuffer[i] & 0x80): + i += 1 + if i == n: + return false + sz += (receivedbuffer[i] & 0x7f) << ((i-1)*7) + i += 1 + if n < i + sz: + return false + + if op == CP_PINGRESP: + assert (sz == 0) + if verbose_level >= 2: + print("PINGRESP") + + elif op & 0xf0 == 0x30: + var topic_len = (receivedbuffer[i]<<8) + receivedbuffer[i+1] + var im = i + 2 + var topic = receivedbuffer.slice(im, im + topic_len).get_string_from_ascii() + im += topic_len + var pid1 = 0 + if op & 6: + pid1 = (receivedbuffer[im]<<8) + receivedbuffer[im+1] + im += 2 + var data = receivedbuffer.slice(im, i + sz) + var msg = data if binarymessages else data.get_string_from_ascii() + + if verbose_level >= 2: + print("received topic=", topic, " msg=", msg) + emit_signal("received_message", topic, msg) + + if op & 6 == 2: + senddata(PackedByteArray([0x40, 0x02, (pid1 >> 8), (pid1 & 0xFF)])) + elif op & 6 == 4: + assert(0) + + elif op == CP_CONNACK: + assert (sz == 2) + var retcode = receivedbuffer[i+1] + if verbose_level: + print("CONNACK ret=%02x" % retcode) + if retcode == 0x00: + brokerconnectmode = BCM_CONNECTED + emit_signal("broker_connected") + else: + if verbose_level: + print("Bad connection retcode=", retcode) # see https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html + emit_signal("broker_connection_failed") + + elif op == CP_PUBREC: + assert (sz == 2) + var apid = (receivedbuffer[i]<<8) + receivedbuffer[i+1] + if verbose_level >= 2: + print("PUBACK[%d]" % apid) + emit_signal("publish_acknowledgewait_msg", apid) + + elif op == CP_SUBACK: + assert (sz == 3) + var apid = (receivedbuffer[i]<<8) + receivedbuffer[i+1] + if verbose_level: + print("SUBACK[%d] ret=%02x" % [apid, receivedbuffer[i+2]]) + #if receivedbuffer[i+2] == 0x80: + # E = FAILED + + elif op == CP_UNSUBACK: + assert (sz == 2) + var apid = (receivedbuffer[i]<<8) + receivedbuffer[i+1] + if verbose_level: + print("UNSUBACK[%d]" % apid) + + else: + if verbose_level: + print("Unknown MQTT opcode op=%x" % op) + + trimreceivedbuffer(i + sz) + return true + +func trimreceivedbuffer(n): + if n == receivedbuffer.size(): + receivedbuffer = PackedByteArray() + else: + assert (n <= receivedbuffer.size()) + receivedbuffer = receivedbuffer.slice(n) diff --git a/august7_mqtt/godot_client/addons/mqtt/mqtt.tscn b/august7_mqtt/godot_client/addons/mqtt/mqtt.tscn new file mode 100644 index 0000000..b59c1e3 --- /dev/null +++ b/august7_mqtt/godot_client/addons/mqtt/mqtt.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://ktm7k0co2o7l"] + +[ext_resource type="Script" path="res://addons/mqtt/mqtt.gd" id="1_unkqx"] + +[node name="MQTT" type="Node"] +script = ExtResource("1_unkqx") diff --git a/august7_mqtt/godot_client/addons/mqtt/plugin.cfg b/august7_mqtt/godot_client/addons/mqtt/plugin.cfg new file mode 100644 index 0000000..180e833 --- /dev/null +++ b/august7_mqtt/godot_client/addons/mqtt/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="MQTT" +description="MQTT client in GDScript" +author="Julian Todd" +version="1.0" +script="mqtt.gd" diff --git a/august7_mqtt/godot_client/export_presets.cfg b/august7_mqtt/godot_client/export_presets.cfg new file mode 100644 index 0000000..430ebae --- /dev/null +++ b/august7_mqtt/godot_client/export_presets.cfg @@ -0,0 +1,37 @@ +[preset.0] + +name="Web" +platform="Web" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../jjjj/Godot-mqtt.html" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) diff --git a/august7_mqtt/godot_client/high_score_example.tscn b/august7_mqtt/godot_client/high_score_example.tscn new file mode 100644 index 0000000..044ab86 --- /dev/null +++ b/august7_mqtt/godot_client/high_score_example.tscn @@ -0,0 +1,96 @@ +[gd_scene load_steps=3 format=3 uid="uid://8726w0yv488e"] + +[ext_resource type="Script" path="res://HighScoreExample.gd" id="1_a1y26"] +[ext_resource type="PackedScene" uid="uid://ktm7k0co2o7l" path="res://addons/mqtt/mqtt.tscn" id="2_8e2pi"] + +[node name="HighScoreExample" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_a1y26") + +[node name="MQTT" parent="." instance=ExtResource("2_8e2pi")] + +[node name="VBox" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="Label" type="Label" parent="VBox"] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +text = "MQTT Based High Score System" +horizontal_alignment = 1 + +[node name="HBoxGamename" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBox/HBoxGamename"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Game name" + +[node name="LineEdit" type="LineEdit" parent="VBox/HBoxGamename"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "A_Great_Godot_Game" + +[node name="HBoxPlayername" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBox/HBoxPlayername"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Player name" + +[node name="LineEdit" type="LineEdit" parent="VBox/HBoxPlayername"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Jonathan" + +[node name="HBoxHighScore" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBox/HBoxHighScore"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "High score" + +[node name="LineEdit" type="LineEdit" parent="VBox/HBoxHighScore"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "1000" + +[node name="HBoxButtons" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="SendScore" type="Button" parent="VBox/HBoxButtons"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Send score" + +[node name="FetchScores" type="Button" parent="VBox/HBoxButtons"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Fetch Scores" + +[node name="HBoxTopscores" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="RichTextLabel" type="RichTextLabel" parent="VBox/HBoxTopscores"] +custom_minimum_size = Vector2(400, 300) +layout_mode = 2 +bbcode_enabled = true +text = "[u]Top ten[/u] +" + +[connection signal="broker_connected" from="MQTT" to="." method="_on_mqtt_broker_connected"] +[connection signal="broker_connection_failed" from="MQTT" to="." method="_on_mqtt_broker_connection_failed"] +[connection signal="broker_disconnected" from="MQTT" to="." method="_on_mqtt_broker_disconnected"] +[connection signal="publish_acknowledge" from="MQTT" to="." method="_on_mqtt_publish_acknowledge"] +[connection signal="received_message" from="MQTT" to="." method="_on_mqtt_received_message"] +[connection signal="pressed" from="VBox/HBoxButtons/SendScore" to="." method="_on_send_score_pressed"] +[connection signal="pressed" from="VBox/HBoxButtons/FetchScores" to="." method="_on_fetch_scores_pressed"] diff --git a/august7_mqtt/godot_client/icon.png b/august7_mqtt/godot_client/icon.png new file mode 100644 index 0000000..7781b0f Binary files /dev/null and b/august7_mqtt/godot_client/icon.png differ diff --git a/august7_mqtt/godot_client/icon.png.import b/august7_mqtt/godot_client/icon.png.import new file mode 100644 index 0000000..357c3fe --- /dev/null +++ b/august7_mqtt/godot_client/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://3qd766itdoge" +path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.png" +dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/august7_mqtt/godot_client/mqttexample.gd b/august7_mqtt/godot_client/mqttexample.gd new file mode 100644 index 0000000..010af35 --- /dev/null +++ b/august7_mqtt/godot_client/mqttexample.gd @@ -0,0 +1,114 @@ +extends Control + +var receivedmessagecount = 0 + +func _ready(): + connectedactionsactive(false) + +func _on_brokerprotocol_item_selected(index): + # port defaults for [tcp, ssl, ws, wss] used at https://test.mosquitto.org/ + $VBox/HBoxBroker/brokerport.text = "%d" % ([ 1883, 8886, 8080, 8081 ][index]) + +func _on_button_connect_toggled(button_pressed): + if button_pressed: + randomize() + $MQTT.client_id = "s%d" % randi() + + if $VBox/HBoxLastwill/lastwilltopic.text: + $MQTT.set_last_will($VBox/HBoxLastwill/lastwilltopic.text, + $VBox/HBoxLastwill/lastwillmessage.text, + $VBox/HBoxLastwill/lastwillretain.button_pressed) + else: + $MQTT.set_last_will("", "", false) + + if $VBox/HBoxBroker/brokeruser.text: + $MQTT.set_user_pass($VBox/HBoxBroker/brokeruser.text, + $VBox/HBoxBroker/brokerpswd.text) + else: + $MQTT.set_user_pass(null, null) + + $VBox/HBoxBrokerControl/status.text = "connecting..." + var brokerurl = $VBox/HBoxBroker/brokeraddress.text + var protocol = $VBox/HBoxBroker/brokerprotocol.get_item_text($VBox/HBoxBroker/brokerprotocol.selected) + $MQTT.connect_to_broker("%s%s:%s" % [protocol, brokerurl, $VBox/HBoxBroker/brokerport.text]) + + else: + #$VBox/HBoxBrokerControl/status.text = "disconnecting..." + $MQTT.disconnect_from_server() + + +func brokersettingsactive(active): + $VBox/HBoxBroker/brokeraddress.editable = active + $VBox/HBoxBroker/brokerport.editable = active + $VBox/HBoxBroker/brokerprotocol.disabled = not active + $VBox/HBoxLastwill/lastwilltopic.editable = active + $VBox/HBoxLastwill/lastwillmessage.editable = active + $VBox/HBoxLastwill/lastwillretain.disabled = not active + $VBox/HBoxBrokerControl/ButtonConnect.button_pressed = not active + +func connectedactionsactive(active): + $VBox/HBoxSubscriptions/subscribetopic.editable = active + $VBox/HBoxSubscriptions/subscribe.disabled = not active + $VBox/HBoxPublish/publishtopic.editable = active + $VBox/HBoxPublish/publishmessage.editable = active + $VBox/HBoxPublish/publishretain.disabled = not active + $VBox/HBoxPublish/publish.disabled = not active + if not active: + $VBox/HBoxSubscriptions/subscriptions.clear() + $VBox/HBoxSubscriptions/subscriptions.disabled = true + $VBox/HBoxSubscriptions/unsubscribe.disabled = true + +func _on_mqtt_broker_connected(): + $VBox/HBoxBrokerControl/status.text = "connected." + brokersettingsactive(false) + connectedactionsactive(true) + receivedmessagecount = 0 + +func _on_mqtt_broker_disconnected(): + $VBox/HBoxBrokerControl/status.text = "disconnected." + brokersettingsactive(true) + connectedactionsactive(false) + +func _on_mqtt_broker_connection_failed(): + $VBox/HBoxBrokerControl/status.text = "failed." + brokersettingsactive(true) + connectedactionsactive(false) + + +func _on_mqtt_received_message(topic, message): + if receivedmessagecount == 0: + $VBox/subscribedmessages.clear() + receivedmessagecount += 1 + $VBox/subscribedmessages.append_text("[b]%s[/b] %s\n" % [topic, message]) + +func _on_publish_pressed(): + var qos = 0 + $MQTT.publish($VBox/HBoxPublish/publishtopic.text, + $VBox/HBoxPublish/publishmessage.text, + $VBox/HBoxPublish/publishretain.button_pressed, + qos) + +func _on_subscribe_pressed(): + var qos = 0 + var topic = $VBox/HBoxSubscriptions/subscribetopic.text.strip_edges() + $MQTT.subscribe(topic, qos) + for i in range($VBox/HBoxSubscriptions/subscriptions.item_count): + if topic == $VBox/HBoxSubscriptions/subscriptions.get_item_text(i): + return + $VBox/HBoxSubscriptions/subscriptions.add_item(topic) + $VBox/HBoxSubscriptions/subscriptions.select($VBox/HBoxSubscriptions/subscriptions.item_count - 1) + $VBox/HBoxSubscriptions/subscriptions.disabled = false + $VBox/HBoxSubscriptions/unsubscribe.disabled = false + +func _on_unsubscribe_pressed(): + var seloptbutt = $VBox/HBoxSubscriptions/subscriptions + var sel = seloptbutt.selected + var topic = seloptbutt.get_item_text(sel) + $MQTT.unsubscribe(topic) + seloptbutt.remove_item(seloptbutt.selected) + $VBox/HBoxSubscriptions/subscriptions.disabled = (seloptbutt.item_count == 0) + $VBox/HBoxSubscriptions/unsubscribe.disabled = (seloptbutt.item_count == 0) + if seloptbutt.item_count != 0: + seloptbutt.select(min(sel, seloptbutt.item_count - 1)) + + diff --git a/august7_mqtt/godot_client/mqttexample.tscn b/august7_mqtt/godot_client/mqttexample.tscn new file mode 100644 index 0000000..c867530 --- /dev/null +++ b/august7_mqtt/godot_client/mqttexample.tscn @@ -0,0 +1,275 @@ +[gd_scene load_steps=8 format=3 uid="uid://sca5nhn6lprk"] + +[ext_resource type="Script" path="res://mqttexample.gd" id="1_6i2w6"] +[ext_resource type="PackedScene" uid="uid://ktm7k0co2o7l" path="res://addons/mqtt/mqtt.tscn" id="2_t6rxa"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_iasn2"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.215686, 0.4, 0.4, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ppk4t"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.341176, 0.101961, 0.0392157, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8b54y"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.341176, 0.101961, 0.0392157, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xasl1"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.341176, 0.101961, 0.0392157, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_njfq6"] +bg_color = Color(0.0156863, 0.133333, 0.0431373, 1) + +[node name="mqttexample" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -123.0 +offset_top = -64.0 +offset_right = -1023.0 +offset_bottom = -584.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_6i2w6") + +[node name="MQTT" parent="." instance=ExtResource("2_t6rxa")] + +[node name="VBox" type="VBoxContainer" parent="."] +self_modulate = Color(0.0588235, 1, 1, 1) +layout_mode = 0 +offset_left = 137.0 +offset_top = 82.0 +offset_right = 1017.0 +offset_bottom = 576.0 + +[node name="HBoxBrokerControl" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="Labelbroker" type="Label" parent="VBox/HBoxBrokerControl"] +layout_mode = 2 +theme_type_variation = &"HeaderLarge" +text = "MQTT broker:" + +[node name="ButtonConnect" type="CheckButton" parent="VBox/HBoxBrokerControl"] +layout_mode = 2 +size_flags_horizontal = 6 +text = "Connect to broker" + +[node name="status" type="Label" parent="VBox/HBoxBrokerControl"] +layout_mode = 2 +size_flags_horizontal = 2 +theme_override_styles/normal = SubResource("StyleBoxFlat_iasn2") +text = "...Status" +horizontal_alignment = 1 + +[node name="HBoxBroker" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBox/HBoxBroker"] +layout_mode = 2 +text = "URL: " + +[node name="brokeraddress" type="LineEdit" parent="VBox/HBoxBroker"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "broker.hivemq.com" + +[node name="Label4" type="Label" parent="VBox/HBoxBroker"] +layout_mode = 2 +text = "User:" + +[node name="brokeruser" type="LineEdit" parent="VBox/HBoxBroker"] +layout_mode = 2 + +[node name="Label5" type="Label" parent="VBox/HBoxBroker"] +layout_mode = 2 +text = "Pswd:" + +[node name="brokerpswd" type="LineEdit" parent="VBox/HBoxBroker"] +layout_mode = 2 + +[node name="Label2" type="Label" parent="VBox/HBoxBroker"] +layout_mode = 2 +text = "Port: " + +[node name="brokerport" type="LineEdit" parent="VBox/HBoxBroker"] +layout_mode = 2 +tooltip_text = "Port +" +text = "1883" + +[node name="Label3" type="Label" parent="VBox/HBoxBroker"] +layout_mode = 2 +text = "Protocol: " + +[node name="brokerprotocol" type="OptionButton" parent="VBox/HBoxBroker"] +layout_mode = 2 +item_count = 4 +selected = 0 +popup/item_0/text = "tcp://" +popup/item_0/id = 0 +popup/item_1/text = "ssl://" +popup/item_1/id = 1 +popup/item_2/text = "ws://" +popup/item_2/id = 2 +popup/item_3/text = "wss://" +popup/item_3/id = 3 + +[node name="HSeparator" type="HSeparator" parent="VBox"] +custom_minimum_size = Vector2(0, 10) +layout_mode = 2 + +[node name="HBoxLastwill" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="Labellastwill" type="Label" parent="VBox/HBoxLastwill"] +layout_mode = 2 +theme_type_variation = &"HeaderMedium" +text = "Last will:" + +[node name="Label" type="Label" parent="VBox/HBoxLastwill"] +layout_mode = 2 +size_flags_horizontal = 10 +text = "topic:" + +[node name="lastwilltopic" type="LineEdit" parent="VBox/HBoxLastwill"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "godot/myname/mywill" + +[node name="Label2" type="Label" parent="VBox/HBoxLastwill"] +layout_mode = 2 +text = "message:" + +[node name="lastwillmessage" type="LineEdit" parent="VBox/HBoxLastwill"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "goodbye world" + +[node name="lastwillretain" type="CheckButton" parent="VBox/HBoxLastwill"] +layout_mode = 2 +text = "Retain +" + +[node name="HSeparator2" type="HSeparator" parent="VBox"] +custom_minimum_size = Vector2(0, 10) +layout_mode = 2 + +[node name="HBoxSubscriptions" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="Labellastwill2" type="Label" parent="VBox/HBoxSubscriptions"] +layout_mode = 2 +theme_type_variation = &"HeaderMedium" +text = "Subscriptions:" + +[node name="Label3" type="Label" parent="VBox/HBoxSubscriptions"] +layout_mode = 2 +size_flags_horizontal = 10 +text = "topic:" + +[node name="subscribetopic" type="LineEdit" parent="VBox/HBoxSubscriptions"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "godot/+" + +[node name="subscribe" type="Button" parent="VBox/HBoxSubscriptions"] +layout_mode = 2 +theme_override_styles/normal = SubResource("StyleBoxFlat_ppk4t") +text = "subscribe +" + +[node name="subscriptions" type="OptionButton" parent="VBox/HBoxSubscriptions"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 10 +tooltip_text = "Subscribed topics +" +item_count = 1 +selected = 0 +popup/item_0/text = "" +popup/item_0/id = 0 + +[node name="unsubscribe" type="Button" parent="VBox/HBoxSubscriptions"] +layout_mode = 2 +theme_override_constants/outline_size = 0 +theme_override_styles/normal = SubResource("StyleBoxFlat_8b54y") +text = "unsubscribe +" + +[node name="HSeparator3" type="HSeparator" parent="VBox"] +custom_minimum_size = Vector2(0, 10) +layout_mode = 2 + +[node name="HBoxPublish" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="Labellastwill" type="Label" parent="VBox/HBoxPublish"] +layout_mode = 2 +theme_type_variation = &"HeaderMedium" +text = "Publish:" + +[node name="Label" type="Label" parent="VBox/HBoxPublish"] +layout_mode = 2 +size_flags_horizontal = 10 +text = "topic:" + +[node name="publishtopic" type="LineEdit" parent="VBox/HBoxPublish"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "godot/myname" + +[node name="Label2" type="Label" parent="VBox/HBoxPublish"] +layout_mode = 2 +text = "message:" + +[node name="publishmessage" type="LineEdit" parent="VBox/HBoxPublish"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "hello there" + +[node name="publishretain" type="CheckButton" parent="VBox/HBoxPublish"] +layout_mode = 2 +text = "Retain +" + +[node name="publish" type="Button" parent="VBox/HBoxPublish"] +layout_mode = 2 +theme_override_styles/normal = SubResource("StyleBoxFlat_xasl1") +text = "Publish" + +[node name="HSeparator4" type="HSeparator" parent="VBox"] +custom_minimum_size = Vector2(0, 10) +layout_mode = 2 + +[node name="subscribedmessages" type="RichTextLabel" parent="VBox"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/normal = SubResource("StyleBoxFlat_njfq6") +bbcode_enabled = true +text = "[b]Instructions[/b] + +MQTT is a lightweight easy-to-use publish and subscribe (\"pubsub\") networking protocol. + +By default this app connects to the public server hosted at [u]https://test.mosquitto.org/[/u] , but you can connect to another broker or your own instance. + +[i]Commands to run on another computer terminal:[/i] + > mosquitto_pub -h test.mosquitto.org -t \"godot/abcd\" -m \"Bingo!\" + > mosquitto_sub -v -h test.mosquitto.org -t \"godot/#\"" + +[connection signal="broker_connected" from="MQTT" to="." method="_on_mqtt_broker_connected"] +[connection signal="broker_connection_failed" from="MQTT" to="." method="_on_mqtt_broker_connection_failed"] +[connection signal="broker_disconnected" from="MQTT" to="." method="_on_mqtt_broker_disconnected"] +[connection signal="received_message" from="MQTT" to="." method="_on_mqtt_received_message"] +[connection signal="toggled" from="VBox/HBoxBrokerControl/ButtonConnect" to="." method="_on_button_connect_toggled"] +[connection signal="item_selected" from="VBox/HBoxBroker/brokerprotocol" to="." method="_on_brokerprotocol_item_selected"] +[connection signal="pressed" from="VBox/HBoxSubscriptions/subscribe" to="." method="_on_subscribe_pressed"] +[connection signal="pressed" from="VBox/HBoxSubscriptions/unsubscribe" to="." method="_on_unsubscribe_pressed"] +[connection signal="pressed" from="VBox/HBoxPublish/publish" to="." method="_on_publish_pressed"] diff --git a/august7_mqtt/godot_client/project.godot b/august7_mqtt/godot_client/project.godot new file mode 100644 index 0000000..0ea48c8 --- /dev/null +++ b/august7_mqtt/godot_client/project.godot @@ -0,0 +1,29 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Godot-mqtt" +run/main_scene="res://mqttexample.tscn" +config/features=PackedStringArray("4.2", "Forward Plus") +config/icon="res://icon.png" + +[audio] + +buses/default_bus_layout="" + +[display] + +window/size/viewport_width=900 +window/size/viewport_height=520 + +[editor_plugins] + +enabled=PackedStringArray("res://addons/mqtt/plugin.cfg") diff --git a/august7_mqtt/resources/ESP32_GENERIC-20240602-v1.23.0.bin b/august7_mqtt/resources/ESP32_GENERIC-20240602-v1.23.0.bin new file mode 100644 index 0000000..16f70b5 Binary files /dev/null and b/august7_mqtt/resources/ESP32_GENERIC-20240602-v1.23.0.bin differ diff --git a/august7_mqtt/resources/README.md b/august7_mqtt/resources/README.md new file mode 100644 index 0000000..12fdbff --- /dev/null +++ b/august7_mqtt/resources/README.md @@ -0,0 +1,12 @@ +Included here is the UF2 file necessary to flash a Raspberry Pi Pico W with MicroPython. + +To use it: +* Unplug your RPI from USB +* Hold down the Bootsel button +* While holding down, connect USB +* Release Bootsel button + +The Pico will appear as a drive on your system. +Drag and drop the UF2 file onto the Pico drive - it will be reset with MicroPython installed. + +You are ready to code! \ No newline at end of file diff --git a/august7_mqtt/resources/RPI_PICO_W-20240602-v1.23.0.uf2 b/august7_mqtt/resources/RPI_PICO_W-20240602-v1.23.0.uf2 new file mode 100644 index 0000000..cab3064 Binary files /dev/null and b/august7_mqtt/resources/RPI_PICO_W-20240602-v1.23.0.uf2 differ diff --git a/august7_mqtt/rp2040_client/README.md b/august7_mqtt/rp2040_client/README.md new file mode 100644 index 0000000..1b9e396 --- /dev/null +++ b/august7_mqtt/rp2040_client/README.md @@ -0,0 +1,51 @@ +In this folder you will find an example of using MQTT with a simple client. Note that this example uses MicroPython as the development environment. + +To use the example, MicroPython must be flashed on the device. + +In the Resources folder you will find a UF2 file that can be used to flash the Raspberry Pi Pico W board. Simply hold down the boot button while powering on the board. It will be reset and will show up as a simple drive on your computer. Drag the UF2 file to the board - it will reboot and have MicroPython installed. + +## ESP32 + +It is possible to use the example with an ESP board by Espressive as well (tested with ESP32 Wroom) + +Installing MicroPython on the ESP32 is slightly more complex, and requires the installation of "esptool", a python library that will allow you to flash a binary file to the board. + +The steps include: + +* Download the propery binary for the board +* Set up a virtual environment for Python +* Install esptool +* Identify the port the device is on +* Use esptool to flash the binary your downloaded to the board + +### Set up Virtual Environment + +Setting up a virtual environment for Python is necessary because Python is now regularly used by the operating system. Accordingly, the OS will not typically allow packages to be installed by another package manager, otherwise critical packages could be overwritten. A virtual environment allows all package dependencies to be installed in a virtual location, protecting system Python packages. + +`python -m venv esptoolenv` + +Now you can activate the environment, which will allow us to install the tool (see below) + +`source esptoolenv/bin/activate` + +### Install ESPTool + +In the virutal environment you can use the pip installer to get the tool. + +`pip install esptool` + +### Identify the port device is on + +`ls /dev/tty*` + +### Flash the binary + +`esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash -z 0x1000 ESP32_GENERIC-20240602-v1.23.0.bin` + +### Deactivate the virtual environment + +`deactivate` + +You can now use your code editor (like Thonny) to connect to your device and write the python code you find here. + +As with the RP2040 you'll need to copy the MQTT client library onto the device. \ No newline at end of file diff --git a/august7_mqtt/rp2040_client/lib/umqtt/simple.py b/august7_mqtt/rp2040_client/lib/umqtt/simple.py new file mode 100644 index 0000000..6da38e4 --- /dev/null +++ b/august7_mqtt/rp2040_client/lib/umqtt/simple.py @@ -0,0 +1,212 @@ +import socket +import struct +from binascii import hexlify + + +class MQTTException(Exception): + pass + + +class MQTTClient: + def __init__( + self, + client_id, + server, + port=0, + user=None, + password=None, + keepalive=0, + ssl=None, + ): + if port == 0: + port = 8883 if ssl else 1883 + self.client_id = client_id + self.sock = None + self.server = server + self.port = port + self.ssl = ssl + self.pid = 0 + self.cb = None + self.user = user + self.pswd = password + self.keepalive = keepalive + self.lw_topic = None + self.lw_msg = None + self.lw_qos = 0 + self.lw_retain = False + + def _send_str(self, s): + self.sock.write(struct.pack("!H", len(s))) + self.sock.write(s) + + def _recv_len(self): + n = 0 + sh = 0 + while 1: + b = self.sock.read(1)[0] + n |= (b & 0x7F) << sh + if not b & 0x80: + return n + sh += 7 + + def set_callback(self, f): + self.cb = f + + def set_last_will(self, topic, msg, retain=False, qos=0): + assert 0 <= qos <= 2 + assert topic + self.lw_topic = topic + self.lw_msg = msg + self.lw_qos = qos + self.lw_retain = retain + + def connect(self, clean_session=True): + self.sock = socket.socket() + addr = socket.getaddrinfo(self.server, self.port)[0][-1] + self.sock.connect(addr) + if self.ssl: + self.sock = self.ssl.wrap_socket(self.sock, server_hostname=self.server) + premsg = bytearray(b"\x10\0\0\0\0\0") + msg = bytearray(b"\x04MQTT\x04\x02\0\0") + + sz = 10 + 2 + len(self.client_id) + msg[6] = clean_session << 1 + if self.user: + sz += 2 + len(self.user) + 2 + len(self.pswd) + msg[6] |= 0xC0 + if self.keepalive: + assert self.keepalive < 65536 + msg[7] |= self.keepalive >> 8 + msg[8] |= self.keepalive & 0x00FF + if self.lw_topic: + sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) + msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 + msg[6] |= self.lw_retain << 5 + + i = 1 + while sz > 0x7F: + premsg[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + premsg[i] = sz + + self.sock.write(premsg, i + 2) + self.sock.write(msg) + # print(hex(len(msg)), hexlify(msg, ":")) + self._send_str(self.client_id) + if self.lw_topic: + self._send_str(self.lw_topic) + self._send_str(self.lw_msg) + if self.user: + self._send_str(self.user) + self._send_str(self.pswd) + resp = self.sock.read(4) + assert resp[0] == 0x20 and resp[1] == 0x02 + if resp[3] != 0: + raise MQTTException(resp[3]) + return resp[2] & 1 + + def disconnect(self): + self.sock.write(b"\xe0\0") + self.sock.close() + + def ping(self): + self.sock.write(b"\xc0\0") + + def publish(self, topic, msg, retain=False, qos=0): + pkt = bytearray(b"\x30\0\0\0") + pkt[0] |= qos << 1 | retain + sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + assert sz < 2097152 + i = 1 + while sz > 0x7F: + pkt[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + pkt[i] = sz + # print(hex(len(pkt)), hexlify(pkt, ":")) + self.sock.write(pkt, i + 1) + self._send_str(topic) + if qos > 0: + self.pid += 1 + pid = self.pid + struct.pack_into("!H", pkt, 0, pid) + self.sock.write(pkt, 2) + self.sock.write(msg) + if qos == 1: + while 1: + op = self.wait_msg() + if op == 0x40: + sz = self.sock.read(1) + assert sz == b"\x02" + rcv_pid = self.sock.read(2) + rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] + if pid == rcv_pid: + return + elif qos == 2: + assert 0 + + def subscribe(self, topic, qos=0): + assert self.cb is not None, "Subscribe callback is not set" + pkt = bytearray(b"\x82\0\0\0") + self.pid += 1 + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) + # print(hex(len(pkt)), hexlify(pkt, ":")) + self.sock.write(pkt) + self._send_str(topic) + self.sock.write(qos.to_bytes(1, "little")) + while 1: + op = self.wait_msg() + if op == 0x90: + resp = self.sock.read(4) + # print(resp) + assert resp[1] == pkt[2] and resp[2] == pkt[3] + if resp[3] == 0x80: + raise MQTTException(resp[3]) + return + + # Wait for a single incoming MQTT message and process it. + # Subscribed messages are delivered to a callback previously + # set by .set_callback() method. Other (internal) MQTT + # messages processed internally. + def wait_msg(self): + res = self.sock.read(1) + self.sock.setblocking(True) + if res is None: + return None + if res == b"": + raise OSError(-1) + if res == b"\xd0": # PINGRESP + sz = self.sock.read(1)[0] + assert sz == 0 + return None + op = res[0] + if op & 0xF0 != 0x30: + return op + sz = self._recv_len() + topic_len = self.sock.read(2) + topic_len = (topic_len[0] << 8) | topic_len[1] + topic = self.sock.read(topic_len) + sz -= topic_len + 2 + if op & 6: + pid = self.sock.read(2) + pid = pid[0] << 8 | pid[1] + sz -= 2 + msg = self.sock.read(sz) + self.cb(topic, msg) + if op & 6 == 2: + pkt = bytearray(b"\x40\x02\0\0") + struct.pack_into("!H", pkt, 2, pid) + self.sock.write(pkt) + elif op & 6 == 4: + assert 0 + return op + + # Checks whether a pending message from server is available. + # If not, returns immediately with None. Otherwise, does + # the same processing as wait_msg. + def check_msg(self): + self.sock.setblocking(False) + return self.wait_msg() diff --git a/august7_mqtt/rp2040_client/main.py b/august7_mqtt/rp2040_client/main.py new file mode 100644 index 0000000..c9d261e --- /dev/null +++ b/august7_mqtt/rp2040_client/main.py @@ -0,0 +1,57 @@ +import time +from machine import Pin +from umqtt.simple import MQTTClient + + +import network +import time +from math import sin + + +# Received messages from subscriptions will be delivered to this callback +def sub_cb(topic, msg): + print((topic, msg)) + if msg == b'on': + print("turn LED on") + #can do some hardware control here + elif msg == b'off': + print("turn LED off") + + +# Replace details in code below + +def main(server="192.168.1.XX"): + # Fill in your WiFi network name (ssid) and password here: + wifi_ssid = "XXXXXXXXX" + wifi_password = "XXXXXXXXX" + + # Connect to WiFi + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + wlan.connect(wifi_ssid, wifi_password) + while wlan.isconnected() == False: + print('Waiting for connection...') + time.sleep(1) + print("Connected to WiFi") + + #client name should be unique + c = MQTTClient("pico_board", server, 1883) + c.set_callback(sub_cb) + c.connect() + c.subscribe(b"godot/#") + while True: + if True: + # Blocking wait for message + c.wait_msg() + else: + # Non-blocking wait for message + c.check_msg() + # Then need to sleep to avoid 100% CPU usage (in a real + # app other useful actions would be performed instead) + time.sleep(1) + + c.disconnect() + + +if __name__ == "__main__": + main()