docs: add detailed 32960 pipeline comments

This commit is contained in:
kkfluous
2026-06-23 13:17:37 +08:00
parent ba68ffe061
commit a096e4ce0e
125 changed files with 493 additions and 14 deletions

View File

@@ -47,6 +47,7 @@ public final class Jt808MessageDecoder implements FrameDecoder<Jt808Message> {
int bodyLength = attrs & 0x03FF;
int encryptType = (attrs >> 10) & 0x07;
boolean subpacket = (attrs & 0x2000) != 0;
// JT/T 808-2019 通过属性 bit14 标识,终端手机号从 6B BCD 扩展到 10B BCD。
boolean v2019 = (attrs & 0x4000) != 0;
Jt808Header.ProtocolVersion version = v2019
@@ -64,6 +65,7 @@ public final class Jt808MessageDecoder implements FrameDecoder<Jt808Message> {
int totalPkgs = 0, pkgSeq = 0;
int bodyStart = headerLen;
if (subpacket) {
// 分包头紧跟消息流水号之后;当前只暴露包总数/序号,重组由上游或后续模块处理。
totalPkgs = ((bytes[bodyStart] & 0xFF) << 8) | (bytes[bodyStart + 1] & 0xFF);
pkgSeq = ((bytes[bodyStart + 2] & 0xFF) << 8) | (bytes[bodyStart + 3] & 0xFF);
bodyStart += 4;
@@ -82,6 +84,7 @@ public final class Jt808MessageDecoder implements FrameDecoder<Jt808Message> {
BodyParser parser = registry.find(messageId);
Jt808Body body;
if (parser == null) {
// 未实现的消息体保持 Raw仍可进入 archive/event-history避免协议扩展导致数据丢失。
byte[] raw = new byte[bodyLength];
bodyBuf.get(raw);
body = new Jt808Body.Raw(messageId, raw);

View File

@@ -4,8 +4,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "lingniu.ingest.jt808")
public class Jt808Properties {
/** 是否启动 JT/T 808 TCP 监听。与 GB32960 独立,通常用于部标终端。 */
private boolean enabled = false;
/** JT808 默认监听端口;不要与 32960 端口共用。 */
private int port = 10808;
/** Netty worker 线程数0 表示使用框架默认值。 */
private int workerThreads = 0;
public boolean isEnabled() { return enabled; }

View File

@@ -67,6 +67,7 @@ public final class Jt808CommandDispatcher implements CommandDispatcher {
CompletableFuture<Jt808Message> pending = pendingRequests.await(ctx.phone, ctx.serial);
writeFrames(ctx, true).whenComplete((unused, ex) -> {
if (ex != null) {
// 写 channel 失败时必须移除 pending否则后续同 phone/serial 重用会匹配到旧 future。
pendingRequests.cancel(ctx.phone, ctx.serial);
pending.completeExceptionally(ex);
}
@@ -74,6 +75,7 @@ public final class Jt808CommandDispatcher implements CommandDispatcher {
return pending.orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
.whenComplete((r, ex) -> {
// 超时/取消同样清理 pending避免长期运行后内存泄漏。
if (ex != null) pendingRequests.cancel(ctx.phone, ctx.serial);
})
.thenApply(msg -> {
@@ -122,6 +124,7 @@ public final class Jt808CommandDispatcher implements CommandDispatcher {
Optional<DeviceSession> s = sessions.findBySessionId(sessionId)
.or(() -> sessions.findByPhone(sessionId))
.or(() -> sessions.findByVin(sessionId));
// HTTP 调用方可以传 sessionId/VIN/phone最终写 Netty Channel 必须落到 JT808 phone。
return s.map(DeviceSession::phone)
.filter(p -> p != null && !p.isBlank())
.orElse(sessionId); // 回落:直接把 sessionId 当 phone

View File

@@ -105,6 +105,8 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
try {
processDecodedFrame(ctx, frame, msg);
} catch (RuntimeException e) {
// 解码成功但业务处理失败时仍派发一条 malformed RawFrame
// 保证原始帧能被 archive/event-file-store 留痕,方便事后排查。
log.warn("[jt808] processing failed peer={} phone={} msgId=0x{} error={}",
addr(ctx), msg.header().phone(), Integer.toHexString(msg.header().messageId()), e.getMessage());
log.debug("[jt808] processing failed stack peer={}", addr(ctx), e);
@@ -175,11 +177,13 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
private IdentityResolution resolveIdentity(Jt808Message msg) {
String phone = msg.header().phone();
if (identityResolver == null) {
// 没有绑定表时用手机号兜底为 VIN保证会话/下行仍可按 phone 找到连接。
return new IdentityResolution(
new VehicleIdentity(phone, false, VehicleIdentitySource.FALLBACK_PHONE),
null);
}
try {
// 优先把 JT808 phone/device/plate 映射到内部 VIN后续所有事件都用内部 VIN 做主键。
return new IdentityResolution(
identityResolver.resolve(new VehicleIdentityLookup(
ProtocolId.JT808,
@@ -206,6 +210,7 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
private void dispatchMalformed(String peer, byte[] frame, String errorMessage, boolean frameError) {
String message = errorMessage == null ? "" : errorMessage;
// 无法解出 header 的坏帧也进入 Dispatcher后续 archive sink 可以保留 raw bytes。
Jt808Message malformed = new Jt808Message(
new Jt808Header(0, frame == null ? 0 : frame.length, 0, false,
Jt808Header.ProtocolVersion.V2013, "unknown", 0, 0, 0),

View File

@@ -168,6 +168,7 @@ public final class Jt808EventMapper implements EventMapper<Jt808Message> {
private IdentityResolution resolveIdentity(Jt808Message message) {
if (message.body() instanceof Jt808Body.Malformed) {
// 坏帧没有可信 header/body统一 unknown原始字节仍由 passthrough 事件保留。
return IdentityResolution.ok(new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN));
}
var header = message.header();
@@ -212,6 +213,7 @@ public final class Jt808EventMapper implements EventMapper<Jt808Message> {
Instant ingestTime) {
List<VehicleEvent> out = new ArrayList<>(batch.locations().size());
Map<String, String> batchMeta = withMeta(meta, "batchType", Integer.toString(batch.batchType()));
// 批量位置拆成多条 Location 事件,方便历史库按单点时间查询和导出。
for (Jt808Body.Location loc : batch.locations()) {
out.add(locationEvent(vin, batchMeta, loc, ingestTime));
}
@@ -224,6 +226,7 @@ public final class Jt808EventMapper implements EventMapper<Jt808Message> {
Instant ingestTime) {
List<VehicleEvent> out = new ArrayList<>(2);
if (media.location() != null) {
// 多媒体上传消息内嵌位置时同时产出 Location避免只看到媒体元数据看不到拍摄位置。
out.add(locationEvent(vin, withMeta(meta, "mediaId", Long.toString(media.mediaId())),
media.location(), ingestTime));
}

View File

@@ -31,6 +31,7 @@ public final class Jt808ChannelRegistry {
reverse.put(channel, phone);
if (old != null && old != channel) {
reverse.remove(old);
// 同一 phone 新连接上线时关闭旧 channel避免下行命令写到已被终端替换的连接。
old.close();
log.info("channel rebound phone={} oldChannel={} newChannel={}", phone, old.id(), channel.id());
}
@@ -55,6 +56,7 @@ public final class Jt808ChannelRegistry {
}
public int nextSerial(String phone) {
// JT808 流水号 16 bit无符号回绕到 0pending 匹配也使用这个序号。
return serials.computeIfAbsent(phone, k -> new AtomicInteger())
.updateAndGet(i -> (i + 1) & 0xFFFF);
}

View File

@@ -23,6 +23,7 @@ public final class Jt808PendingRequests {
public CompletableFuture<Jt808Message> await(String phone, int serial) {
CompletableFuture<Jt808Message> cf = new CompletableFuture<>();
// JT808 平台下行流水号按终端 phone 独立递增,因此 phone+serial 才能唯一定位一次请求。
pending.put(key(phone, serial), cf);
return cf;
}