Interacting with Shimmer3 using Node SerialPort
Tags: javascript, bluetooth, serialport, node
Published: 2023-04-23 09:46:24

The [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/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](https://blog.nordicsemi.com/getconnected/the-difference-between-classic-bluetooth-and-bluetooth-low-energy) explains it well. The W3C Draft Community Group [Report](https://webbluetoothcg.github.io/web-bluetooth/) 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](https://dev.to/manufac/interacting-with-polar-verity-sense-using-web-bluetooth-553a) 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](https://www.bluetooth.com/learn-about-bluetooth/feature-enhancements/le-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](https://vimeo.com/717719167)!

> {% embed https://player.vimeo.com/video/717719167?h=c55b79e250"  %}

---

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](https://shimmersensing.com/product/shimmer3-gsr-unit/), 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](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API) (for browser environments) and [Node SerialPort](https://serialport.io/) (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.

> {% embed https://github.com/ShimmerResearch/shimmer3 %}

**Steps:**

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

```ts
import { SerialPort } from "serialport";
const availablePorts = await SerialPort.list();
```

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

```ts
const port = new SerialPort({ path: comPort, baudRate: 115200 }); 
```

`115200` was chosen as the baud rate as per the Shimmer team's GitHub [info](https://github.com/ShimmerResearch/shimmer3/blob/master/LogAndStream/python_scripts/s1_aAccel5Hz-sdbt.py#L21). 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](https://wicg.github.io/serial/#open-method), it is necessary to open the port before beginning any communication with the device.

C. The only thing left now is [communicate](https://serialport.io/docs/guide-usage#reading-data) with the device. The data can be read and written in the following way:

```ts
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'));

```

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

```ts
port.close()
```

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:

```ts
import { pack } from "python-struct";

const command = pack("BBBB", 0x08, 0x04, 0x21, 0x00); 
port.write(command);
```

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

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

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

```ts
const samplingFrequency = 2;
const clockWait = 32768 / samplingFrequency;
const command = pack("<BH", 0x05, clockWait);
port.write(command);
```

**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:

```ts
const command = pack("B", 0x07);
port.write(command);
```

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

```ts
port.on("data", (res: Buffer) => {
  // Do whatever
}
```

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

```ts
const command = pack("B", 0x20);
port.write(command);
// Wait for acknowledgement, and then close the port
...
...
port.close();
```

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?

```ts
import { pack } from "python-struct";

const command = pack("BBBB", 0x08, 0x04, 0x21, 0x00); 
```

[struct](https://docs.python.org/3/library/struct.html) is a Python core module and [python-struct](https://github.com/danielgindi/node-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`:

> {% embed https://youtu.be/gViM3ZuDQrw %}

- Node SerialPort also ships with a bunch of [parsers](https://serialport.io/docs/api-parsers-overview) 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`.

---