Raspi Control SainSmart USB Connected Relay

I'm in the process of switching from a Denkovi wifi modbus relay to a SainSmart USB connected relay, both are 16 port relays. I'm trying to use gdziuba/node-red-usbhid to connect up. I've also tried the other hid modules and can't get any of them to work as expected. I'm assuming this has something to do with the configuration of the Raspberry Pi more than it is an issue with Node-Red but I'm not positive about that.

This is the new SainSmart relay: 16-Channel 9-36V USB Relay Module – SainSmart.com

Below is the USBHID flow I'm trying to use to connect to the USB Relay and a screenshot of the LUSB scan to determine the VID and PID.

I've added a file to /etc/udev/rules.d with the additional udev rules I think are needed and added the pi user to the input group.

The debug data is below - the getHIDdevices is throwing the empty array and the HIDdevice is showing disconnected and debug shows the bottom error from that node.

Any ideas on what I'm missing on this? I'm switching relays because I want the local connection for reliability and the Denkovi relay also just stopped connecting and being accessible to my network. A full factory reset on the device (several times) wasn't able to fix it so I'm moving the switchover to the SainSmart relay up on the schedule but struggling to make the SainSmart relay available to node-red.

I haven't really been able to find any support for SainSmart USB relays with node-red so hopefully this thread also is useful for the next person.

[{"id":"cbae0763803aa6e9","type":"group","z":"3b6a718c4021497b","name":"USBHID","style":{"label":true},"nodes":["78f82fb335672aca","c1ecae6308a0525f","d99660be8c6c6066","546ab3a525f72ecc","1b5120024bf4bc62","f8d8cae1919e186c","e1d69c8863f6de73"],"x":34,"y":19,"w":692,"h":122},{"id":"78f82fb335672aca","type":"debug","z":"3b6a718c4021497b","g":"cbae0763803aa6e9","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":460,"y":60,"wires":[]},{"id":"c1ecae6308a0525f","type":"getHIDdevices","z":"3b6a718c4021497b","g":"cbae0763803aa6e9","name":"","x":300,"y":60,"wires":[["78f82fb335672aca"]]},{"id":"d99660be8c6c6066","type":"inject","z":"3b6a718c4021497b","g":"cbae0763803aa6e9","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":60,"wires":[["c1ecae6308a0525f"]]},{"id":"546ab3a525f72ecc","type":"HIDdevice","z":"3b6a718c4021497b","g":"cbae0763803aa6e9","connection":"68a0ec4b6af1dcb1","name":"","x":290,"y":100,"wires":[["e1d69c8863f6de73"],[]]},{"id":"1b5120024bf4bc62","type":"inject","z":"3b6a718c4021497b","g":"cbae0763803aa6e9","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":100,"wires":[["546ab3a525f72ecc"]]},{"id":"f8d8cae1919e186c","type":"debug","z":"3b6a718c4021497b","g":"cbae0763803aa6e9","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":620,"y":100,"wires":[]},{"id":"e1d69c8863f6de73","type":"function","z":"3b6a718c4021497b","g":"cbae0763803aa6e9","name":"Hex to String","func":"const hexBuffer = msg.payload;\n\n// Convert the hex buffer to a Buffer object\nconst buffer = Buffer.from(hexBuffer, 'hex');\n\n// Convert the Buffer to a string\nconst string = buffer.toString();\n\n// Assign the string to msg.payload for further processing\nmsg.payload = string;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":100,"wires":[["f8d8cae1919e186c"]]},{"id":"68a0ec4b6af1dcb1","type":"HIDConfig","vid":"1504","pid":"1536","name":"Barcode Scanner"}]

Screenshot 2024-09-30 at 1.08.54 PM

Can you point us to the documentation for using the board?

