chore: initial import of lingniu-vehicle-ingest
Multi-module Spring Boot ingest service for vehicle telemetry. Modules: - ingest-api / ingest-core / ingest-codec-common: shared SPI, dispatcher, Disruptor event bus, BCC/BCD codec helpers - protocol-gb32960: GB/T 32960.3 inbound (Netty + per-version parser packages v2016/v2025), platform login auth, VIN whitelist, idle handler - protocol-jt808 / protocol-jt1078 / protocol-jsatl12: JT/T inbound - inbound-mqtt / inbound-xinda-push: alternative ingest channels - session-core: per-channel session state - sink-archive / sink-mq: persistence sinks (local file / Kafka) - command-gateway: terminal control command gateway - bootstrap-all: aggregator Spring Boot app - observability: Micrometer / Actuator wiring Includes hex-dump golden samples under protocol-gb32960/src/test/resources and the GB/T 32960.3-2016 / 2025 reference PDFs under reference/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package com.lingniu.ingest.bootstrap;
|
||||
|
||||
import com.lingniu.ingest.api.event.VehicleEvent;
|
||||
import com.lingniu.ingest.api.sink.EventSink;
|
||||
import com.lingniu.ingest.codec.BccChecksum;
|
||||
import com.lingniu.ingest.protocol.gb32960.inbound.Gb32960NettyServer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 端到端集成测试:
|
||||
* <pre>
|
||||
* test client → TCP → Gb32960 Netty server → frame decoder → dispatcher
|
||||
* → InterceptorChain → Handler → DisruptorEventBus → TestEventSink
|
||||
* </pre>
|
||||
*
|
||||
* <p>关闭了所有非 gb32960 的协议与 Kafka sink,只测核心链路连通性。
|
||||
*/
|
||||
@SpringBootTest(
|
||||
classes = IngestApplication.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"lingniu.ingest.gb32960.enabled=true",
|
||||
"lingniu.ingest.gb32960.port=0",
|
||||
"lingniu.ingest.jt808.enabled=false",
|
||||
"lingniu.ingest.jt1078.enabled=false",
|
||||
"lingniu.ingest.jsatl12.enabled=false",
|
||||
"lingniu.ingest.mqtt.enabled=false",
|
||||
"lingniu.ingest.xinda-push.enabled=false",
|
||||
"lingniu.ingest.command-gateway.enabled=false",
|
||||
// 总开关关闭 Kafka sink:TestEventSink 成为唯一 Sink
|
||||
"lingniu.ingest.sink.mq.enabled=false",
|
||||
"lingniu.ingest.sink.archive.enabled=false",
|
||||
"spring.main.allow-bean-definition-overriding=true"
|
||||
})
|
||||
@Import(IngestEndToEndTest.TestConfig.class)
|
||||
class IngestEndToEndTest {
|
||||
|
||||
@Autowired
|
||||
private Gb32960NettyServer server;
|
||||
|
||||
@Autowired
|
||||
private TestEventSink testSink;
|
||||
|
||||
@Test
|
||||
void gb32960FrameReachesEventSink() throws Exception {
|
||||
int port = server.getBoundPort();
|
||||
assertThat(port).isGreaterThan(0);
|
||||
|
||||
byte[] frame = buildRealtimeFrame("LTEST0000000E2E01");
|
||||
try (Socket socket = new Socket("127.0.0.1", port)) {
|
||||
socket.getOutputStream().write(frame);
|
||||
socket.getOutputStream().flush();
|
||||
}
|
||||
|
||||
// DisruptorEventBus + Virtual threads have some jitter; poll up to 3s.
|
||||
VehicleEvent captured = null;
|
||||
long deadline = System.currentTimeMillis() + 3000;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
captured = testSink.queue.poll(100, TimeUnit.MILLISECONDS);
|
||||
if (captured instanceof VehicleEvent.Realtime) break;
|
||||
if (captured != null) continue;
|
||||
}
|
||||
assertThat(captured).isInstanceOf(VehicleEvent.Realtime.class);
|
||||
assertThat(captured.vin()).isEqualTo("LTEST0000000E2E01");
|
||||
}
|
||||
|
||||
// ===== test config =====
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
@Bean
|
||||
TestEventSink testEventSink() {
|
||||
return new TestEventSink();
|
||||
}
|
||||
}
|
||||
|
||||
/** 收集所有 publish 的事件。 */
|
||||
static final class TestEventSink implements EventSink {
|
||||
final BlockingQueue<VehicleEvent> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
@Override public String name() { return "test"; }
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> publish(VehicleEvent event) {
|
||||
queue.add(event);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> publishBatch(List<VehicleEvent> batch) {
|
||||
queue.addAll(batch);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== frame builder (inlined copy of Gb32960DecoderTest#buildRealtimeFrame) =====
|
||||
|
||||
private static byte[] buildRealtimeFrame(String vin) {
|
||||
ByteArrayOutputStream body = new ByteArrayOutputStream();
|
||||
body.write(24); body.write(1); body.write(2); body.write(3); body.write(4); body.write(5);
|
||||
|
||||
// 0x01 整车 20 字节
|
||||
body.write(0x01);
|
||||
body.write(0x01); body.write(0x03); body.write(0x01);
|
||||
writeU16(body, 523);
|
||||
writeU32(body, 1234567);
|
||||
writeU16(body, 6000);
|
||||
writeU16(body, 1100);
|
||||
body.write(70);
|
||||
body.write(0x01);
|
||||
body.write(0x0F);
|
||||
writeU16(body, 500);
|
||||
body.write(30);
|
||||
body.write(0);
|
||||
|
||||
// 0x05 位置 9 字节
|
||||
body.write(0x05);
|
||||
body.write(0x00);
|
||||
writeU32(body, 116_397_128L);
|
||||
writeU32(body, 39_916_527L);
|
||||
|
||||
byte[] bodyBytes = body.toByteArray();
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(0x23); out.write(0x23);
|
||||
out.write(0x02);
|
||||
out.write(0xFE);
|
||||
byte[] vinBytes = vin.getBytes(StandardCharsets.US_ASCII);
|
||||
out.write(vinBytes, 0, 17);
|
||||
out.write(0x01);
|
||||
writeU16(out, bodyBytes.length);
|
||||
out.write(bodyBytes, 0, bodyBytes.length);
|
||||
byte[] almost = out.toByteArray();
|
||||
byte bcc = BccChecksum.compute(almost, 2, almost.length - 2);
|
||||
out.write(bcc & 0xFF);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private static void writeU16(ByteArrayOutputStream os, int v) {
|
||||
os.write((v >> 8) & 0xFF);
|
||||
os.write(v & 0xFF);
|
||||
}
|
||||
|
||||
private static void writeU32(ByteArrayOutputStream os, long v) {
|
||||
os.write((int) ((v >> 24) & 0xFF));
|
||||
os.write((int) ((v >> 16) & 0xFF));
|
||||
os.write((int) ((v >> 8) & 0xFF));
|
||||
os.write((int) (v & 0xFF));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user