Exemples de code d’intégration de données des capteurs Dracal en mode VCP

[Dernière mise à jour: 14/03/2024]

computer with codes and Dracal logo + VCP infographics

Introduction

Il est possible de se procurer la majorité des capteurs USB Dracal munis de l'option VCP. Cette option permet à l'utilisateur de choisir le protocole de communication utilisé par l'instrument pour communiquer ses données. Les instruments munis de cette option sont identifiables par la présence du préfixe "VCP-" (au lieu de "USB-") dans leur code de produit. L'utilisation du protocole VCP permet l'intégration des données sans passer par l'intermédiare d'un logiciel tiers tel que notre outil en ligne de commande dracal-usb-get par exemple. Suivez ce lien pour un survol des outils d'intégration disponibles pour vos produits Dracal.

L'objectif de cette page est d'illustrer par des exemples concrets comment intégrer les données de vos instruments communiquant en mode VCP dans différents langages et environnements.

1. Prérequis

1. Avoir en sa possession un instrument muni de l'option VCP

Ces instruments sont identifiables par le préfixe "VCP-" présent dans leur nom de produit.

2. Avoir une connaissances élémentaires d'utilisation de la ligne de commande

Pourquoi? Parce que tous les instruments Dracal sont livrés en mode USB et que le passage entre les modes USB et VCP se fait via les outils en ligne de commande. Suivez ce lien si vous cherchez où trouver les outils en ligne de commande.

3. Avoir pris connaissance du guide d'utilisation des produits VCP

Si ce n'est déjà fait, voici le lien vers le guide d'utilisation des produits VCP.

La présente documentation considère également que vous avez réussi avec succès à basculer votre instrument du mode USB vers le mode VCP et que vous avez réussi communiquer avec votre instrument. Si tel n'est pas le cas, il est recommandé de consulter le tutoriel Démarrer avec le mode VCP avant de poursuivre votre lecture.

2. Exemples dans différents langages de programmation

Python Icon 2.1. Python

Github Icon  Dépôt GitHub contenant un exemple de code Python prêt à l'emploi

Voici un exemple d'utilisation des capteurs Dracal en utilisant uniquement les bibliothèques Python. Il appose un horodatage devant chaque ligne de données et les enregistre dans un fichier. Une validation de l'intégrité des données est également effectuée. Bibliothèques utilisées :

- Le module "serial" nous permet d'interagir avec le dispositif via le protocole VCP.
- Le module "crccheck" permet de vérifier l'intégrité des données reçues.

Note Les deux bibliothèques sont listées sur pypi.org et peuvent être installées à l'aide de pip (e.g. pip install pyserial).

import sys
import time

import serial  # https://pypi.org/project/pyserial/
import crccheck  # https://pypi.org/project/crccheck/


# Parse command-line arguments
if len(sys.argv) not in (2, 3):
    print("Syntax: %s  [poll_interval_ms]" % sys.argv[0])
    print("Example (Windows)  %s COM1 1000" % sys.argv[0])
    print("Example (Linux/MacOS)    %s /dev/ttyACM0 1000" % sys.argv[0])
    sys.exit(1)

port = sys.argv[1]

if len(sys.argv) >= 3:
    interval = int(sys.argv[2])
else:
    interval = 1000  # default


crc_checker = crccheck.crc.CrcXmodem()

# Open serial port

with serial.Serial(port) as ser:
    ser.readlines(2)  # Discard the first two lines as they may be partial

    ser.write(b"INFO\n")  # Get the info line

    time.sleep(0.3)  # Allow 100 ms for request to complete
    ser.write(b"POLL %d\n" % interval)  # Set poll interval

    time.sleep(0.3)
    ser.write(b"FRAC 2\n")  # Return data with two digits past the decimal

    # Process all lines in a loop
    while True:
        line = ser.readline()
        t = time.ctime()

        if not line:
            break

        # Check data integrity using CRC-16-CCITT (XMODEM)
        try:
            data, crc = line.split(b"*")
            crc = int(crc, 16)  # parse hexadecimal string into an integer variable
            crc_checker.process(data)
            computed_crc = crc_checker.final()
            crc_checker.reset()
            crc_success = computed_crc == crc
        except ValueError:
            # We will get here if there isn't exactly one '*' character in the line.
            # If that's the case, data is most certainly corrupt!
            crc_success = False

        if not crc_success:
            print("Data integrity error")
            break

        # Decode bytes into a list of strings
        data = data.decode("ASCII").strip(",").split(",")

        if data[0] == "I":
            if data[1] == "Product ID":  # For the INFO command response
                info_line = data
                padlen = max(len(s) for s in info_line[4::2])
                print(", ".join(info_line))
            else:  # Other info lines only need the message to be echoed
                print(data[3])
        else:
            # Create an ID for the device
            device = f"{data[1]} {data[2]}"

            # Convert number strings to the appropriate numerical format
            for i in range(4, len(data), 2):
                try:
                    data[i] = int(data[i])
                except ValueError:
                    data[i] = float(data[i])

            # Convert data to a tuple of (sensor, value, unit) triads
            data = zip(info_line[4::2], data[4::2], data[5::2])

            # Display the current time, product id and serial number before every point
            print(f"\n{t}, {device}")
            for d in data:
                print(("{:" + str(padlen + 2) + "}{} {}").format(*d))

