Archives par mot-clé : Modbus

Raspberry Pi : Une passerelle Modbus – Communication S7

Bonjour,

Suivant le fil de nos expériences sur Raspberry Pi / Node-RED / netPi, netIOT… il est donc possible de communiquer en Modbus d’une part et en communication S7 sur Iso on TCP / RFC 1006 d’autre part.

Bien que cela ne soit pas une demande très fréquente il m’a été demandé quelques fois une passerelle permettant de connecter un automate communiquant selon le protocole Modbus, généralement il faut bien le dire un automate de la gamme SCHNEIDER ELECTRIC, avec un automate de la gamme SIEMENS S7.

S’il est possible bien sûr, et c’est souvent ainsi que la connexion est réalisée, d’utiliser une passerelle de la gamme Hilscher comme les netTAP 50 / 100 / 151 pour des conversions Modbus ou Ethernet/IP vers PROFIBUS ou PROFINET, il est nécessaire dans ce cas de modifier la configuration du réseau et de l’automate SIEMENS S7 pour y ajouter la passerelle en tant qu’esclave et programmer les échanges.

Cela demande de toute évidence de disposer des outils, des compétences, du programme et de l’autorisation de le modifier.

Dans certains cas, on souhaite seulement accéder aux données de l’automate qui sont déjà disponibles, par exemple dans un bloc de données mises à disposition pour un SCADA, et c’est ce que permet la communication S7.

Ainsi, le « flow » suivant réalise une communication entre un automate SCHNEIDER ELECTRIC M340, dans ce cas serveur Modbus TCP, et un automate SIEMENS S7-1500.

La passerelle équipée de Node-RED est dans ce cas le client Modbus TCP et lit et écrit les données dans le M340 d’une part.
D’autre part, elle lit et écrit les données dans le S7-1500.

Avec la version JSON pour l’import :
Node-RED-Modbus-S7-GW-00

On a donc un potentiomètre qui permet de régler le %MW10 en l’écrivant dans le S-1500 et une relecture de ce même mot.
Le résultat de la lecture est affiché d’une part dans une jauge et d’autre part écrit dans le %MW205 du M340.
Finalement, on lit ce %MW205 dans le M340 et on affiche sa valeur dans une jauge.

Un autre cas de figure est traité en partie basse du « flow », le cas où la passerelle est serveur Modbus TCP.

On a donc un nœud Modbus Serveur qui va être interrogé par un client Modbus TCP quelconque, en l’occurrence pour ma démonstration Modbus Poll.

Pour tester le nœud Modbus Serveur, un potentiomètre permet d’écrire une valeur dans le %MW0.
Le %MW10 remontant du S7-1500 est lui écrit en %MW1 (en même temps qu’il est écrit dans le M340).

On lit dans le nœud Modbus Serveur les données (et on y écrit) avec les mêmes nœuds que pour le M340 bien sûr sauf que cela se passe sur l’hôte local (localhost : 127.0.0.1).

On obtient le tableau de bord suivant :

Et Modbus Poll affiche bien les données attendues !

Cordialement,
Stéphane

Raspberry Pi : un tableau de bord pour votre automate Modbus

Bonjour,

L’article précédent montrait la réalisation d’un tableau de bord pour un serveur Modbus TCP un peu particulier avec Node-RED.

Il est bien sûr possible d’appliquer ces mêmes technologies pour créer un tableau de bord quel que soit le serveur Modbus TCP ou l’esclave Modbus RTU.

En l’occurrence, j’ai à ma disposition un automate M340 de SCHNEIDER ELECTRIC, muni d’un coupleur NOC04101, et qui possède, entre autres, la fonctionnalité serveur Modbus TCP et le « flow » ci-après réalise les opérations suivantes :

D’une part, on lit et on affiche dans une jauge la valeur du %MW200.
On trouve donc les nœuds suivants :

  • un nœud qui effectue la lecture du %MW200, fonction FC3, lecture de registres internes,
  • un autre, l’extraction et la mise en forme de la donnée pour la jauge,
  • et la jauge elle-même.

D’autre part, un potentiomètre permet de régler la valeur du %MW210.
Il y a ainsi :

  • le nœud du potentiomètre,
  • et celui réalise l’écriture du %MW210, FC6, écriture d’un registre interne.

Le code de la fonction d’extraction est très simple, la requête FC3 remonte un tableau de données et l’on ne s’intéresse qu’à la première :

var msg_out = {payload:msg.payload[0]};
return msg_out;

Le programme automate se contente de recopier la valeur du %MW210 dans le %MW200 pour les besoins de ma démonstration.

