8 Channel USB Relay with MacHIDMBS

Here is something in python for that specific relay module.

[code]#!/usr/bin/python

import os
import sys
import time
import math

import numpy # sudo apt-get python-numpy

import usb.core # sudo apt-get python-pyusb
import usb.util # sudo apt-get python-pyusb

os.environ[“PYUSB_DEBUG”] = “debug”
os.environ[“PYUSB_LOG_FILENAME”] = “/home/pi/starkwolf/pyusb.log”

class HIDRelay:
“”“HID USB SainSmart 16 Relay Module using NUC122ZC1 - AN413AD22 - 31B033 - ARM”""

print_cfg = False

vid = 0x0416
pid = 0x5020

hid = None
ep_in = None  # From Device to Host Computer.
ep_in_address = 0x84 # bEndpointAddress -> 0x84  EP 4 IN
ep_out = None  # From Host Computer to Device.
ep_out_address = 0x05 # bEndpointAddress -> 0x05  EP 5 OUT

relayBitmap = None
relayBitmap16 = [128, 256, 64, 512, 32, 1024, 16, 2048, 8, 4096, 4, 8192, 2, 16384, 1, 32768]
relayBitmap8 = [128, 256, 64, 512, 32, 1024, 16, 2048, 8]

max_relay_id = None
max_relay_id16 = 15
max_relay_id8 = 7

packet_len = 64

"""Initiates de device. Ends the process if no device was found."""
def __init__(self, version=16):

    if (version is 8) or (version is "8"):
        self.relayBitmap = self.relayBitmap8
        self.max_relay_id = self.max_relay_id8
    else:
        self.relayBitmap = self.relayBitmap16
        self.max_relay_id = self.max_relay_id16

    init_ok = self.get_device()

    return None

"""Small function to pack array of bytes"""
def pack_bytes(self, bytesArray):
    packet = [0x0] * self.packet_len
    i = 0
    for byte in bytesArray:
        packet[i] = byte
        i += 1

    bytesString = ''.join([chr(c) for c in packet])
    return bytesString

"""Small function to unpack array of bytes"""
def unpack_bytes(self, bytesString):
    bytesArray = bytearray()
    bytesArray.extend(bytesString)

    return bytesArray