This is about the only documentation I can find from their website. I've pasted the text of this document below. I am able to setup a serial control node to /dev/ttyusb0 and that's showing a green light, a serial-in node is showing not connected and a serial-request node times out when I try to send a status request of "status = usbrelay[0][1] " to it via an inject node. I'm assuming I have something configured wrong here but changing to serial communications is maybe the direction I need to go?

https://s3.us-east-1.amazonaws.com/s3.image.smart/download/101-70-208/WIN10%26Python3.7.txt


This 16-channel usb relay (101-70-208) works great with Python 3.7 and Windows 10 (see sample Python code)
The HEX table from Sainsmart needs to be converted to ASCII chars. See sample Python code:
========== python code below =========
import serial
from time import sleep

port = 'COM13' # change this to YOUR usb port
ser = serial.Serial(port, 9600, timeout=5)

delay = 1
tap_delay = 2
ready_delay = 3
off_delay = .5

"""
Sainsmart 16-Channel 9-36v USB Relay Module (CH341 chip)(sku# 101-70-208)

  1. Requires CH341 Windows driver installed (see http://wiki.sainsmart.com/index.php/101-70-208)
  2. The hex table provided by Sainsmart requires converting to ASCII chars (see 'usbrelay' below)
  3. Format of 'usbrelay' two-dimensional array is:
    usbrelay = [row][ch-off, ch-on] so that the 2nd index selects ON/OFF value
    while the 1st index selects the array row.
    Example...
    status = usbrelay[0][1] # row-0 (status)
    stat_ret = usbrelay[0][0] # row-0 (status return)
    ch_1_on = usbrelay[1][1] # row-1 (chan-1 off)
    ch_1_off = usbrelay[1][0] # row-1 (chan-1 off)
    ch_16_on = usbrelay[16][1] # row-16 (chan-16 on)
    ch_16_off = usbrelay[16][0] # row-16 (chan-16 off)
    all_on = usbrelay[17][1] # row-17 (all on)
    all_off = usbrelay[17][0] # row-17 (all off)
    """

usbrelay = [[b':FE0100200000FF\r\n', b':FE0100000010F1\r\n'], # status & status return
[b':FE0500000000FD\r\n', b':FE050000FF00FE\r\n'], # channel-1
[b':FE0500010000FC\r\n', b':FE050001FF00FD\r\n'], # channel-2
[b':FE0500020000FB\r\n', b':FE050002FF00FC\r\n'], # channel-3
[b':FE0500030000FA\r\n', b':FE050003FF00FB\r\n'], # channel-4
[b':FE0500040000F9\r\n', b':FE050004FF00FA\r\n'], # channel-5
[b':FE0500050000F8\r\n', b':FE050005FF00F9\r\n'], # channel-6
[b':FE0500060000F7\r\n', b':FE050006FF00F8\r\n'], # channel-7
[b':FE0500070000F6\r\n', b':FE050007FF00F7\r\n'], # channel-8
[b':FE0500080000F5\r\n', b':FE050008FF00F6\r\n'], # channel-9
[b':FE0500090000F4\r\n', b':FE050009FF00F5\r\n'], # channel-10
[b':FE05000A0000F3\r\n', b':FE05000AFF00F4\r\n'], # channel-11
[b':FE05000B0000F2\r\n', b':FE05000BFF00F3\r\n'], # channel-12
[b':FE05000C0000F1\r\n', b':FE05000CFF00F2\r\n'], # channel-13
[b':FE05000D0000F0\r\n', b':FE05000DFF00F1\r\n'], # channel-14
[b':FE05000E0000FF\r\n', b':FE05000EFF00F0\r\n'], # channel-15
[b':FE05000F0000FE\r\n', b':FE05000FFF00FF\r\n'], # channel-16
[b':FE0F00000010020000E1\r\n', b':FE0F0000001002FFFFE3\r\n']] # all channels

# uncomment below for quick sequencing test

for row in range(len(usbrelay)): # sequence through each row

ser.write(usbrelay[row][1]) # turn ON

