Enterprise

# Connect custom devices

> Connect a device that the Driver does not support with a Python or TypeScript script: record its values, send it commands, and show its state in the Console.

When the [Driver](https://docs.synnaxlabs.com/reference/hardware#supported-integrations) does not support your device, connect it with a script. The script reads values from the device and writes them to channels, and streams commands from channels and sends them to the device. This page uses an Arduino on a serial port. The same steps work for each device that your code can read from and write to.

## Prerequisites

* A [Synnax Enterprise](https://docs.synnaxlabs.com/reference/enterprise) Core that the computer of the device can reach. Scripts cannot connect to Synnax Desktop.
* The [Console](https://docs.synnaxlabs.com/reference/enterprise/console), logged in to the same Core.
* An Arduino board and the [Arduino IDE](https://www.arduino.cc/en/software).

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

Python 3.12 or later, with the client and pyserial:

```bash
pip install synnax pyserial
```

**TypeScript**

Node.js 22 or later. The scripts use top-level await, so make an ES module project:

```bash
npm init -y
npm pkg set type=module
npm install @synnaxlabs/client serialport @serialport/parser-readline tsx
```

Run each script with `npx tsx <file>`.

The scripts connect to `localhost:9090` with the username `synnax` and the password `seldon`, the defaults of a new Enterprise Core. Change these values to match your Core. See [Connect to a Core](https://docs.synnaxlabs.com/reference/client/connect). Set `PORT` to the serial port of your Arduino, which the Arduino IDE shows.

## Record values from the device

### Step 1Upload the Arduino code

The Arduino sends the value of analog input `A0` on the serial port about 10 times each second:

```cpp
const int analogPin = A0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println(analogRead(analogPin));
  delay(100);
}
```

### Step 2Run the script

The script creates an index channel `arduino_time` and a data channel `arduino_value`. Then it writes each value with the time that it arrived.

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

```python
import serial
import synnax as sy

PORT = "/dev/ttyACM0"

client = sy.Synnax(
    host="localhost",
    port=9090,
    username="synnax",
    password="seldon",
)

time = client.channels.create(
    name="arduino_time",
    is_index=True,
    data_type=sy.DataType.TIMESTAMP,
    retrieve_if_name_exists=True,
)
client.channels.create(
    name="arduino_value",
    index=time.key,
    data_type=sy.DataType.FLOAT32,
    retrieve_if_name_exists=True,
)

device = serial.Serial(PORT, 9600)
with client.open_writer(
    start=sy.TimeStamp.now(),
    channels=["arduino_time", "arduino_value"],
) as writer:
    while True:
        line = device.readline().decode("utf-8").strip()
        try:
            value = float(line)
        except ValueError:
            continue
        writer.write({"arduino_time": sy.TimeStamp.now(), "arduino_value": value})
```

**TypeScript**

```typescript
import { ReadlineParser } from "@serialport/parser-readline";
import { DataType, Synnax, TimeStamp } from "@synnaxlabs/client";
import { SerialPort } from "serialport";

const PORT = "/dev/ttyACM0";

const client = new Synnax({
  host: "localhost",
  port: 9090,
  username: "synnax",
  password: "seldon",
});

const time = await client.channels.create(
  { name: "arduino_time", isIndex: true, dataType: DataType.TIMESTAMP },
  { retrieveIfNameExists: true },
);
await client.channels.create(
  { name: "arduino_value", index: time.key, dataType: DataType.FLOAT32 },
  { retrieveIfNameExists: true },
);

const device = new SerialPort({ path: PORT, baudRate: 9600 });
const lines = device.pipe(new ReadlineParser({ delimiter: "\n" }));
const writer = await client.openWriter({
  start: TimeStamp.now(),
  channels: ["arduino_time", "arduino_value"],
});
try {
  for await (const line of lines) {
    const value = Number.parseFloat(line);
    if (Number.isNaN(value)) continue;
    await writer.write({ arduino_time: TimeStamp.now(), arduino_value: value });
  }
} finally {
  await writer.close();
}
```

The script skips a line that is not a number. The first line after the port opens can be incomplete. With `retrieve_if_name_exists` (`retrieveIfNameExists` in TypeScript), the script uses the existing channels when you run it again. For more about writers, see [Write data](https://docs.synnaxlabs.com/reference/client/write-data).

### Step 3Plot the values

While the script runs, open a line plot in the Console and add `arduino_value`:

[](https://synnax.nyc3.cdn.digitaloceanspaces.com/docs/client/resources/build-device-driver/line-plot-dark.mp4#t=0.001)

## Send commands to the device

### Step 1Upload the Arduino code

The Arduino sets its built-in LED on pin 13 when it gets the character `1`, and clears it when it gets `0`:

```cpp
const int digitalPin = 13;

void setup() {
  Serial.begin(9600);
  pinMode(digitalPin, OUTPUT);
}

void loop() {
  if (Serial.available() > 0) {
    char command = Serial.read();
    if (command == '1') digitalWrite(digitalPin, HIGH);
    else if (command == '0') digitalWrite(digitalPin, LOW);
  }
  delay(10);
}
```

### Step 2Run the script

The script creates the command channel `arduino_command`. It is a [virtual channel](https://docs.synnaxlabs.com/reference/channels#virtual-channels), so the Core does not store its values, and it needs no index channel. The script streams the channel, and sends each command to the Arduino.

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

```python
import serial
import synnax as sy

PORT = "/dev/ttyACM0"

client = sy.Synnax(
    host="localhost",
    port=9090,
    username="synnax",
    password="seldon",
)

client.channels.create(
    name="arduino_command",
    data_type=sy.DataType.UINT8,
    virtual=True,
    retrieve_if_name_exists=True,
)

device = serial.Serial(PORT, 9600)
with client.open_streamer(["arduino_command"]) as streamer:
    for frame in streamer:
        command = int(frame["arduino_command"][-1])
        device.write(str(command).encode("utf-8"))
```

**TypeScript**

```typescript
import { DataType, Synnax } from "@synnaxlabs/client";
import { SerialPort } from "serialport";

const PORT = "/dev/ttyACM0";

const client = new Synnax({
  host: "localhost",
  port: 9090,
  username: "synnax",
  password: "seldon",
});

await client.channels.create(
  { name: "arduino_command", dataType: DataType.UINT8, virtual: true },
  { retrieveIfNameExists: true },
);

const device = new SerialPort({ path: PORT, baudRate: 9600 });
const streamer = await client.openStreamer(["arduino_command"]);
try {
  for await (const frame of streamer) {
    const command = frame.get("arduino_command").at(-1);
    device.write(String(command));
  }
} finally {
  streamer.close();
}
```

When a frame holds more than one command, the script sends only the last one.

### Step 3Add a switch

While the script runs, add a switch to a [schematic](https://docs.synnaxlabs.com/reference/visualize/schematics), and set its command channel and its state channel to `arduino_command`. Then take control of the schematic, and click the switch:

[](https://synnax.nyc3.cdn.digitaloceanspaces.com/docs/client/resources/build-device-driver/schematic-dark.mp4#t=0.001)

The switch changes when you click it, even when the script does not run. Five seconds after each click, it shows the stale color. The next section fixes this.

## Record the state of the device

To show the real state of the device, use two channels. The switch writes to a command channel, and shows the value of a state channel that the script writes from the device. When the script or the device stops, the switch keeps its last state and shows the stale color after 5 seconds.

### Step 1Upload the Arduino code

The Arduino runs each command, and sends its state and the value of `A0` on one line, such as `1,512`:

```cpp
const int digitalPin = 13;
const int analogPin = A0;
int state = 0;

void setup() {
  Serial.begin(9600);
  pinMode(digitalPin, OUTPUT);
}

void loop() {
  if (Serial.available() > 0) {
    char command = Serial.read();
    if (command == '1') state = 1;
    else if (command == '0') state = 0;
    digitalWrite(digitalPin, state);
  }
  Serial.print(state);
  Serial.print(",");
  Serial.println(analogRead(analogPin));
  delay(10);
}
```

### Step 2Run the script

The script creates a command channel, an index channel, and two data channels for the state and the value. It streams commands to the Arduino, and writes the state and the value from the Arduino.

[Enterprise](https://docs.synnaxlabs.com/reference/get-started/editions "Needs Synnax Enterprise")

**Python**

```python
import serial
import synnax as sy

PORT = "/dev/ttyACM0"

client = sy.Synnax(
    host="localhost",
    port=9090,
    username="synnax",
    password="seldon",
)

client.channels.create(
    name="arduino_command",
    data_type=sy.DataType.UINT8,
    virtual=True,
    retrieve_if_name_exists=True,
)
time = client.channels.create(
    name="arduino_time",
    is_index=True,
    data_type=sy.DataType.TIMESTAMP,
    retrieve_if_name_exists=True,
)
client.channels.create(
    name="arduino_state",
    index=time.key,
    data_type=sy.DataType.UINT8,
    retrieve_if_name_exists=True,
)
client.channels.create(
    name="arduino_value",
    index=time.key,
    data_type=sy.DataType.FLOAT32,
    retrieve_if_name_exists=True,
)

device = serial.Serial(PORT, 9600)
with client.open_streamer(["arduino_command"]) as streamer, client.open_writer(
    start=sy.TimeStamp.now(),
    channels=["arduino_time", "arduino_state", "arduino_value"],
) as writer:
    while True:
        frame = streamer.read(timeout=0)
        if frame is not None:
            command = int(frame["arduino_command"][-1])
            device.write(str(command).encode("utf-8"))
        line = device.readline().decode("utf-8").strip()
        try:
            state, value = line.split(",")
            sample = {"arduino_state": int(state), "arduino_value": float(value)}
        except ValueError:
            continue
        writer.write({"arduino_time": sy.TimeStamp.now(), **sample})
```

`streamer.read(timeout=0)` returns `None` at once when no command is waiting, so the loop does not stop at the streamer.

**TypeScript**

```typescript
import { ReadlineParser } from "@serialport/parser-readline";
import { DataType, Synnax, TimeStamp } from "@synnaxlabs/client";
import { SerialPort } from "serialport";

const PORT = "/dev/ttyACM0";

const client = new Synnax({
  host: "localhost",
  port: 9090,
  username: "synnax",
  password: "seldon",
});

await client.channels.create(
  { name: "arduino_command", dataType: DataType.UINT8, virtual: true },
  { retrieveIfNameExists: true },
);
const time = await client.channels.create(
  { name: "arduino_time", isIndex: true, dataType: DataType.TIMESTAMP },
  { retrieveIfNameExists: true },
);
await client.channels.create(
  [
    { name: "arduino_state", index: time.key, dataType: DataType.UINT8 },
    { name: "arduino_value", index: time.key, dataType: DataType.FLOAT32 },
  ],
  { retrieveIfNameExists: true },
);

const device = new SerialPort({ path: PORT, baudRate: 9600 });
const lines = device.pipe(new ReadlineParser({ delimiter: "\n" }));
const streamer = await client.openStreamer(["arduino_command"]);
const writer = await client.openWriter({
  start: TimeStamp.now(),
  channels: ["arduino_time", "arduino_state", "arduino_value"],
});

const sendCommands = async () => {
  for await (const frame of streamer)
    device.write(String(frame.get("arduino_command").at(-1)));
};

const recordState = async () => {
  for await (const line of lines) {
    const match = /^(\d+),(\d+)/.exec(line);
    if (match == null) continue;
    await writer.write({
      arduino_time: TimeStamp.now(),
      arduino_state: Number(match[1]),
      arduino_value: Number(match[2]),
    });
  }
};

try {
  await Promise.all([sendCommands(), recordState()]);
} finally {
  streamer.close();
  await writer.close();
}
```

The two loops run at the same time: one sends commands, and one records the state and the value.

### Step 3Bind the switch to the state

While the script runs, set the command channel of the switch to `arduino_command` and its state channel to `arduino_state`. Then take control, and click the switch. It turns on when the Arduino reports the new state:

[](https://synnax.nyc3.cdn.digitaloceanspaces.com/docs/client/resources/build-device-driver/console-setup-dark.mp4#t=0.001)

To plot `arduino_value`, add it to a line plot, as in the first section.

## When to use the Driver

A script is the fastest way to connect one device. When the Driver supports your device, use a [task](https://docs.synnaxlabs.com/reference/hardware/tasks) instead. Tasks need no code, and you start and stop them from the Console. See [Supported integrations](https://docs.synnaxlabs.com/reference/hardware#supported-integrations).
