Archives de catégorie : Ada4Automation

« Ada for Automation » est un cadriciel pour développer des applications d’automatisme évoluées dans le langage Ada.

A4A : Conteneur Docker

Bonjour,

Docker

J’indiquais dans l’article précédent que, sur les solutions netIOT Edge de Hilscher, Docker permet d’intégrer toutes sortes d’applications pour traiter les données acquises depuis le terrain ou le process avant de fournir celles-ci digérées aux applications hébergées dans le fameux nuage d’où doit pleuvoir la manne.

C’est aussi valable pour le netPI comme pour le Raspberry Pi sur lequel il est tout à fait possible d’installer Docker.

Il est bien sûr indispensable de se familiariser avec Docker puis de consulter la documentation pour comprendre les concepts.

Avec Docker, une application est empaquetée avec l’environnement nécessaire à son exécution dans une image que l’on va pouvoir instancier en conteneur(s) que Docker va exécuter.

On peut voir cela comme une approche composant où l’on isole chaque application et son environnement.

Pour le netPI, Hilscher offre nombre d’images prêtes à l’emploi sur le Hub communautaire où figurent également la plupart des projets  open source.

Ces images sont construites à partir de recettes, les docker files, et tous les fichiers pour les images fournies sont disponibles sur GitHub.

Dockerfile

Afin de me former sur ces technologies bien attirantes il m’est apparu que j’avais un projet candidat à la conteneurisation, Ada for Automation bien sûr !

J’ai donc installé Docker sur mon Raspberry Pi et, en m’inspirant des Dockerfiles de mes collègues, j’en ai écrit un basique que je vous présente ci-dessous.

#use latest armv7hf compatible raspbian OS version from group balena as base image
FROM balenalib/armv7hf-debian:stretch

#enable building ARM container on x86 machinery on the web (comment out next line if built on Raspberry)
#RUN [ "cross-build-start" ]

#labeling
LABEL maintainer="slos@hilscher.com" \
      version="V0.0.1" \
      description="Debian(stretch) / Ada for Automation Development"

#version
ENV ADA_FOR_AUTOMATION_BASIS_DEV_VERSION 0.0.1

#install ssh, gcc, create user "pi" and make him sudo
RUN apt-get update  \
    && apt-get install -y openssh-server build-essential \
    && mkdir /var/run/sshd \
    && useradd --create-home --shell /bin/bash pi \
    && echo 'pi:raspberry' | chpasswd \
    && adduser pi sudo
   
#install git, gnat and gprbuild
RUN apt-get update  \
    && apt-get install git gnat gprbuild

#install libmodbus
RUN apt-get update  \
    && apt-get install libmodbus5 libmodbus-dev

#install Ada for Automation and build some demo applications
RUN mkdir --parents /home/pi/Ada \
    && cd /home/pi/Ada \
    && git clone https://gitlab.com/ada-for-automation/ada-for-automation.git A4A \
    && cd "/home/pi/Ada/A4A/demo/000 a4a-k0-cli" && make

#SSH port
EXPOSE 22

#the entrypoint shall start ssh
ENTRYPOINT ["/usr/sbin/sshd", "-D"]

#set STOPSGINAL
STOPSIGNAL SIGTERM

#stop processing ARM emulation (comment out next line if built on Raspberry)
#RUN [ "cross-build-end" ]

Celui-ci, à partir d’une debian stretch, installe SSH, GCC, Git, les outils pour Ada GNAT, GPRBuild, et la librairie libmodbus.

Pour finir, il tire « Ada for Automation » depuis gitlab et compile la plus simple des applications qui implémente un serveur Modbus TCP et recopie quelques registres en entrée sur quelques registres en sortie.

Image

Construire l’image est trivial. Depuis le répertoire contenant le Dockerfile, il suffit de saisir la commande :

docker build --tag=rpi-a4a-basis-dev:latest .

Cette commande construit donc une image ayant pour nom « rpi-a4a-basis-dev » et taguée « latest » depuis le répertoire courant.

La commande suivante permet de lister les images produites :

docker image ls -a