sleep(delay/2)

ser.write(usbrelay[row][0]) # turn OFF

sleep(delay/2)

below is example of turning single usbrelay channel on/off

def ch_9_on_off():
ser.write(usbrelay[9][1])
sleep(delay)
ser.write(usbrelay[9][0])
sleep(off_delay)
def ch_10_on_off():
ser.write(usbrelay[10][1])
sleep(delay)
ser.write(usbrelay[10][0])
sleep(off_delay)
def ch_11_on_off():
ser.write(usbrelay[11][1])
sleep(delay)
ser.write(usbrelay[11][0])
sleep(off_delay)
def ch_12_on_off():
ser.write(usbrelay[12][1])
sleep(delay)
ser.write(usbrelay[12][0])
sleep(off_delay)

# below is example of using this usbrelay bd to control a 4-port usb switch.

(usb_port_switching uses PAIR of usbrelay channels)

def usb_1_ON():
ser.write(usbrelay[1][1])
ser.write(usbrelay[2][1])
sleep(ready_delay)
def usb_1_OFF():
ser.write(usbrelay[1][0])
ser.write(usbrelay[2][0])
sleep(off_delay)
def usb_2_ON():
ser.write(usbrelay[3][1])
ser.write(usbrelay[4][1])
sleep(ready_delay)
def usb_2_OFF():
ser.write(usbrelay[3][0])
ser.write(usbrelay[4][0])
sleep(off_delay)
def usb_3_ON():
ser.write(usbrelay[5][1])
ser.write(usbrelay[6][1])
sleep(ready_delay)
def usb_3_OFF():
ser.write(usbrelay[5][0])
ser.write(usbrelay[6][0])
sleep(off_delay)
def usb_4_ON():
ser.write(usbrelay[7][1])
ser.write(usbrelay[8][1])
sleep(ready_delay)
def usb_4_OFF():
ser.write(usbrelay[7][0])
ser.write(usbrelay[8][0])
sleep(off_delay)

def main():
ch_9_on_off()
ch_10_on_off()
ch_11_on_off()
ch_12_on_off()

usb_1_ON()
usb_1_OFF()
usb_2_ON()
usb_2_OFF()
usb_3_ON()
usb_3_OFF()
usb_4_ON()
usb_4_OFF()

if name == "main":
main()
ser.close()

This is the lsusb -v ouput for the device in question and it is listing it as a "serial converter".

Bus 001 Device 003: ID 1a86:7523 QinHeng Electronics CH340 serial converter
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.10
bDeviceClass 255 Vendor Specific Class
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 8
idVendor 0x1a86 QinHeng Electronics
idProduct 0x7523 CH340 serial converter
bcdDevice 2.64
iManufacturer 0
iProduct 2 USB Serial
iSerial 0
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 0x0027
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 0
bmAttributes 0x80
(Bus Powered)
MaxPower 98mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 3
bInterfaceClass 255 Vendor Specific Class
bInterfaceSubClass 1
bInterfaceProtocol 2
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x82 EP 2 IN
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0020 1x 32 bytes
bInterval 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x02 EP 2 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0020 1x 32 bytes
bInterval 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0008 1x 8 bytes
bInterval 1
Device Status: 0x0000
(Bus Powered)

Unplug the usb connection, open a command window and run tail -f /var/log/syslog and plug in the usb connector. Copy/paste here what is shown in the log. Copy/paste please, not screen shot.

What does the command /dev/serial/by_id show?

Uninstall the hid nodes, use serial instead.

Show us the output when you start node red in a command window.


journalctl output because syslog doesn't exist on raspberry pi as rsyslog isn't installed by default.

Oct 01 08:46:52 micros kernel: usb 1-1.2: new full-speed USB device number 4 using xhci_hcd

Oct 01 08:46:52 micros kernel: usb 1-1.2: New USB device found, idVendor=1a86, idProduct=7523, bcdDevice= 2.64