"""Gets the devices and sets the two endpoints."""
def get_device(self):

    # initialising debuging - don't have a clue if this will work
    self.hid = usb.core.find(idVendor=self.vid, idProduct=self.pid)

    # was it found?
    if self.hid is None:
        print 'Device not found'
        return False

    try:
        self.hid.detach_kernel_driver(0)
    except:  # this usually mean that kernel driver has already been dettached
        pass

    if self.print_cfg is True:
        for cfg in self.hid:
            sys.stdout.write('cfg.bConfigurationValue: ' + str(cfg.bConfigurationValue) + '\

‘)
for intf in cfg:
sys.stdout.write(’\t’ + \
'intf.bInterfaceNumber: ’ + str(intf.bInterfaceNumber) + \
‘,’ + \
‘intf.bAlternateSetting: ’ + str(intf.bAlternateSetting) + \

‘)
for ep in intf:
sys.stdout.write(’\t\t’ + \
'ep.bEndpointAddress: ’ + str(ep.bEndpointAddress) + \

')

    # access the second configuration
    cfg = self.hid[0]
    # access the first interface
    intf = cfg[(0,0)]
    # IN endpoint
    self.ep_in = intf[0]
    # OUT endpoint
    self.ep_out = intf[1]

    # Set the first configuration
    self.hid.set_configuration()

    return True

def read(self):

    readCmd = [0xD2, 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x48, 0x49, 0x44,
               0x43, 0x80, 0x02, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
               0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
               0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
               0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC]

    readCmd_pack = self.pack_bytes(readCmd)
    # assert len(self.hid.write(self.ep_out_address, readCmd_pack, 100)) == len(readCmd_pack)
    self.hid.write(self.ep_out_address, readCmd_pack, 100)

    data_pack = self.hid.read(self.ep_in_address, self.packet_len, 100)

    np_arr8 = numpy.uint8([data_pack[2], data_pack[3]])

    arr16 = np_arr8.view('uint16')

    mask = arr16[0]

    return mask

def reset(self):

    resetCmd = [0x71, 0x0E, 0x71, 0x00, 0x00, 0x00, 0x11, 0x11, 0x00, 0x00, 0x48, 0x49, 0x44,
                0x43, 0x2A, 0x02, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
                0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
                0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
                0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC]

    resetCmd_pack = self.pack_bytes(resetCmd)

    self.hid.write(self.ep_out_address, resetCmd_pack, 100)

def write(self, mask):
    if (type(mask) is not int) or (mask < 0) or (mask > 0xFFFF):
        raise ValueError('Invalid write mask: ', mask)

    writeCmd = [0xC3, 0x0E, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x49, 0x44,
                0x43, 0xEE, 0x01, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
                0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
                0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
                0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC];

    np_arr16 = numpy.uint16([mask])
    np_arr8 = np_arr16.view('uint8')

    writeCmd[2] = np_arr8[0]
    writeCmd[3] = np_arr8[1]

    size = writeCmd[1]

    writeCmd_sliced = writeCmd[0:size]
    checksum = reduce((lambda a, b: a + b), writeCmd_sliced)

    np_arr32 = numpy.uint32([checksum])
    arr8 = np_arr32.view('uint8')

    for x in range(0, 3):
        writeCmd[size + x] = arr8[x]

    writeCmd_pack = self.pack_bytes(writeCmd)
    self.hid.write(self.ep_out_address, writeCmd_pack, 100)

    return True

def set(self, relay_id, state):
    if type(relay_id) is not int:
        raise ValueError('Invalid relay ID type: ', relay_id)

    if (relay_id < 0) or (relay_id > self.max_relay_id):
        raise ValueError('Invalid relay ID: ', relay_id)

    if type(state) is not bool:
         raise ValueError('Invalid state: ', state)

    readmask = self.read()
    print "Readmask: ", readmask

    bit = int(math.pow(2, relay_id))

    mask = 0

    isOn = False

    for x in range(0, self.max_relay_id):
        if (readmask & self.relayBitmap[x]) != 0:
            currentRelayMask = int(math.pow(2, x))
            mask = mask | currentRelayMask
            if currentRelayMask == int(bit):
                isOn = True
            print "Led ", x, " is ON"

    print "Current Mask: ", mask

    if state is True:
        mask = mask | bit
    elif state is False:
        if isOn is True:
            mask = mask ^ bit


    print "Writing Mask:" , mask
    writemask = self.write(mask)
    print "Writemask: ", writemask

    new_readmask = self.read()

    print "New Readmask: ", new_readmask

    return writemask

def testRelay():
relay = HIDRelay(8)

relay.write(0)
relay.reset()
time.sleep(1)

relay.read()
time.sleep(0.25)
relay.write(0xFFFF)
time.sleep(0.25)
relay.write(0)
time.sleep(0.25)
relay.write(0xFFFF)
time.sleep(0.25)
relay.write(0)
p = relay.read()
time.sleep(0.5)

for x in range(0, relay.max_relay_id):
    p = relay.set(x, True)
    time.sleep(0.7)

for x in range(relay.max_relay_id, 0, -1):
    p = relay.set(x, False)
    time.sleep(0.7)

time.sleep(0.5)
relay.write(0xFFFF)
time.sleep(0.25)
relay.write(0)

def mapRelay():
relay = HIDRelay(8)

relayBitmap = []

# Turn off all relays
relay.write(0)
relay.read()
time.sleep(2)

for x in range(0, relay.max_relay_id):
    relay.write(int(math.pow(2, x)))
    readmask = relay.read()
    print "Readmask", readmask
    relayBitmap.insert(x, readmask)
    time.sleep(2)

# Clear all relays
relay.write(0)

print "Relay BitMap:", str(relayBitmap)

print ‘Number of arguments:’, len(sys.argv), ‘arguments.’
print ‘Argument List:’, str(sys.argv)
if (len(sys.argv) > 1):
relay = HIDRelay(8)
cliArgs = sys.argv
print ‘Command is:’, cliArgs[1]

if (cliArgs[1] == 'reset'):
    print 'Resetting...'
    relay.read()
    relay.write(0)
    relay.reset()

if (cliArgs[1] == 'test'):
    print 'Testing...'
    testRelay()

if (cliArgs[1] == 'map'):
    print 'Mapping...'
    mapRelay()

if (cliArgs[1] == 'on') and len(cliArgs) == 3:
    state = True
    relay_id = int(cliArgs[2])
    print "Turning Relay", relay_id, "On"
    relay.set(relay_id, state)

if (cliArgs[1] == 'off') and len(cliArgs) == 3:
    state = False
    relay_id = int(cliArgs[2])
    print "Turning Relay", relay_id, "Off"
    relay.set(relay_id, state)

if (cliArgs[1] == 'write') and len(cliArgs) == 3:
    mask = int(cliArgs[2])
    relay.write(mask)
    readmask = relay.read()
    print "Readmask: ", readmask

if (cliArgs[1] == 'read'):
    readmask = relay.read()
    print "Readmask: ", readmask[/code]

https://github.com/mvines/sainsmart-relay16

I wonder if it needs a lot more information than I’m sending it, it seemed like JC said it was working with the couple bytes he was sending.

Hmm, my last post seems to have disappeared.
I checked the Sainsmart site and discovered that the relay board does indeed use the FTDI chip. So, it does have a VCP interface. Looking at the datasheet, it also supports what appears to be a proprietary FTDI D2XX driver, which probably is HID. If so, you probably need to download that driver from FTDI. Here is a link to the D2XX driver programmer’s manual and the D2XX download page:
http://www.ftdichip.com/Support/Documents/ProgramGuides/D2XX_Programmer’s_Guide(FT_000071).pdf
http://www.ftdichip.com/Drivers/D2XX.htm

Still, it seems that it would be far easier to use the VCP interface so that you can just use serial commands. I don’t know why it doesn’t show up in your list of COM ports (assuming you’re using Windows).

I am using a mac and tried several things to try and get it to be recognized as a serial device. Also I did install the drivers on a windows box and it still only shows up as an HID device. I’m going to comb through that Programmers Guide next. Thank you.

That’s strange. I’ve used the FTDI chips, and never had a problem using them as a serial device on a Mac. I’ve encountered a bit of quirky behaviour on Windows machines (Windows assigns a new port number to it every time it’s plugged in, but doesn’t forget the old port number), but still managed to get them to work.

Here is some more code SainSmart sent me, doesn’t make a lot of sense to me.

[code]void CUSB_RelayFour_VCDlg::SendCmd(CString strHex,int len)
{
if (!bOpen)
{
this->MessageBox("«Î¥Úø™¥Æø?.");
return;
}
if(!m_Com.GetPortOpen())
OpenPort();
CByteArray bytSendCmd;
String2Hex(strHex ,bytSendCmd,len);
m_Com.SetOutput(COleVariant(bytSendCmd));
}

void CUSB_RelayFour_VCDlg::String2Hex(CString str, CByteArray &senddata,int len)
{
int i,highhexdata,lowhexdata;
senddata.SetSize(len);
for(i=0;i<len;i++)
{
highhexdata=Chr2Hex(str[i*3]);
lowhexdata=Chr2Hex(str[i*3+1]);
senddata[i]=(char)(highhexdata*16+lowhexdata);
}
}
//?ˆ¥Ì?µªÿ-1,’?»?0–F
char CUSB_RelayFour_VCDlg::Chr2Hex(char ch)
{
if((ch>=‘0’)&&(ch<=‘9’))
return ch-0x30;
else if((ch>=‘A’)&&(ch<=‘F’))
return ch-‘A’+10;
else if((ch>=‘a’)&&(ch<=‘f’))
return ch-‘a’+10;
else return (-1);
}

BEGIN_EVENTSINK_MAP(CUSB_RelayFour_VCDlg, CDialog)
//{{AFX_EVENTSINK_MAP(CUSB_RelayFour_VCDlg)
ON_EVENT(CUSB_RelayFour_VCDlg, IDC_COMMCTRL, 1 /* OnComm */, OnOnCommCommctrl, VTS_NONE)
//}}AFX_EVENTSINK_MAP
END_EVENTSINK_MAP()

void CUSB_RelayFour_VCDlg::OnOnCommCommctrl()
{
VARIANT variant_inp;
COleSafeArray safearray_inp;
LONG len,k;
BYTE readBuffer[16];
CString strtemp;
::Sleep(100);
if(m_Com.GetCommEvent()==2) // ¬º?÷µŒ™2±Ì æ?” ’ª??«¯ƒ?”–?÷??
{
variant_inp = m_Com.GetInput();
safearray_inp = variant_inp; //VARIANT–Õ±‰¡ø?™ªªŒ™ColeSafeArray–Õ±‰¡ø
len=safearray_inp.GetOneDimSize(); //µ?µ?”––ß ?æ›?§?»
CBitmap bmp;
CButton pButton;
for(k=0;k<len;k++)
safearray_inp.GetElement(&k,readBuffer+k);//?™ªªŒ™BYTE–Õ ??È
if (readBuffer[0] == 0xA3)
{
CString s;
int i;
CTime time=CTime::GetCurrentTime();
s=time.Format("%H:%M:%S")+" ?” ’:";
this->mTxtMsg.GetWindowText(this->m_Msg);
this->m_Msg=this->m_Msg+s;
if ((readBuffer[4] == 0x0D) && (readBuffer[5] == 0x0A))
{
for (i = 0; i < 6; i++)
{
s.Format ("%x%x",readBuffer[i] / 16,readBuffer[i] % 16);
s.MakeUpper ();
this->m_Msg=this->m_Msg+s+" “;
}
this->m_Msg=this->m_Msg+”\r
\r
";
this->mTxtMsg.SetWindowText(this->m_Msg);
switch ((int)readBuffer[2])
{
case 1:
if (readBuffer[3] == 0x00)
{
pButton=(CButton
)GetDlgItem(IDC_BUTTON_K1);
pButton-> SetWindowText( “K1±’?œ”);
bmp.LoadBitmap(IDB_BITMAP_OFF);
m_picK1.SetBitmap ((HBITMAP)bmp);
bmp.Detach();
}
else if (readBuffer[3] == 0xFF)
{
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K1);
pButton-> SetWindowText( “K1?œø™”);
bmp.LoadBitmap(IDB_BITMAP_ON);
m_picK1.SetBitmap ((HBITMAP)bmp);
bmp.Detach();
}
break;
case 2:
if (readBuffer[3] == 0x00)
{
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K2);
pButton-> SetWindowText( “K2±’?œ”);
bmp.LoadBitmap(IDB_BITMAP_OFF);
m_picK2.SetBitmap ((HBITMAP)bmp);
bmp.Detach();
}
else if (readBuffer[3] == 0xFF)
{
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K2);
pButton-> SetWindowText( “K2?œø™”);
bmp.LoadBitmap(IDB_BITMAP_ON);
m_picK2.SetBitmap ((HBITMAP)bmp);
bmp.Detach();
}
break;
case 3:
if (readBuffer[3] == 0x00)
{
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K3);
pButton-> SetWindowText( “K3±’?œ”);
bmp.LoadBitmap(IDB_BITMAP_OFF);
m_picK3.SetBitmap ((HBITMAP)bmp);
bmp.Detach();
}
else if (readBuffer[3] == 0xFF)
{
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K3);
pButton-> SetWindowText( “K3?œø™”);
bmp.LoadBitmap(IDB_BITMAP_ON);
m_picK3.SetBitmap ((HBITMAP)bmp);
bmp.Detach();
}
break;
case 4:
if (readBuffer[3] == 0x00)
{
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K4);
pButton-> SetWindowText( “K4±’?œ”);
bmp.LoadBitmap(IDB_BITMAP_OFF);
m_picK4.SetBitmap ((HBITMAP)bmp);
bmp.Detach();
}
else if (readBuffer[3] == 0xFF)
{
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K4);
pButton-> SetWindowText( “K4?œø™”);
bmp.LoadBitmap(IDB_BITMAP_ON);
m_picK4.SetBitmap ((HBITMAP)bmp);
bmp.Detach();
}
break;
default:
break;
}
}
else if ((readBuffer[6] == 0x0D) && (readBuffer[7] == 0x0A))
{
for (i = 0; i < 7; i++)
{
s.Format ("%x%x",readBuffer[i] / 16,readBuffer[i] % 16);
s.MakeUpper ();
this->m_Msg=this->m_Msg+s+" “;
}
this->m_Msg=this->m_Msg+”\r
\r
";
this->mTxtMsg.SetWindowText(this->m_Msg);
if (readBuffer[2] == 0x00)
{
CBitmap bmpTemp;
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K1);
pButton-> SetWindowText( “K1±’?œ”);
bmpTemp.LoadBitmap(IDB_BITMAP_OFF);
m_picK1.SetBitmap ((HBITMAP)bmpTemp);
bmpTemp.Detach();
}
else if (readBuffer[2] == 0xFF)
{
CBitmap bmpTemp;
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K1);
pButton-> SetWindowText( “K1?œø™”);
bmpTemp.LoadBitmap(IDB_BITMAP_ON);
m_picK1.SetBitmap ((HBITMAP)bmpTemp);
bmpTemp.Detach();
}
if (readBuffer[3] == 0x00)
{
CBitmap bmpTemp;
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K2);
pButton-> SetWindowText( “K2±’?œ”);
bmpTemp.LoadBitmap(IDB_BITMAP_OFF);
m_picK2.SetBitmap ((HBITMAP)bmpTemp);
bmpTemp.Detach();
}
else if (readBuffer[3] == 0xFF)
{
CBitmap bmpTemp;
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K2);
pButton-> SetWindowText( “K2?œø™”);
bmpTemp.LoadBitmap(IDB_BITMAP_ON);
m_picK2.SetBitmap ((HBITMAP)bmpTemp);
bmpTemp.Detach();
}
if (readBuffer[4] == 0x00)
{
CBitmap bmpTemp;
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K3);
pButton-> SetWindowText( “K3±’?œ”);
bmpTemp.LoadBitmap(IDB_BITMAP_OFF);
m_picK3.SetBitmap ((HBITMAP)bmpTemp);
bmpTemp.Detach();
}
else if (readBuffer[4] == 0xFF)
{
CBitmap bmpTemp;
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K3);
pButton-> SetWindowText( “K3?œø™”);
bmpTemp.LoadBitmap(IDB_BITMAP_ON);
m_picK3.SetBitmap ((HBITMAP)bmpTemp);
bmpTemp.Detach();
}
if (readBuffer[5] == 0x00)
{
CBitmap bmpTemp;
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K4);
pButton-> SetWindowText( “K4±’?œ”);
bmpTemp.LoadBitmap(IDB_BITMAP_OFF);
m_picK4.SetBitmap ((HBITMAP)bmpTemp);
bmpTemp.Detach();
}
else if (readBuffer[5] == 0xFF)
{
CBitmap bmpTemp;
pButton=(CButton*)GetDlgItem(IDC_BUTTON_K4);
pButton-> SetWindowText( “K4?œø™”);
bmpTemp.LoadBitmap(IDB_BITMAP_ON);
m_picK4.SetBitmap ((HBITMAP)bmpTemp);
bmpTemp.Detach();
}
}
else
{
}
}
this->UpdateData (FALSE);
Invalidate();
}
}
[/code]