générant la sortie suivante:

C Icon 2.2. C (POSIX)

Github Icon  Dépôt GitHub contenant un exemple de code C (posix) prêt à l'emploi

Cet exemple vous permet d'accéder aux données d'un capteur en mode VCP à l'aide du langage C (pour les systèmes d'exploitation de type *nix ; par exemple Unix/Linux). Vous trouverez des méthodes qui ouvrent une connexion, envoient, lisent et interprètent les données du dispositif.

Note: ce programme a été conçu pour fonctionner avec les capteurs de la série PTH. Vous devrez modifier les chaînes de format et les déclarations de variables pour qu'elles fonctionnent avec d'autres types de capteurs.

Ce programme utilise la bibliothèque libcrc. Pour apprendre à utiliser ses fonctions, veuillez vous référer aux instructions spécifiques à votre compilateur ou à votre environnement de développement intégré (IDE) sur la manière d'inclure des bibliothèques de manière statique (le dépôt Github ci-dessus en fournit un exemple).

#include     // standard input / output functions
#include    // general purpose functions
#include    // string function definitions
#include    // UNIX standard function definitions
#include     // File control definitions
#include     // Error number definitions
#include   // POSIX terminal control definitions
#include   // Boolean types and values
#include      // Timekeeping types and functions
#include     // GNU Regular expression definitions

#include "checksum.h"   // CRC calculation library from github.com/lammertb/libcrc

/**
 * Path to the file descriptor of the port to be read from
 * MacOS: /dev/tty.usbmodem[serial of Dracal device]1
 * Linux: /dev/ttyACM[number]
**/
const char* dev = "/dev/ttyACM0";

int read_line(int fd, char* line) {

  // Allocate memory for read buffer
  char buf[256]; // Read buffer
  memset(buf, '\0', sizeof buf);

  // Loop until a complete line is read
  // Note: A full CRLF is expected but in case the CR is ignored we wait for the LF only
  while (!strstr(line, "\n")) {

    // Read the port's content to buf
    if (read(fd, buf, sizeof buf) < 0) {
      if (errno == EAGAIN) {
        // This only means the port had no data
        usleep(100000); // retry in 100 ms
        continue;
      }
      else {
        return -1;
      }
    }

    if (*buf != '\0') {
      strcat(line, buf);
      memset(buf, '\0', sizeof buf);
    }
  }

  // Variables necessary to the integrity check
  uint16_t crc;       // Read checksum value
  char* sep;          // Position of the asterisk in the line
  static regex_t re;  // Pattern to match to the expected content of a line
  static bool is_compiled = false;

  if (!is_compiled) {
    regcomp(&re, "^[^\\*]+\\*[0-9a-f]{4}\\s*$", 0);
    is_compiled = true;
  }

  // Filter out lines whose format would crash the CRC check, they are surely invalid
  if (regexec(&re, line, 0, NULL, 0)) {

    sep = strchr(line, '*');
    crc = strtol(sep + 1, NULL, 16);

    // CRC validation
    if (crc == crc_xmodem((unsigned char*)line, (size_t)(sep - line))) {
      *sep = '\0'; // Replace the * with a null character, now line stops at the end of the content
      return 0;
    }
  }

  // We will get here if the checks failed
  printf("Integrity error: %s\n", line);
  return 0;
}

/**
 * This function opens a connection, sets the necessary settings and
 * returns a file descriptor with which data can be read from or sent to.
 * dev is a string of the path to the device to converse with such as
 *
**/
int open_port(const char* dev) {

  // Open the file and get the descriptor
  int fd = open(dev, O_RDWR | O_NOCTTY | O_NDELAY);
  if (fd < 0) {
    perror("Error opening file");
    return -1;
  }

  // Configure Port
  struct termios options;
  memset(&options, 0, sizeof options);

  // Get the current options
  if (tcgetattr(fd, &options) != 0) {
    perror("Error in tcgettattr");
    return -1;
  }

  // Set Baud Rate
  cfsetospeed(&options, B9600);
  cfsetispeed(&options, B9600);

  // Setting other options
  options.c_cflag &= ~(PARENB | CSTOPB);              // No parity, 1 stop bit
  options.c_cflag &= ~CSIZE;                          // Charater size mask
  options.c_cflag |= CS8;                             // 8 bits
  options.c_cflag &= ~CRTSCTS;                        // No flow control
  options.c_cflag |= CREAD | CLOCAL;                  // Turn on READ & ignore ctrl lines

  options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // Raw input

  options.c_iflag &= ~(IXON | IXOFF | IXANY);         // Turn off software flow contrl
  options.c_iflag &= ~IGNCR;                          // Don't ignore CR character

  options.c_oflag &= ~OPOST;                          // Don't replace outgoing LF with CRLF - for clarity they are explicit here

  // Flush port
  if (tcflush(fd, TCIFLUSH) != 0) {
    perror("Error in tcflush");
    return -1;
  }
  // Apply attributes
  if (tcsetattr(fd, TCSANOW, &options) != 0) {
    perror("Error in tcsettattr");
    return -1;
  }
  return fd;
}

