mqtt/client.py
author Edouard Tisserant <edouard@beremiz.fr>
Tue, 23 Jul 2024 15:30:04 +0200
changeset 3998 0145c60b9560
parent 3991 28354ba489b9
child 4001 5e0660d394e3
permissions -rw-r--r--
MQTT: WIP, PLC starts even if MQTT client can't connect broker, and publish+subscribe again when reconnecting.
# mqtt/client.py

from __future__ import absolute_import

import os

from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT
from .mqtt_client_gen import MQTTClientPanel, MQTTClientModel, MQTT_IEC_types, authParams

import util.paths as paths


# assumes that "build" directory was created in paho.mqtt.c source directory,
# and cmake build was invoked from this directory
PahoMqttCLibraryPath = paths.ThirdPartyPath("paho.mqtt.c", "build", "src")

PahoMqttCIncludePaths = [
    paths.ThirdPartyPath("paho.mqtt.c", "build"),  # VersionInfo.h
    paths.ThirdPartyPath("paho.mqtt.c", "src")
]

class MQTTClientEditor(ConfTreeNodeEditor):
    CONFNODEEDITOR_TABS = [
        (_("MQTT Client"), "CreateMQTTClient_UI")]

    def Log(self, msg):
        self.Controler.GetCTRoot().logger.write(msg)

    def CreateMQTTClient_UI(self, parent):
        return MQTTClientPanel(parent, self.Controler.GetModelData(), self.Log, self.Controler.GetConfig)

class MQTTClient(object):
    XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?>
    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
      <xsd:element name="MQTTClient">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="AuthType" minOccurs="0">
              <xsd:complexType>
                <xsd:choice minOccurs="0">
                  <xsd:element name="x509">
                    <xsd:complexType>
                      <xsd:attribute name="Certificate" type="xsd:string" use="optional" default="certificate.pem"/>
                      <xsd:attribute name="PrivateKey" type="xsd:string" use="optional" default="private_key.pem"/>
                    </xsd:complexType>
                  </xsd:element>
                  <xsd:element name="UserPassword">
                    <xsd:complexType>
                      <xsd:attribute name="User" type="xsd:string" use="optional"/>
                      <xsd:attribute name="Password" type="xsd:string" use="optional"/>
                    </xsd:complexType>
                  </xsd:element>
                </xsd:choice>
              </xsd:complexType>
            </xsd:element>
          </xsd:sequence>
          <xsd:attribute name="Use_MQTT_5" type="xsd:boolean" use="optional" default="true"/>
          <xsd:attribute name="Broker_URI" type="xsd:string" use="optional" default="ws://localhost:1883"/>
          <xsd:attribute name="Client_ID" type="xsd:string" use="optional" default=""/>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>
    """

    EditorType = MQTTClientEditor

    def __init__(self):
        self.modeldata = MQTTClientModel(self.Log, self.CTNMarkModified)

        filepath = self.GetFileName()
        if os.path.isfile(filepath):
            self.modeldata.LoadCSV(filepath)

    def Log(self, msg):
        self.GetCTRoot().logger.write(msg)

    def GetModelData(self):
        return self.modeldata

    def GetConfig(self):
        def cfg(path): 
            try:
                attr=self.GetParamsAttributes("MQTTClient."+path)
            except ValueError:
                return None
            return attr["value"]

        AuthType = cfg("AuthType")
        res = dict(
            URI=cfg("Broker_URI"),
            AuthType=AuthType,
            clientID=cfg("Client_ID"),
            UseMQTT5=cfg("Use_MQTT_5"))

        paramList = authParams.get(AuthType, None)
        if paramList:
            for name,default in paramList:
                value = cfg("AuthType."+name)
                if value == "" or value is None:
                    value = default
                # cryptomaterial is expected to be in project's user provide file directory
                if name in ["Certificate","PrivateKey"]:
                    value = os.path.join(self.GetCTRoot()._getProjectFilesPath(), value)
                res[name] = value

        return res

    def GetFileName(self):
        return os.path.join(self.CTNPath(), 'selected.csv')

    def OnCTNSave(self, from_project_path=None):
        self.modeldata.SaveCSV(self.GetFileName())
        return True

    def CTNGenerate_C(self, buildpath, locations):
        current_location = self.GetCurrentLocation()
        locstr = "_".join(map(str, current_location))
        c_path = os.path.join(buildpath, "mqtt_client__%s.c" % locstr)

        c_code = """
#include "iec_types_all.h"
#include "beremiz.h"
"""
        c_code += self.modeldata.GenerateC(c_path, locstr, self.GetConfig())

        with open(c_path, 'w') as c_file:
            c_file.write(c_code)

        LDFLAGS = [' "' + os.path.join(PahoMqttCLibraryPath, "libpaho-mqtt3cs.a") + '"', '-lssl', '-lcrypto']

        CFLAGS = ' '.join(['-I"' + path + '"' for path in PahoMqttCIncludePaths])

        return [(c_path, CFLAGS)], LDFLAGS, True

    def GetVariableLocationTree(self):
        current_location = self.GetCurrentLocation()
        locstr = "_".join(map(str, current_location))
        name = self.BaseParams.getName()

        entries = []
        children = []

        for row in self.modeldata["output"]:
            Topic, QoS, _Retained, iec_type, iec_number = row
            entries.append((Topic, QoS, iec_type, iec_number, "Q", LOCATION_VAR_OUTPUT))

        for row in self.modeldata["input"]:
            Topic, QoS, iec_type, iec_number = row
            entries.append((Topic, QoS, iec_type, iec_number, "I", LOCATION_VAR_INPUT))

        for Topic, QoS, iec_type, iec_number, iec_dir_prefix, loc_type in entries:
            C_type, iec_size_prefix = MQTT_IEC_types[iec_type]
            c_loc_name = "__" + iec_dir_prefix + iec_size_prefix + locstr + "_" + str(iec_number)
            children.append({
                "name": Topic,
                "type": loc_type,
                "size": {"X":1, "B":8, "W":16, "D":32, "L":64}[iec_size_prefix],
                "IEC_type": iec_type,
                "var_name": c_loc_name,
                "location": "%" + iec_dir_prefix + iec_size_prefix + ".".join([str(i) for i in current_location]) + "." + str(iec_number),
                "description": "",
                "children": []})

        return {"name": name,
                "type": LOCATION_CONFNODE,
                "location": ".".join([str(i) for i in current_location]) + ".x",
                "children": children}