diff --git a/src/main/cpp/_nix_based/jssc.cpp b/src/main/cpp/_nix_based/jssc.cpp index 6887a0b6a..32eef8b59 100644 --- a/src/main/cpp/_nix_based/jssc.cpp +++ b/src/main/cpp/_nix_based/jssc.cpp @@ -40,19 +40,9 @@ #endif #ifdef __APPLE__ #include //Needed for IOSSIOSPEED in Mac OS X (Non standard baudrate) -#elif !defined(HAVE_POLL) - // Seems as poll has some portability issues on OsX (Search for "poll" in - // "https://cr.yp.to/docs/unixport.html"). So we only make use of poll on - // all platforms except "__APPLE__". - // If you want to force usage of 'poll', pass "-DHAVE_POLL=1" to gcc. - #define HAVE_POLL 1 #endif -#if HAVE_POLL == 0 - #include -#else - #include -#endif +#include #include #include @@ -527,97 +517,123 @@ JNIEXPORT jboolean JNICALL Java_jssc_SerialNativeInterface_setDTR */ JNIEXPORT jboolean JNICALL Java_jssc_SerialNativeInterface_writeBytes (JNIEnv *env, jobject, jlong portHandle, jbyteArray buffer){ - jbyte* jBuffer = env->GetByteArrayElements(buffer, JNI_FALSE); + jbyte* jBuffer = NULL; jint bufferSize = env->GetArrayLength(buffer); - jint result = write(portHandle, jBuffer, (size_t)bufferSize); - env->ReleaseByteArrayElements(buffer, jBuffer, 0); - return result == bufferSize ? JNI_TRUE : JNI_FALSE; -} + fd_set write_fd_set; + int byteRemains = bufferSize; + struct timeval timeout; + int result; + jclass threadClass = env->FindClass("java/lang/Thread"); + jmethodID areWeInterruptedMethod = env->GetStaticMethodID(threadClass, "interrupted", "()Z"); -/** - * Waits until 'read()' has something to tell for the specified filedescriptor. - */ -static void awaitReadReady(JNIEnv*, jlong fd){ -#if HAVE_POLL == 0 - // Alternative impl using 'select' as 'poll' isn't available (or broken). - - //assert(fd < FD_SETSIZE); // <- Might help when hunting SEGFAULTs. - fd_set readFds; - while(true) { - FD_ZERO(&readFds); - FD_SET(fd, &readFds); - int result = select(fd + 1, &readFds, NULL, NULL, NULL); - if(result < 0){ - // man select: On error, -1 is returned, and errno is set to indicate the error - // TODO: Maybe a candidate to raise a java exception. But won't do - // yet for backward compatibility. - continue; - } - // Did wait successfully. - break; + if (portHandle < 0 || portHandle > FD_SETSIZE) { + char msg[95]; + snprintf(msg, sizeof msg, "select(): file descriptor %ld outside expected range 0..%d", portHandle, FD_SETSIZE); + return env->ThrowNew(env->FindClass("java/lang/IndexOutOfBoundsException"), msg); } - FD_CLR(fd, &readFds); -#else - // Default impl using 'poll'. This is more robust against fd>=1024 (eg - // SEGFAULT problems). - - struct pollfd fds[1]; - fds[0].fd = fd; - fds[0].events = POLLIN; - while(true){ - int result = poll(fds, 1, -1); - if(result < 0){ - // man poll: On error, -1 is returned, and errno is set to indicate the error. - // TODO: Maybe a candidate to raise a java exception. But won't do - // yet for backward compatibility. + jBuffer = env->GetByteArrayElements(buffer, JNI_FALSE); + + while(byteRemains > 0) { + + // Check if the java thread has been interrupted, and if so, throw the exception + if (env->CallStaticBooleanMethod(threadClass, areWeInterruptedMethod)) { + jclass excClass = env->FindClass("java/lang/InterruptedException"); + env->ThrowNew(excClass, "Interrupted while writing to serial port"); + // It shouldn't matter what we return, the exception will be thrown right away + break; + } + + timeout.tv_sec = 0; + timeout.tv_usec = 100000; // 100 ms + FD_ZERO(&write_fd_set); + FD_SET(portHandle, &write_fd_set); + + result = select(portHandle + 1, NULL, &write_fd_set, NULL, &timeout); + if (result < 0) { + env->ThrowNew(env->FindClass("java/io/IOException"), "Waiting for serial port to become writable failed"); + // Return value is ignored anyway, exception is handled immidiatly + break; + } else if (result == 0) { + // timeout continue; } - // Did wait successfully. - break; + + result = write(portHandle, jBuffer + (bufferSize - byteRemains), byteRemains); + if(result < 0){ + env->ThrowNew(env->FindClass("java/io/IOException"), "Error writing to serial port"); + break; + } else if (result == 0) { + env->ThrowNew(env->FindClass("java/io/IOException"), "Serial port was closed unexpectedly"); + break; + } else { // result > 0 + byteRemains -= result; + } } - -#endif + env->ReleaseByteArrayElements(buffer, jBuffer, 0); + return JNI_TRUE; //result == bufferSize ? JNI_TRUE : JNI_FALSE; } -/* OK */ + /* * Reading data from the port - * - * Rewritten to use poll() instead of select() to handle fd>=1024 */ JNIEXPORT jbyteArray JNICALL Java_jssc_SerialNativeInterface_readBytes (JNIEnv *env, jobject, jlong portHandle, jint byteCount){ + fd_set read_fd_set; + jbyte *lpBuffer = NULL; + int byteRemains = byteCount; + struct timeval timeout; + int result; + jclass threadClass = env->FindClass("java/lang/Thread"); + jmethodID areWeInterruptedMethod = env->GetStaticMethodID(threadClass, "interrupted", "()Z"); - // TODO: Errors should be communicated by raising java exceptions; Will break - // backwards compatibility. + if (portHandle < 0 || portHandle > FD_SETSIZE) { + char msg[95]; + snprintf(msg, sizeof msg, "select(): file descriptor %ld outside expected range 0..%d", portHandle, FD_SETSIZE); + env->ThrowNew(env->FindClass("java/lang/IndexOutOfBoundsException"), msg); + return NULL; + } - jbyte *lpBuffer = new jbyte[byteCount]; - jbyteArray returnArray = NULL; - int byteRemains = byteCount; + lpBuffer = new jbyte[byteCount]; while(byteRemains > 0) { - int result = 0; - - awaitReadReady(env, portHandle); - errno = 0; - result = read(portHandle, lpBuffer + (byteCount - byteRemains), byteRemains); - if (result < 0) { - // man read: On error, -1 is returned, and errno is set to indicate the error. - // TODO: May candidate for raising a java exception. See comment at begin of function. + // Check if the java thread has been interrupted, and if so, throw the exception + if (env->CallStaticBooleanMethod(threadClass, areWeInterruptedMethod)) { + jclass excClass = env->FindClass("java/lang/InterruptedException"); + env->ThrowNew(excClass, "Interrupted while reading from serial port"); + // It shouldn't matter what we return, the exception will be thrown right away + break; } - else if (result == 0) { - // AFAIK this happens either on EOF or on EWOULDBLOCK (see 'man read'). - // TODO: Is "just continue" really the right thing to do? I will keep it that - // way because the old code did so and I don't know better. + + timeout.tv_sec = 0; + timeout.tv_usec = 100000; // 100 ms + FD_ZERO(&read_fd_set); + FD_SET(portHandle, &read_fd_set); + + result = select(portHandle + 1, &read_fd_set, NULL, NULL, &timeout); + if (result < 0) { + env->ThrowNew(env->FindClass("java/io/IOException"), "Error while waiting for input from serial port"); + // Return value is ignored anyway, exception is handled immidiatly + break; + } else if (result == 0) { + // timeout + continue; } - else { + + result = read(portHandle, lpBuffer + (byteCount - byteRemains), byteRemains); + if(result < 0){ + env->ThrowNew(env->FindClass("java/io/IOException"), "Error reading from serial port"); + break; + } else if (result == 0) { + env->ThrowNew(env->FindClass("java/io/IOException"), "Serial port was closed unexpectedly"); + break; + } else { // result > 0 byteRemains -= result; } } - - returnArray = env->NewByteArray(byteCount); + jbyteArray returnArray = env->NewByteArray(byteCount); env->SetByteArrayRegion(returnArray, 0, byteCount, lpBuffer); delete[] lpBuffer; return returnArray; diff --git a/src/main/cpp/jssc_SerialNativeInterface.h b/src/main/cpp/jssc_SerialNativeInterface.h index afde9b032..7bb4bef89 100644 --- a/src/main/cpp/jssc_SerialNativeInterface.h +++ b/src/main/cpp/jssc_SerialNativeInterface.h @@ -170,4 +170,5 @@ JNIEXPORT jboolean JNICALL Java_jssc_SerialNativeInterface_sendBreak #ifdef __cplusplus } #endif -#endif \ No newline at end of file +#endif + diff --git a/src/main/java/jssc/SerialPortStream.java b/src/main/java/jssc/SerialPortStream.java new file mode 100644 index 000000000..e64448e28 --- /dev/null +++ b/src/main/java/jssc/SerialPortStream.java @@ -0,0 +1,221 @@ +/* jSSC (Java Simple Serial Connector) - serial port communication library. + * © Alexey Sokolov (scream3r), 2010-2014. + * + * This file is part of jSSC. + * + * jSSC is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * jSSC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with jSSC. If not, see . + * + * If you use jSSC in public project you can inform me about this by e-mail, + * of course if you want it. + * + * e-mail: scream3r.org@gmail.com + * web-site: http://scream3r.org | http://code.google.com/p/java-simple-serial-connector/ + * + * SerialPortStream.java is written and contributed under the same license + * to the jSSC project by FactorIT B.V. in 2022 (factoritbv on github) + * + */ +package jssc; + +import java.net.*; +import java.util.*; +import java.util.concurrent.*; +import java.io.*; + +public class SerialPortStream { + + private SerialPort m_serial_port; + private SerialPortInputStream m_serial_port_input_stream; + private SerialPortOutputStream m_serial_port_output_stream; + private volatile boolean m_closed; + + private class SerialPortInputStream extends InputStream { + private byte[] m_buffer; + private int m_buffer_pos = 0; + private int m_buffer_len = 0; + + public SerialPortInputStream() { + m_buffer_len = 0; + m_buffer_pos = 0; + } + + public void close() throws IOException { + SerialPortInputStream.this.close(); + } + + public int read() throws IOException { + try { + // See if we run out of available bytes, and try to re-fill the buffer + if (m_buffer_pos >= m_buffer_len) { + m_buffer_len = m_serial_port.getInputBufferBytesCount(); + if (m_buffer_len == 0) { + // Nothing available, just block until the first byte comes available and return directly + return (int)m_serial_port.readBytes(1)[0]; + } + // Fetch the available bytes at once + m_buffer = m_serial_port.readBytes(m_buffer_len); + // Reset the position in the buffer + m_buffer_pos = 0; + } + return m_buffer[m_buffer_pos++]; + } catch (SerialPortException ex) { + throw new IOException(ex); + } + } + + public int available() throws IOException { + try { + return m_serial_port.getInputBufferBytesCount(); + } catch (SerialPortException ex) { + throw new IOException(ex); + } + } + } + + private class SerialPortOutputStream extends OutputStream { + + public void close() throws IOException { + SerialPortStream.this.close(); + } + + public void write(byte[] b) throws IOException { + try { + m_serial_port.writeBytes(b); + } catch (Exception ex) { + throw new IOException(ex); + } + } + + public void write(byte[] b, int off, int len) throws IOException { + byte[] buffer = Arrays.copyOfRange(b,off,off+len); + try { + m_serial_port.writeBytes(buffer); + } catch (Exception ex) { + throw new IOException(ex); + } + } + + public void write(int b) throws IOException { + try { + m_serial_port.writeBytes(new byte[]{(byte)b}); + } catch (Exception ex) { + throw new IOException(ex); + } + } + } + + public SerialPortStream(SerialPort serial_port, int baudrate) throws IOException { + super(); + m_serial_port = serial_port; + try { + // Open the serial port if it is still closed + if (!m_serial_port.isOpened()) { + if (!m_serial_port.openPort()) { + throw new IOException("Could not open serial port [" + m_serial_port.getPortName() + "]"); + } + // For the USB CDC class seems to be mandatory to set the line coding. + // So we use the most common values (9600 baud, 8 bits chars, 1 close bit, no parity) + if (!m_serial_port.setParams(baudrate,8,1,0)) { + throw new IOException("Could not set params for serial port [" + m_serial_port.getPortName() + "]"); + } + } + } catch (SerialPortException ex) { + // Redirect exeption to an IO exception + throw new IOException(ex); + } + m_serial_port_input_stream = new SerialPortInputStream(); + m_serial_port_output_stream = new SerialPortOutputStream(); + } + + public SerialPortStream(SerialPort serial_port) throws IOException { + this(serial_port,9600); + } + + public SerialPortStream(String serial_port_name, int baudrate) throws IOException { + this(new SerialPort(serial_port_name),baudrate); + } + + public SerialPortStream(String serial_port_name) throws IOException { + this(serial_port_name,9600); + } + + public SerialPort getSerialPort() { + return m_serial_port; + } + + public String getName() { + return m_serial_port.getPortName(); + } + + public InputStream getInputStream() { + return m_serial_port_input_stream; + } + + public OutputStream getOutputStream() { + return m_serial_port_output_stream; + } + + public synchronized void close() throws IOException { + // Immidiatly return when connection was already lost and cleaned up in the past + if (m_closed) return; + + // Update the connection status + m_closed = true; + + boolean failure = false; + + // Try to close and clean up the input stream + try { + m_serial_port_input_stream.close(); + } catch (IOException ex) { + failure = true; + } + + // Try to close and clean up the output stream + try { + m_serial_port_output_stream.close(); + } catch (IOException ex) { + failure = true; + } + + if (failure) { + throw new IOException("Error occured while closing and cleaning up the in/output streams"); + } + + // Only try to close the serial port if it is still open + if (m_serial_port.isOpened()) { + try { + m_serial_port.closePort(); + } catch (SerialPortException ex) { + throw new IOException(ex); + } + } + } + + public synchronized boolean isClosed() { + if (!m_closed) { + try { + m_serial_port_input_stream.available(); + } catch (IOException ex) { + try { + this.close(); + } catch (IOException ex2) { + // ignore + } + } + } + return m_closed; + } +} +