I have extensive experience fighting with the stupidity that is FTDI chips.

A few things I can clear up:

a) An FTDI device WILL NOT SHOW UP at all as a serial device unless you have the correct FTDI driver (VCP for the mac) for the chip in question installed. On recent versions of OS X, this may or may not require you to enable disabled third party software in the Security tab of System Preferences.

b) Some (not all) FTDI chips also provide a separate interface that is accessed using the D2XX drivers. This is NOT HID. It is proprietary to FTDI, and IMHO is only worth considering on Windows. It also requires a specific driver for the specific FTDI chip. I strongly recommend you go the VCP route instead.

c) The examples you provided appear to be leveraging the VCP approach - i.e., they are talking to a serial port. The first example you provided:

serialPort1.Write(new byte[] { 0xFF, 0x01, 0x01 }, 0, 3);

Is a dead giveaway that this is using the VCP driver.

d) Manufacturers who use FTDI chips are supposed to customize the chips with their own Product ID (USB PID) prior to shipping them. However, this requires that they ALSO distribute their own versions of the FTDI-provided drivers that matches the VID and custom PID, as the generic FTDI drivers do not work with custom VID/PID combos, so the device will never show up as a serial port, even with the FTDI-supplied drivers installed.

Due to lots of problems based around all the above, we ditched FTDI last year and switched our hardware devices we manufacture to use a generic HID chip, that I’m communicating with using MonkeyBread.