int main(int argc, char** argv) {
  // ignore unused
  (void)argc; (void)argv;

  // Open File Descriptor
  int fd = open_port(dev);
  if (fd < 0) {
    return EXIT_FAILURE;
  }

  // Defining commands as strings is convenient for the use of the sizeof operator
  const unsigned char poll_cmd[] = "POLL 1000\r\n";

  // Variables for timekeeping
  time_t t;
  struct tm* localt;
  char timestr[20];

  // Variables related to line manipulation
  char line[256];    // Contents of the active line

  // Variables containing processed data from the device.
  // This was made with a PTH sensor in mind, other sensor types will need different declarations
  int    pressure;      // Pressure in Pascals
  float  temperature;   // Temperature in Celsius
  float  humidity;      // Humidity in %
  char   model[32];     // Model id of device
  char   serial[7];     // Serial number of device
  char   message[128];  // Message contained for info lines

  bool info_line_read = false;

  // Could be any number or a while loop, change as needed.
  // i variable is not used but could be useful for unique line IDs
  for (int i = 0; i < 10; i++) {

    // Set the poll rate if it has not been set yet.
    if (!info_line_read) {
      if (write(fd, poll_cmd, sizeof(poll_cmd) - 1) < 0) {
        perror("Error writing");
      }
    }

    // (Re)initialize line
    memset(line, '\0', sizeof line);

    // Wait until a full line has been read and validated
    if (read_line(fd, line) < 0) {
      perror("Error reading");
    }

    // Here we generate a string to represent the time at which the line was recieved
    t = time(NULL);
    localt = localtime(&t);
    strftime(timestr, 20, "%F %T", localt); // YYYY-MM-DD HH:MM:SS


    if (line[0] == 'I') {
      // For info lines (the POLL response in this case)
      sscanf(
        line,
        "I,%[^,],%[^,],%[^,]",
        model,
        serial,
        message
      );

      printf("\n%s\n", message);
      info_line_read = true;
    }
    else {
      /**
       * Interpret the line and save the result into the variables.
       * The format string to use would depend on the sensor, this example was made with the PTH sensor in mind.
       * Refer to these resources to learn more on how to do so
       *   Format strings for the scanf functions  : cplusplus.com/reference/cstdio/scanf/
       *   Dracal sensor VCP mode output format    : dracal.com/en/usage-guides/vcp_howto
      **/
      sscanf(line, "%*c,%*[^,],%*[^,],,%i,Pa,%f,C,%f,%%", &pressure, &temperature, &humidity);

      // This is where you would put your own code to be executed on data.
      printf(
        "\n%s %s @ %s\nP = %i Pa\nT = %.2f C\nH = %.2f %%\n",
        model, serial, timestr,
        pressure, temperature, humidity
      );
    }
  }

  close(fd); // Close the serial port

  return EXIT_SUCCESS;
}
}

 

générant la sortie suivante:

 

C Icon 2.3. C/C++ (Win32)

Github Icon  Dépôt GitHub contenant un exemple de code C/C++ (Win32) prêt à l'emploi

Cet exemple vous permet d'accéder aux données d'un capteur en mode VCP à l'aide de C/C++ (Windows/Win32). Vous trouverez des méthodes permettant d'ouvrir une connexion, d'envoyer, de lire et d'interpréter les données provenant de l'appareil.

Note: Ce programme a été conçu pour fonctionner avec les capteurs de la série PTH. Vous devrez modifier les chaînes de format et les déclarations de variables pour qu'elles fonctionnent avec d'autres types de capteurs.

Ce programme utilise la bibliothèque libcrc. Pour apprendre à utiliser ses fonctions, veuillez vous référer aux instructions spécifiques à votre compilateur ou à votre environnement de développement intégré (IDE) sur la manière d'inclure des bibliothèques de manière statique (le dépôt Github ci-dessus en fournit un exemple).

#include <stdio.h>
#include <windows.h>
#include <time.h>
#include <string.h>
#include <stdbool.h>

#include <checksum.h> // CRC calculation library from github.com/lammertb/libcrc

// COM id of the plugged in device. 
const char* dev = "\\\\.\\COM3";
const int line_size_max = 256;

// Small enum type for readline to return
typedef enum {
    SUCCESS,
    READ_ERROR,
    INTEGRITY_ERROR,
} error_t;

error_t read_line(HANDLE h, char* line) {

    memset(line, '\0', line_size_max);

    char buf[2]; // Buffer of 1 character + null terminator
    memset(buf, '\0', sizeof buf);

    do {
        if (!ReadFile(h, buf, 1, NULL, NULL)) {
            return READ_ERROR;
        }
        strcat_s(line, line_size_max, buf);
    } while (!strchr(line, '\n'));

    uint16_t crc;       // Checksum value read from the string
    char* sep;          // Position of the asterisk in the line

    sep = strchr(line, '*');
    if (!sep) {
        return INTEGRITY_ERROR;
    }
    crc = (uint16_t) strtol(sep + 1, NULL, 16);

    if (crc != crc_xmodem((unsigned char*)line, (size_t)(sep - line))) {
        return INTEGRITY_ERROR;
    }
    *sep = '\0'; // Replace the * with a null character, now line stops at the end of the content
    return SUCCESS;
}

