Code examples for integrating data from your VCP-enabled Dracal sensors

[Last update: 28/06/2023]

computer with codes and Dracal logo + VCP infographics

 

Introduction

It is possible to acquire the majority of Dracal USB sensors equipped with the VCP option. This option allows the user to choose the communication protocol used by the instrument to transmit its data. Instruments equipped with this option can be identified by the presence of the prefix "VCP-" (instead of "USB-") in their product code. The use of the VCP protocol allows for data integration without the need for third-party software such as our command-line tool dracal-usb-get, for example. Follow this link for an overview of the integration tools available for your Dracal products.

The objective of this page is to illustrate through concrete examples how to integrate the data from your instruments communicating in VCP mode using different languages and environments.

1) Prerequisites

1. Have an instrument equipped with the VCP option.

These instruments can be identified by the "VCP-" prefix present in their product name.

2. Have basic knowledge of using the command line.

Why? Because all Dracal instruments are delivered in USB mode and the switch between USB and VCP modes is done through command-line tools. Follow this link if you are looking for where to find the command-line tools.

3. Have read the VCP product user guide.

If you haven't already, here is the link to the VCP product user guide.

This documentation also assumes that you have successfully switched your instrument from USB mode to VCP mode and have successfully communicated with your instrument. If this is not the case, it is recommended to consult the tutorial Getting Started with VCP Mode before continuing your reading.

2) Examples in different programming languages

2.1) Python

Here is an example of using Dracal sensors using only Python libraries. It adds a timestamp in front of each data line and saves them to a file. Data integrity validation is also performed.

- The "serial" module allows us to interact with the device using the VCP protocol.
- The "crccheck" module allows us to verify the integrity of the received data.

Note: The "serial" module used here is part of the "pyserial" package listed on pypi.org and must be installed using "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))

2.2) C (POSIX)

Here is an example that allows you to access data from a sensor in VCP mode using the C language. In this example, you will find methods to:

- Open a connection with the device

- Send commands to the device

- Read data from the device

- Interpret the text and save the data into numerical variables

Note: This example was designed to work with sensors from the PTH series. You will need to modify the format strings and variable declarations to work with other sensor types.

To learn how to use the libcrc functions, please refer to the instructions specific to your compiler or integrated development environment (IDE) on how to include libraries statically.


#include <stdio.h> // standard input / output functions
#include <stdlib.h> // general purpose functions
#include <string.h> // string function definitions
#include <unistd.h> // UNIX standard function definitions
#include <fcntl.h> // File control definitions
#include <errno.h> // Error number definitions
#include <termios.h> // POSIX terminal control definitions
#include <stdbool.h> // Boolean types and values
#include <time.h> // Timekeeping types and functions
#include <regex.h> // 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/tty.usbmodemE165181";

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
        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(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) {

  // 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");
      }
      sleep()
    }

    // (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_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 {
      /**
       * 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,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;
}

2.3) C/C++ (Win32)

In this example, you will find methods to:

- Open a connection with the device

- Send commands to the device

- Read data from the device

- Interpret the text and save the data into numerical variables

Note: This example was designed to work with sensors from the PTH series. You will need to modify the format strings and variable declarations to work with other sensor types.

To learn how to use the libcrc functions, please refer to the instructions specific to your compiler or integrated development environment (IDE) on how to include libraries statically.


#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;
}

License and disclaimer

The code on this page is in the Public domain and may be freely incorporated in any software project, commercial or otherwise.

The code examples on this page are provided "as is" in the hope that they will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Best practises, such as error checking and handling, input sanitization, and testing are YOUR responsibility, and YOU assume the entire risk as to the quality and performance of the code.

Dracal Technologies Inc. does not accept any responsibility of liability for the accuracy, completeness, or reliability of the code presented on this page. In no event will Dracal Technologies Inc. be liable to you for damages, including any general, special, incidental or consequential damages arising out of the use or inability to use the code (including but not limited to loss of data or data being rendered inaccurate or losses sustained by you or third parties).