[
    {
        "id": "43e491b2.0b35a8",
        "type": "tab",
        "label": "Modbus",
        "disabled": false,
        "info": ""
    },
    {
        "id": "6ca8bbe5.c10cfc",
        "type": "modbus-read",
        "z": "43e491b2.0b35a8",
        "name": "",
        "topic": "",
        "showStatusActivities": false,
        "showErrors": false,
        "unitid": "",
        "dataType": "HoldingRegister",
        "adr": "200",
        "quantity": "1",
        "rate": "1",
        "rateUnit": "s",
        "delayOnStart": false,
        "startDelayTime": "",
        "server": "43c0221.3e519dc",
        "x": 150,
        "y": 100,
        "wires": [
            [
                "d1453b66.31e57",
                "74b2d916.437c08"
            ],
            []
        ]
    },
    {
        "id": "d1453b66.31e57",
        "type": "debug",
        "z": "43e491b2.0b35a8",
        "name": "",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "x": 350,
        "y": 100,
        "wires": []
    },
    {
        "id": "35d64d2a.3a0dfa",
        "type": "ui_gauge",
        "z": "43e491b2.0b35a8",
        "name": "Modbus Gauge",
        "group": "338d75bf.8d7ca2",
        "order": 1,
        "width": 0,
        "height": 0,
        "gtype": "gage",
        "title": "MW200 Gauge",
        "label": "units",
        "format": "{{value}}",
        "min": 0,
        "max": "65535",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "",
        "seg2": "",
        "x": 500,
        "y": 180,
        "wires": []
    },
    {
        "id": "74b2d916.437c08",
        "type": "function",
        "z": "43e491b2.0b35a8",
        "name": "",
        "func": "var msg_out = {payload:msg.payload[0]};\nreturn msg_out;",
        "outputs": 1,
        "noerr": 0,
        "x": 330,
        "y": 140,
        "wires": [
            [
                "35d64d2a.3a0dfa",
                "de0996f0.d1aef"
            ]
        ]
    },
    {
        "id": "de0996f0.d1aef",
        "type": "debug",
        "z": "43e491b2.0b35a8",
        "name": "",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "x": 490,
        "y": 140,
        "wires": []
    },
    {
        "id": "82cf117c.244d88",
        "type": "modbus-write",
        "z": "43e491b2.0b35a8",
        "name": "",
        "showStatusActivities": false,
        "showErrors": false,
        "unitid": "",
        "dataType": "HoldingRegister",
        "adr": "210",
        "quantity": "1",
        "server": "43c0221.3e519dc",
        "x": 500,
        "y": 320,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "7fad2d38.5ba184",
        "type": "ui_slider",
        "z": "43e491b2.0b35a8",
        "name": "Modbus MW210 Slider",
        "label": "MW210 Slider",
        "group": "338d75bf.8d7ca2",
        "order": 0,
        "width": 0,
        "height": 0,
        "passthru": true,
        "topic": "",
        "min": 0,
        "max": "65535",
        "step": 1,
        "x": 160,
        "y": 320,
        "wires": [
            [
                "82cf117c.244d88"
            ]
        ]
    },
    {
        "id": "43c0221.3e519dc",
        "type": "modbus-client",
        "z": "43e491b2.0b35a8",
        "name": "M340",
        "clienttype": "tcp",
        "bufferCommands": true,
        "stateLogEnabled": false,
        "tcpHost": "192.168.1.100",
        "tcpPort": "502",
        "tcpType": "DEFAULT",
        "serialPort": "/dev/ttyUSB",
        "serialType": "RTU-BUFFERD",
        "serialBaudrate": "9600",
        "serialDatabits": "8",
        "serialStopbits": "1",
        "serialParity": "none",
        "serialConnectionDelay": "100",
        "unit_id": 1,
        "commandDelay": 1,
        "clientTimeout": 1000,
        "reconnectTimeout": 2000
    },
    {
        "id": "338d75bf.8d7ca2",
        "type": "ui_group",
        "z": "43e491b2.0b35a8",
        "name": "Modbus",
        "tab": "52e4f040.d2daf",
        "order": 1,
        "disp": true,
        "width": "6"
    },
    {
        "id": "52e4f040.d2daf",
        "type": "ui_tab",
        "z": "43e491b2.0b35a8",
        "name": "Modbus",
        "icon": "dashboard",
        "order": 3
    }
]

Le code du flow est disponible ici :
Node-RED-M340-OMB0

Et le tableau de bord résultant ressemble à ceci :

Je suis certain que vous voyez déjà toutes les opportunités qui s’offrent à vous pour un investissement somme toute modique.

Avec un œil sur votre automate depuis n’importe quel navigateur, de votre bureau ou sur votre mobile, vous pourrez vous consacrer à d’autres tâches comme apprendre Ada 😉 :
https://learn.adacore.com/index.html#

Bien sûr, lorsque vous vous serez fait la main sur votre Raspberry Pi, vous pourrez compter sur une version industrialisée de celui ci comme le netPI CORE 3 :
https://www.hilscher.com/products/product-groups/industrial-internet-industry-40/netiot-edge/niot-e-npi3-en/

Cordialement,
Stéphane

Raspberry Pi 3 / SenseHAT / Node-RED

Bonjour,

J’ai eu l’occasion de travailler sur un sujet déjà évoqué, le Raspberry Pi équipé d’un SenseHAT, un matériel avec lequel j’ai voulu faire une démonstration de Node-RED.

Node-RED est un logiciel qui permet de réaliser une programmation graphique à l’aide de blocs ou nœuds issus d’une bibliothèque standard qui peut être étendue.

Il existe à ce jour une grande variété de nœuds :
https://flows.nodered.org

Il est aussi bien sûr possible de créer ses propres blocs :
https://nodered.org/docs/creating-nodes

L’exemple qui va suivre montre l’utilisation d’un bloc « Function » qui permet de créer donc une fonction en JavaScript comme expliqué ici :
https://nodered.org/docs/writing-functions

Node-RED est disponible dans Raspbian, la distribution standard sur le Raspberry Pi, ainsi que sur la gamme de produits netIOT Edge de Hilscher.

Un site est dédié à l’offre spécifique Industrial Internet et Industry 4.0 : netIOT – Industrial Cloud Communication

