Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
697b68ec39 | ||
|
|
645bbc4b88 | ||
|
|
2d7899ebc8 | ||
|
|
c34a9da0d7 | ||
|
|
a8f6384e04 | ||
|
|
3c68dc3621 |
No files matched your search
+6
-8
@@ -27,9 +27,10 @@ their names and fields; new data goes into new measurements (ADR-005). The heat
|
||||
- [x] Config file outside the jar (D3), wire `Main`, drop modbus4j, influxdb-java and the
|
||||
baseline classes (D4, D5)
|
||||
- [x] Three parts with their images and compose (ADR-010); releases by tag, `update.sh` on the Pi
|
||||
- [ ] On the Pi, each step approved: D7, Docker, `gateway.json`, IIO overlay and reboot, stop
|
||||
`rpi-measurements` before the gateway polls the meter, first `./update.sh`, then retire both
|
||||
`rpi-measurements` and the old `home-pump.service`
|
||||
- [x] On the Pi (2026-10-24): D7, Docker (no iptables, no forwarding), `gateway.json`, ufw 8080
|
||||
on `wg0` only, `v1.1.1` running with the heat pump, Deye and Fronius; old service stopped
|
||||
- [ ] IIO overlay and reboot, then BME280; stop `rpi-measurements` before the gateway polls the
|
||||
meter, then retire it and the old `home-pump.service` files
|
||||
|
||||
## Next
|
||||
|
||||
@@ -51,11 +52,8 @@ their names and fields; new data goes into new measurements (ADR-005). The heat
|
||||
- D6 Development runs wrote test fields into the production `Dom_Pompa` database.
|
||||
- D8 Each field is written as its own point with its own timestamp, so one poll can split into
|
||||
points 1 ms apart (`InfluxMetricModbusSender`); write one point per poll.
|
||||
- D7 Pi network: all 880 DNS errors of the week came in one hour (19.10, 10:00), likely an outage,
|
||||
not config. But the Pi's WireGuard routes most of `192.168.0.0/24` into the tunnel (Kraków uses
|
||||
the same subnet), so it reaches only `.10` and `.150` locally. Fix: Pi `AllowedIPs` only
|
||||
`10.0.0.0/24, 10.2.0.0/24` (Influx and Gitea go via the public IP anyway), plus outbound NAT on
|
||||
OPNsense's WireGuard interface so the Pi answers Kraków through the tunnel.
|
||||
- D7 fixed 2026-10-24: Pi `AllowedIPs` only `10.0.0.0/24, 10.2.0.0/24`, and OPNsense NATs
|
||||
`192.168.0.0/24 → 10.0.0.2` to its tunnel address, as both sites use `192.168.0.0/24`.
|
||||
|
||||
## Knowledge
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#0e2a21">
|
||||
<title>Dom Krosno</title>
|
||||
<title>Dom Szczepańcowa</title>
|
||||
<link rel="icon" href="data:,">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -60,7 +60,7 @@ function SideFooter() {
|
||||
<Icon name="sun" className="size-9 text-amber-400" />
|
||||
<div>
|
||||
<p className="text-2xl font-semibold">{celsius(data?.temperatures.outdoor)}</p>
|
||||
<p className="text-sm text-white/60">Krosno</p>
|
||||
<p className="text-sm text-white/60">Szczepańcowa</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-white/80">{longDate(now)} · {hour(now)}</p>
|
||||
@@ -97,7 +97,7 @@ export function App() {
|
||||
<div className="mb-8 hidden items-center gap-3 px-2 lg:flex">
|
||||
<span className="grid size-10 place-items-center rounded-xl bg-brand/20 text-brand"><Icon name="leaf" className="size-6" /></span>
|
||||
<div>
|
||||
<p className="text-lg font-bold leading-tight">Dom Krosno</p>
|
||||
<p className="text-lg font-bold leading-tight">Dom Szczepańcowa</p>
|
||||
<p className="text-xs text-white/60">Pompa ciepła i energia</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,16 @@ import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Modbus TCP (MBAP) over one long-lived socket. The MK01 gateway serves a single client and a
|
||||
* second connection drops the first, so the socket is reused and requests never overlap.
|
||||
* Modbus TCP (MBAP). The MK01 gateway serves a single client and a second connection drops the
|
||||
* first, so requests never overlap and the socket is reused for the requests of one poll. It is
|
||||
* closed after a few idle seconds: MK01 resets a connection left idle for minutes, which cost every
|
||||
* other poll, and while one is open the vendor's app cannot connect.
|
||||
*/
|
||||
public final class TcpTransport implements ModbusTransport {
|
||||
|
||||
@@ -20,17 +25,26 @@ public final class TcpTransport implements ModbusTransport {
|
||||
}
|
||||
|
||||
private static final int HEADER_LENGTH = 7;
|
||||
static final Duration IDLE_CLOSE = Duration.ofSeconds(5);
|
||||
|
||||
// Not synchronized: a virtual thread blocked in a monitor pins its carrier thread.
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final Settings settings;
|
||||
private final Duration idleClose;
|
||||
private final ScheduledExecutorService idleCloser = Executors.newSingleThreadScheduledExecutor(
|
||||
Thread.ofVirtual().name("modbus-idle-close").factory());
|
||||
private Socket socket;
|
||||
private DataInputStream in;
|
||||
private int transactionId;
|
||||
private long lastResponseNanos;
|
||||
|
||||
public TcpTransport(Settings settings) {
|
||||
this(settings, IDLE_CLOSE);
|
||||
}
|
||||
|
||||
TcpTransport(Settings settings, Duration idleClose) {
|
||||
this.settings = settings;
|
||||
this.idleClose = idleClose;
|
||||
this.lastResponseNanos = System.nanoTime() - settings.minimumPause().toNanos();
|
||||
}
|
||||
|
||||
@@ -52,6 +66,9 @@ public final class TcpTransport implements ModbusTransport {
|
||||
socket.getOutputStream().write(frame(id, unitId, requestPdu));
|
||||
byte[] response = readResponse(id, unitId);
|
||||
lastResponseNanos = System.nanoTime();
|
||||
if (!idleCloser.isShutdown()) {
|
||||
idleCloser.schedule(this::closeIfIdle, idleClose.toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
return response;
|
||||
} catch (IOException e) {
|
||||
closeSocket();
|
||||
@@ -63,12 +80,25 @@ public final class TcpTransport implements ModbusTransport {
|
||||
public void close() {
|
||||
lock.lock();
|
||||
try {
|
||||
idleCloser.shutdownNow();
|
||||
closeSocket();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** A later request in the same poll moved lastResponseNanos on, and the socket stays. */
|
||||
private void closeIfIdle() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (System.nanoTime() - lastResponseNanos >= idleClose.toNanos()) {
|
||||
closeSocket();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitPause() throws InterruptedIOException {
|
||||
long remaining = settings.minimumPause().toNanos() - (System.nanoTime() - lastResponseNanos);
|
||||
if (remaining <= 0) {
|
||||
|
||||
@@ -21,6 +21,7 @@ public final class MockModbusServer implements Closeable {
|
||||
private final Map<Integer, Integer> holding = new ConcurrentHashMap<>();
|
||||
private final Map<Integer, Integer> input = new ConcurrentHashMap<>();
|
||||
private final AtomicInteger connections = new AtomicInteger();
|
||||
private final AtomicInteger open = new AtomicInteger();
|
||||
private final List<int[]> writes = new CopyOnWriteArrayList<>();
|
||||
private volatile Duration responseDelay = Duration.ZERO;
|
||||
private volatile boolean dropNextRequest;
|
||||
@@ -42,6 +43,11 @@ public final class MockModbusServer implements Closeable {
|
||||
return connections.get();
|
||||
}
|
||||
|
||||
/** Connections the client has not closed yet. */
|
||||
public int openConnections() {
|
||||
return open.get();
|
||||
}
|
||||
|
||||
public void holding(int address, int value) {
|
||||
holding.put(address, value);
|
||||
}
|
||||
@@ -94,6 +100,7 @@ public final class MockModbusServer implements Closeable {
|
||||
try {
|
||||
Socket client = server.accept();
|
||||
connections.incrementAndGet();
|
||||
open.incrementAndGet();
|
||||
Thread.ofVirtual().start(() -> serve(client));
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
@@ -128,6 +135,8 @@ public final class MockModbusServer implements Closeable {
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// the client went away, which is a normal end of a test
|
||||
} finally {
|
||||
open.decrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,28 @@ class TcpTransportTest {
|
||||
assertEquals(1, server.connections());
|
||||
}
|
||||
|
||||
@Test
|
||||
void closesTheConnectionWhenIdleAndReconnectsForTheNextPoll() throws Exception {
|
||||
transport.close();
|
||||
transport = new TcpTransport(new TcpTransport.Settings(
|
||||
"localhost", server.port(), Duration.ofSeconds(2), Duration.ofSeconds(2), Duration.ZERO),
|
||||
Duration.ofMillis(100));
|
||||
|
||||
transport.exchange(1, READ_HOLDING_10);
|
||||
transport.exchange(1, READ_HOLDING_10);
|
||||
assertEquals(1, server.openConnections());
|
||||
|
||||
// Idle: the controller is free for the vendor's app, and no socket goes stale.
|
||||
long deadline = System.nanoTime() + Duration.ofSeconds(3).toNanos();
|
||||
while (server.openConnections() > 0 && System.nanoTime() < deadline) {
|
||||
Thread.sleep(20);
|
||||
}
|
||||
assertEquals(0, server.openConnections());
|
||||
|
||||
assertArrayEquals(HOLDING_10_RESPONSE, transport.exchange(1, READ_HOLDING_10));
|
||||
assertEquals(2, server.connections());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconnectsAfterServerDropsConnection() throws IOException {
|
||||
transport.exchange(1, READ_HOLDING_10);
|
||||
|
||||
Reference in new issue
Block a user