Conteneur

On peut bien sûr créer aussitôt un conteneur à partir de cette image avec la commande suivante :

docker container create --publish 22:22 -p 1503:1503 --privileged rpi-a4a-basis-dev:latest

On publie le port 22 pour SSH et le port 1503 pour Modbus TCP Serveur et on octroie le privilège de pouvoir tourner avec une priorité temps réel si nécessaire.

Notre conteneur est créé comme nous pouvons le constater avec la commande qui nous donne le nom qui lui a été affecté automatiquement :

docker container ls -a

Et on peut s’en servir dès à présent :

docker container start <nom_du_conteneur>

Un ssh en local ou distant permet de se connecter à notre conteneur, de naviguer en ligne de commande dans l’arborescence et de démarrer l’application de démonstration.

Empiler

Notre conteneur basique permet de réaliser des applications avec une interface en ligne de commande, sans interface graphique basée sur GtkAda ou Gnoga.

Avec Docker, il est facile d’empiler une nouvelle couche pour étendre la fonctionnalité.

Ainsi, le dockerfile suivant ajoute à l’image de base Gnoga et ses dépendances, SimpleComponents et Zanyblue, et compile l’application A4A_Piano qui dispose d’une interface web.

#use latest Ada for Automation Basis Development as base image
FROM rpi-a4a-basis-dev:latest

#enable building ARM container on x86 machinery on the web (comment out next line if built on Raspberry)
#RUN [ "cross-build-start" ]

#labeling
LABEL maintainer="slos@hilscher.com" \
      version="V0.0.1" \
      description="Debian(stretch) / Ada for Automation Web Development"

#version
ENV ADA_FOR_AUTOMATION_WEB_DEV_VERSION 0.0.1

#install Gnoga and dependencies (Simple Components, ZanyBlue)
RUN mkdir --parents /home/pi/Ada/Gnoga \
    &amp;&amp; cd /home/pi/Ada/Gnoga \
    &amp;&amp; git clone https://git.code.sf.net/p/gnoga/code gnoga \
    &amp;&amp; cd /home/pi/Ada/Gnoga/gnoga \
    &amp;&amp; make release \
    &amp;&amp; make install

#build some Ada for Automation Web demo applications
RUN cd "/home/pi/Ada/A4A/demo/010 a4a_piano" &amp;&amp; make

#SSH port
EXPOSE 22

#the entrypoint shall start ssh
ENTRYPOINT ["/usr/sbin/sshd", "-D"]

#set STOPSGINAL
STOPSIGNAL SIGTERM

#stop processing ARM emulation (comment out next line if built on Raspberry)
#RUN [ "cross-build-end" ]

On construit l’image de la même façon que la précédente :

docker build --tag=rpi-a4a-web-dev:latest .

Puis on crée le conteneur avec un port supplémentaire pour le serveur web intégré :

docker container create --publish 22:22 -p 1504:1504 -p 8081:8081 --privileged rpi-a4a-web-dev:latest

On démarre le conteneur :

docker container start <nom_du_conteneur>

Un ssh en local ou distant permet de se connecter à notre conteneur, de naviguer en ligne de commande dans l’arborescence et de démarrer l’application de démonstration.

Et en pointant un navigateur sur l’adresse du site on trouve les pages attendues !

Tandis qu’on peut jouer avec :

Il ne me reste plus qu’à y envoyer dans mon espace du Hub Docker.

Taguons nos images :

docker tag rpi-a4a-basis-dev soloist/rpi-a4a-basis-dev:latest
docker tag rpi-a4a-web-dev soloist/rpi-a4a-web-dev:latest

Connectons nous :

docker login

Et poussons :

docker push soloist/rpi-a4a-basis-dev
docker push soloist/rpi-a4a-web-dev

C’est prêt pour une utilisation avec votre Raspberry Pi ou le netPI !

Cordialement,
Stéphane

A4A : Pilotage d’un variateur en EtherCAT

Bonjour,

