Skip to content

Commit 3827ba0

Browse files
committed
Add flow control tests (RTS/CTS and XON/XOFF).
1 parent 13876df commit 3827ba0

File tree

2 files changed

+280
-1
lines changed

2 files changed

+280
-1
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
package gnu.io;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.io.OutputStream;
11+
import java.util.logging.Logger;
12+
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.condition.DisabledOnOs;
15+
import org.junit.jupiter.api.condition.OS;
16+
import org.junit.jupiter.api.extension.RegisterExtension;
17+
18+
/**
19+
* Test the ability of the {@link SerialPort} implementation to mediate data
20+
* exchange with flow control.
21+
* <p>
22+
* This test is nonspecific as to <em>which</em> implementation; it exercises
23+
* only the public interface of the Java Communications API. Ports are opened
24+
* by the {@link SerialPortExtension} test extension, presumably by way of
25+
* {@link CommPortIdentifier}.
26+
*/
27+
public class SerialPortFlowControlTest
28+
{
29+
private static final Logger log = Logger.getLogger(SerialPortFlowControlTest.class.getName());
30+
31+
private static final String WROTE_WITHOUT_CTS = "Port A wrote data even though it wasn't clear to send";
32+
private static final String MISSING_CTS_WRITE = "Port A didn't write buffered data after port B asserted RTS";
33+
34+
private static final String ERRONEOUS_CTS = "Port A is still asserting RTS even though its input buffer should be full";
35+
private static final String FILLED_INPUT_BUFFER = "Filled the input buffer of port A with %d bytes.";
36+
37+
private static final String MISSING_INITIAL_WRITE = "Port A didn't write data before XOFF was sent";
38+
private static final String WROTE_WITH_XOFF = "Port A wrote data after XOFF was sent";
39+
private static final String MISSING_XON_WRITE = "Port A didn't write buffered data after being cleared to do so";
40+
41+
private static final String MISSING_XOFF = "Port A never sent XOFF even though its input buffer should be full";
42+
43+
/**
44+
* How long to wait (in milliseconds) for changes to control line states
45+
* on one port to affect the other port.
46+
*/
47+
private static final int STATE_WAIT = 50;
48+
/**
49+
* How long to wait (in milliseconds) for data sent from one port to arrive
50+
* at the other.
51+
*/
52+
private static final int TIMEOUT = 50;
53+
54+
/** The XON character for software flow control. */
55+
private static final byte XON = 0x11;
56+
/** The XOFF character for software flow control. */
57+
private static final byte XOFF = 0x13;
58+
59+
/**
60+
* The baud rate at which to run the flow control read tests.
61+
* <p>
62+
* Because those tests require filling the port input buffer, this should
63+
* be as fast as possible to minimize test runtime.
64+
*/
65+
private static final int READ_BAUD = 115_200;
66+
67+
/**
68+
* The size of the input buffer is unknown, and
69+
* {@link CommPort#getInputBufferSize()} does not purport to report it
70+
* accurately. To test port behaviour upon filling it, we'll try to send
71+
* this much data, and hope that we hit the limit.
72+
*/
73+
private static final int INPUT_BUFFER_MAX = 128 * 1024;
74+
/**
75+
* Write in chunks of this size when attempting to hit the input buffer
76+
* limit so that we can return early after hitting it.
77+
*/
78+
private static final int INPUT_BUFFER_CHUNK = 4 * 1024;
79+
80+
@RegisterExtension
81+
SerialPortExtension ports = new SerialPortExtension();
82+
83+
/**
84+
* Test that hardware flow control (aka RTS/CTS) correctly restricts
85+
* writing.
86+
* <p>
87+
* This test works by enabling hardware flow control on one port while
88+
* leaving it disabled on the other. The control lines of the second port
89+
* can then be manually toggled as necessary to verify flow control
90+
* behaviour on the first port.
91+
*
92+
* @throws UnsupportedCommOperationException if the flow control mode is
93+
* unsupported by the driver
94+
* @throws InterruptedException if the test is interrupted
95+
* while waiting for serial port
96+
* activity
97+
* @throws IOException if an error occurs while
98+
* writing to or reading from one
99+
* of the ports
100+
*/
101+
@Test
102+
void testHardwareFlowControlWrite() throws UnsupportedCommOperationException, InterruptedException, IOException
103+
{
104+
/* On Windows, RTS is off by default when opening the port. On other
105+
* platforms, it's on. We'll explicitly turn it off for consistency. */
106+
this.ports.b.setRTS(false);
107+
108+
this.ports.a.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
109+
110+
this.ports.b.enableReceiveTimeout(SerialPortFlowControlTest.TIMEOUT);
111+
112+
try (OutputStream out = this.ports.a.getOutputStream();
113+
InputStream in = this.ports.b.getInputStream())
114+
{
115+
/* Because we haven't enabled flow control for port B, port A should be
116+
* waiting to send. */
117+
assertFalse(this.ports.a.isCTS());
118+
119+
out.write(0x00);
120+
assertEquals(0, in.available(), SerialPortFlowControlTest.WROTE_WITHOUT_CTS);
121+
122+
this.ports.b.setRTS(true);
123+
Thread.sleep(SerialPortFlowControlTest.STATE_WAIT);
124+
125+
/* Port A should send once port B unblocks it. */
126+
assertTrue(this.ports.a.isCTS());
127+
assertNotEquals(-1, in.read(), SerialPortFlowControlTest.MISSING_CTS_WRITE);
128+
}
129+
}
130+
131+
/**
132+
* Test that hardware flow control (aka RTS/CTS) is correctly asserted when
133+
* receiving data.
134+
* <p>
135+
* This test works by enabling hardware flow control on one port while
136+
* leaving it disabled on the other. The flow control behaviour of the
137+
* first port can then be verified by observing its control lines from the
138+
* second port.
139+
*
140+
* @throws UnsupportedCommOperationException if the flow control mode is
141+
* unsupported by the driver
142+
* @throws IOException if an error occurs while
143+
* writing to or reading from one
144+
* of the ports
145+
*/
146+
@Test
147+
void testHardwareFlowControlRead() throws UnsupportedCommOperationException, IOException
148+
{
149+
this.ports.a.setSerialPortParams(
150+
SerialPortFlowControlTest.READ_BAUD,
151+
SerialPort.DATABITS_8,
152+
SerialPort.STOPBITS_1,
153+
SerialPort.PARITY_NONE);
154+
this.ports.b.setSerialPortParams(
155+
SerialPortFlowControlTest.READ_BAUD,
156+
SerialPort.DATABITS_8,
157+
SerialPort.STOPBITS_1,
158+
SerialPort.PARITY_NONE);
159+
this.ports.a.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
160+
161+
byte[] buffer = new byte[SerialPortFlowControlTest.INPUT_BUFFER_CHUNK];
162+
163+
try (OutputStream out = this.ports.b.getOutputStream())
164+
{
165+
assertTrue(this.ports.b.isCTS());
166+
167+
/* Port A should deassert RTS once its input buffer is full. How
168+
* big is its input buffer? `CommPort.getInputBufferSize()` can't
169+
* be trusted to tell us. We'll have to just keep blasting data at
170+
* it until it starts rejecting it. */
171+
int written;
172+
for (written = 0; written < SerialPortFlowControlTest.INPUT_BUFFER_MAX
173+
&& this.ports.b.isCTS(); written += buffer.length)
174+
{
175+
out.write(buffer);
176+
}
177+
178+
assertFalse(this.ports.b.isCTS(), SerialPortFlowControlTest.ERRONEOUS_CTS);
179+
log.info(String.format(SerialPortFlowControlTest.FILLED_INPUT_BUFFER, written));
180+
}
181+
}
182+
183+
/**
184+
* Test that software flow control (aka XON/XOFF) correctly restricts
185+
* writing.
186+
* <p>
187+
* This test works by enabling software flow control on one port while
188+
* leaving it disabled on the other. The control characters can then be
189+
* manually sent from the second port as necessary to verify flow control
190+
* behaviour on the first port.
191+
*
192+
* @throws UnsupportedCommOperationException if the flow control mode is
193+
* unsupported by the driver
194+
* @throws IOException if an error occurs while
195+
* writing to or reading from one
196+
* of the ports
197+
*/
198+
@Test
199+
void testSoftwareFlowControlWrite() throws UnsupportedCommOperationException, IOException
200+
{
201+
this.ports.a.setFlowControlMode(SerialPort.FLOWCONTROL_XONXOFF_IN | SerialPort.FLOWCONTROL_XONXOFF_OUT);
202+
203+
this.ports.b.enableReceiveTimeout(SerialPortFlowControlTest.TIMEOUT);
204+
205+
try (OutputStream outA = this.ports.a.getOutputStream();
206+
OutputStream outB = this.ports.a.getOutputStream();
207+
InputStream in = this.ports.b.getInputStream())
208+
{
209+
/* We should be able to write normally... */
210+
outA.write(0x00);
211+
assertNotEquals(-1, in.read(), SerialPortFlowControlTest.MISSING_INITIAL_WRITE);
212+
213+
/* ...until XOFF is sent from the receiver... */
214+
outB.write(SerialPortFlowControlTest.XOFF);
215+
outA.write(0x00);
216+
assertEquals(0, in.available(), SerialPortFlowControlTest.WROTE_WITH_XOFF);
217+
218+
/* ...and life should resume upon XON. */
219+
outB.write(SerialPortFlowControlTest.XON);
220+
assertNotEquals(-1, in.read(), SerialPortFlowControlTest.MISSING_XON_WRITE);
221+
}
222+
}
223+
224+
/**
225+
* Test that software flow control (aka XON/XOFF) control characters are
226+
* generated when receiving data.
227+
* <p>
228+
* This test works by enabling software flow control on one port while
229+
* leaving it disabled on the other. The generation of flow control
230+
* characters by first port can then be verified by reading from the second
231+
* port.
232+
* <p>
233+
* FIXME: On macOS (tested 10.15), I never received the XOFF even after
234+
* passing multiple megabytes of data.
235+
*
236+
* @throws UnsupportedCommOperationException if the flow control mode is
237+
* unsupported by the driver
238+
* @throws IOException if an error occurs while
239+
* writing to or reading from one
240+
* of the ports
241+
*/
242+
@Test
243+
@DisabledOnOs(OS.MAC)
244+
void testSoftwareFlowControlRead() throws UnsupportedCommOperationException, IOException
245+
{
246+
this.ports.a.setSerialPortParams(
247+
SerialPortFlowControlTest.READ_BAUD,
248+
SerialPort.DATABITS_8,
249+
SerialPort.STOPBITS_1,
250+
SerialPort.PARITY_NONE);
251+
this.ports.b.setSerialPortParams(
252+
SerialPortFlowControlTest.READ_BAUD,
253+
SerialPort.DATABITS_8,
254+
SerialPort.STOPBITS_1,
255+
SerialPort.PARITY_NONE);
256+
this.ports.a.setFlowControlMode(SerialPort.FLOWCONTROL_XONXOFF_IN | SerialPort.FLOWCONTROL_XONXOFF_OUT);
257+
258+
byte[] buffer = new byte[SerialPortFlowControlTest.INPUT_BUFFER_CHUNK];
259+
260+
try (OutputStream out = this.ports.b.getOutputStream();
261+
InputStream in = this.ports.b.getInputStream())
262+
{
263+
assertEquals(0, in.available());
264+
265+
/* Port A should send XOFF once its input buffer is full. See
266+
* `SerialPortFlowControlTest.testHardwareFlowControlRead()` for
267+
* details. */
268+
int written;
269+
for (written = 0; written < SerialPortFlowControlTest.INPUT_BUFFER_MAX
270+
&& in.available() == 0; written += buffer.length)
271+
{
272+
out.write(buffer);
273+
}
274+
275+
assertEquals(1, in.available(), SerialPortFlowControlTest.MISSING_XOFF);
276+
log.info(String.format(SerialPortFlowControlTest.FILLED_INPUT_BUFFER, written));
277+
}
278+
}
279+
}

src/test/java/gnu/io/SerialPortReadWriteTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ else if (elapsed > SerialPortReadWriteTest.LOW_SPEED_TRAP)
336336
*
337337
* @param buffer the buffer to fill
338338
*/
339-
private static void fillBuffer(byte[] buffer)
339+
static void fillBuffer(byte[] buffer)
340340
{
341341
for (int i = 0; i < buffer.length; ++i)
342342
{

0 commit comments

Comments
 (0)