int main()
{
    HANDLE COM = CreateFileA(dev,           // Port name
        GENERIC_READ | GENERIC_WRITE,       // Read/Write
        0,                                  // No Sharing
        NULL,                               // No Security
        OPEN_EXISTING,                      // Open existing port only
        0,                                  // Non Overlapped I/O
        NULL);                              // Null for Comm Devices

    if (COM == INVALID_HANDLE_VALUE) {
        printf("Error opening serial port\r\n");
        return EXIT_FAILURE;
    }
    else {
        printf("Opening serial port successful\r\n");
    }


    char line[256];
    memset(line, '\0', sizeof line);

    DWORD comm_mask;

    GetCommMask(COM, &comm_mask);
    printf("comm_mask: %x\r\n", comm_mask);

    // Variables for timekeeping
    time_t t;
    struct tm localt;
    char timestr[20];

    // Variables containing processed data from the device. This example was written with a PTH in mind
    int    pressure;      // Pressure in Pascals
    float  temperature;   // Temperature in Celsius
    float  humidity;      // Humidity in %
    char   model[32] = "";     // Model id of device
    char   serial[7] = "";     // Serial number of device
    char   message[128];  // Message contained for info lines

    char poll_cmd[] = "POLL 1000\r\n";

    // Whether an info line has been read yet
    bool info_line_read = false;

    for (int i = 0;; i++) {

        if (!info_line_read) {
            //PurgeComm(COM, PURGE_TXCLEAR);
            if (!WriteFile(COM, poll_cmd, sizeof poll_cmd -1, NULL, NULL)) {
                printf("Write error = %i", GetLastError());
            }
        }

        switch (read_line(COM, line)) {
        case SUCCESS: // Here, the code that runs when everything is fine
            
            
            if (line[0] == 'I') {
                // For info lines (the POLL response in this case)
                sscanf_s(
                    line, 
                    "I,%[^,],%[^,],%[^,]",
                    model,   (int) sizeof model, 
                    serial,  (int) sizeof serial, 
                    message, (int) sizeof message
                );

                printf("\n%s\n", message);
                info_line_read = true;
            }
            else {
                t = time(NULL);
                localtime_s(&localt, &t);
                strftime(timestr, 20, "%F %T", &localt); // YYYY-MM-DD HH:MM:SS

                /**
                 * Interpret the line and save the result into the variables.
                 * The format string to use would depend on the sensor, this example was made with the PTH sensor in mind.
                 * Refer to these resources to learn more on how to do so
                 *   Format strings for the scanf functions  : cplusplus.com/reference/cstdio/scanf/
                 *   Dracal sensor VCP mode output format    : dracal.com/en/usage-guides/vcp_howto
                **/
                sscanf_s(line, "%*c,%*[^,],%*[^,],,%i,%*2c,%f,%*c,%f", &pressure, &temperature, &humidity);

                // This is where you would put your code to be executed on data.
                printf(
                    "\n%s %s @ %s\nP = %i Pa\nT = %.2f C\nH = %.2f %%\n", 
                    model, serial, timestr, 
                    pressure, temperature, humidity
                );
            } 
            
            break;

        case READ_ERROR: // This may happen if eg. the device is unplugged
            return EXIT_FAILURE;

        case INTEGRITY_ERROR: // When the integrity check failed
            if (i != 0) { 
                // First line is likely to be garbage, no need to warn us about it
                printf("Integrity error on line %i: \"%s\"", i, line);
            }
            break;
        }
    }


    CloseHandle(COM); // Close the serial port

    return EXIT_SUCCESS;
}

produisant la sortie suivante:

 

Node Icon 2.4. Node.js

Github Icon  Dépôt GitHub contenant un exemple de code Node.js prêt à l'emploi

L'exemple Node.js ci-dessous a été produit en utilisant un capteur VCP-PTH450-CAL, mais devrait pouvoir s'adapter à d'autres capteurs - de manière similaire à l'exemple en Python. Il ouvre séquentiellement la connexion série, envoie des instructions d'écriture pertinentes et interprète la sortie résultante. Nous ajoutons des horodatages devant chaque ligne de données, et une validation de l'intégrité des données est également effectuée. Les bibliothèques JS utilisées (toutes deux faisant partie de l'écosystème Node SerialPort disponible sur le gestionnaire de paquets NPM) sont :

serialport
@serialport/parser-readline

// if not using Babel, you'll want to replace `import` statements with ES5 format, e.g. `const { SerialPort } = require('SerialPort')...`
import { SerialPort } from 'serialport';
import { ReadlineParser } from '@serialport/parser-readline';

// windows: e.g. path = 'COM3';
const [path, interval, baudRate ] = ['/dev/ttyACM0', 1000, 9600];
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))

// open port
const port = new SerialPort({
  path,
  baudRate,
});
const parser = port.pipe(new ReadlineParser({ delimiter: '\n' }));

// write instructions
port.write(Buffer.from('INFO\r\n'), err => { if (err) console.log('ERROR!', err) });
port.drain(err => { delay(100) });
port.write(Buffer.from(`POLL 1000\r\n`), err => { if (err) console.log('ERROR!', err) });
port.drain(err => { delay(100) });
port.write(Buffer.from('FRAC 2\r\n'), err => { if (err) console.log('ERROR!', err) });
port.drain(err => { delay(100) });