J’espérais à voix haute pouvoir réaliser des manipulations avec un variateur communiquant en CANopen ou EtherCAT et mon souhait a été exaucé par un ami travaillant chez INFRANOR à Lourdes qui a bien voulu mettre à ma disposition un banc de test composé d’un variateur et d’un ensemble moteur / codeur… Miracle !

Je l’en remercie très chaleureusement !

Le banc est équipé d’un variateur XtrapulsPac et l’on peut depuis le site télécharger bien sûr la documentation, le logiciel de configuration Gem Drive Studio, le fichier ESI EtherCAT, en bref tout le nécessaire pour la manipulation.

Je n’ai pas rencontré de problème particulier avec Gem Drive Studio et j’ai pu configurer, régler le variateur et me familiariser avec son fonctionnement via la liaison série.

Pour ma démonstration, le PC de test étant un peu ancien, j’ai utilisé une carte Hilscher cifX au format PCI.

On peut télécharger une archive du DVD contenant la documentation, les outils de configuration et les firmwares ici.

Cependant, je préfère me fournir dans la base de connaissance qui dispose des dernières versions…

Ainsi, il est possible de se procurer la dernière version de l’outil de configuration SYCON.net.

Il est aussi possible de récupérer le dernier firmware EtherCAT Master .

On a donc importé dans SYCON.net le fichier ESI du variateur, menu « Network > Import Device Descriptions… », ne pas oublier de sélectionner le type de fichier « EtherCAT DDF », inséré l’esclave EtherCAT sur le réseau de la cifX configurée en EtherCAT Maitre et sélectionné les PDO en entrée et sortie pour un fonctionnement en boucle de vitesse.

Côté EtherCAT Maitre, dans la mémoire process de la carte Hilscher cifX donc, on retrouve les données d’entrée et sortie correspondantes :

Gem Drive Studio comme SYCON.net fonctionnent sous Microsoft Windows(R).

Cependant notre application va s’exécuter sous Linux avec les modifications PREEMPT_RT permettant d’obtenir un comportement temps réel.
Nous récupérerons donc les fichiers de firmware et de configuration pour les disposer dans l’arborescence du pilote cifX pour Linux.

Je travaille principalement sur des systèmes Debian et sur cet OS il existe un noyau Linux déjà compilé avec les patches PREEMPT_RT qui n’est donc qu’à une portée de clics ou d’une ligne de commande.
Cherchez « linux-image-rt » dans votre outil habituel et tant que vous y êtes installez également les en-têtes correspondants qui seront nécessaires pour la compilation du pilote Hilscher cifX qui n’est fourni que sous forme de code source.

Une fois installé, il suffit de redémarrer la machine en sélectionnant ce noyau.

Compilez ensuite le pilote comme expliqué dans la documentation relative. Ce pilote est en deux parties, l’une en espace noyau, uio_netx, qu’il vous faudra recompiler à chaque mise à jour du noyau, l’autre en espace utilisateur, la libcifx, qui est l’API pour votre application.

Voyez également la documentation pour l’utilisation du module si vous voulez vous passer de l’incantation en « root » :

modprobe -v uio_netx

Si tout ce passe bien, l’incantation fournit un résultat tel que :

On a disposé firmware et configuration dans le dossier idoine, par exemple :

Vous avez bien sûr installé « Ada for Automation », les dépendances et les outils pour le développement et vous avez navigué vers le répertoire contenant la démonstration « 122 ap7-wui ».
Un simple « make » doit vous permettre de générer l’application dont il est question dans cet article.

Cette application nécessite des droits spécifiques pour tourner avec des priorités temps réel.
Aussi il faut faire partie du groupe dédié ou être « root » pour pouvoir l’exécuter.

Cela démarre sur les chapeaux de roues en laissant une trace de ce style dans la console :