I’d be happy to help you figure out exactly what is going on with this device. First step: What does your mac think this thing is?
Open System Profiler, click on the USB section, and expand all the sections at the top of the window. Do you see your device listed? Do you see another device listed named “USB to Serial Converter”?

The Mac sees it as an HID Device. Which I think is the approach I’d like to take.
In the USB Device Tree it reports as “HID Transfer”, I do not see “USB to Serial Converter”
In researching this, I did try to see if the Mac could recognize it as a serial device by disabling certain kexts and installing others but I could never get it to see it as a serial device, which I don’t think I want to do anyways.

Here is the ID information returned by the MBS test program:
1046 Nuvoton 20512 HID Transfer USB_0416_5020_fd132300 -1
This is the device I have: https://www.sainsmart.com/products/16-channel-usb-hid-programmable-control-relay-module

Although I haven’t gotten as far as having the relay respond, this code recognizes the device and returns that bytes have been written. I think I don’t know how to package data correctly or what the device expects to receive.

[code] dim MYHID as HIDAPIDeviceMBS = HIDAPIDeviceMBS.Open(1046,20512,"")
dim buf as new MemoryBlock(17)
dim r as integer

//buf.UInt8Value(0) = 0 // First byte is report number
//buf.UInt8Value(1) = 255
//buf.UInt8Value(2) = 1

