Code examples for integrating data from your VCP-enabled Dracal sensors
[Last update: 14/03/2024]
- Introduction
- 1. Prerequisites
- 2. Examples in different programming languages
- 3. License and disclaimer
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
Fully-functional Python code sample GitHub repository
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. Libraries used:
- 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: Both libraries are listed on pypi.org and can be installed using 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))
generating the following output:
2.2. C (POSIX)
Fully-functional C (posix) code sample GitHub repository
This example allows you to access data from a sensor in VCP mode using the C language (for *nix-like OSes; e.g. Unix/Linux). You will find methods that open a connection, send, read and interpret data from the device.
Note: It 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.
This makes use of libcrc library. To learn how to use its functions, please refer to the instructions specific to your compiler or integrated development environment (IDE) on how to include libraries statically (the Github repository above provides one example).
#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;
}
}
generating the following output:
2.3. C/C++ (Win32)
Fully-functional C/C++ (Win32) code sample GitHub repository
This example allows you to access data from a sensor in VCP mode using C/C++ (Windows/Win32). You will find methods that open a connection, send, read and interpret data from the device.
Note: It 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.
This makes use of libcrc library. To learn how to use its functions, please refer to the instructions specific to your compiler or integrated development environment (IDE) on how to include libraries statically (the Github repository above provides one example).
#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;
}
generating the following output:
2.4. Node.js
Fully-functional Node.js code sample GitHub repository
This Node.js example below has been produced using a VCP-PTH450-CAL sensor, but should be able to adapt to other sensors - similarly to the Python example. It sequentially opens the serial connection, sends relevant write instructions and interprets resulting output. We're adding timestamps in front of each data line, and data integrity validation is also performed. JS libraries used (both part of the Node SerialPort ecosystem available on NPM package manager):
- 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');
});
generating the following output:
2.5. Bash
Fully-functional Bash code sample GitHub repository
The Bash example below has been produced using a VCP-PTH450-CAL sensor, but should be able to adapt to other sensors - similarly to the Python and Node.js examples. It sequentially opens the serial connection, sends relevant write instructions and interprets resulting output. We're adding timestamps in front of each data line, and simple data validation is also performed. It uses a few shell-scripting concepts, including:
- File descriptors
- IFS and arrays
#!/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<&-
generating the following output:
2.6. Java
Fully-functional Java code sample GitHub repository
The Java example below has been produced using a VCP-PTH450-CAL sensor, but should be able to adapt to other sensors - similarly to the Python and Node examples. It sequentially opens the serial connection, sends relevant write instructions and interprets resulting output. On the Read, line breaks (\r\n) are processed manually. We're adding timestamps in front of each data line, and basic data validation is also performed. Library java-native:jssc is used to read serial ports.
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");
}
}
generating the following output:
2.7. C#
Fully-functional C# code sample GitHub repository
The C# example below has been produced using a VCP-PTH450-CAL sensor; it should be able to adapt to other sensors - similarly to the Python and Node examples.
It sequentially opens the serial connection, sends relevant write instructions and interprets resulting output through a status-bound loop. On the Read, line breaks are processed automatically through Readline()
. We're adding timestamps in front of each data line, and basic data validation is also performed.
Note that the standard Read approach with SerialPort
would be to use an Event handler added to `port.DataReceived`. Unfortunately that only seems to read input reactions, but not subsequent/regular outputs from the device. This is why the loop option is chosen here. You are of course welcome to branch out and open a Pull Request if you'd like to propose other solutions to this.
System.IO
.Ports.SerialPort
is used to read the device.
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");
}
}
generating the following output:
2.8. C++/CLI (.NET)
Fully-functional C++ (.NET) code sample GitHub repository
The C++/CLI (.NET) example below has been produced using a VCP-PTH450-CAL sensor; it should be able to adapt to other sensors - similarly to the Python and Node examples.
It sequentially opens the serial connection, sends relevant write instructions and interprets resulting output through a status-bound loop. On the Read, line breaks are processed automatically through Readline()
. We're adding timestamps in front of each data line, and basic data validation is also performed.
Note that - similarly to C# - the standard Read approach with SerialPort
would be to use an Event handler added to `port->DataReceived`. Unfortunately that only seems to read input reactions, but not subsequent/regular outputs from the device. This is why the loop option is chosen here.
System::IO::Ports::SerialPort
is used to read the device.
#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());
}
}
generating the following output:
2.9. VB.Net
Fully-functional VB.Net code sample GitHub repository
The VB.Net example below has been produced using a VCP-PTH450-CAL sensor; it should be able to adapt to other sensors - similarly to C#, Python and Node examples.
It sequentially opens the serial connection, sends relevant write instructions and interprets resulting output through a status-bound loop. On the Read, line breaks are processed automatically through Readline()
. We're adding timestamps in front of each data line, and basic data validation is also performed.
Note that - similarly to C# - the standard Read approach with SerialPort
would be to use an Event handler added to port->DataReceived
. Unfortunately that only seems to read input reactions, but not subsequent/regular outputs from the device. This is why the loop option is chosen here.
System.IO.Ports.SerialPort
is used to read the device.
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
generating the following output:
3. 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).