// read event
let info_line, padlen;
parser.on('data', async (data) => {
  const split = data.replace(', ', ',').split(',');

  // example info line:
  // I,Product ID,Serial Number,Message,MS5611 Pressure,Pa,SHT31 Temperature,C,SHT31 Relative Humidity,%,*bbdd

  // extract info line
  if (split[0] === 'I') {
    // parse field titles
    if (split[1] == "Product ID") {
      info_line = split
      padlen = Math.max(...(split.slice(4).map(s => s.length)));
      console.info(info_line.join(','))

    // echo any other info lines
    } else {
      console.info(split[3]);
    }
    return;
  }
  if (!info_line) return console.info('Awaiting info line...');

  // example readout line:
  // D,VCP-PTH450-CAL,E24638,,102466,Pa,24.87,C,66.81,%,*d16d

  // extract readout values
  const device = `${split[1]} ${split[2]}`
  const sensors = info_line.slice(4).filter((v, i) => i % 2 < 1);
  const values = split.slice(4).filter((v, i) => i % 2 < 1).map(parseFloat);
  const units = split.slice(4).filter((v, i) => i % 2 > 0);

  // print result
  console.info(`${new Date().toLocaleString('en-CA')} ${device}`)
  for (const i in units) {  // `units` will have the shorter range (no *abcd value)
    console.info(`${sensors[i].padEnd(padlen + 2)} ${values[i]} ${units[i]}`)
  }
  console.info('\n');

});

générant la sortie suivante:

BashIcon 2.5. Bash

Github Icon  Dépôt GitHub contenant un exemple de code Bash prêt à l'emploi

L'exemple Bash ci-dessous a été réalisé avec un capteur VCP-PTH450-CAL, mais devrait pouvoir s'adapter à d'autres capteurs - de la même manière que les exemples Python et Node. Il ouvre séquentiellement la connexion série, envoie les instructions d'écriture appropriées et interprète les résultats obtenus. Nous ajoutons des horodatages devant chaque ligne de données, et une validation simple des données est également effectuée. Il utilise quelques concepts de scripts shell, notamment

- les descripteurs de fichiers

- IFS et tableaux

#!/bin/bash

# read exec parameters
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
  echo "Syntax: $0  [poll_interval_ms]"
  echo "Example (Linux/MacOS): $0 /dev/ttyACM0 1000"
  exit 1
fi
port=$1
interval=${2:-1000}  # default interval is 1000 if not provided

# open serial port for read/write (using file descriptor 3)
exec 3<>"$port"
stty -F "$port" -icrnl -onlcr

# set poll interval (allow 100 ms for request to complete)
echo -e "POLL $interval" > "$port"
sleep 0.3
# two digits past the decimal
echo -e "FRAC 2" > "$port"
sleep 0.3
# get the info line
echo -e "INFO" > "$port"
sleep 0.3