root@hf-test-2:/home/slos/Ada/A4A/demo/122 app7-wui# ./bin/App7_WUI_cifX
error
Log_Level = LEVEL_ERROR
2018-12-17 14:43:29.94 => A4A.Kernel.Sig_Handler : Waiting Signal Interrupt... Use Ctrl+C to exit
2018-12-17 14:43:29.94 => A4A.Kernel.Main_Task : Main_Task's ID is the_main_task_00005579443C2E50
Started at 2018-12-17 14:43:29.94
2018-12-17 14:43:29.94 => A4A.Generic_Periodic_Task : started !
2018-12-17 14:43:29.94 => A4A.Kernel.Main_Task : Initialising cifX Driver...
2018-12-17 14:43:31.61 => A4A.Kernel.Main_Task : Opening cifX Driver...
2018-12-17 14:43:31.61 => A4A.Kernel.Main_Task : Starting cifX TCP Server...
2018-12-17 14:43:31.61 => A4A.Kernel.Main_Task :
***********************************************
          Driver Information
Version     : cifX Toolkit 1.2.0.0
Board Count :  1
***********************************************

2018-12-17 14:43:31.61 => A4A.Kernel.Main_Task : Waiting for the firmware to start...
2018-12-17 14:43:36.61 => A4A.Kernel.Main_Task : Opening cifX Channel...
2018-12-17 14:43:36.61 => A4A.Kernel.Main_Task :
***********************************************
          Channel Information
Board_Name    : cifX0
Board Alias   : ECM
Device Number :  1250100
Serial Number :  20952

Firmware : EtherCAT Master
Version  :  4. 4. 9(Build  0)
Date     :  2018- 10- 10
***********************************************

