DEV Community

Cover image for Interacting with Shimmer3 using Node SerialPort

Interacting with Shimmer3 using Node SerialPort

The Web Bluetooth API allows us to connect and interact with Bluetooth Low Energy (BLE) devices. But what are these BLE devices, and is there any other category of bluetooth devices too?

A Nordic Semiconductor blog explains it well. The W3C Draft Community Group Report on Web Bluetooth also indicates towards two major modes of Bluetooth protocols: Bluetooth Classic and Bluetooth Low Energy (alias, Bluetooth Smart). The Classic variant can support data transfer speeds of upto 24 Mbps, whereas BLE is limited to about 1MBps only.

But then why is BLE "smart"? The hint is in the name. BLE protocol allow the devices to leave their transmitter off most of the time, hence, reducing their energy/battery consumption.

So, we have Web Bluetooth API to handle BLE devices. We've established that in the past as well, but what about Bluetooth Classic devices? How to connect to those? Bluetooth Classic has good use cases in audio streaming applications. Even if BLE Audio takes over these audio applications in the future, there can still be legacy Bluetooth devices that you need to connect to.

BTW, Auracast, which is one of the BLE Audio capabilities, looks so cool!


In this article, we'll try to address how to connect to a Bluetooth Classic device in the JS/TS world. Instead of just giving a general overview of such a process, we will also focus on connecting a NodeJS application to Shimmer3, a legacy Bluetooth device.

We hope that at least some of the learnings shared below are transferable to your own use cases and help you connect to your devices without feeling too information-constrained.


Let's begin!

There are some legacy bluetooth (Bluetooth Classic) devices that emulate a serial port. Hence, we can treat such devices as serial devices and open up the possibility to utilize tools like the Web Serial API (for browser environments) and Node SerialPort (for NodeJS environments) to interact with them.

Under this premise, here's how we interacted with a Shimmer3 device (which does emulate a serial port) via a NodeJS application.

The Shimmer team has open sourced the code for interacting with a Shimmer3 device using Python. The JS/TS approach that we are sharing is built upon the learnings of their Python source code.

GitHub logo ShimmerResearch / shimmer3

Shimmer3 applications for Code Composer Studio

Steps:

A. Fetch a list of all available Serial Port devices.

import { SerialPort } from "serialport";
const availablePorts = await SerialPort.list();
Enter fullscreen mode Exit fullscreen mode

B. Establish connection with the desired port (the one corresponding to the Shimmer3 device).

const port = new SerialPort({ path: comPort, baudRate: 115200 }); 
Enter fullscreen mode Exit fullscreen mode

115200 was chosen as the baud rate as per the Shimmer team's GitHub info. This value indicates that the concerned port is capable of transferring a maximum of 115200 bits per second. Further, instantiating the SerialPort class immediately opens the port. As per the Web Serial API specifications draft, it is necessary to open the port before beginning any communication with the device.

C. The only thing left now is communicate with the device. The data can be read and written in the following way:

import { Buffer } from "node:buffer";

// Read

port.on('data', (data: Buffer) => {
  console.log(`Data: ${data}`);
});

// Write

port.write('Message Details');

// OR

port.write(Buffer.from('Message Details'));

Enter fullscreen mode Exit fullscreen mode

D. Once done, the port can simply be closed:

port.close()
Enter fullscreen mode Exit fullscreen mode

Now that we have a general handle over things, let's get into some specifics to understand how a typical communication between a serial port device and a NodeJS application might look like.

You might have already inferred by now that serial ports support 2-way data transfer (data coming from the device & data going to the device).

In the context of Shimmer3, the data that gets sent to the device are some types of commands. In return, the device responds back with some type of response.

Say, we want to read Battery Voltage, GSR, and PPG values from the device. We can convey it to the device via:

import { pack } from "python-struct";

const command = pack("BBBB", 0x08, 0x04, 0x21, 0x00); 
port.write(command);
Enter fullscreen mode Exit fullscreen mode

Thereafter, we should wait for the device to acknowledge & accept our command. The acknowledgement message that Shimmer3 returns looks like:

port.on("data", (res: Buffer) => {
    const acknowledgment = pack("B", 0xff);
    if (res.toString() === acknowledgment.toString()) {
      console.log("Device has acknowledged our command.");
    }
});
Enter fullscreen mode Exit fullscreen mode

Similarly, we can specify the sampling frequency to the device via:

const samplingFrequency = 2;
const clockWait = 32768 / samplingFrequency;
const command = pack("<BH", 0x05, clockWait);
port.write(command);
Enter fullscreen mode Exit fullscreen mode

For each such command, we should wait for an acknowledgement as done above.

Finally, we can ask the device to start streaming by sending the following command:

const command = pack("B", 0x07);
port.write(command);
Enter fullscreen mode Exit fullscreen mode

The data can be received the same way as done before:

port.on("data", (res: Buffer) => {
  // Do whatever
}
Enter fullscreen mode Exit fullscreen mode

Once done, the streaming can be stopped and the port can be closed:

const command = pack("B", 0x20);
port.write(command);
// Wait for acknowledgement, and then close the port
...
...
port.close();
Enter fullscreen mode Exit fullscreen mode

That's it; that's the majority of the workflow. YMMV depending on the device you end up using, the structure of the request (command) and response messages, your data post-processing needs, etc.


Parting Notes:

  • So far, we have seen how to open the serial port, set up the device, start streaming, receive data, stop streaming, and finally, close the port. However, it still may not have made complete sense since were dealing with binary format.

What does the following stuff from earlier even mean?

import { pack } from "python-struct";

const command = pack("BBBB", 0x08, 0x04, 0x21, 0x00); 
Enter fullscreen mode Exit fullscreen mode

struct is a Python core module and python-struct happens to be a JS equivalent of it. Essentially, this module helps in packing data into binary format in a structured way so that the data can later be decoded & consumed properly by the receiving device. The decoding/unpacking can be done using the unpack command.

Here's a good primer on struct:

  • Node SerialPort also ships with a bunch of parsers to turn the binary data into readable bits of messages. Depending on your use case, these parsers may turn out to be more suitable as compared to python-struct. Likewise, some interesting use cases might even warrant using these parsers in tandem with python-struct.

Top comments (0)