MQTT: allow publish and subscribe from user python code.
authorEdouard Tisserant <edouard@beremiz.fr>
Fri, 20 Dec 2024 14:32:33 +0100 (2 months ago)
changeset 4081 86fbb3991af6
parent 4080 803d4e64cc68
child 4082 3ef4b5b202fc
MQTT: allow publish and subscribe from user python code.
features.py
mqtt/__init__.py
mqtt/client.py
mqtt/library.py
mqtt/mqtt_client_gen.py
mqtt/mqtt_template.c
mqtt/pous.xml
tests/projects/mqtt_client/beremiz.xml
tests/projects/mqtt_client/plc.xml
tests/projects/mqtt_client/py_ext_0@py_ext/baseconfnode.xml
tests/projects/mqtt_client/py_ext_0@py_ext/pyfile.xml
--- a/features.py	Thu Jan 09 09:46:43 2025 +0100
+++ b/features.py	Fri Dec 20 14:32:33 2024 +0100
@@ -12,7 +12,8 @@
     ('Native', 'NativeLib.NativeLibrary', True),
     ('Python', 'py_ext.PythonLibrary', True),
     # FIXME ('Etherlab', 'etherlab.EthercatMaster.EtherlabLibrary', False),
-    ('SVGHMI', 'svghmi.SVGHMILibrary', 'svghmi')]
+    ('SVGHMI', 'svghmi.SVGHMILibrary', 'svghmi'),
+    ('MQTT', 'mqtt.MQTTLibrary', False)]
 
 catalog = [
     ('mqtt', _('MQTT client'), _('Map MQTT topics as located variables'), 'mqtt.MQTTClient'),
--- a/mqtt/__init__.py	Thu Jan 09 09:46:43 2025 +0100
+++ b/mqtt/__init__.py	Fri Dec 20 14:32:33 2024 +0100
@@ -3,4 +3,5 @@
 from __future__ import absolute_import
 
 from .client import MQTTClient
+from .library import MQTTLibrary
 
--- a/mqtt/client.py	Thu Jan 09 09:46:43 2025 +0100
+++ b/mqtt/client.py	Fri Dec 20 14:32:33 2024 +0100
@@ -178,7 +178,8 @@
 #include "beremiz.h"
 """
         config = self.GetConfig()
-        c_code += self.modeldata.GenerateC(c_path, locstr, config, self.GetDataTypeInfos)
+        name = self.BaseParams.getName()
+        c_code += self.modeldata.GenerateC(name, c_path, locstr, config, self.GetDataTypeInfos)
 
         with open(c_path, 'w') as c_file:
             c_file.write(c_code)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mqtt/library.py	Fri Dec 20 14:32:33 2024 +0100
@@ -0,0 +1,84 @@
+# mqtt/client.py
+
+from __future__ import absolute_import
+
+import os
+
+from POULibrary import POULibrary
+import util.paths as paths
+
+mqtt_python_lib_code = """
+def MQTT_publish(clientname, topic, payload, QoS = 1, Retained = False):
+    c_function_name = "__mqtt_python_publish_" + clientname
+    c_function = getattr(PLCBinary, c_function_name)
+    c_function.restype = ctypes.c_int # error or 0
+    c_function.argtypes = [
+        ctypes.c_char_p,  # topic
+        ctypes.c_char_p,  # data
+        ctypes.c_uint32,  # datalen
+        ctypes.c_uint8,   # QoS
+        ctypes.c_uint8,   # Retained
+    ]
+    res = c_function(topic, payload, len(payload), QoS, Retained)
+    return res
+
+# C per client CallBack type for __mqtt_python_subscribe_{name}
+c_cb_type = ctypes.CFUNCTYPE(ctypes.c_int,                   # return
+                             ctypes.c_char_p,                # topic
+                             ctypes.POINTER(ctypes.c_char),  # data
+                             ctypes.c_uint32)                # data length
+
+# CallBacks management
+# - each call to MQTT_subscribe registers a callback
+MQTT_subscribers_cbs = {}
+
+# - one callback registered to C side per client
+MQTT_client_cbs = {}
+
+def per_client_cb_factory(client):
+    def per_client_cb(topic, dataptr, datalen):
+        payload = ctypes.string_at(dataptr, datalen)
+        subscriber = MQTT_subscribers_cbs[client].get(topic, None)
+        if subscriber:
+            subscriber(topic, payload)
+            return 0
+        return 1
+    return per_client_cb
+    
+def MQTT_subscribe(clientname, topic, cb, QoS = 1):
+    global MQTT_client_cbs, MQTT_subscribers_cbs
+    c_function_name = "__mqtt_python_subscribe_" + clientname
+    c_function = getattr(PLCBinary, c_function_name)
+    c_function.restype = ctypes.c_int # error or 0
+    c_function.argtypes = [
+        ctypes.c_char_p,  # topic
+        ctypes.c_uint8]   # QoS
+
+    MQTT_subscribers_cbs.setdefault(clientname, {})[topic] = cb
+
+    c_cb = MQTT_client_cbs.get(clientname, None)
+    if c_cb is None:
+        c_cb = c_cb_type(per_client_cb_factory(clientname))
+        MQTT_client_cbs[clientname] = c_cb
+        register_c_function = getattr(PLCBinary, "__mqtt_python_callback_setter_"+clientname )
+        register_c_function.argtypes = [c_cb_type]
+        register_c_function(c_cb)
+
+    res = c_function(topic, QoS)
+    return res
+
+"""
+
+class MQTTLibrary(POULibrary):
+    
+    def GetLibraryPath(self):
+        return paths.AbsNeighbourFile(__file__, "pous.xml")
+
+    def Generate_C(self, buildpath, varlist, IECCFLAGS):
+
+        runtimefile_path = os.path.join(buildpath, "runtime_00_mqtt.py")
+        runtimefile = open(runtimefile_path, 'w')
+        runtimefile.write(mqtt_python_lib_code)
+        runtimefile.close()
+        return ((["mqtt"], [], False), "",
+                ("runtime_00_mqtt.py", open(runtimefile_path, "rb")))
--- a/mqtt/mqtt_client_gen.py	Thu Jan 09 09:46:43 2025 +0100
+++ b/mqtt/mqtt_client_gen.py	Fri Dec 20 14:32:33 2024 +0100
@@ -356,7 +356,7 @@
                 for row in data:
                     writer.writerow([direction] + row)
 
-    def GenerateC(self, path, locstr, config, datatype_info_getter):
+    def GenerateC(self, name, path, locstr, config, datatype_info_getter):
         c_template_filepath = paths.AbsNeighbourFile(__file__, "mqtt_template.c")
         c_template_file = open(c_template_filepath , 'rb')
         c_template = c_template_file.read()
@@ -365,6 +365,7 @@
         json_types = OD()
 
         formatdict = dict(
+            name            = name,
             locstr          = locstr,
             uri             = config["URI"],
             clientID        = config["clientID"],
--- a/mqtt/mqtt_template.c	Thu Jan 09 09:46:43 2025 +0100
+++ b/mqtt/mqtt_template.c	Fri Dec 20 14:32:33 2024 +0100
@@ -260,8 +260,10 @@
 }}
 
 
-
-int messageArrived(void *context, char *topicName, int topicLen, MQTTClient_message *message)
+typedef int(*callback_fptr_t)(char* topic, char* data, uint32_t datalen);
+static callback_fptr_t __mqtt_python_callback_fptr_{name} = NULL;
+
+static int messageArrived(void *context, char *topicName, int topicLen, MQTTClient_message *message)
 {{
     int low = 0;
     int size = sizeof(topics) / sizeof(topics[0]);
@@ -307,7 +309,15 @@
             high = mid - 1;
     }}
     // If we reach here, then the element was not present
-    LogWarning("MQTT unknown topic: %s\n", topicName);
+    if(__mqtt_python_callback_fptr_{name} && 
+       (*__mqtt_python_callback_fptr_{name})(topicName,
+                                     (char*)message->payload,
+                                     message->payloadlen) == 0){{
+        // Topic was handled in python
+        goto exit;
+    }} else {{
+        LogWarning("MQTT unknown topic: %s\n", topicName);
+    }}
     goto exit;
 
 found:
@@ -357,19 +367,19 @@
 
 #ifdef USE_MQTT_5
 #define _SUBSCRIBE(Topic, QoS)                                                                    \
-        MQTTResponse response = MQTTClient_subscribe5(client, #Topic, QoS, NULL, NULL);           \
+        MQTTResponse response = MQTTClient_subscribe5(client, Topic, QoS, NULL, NULL);            \
         /* when using MQTT5 responce code is 1 for some reason even if no error */                \
         rc = response.reasonCode == 1 ? MQTTCLIENT_SUCCESS : response.reasonCode;                 \
         MQTTResponse_free(response);
 #else
 #define _SUBSCRIBE(Topic, QoS)                                                                    \
-        rc = MQTTClient_subscribe(client, #Topic, QoS);
+        rc = MQTTClient_subscribe(client, Topic, QoS);
 #endif
 
 #define INIT_SUBSCRIPTION(Topic, QoS)                                                             \
     {{                                                                                            \
         int rc;                                                                                   \
-        _SUBSCRIBE(Topic, QoS)                                                                    \
+        _SUBSCRIBE(#Topic, QoS)                                                                   \
         if (rc != MQTTCLIENT_SUCCESS)                                                             \
         {{                                                                                        \
             LogError("MQTT client failed to subscribe to '%s', return code %d\n", #Topic, rc);    \
@@ -379,18 +389,18 @@
 
 #ifdef USE_MQTT_5
 #define _PUBLISH(Topic, QoS, cstring_size, cstring_ptr, Retained)                                 \
-        MQTTResponse response = MQTTClient_publish5(client, #Topic, cstring_size,                  \
+        MQTTResponse response = MQTTClient_publish5(client, Topic, cstring_size,                  \
             cstring_ptr, QoS, Retained, NULL, NULL);                                              \
         rc = response.reasonCode;                                                                 \
         MQTTResponse_free(response);
 #else
 #define _PUBLISH(Topic, QoS, cstring_size, cstring_ptr, Retained)                                 \
-        rc = MQTTClient_publish(client, #Topic, cstring_size,                                     \
+        rc = MQTTClient_publish(client, Topic, cstring_size,                                      \
             cstring_ptr, QoS, Retained, NULL);
 #endif
 
 #define PUBLISH_SIMPLE(Topic, QoS, C_type, c_loc_name, Retained)                                  \
-        _PUBLISH(Topic, QoS, sizeof(C_type), &MQTT_##c_loc_name##_buf, Retained)
+        _PUBLISH(#Topic, QoS, sizeof(C_type), &MQTT_##c_loc_name##_buf, Retained)
 
 #define PUBLISH_JSON(Topic, QoS, C_type, c_loc_name, Retained)                                    \
         int res = json_gen_##c_loc_name(&MQTT_##c_loc_name##_buf);                                \
@@ -589,3 +599,39 @@
     }}
 }}
 
+int __mqtt_python_publish_{name}(char* topic, char* data, uint32_t datalen, uint8_t QoS, uint8_t Retained)
+{{
+    int rc = 0;
+
+    if((rc = pthread_mutex_lock(&MQTT_thread_wakeup_mutex)) == 0){{
+        _PUBLISH(topic, QoS, datalen, data, Retained)
+        pthread_mutex_unlock(&MQTT_thread_wakeup_mutex);
+        if (rc != MQTTCLIENT_SUCCESS){{
+            LogError("MQTT python can't publish \"%s\", return code %d\n", topic, rc);
+            return rc;
+        }}
+    }} else {{
+        LogError("MQTT python can't obtain lock, return code %d\n", rc);
+        return rc;
+    }}
+    return 0;
+}}
+
+
+int __mqtt_python_subscribe_{name}(char* topic, uint8_t QoS)
+{{
+    int rc;
+    _SUBSCRIBE(topic, QoS)
+    if (rc != MQTTCLIENT_SUCCESS)
+    {{
+        LogError("MQTT client failed to subscribe to '%s', return code %d\n", topic, rc);
+        return rc;
+    }}
+    return 0;
+}}
+
+int __mqtt_python_callback_setter_{name}(callback_fptr_t cb)
+{{
+    __mqtt_python_callback_fptr_{name} = cb;
+    return 0;
+}}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mqtt/pous.xml	Fri Dec 20 14:32:33 2024 +0100
@@ -0,0 +1,24 @@
+<?xml version='1.0' encoding='utf-8'?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xsi:schemaLocation="http://www.plcopen.org/xml/tc6_0201">
+  <fileHeader companyName="Beremiz" productName="Beremiz" productVersion="0.0" creationDateTime="2008-12-14T16:53:26"/>
+  <contentHeader name="Beremiz non-standard POUs library" modificationDateTime="2019-08-06T14:08:26">
+    <coordinateInfo>
+      <fbd>
+        <scaling x="0" y="0"/>
+      </fbd>
+      <ld>
+        <scaling x="0" y="0"/>
+      </ld>
+      <sfc>
+        <scaling x="0" y="0"/>
+      </sfc>
+    </coordinateInfo>
+  </contentHeader>
+  <types>
+    <dataTypes/>
+    <pous/>
+  </types>
+  <instances>
+    <configurations/>
+  </instances>
+</project>
--- a/tests/projects/mqtt_client/beremiz.xml	Thu Jan 09 09:46:43 2025 +0100
+++ b/tests/projects/mqtt_client/beremiz.xml	Fri Dec 20 14:32:33 2024 +0100
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
 <BeremizRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" URI_location="PYRO://localhost:61131" Disable_Extensions="false">
   <TargetType/>
-  <Libraries Enable_Python_Library="false"/>
+  <Libraries Enable_Python_Library="true" Enable_MQTT_Library="true"/>
 </BeremizRoot>
--- a/tests/projects/mqtt_client/plc.xml	Thu Jan 09 09:46:43 2025 +0100
+++ b/tests/projects/mqtt_client/plc.xml	Fri Dec 20 14:32:33 2024 +0100
@@ -1,7 +1,7 @@
 <?xml version='1.0' encoding='utf-8'?>
 <project xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.plcopen.org/xml/tc6_0201">
   <fileHeader companyName="Beremiz" productName="Beremiz" productVersion="1" creationDateTime="2016-10-24T18:09:22"/>
-  <contentHeader name="First Steps" modificationDateTime="2024-07-25T16:55:27">
+  <contentHeader name="First Steps" modificationDateTime="2024-12-18T14:47:09">
     <coordinateInfo>
       <fbd>
         <scaling x="0" y="0"/>
@@ -41,6 +41,18 @@
               </type>
             </variable>
           </localVars>
+          <localVars>
+            <variable name="python_eval0">
+              <type>
+                <derived name="python_eval"/>
+              </type>
+            </variable>
+            <variable name="python_eval1">
+              <type>
+                <derived name="python_eval"/>
+              </type>
+            </variable>
+          </localVars>
         </interface>
         <body>
           <FBD>
@@ -52,7 +64,7 @@
               <expression>LocalVar0</expression>
             </inVariable>
             <inVariable localId="3" executionOrderId="0" height="27" width="82" negated="false">
-              <position x="126" y="224"/>
+              <position x="814" y="106"/>
               <connectionPointOut>
                 <relPosition x="82" y="13"/>
               </connectionPointOut>
@@ -224,11 +236,11 @@
                 <variable formalParameter="IN0">
                   <connectionPointIn>
                     <relPosition x="0" y="50"/>
-                    <connection refLocalId="12">
+                    <connection refLocalId="3">
                       <position x="956" y="103"/>
-                      <position x="927" y="103"/>
-                      <position x="927" y="115"/>
-                      <position x="898" y="115"/>
+                      <position x="926" y="103"/>
+                      <position x="926" y="119"/>
+                      <position x="896" y="119"/>
                     </connection>
                   </connectionPointIn>
                 </variable>
@@ -238,35 +250,201 @@
                     <connection refLocalId="13">
                       <position x="956" y="123"/>
                       <position x="933" y="123"/>
-                      <position x="933" y="171"/>
-                      <position x="910" y="171"/>
-                    </connection>
-                  </connectionPointIn>
-                </variable>
-              </inputVariables>
-              <inOutVariables/>
-              <outputVariables>
-                <variable formalParameter="OUT">
-                  <connectionPointOut>
-                    <relPosition x="63" y="30"/>
-                  </connectionPointOut>
-                </variable>
-              </outputVariables>
-            </block>
-            <inVariable localId="12" executionOrderId="0" height="27" width="34" negated="false">
-              <position x="872" y="102"/>
+                      <position x="933" y="159"/>
+                      <position x="910" y="159"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="OUT">
+                  <connectionPointOut>
+                    <relPosition x="63" y="30"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <inVariable localId="13" executionOrderId="0" height="27" width="34" negated="false">
+              <position x="876" y="146"/>
               <connectionPointOut>
                 <relPosition x="34" y="13"/>
               </connectionPointOut>
               <expression>666</expression>
             </inVariable>
-            <inVariable localId="13" executionOrderId="0" height="27" width="34" negated="false">
-              <position x="876" y="158"/>
-              <connectionPointOut>
-                <relPosition x="34" y="13"/>
-              </connectionPointOut>
-              <expression>666</expression>
-            </inVariable>
+            <block localId="14" typeName="python_eval" instanceName="python_eval0" executionOrderId="0" height="60" width="98">
+              <position x="849" y="235"/>
+              <inputVariables>
+                <variable formalParameter="TRIG">
+                  <connectionPointIn>
+                    <relPosition x="0" y="30"/>
+                    <connection refLocalId="8" formalParameter="OUT">
+                      <position x="849" y="265"/>
+                      <position x="782" y="265"/>
+                      <position x="782" y="199"/>
+                      <position x="794" y="199"/>
+                      <position x="794" y="95"/>
+                      <position x="784" y="95"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="CODE">
+                  <connectionPointIn>
+                    <relPosition x="0" y="50"/>
+                    <connection refLocalId="16">
+                      <position x="849" y="285"/>
+                      <position x="797" y="285"/>
+                      <position x="797" y="291"/>
+                      <position x="745" y="291"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="ACK">
+                  <connectionPointOut>
+                    <relPosition x="98" y="30"/>
+                  </connectionPointOut>
+                </variable>
+                <variable formalParameter="RESULT">
+                  <connectionPointOut>
+                    <relPosition x="98" y="50"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <inVariable localId="15" executionOrderId="0" height="27" width="354" negated="false">
+              <position x="1384" y="186"/>
+              <connectionPointOut>
+                <relPosition x="354" y="13"/>
+              </connectionPointOut>
+              <expression>'sys.stdout.write("MQTT PYTHON TEST OK\n")'</expression>
+            </inVariable>
+            <inVariable localId="16" executionOrderId="0" height="27" width="218" negated="false">
+              <position x="527" y="278"/>
+              <connectionPointOut>
+                <relPosition x="218" y="13"/>
+              </connectionPointOut>
+              <expression>'publish_example("mhooo")'</expression>
+            </inVariable>
+            <block localId="12" typeName="python_eval" instanceName="python_eval1" executionOrderId="0" height="60" width="98">
+              <position x="1803" y="146"/>
+              <inputVariables>
+                <variable formalParameter="TRIG">
+                  <connectionPointIn>
+                    <relPosition x="0" y="30"/>
+                    <connection refLocalId="19" formalParameter="OUT">
+                      <position x="1803" y="176"/>
+                      <position x="1472" y="176"/>
+                      <position x="1472" y="173"/>
+                      <position x="1302" y="173"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="CODE">
+                  <connectionPointIn>
+                    <relPosition x="0" y="50"/>
+                    <connection refLocalId="15">
+                      <position x="1803" y="196"/>
+                      <position x="1748" y="196"/>
+                      <position x="1748" y="199"/>
+                      <position x="1738" y="199"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="ACK">
+                  <connectionPointOut>
+                    <relPosition x="98" y="30"/>
+                  </connectionPointOut>
+                </variable>
+                <variable formalParameter="RESULT">
+                  <connectionPointOut>
+                    <relPosition x="98" y="50"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <block localId="17" typeName="EQ" executionOrderId="0" height="60" width="63">
+              <position x="1144" y="204"/>
+              <inputVariables>
+                <variable formalParameter="IN1">
+                  <connectionPointIn>
+                    <relPosition x="0" y="30"/>
+                    <connection refLocalId="18">
+                      <position x="1144" y="234"/>
+                      <position x="1114" y="234"/>
+                      <position x="1114" y="235"/>
+                      <position x="1104" y="235"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN2">
+                  <connectionPointIn>
+                    <relPosition x="0" y="50"/>
+                    <connection refLocalId="14" formalParameter="RESULT">
+                      <position x="1144" y="254"/>
+                      <position x="1063" y="254"/>
+                      <position x="1063" y="285"/>
+                      <position x="947" y="285"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="OUT">
+                  <connectionPointOut>
+                    <relPosition x="63" y="30"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <inVariable localId="18" executionOrderId="0" height="27" width="95" negated="false">
+              <position x="1009" y="222"/>
+              <connectionPointOut>
+                <relPosition x="95" y="13"/>
+              </connectionPointOut>
+              <expression>'mhooo'</expression>
+            </inVariable>
+            <block localId="19" typeName="AND" executionOrderId="0" height="60" width="63">
+              <position x="1239" y="143"/>
+              <inputVariables>
+                <variable formalParameter="IN1">
+                  <connectionPointIn>
+                    <relPosition x="0" y="30"/>
+                    <connection refLocalId="14" formalParameter="ACK">
+                      <position x="1239" y="173"/>
+                      <position x="990" y="173"/>
+                      <position x="990" y="265"/>
+                      <position x="947" y="265"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN2">
+                  <connectionPointIn>
+                    <relPosition x="0" y="50"/>
+                    <connection refLocalId="17" formalParameter="OUT">
+                      <position x="1239" y="193"/>
+                      <position x="1223" y="193"/>
+                      <position x="1223" y="234"/>
+                      <position x="1207" y="234"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="OUT">
+                  <connectionPointOut>
+                    <relPosition x="63" y="30"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
           </FBD>
         </body>
       </pou>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_client/py_ext_0@py_ext/baseconfnode.xml	Fri Dec 20 14:32:33 2024 +0100
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='utf-8'?>
+<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="1" Name="py_ext_0"/>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_client/py_ext_0@py_ext/pyfile.xml	Fri Dec 20 14:32:33 2024 +0100
@@ -0,0 +1,38 @@
+<?xml version='1.0' encoding='utf-8'?>
+<PyFile xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <variables/>
+  <globals>
+    <xhtml:p><![CDATA[
+import sys
+last_payload = None
+
+def topic_callback(topic, payload):
+    global last_payload
+    last_payload = payload
+
+# called from PLC
+def publish_example(data):
+    global last_payload
+    MQTT_publish("mqtt_0", "py_test", data)
+    return last_payload
+
+]]></xhtml:p>
+  </globals>
+  <init>
+    <xhtml:p><![CDATA[
+]]></xhtml:p>
+  </init>
+  <cleanup>
+    <xhtml:p><![CDATA[
+]]></xhtml:p>
+  </cleanup>
+  <start>
+    <xhtml:p><![CDATA[
+MQTT_subscribe("mqtt_0", "py_test", topic_callback)
+]]></xhtml:p>
+  </start>
+  <stop>
+    <xhtml:p><![CDATA[
+]]></xhtml:p>
+  </stop>
+</PyFile>