buf.UInt8Value(0) = &h00
buf.UInt8Value(1) = &h00
buf.UInt8Value(2) = &h00
buf.UInt8Value(3) = &h0D
buf.UInt8Value(4) = &h0A
//buf.UInt8Value(5) = &h00
//buf.UInt8Value(6) = &h0D
//buf.UInt8Value(7) = &h0A

r = MYHID.Write(buf)
'r = MYHID.SendFeatureReport(buf)

MsgBox str®[/code]

Ok, I re-read the old conversation where someone else got it working. You are constructing your memory block differently than they did. Here is what they reported working:

[code]dim mb as New MemoryBlock(2)

'switching relays ON with 255
mb.byte(0)=255
mb.byte(1)=4 'relay number 1-8

'switching relays OFF with 253
mb.byte(0)=253
mb.byte(1)=4 'relay number 1-8

m.SendMessageMemory(mb,0,2)[/code]

Note that they are using a memoryblock of size 2, not 17 as you are. Also note that they are setting the first byte to the report ID - you are setting yours to null.

[code]‘use strict’;

class Relay16 {

constructor() {
const USB_VID = 0x0416;
const USB_PID = 0x5020;
const HID = require(‘node-hid’);

// Generated by mapRelays()
this.relayBitmap = [128, 256, 64, 512, 32, 1024, 16, 2048, 8, 4096, 4, 8192, 2, 16384, 1, 32768];

console.log('Detected devices:', HID.devices(USB_VID, USB_PID));
this.hid = new HID.HID(USB_VID, USB_PID);

}

close() {
this.hid.close();
}

set(id, state) {
if (typeof id !== ‘number’) {
throw new Error(Invalid relay ID type: ${typeof id});
}
if (id < 0 || id > 15) {
throw new Error(Invalid relay ID: ${id});
}
if (typeof state !== ‘boolean’) {
throw new Error(Invalid state: ${state});
}

return this.read()
.then((readmask) => {
  let bit = Math.pow(2, id);

  // Map the read mask into the write mask
  let mask = 0;
  for (let i = 0; i < 16; i++) {
    if (readmask & this.relayBitmap[i]) {
      mask = mask | Math.pow(2, i);
    }
  }

  if (state) {
    mask = mask | bit;
  } else {
    mask = mask ^ bit;
  }
  return this.write(mask);
});

}

read() {
return new Promise((resolve, reject) => {
let readCmd = [
0xD2, 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x48, 0x49, 0x44,
0x43, 0x80, 0x02, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
];
this.hid.write(readCmd);

  this.hid.read((err, data) => {
    if (err) {
      return reject('hid read error: ${err}');
    }

    let arr16 = new Uint16Array(1);
    let arr8 = new Uint8Array(arr16.buffer);
    arr8[0] = data[2];
    arr8[1] = data[3];

    let mask = arr16[0];
    return resolve(mask);
  });
});

}

reset() {
let resetCmd = [
0x71, 0x0E, 0x71, 0x00, 0x00, 0x00, 0x11, 0x11, 0x00, 0x00, 0x48, 0x49, 0x44,
0x43, 0x2A, 0x02, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
];
this.hid.write(resetCmd);
return Promise.resolve();
}

write(mask) {
if (typeof mask !== ‘number’ || mask < 0 || mask > 0xFFFF) {
throw new Error(Invalid write mask: ${mask});
}

let writeCmd = [
  0xC3, 0x0E, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x49, 0x44,
  0x43, 0xEE, 0x01, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
  0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
  0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
  0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
];

let arr16 = new Uint16Array(1);
let arr8 = new Uint8Array(arr16.buffer);
arr16[0] = mask;

writeCmd[2] = arr8[0];
writeCmd[3] = arr8[1];

const addCommandChecksum = (cmd) => {
  let size = cmd[1];
  let checksum = cmd.slice(0, size).reduce((a, b) => a + b, 0);

  let arr32 = new Uint32Array(1);
  let arr8 = new Uint8Array(arr32.buffer);
  arr32[0] = checksum;

  for (let i = 0; i < 4; i++) {
    cmd[size + i] = arr8[i];
  }
  return cmd;
};

this.hid.write(addCommandChecksum(writeCmd));
return Promise.resolve();

}
}