Oct 01 08:46:52 micros kernel: usb 1-1.2: New USB device strings: Mfr=0, Product=2, SerialNumber=0

Oct 01 08:46:52 micros kernel: usb 1-1.2: Product: USB Serial

Oct 01 08:46:52 micros kernel: ch341 1-1.2:1.0: ch341-uart converter detected

Oct 01 08:46:52 micros kernel: usb 1-1.2: ch341-uart converter now attached to ttyUSB0

Oct 01 08:46:52 micros mtp-probe[3839]: checking bus 1, device 4: "/sys/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2"

Oct 01 08:46:52 micros mtp-probe[3839]: bus: 1, device: 4 was not an MTP device

Oct 01 08:46:52 micros mtp-probe[3850]: checking bus 1, device 4: "/sys/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2"

Oct 01 08:46:52 micros mtp-probe[3850]: bus: 1, device: 4 was not an MTP device

Oct 01 08:46:54 micros ModemManager[643]: [base-manager] couldn't check support for device '/sys/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2': not supported by any plugin


pi@micros:/dev/serial $ /dev/serial/by-id/ show
-bash: /dev/serial/by-id/: Is a directory

but the by_id folder does list usb-1a86_USB_Serial-if00-port0


1 Oct 09:07:34 - [info] Node-RED version: v4.0.3
1 Oct 09:07:34 - [info] Node.js version: v20.17.0
1 Oct 09:07:34 - [info] Linux 6.6.51+rpt-rpi-v8 arm64 LE
1 Oct 09:07:34 - [info] Loading palette nodes
1 Oct 09:07:35 - [info] Dashboard version 3.6.5 started at /ui
1 Oct 09:07:36 - [info] Settings file : /home/pi/.node-red/settings.js
1 Oct 09:07:36 - [info] Context store : 'default' [module=memory]
1 Oct 09:07:36 - [info] User directory : /home/pi/.node-red
1 Oct 09:07:36 - [warn] Projects disabled : editorTheme.projects.enabled=false
1 Oct 09:07:36 - [info] Flows file : /home/pi/.node-red/flows.json
1 Oct 09:07:36 - [info] Server now running at http://127.0.0.1:1880/
1 Oct 09:07:36 - [warn] Using unencrypted credentials
1 Oct 09:07:36 - [info] Starting flows
1 Oct 09:07:36 - [warn] Unknown context store 'default' specified. Using default store.
1 Oct 09:07:37 - [info] Started flows
1 Oct 09:07:37 - [error] [serialconfig:fbc944192c31becd] serial port /dev/ttyusb0 error: Error: Error: No such file or directory, cannot open /dev/ttyusb0

This is LInux. Case matters.

Sorry, I meant to say ls -al /dev/serial/by-id
Since it does show the device you can use /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 instead of /dev/ttyUSB0. That has the advantage that it will not change if the OS decides today to mount it as /dev/ttyUSB1, which is quite possible.

Alright, It appears that I'm connected to that serial port.

Now I'm trying to figure out how to actually send commands and request relay status from the device. Any ideas on example flows out there that are similar?

What exact messages do you need to send/receive?

I'm utilizing this setup to turn on and off 16 individual peristaltic pumps using the relay to water a rack with 16 different microgreen trays for a commercial restaurant. These get watered on a set schedule multiple times a day with varying amounts of water. What I need to do with the relay is just be able to send power-on and power-off signals to each individual relay based on whether or not there is a tray in that slot.

Below is the flow I'm using for one individual slot on the relay. Because of the unreliability I was occasionally having with the Denkovi Wifi relay, I had a 2nd and 3rd turn off signal being sent on a delay to make sure the pumps turn off. None of that's important, just notes for anyone else looking at the flow.