Il existe des nœuds pour gérer le protocole Modbus dans Node-RED :
https://flows.nodered.org/node/node-red-contrib-modbus

Nous pourrons donc développer une application Node-RED au-dessus du serveur Modbus TCP développé en Python et utilisé dans l’article précédent.

Pour réaliser une interface graphique simple, un tableau de bord, il est possible d’utiliser des composants issus de la bibliothèque suivante :
https://github.com/node-red/node-red-dashboard

J’ai repris le code Python pour pouvoir piloter la matrice de LEDs.
Le code ajouté permet uniquement de modifier la couleur de l’ensemble des LEDs via Modbus TCP.
Cela suffit à ma démonstration, c’est plus joli !

#!/usr/bin/env python
# -*- coding: utf_8 -*-
"""
 Modbus TestKit: Implementation of Modbus protocol in python

 (C)2009 - Luc Jean - luc.jean@gmail.com
 (C)2009 - Apidev - http://www.apidev.fr

 This is distributed under GNU LGPL license, see license.txt
"""


import sys
import struct
import numpy as np

import modbus_tk
import modbus_tk.defines as cst
from modbus_tk import modbus_tcp

"""from sense_emu import SenseHat"""
from sense_hat import SenseHat

import kbhit, time;

sense = SenseHat()

def main():
    """main"""

    kbhit.init();
    running = True;

    logger = modbus_tk.utils.create_logger(name="console", record_format="%(message)s")

    screen = np.zeros((8, 8, 3), dtype=np.uint8)
    actual_color_value1 = int(0)
    actual_color_value2 = int(0)
    actual_color_value3 = int(0)

    try:
        #Create the server
        server = modbus_tcp.TcpServer(port=1502)
        logger.info("running...")
        logger.info("enter 'q' for closing the server")

        server.start()

        slave_1 = server.add_slave(1)
        slave_1.add_block('0', cst.HOLDING_REGISTERS, 0, 100)

        while running:
            if kbhit.kbhit():
                ch = kbhit.getch();
                if 'q' == ch:
                    running = False;

            north = sense.compass;
            """print("North: %s" %north);"""
           
            slave_1.set_values('0', 0, struct.unpack('>HH', struct.pack('>f', sense.temp)))
            slave_1.set_values('0', 2, struct.unpack('>HH', struct.pack('>f', sense.pressure)))
            slave_1.set_values('0', 4, struct.unpack('>HH', struct.pack('>f', sense.humidity)))
            slave_1.set_values('0', 6, struct.unpack('>HH', struct.pack('>f', north)))

            out_values = slave_1.get_values('0', 20, 3)

            color_value1 = out_values[0]
            color_value2 = out_values[1]
            color_value3 = out_values[2]

            if actual_color_value1 != color_value1 or actual_color_value2 != color_value2 or actual_color_value3 != color_value3 :
                color = (color_value1, color_value2, color_value3)
                screen[0:8, 0:8, :] = color
                sense.set_pixels([pixel for row in screen for pixel in row])
                actual_color_value1 = color_value1
                actual_color_value2 = color_value2
                actual_color_value3 = color_value3

            """time.sleep(0.05);"""

    finally:
        server.stop()
        kbhit.restore();

if __name__ == "__main__":
    main()

Avec ce petit bout de code on peut piloter le SenseHAT depuis n’importe quel Client Modbus TCP, comme un automate, « Ada for Automation »…

… ou Node-RED donc :

Ci-dessous une vue de l’éditeur Node-RED tournant dans un navigateur, Firefox ici en l’occurrence :

Il est possible d’exporter des portions de programme au format JSON.
Ci-dessous le « flow » exporté dans le presse-papiers et passé à la moulinette pour le rendre joli :
https://jsonformatter.org/json-pretty-print