2018-12-17 14:43:36.61 => A4A.Kernel.Main_Task : Setting Flag Host Ready...
2018-12-17 14:43:36.61 => A4A.Kernel.Main_Task : Setting Flag Bus On...
2018-12-17 14:43:38.61 => A4A.Kernel.Main_Task : (16#800C0021#)COM-flag not set
2018-12-17 14:43:38.61 => A4A.Kernel.Main_Task : Initialising Channel Messaging...
2018-12-17 14:43:38.61 => A4A.Kernel.Main_Task : Getting DPM IO Info...
2018-12-17 14:43:39.11 => A4A.Kernel.Main_Task :
***********************************************
          DPM IO_Info

Number of IO Block Info    :  2

Input Area :
Type of sub block    :  2
Flags                :  17
Offset               :  0
Length               :  10

Output Area :
Type of sub block    :  2
Flags                :  18
Offset               :  0
Length               :  6
***********************************************

2018-12-17 14:43:39.11 => A4A.Kernel.Main_Task : Calling Cold_Start...
2018-12-17 14:43:39.11 => A4A.Kernel.Main_Task : (16#800C0021#)COM-flag not set
Gnoga            :1.2b
Application root :/home/slos/Ada/A4A/demo/122 app7-wui/
Executable at    :/home/slos/Ada/A4A/demo/122 app7-wui/bin/
HTML root        :/home/slos/Ada/A4A/demo/122 app7-wui/html/
Upload directory :/home/slos/Ada/A4A/demo/122 app7-wui/html/
Templates root   :/home/slos/Ada/A4A/demo/122 app7-wui/
/js  at          :/home/slos/Ada/A4A/demo/122 app7-wui/js/
/css at          :/home/slos/Ada/A4A/demo/122 app7-wui/css/
/img at          :/home/slos/Ada/A4A/demo/122 app7-wui/img/
Boot file        :000-boot.html
HTTP listen on   ::8091
Press Ctrl-C to close server.
2018-12-17 14:43:39.96 : HTTP Server Started

Si je pointe mon navigateur sur l’adresse locale et le port choisi (:8091 cf. la trace), je peux piloter le variateur depuis le mot de contrôle et observer un signal en dent de scie sur la consigne et la mesure…

… tandis que je mate une vidéo de lapin…

… ce qui charge ma vieille CPU comme une bête …

Dans cette vue de « htop » on voit bien les priorités des processes de l’application temps réel, notamment les deux avec la priorité -89 sont les tâches « main » et « periodic », une priorité normale en FIFO étant à -50, -89 est une priorité plutôt haute donc.

C’est ce qui permet à ces tâches de se comporter honnêtement :

Ainsi, la tâche « main », cadencée à 1 ms, subit une fois sur mille un dépassement de son heure d’activation.

La vue d’état de la cifX montre des informations classiques…


… que je peux vérifier également depuis SYCON.net sur un autre poste :

Car « Ada for Automation » implémente « netX Diagnostic and Remote Access » comme déjà évoqué.

J’espère bien trouver un peu de temps pour mener d’autres expérimentations comme implémenter quelques fonctions de Motion Control

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

A4A : Raspberry Pi 3 / netHAT / PROFINET IO / Temps Réel

Bonjour,

L’article précédent présentait une mise en œuvre d’un module Hilscher netHAT configuré en PROFINET IO Device sur un Raspberry Pi 3. Ce module netHAT communiquait avec un automate SIEMENS S7 1500-PN PROFINET IO Contrôleur.

Pour illustrer la chose, la voici :

Raspberry Pi 3 / netHAT
Raspberry Pi 3 / netHAT

Le système d’exploitation est une Raspbian Stretch tout ce qu’il y a de plus standard :

Raspbian Stretch

Au-dessus de l’API cifX, fournie par le pilote Hilscher pour Linux, tourne une application exemple « Ada for Automation », le fameux Piano.

Cette application est configurée avec une tâche Main périodique avec une période de 50 ms et une priorité temps réel, ce que l’on peut constater aisément avec htop :

htop

Une priorité à 20 est une priorité non temps réel normale, à -50 c’est une priorité temps réel par défaut, à -89 une priorité temps réel élevée.

Et on relève que, en général, la période de 50 ms est respectée à +100 µs près, et dans environ 1 % des cas à moins de 2 ms près :

Scheduling Stats

Côté PROFINET IO Contrôleur, j’ai utilisé une carte cifX 50-RE Hilscher configurée bien sûr en PROFINET IO Controller avec SYCON.net, l’outil ad-hoc, et au-dessus également une application « Ada for Automation » mais avec une configuration non temps réel.

On constate tout de suite la différence au niveau ordonnancement des tâches :

Pour les besoins de la démo le netHAT remplace parfaitement le netRAPID évoqué sur ce blog.

Et vous pouvez retrouver cette démo vivante sur le portail :
http://ada4automation.slo-ist.fr/#app2

Ainsi, le temps réel sous Linux fonctionne plutôt bien, n’est-ce pas ?
Cela doit pouvoir convenir à bien des applications d’automatisme.

Cordialement,
Stéphane

A4A : Raspberry Pi 3 / netHAT / PROFINET IO

Bonjour,

Je disais donc dans mon dernier article qu’il fallait que je vous fasse une petite démonstration de l’utilisation du netHAT Hilscher avec un Raspberry Pi 3 et « Ada for Automation ».

Voilà donc une copie d’écran montrant l’application piano connectée, via le netHAT configuré en PROFINET IO Device, à un automate SIEMENS S7 1500-PN PROFINET IO Contrôleur :

Raspberry Pi 3 Ada for Automation netHAT
Raspberry Pi 3 Ada for Automation netHAT

On reconnait dans cette vue de TIA Portal les valeurs d’E/S affichées dans la vue ci-dessus, les bits de poids faible sont à gauche dans la vue du piano (b0 .. b7) :

netHAT TIA Portal Visu
netHAT TIA Portal Visu

La vue d’état cifX montre donc l’état du netHAT puisque celui-ci partage avec celle-ci, comme la plupart des produits Hilscher, l’API cifX, la Dual Port Memory, les piles de protocoles et l’OS.

La seule subtilité que j’ai rencontrée, c’est que la version du pilote Linux livrée avec le netHAT est plus récente que celle que j’utilisais jusqu’alors (la V1.1.0.0) et que l’architecture en a été revue.
En substance, un plugin permet maintenant de gérer la connexion via SPI et il n’est plus nécessaire de le gérer côté application. Le netHAT est directement vu comme une cifX.

netHAT cifX Status
netHAT cifX Status

Bien sûr, tout baigne côté automate aussi !

netHAT TIA Portal Network
netHAT TIA Portal Network

Une petite formation ? 😉

Cordialement,
Stéphane