Merge PR #26: feat(solarman): add Solarman V5 transport for Modbus RTU
This commit is contained in:
commit
6c6f071a80
4 files changed
+554
No files matched your search
@@ -0,0 +1,54 @@
|
||||
package home.gateway.adapters.solarman;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/** Modbus RTU frame: unit id, PDU, CRC16 with the low byte first. */
|
||||
final class ModbusRtuFrame {
|
||||
|
||||
private static final int CRC_LENGTH = 2;
|
||||
private static final int MIN_LENGTH = 1 + 1 + CRC_LENGTH;
|
||||
|
||||
private ModbusRtuFrame() {
|
||||
}
|
||||
|
||||
static byte[] build(int unitId, byte[] pdu) {
|
||||
byte[] frame = new byte[1 + pdu.length + CRC_LENGTH];
|
||||
frame[0] = (byte) unitId;
|
||||
System.arraycopy(pdu, 0, frame, 1, pdu.length);
|
||||
int crc = crc16(frame, frame.length - CRC_LENGTH);
|
||||
frame[frame.length - 2] = (byte) crc;
|
||||
frame[frame.length - 1] = (byte) (crc >> 8);
|
||||
return frame;
|
||||
}
|
||||
|
||||
static boolean hasValidCrc(byte[] frame) {
|
||||
if (frame.length < MIN_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
int crc = crc16(frame, frame.length - CRC_LENGTH);
|
||||
return frame[frame.length - 2] == (byte) crc && frame[frame.length - 1] == (byte) (crc >> 8);
|
||||
}
|
||||
|
||||
static byte[] parse(byte[] frame, int expectedUnitId) throws IOException {
|
||||
if (!hasValidCrc(frame)) {
|
||||
throw new IOException("Modbus RTU frame has a bad CRC or is too short");
|
||||
}
|
||||
if ((frame[0] & 0xFF) != expectedUnitId) {
|
||||
throw new IOException("Modbus RTU frame from unit " + (frame[0] & 0xFF) + ", expected " + expectedUnitId);
|
||||
}
|
||||
byte[] pdu = new byte[frame.length - 1 - CRC_LENGTH];
|
||||
System.arraycopy(frame, 1, pdu, 0, pdu.length);
|
||||
return pdu;
|
||||
}
|
||||
|
||||
static int crc16(byte[] data, int length) {
|
||||
int crc = 0xFFFF;
|
||||
for (int i = 0; i < length; i++) {
|
||||
crc ^= data[i] & 0xFF;
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
crc = (crc & 1) != 0 ? (crc >>> 1) ^ 0xA001 : crc >>> 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package home.gateway.adapters.solarman;
|
||||
|
||||
import home.gateway.adapters.modbus.ModbusTransport;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Modbus RTU carried in Solarman V5 frames over the Wi-Fi logger's TCP port (usually 8899).
|
||||
* One connection is kept and reused, one request at a time; after any error it is closed and
|
||||
* the next call reconnects. All V5 fields are little endian, the RTU frame inside is not.
|
||||
*/
|
||||
public final class SolarmanV5Transport implements ModbusTransport {
|
||||
|
||||
public record Settings(String host, int port, long loggerSerial, Duration connectTimeout, Duration readTimeout) {
|
||||
}
|
||||
|
||||
private static final int START = 0xA5;
|
||||
private static final int END = 0x15;
|
||||
private static final int REQUEST_CONTROL_CODE = 0x4510;
|
||||
private static final int RESPONSE_CONTROL_CODE = 0x1510;
|
||||
private static final int HEADER_LENGTH = 11;
|
||||
private static final int TRAILER_LENGTH = 2;
|
||||
private static final int REQUEST_PAYLOAD_PREFIX = 15;
|
||||
private static final int RESPONSE_PAYLOAD_PREFIX = 14;
|
||||
private static final int INVERTER_FRAME_TYPE = 0x02;
|
||||
private static final int DOUBLE_CRC_LENGTH = 2;
|
||||
|
||||
private final Settings settings;
|
||||
private Socket socket;
|
||||
private int sequence = (int) (Math.random() * 256);
|
||||
|
||||
public SolarmanV5Transport(Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized byte[] exchange(int unitId, byte[] requestPdu) throws IOException {
|
||||
try {
|
||||
Socket connection = connection();
|
||||
int requestSequence = nextSequence();
|
||||
OutputStream out = connection.getOutputStream();
|
||||
out.write(requestFrame(requestSequence, ModbusRtuFrame.build(unitId, requestPdu)));
|
||||
out.flush();
|
||||
byte[] rtu = readResponseRtu(new DataInputStream(connection.getInputStream()), requestSequence);
|
||||
return ModbusRtuFrame.parse(rtu, unitId);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
closeConnection();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
private Socket connection() throws IOException {
|
||||
if (socket == null) {
|
||||
Socket candidate = new Socket();
|
||||
try {
|
||||
candidate.connect(new InetSocketAddress(settings.host(), settings.port()),
|
||||
(int) settings.connectTimeout().toMillis());
|
||||
candidate.setSoTimeout((int) settings.readTimeout().toMillis());
|
||||
} catch (IOException e) {
|
||||
candidate.close();
|
||||
throw e;
|
||||
}
|
||||
socket = candidate;
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
private void closeConnection() {
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignored) {
|
||||
// already failing; the next call reconnects
|
||||
}
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Only the low byte is echoed by the logger; it counts the high byte itself.
|
||||
private int nextSequence() {
|
||||
sequence = (sequence + 1) & 0xFF;
|
||||
return sequence;
|
||||
}
|
||||
|
||||
private byte[] requestFrame(int requestSequence, byte[] rtu) {
|
||||
int payloadLength = REQUEST_PAYLOAD_PREFIX + rtu.length;
|
||||
ByteBuffer frame = ByteBuffer.allocate(HEADER_LENGTH + payloadLength + TRAILER_LENGTH)
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
frame.put((byte) START)
|
||||
.putShort((short) payloadLength)
|
||||
.putShort((short) REQUEST_CONTROL_CODE)
|
||||
.putShort((short) requestSequence)
|
||||
.putInt((int) settings.loggerSerial())
|
||||
.put((byte) INVERTER_FRAME_TYPE)
|
||||
.put(new byte[REQUEST_PAYLOAD_PREFIX - 1]) // sensor type and three timers, all zero
|
||||
.put(rtu);
|
||||
frame.put((byte) checksum(frame.array(), frame.position()));
|
||||
frame.put((byte) END);
|
||||
return frame.array();
|
||||
}
|
||||
|
||||
private byte[] readResponseRtu(DataInputStream in, int requestSequence) throws IOException {
|
||||
byte[] header = new byte[HEADER_LENGTH];
|
||||
in.readFully(header);
|
||||
ByteBuffer head = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
|
||||
int payloadLength = head.getShort(1) & 0xFFFF;
|
||||
if ((header[0] & 0xFF) != START) {
|
||||
throw new IOException("V5 response does not start with 0xA5");
|
||||
}
|
||||
if (payloadLength < RESPONSE_PAYLOAD_PREFIX) {
|
||||
throw new IOException("V5 response payload too short: " + payloadLength);
|
||||
}
|
||||
byte[] rest = new byte[payloadLength + TRAILER_LENGTH];
|
||||
in.readFully(rest);
|
||||
verify(head, header, rest, requestSequence);
|
||||
byte[] rtu = new byte[payloadLength - RESPONSE_PAYLOAD_PREFIX];
|
||||
System.arraycopy(rest, RESPONSE_PAYLOAD_PREFIX, rtu, 0, rtu.length);
|
||||
return withoutDoubleCrc(rtu);
|
||||
}
|
||||
|
||||
private void verify(ByteBuffer head, byte[] header, byte[] rest, int requestSequence) throws IOException {
|
||||
int controlCode = head.getShort(3) & 0xFFFF;
|
||||
if (controlCode != RESPONSE_CONTROL_CODE) {
|
||||
throw new IOException("Unexpected V5 control code 0x" + Integer.toHexString(controlCode));
|
||||
}
|
||||
if ((head.getShort(5) & 0xFF) != requestSequence) {
|
||||
throw new IOException("V5 response answers another request");
|
||||
}
|
||||
if (head.getInt(7) != (int) settings.loggerSerial()) {
|
||||
throw new IOException("V5 response from another logger");
|
||||
}
|
||||
byte[] checked = new byte[header.length + rest.length - TRAILER_LENGTH];
|
||||
System.arraycopy(header, 0, checked, 0, header.length);
|
||||
System.arraycopy(rest, 0, checked, header.length, rest.length - TRAILER_LENGTH);
|
||||
if ((rest[rest.length - 2] & 0xFF) != checksum(checked, checked.length)) {
|
||||
throw new IOException("V5 response has a bad checksum");
|
||||
}
|
||||
if ((rest[rest.length - 1] & 0xFF) != END) {
|
||||
throw new IOException("V5 response does not end with 0x15");
|
||||
}
|
||||
}
|
||||
|
||||
// Deye loggers append a second, always zero CRC. Check the trimmed frame first: a frame
|
||||
// followed by its own CRC also passes the CRC check, with the CRC taken as payload.
|
||||
private static byte[] withoutDoubleCrc(byte[] rtu) {
|
||||
if (rtu.length > DOUBLE_CRC_LENGTH) {
|
||||
byte[] trimmed = Arrays.copyOf(rtu, rtu.length - DOUBLE_CRC_LENGTH);
|
||||
if (ModbusRtuFrame.hasValidCrc(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return rtu;
|
||||
}
|
||||
|
||||
/** Sum of all bytes between the start byte and the checksum, modulo 256. */
|
||||
private static int checksum(byte[] frame, int end) {
|
||||
int sum = 0;
|
||||
for (int i = 1; i < end; i++) {
|
||||
sum += frame[i] & 0xFF;
|
||||
}
|
||||
return sum & 0xFF;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package home.gateway.adapters.solarman;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ModbusRtuFrameTest {
|
||||
|
||||
private static final byte[] READ_10_AT_0 = {0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, (byte) 0xC5, (byte) 0xCD};
|
||||
|
||||
@Test
|
||||
void buildsTheStandardExampleWithCrcLowByteFirst() {
|
||||
byte[] pdu = {0x03, 0x00, 0x00, 0x00, 0x0A};
|
||||
assertArrayEquals(READ_10_AT_0, ModbusRtuFrame.build(1, pdu));
|
||||
}
|
||||
|
||||
@Test
|
||||
void crcOfAKnownResponse() {
|
||||
byte[] frame = {0x01, 0x04, 0x02, (byte) 0xFF, (byte) 0xFF};
|
||||
assertEquals(0x80B8, ModbusRtuFrame.crc16(frame, frame.length));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesBackThePdu() throws IOException {
|
||||
assertArrayEquals(new byte[] {0x03, 0x00, 0x00, 0x00, 0x0A}, ModbusRtuFrame.parse(READ_10_AT_0, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBadCrc() {
|
||||
byte[] frame = READ_10_AT_0.clone();
|
||||
frame[7] ^= 1;
|
||||
assertThrows(IOException.class, () -> ModbusRtuFrame.parse(frame, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOtherUnit() {
|
||||
assertThrows(IOException.class, () -> ModbusRtuFrame.parse(READ_10_AT_0, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTooShortFrame() {
|
||||
assertThrows(IOException.class, () -> ModbusRtuFrame.parse(new byte[] {0x01, 0x03}, 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package home.gateway.adapters.solarman;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Function;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SolarmanV5TransportTest {
|
||||
|
||||
private static final long SERIAL = 3112107147L;
|
||||
private static final int UNIT = 1;
|
||||
private static final byte[] READ_REQUEST = {0x03, 0x00, 0x00, 0x00, 0x02};
|
||||
private static final byte[] READ_RESPONSE = {0x03, 0x04, 0x00, 0x2A, 0x01, (byte) 0xF4};
|
||||
|
||||
private FakeLogger logger;
|
||||
private SolarmanV5Transport transport;
|
||||
|
||||
@BeforeEach
|
||||
void start() throws IOException {
|
||||
logger = new FakeLogger();
|
||||
transport = new SolarmanV5Transport(new SolarmanV5Transport.Settings(
|
||||
"localhost", logger.port(), SERIAL, Duration.ofSeconds(2), Duration.ofMillis(500)));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void stop() throws IOException {
|
||||
transport.close();
|
||||
logger.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendsDocumentedRequestFrame() throws IOException {
|
||||
logger.respondWith(request -> response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE)));
|
||||
|
||||
transport.exchange(UNIT, READ_REQUEST);
|
||||
|
||||
byte[] frame = logger.requests.get(0);
|
||||
byte[] rtu = ModbusRtuFrame.build(UNIT, READ_REQUEST);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(frame).order(ByteOrder.LITTLE_ENDIAN);
|
||||
assertEquals(28 + rtu.length, frame.length);
|
||||
assertEquals((byte) 0xA5, frame[0]);
|
||||
assertEquals(15 + rtu.length, buffer.getShort(1));
|
||||
assertEquals(0x4510, buffer.getShort(3));
|
||||
assertEquals(SERIAL, buffer.getInt(7) & 0xFFFFFFFFL);
|
||||
assertEquals(0x02, frame[11]);
|
||||
for (int i = 12; i < 26; i++) {
|
||||
assertEquals(0, frame[i], "timer byte " + i);
|
||||
}
|
||||
assertArrayEquals(rtu, Arrays.copyOfRange(frame, 26, 26 + rtu.length));
|
||||
assertEquals(sum(frame, 1, frame.length - 2), frame[frame.length - 2] & 0xFF);
|
||||
assertEquals(0x15, frame[frame.length - 1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsTheResponsePdu() throws IOException {
|
||||
logger.respondWith(request -> response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE)));
|
||||
|
||||
assertArrayEquals(READ_RESPONSE, transport.exchange(UNIT, READ_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsTheDoubleCrcOfDeyeLoggers() throws IOException {
|
||||
byte[] rtu = ModbusRtuFrame.build(UNIT, READ_RESPONSE);
|
||||
byte[] doubled = Arrays.copyOf(rtu, rtu.length + 2);
|
||||
logger.respondWith(request -> response(request, doubled));
|
||||
|
||||
assertArrayEquals(READ_RESPONSE, transport.exchange(UNIT, READ_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reusesOneConnectionForSuccessiveRequests() throws IOException {
|
||||
logger.respondWith(request -> response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE)));
|
||||
|
||||
transport.exchange(UNIT, READ_REQUEST);
|
||||
transport.exchange(UNIT, READ_REQUEST);
|
||||
|
||||
assertEquals(1, logger.connections);
|
||||
assertEquals(2, logger.requests.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesIncreasingSequenceNumbers() throws IOException {
|
||||
logger.respondWith(request -> response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE)));
|
||||
|
||||
transport.exchange(UNIT, READ_REQUEST);
|
||||
transport.exchange(UNIT, READ_REQUEST);
|
||||
|
||||
int first = logger.requests.get(0)[5] & 0xFF;
|
||||
int second = logger.requests.get(1)[5] & 0xFF;
|
||||
assertEquals((first + 1) & 0xFF, second);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBadChecksumAndReconnects() throws IOException {
|
||||
logger.respondWith(request -> {
|
||||
byte[] bad = response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE));
|
||||
bad[bad.length - 2] ^= 1;
|
||||
return bad;
|
||||
});
|
||||
assertThrows(IOException.class, () -> transport.exchange(UNIT, READ_REQUEST));
|
||||
|
||||
logger.respondWith(request -> response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE)));
|
||||
assertArrayEquals(READ_RESPONSE, transport.exchange(UNIT, READ_REQUEST));
|
||||
assertEquals(2, logger.connections);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBadRtuCrc() {
|
||||
byte[] rtu = ModbusRtuFrame.build(UNIT, READ_RESPONSE);
|
||||
rtu[rtu.length - 1] ^= 1;
|
||||
logger.respondWith(request -> response(request, rtu));
|
||||
|
||||
assertThrows(IOException.class, () -> transport.exchange(UNIT, READ_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWrongControlCode() {
|
||||
logger.respondWith(request -> {
|
||||
byte[] frame = response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE));
|
||||
frame[4] = 0x17;
|
||||
return withFixedChecksum(frame);
|
||||
});
|
||||
|
||||
assertThrows(IOException.class, () -> transport.exchange(UNIT, READ_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsResponseToAnotherSequence() {
|
||||
logger.respondWith(request -> {
|
||||
request[5]++;
|
||||
return response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE));
|
||||
});
|
||||
|
||||
assertThrows(IOException.class, () -> transport.exchange(UNIT, READ_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsResponseFromAnotherUnit() {
|
||||
logger.respondWith(request -> response(request, ModbusRtuFrame.build(2, READ_RESPONSE)));
|
||||
|
||||
assertThrows(IOException.class, () -> transport.exchange(UNIT, READ_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconnectsAfterTheLoggerDropsTheConnection() throws IOException {
|
||||
logger.respondWith(request -> null);
|
||||
assertThrows(IOException.class, () -> transport.exchange(UNIT, READ_REQUEST));
|
||||
|
||||
logger.respondWith(request -> response(request, ModbusRtuFrame.build(UNIT, READ_RESPONSE)));
|
||||
assertArrayEquals(READ_RESPONSE, transport.exchange(UNIT, READ_REQUEST));
|
||||
assertEquals(2, logger.connections);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timesOutWhenTheLoggerStaysSilent() {
|
||||
logger.respondWith(request -> new byte[0]);
|
||||
|
||||
assertThrows(IOException.class, () -> transport.exchange(UNIT, READ_REQUEST));
|
||||
}
|
||||
|
||||
/** A V5 response as the logger sends it: echoes sequence and serial, status 1, three timers. */
|
||||
private static byte[] response(byte[] request, byte[] rtu) {
|
||||
ByteBuffer frame = ByteBuffer.allocate(11 + 14 + rtu.length + 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
frame.put((byte) 0xA5)
|
||||
.putShort((short) (14 + rtu.length))
|
||||
.putShort((short) 0x1510)
|
||||
.put(request, 5, 2)
|
||||
.put(request, 7, 4)
|
||||
.put((byte) 0x02)
|
||||
.put((byte) 0x01)
|
||||
.putInt(100)
|
||||
.putInt(200)
|
||||
.putInt(300)
|
||||
.put(rtu)
|
||||
.put((byte) 0)
|
||||
.put((byte) 0x15);
|
||||
return withFixedChecksum(frame.array());
|
||||
}
|
||||
|
||||
private static byte[] withFixedChecksum(byte[] frame) {
|
||||
frame[frame.length - 2] = (byte) sum(frame, 1, frame.length - 2);
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static int sum(byte[] frame, int from, int to) {
|
||||
int total = 0;
|
||||
for (int i = from; i < to; i++) {
|
||||
total += frame[i] & 0xFF;
|
||||
}
|
||||
return total & 0xFF;
|
||||
}
|
||||
|
||||
/** Local stand-in for the Wi-Fi logger. A null answer drops the connection, an empty one stays silent. */
|
||||
private static final class FakeLogger implements AutoCloseable {
|
||||
|
||||
final List<byte[]> requests = new CopyOnWriteArrayList<>();
|
||||
volatile int connections;
|
||||
private final ServerSocket server = new ServerSocket(0);
|
||||
private final List<Socket> accepted = new CopyOnWriteArrayList<>();
|
||||
private volatile Function<byte[], byte[]> responder = request -> null;
|
||||
|
||||
FakeLogger() throws IOException {
|
||||
Thread.ofVirtual().start(this::acceptLoop);
|
||||
}
|
||||
|
||||
int port() {
|
||||
return server.getLocalPort();
|
||||
}
|
||||
|
||||
void respondWith(Function<byte[], byte[]> responder) {
|
||||
this.responder = responder;
|
||||
}
|
||||
|
||||
private void acceptLoop() {
|
||||
try {
|
||||
while (true) {
|
||||
Socket socket = server.accept();
|
||||
accepted.add(socket);
|
||||
connections++;
|
||||
Thread.ofVirtual().start(() -> serve(socket));
|
||||
}
|
||||
} catch (IOException closed) {
|
||||
// server closed at the end of the test
|
||||
}
|
||||
}
|
||||
|
||||
private void serve(Socket socket) {
|
||||
try (socket) {
|
||||
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||
while (true) {
|
||||
byte[] request = readRequest(in);
|
||||
requests.add(request.clone());
|
||||
byte[] answer = responder.apply(request);
|
||||
if (answer == null) {
|
||||
return;
|
||||
}
|
||||
socket.getOutputStream().write(answer);
|
||||
}
|
||||
} catch (IOException closed) {
|
||||
// client went away
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readRequest(DataInputStream in) throws IOException {
|
||||
byte[] header = new byte[11];
|
||||
in.readFully(header);
|
||||
int payload = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN).getShort(1);
|
||||
byte[] frame = new byte[11 + payload + 2];
|
||||
System.arraycopy(header, 0, frame, 0, 11);
|
||||
in.readFully(frame, 11, payload + 2);
|
||||
return frame;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
server.close();
|
||||
for (Socket socket : new ArrayList<>(accepted)) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user