[
  {
    "id": "fd47437e.e21aa",
    "type": "tab",
    "label": "SenseHAT",
    "disabled": false,
    "info": ""
  },
  {
    "id": "77438b0a.41299c",
    "type": "modbus-read",
    "z": "fd47437e.e21aa",
    "name": "ReadSenseHAT",
    "topic": "",
    "showStatusActivities": true,
    "showErrors": true,
    "unitid": "",
    "dataType": "HoldingRegister",
    "adr": "0",
    "quantity": "10",
    "rate": "1000",
    "rateUnit": "ms",
    "delayOnStart": false,
    "startDelayTime": "",
    "server": "b96d491a.dcdc78",
    "useIOFile": false,
    "ioFile": "",
    "useIOForPayload": false,
    "x": 120,
    "y": 140,
    "wires": [
      [
        "4a16a9de.7c964"
      ],
      [
        "50d27a17.1ae20c",
        "f74fbbe6.028478"
      ]
    ]
  },
  {
    "id": "4a16a9de.7c964",
    "type": "debug",
    "z": "fd47437e.e21aa",
    "name": "",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "false",
    "x": 330,
    "y": 80,
    "wires": []
  },
  {
    "id": "50d27a17.1ae20c",
    "type": "debug",
    "z": "fd47437e.e21aa",
    "name": "",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "false",
    "x": 330,
    "y": 120,
    "wires": []
  },
  {
    "id": "f74fbbe6.028478",
    "type": "function",
    "z": "fd47437e.e21aa",
    "name": "DataSplitter",
    "func": "var Temperature = { payload:msg.payload.buffer.readFloatBE(0) };\nvar Pressure = { payload:msg.payload.buffer.readFloatBE(4) };\nvar Humidity = { payload:msg.payload.buffer.readFloatBE(8) };\nvar Orientation = { payload:msg.payload.buffer.readFloatBE(12) };\nreturn [ Temperature, Pressure, Humidity, Orientation ];",
    "outputs": 4,
    "noerr": 0,
    "x": 330,
    "y": 180,
    "wires": [
      [
        "9ad2be63.c8adc",
        "684f463e.a67e08"
      ],
      [
        "2526530a.198c7c"
      ],
      [
        "5f81af96.9837a"
      ],
      [
        "ddd2a50b.358408"
      ]
    ]
  },
  {
    "id": "9ad2be63.c8adc",
    "type": "debug",
    "z": "fd47437e.e21aa",
    "name": "",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "false",
    "x": 570,
    "y": 160,
    "wires": []
  },
  {
    "id": "66618513.e64734",
    "type": "modbus-write",
    "z": "fd47437e.e21aa",
    "name": "WriteSenseHAT",
    "showStatusActivities": true,
    "showErrors": true,
    "unitid": "",
    "dataType": "MHoldingRegisters",
    "adr": "20",
    "quantity": "3",
    "server": "b96d491a.dcdc78",
    "x": 560,
    "y": 620,
    "wires": [
      [
        "3941225c.7f078e"
      ],
      []
    ]
  },
  {
    "id": "3941225c.7f078e",
    "type": "debug",
    "z": "fd47437e.e21aa",
    "name": "",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "false",
    "x": 750,
    "y": 620,
    "wires": []
  },
  {
    "id": "5a82fd29.44ccac",
    "type": "function",
    "z": "fd47437e.e21aa",
    "name": "RGB",
    "func": "var msg_out = { payload:[] };\nmsg_out.payload[0] = msg.payload.r; \nmsg_out.payload[1] = msg.payload.g; \nmsg_out.payload[2] = msg.payload.b; \nreturn msg_out;",
    "outputs": 1,
    "noerr": 0,
    "x": 410,
    "y": 620,
    "wires": [
      [
        "66618513.e64734"
      ]
    ]
  },
  {
    "id": "b461cb2e.d156",
    "type": "ui_colour_picker",
    "z": "fd47437e.e21aa",
    "name": "",
    "label": "LED",
    "group": "be67e649.357968",
    "format": "rgb",
    "outformat": "object",
    "showSwatch": true,
    "showPicker": true,
    "showValue": true,
    "showHue": false,
    "showAlpha": false,
    "showLightness": true,
    "dynOutput": "false",
    "order": 0,
    "width": 0,
    "height": 0,
    "passthru": true,
    "topic": "",
    "x": 270,
    "y": 620,
    "wires": [
      [
        "5a82fd29.44ccac"
      ]
    ]
  },
  {
    "id": "99dde901.715ce",
    "type": "ui_gauge",
    "z": "fd47437e.e21aa",
    "name": "Temperature Gauge",
    "group": "7e7ef767.3e339",
    "order": 1,
    "width": 0,
    "height": 0,
    "gtype": "gage",
    "title": "Temperature",
    "label": "°C",
    "format": "{{value | number:1}}",
    "min": "-30",
    "max": "100",
    "colors": [
      "#00b500",
      "#e6e600",
      "#ca3838"
    ],
    "seg1": "",
    "seg2": "",
    "x": 780,
    "y": 220,
    "wires": []
  },
  {
    "id": "a0adf36d.51b37",
    "type": "ui_gauge",
    "z": "fd47437e.e21aa",
    "name": "Humidity Gauge",
    "group": "26985721.743b18",
    "order": 1,
    "width": 0,
    "height": 0,
    "gtype": "gage",
    "title": "Humidity",
    "label": "%",
    "format": "{{value | number:1}}",
    "min": 0,
    "max": "100",
    "colors": [
      "#00b500",
      "#e6e600",
      "#ca3838"
    ],
    "seg1": "",
    "seg2": "",
    "x": 760,
    "y": 420,
    "wires": []
  },
  {
    "id": "9922e191.af4fc8",
    "type": "ui_gauge",
    "z": "fd47437e.e21aa",
    "name": "Orientation",
    "group": "d4761c71.80721",
    "order": 4,
    "width": 0,
    "height": 0,
    "gtype": "compass",
    "title": "Orientation",
    "label": "°",
    "format": "{{value | number:1}}",
    "min": "0",
    "max": "360",
    "colors": [
      "#00b500",
      "#e6e600",
      "#ca3838"
    ],
    "seg1": "",
    "seg2": "",
    "x": 750,
    "y": 520,
    "wires": []
  },
  {
    "id": "ba444fa5.e56808",
    "type": "ui_chart",
    "z": "fd47437e.e21aa",
    "name": "Pressure Chart",
    "group": "a3b17833.ce9cf8",
    "order": 3,
    "width": 0,
    "height": 0,
    "label": "Pressure",
    "chartType": "line",
    "legend": "false",
    "xformat": "HH:mm:ss",
    "interpolate": "linear",
    "nodata": "no data",
    "dot": false,
    "ymin": "",
    "ymax": "",
    "removeOlder": 1,
    "removeOlderPoints": "",
    "removeOlderUnit": "3600",
    "cutout": 0,
    "useOneColor": false,
    "colors": [
      "#1f77b4",
      "#aec7e8",
      "#ff7f0e",
      "#2ca02c",
      "#98df8a",
      "#d62728",
      "#ff9896",
      "#9467bd",
      "#c5b0d5"
    ],
    "useOldStyle": false,
    "x": 760,
    "y": 360,
    "wires": [
      [],
      []
    ]
  },
  {
    "id": "684f463e.a67e08",
    "type": "rbe",
    "z": "fd47437e.e21aa",
    "name": "",
    "func": "deadbandEq",
    "gap": "0.1",
    "start": "",
    "inout": "out",
    "property": "payload",
    "x": 570,
    "y": 220,
    "wires": [
      [
        "99dde901.715ce",
        "dea80a9.06a4af8"
      ]
    ]
  },
  {
    "id": "5f81af96.9837a",
    "type": "rbe",
    "z": "fd47437e.e21aa",
    "name": "",
    "func": "deadbandEq",
    "gap": "0.1",
    "start": "",
    "inout": "out",
    "property": "payload",
    "x": 570,
    "y": 420,
    "wires": [
      [
        "a0adf36d.51b37",
        "af44a971.a42448"
      ]
    ]
  },
  {
    "id": "2526530a.198c7c",
    "type": "rbe",
    "z": "fd47437e.e21aa",
    "name": "",
    "func": "deadbandEq",
    "gap": "0.1",
    "start": "",
    "inout": "out",
    "property": "payload",
    "x": 570,
    "y": 320,
    "wires": [
      [
        "ba444fa5.e56808",
        "ea0515e0.796008"
      ]
    ]
  },
  {
    "id": "ddd2a50b.358408",
    "type": "rbe",
    "z": "fd47437e.e21aa",
    "name": "",
    "func": "deadbandEq",
    "gap": "0.1",
    "start": "",
    "inout": "out",
    "property": "payload",
    "x": 570,
    "y": 520,
    "wires": [
      [
        "9922e191.af4fc8"
      ]
    ]
  },
  {
    "id": "9a71ea81.cac458",
    "type": "ui_button",
    "z": "fd47437e.e21aa",
    "name": "",
    "group": "be67e649.357968",
    "order": 0,
    "width": 0,
    "height": 0,
    "passthru": false,
    "label": "Red",
    "color": "",
    "bgcolor": "Red",
    "icon": "",
    "payload": "{"r":255,"g":0,"b":0,"a":1}",
    "payloadType": "json",
    "topic": "",
    "x": 90,
    "y": 580,
    "wires": [
      [
        "b461cb2e.d156"
      ]
    ]
  },
  {
    "id": "6d6a4864.12d508",
    "type": "ui_button",
    "z": "fd47437e.e21aa",
    "name": "",
    "group": "be67e649.357968",
    "order": 0,
    "width": 0,
    "height": 0,
    "passthru": false,
    "label": "Green",
    "color": "",
    "bgcolor": "Green",
    "icon": "",
    "payload": "{"r":0,"g":255,"b":0,"a":1}",
    "payloadType": "json",
    "topic": "",
    "x": 90,
    "y": 620,
    "wires": [
      [
        "b461cb2e.d156"
      ]
    ]
  },
  {
    "id": "e63c60a8.faae7",
    "type": "ui_button",
    "z": "fd47437e.e21aa",
    "name": "",
    "group": "be67e649.357968",
    "order": 0,
    "width": 0,
    "height": 0,
    "passthru": false,
    "label": "Blue",
    "color": "",
    "bgcolor": "Blue",
    "icon": "",
    "payload": "{"r":0,"g":0,"b":255,"a":1}",
    "payloadType": "json",
    "topic": "",
    "x": 90,
    "y": 660,
    "wires": [
      [
        "b461cb2e.d156"
      ]
    ]
  },
  {
    "id": "dea80a9.06a4af8",
    "type": "ui_chart",
    "z": "fd47437e.e21aa",
    "name": "Temperature Chart",
    "group": "7e7ef767.3e339",
    "order": 2,
    "width": 0,
    "height": 0,
    "label": "Temperature",
    "chartType": "line",
    "legend": "false",
    "xformat": "HH:mm:ss",
    "interpolate": "linear",
    "nodata": "no data",
    "dot": false,
    "ymin": "",
    "ymax": "",
    "removeOlder": 1,
    "removeOlderPoints": "",
    "removeOlderUnit": "3600",
    "cutout": 0,
    "useOneColor": false,
    "colors": [
      "#1f77b4",
      "#aec7e8",
      "#ff7f0e",
      "#2ca02c",
      "#98df8a",
      "#d62728",
      "#ff9896",
      "#9467bd",
      "#c5b0d5"
    ],
    "useOldStyle": false,
    "x": 770,
    "y": 260,
    "wires": [
      [],
      []
    ]
  },
  {
    "id": "ea0515e0.796008",
    "type": "ui_gauge",
    "z": "fd47437e.e21aa",
    "name": "Pressure Gauge",
    "group": "a3b17833.ce9cf8",
    "order": 1,
    "width": 0,
    "height": 0,
    "gtype": "gage",
    "title": "Pressure",
    "label": "mbar",
    "format": "{{value | number:1}}",
    "min": "260",
    "max": "1260",
    "colors": [
      "#00b500",
      "#e6e600",
      "#ca3838"
    ],
    "seg1": "",
    "seg2": "",
    "x": 770,
    "y": 320,
    "wires": []
  },
  {
    "id": "af44a971.a42448",
    "type": "ui_chart",
    "z": "fd47437e.e21aa",
    "name": "Humidity Chart",
    "group": "26985721.743b18",
    "order": 3,
    "width": 0,
    "height": 0,
    "label": "Humidity",
    "chartType": "line",
    "legend": "false",
    "xformat": "HH:mm:ss",
    "interpolate": "linear",
    "nodata": "no data",
    "dot": false,
    "ymin": "",
    "ymax": "",
    "removeOlder": 1,
    "removeOlderPoints": "",
    "removeOlderUnit": "3600",
    "cutout": 0,
    "useOneColor": false,
    "colors": [
      "#1f77b4",
      "#aec7e8",
      "#ff7f0e",
      "#2ca02c",
      "#98df8a",
      "#d62728",
      "#ff9896",
      "#9467bd",
      "#c5b0d5"
    ],
    "useOldStyle": false,
    "x": 760,
    "y": 460,
    "wires": [
      [],
      []
    ]
  },
  {
    "id": "fa47ae1e.212af",
    "type": "ui_button",
    "z": "fd47437e.e21aa",
    "name": "",
    "group": "be67e649.357968",
    "order": 0,
    "width": 0,
    "height": 0,
    "passthru": false,
    "label": "Black",
    "color": "",
    "bgcolor": "Black",
    "icon": "",
    "payload": "{"r":0,"g":0,"b":0,"a":1}",
    "payloadType": "json",
    "topic": "",
    "x": 90,
    "y": 700,
    "wires": [
      [
        "b461cb2e.d156"
      ]
    ]
  },
  {
    "id": "5768184e.c56d7",
    "type": "ui_button",
    "z": "fd47437e.e21aa",
    "name": "",
    "group": "be67e649.357968",
    "order": 0,
    "width": 0,
    "height": 0,
    "passthru": false,
    "label": "White",
    "color": "Black",
    "bgcolor": "White",
    "icon": "",
    "payload": "{"r":255,"g":255,"b":255,"a":1}",
    "payloadType": "json",
    "topic": "",
    "x": 90,
    "y": 740,
    "wires": [
      [
        "b461cb2e.d156"
      ]
    ]
  },
  {
    "id": "b96d491a.dcdc78",
    "type": "modbus-client",
    "z": "",
    "name": "Sense-OMB",
    "clienttype": "tcp",
    "bufferCommands": true,
    "stateLogEnabled": false,
    "tcpHost": "127.0.0.1",
    "tcpPort": "1502",
    "tcpType": "DEFAULT",
    "serialPort": "/dev/ttyUSB",
    "serialType": "RTU-BUFFERD",
    "serialBaudrate": "9600",
    "serialDatabits": "8",
    "serialStopbits": "1",
    "serialParity": "none",
    "serialConnectionDelay": "100",
    "unit_id": 1,
    "commandDelay": 1,
    "clientTimeout": 1000,
    "reconnectTimeout": 2000
  },
  {
    "id": "be67e649.357968",
    "type": "ui_group",
    "z": "",
    "name": "Output",
    "tab": "8664d22c.b212",
    "order": 5,
    "disp": true,
    "width": "6",
    "collapse": false
  },
  {
    "id": "7e7ef767.3e339",
    "type": "ui_group",
    "z": "",
    "name": "Temperature",
    "tab": "8664d22c.b212",
    "order": 1,
    "disp": true,
    "width": "6",
    "collapse": false
  },
  {
    "id": "26985721.743b18",
    "type": "ui_group",
    "z": "",
    "name": "Humidity",
    "tab": "8664d22c.b212",
    "order": 3,
    "disp": true,
    "width": "6",
    "collapse": false
  },
  {
    "id": "d4761c71.80721",
    "type": "ui_group",
    "z": "",
    "name": "Inertial Measurement Unit",
    "tab": "8664d22c.b212",
    "order": 4,
    "disp": true,
    "width": "6",
    "collapse": false
  },
  {
    "id": "a3b17833.ce9cf8",
    "type": "ui_group",
    "z": "",
    "name": "Pressure",
    "tab": "8664d22c.b212",
    "order": 2,
    "disp": true,
    "width": "6",
    "collapse": false
  },
  {
    "id": "8664d22c.b212",
    "type": "ui_tab",
    "z": "",
    "name": "SenseHAT",
    "icon": "dashboard",
    "order": 1
  }
]

