diff --git a/src/main/java/gnu/io/RXTXCommDriver.java b/src/main/java/gnu/io/RXTXCommDriver.java index 9401bc17..04859b5c 100644 --- a/src/main/java/gnu/io/RXTXCommDriver.java +++ b/src/main/java/gnu/io/RXTXCommDriver.java @@ -655,6 +655,7 @@ else if ( osName.equals("Solaris") || osName.equals("SunOS")) "ttyUSB", // for USB frobs "ttyAMA", // Raspberry Pi "rfcomm", // bluetooth serial device + "hidraw", "ttyircomm", // linux IrCommdevices (IrDA serial emu) "ttyACM",// linux CDC ACM devices "DyIO",// NRDyIO @@ -677,6 +678,7 @@ else if(osName.equals("Linux-all-ports")) "holter", // custom card for heart monitoring "modem", // linux symbolic link to modem. "rfcomm", // bluetooth serial device + "hidraw", "ttyircomm", // linux IrCommdevices (IrDA serial emu) "ttycosa0c", // linux COSA/SRP synchronous serial card "ttycosa1c", // linux COSA/SRP synchronous serial card diff --git a/test/src/test/mpp/CRCUtil.java b/test/src/test/mpp/CRCUtil.java new file mode 100644 index 00000000..7b012a6a --- /dev/null +++ b/test/src/test/mpp/CRCUtil.java @@ -0,0 +1,90 @@ +package test.mpp; +// adapted from https://github.com/synogen/mpp/blob/master/src/main/java/org/mppsolartest/serial/CRCUtil.java + +public class CRCUtil { + private static final char[] crc_tb = new char[] { '\u0000', 'အ', '⁂', 'っ', '䂄', '傥', '惆', '烧', '脈', '鄩', 'ꅊ', '녫', + '소', '톭', '\ue1ce', '\uf1ef', 'ሱ', 'Ȑ', '㉳', '≒', '劵', '䊔', '狷', '拖', '錹', '茘', '덻', 'ꍚ', '펽', '쎜', + '\uf3ff', '\ue3de', '③', '㑃', 'Р', 'ᐁ', '擦', '瓇', '䒤', '咅', 'ꕪ', '땋', '蔨', '锉', '\ue5ee', '\uf5cf', '얬', + '햍', '㙓', '♲', 'ᘑ', 'ذ', '盗', '曶', '嚕', '䚴', '띛', 'ꝺ', '霙', '蜸', '\uf7df', '\ue7fe', '힝', '잼', '䣄', '壥', + '梆', '碧', 'ࡀ', 'ᡡ', '⠂', '㠣', '짌', '\ud9ed', '\ue98e', '羚', '襈', '饩', 'ꤊ', '뤫', '嫵', '䫔', '窷', '檖', 'ᩱ', + '\u0a50', '㨳', '⨒', '\udbfd', '쯜', '﮿', '\ueb9e', '魹', '識', '묻', '\uab1a', '沦', '粇', '䳤', '峅', 'Ⱒ', '㰃', + 'ౠ', '᱁', '\uedae', 'ﶏ', '췬', '\uddcd', '괪', '봋', '赨', '鵉', '纗', '溶', '廕', '仴', '㸓', '⸲', 'ṑ', '\u0e70', + '゚', '\uefbe', '\udfdd', '쿼', '뼛', '꼺', '齙', '轸', '醈', '膩', '뇊', 'ꇫ', '턌', '섭', '\uf14e', '\ue16f', 'ႀ', + '¡', 'ヂ', '⃣', '倄', '䀥', '灆', '恧', '莹', '鎘', 'ꏻ', '돚', '쌽', '팜', '\ue37f', '\uf35e', 'ʱ', 'ነ', '⋳', '㋒', + '䈵', '刔', '扷', '牖', '뗪', 'ꗋ', '閨', '薉', '\uf56e', '\ue54f', '픬', '씍', '㓢', 'Ⓝ', 'ᒠ', 'ҁ', '瑦', '摇', '吤', + '䐅', '\ua7db', '럺', '螙', '鞸', '\ue75f', '\uf77e', '윝', '휼', '⛓', '㛲', 'ڑ', 'ᚰ', '晗', '癶', '䘕', '嘴', + '\ud94c', '쥭', '癩', '\ue92f', '駈', '觩', '릊', 'ꦫ', '塄', '䡥', '砆', '栧', 'ᣀ', '࣡', '㢂', '⢣', '쭽', '\udb5c', + '\ueb3f', 'ﬞ', '诹', '鯘', 'ꮻ', '뮚', '䩵', '婔', '樷', '稖', '૱', '\u1ad0', '⪳', '㪒', 'ﴮ', '\ued0f', '\udd6c', + '쵍', '붪', '궋', '鷨', '跉', '簦', '氇', '層', '䱅', '㲢', 'ⲃ', '᳠', 'ು', '\uef1f', '^', '콝', '\udf7c', '꾛', '뾺', + '这', '鿸', '渗', '縶', '乕', '年', '⺓', '㺲', '໑', 'Ự' }; + + public static boolean checkCRC(String resultValue) { + if (resultValue.length() <= 2) + return false; + var firstValue = resultValue.substring(0, resultValue.length() - 2); + var lastValue = resultValue.substring(resultValue.length() - 2); + var pByte = firstValue.getBytes(); + var returnV = calculateCRC(pByte); + var lastV = toHexString(lastValue); + var reV = Integer.parseInt(lastV, 16); + return reV == returnV; + } + + public static byte[] getCRCByte(String command) { + var crcint = calculateCRC(command.getBytes()); + var crclow = crcint & 255; + var crchigh = crcint >> 8 & 255; + return new byte[] { (byte) crchigh, (byte) crclow }; + } + + private static String toHexString(String s) { + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < s.length(); ++i) { + var ch = (short) s.charAt(i); + if (ch < 0) + ch = (short) (ch + 256); + + var chString = Integer.toHexString(ch); + if (chString.length() < 2) + chString = "0" + chString; + + result.append(chString); + } + + return result.toString(); + } + + private static int calculateCRC(byte[] pByte) { + try { + int len = pByte.length; + int i = 0; + + int crc; + for (crc = 0; len-- != 0; ++i) { + int da = 255 & (255 & crc >> 8) >> 4; + crc <<= 4; + crc ^= crc_tb[255 & (da ^ pByte[i] >> 4)]; + da = 255 & (255 & crc >> 8) >> 4; + crc <<= 4; + int temp = 255 & (da ^ pByte[i] & 15); + crc ^= crc_tb[temp]; + } + + int bCRCLow = 255 & crc; + int bCRCHign = 255 & crc >> 8; + if (bCRCLow == 40 || bCRCLow == 13 || bCRCLow == 10) + ++bCRCLow; + + if (bCRCHign == 40 || bCRCHign == 13 || bCRCHign == 10) + ++bCRCHign; + + crc = (255 & bCRCHign) << 8; + crc += bCRCLow; + return crc; + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } +} diff --git a/test/src/test/mpp/MPP.java b/test/src/test/mpp/MPP.java new file mode 100644 index 00000000..7c262f40 --- /dev/null +++ b/test/src/test/mpp/MPP.java @@ -0,0 +1,71 @@ +package test.mpp; + +import java.io.IOException; + +import gnu.io.CommPortIdentifier; +import gnu.io.NoSuchPortException; +import gnu.io.PortInUseException; +import gnu.io.RXTXPort; + +public class MPP implements AutoCloseable { + private RXTXPort r; + private SerialHandler serialHandler; + + public MPP(final String port) throws NoSuchPortException, PortInUseException { + r = CommPortIdentifier.getPortIdentifier(port).open(MPP.class.getSimpleName(), 1000); // calls nativeavailable + try { + /* + * read(1) on hidraw devices does not work, so read(byte[]) must be done. But + * ioctl(…FIORDCHK, 0) also does not work, so to avoid calling RXTXPort.nativeavailable + * threshold must be set. It turns out that the received data arrives in batches of 8. + */ + r.enableReceiveThreshold(8); + } catch (Exception e) { + /* + * IOException: Invalid argument in TimeoutThreshold, likewise for below. The reason + * is that RXTXPort.NativeEnableReceiveTimeoutThreshold aborts when tcgetattr() fails. + * Why Excepiton and not IOException? Because the declaration in RXTXPort.java for + * native void NativeEnableReceiveTimeoutThreshold does not say it can throw an + * exception, so it throws only RuntimeExceptions in theory and IOException in practice. + */ + } + try { // useful when the cable is unplugged or device is off + r.enableReceiveTimeout(1000); + } catch (Exception e) {} // as above + serialHandler = new SerialHandler(r.getInputStream(), r.getOutputStream()); + } + + public static void main(String[] args) { + final String port = args.length == 1 ? args[0] : "/dev/hidraw0"; + + try (MPP d = new MPP(port)) { + System.out.println("Main CPU Firmware: " + d.command("QVFW")); + System.out.println("Another CPU Firmware: " + d.command("QVFW2")); + System.out.println("Device Protocol ID: " + d.command("QPI")); + System.out.println("Device Serial Number: " + d.command("QID")); + } catch (NoSuchPortException e) { + System.err.println("No such port " + port + " " + e); + } catch (PortInUseException e) { + System.err.println("Port in use " + e); + } + } + + public String command(String command) { + try { + return serialHandler.executeCommand(command); + } catch (IOException e) { + e.printStackTrace(); + return ""; + } + } + + @Override + public void close() { + final RXTXPort localR = r; + if (localR != null) { + localR.close(); + r = null; + } + serialHandler = null; + } +} diff --git a/test/src/test/mpp/README.md b/test/src/test/mpp/README.md new file mode 100644 index 00000000..2cc0b6d3 --- /dev/null +++ b/test/src/test/mpp/README.md @@ -0,0 +1,30 @@ +# Introduction + +This shows how to use a /dev/hidraw device in the example of idVendor=0665 (Cypress Semiconductor), idProduct=5161 (USB to Serial), bcdDevice= 0.02, USB device string Mfr=3, Product=1, SerialNumber=0. + +The device is embedded in inverters known as Axpert, Effekta, MPP, Voltronic, or even having no name. + +Communication with it can be done using a USB Type B cable, demonstrated herein. It supports also communication over LAN cable, bluetooth, RS232. + +# Caveats imposed by the Linux Kernel + +* reading one byte or writing one byte does not work. +* tcgetattr() and tcsetattr() also do not work. tcgetattr() sets errno to EINVAL, but should set it to ENOTTY, cf. https://lore.kernel.org/linux-input/24eaed9105633d03eded13e11c5a994bd93a81aa.camel@aegee.org/. +* ioctl(,FIONREAD,) also does not work. +* reading data arrives in batches of 8 bytes. + +# Implications for the implementation + +* Do not fetch one byte with RXTXPort.getInputStream().read(), do not use RXTXPort.getOutputStream.write(byte). +* Avoid RXTXPort.nativeavailable() by utilizing enableReceiveThreshold(8). It calls tcgetattr(), which throws an exception, but nevertheless sets the threshold. +* To make progress, when the cable is unplugged, set enableReceiveTimeout(1000). It again throws an exception when invoking tcgetattr(), but nevertheless sets the timeout for select(). + +# Building + +cd nrjavaserial/test/src +javac test/mpp/CRCUtil.java test/mpp/SerialHandler.java +java -classpath ../../build/libs/nrjavaserial-5.2.1.jar:. test/mpp/MPP.java + +To utilizie /dev/hidraw1, instead of the default /dev/hidraw0, use + +java -classpath ../../build/libs/nrjavaserial-5.2.1.jar:. test/mpp/MPP.java /dev/hidraw1 diff --git a/test/src/test/mpp/SerialHandler.java b/test/src/test/mpp/SerialHandler.java new file mode 100644 index 00000000..c3816f3e --- /dev/null +++ b/test/src/test/mpp/SerialHandler.java @@ -0,0 +1,67 @@ +package test.mpp; +// adapted from https://github.com/synogen/mpp/blob/master/src/main/java/org/mppsolartest/serial/SerialHandler.java + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class SerialHandler { + private InputStream input; + private OutputStream output; + private int errorCount = 0; + + public SerialHandler(InputStream input, OutputStream output) { + this.input = input; + this.output = output; + } + + public synchronized String executeCommand(String command) throws IOException { + var result = true; + try { + output.write(command.getBytes()); + var crc = CRCUtil.getCRCByte(command); + // on /dev/hidraw writing one byte fails + // write(8, "\r", 1) = -1 EINVAL (Invalid argument) + byte[] hack = new byte[crc.length + 1]; + System.arraycopy(crc, 0, hack, 0, crc.length); + hack[crc.length] = '\r'; + output.write(hack); + /* This could be useful, when using MPP over LAN cable, RS232 or Bluetooth + * try { // on hidraw devices ioctl(8, TCSBRK, 1) causes "Invalid argument in nativeDrain" + * output.flush(); + * } catch (IOException e) { } + */ + var timeout = System.currentTimeMillis() + 3000L; + var sb = new StringBuilder(); + var linebreak = false; + byte[] b = new byte[8]; + outerloop: + while (System.currentTimeMillis() < timeout) + if (input.read(b) > 0) { + for (int by : b) { + if (by == 13) { + linebreak = true; + break outerloop; + } + sb.append((char) by); + } + } + + if (!linebreak) + result = false; + var returnValue = sb.toString(); + return CRCUtil.checkCRC(returnValue) ? returnValue.substring(1, returnValue.length() - 2) : ""; + } catch (IOException e) { + result = false; + throw e; + } finally { + errorCount = result ? 0 : errorCount + 1; + if (errorCount >= 12) + System.err.println("[Serial] Communication failed " + Integer.toString(errorCount) + " times"); + } + } + + public int errorCount() { + return errorCount; + } +}