# handle incoming data
process_data() {
  while IFS= read -r -u 3 line; do
    t=$(date +"%Y-%m-%d %H:%M:%S")
    if [ -z "$line" ]; then
      break
    fi

    # example info line:
    # I,Product ID,Serial Number,Message,MS5611 Pressure,Pa,SHT31 Temperature,C,SHT31 Relative Humidity,%,*bbdd

    # clean out & split data line to array
    data=$(echo "$line" | cut -d'*' -f1)
    data=($data)
    olfIFS="$IFS"; IFS=',' read -r -a data <<< "${data[@]}"; IFS="$oldIFS"

    # extract info line
    if [ "${data[0]}" == "I" ]; then
      # parse field titles
      if [ "${data[1]}" == "Product ID" ]; then
        info_line=("${data[@]}")
        padlen=$(printf "%s\n" "${info_line[@]:4}" | awk '{ print length }' | sort -n | tail -n1)
        printf "%s," "${info_line[@]}"
        echo ""
      # echo any other info lines
      else
        echo "${data[@]:3}"
      fi
      continue;
    fi
    if [ -z "$info_line" ]; then echo 'Awaiting info line...'; continue; fi

    # example readout line:
    # D,VCP-PTH450-CAL,E24638,,102466,Pa,24.87,C,66.81,%,*d16d

    # extract device ID
    device="${data[1]} ${data[2]}"

    # extract readout values
    for i in $(seq 4 2 $(echo $data | wc -w)); do
      if [[ ! $(echo $data | awk "{print \$$i}") =~ ^[0-9]+$ ]]; then
        data[$i]=$(echo $data | awk "{print \$$i}" | tr -d ',')
      else
        data[$i]=$(echo $data | awk "{print \$$i}")
      fi
    done

    # print it out
    echo -e "\n$t, $device"
    for ((i=4; i<${#data[@]}; i+=2)); do
      printf "%-${padlen}s %s %s\n" "${info_line[i]}" "${data[i]}" "${data[i+1]}"
    done

  done
}
# run process_data in the background
process_data &

# Wait for background process to finish
wait
exec 3<&-

générant la sortie suivante:

Java Icon 2.6. Java

Github Icon  Dépôt GitHub contenant un exemple de code Java prêt à l'emploi

L'exemple Java ci-dessous a été réalisé avec un capteur VCP-PTH450-CAL, mais il devrait pouvoir s'adapter à d'autres capteurs - de la même manière que les exemples Python et Node. Il ouvre séquentiellement la connexion série, envoie les instructions d'écriture appropriées et interprète les résultats obtenus. En lecture, les sauts de ligne (\r\n) sont traités manuellement. Nous ajoutons des horodatages devant chaque ligne de données, et une validation de base des données est également effectuée. La bibliothèque java-native:jssc est utilisée pour lire les ports série.

 

package src.main.java;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;

import jssc.SerialPort;
import jssc.SerialPortEvent;
import jssc.SerialPortEventListener;
import jssc.SerialPortException;

public class Main {
  // replace with appropriate path, interval, and baudRate
  static String path = "/dev/ttyACM0";
  static int interval = 1000;
  static int baudRate = 9600;

  // working globals
  static String[] info_line;
  static int padlen;

  public static void main(String[] args) {
    // open port
    SerialPort serialPort = new SerialPort(path);
    try {
      serialPort.openPort();
      serialPort.setParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
    } catch (SerialPortException ex) {
      System.out.println("Error setting up serial port: " + ex);
    }

    // write instructions
    try {
      serialPort.writeBytes("INFO\r\n".getBytes());
      Thread.sleep(1000);
      serialPort.writeBytes("POLL 1000\r\n".getBytes());
      Thread.sleep(1000);
      serialPort.writeBytes("FRAC 2\r\n".getBytes());
      Thread.sleep(1000);
    } catch (SerialPortException | InterruptedException ex) {
      System.out.println("Error writing to serial port: " + ex);
    }

    // read event
    try {
      serialPort.addEventListener(new SerialPortEventListener() {
        StringBuilder line = new StringBuilder();

        @Override
        public void serialEvent(SerialPortEvent event) {
          if (event.isRXCHAR()) {
            try {
              // accumulate reads until the end of line
              byte[] buffer = serialPort.readBytes();
              for (byte currentByte: buffer) {
                if ( (currentByte == '\r' || currentByte == '\n') && line.length() > 0) {
                  // send for processing
                  processData(line.toString());

                  line.setLength(0);
                }
                else {
                  line.append((char) currentByte);
                }
              }
            } catch (SerialPortException ex) {
              System.out.println("Error reading from serial port: " + ex);
            }
          }
        }
      });
    } catch (SerialPortException ex) {
      System.out.println("Error setting up event listener: " + ex);
    }
  }

  // parse a data line and display results
  private static void processData(String data) {
    String[] split = data.replace(", ", ",").split("\\*")[0].split(",");

    // extract info line
    if (split[0].contains("I")) {
      // parse field titles
      if (split[1].equals("Product ID")) {
        info_line = split;

        List values = Arrays.asList(Arrays.copyOfRange(split, 4, split.length));
        padlen = values.stream().map(String::length).max(Integer::compareTo).get();

        System.out.println(Arrays.toString(info_line));
      } else {
        System.out.println(split[3]);
      }
      return;
    }
    if (info_line == null) {
      System.out.println("Awaiting info line...");
      return;
    }

    // parse readout values
    String device = split[1] + " " + split[2];
    String[] sensors = new String[(info_line.length - 4) / 2];
    double[] values = new double[sensors.length];
    String[] units = new String[sensors.length];

    String[] info = Arrays.copyOfRange(info_line, 4, info_line.length);
    for (int i = 0; i < info.length - 1; i += 2) {
      sensors[i/2] = info[i].trim();
      values[i/2] = Double.parseDouble(split[i + 4]);
      units[i/2] = split[i + 5].trim();
    }

    // print result
    String now = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now());
    System.out.println(now + " " + device);
    for (int i = 0; i < units.length; i++) {
      System.out.println(String.format("%-" + (padlen + 2) + "s %s %s", sensors[i], values[i], units[i]));
    }
    System.out.println("\n");
  }
}

générant la sortie suivante:

C# Icon 2.7. C#

Github Icon  Dépôt GitHub contenant un exemple de code Java prêt à l'emploi

L'exemple C# ci-dessous a été réalisé à l'aide d'un capteur VCP-PTH450-CAL ; il devrait pouvoir s'adapter à d'autres capteurs - de la même manière que les exemples Python et Node.

Il ouvre séquentiellement la connexion série, envoie les instructions d'écriture appropriées et interprète la sortie résultante par le biais d'une boucle liée à l'état. Lors de la lecture, les sauts de ligne sont traités automatiquement par Readline(). Nous ajoutons des horodatages devant chaque ligne de données, et une validation de base des données est également effectuée.

Notez que l'approche standard de la lecture avec SerialPort serait d'utiliser un gestionnaire d'événement ajouté à `port.DataReceived`. Malheureusement, cela ne semble lire que les réactions d'entrée, mais pas les sorties ultérieures/régulières du périphérique. C'est pourquoi l'option loop est choisie ici. Vous êtes bien sûr invités à ouvrir un Pull Request si vous souhaitez proposer d'autres solutions à ce problème. 

System.IO.Ports.SerialPort est utilisé pour lire le périphérique.

using System.IO.Ports;

class App
{
    const string PATH = "COM4";
    const int BAUDRATE = 9600;
    const int INTERVAL = 1000;

    static SerialPort port = new(PATH, BAUDRATE, Parity.None, 8, StopBits.One);
    static string[]? info_line;
    static int padlen;
    
    static void Main(string[] args)
    {
        Console.CancelKeyPress += (s, e) => { Environment.Exit(0); };
        try
        {
            port.Open();

            port.Write($"POLL {INTERVAL}\r\n");
            Task.Delay(100).Wait();
            port.Write("FRAC 2\r\n");
            Task.Delay(100).Wait();
            port.Write("INFO\r\n");
            Task.Delay(100).Wait();

            // NOTE: while the standard approach would be to use Event handler `port.DataReceived`, it has
            // proven unable to receive non-input driven readout data thus far.
            while (port.IsOpen)
            {
                handleReceivedData();
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }
    }

    static void handleReceivedData()
    {
        string data = port.ReadLine();
        string[] split = data.Replace(", ", ",").Split('*')[0].Split(',');

        if (split[0] == "I")
        {
            if (split[1] == "Product ID")
            {
                info_line = split;
                padlen = split.Skip(4).OrderByDescending(s => s.Length).First().Length;
                Console.WriteLine(string.Join(",", split));
            }
            else
            {
                Console.WriteLine(split[3]);
            }
            return;
        }
        if (info_line == null)
        {
            Console.WriteLine("Awaiting info line...");
            return;
        }

        string device = $"{split[1]} {split[2]}";
        string[] sensors = info_line[4..].Where((v, i) => i % 2 < 1).ToArray();
        string[] values = split[4..].Where((v, i) => i % 2 < 1)/*.Select(double.Parse)*/.ToArray();
        string[] units = split[4..].Where((v, i) => i % 2 > 0).ToArray();

        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} {device}");
        for (int i = 0; i < units.Length; i++)
        {
            Console.WriteLine($"{sensors[i].PadRight(padlen + 2)} {values[i]} {units[i]}");
        }
        Console.WriteLine("\n");
    }
}

générant la sortie suivante:

 

C++ Icon 2.8. C++/CLI (.NET)

Github Icon  Dépôt GitHub contenant un exemple de code C++ (.NET) prêt à l'emploi

L'exemple C++/CLI (.NET) ci-dessous a été réalisé avec un capteur VCP-PTH450-CAL ; il devrait pouvoir s'adapter à d'autres capteurs - de la même manière que les exemples Python et Node.

Il ouvre séquentiellement la connexion série, envoie les instructions d'écriture appropriées et interprète la sortie résultante par le biais d'une boucle liée à l'état. Lors de la lecture, les sauts de ligne sont traités automatiquement par Readline(). Nous ajoutons des horodatages devant chaque ligne de données, et une validation de base des données est également effectuée. Notez que - comme en C# - l'approche standard de la lecture avec SerialPort serait d'utiliser un gestionnaire d'événement ajouté à `port->DataReceived`. Malheureusement, cela ne semble que lire les réactions d'entrée, mais pas les sorties ultérieures/régulières de l'instrument. C'est pourquoi l'option de la boucle est choisie ici.

System::IO::Ports::SerialPort est utilisé pour lire le périphérique. 

#include <msclr/gcroot.h>
#using <System.dll>

using namespace msclr;
using namespace System;
using namespace System::IO::Ports;
using namespace System::Threading;

const auto PATH = "COM3";
const int BAUDRATE = 9600;
const int INTERVAL = 1000;

// allow ref type static storage w/ gcroot()
gcroot<array<String^>^> info_line;
int padlen = 0;

int GetMaxLength(array<String^>^ split)
{
    int maxLength = 0;
    for each (auto str in split) {
        maxLength = std::max(str->Length, maxLength);
    }
    return maxLength;
}

void handleReceivedData(SerialPort^ port)
{
    String^ data = port->ReadLine();
    array<String^>^ split = data->Split(',');

    if (split[0] == "I")
    {
        if (split[1] == "Product ID")
        {
            info_line = split;
            padlen = GetMaxLength(split);
            Console::WriteLine(String::Join(",", info_line));
        }
        else
        {
            Console::WriteLine(split[3]);
        }
        return;
    }

    if (info_line == nullptr || info_line->Length < 1)
    {
        Console::WriteLine("Awaiting info line...");
        return;
    }

    String^ device = String::Format("{0} {1}", split[1], split[2]);
    array<String^>^ sensors = gcnew array<String^>((info_line->Length - 4) / 2);
    array<Double^>^ values = gcnew array<Double^>(sensors->Length);
    array<String^>^ units = gcnew array<String^>(sensors->Length);

    // map value positions into dedicated arrays
    for (int i = 0; i < info_line->Length - 5; i += 2) {
        sensors[i / 2] = info_line[i + 4]->Trim();
        values[i / 2] = Double::Parse(split[i + 4]);
        units[i / 2] = split[i + 5]->Trim();
    }

    // display results
    Console::WriteLine(String::Format("\n{0} {1} {2}", DateTime::Now.ToShortDateString(), DateTime::Now.ToShortTimeString(), device));
    for (int i = 0; i < units->Length; i++)
    {
        Console::WriteLine(String::Format("{0} {1} {2}", sensors[i]->PadRight(padlen + 2, ' '), values[i], units[i]));
    }
}

void Console_CancelKeyPress(Object^ sender, ConsoleCancelEventArgs^ e)
{
    Environment::Exit(0);
}

int main(array<String^>^ args)
{
    Console::CancelKeyPress += gcnew ConsoleCancelEventHandler(Console_CancelKeyPress);

    String^ path = gcnew String(PATH);
    SerialPort^ port = gcnew SerialPort(path, BAUDRATE, Parity::None, 8, StopBits::One);

    try
    {
        port->Open();

        port->WriteLine("POLL " + INTERVAL);
        Thread::Sleep(100);
        port->WriteLine("FRAC 2");
        Thread::Sleep(100);
        port->WriteLine("INFO");
        Thread::Sleep(100);

        // NOTE: while the standard approach would be to use Event handler `port.DataReceived`, it has
        // proven unable to receive non-input driven readout data thus far.
        while (port->IsOpen)
        {
            handleReceivedData(port);
        }
    }
    catch (Exception^ e)
    {
        Console::WriteLine(e->ToString());
    }
}

produisant la sortie suivante:

VB Icon 2.9. VB.Net

Github Icon  Dépôt GitHub contenant un exemple de code VB.Net prêt à l'emploi

L'exemple VB.Net ci-dessous a été réalisé à l'aide d'un capteur VCP-PTH450-CAL ; il devrait pouvoir s'adapter à d'autres capteurs - de la même manière que les exemples C#, Python et Node.

Il ouvre séquentiellement la connexion série, envoie les instructions d'écriture appropriées et interprète la sortie résultante par le biais d'une boucle liée à l'état. Lors de la lecture, les sauts de ligne sont traités automatiquement par Readline(). Nous ajoutons des horodatages devant chaque ligne de données, et une validation de base des données est également effectuée.

Notez que - comme en C# - l'approche standard de la lecture avec SerialPort serait d'utiliser un gestionnaire d'événement ajouté à port->DataReceived. Malheureusement, cela ne semble que lire les réactions d'entrée, mais pas les sorties ultérieures/régulières de l'appareil. C'est pourquoi l'option de la boucle est choisie ici.

System.IO.Ports.SerialPort est utilisé pour lire le périphérique.

Imports System.IO.Ports

Module App
    Const PATH As String = "COM3"
    Const BAUDRATE As Integer = 9600
    Const INTERVAL As Integer = 1000

    Dim port As SerialPort = New SerialPort(PATH, BAUDRATE, Parity.None, 8, StopBits.One)
    Dim info_line As String() = Nothing
    Dim padlen As Integer = 0

    Sub Main(args As String())
        AddHandler Console.CancelKeyPress, Sub(s, e)
                                               port.Close()
                                               Environment.Exit(0)
                                           End Sub
        Try
            port.Open()

            port.Write($"POLL {INTERVAL}" & vbCrLf)
            Task.Delay(100).Wait()
            port.Write("FRAC 2" & vbCrLf)
            Task.Delay(100).Wait()
            port.Write("INFO" & vbCrLf)
            Task.Delay(100).Wait()

            ' NOTE: while the standard approach would be to use Event handler `port.DataReceived`, it has
            ' proven unable to receive non-input driven readout data thus far.
            While port.IsOpen
                handleReceivedData()
            End While
        Catch e As Exception
            Console.WriteLine(e.ToString())
        End Try
    End Sub

    Sub handleReceivedData()
        Dim data As String = port.ReadLine()
        Dim split As String() = data.Replace(", ", ",").Split("*"c)(0).Split(","c)

        If split(0) = "I" Then
            If split(1) = "Product ID" Then
                info_line = split
                padlen = split.Skip(4).OrderByDescending(Function(s) s.Length).First().Length
                Console.WriteLine(String.Join(",", split))
            Else
                Console.WriteLine(split(3))
            End If
            Return
        End If
        If info_line Is Nothing Then
            Console.WriteLine("Awaiting info line...")
            Return
        End If

        Dim device As String = $"{vbCrLf}{vbCrLf}{split(1)} {split(2)}"
        Dim sensors As String() = info_line.Skip(4).Where(Function(v, i) (i Mod 2 < 1)).ToArray()
        Dim values As String() = split.Skip(4).Where(Function(v, i) (i Mod 2 < 1)).ToArray()
        Dim units As String() = split.Skip(4).Where(Function(v, i) (i Mod 2 > 0)).ToArray()

        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} {device}")
        For i As Integer = 0 To units.Length - 1
            Console.WriteLine($"{sensors(i).PadRight(padlen + 2)} {values(i)} {units(i)}")
        Next
    End Sub
End Module

produisant la sortie suivante:

3. License et avis de non-responsabilité

Sauf indication contraire, les extraits de code sur cette page sont placés dans le domaine public et peuvent être incorporés dans tout logiciel, commercial ou non. Les exemples sur cette page sont fournis "en l'état", dans l'espoir qu'ils puissent servir, mais sans AUCUNE GARANTIE de quelque nature que ce soit, expresse ou implicite, y compris, mais sans y être limité, les garanties implicites de commerciabilité et de la conformité à une utilisation particulière. Les pratiques recommandées de programmation, telles que la gestion et détection d'erreurs, la validation des entrées et les tests, sont sous la responsabilité de l'utilisateur, qui assume également la totalité des risques liés à la qualité et aux performances du code. Dracal Technologies Inc. n'accepte aucune responsabilité quant à l'exactitude, l'intégralité, la performance ou la fiabilité du code sur cette page. Dracal Technologies Inc. ne pourrait être tenue responsable à votre égard des dommages, incluant les dommages génériques, spécifiques, secondaires ou consécutifs, résultant de l'utilisation ou de l'incapacité d'utiliser le code (y compris, mais sans y être limité, la perte de données, ou le fait que des données soient rendues imprécises, ou les pertes éprouvées par vous ou par des tiers).