module.exports = Relay16;
[/code]

Hoo boy. I’ll do my best to help explain what is going on here, but keep in mind I’m not a nodeJS dev. I can read it well enough to be dangerous is all…

So, the section that you really care about is the write(mask) method. Here it is again, with a bunch of my own comments added to help clarify what it is doing:

[code]
//This method will receive a number named mask. I believe this is to select which action for the device to take, or which channel to take that action on. I’m not sure.
write(mask) {
//verify that the mask is in fact a number, and is in the range of 1 to 655535 (0x0001 to 0xFFFF)
if (typeof mask !== ‘number’ || mask < 0 || mask > 0xFFFF) {
throw new Error(Invalid write mask: ${mask});
}

//THIS THING IS IMPORTANT: This is an array of bytes (looks like 64 of them, but I didn’t count) that will be used as the basic report structure to send to the device. Note that the first byte is 0xC3 - this is likely the reportID. The rest of this method swaps out some of the bytes in this array.
let writeCmd = [
0xC3, 0x0E, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x49, 0x44,
0x43, 0xEE, 0x01, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
];
//So we’ll define a Uint16 array
let arr16 = new Uint16Array(1);
//Then put that array’s buffer into an array of Uint8’s. No idea why it is doing this, except that maybe it’s the only way to get to the individual bytes from the mask passed in?
let arr8 = new Uint8Array(arr16.buffer);
//Yeah, must be, because now we set the mask bytes into that Uint16 array, which means that we can get to the LSB and MSB (Least Significant Byte and Most Significant Byte) by accessing the arr8 (which is just peeking into the arr16)
arr16[0] = mask;

//Grab the byte from the mask at index 0 and put it into the writeCmd array at position 2 (note arrays are probably 0 based, so this would be the 3rd byte)
writeCmd[2] = arr8[0];
//put the byte from the mask at index 1 and put it into the writeCmd at index 3.
writeCmd[3] = arr8[1];

//OK - so far we’ve created what I believe is a generic report in writeCmd, then twiddled 2 of it’s bytes into new values, pulled from the mask provided to this method. Now we need to do some sort of checksum operation. This next bit declares a little addCommandChecksum function that will be used shortly. This function is declared in a way that is likely unfamiliar to you: I’m not sure of the exact term in NodeJS for this type of thing, but I’ve seen them called closures, inline functions, or similar. The idea here is that in the middle of the function called write, we will declare a variable called addCommandChecksum, but the value of that variable will not be something like a string, int, or float - it’ll be a function itself. A function within a function, if you will.

const addCommandChecksum = (cmd) => {
  let size = cmd[1];

//There is some js-specific syntax here that I’m not entirely familiar with. Look up reduce and slice if you absolutely must know what this is doing (and I think you do)
let checksum = cmd.slice(0, size).reduce((a, b) => a + b, 0);
//more bit twiddling on the computed checksum
let arr32 = new Uint32Array(1);
let arr8 = new Uint8Array(arr32.buffer);
arr32[0] = checksum;
//apparently there will be 4 bytes from the checksum - looks like it is tacking those bytes onto the very end of the original cmd that will be passed into this method.
for (let i = 0; i < 4; i++) {
cmd[size + i] = arr8[i];
}
//then it spits the passed-in array back out to the calling context.
return cmd;
};

//Ok, we are finished defining the checksum function, and are back to the main body of the write() function. Now we’ll take our writeCmd array that has 2 bits twiddled into it from the mask, pass it through the checksum method defined just above, then write the results out to the device.

this.hid.write(addCommandChecksum(writeCmd));
return Promise.resolve();

}[/code]

So: if I were in your shoes, I’d try to find a way to get at a few values during runtime: specifically, what are the bytes in the mask variable passed into this array? What does the writeCmd look like after the bytes from mask have been swapped into it? Finally, what does the checksum look like? (i.e., what does the FINAL writeCmd look like that is actually sent to the device?

Any chance you can get a debugger for node stuff? Or at the very least print or echo the values of variables at runtime?

Hope this is helpful. Good luck!

Thanks so much for that insight. I have it working now. I’ll post some finished working code.

I have this fully working in HIDAPI and MacHID, it was pretty easy after determining what the packet structure should look like.