Ce « flow » procède donc à une lecture des mesures disponibles dans le SenseHAT, avec un nœud qui effectue une requête FC3, (lecture de registres), et une fonction (DataSplitter) qui découpe la réponse en ses composantes.

Une fonction prend un message en entrée et peut retourner un ou plusieurs messages en sortie.

Ci-gît le code du DataSplitter :

var Temperature = { payload:msg.payload.buffer.readFloatBE(0) };
var Pressure = { payload:msg.payload.buffer.readFloatBE(4) };
var Humidity = { payload:msg.payload.buffer.readFloatBE(8) };
var Orientation = { payload:msg.payload.buffer.readFloatBE(12) };
return [ Temperature, Pressure, Humidity, Orientation ];

Une mesure physique évolue sans cesse, les blocs « deadband » permettent de limiter le rafraichissement de l’interface utilisateur qui pourrait saturer si l’on augmentait la fréquence de la lecture.
Comme l’UI n’affiche qu’un chiffre après la virgule, le « deadband » est configuré pour ne laisser passer la valeur que si l’écart avec la valeur précédente est supérieur ou égal à 0.1.

Enfin, la mesure est affichée grâce aux composants graphiques du tableau de bord, jauges et courbes de tendance.

Pour ce qui est des LEDs, il est possible de choisir une couleur dans la palette ou une présélection avec les boutons.

