Skip to content

Pass the port along with the SerialPortEvent#86

Merged
tresf merged 3 commits intojava-native:masterfrom
Omnieboer:add-bytes-to-event
May 4, 2021
Merged

Pass the port along with the SerialPortEvent#86
tresf merged 3 commits intojava-native:masterfrom
Omnieboer:add-bytes-to-event

Conversation

@Omnieboer
Copy link

@Omnieboer Omnieboer commented Apr 30, 2021

SerialPortEvent now also saves the byte buffer. It does introduce the SerialPortException to a function that didn't have it before. I'm not sure how this should be handled, thus this is WIP.

Reason for passing along the bytes:
#85 (comment)
And from Slack (me):

So, I'm not sure I understand the code correctly, but I don't think it's great to call port.addEventListener(MyPortListener(port)) , the port given to the listener could be a different port than the port the listener is being assigned to. I'm currently working on a PR to have the bytes that trigger a SerialPortEvent given along with the event, so then in your own PortListener, you can do something like event.getBytes() to return a byte array of the latest message.

This makes the example implementation of the event listener in Java the following:

public static void main(String[] args) throws SerialPortException {
        SerialPort port = new SerialPort("COM10");
        port.openPort();
        port.setParams(BAUDRATE_9600, DATABITS_8, STOPBITS_1, PARITY_NONE);
        // port.setParams(9600, 8, 1, 0); // alternate technique
        int mask = SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR;
        port.setEventsMask(mask);
        port.addEventListener(new MyPortListener() /* defined below */);
}

class MyPortListener implements SerialPortEventListener {
    public void serialEvent(SerialPortEvent event) {
        if (event.isRXCHAR()) { // data is available
            // read data, if 10 bytes available
            if (event.getEventValue() == 10) {
                byte[] buffer = event.getBytes();
                System.out.println(Arrays.toString(buffer));
            }
        } else if (event.isCTS()) { // CTS line has changed state
            if (event.getEventValue() == 1) { // line is ON
                System.out.println("CTS - ON");
            } else {
                System.out.println("CTS - OFF");
            }
        } else if (event.isDSR()) { // DSR line has changed state
            if (event.getEventValue() == 1) { // line is ON
                System.out.println("DSR - ON");
            } else {
                System.out.println("DSR - OFF");
            }
        }
    }
}

This code works for me, tested with 0,01 second interval between messages of 10 bytes.

Closes #87

@tresf
Copy link

tresf commented Apr 30, 2021

I'm wondering if better encapsulation would actually be to have SerialPortEventListener a nested class within SerialPort so that it has direct access to it's own SerialPort.this instance. If so, we should retain the old namespace as deprecated (even as an extension to the nested class) as to not break backwards compatibility.

I don't vote for this option... this would only fix encapsulation though, not necessarily make it less clunky, I'll explain...

My experience with eventLIsteners is mostly limited to Swing and AWT, neither which stash references to the objects that require them For that reason, I find this statement from #87 to be a bit misleading:

passing port listener to port is clunky

Passing callee by reference isn't much different than, say, a JButton. In the case of AWT/Swing events, there's a way to get the caller, so that might be the closest we'd have.

I think what is perceived as "clunky" from an OO perspective is that internally, SerialPortEvent has reference to the portName (String), not SerialPort (Object).

private String portName;

However, I believe this design decision was intended by scr3mer to keep the CPP mapping back to Java as simple as possible. If a pointer to the parent object is attainable, I think we should prefer that, since ports and listeners are nearly always a 1:1 relationship (though, I think you can actually re-use a listener for multiple ports, we should keep that in mind!)

errorprone

I see no evidence of the API being errorprone in its current state. On the contrary, swallowing exceptions blindly seems to be error prone. 🍻 I realize you've commented "What do we do?" but I think the user should be able to handle the exceptions themselves, which can range from the port being closed, to a timeout occurringg (SerialPortException, SerialPortTimeoutException)

For example, readBytes() checks to see if the port is open and also provides an optional timeout value. We shouldn't assume people don't want this functionality simply because a listener is attached.

public byte[] readBytes(int byteCount, int timeout) throws SerialPortException, SerialPortTimeoutException {
checkPortOpened("readBytes()");
waitBytesWithTimeout("readBytes()", byteCount, timeout);
return readBytes(byteCount);
}

Lastly, and this is very important, a PR shouldn't ever break the API unless absolutely necessary without first deprecating it for a long while, it's just common courtesy. 🍻

Last, some implementations decided to only readBytes() when the isRXCHAR() is true, I think this use-case may still be desired, no?

