Skip to content

Commit 59a72af

Browse files
committed
Add flow control tests (RTS/CTS and XON/XOFF).
1 parent 18b3999 commit 59a72af

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

0 commit comments

Comments
 (0)