Les composantes Rouge, Vert et Bleu sont mises en forme par la fonction RGB :

var msg_out = { payload:[] };
msg_out.payload[0] = msg.payload.r;
msg_out.payload[1] = msg.payload.g;
msg_out.payload[2] = msg.payload.b;
return msg_out;

Elles sont pour finir transmises au SenseHAT avec la fonction FC16 (écriture de registres).

Et ça nous donne un superbe tableau de bord :

Mon WordPress ne permet pas que je vous donne les fichiers .py, sécurité oblige.

Veuillez trouver ci-dessous les liens vers les fichiers Python et Node-RED :

Node-RED exporte tout sur une seule ligne en version compact :
Node-RED 20181121

Le même fichier passé à la moulinette ou exporté en version formatée :
Node-RED 20181121-1

Le lien que j’avais fourni n’est plus d’actualité :
kbhit.py

Le serveur Modbus TCP / Sense HAT :
sense-omb.py

Cordialement,
Stéphane

Raspberry Pi 3 / SenseHAT / netHAT / netPi

Bonjour,

Je cherchais pour mes expériences une solution qui soit ludique, et aussi accessible à tout un chacun, et mes tribulations m’ont conduit à sélectionner un combo Raspberry Pi 3 + SenseHAT.

Hilscher France a donc fait l’acquisition de ces éléments chez KUBII :

