# HG changeset patch # User Edouard Tisserant <edouard@beremiz.fr> # Date 1734701553 -3600 # Node ID 86fbb3991af627eb80f500e3896c1e67091c377e # Parent 803d4e64cc683f8d00109465aa27434b96547e32 MQTT: allow publish and subscribe from user python code. diff -r 803d4e64cc68 -r 86fbb3991af6 features.py --- 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'), diff -r 803d4e64cc68 -r 86fbb3991af6 mqtt/__init__.py --- 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 diff -r 803d4e64cc68 -r 86fbb3991af6 mqtt/client.py --- 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) diff -r 803d4e64cc68 -r 86fbb3991af6 mqtt/library.py --- /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"))) diff -r 803d4e64cc68 -r 86fbb3991af6 mqtt/mqtt_client_gen.py --- 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"], diff -r 803d4e64cc68 -r 86fbb3991af6 mqtt/mqtt_template.c --- 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; +}} diff -r 803d4e64cc68 -r 86fbb3991af6 mqtt/pous.xml --- /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> diff -r 803d4e64cc68 -r 86fbb3991af6 tests/projects/mqtt_client/beremiz.xml --- 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> diff -r 803d4e64cc68 -r 86fbb3991af6 tests/projects/mqtt_client/plc.xml --- 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> diff -r 803d4e64cc68 -r 86fbb3991af6 tests/projects/mqtt_client/py_ext_0@py_ext/baseconfnode.xml --- /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"/> diff -r 803d4e64cc68 -r 86fbb3991af6 tests/projects/mqtt_client/py_ext_0@py_ext/pyfile.xml --- /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>