MQTT: allow publish and subscribe from user python code.
--- 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>