Starter Kit Officiel Pi3
https://www.kubii.fr/fr/kits-raspberry-pi/1637-kit-demarrage-raspberry-pi3-3272496004207.html

Raspberry Pi Sense Hat
https://www.kubii.fr/fr/cartes-extension-cameras-raspberry-pi/1081-raspberry-pi-sense-hat-640522710799.html

Pourquoi donc un tel investissement me direz vous ?

Sans doute parce que depuis quelques temps cette plateforme économique et performante permet d’imaginer tout un tas d’utilisations dans ce qu’il est convenu d’appeler l’IoT, l’IIoT, Industry 4.0, etc…

Aussi, à l’instar de certains confrères, la société Hilscher a développé certains produits autour de cette plateforme comme le netHAT, le netPi et le Edge Gateway « Connect » :
https://www.netiot.com/interface/nethat/
https://www.netiot.com/netpi/industrial-raspberry-pi-3/
https://www.netiot.com/edge/

Hilscher innove en testant le canal de vente Amazon et l’on peut y acheter le netHAT et le netPi :
https://www.amazon.fr/Hilscher-NXHAT52-RTE-nethat-52-de-RTE/dp/B01MFH0FP9
https://www.amazon.fr/Industrial-Raspberry-Industry-Communication-4×1-2Ghz-Real-Time-Ethernet/dp/B0756XD2CN

Pour ces produits, Hilscher France n’assure pas de support.
Cependant, l’on peut vous proposer une formation !

Le netHAT est fourni avec un pilote Linux compilé, des firmwares en version limitée à 32 octets d’E/S pour EtherCAT, Ethernet/IP et PROFINET IO Device et bien sûr de la documentation.

Le tout s’installe sans encombre sur la Raspbian et ça tombe en marche comme sur le plan.

Vous pouvez donc vous familiariser avec la technologie Hilscher pour une quarantaine d’euros, ce qui est modique vous en conviendrez aisément.

Bien sûr, « Ada for Automation » peut tout à fait être utilisé avec le netHAT. J’y reviendrai bien sûr.

Avec un Raspberry Pi + un netHAT, on peut aussi tester le concept netPi et développer des applications qui tourneront sur le netPi sans modification.

Bref, je souhaitais monter une manipulation avec un Rasberry Pi 3, un SenseHAT pour des capteurs pas chers et un netHAT pour connecter ce bijou de technologie à votre automate préféré.

En fait, le SenseHAT et le netHAT ne peuvent pas se monter l’un sur l’autre comme on pourrait le penser de prime abord.

Je pensais développer un binding Ada pour le SenseHAT mais ce n’est pas si simple.
Le langage choisi par l’équipe Raspberry est plutôt le Python et la plupart des bibliothèques fournies pour les « HAT » sont en Python.
La bibliothèque disponible pour le SenseHAT est donc en Python aussi et utilise d’une part une bibliothèque en C++ qui gère nombre de capteurs et d’autre part le framebuffer pour les LEDs et un IO device pour le joystick.

Monsieur Phil Munts, que je remercie, m’a bien fait part de sa librairie :
http://git.munts.com/libsimpleio/ada/

Mais je voulais quelque chose de super vite fait et j’ai penché pour une solution mettant en œuvre un framework Modbus développé par un collègue, Monsieur Luc JEAN, que je remercie chaleureusement :
https://github.com/ljean/modbus-tk

Pourquoi donc ? « Ada for Automation » disposant de la fonctionnalité Modbus TCP Client et le framework permettant de réaliser très simplement un serveur Modbus TCP, il suffisait donc de raccrocher les données des capteurs dans les registres du serveur.

J’ai utilisé également ce bout de code qui m’a bien aidé, je remercie aussi son auteur :
https://frank-deng.github.io/python-kbhit.en.html

Le SenseHAT dispose d’une application de simulation avec interface graphique et il est possible d’utiliser celle-ci en lieu et place du matériel tel que dans l’exemple suivant.