@Omnieboer
Copy link
Author

Hmm, clunky and errorprone are definitely too harsh, but I didn't know how else to word it.

I think the problem I have with this, is the fact you call .addEventListener() on some instance of a SerialPort, but you can easily (but maybe only if you're an idiot, in which case it's your own fault) attach a PortListener implementation linked to another port:

port.addEventListener(new MyPortListener(otherPort) );

which gives no errors and even kind of works. The listener is called when port gets a message, but tries to read otherPort. Sometimes this gives me null, sometimes nothing and sometimes the message that was sent on otherPort.
I understand that this is an error on the users side, but it seems flaky.

Maybe for sure my "Solution" was too hasty and not at all complete, but I think it's a road worth exploring. Maybe looking at C-family ways of doing this.
In C# you can attach a listener that is a Method, which can exist in the same scope as the SerialPort, so you have access to all the port functionality, including readBytes() from the place you handle the fact you got a message.

All the while the PortName is given along with the event, but I don't think that really does anything for you.

I'll try to see about some more options over the weekend, but you raise very valid concerns. And it does work as it is now, so it might indeed not be worth to break API for a non-necessary quality of life change.

The PR was mostly a first step in my head, but I wasn't sure what to consider. All I knew was that there was a lot of it ;) So I'm grateful for you brining up

Lastly, and this is very important, a PR shouldn't ever break the API unless absolutely necessary without first deprecating it for a long while, it's just common courtesy. 🍻

Last, some implementations decided to only readBytes() when the isRXCHAR() is true, I think this use-case may still be desired, no?

Again, I'll see if I can dig a bit deeper in the C++ and C# implementations and the Java shell in this case, I'll get back to you.

@tresf
Copy link

tresf commented May 3, 2021

Sorry for the delay...

I took a longer look and came up with the following workable example that uses a lambda, doesn't need to keep track of the port. I think this is very close to what you're aiming for, but without removing the functionality of the readBytes(...).

 port.addEventListener(event -> {
    if(event.isRXCHAR()){ // data is available
        // read data, if 10 bytes available
        if(event.getEventValue() == 10){
            try {
                byte[] buffer = event.getPort().readBytes(10);
            //                        ^------ port can be referenced directly from within the lambda
            } catch (SerialPortException ex) {
                System.out.println(ex);
            }
        }
    }
    // ... 
}); 

To do this, you'll need to apply a diff something like this: https://gist.github.com/tresf/96ed6ce0cf37c4f227f01b342ff5ace9

I haven't tested this code yet, but what it does is attempt to preserve the existing class signatures for backwards compatibility while maintaining a reference to the SerialPort that triggered the event to begin with.

This should work because although a listener can listen upon multiple ports, the events will have the SerialPort object referenced, allowing readBytes(...) without retaining (or passing in) a special pointer.

Tomas Nieboer added 2 commits May 4, 2021 11:16
Makes usage of PortName in both cases deprecated. Removed internal usage of the deprecated code
@Omnieboer
Copy link
Author

The following now indeed works, thanks for the idea @tresf
I tested this quickly with messages with 0,01 sec delay between them.

I'll mark this PR ready for review.

class MyPortListener implements SerialPortEventListener {
    public void serialEvent(SerialPortEvent event) {
        if (event.isRXCHAR()) { // data is available
            // read data, if 10 bytes available
            if (event.getEventValue() == 10) {
                try {
                    byte[] buffer = event.getPort().readBytes();
                    System.out.println(Arrays.toString(buffer));
                } catch (SerialPortException ex) {
                    System.out.println(ex);
                }
            }
        } else if (event.isCTS()) { // CTS line has changed state
            if (event.getEventValue() == 1) { // line is ON
                System.out.println("CTS - ON");
            } else {
                System.out.println("CTS - OFF");
            }
        } else if (event.isDSR()) { // DSR line has changed state
            if (event.getEventValue() == 1) { // line is ON
                System.out.println("DSR - ON");
            } else {
                System.out.println("DSR - OFF");
            }
        }
    }
}

@Omnieboer Omnieboer marked this pull request as ready for review May 4, 2021 09:50
@tresf
Copy link

tresf commented May 4, 2021

Thanks. I think this is safe to merge. We can update the examples once the next release is finished.

@tresf tresf merged commit 8f6370a into java-native:master May 4, 2021
@Omnieboer Omnieboer changed the title WIP: Pass the bytes along with the SerialPortEventa Pass the port along with the SerialPortEvent May 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Passing port listener to port is clunky and errorprone

2 participants