[
    {
        "id": "0923e25dcc8f66ee",
        "type": "delay",
        "z": "4d39bd7ab87afe9a",
        "name": "",
        "pauseType": "delay",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 740,
        "y": 100,
        "wires": [
            [
                "cbe4087d2441c349",
                "271c44eaa54985e6"
            ]
        ]
    },
    {
        "id": "0e9d7eae1b870f33",
        "type": "delay",
        "z": "4d39bd7ab87afe9a",
        "name": "",
        "pauseType": "delay",
        "timeout": "10",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 740,
        "y": 140,
        "wires": [
            [
                "cbe4087d2441c349",
                "271c44eaa54985e6"
            ]
        ]
    },
    {
        "id": "271c44eaa54985e6",
        "type": "debug",
        "z": "4d39bd7ab87afe9a",
        "name": "Backup P1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 930,
        "y": 140,
        "wires": []
    },
    {
        "id": "cbe4087d2441c349",
        "type": "tcp request",
        "z": "4d39bd7ab87afe9a",
        "name": "",
        "server": "192.168.2.17",
        "port": "8899",
        "out": "time",
        "ret": "buffer",
        "splitc": "0",
        "newline": "",
        "trim": false,
        "tls": "",
        "x": 940,
        "y": 80,
        "wires": [
            []
        ]
    },
    {
        "id": "aca132168538208f",
        "type": "function",
        "z": "4d39bd7ab87afe9a",
        "name": "Send Dose",
        "func": "msg.payload = \"01+//\";\nsetTimeout(() => {\n    node.send([{payload: \"01-//\"}, {payload: \"01-//\"}]);\n}, flow.get(\"DoseTimeP1\") * 1000);\nreturn [msg, null];",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 470,
        "y": 80,
        "wires": [
            [
                "e1216a4118849adc",
                "cbe4087d2441c349",
                "60ec51f67c6ef72e"
            ]
        ]
    },
    {
        "id": "15ea8fa8f389eb9d",
        "type": "function",
        "z": "4d39bd7ab87afe9a",
        "name": "set_global_DoseTime",
        "func": "global.set(\"DoseTimeP1\",msg.payload)\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 280,
        "y": 140,
        "wires": [
            [
                "46ec7d208d525083"
            ]
        ]
    },
    {
        "id": "46ec7d208d525083",
        "type": "change",
        "z": "4d39bd7ab87afe9a",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "DoseTimeP1",
                "pt": "flow",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 500,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "b03bf6155f607823",
        "type": "inject",
        "z": "4d39bd7ab87afe9a",
        "name": "",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "str",
        "x": 310,
        "y": 40,
        "wires": [
            [
                "e39e566a7f7e8e14"
            ]
        ]
    },
    {
        "id": "e1216a4118849adc",
        "type": "debug",
        "z": "4d39bd7ab87afe9a",
        "name": "Primary P1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 630,
        "y": 60,
        "wires": []
    },
    {
        "id": "60ec51f67c6ef72e",
        "type": "switch",
        "z": "4d39bd7ab87afe9a",
        "name": "",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "01-//",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 1,
        "x": 610,
        "y": 100,
        "wires": [
            [
                "0e9d7eae1b870f33",
                "0923e25dcc8f66ee"
            ]
        ]
    },
    {
        "id": "b9b78ee5bd94ab4a",
        "type": "ui_switch",
        "z": "4d39bd7ab87afe9a",
        "name": "Off/On",
        "label": "Off/On",
        "tooltip": "",
        "group": "8681125fd492334c",
        "order": 1,
        "width": 0,
        "height": 0,
        "passthru": true,
        "decouple": "false",
        "topic": "control",
        "topicType": "str",
        "style": "",
        "onvalue": "open",
        "onvalueType": "str",
        "onicon": "",
        "oncolor": "",
        "offvalue": "close",
        "offvalueType": "str",
        "officon": "",
        "offcolor": "",
        "animate": false,
        "className": "",
        "x": 70,
        "y": 40,
        "wires": [
            [
                "e39e566a7f7e8e14"
            ]
        ]
    },
    {
        "id": "a53ee1e99199fb08",
        "type": "ui_numeric",
        "z": "4d39bd7ab87afe9a",
        "name": "",
        "label": "Dosing Time",
        "tooltip": "",
        "group": "8681125fd492334c",
        "order": 2,
        "width": 5,
        "height": 1,
        "wrap": false,
        "passthru": true,
        "topic": "topic",
        "topicType": "msg",
        "format": "{{value}}",
        "min": "0",
        "max": "260000",
        "step": "10",
        "className": "",
        "x": 90,
        "y": 140,
        "wires": [
            [
                "15ea8fa8f389eb9d"
            ]
        ]
    },
    {
        "id": "e39e566a7f7e8e14",
        "type": "gate",
        "z": "4d39bd7ab87afe9a",
        "name": "Pump Gate",
        "controlTopic": "control",
        "defaultState": "closed",
        "openCmd": "Open",
        "closeCmd": "Close",
        "toggleCmd": "toggle",
        "defaultCmd": "default",
        "statusCmd": "status",
        "persist": true,
        "storeName": "memory",
        "x": 310,
        "y": 80,
        "wires": [
            [
                "aca132168538208f"
            ]
        ]
    },
    {
        "id": "fe50969e5b62c435",
        "type": "chronos-scheduler",
        "z": "4d39bd7ab87afe9a",
        "name": "Pump 1 Schedule",
        "config": "dbcf10fd9c32cebf",
        "schedule": [
            {
                "trigger": {
                    "type": "time",
                    "value": "1:00",
                    "offset": 0,
                    "random": false
                },
                "output": {
                    "type": "msg",
                    "property": {
                        "name": "payload",
                        "type": "str",
                        "value": ""
                    }
                }
            },
            {
                "trigger": {
                    "type": "time",
                    "value": "9:00",
                    "offset": 0,
                    "random": false
                },
                "output": {
                    "type": "msg",
                    "property": {
                        "name": "payload",
                        "type": "str",
                        "value": ""
                    }
                }
            },
            {
                "trigger": {
                    "type": "time",
                    "value": "16:00",
                    "offset": 0,
                    "random": false
                },
                "output": {
                    "type": "msg",
                    "property": {
                        "name": "payload",
                        "type": "str",
                        "value": ""
                    }
                }
            }
        ],
        "disabled": false,
        "multiPort": false,
        "nextEventPort": false,
        "delayOnStart": true,
        "onStartDelay": "",
        "outputs": 1,
        "x": 110,
        "y": 80,
        "wires": [
            [
                "e39e566a7f7e8e14"
            ]
        ]
    },
    {
        "id": "8681125fd492334c",
        "type": "ui_group",
        "name": "Pump 1",
        "tab": "168c1349c26582fe",
        "order": 1,
        "disp": true,
        "width": "6",
        "collapse": false,
        "className": ""
    },
    {
        "id": "dbcf10fd9c32cebf",
        "type": "chronos-config",
        "name": "Ripon",
        "latitudeType": "num",
        "longitudeType": "num",
        "timezone": "",
        "sunPositions": []
    },
    {
        "id": "168c1349c26582fe",
        "type": "ui_tab",
        "name": "Scheduler",
        "icon": "dashboard",
        "order": 1,
        "disabled": false,
        "hidden": false
    }
]

It is not clear to me whether you still have a problem. If you do then what is it?

What I'm trying to figure out is what commands to send via a serial node that will turn on and off the relays. The below screenshots are in the documentation but getting this into a usable format that will enable or disable a relay is my challenge.

I have not used the serial port to send binary data, but at a guess something like this in a function node, sent to a serial out node should work to, for example, switch channel 2 on

msg.payload = Buffer.from([0x3a, 0x46, 0x45, ....., 0x0d, 0x0a])
return msg;

With the rest filled in obviously.