docs: add detailed 32960 pipeline comments
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,无符号回绕到 0;pending 匹配也使用这个序号。
|
||||
return serials.computeIfAbsent(phone, k -> new AtomicInteger())
|
||||
.updateAndGet(i -> (i + 1) & 0xFFFF);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user