SenseHAT Simu GUI
SenseHAT Simu GUI

On y démarre depuis un terminal :

python 3 sense-omb.py
python 3 sense-omb.py

Et on teste par exemple avec Modbus Poll :

Modbus Poll Example
Modbus Poll Example

En Python, c’est une vingtaine de lignes de code pour remonter température, pression, hygrométrie et cap :

#!/usr/bin/env python
# -*- coding: utf_8 -*-
"""
 Modbus TestKit: Implementation of Modbus protocol in python

 (C)2009 - Luc Jean - luc.jean@gmail.com
 (C)2009 - Apidev - http://www.apidev.fr

 This is distributed under GNU LGPL license, see license.txt
"""


import sys
import struct

import modbus_tk
import modbus_tk.defines as cst
from modbus_tk import modbus_tcp

from sense_emu import SenseHat

import kbhit, time;

sense = SenseHat()

def main():
    """main"""

    kbhit.init();
    running = True;

    logger = modbus_tk.utils.create_logger(name="console", record_format="%(message)s")

    try:
        #Create the server
        server = modbus_tcp.TcpServer(port=1502)
        logger.info("running...")
        logger.info("enter 'q' for closing the server")

        server.start()

        slave_1 = server.add_slave(1)
        slave_1.add_block('0', cst.HOLDING_REGISTERS, 0, 100)

        while running:
            if kbhit.kbhit():
                ch = kbhit.getch();
                if 'q' == ch:
                    running = False;

            slave_1.set_values('0', 0, struct.unpack('>HH', struct.pack('>f', sense.temp)))
            slave_1.set_values('0', 2, struct.unpack('>HH', struct.pack('>f', sense.pressure)))
            slave_1.set_values('0', 4, struct.unpack('>HH', struct.pack('>f', sense.humidity)))
            slave_1.set_values('0', 6, struct.unpack('>HH', struct.pack('>f', sense.compass)))

            time.sleep(0.1);

    finally:
        server.stop()
        kbhit.restore();

if __name__ == "__main__":
    main()

C’est naturellement pas très temps réel mais c’est très bien pour mon cas d’école.

Et en plus, on peut facilement imaginer de reproduire ce schéma avec d’autres HATs comme le « Automation HAT » par exemple :
https://shop.pimoroni.com/products/automation-hat

Vous pouvez donc remonter les données du SenseHAT vers votre automate préféré disposant d’une connectivité Modbus TCP Client.
Bon, en travaillant un peu, ça doit fonctionner dans les deux sens, hein !

Quid du netHAT ? Si on utilise l’application de simulation SenseHAT on peut bien sûr le mettre sur le même Raspberry Pi et se connecter en local.
Si l’on souhaite de vraies données physiques, il faudra l’installer sur un autre Raspberry Pi, et le faire communiquer avec le premier, toujours en Modbus TCP.
C’est trivial avec « Ada for Automation » et je vous le montrerai ce tantôt.

Cordialement,
Stéphane

Un portail de démo pour « Ada for Automation »

Bonjour,

Il a déjà été question dans ces pages de « Ada for Automation » dans le nuage et le site consacré à Gnoga montre un usage du serveur Apache configuré comme frontal (proxy) de démonstration.

J’ai donc marché dans les pas de Gnoga et créé également un portail de démonstration pour « Ada for Automation ».

Ainsi, ce portail présente quelques applications mettant en œuvre bien sûr les technologies web déjà mentionnées, du Modbus TCP en Client / Serveur grâce à libmodbus, et du PROFINET IO Contrôleur et Équipement (Device).

L’on pourra donc interagir avec :

  • une application basique Modbus TCP en Client / Serveur supervisant et contrôlant un « piano » Modbus TCP Serveur,
  • l’application historique App1, Modbus TCP en Client / Serveur, supervisant et contrôlant une application Modbus TCP Serveur simulant la partie opérante,
  • une application basique pilotant en PROFINET le fameux « piano ».

Les deux premières démonstrations ont lieu entièrement dans le cloud, comme évoqué dans les articles précédents.

Pour la troisième démonstration où l’on met en œuvre l’API Hilscher cifX, on utilise un PC industriel sous Debian Linux dans lequel une carte cifX est configurée en contrôleur PROFINET IO et un Raspberry Pi (Raspbian) connecté via une liaison SPI à une carte d’évaluation netRAPID configurée en PROFINET IO Device.

On a donc une application « Ada for Automation » sur le PC au-dessus de la cifX et une autre sur le Raspberry Pi au-dessus du netRAPID, communiquant via la même API.
Ne manquez pas de visiter depuis le menu de l’application les pages d’état où l’on reconnaîtra en particulier les informations du « Common Status » pour l’API cifX.

Cette POC (Proof Of Concept) ne demande qu’à être étoffée.
Faute de matériel plus divers, je pense par exemple à une démonstration tout aussi peu palpable avec l’implémentation Hilscher de PROFIdrive.
Il vous serait ainsi offert de piloter un variateur virtuel dans le cloud, ce qui, admettez, est un peu perché.

Il serait tout aussi possible d’envisager une démonstration avec l’une des nombreuses autres technologies de bus de terrain supportées par Hilscher.

Ouvert à toute forme de collaboration, n’hésitez pas à me faire part de vos besoins si vous souhaitez une manipulation avec un matériel quelconque de votre fourniture.

Cordialement,
Stéphane