feat: productionize raw history ingestion
This commit is contained in:
@@ -9,13 +9,14 @@ import com.lingniu.ingest.identity.VehicleIdentityLookup;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityRegistry;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityResolver;
|
||||
import com.lingniu.ingest.identity.VehicleIdentitySource;
|
||||
import com.lingniu.ingest.identity.VehicleRegistrationBinding;
|
||||
import com.lingniu.ingest.protocol.jt808.codec.Jt808FrameEncoder;
|
||||
import com.lingniu.ingest.protocol.jt808.codec.Jt808MalformedFrame;
|
||||
import com.lingniu.ingest.protocol.jt808.codec.Jt808MessageDecoder;
|
||||
import com.lingniu.ingest.protocol.jt808.downlink.Jt808Commands;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Header;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808LocationAdditionalInfo;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808LocationFlagInfo;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Message;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808MessageId;
|
||||
import com.lingniu.ingest.protocol.jt808.session.Jt808ChannelRegistry;
|
||||
@@ -25,17 +26,18 @@ import com.lingniu.ingest.session.SessionStore;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* JT808 Netty 入站处理器。职责:
|
||||
@@ -51,8 +53,6 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Jt808ChannelHandler.class);
|
||||
private static final String PROCESSING_ERROR_PREFIX = "processing failed: ";
|
||||
private static final AttributeKey<CompletableFuture<Void>> DURABLE_ACK_CHAIN_ATTR =
|
||||
AttributeKey.valueOf("jt808.durableAckChain");
|
||||
|
||||
private final Jt808MessageDecoder decoder;
|
||||
private final Dispatcher dispatcher;
|
||||
@@ -123,21 +123,29 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
// ====== 会话维护 & 同步应答匹配 ======
|
||||
String phone = msg.header().phone();
|
||||
IdentityResolution identity = resolveIdentity(msg);
|
||||
byte[] durableAck = null;
|
||||
if (msg.body() instanceof Jt808Body.Register reg) {
|
||||
identity = registerIdentity(msg, reg, identity);
|
||||
}
|
||||
byte[] registerAck = null;
|
||||
switch (msg.header().messageId()) {
|
||||
case Jt808MessageId.TERMINAL_REGISTER -> {
|
||||
DeviceSession session = upsertSession(ctx, msg, identity.identity());
|
||||
channelRegistry.bind(phone, ctx.channel());
|
||||
durableAck = registerAckFrame(phone, msg.header().serialNo(), session.token());
|
||||
var command = Jt808Commands.registerAck(msg.header().serialNo(), 0, session.token());
|
||||
registerAck = Jt808FrameEncoder.encode(
|
||||
command.messageId(),
|
||||
phone,
|
||||
channelRegistry.nextSerial(phone),
|
||||
command.body());
|
||||
}
|
||||
case Jt808MessageId.TERMINAL_AUTH -> {
|
||||
upsertSession(ctx, msg, identity.identity());
|
||||
channelRegistry.bind(phone, ctx.channel());
|
||||
durableAck = terminalAckFrame(phone, msg.header().serialNo(), msg.header().messageId(), 0);
|
||||
ackTerminal(ctx, phone, msg.header().serialNo(), msg.header().messageId(), 0);
|
||||
}
|
||||
case Jt808MessageId.TERMINAL_HEARTBEAT -> {
|
||||
channelRegistry.bind(phone, ctx.channel());
|
||||
durableAck = terminalAckFrame(phone, msg.header().serialNo(), msg.header().messageId(), 0);
|
||||
ackTerminal(ctx, phone, msg.header().serialNo(), msg.header().messageId(), 0);
|
||||
}
|
||||
case Jt808MessageId.TERMINAL_GENERAL_RESPONSE -> {
|
||||
// body: ackSerial(2) + ackMsgId(2) + result(1)
|
||||
@@ -153,7 +161,7 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
}
|
||||
|
||||
// ====== 派发到 Dispatcher ======
|
||||
Map<String, String> meta = new HashMap<>(6);
|
||||
Map<String, String> meta = new HashMap<>(32);
|
||||
meta.put("vin", identity.identity().vin());
|
||||
meta.put("phone", phone);
|
||||
meta.put("identityResolved", Boolean.toString(identity.identity().resolved()));
|
||||
@@ -163,8 +171,11 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
meta.put("identityErrorMessage", identity.errorMessage());
|
||||
}
|
||||
meta.put("seq", Integer.toString(msg.header().serialNo()));
|
||||
meta.put("messageId", "0x" + Jt808MessageId.hex(msg.header().messageId()));
|
||||
meta.put("messageName", Jt808MessageId.nameOf(msg.header().messageId()));
|
||||
meta.put("reportType", Jt808MessageId.reportTypeOf(msg.header().messageId()));
|
||||
putBodyMetadata(meta, msg.body());
|
||||
meta.put("peer", addr(ctx));
|
||||
addRegistrationMetadata(meta, msg.body());
|
||||
RawFrame rf = new RawFrame(
|
||||
ProtocolId.JT808,
|
||||
msg.header().messageId(),
|
||||
@@ -173,63 +184,19 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
frame,
|
||||
meta,
|
||||
Instant.now());
|
||||
if (durableAck == null) {
|
||||
dispatcher.dispatch(rf);
|
||||
if (registerAck != null) {
|
||||
byte[] ack = registerAck;
|
||||
dispatcher.dispatchAndAwait(rf)
|
||||
.thenRun(() -> ctx.writeAndFlush(Unpooled.wrappedBuffer(ack)))
|
||||
.exceptionally(ex -> {
|
||||
log.warn("[jt808] register ack withheld because dispatch durability failed peer={} phone={} seq={} error={}",
|
||||
addr(ctx), phone, msg.header().serialNo(),
|
||||
ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage());
|
||||
return null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
enqueueDurableAck(ctx, phone, msg.header().messageId(), dispatcher.dispatchAndAwait(rf), durableAck);
|
||||
}
|
||||
|
||||
private void enqueueDurableAck(ChannelHandlerContext ctx,
|
||||
String phone,
|
||||
int messageId,
|
||||
CompletableFuture<Void> dispatchFuture,
|
||||
byte[] ackFrame) {
|
||||
CompletableFuture<Void> previous = ctx.channel().attr(DURABLE_ACK_CHAIN_ATTR).get();
|
||||
if (previous == null) {
|
||||
previous = CompletableFuture.completedFuture(null);
|
||||
}
|
||||
CompletableFuture<Void> next = previous
|
||||
.handle((ignored, previousFailure) -> null)
|
||||
.thenCompose(ignored -> dispatchFuture.handle((ok, failure) -> failure))
|
||||
.thenCompose(failure -> runOnEventLoop(ctx,
|
||||
() -> handleDurableAckResult(ctx, phone, messageId, failure, ackFrame)));
|
||||
ctx.channel().attr(DURABLE_ACK_CHAIN_ATTR).set(next);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> runOnEventLoop(ChannelHandlerContext ctx, Runnable task) {
|
||||
CompletableFuture<Void> done = new CompletableFuture<>();
|
||||
Runnable wrapped = () -> {
|
||||
try {
|
||||
task.run();
|
||||
done.complete(null);
|
||||
} catch (Throwable t) {
|
||||
done.completeExceptionally(t);
|
||||
}
|
||||
};
|
||||
if (ctx.executor().inEventLoop()) {
|
||||
wrapped.run();
|
||||
} else {
|
||||
ctx.executor().execute(wrapped);
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
private void handleDurableAckResult(ChannelHandlerContext ctx,
|
||||
String phone,
|
||||
int messageId,
|
||||
Throwable failure,
|
||||
byte[] ackFrame) {
|
||||
if (!ctx.channel().isActive()) {
|
||||
return;
|
||||
}
|
||||
if (failure != null) {
|
||||
log.warn("[jt808] dispatch failed before ack peer={} phone={} msgId=0x{}",
|
||||
addr(ctx), phone, Integer.toHexString(messageId), failure);
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
ctx.writeAndFlush(Unpooled.wrappedBuffer(ackFrame));
|
||||
dispatcher.dispatch(rf);
|
||||
}
|
||||
|
||||
private IdentityResolution resolveIdentity(Jt808Message msg) {
|
||||
@@ -267,6 +234,28 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
return new VehicleIdentity("unknown", false, identity.source());
|
||||
}
|
||||
|
||||
private IdentityResolution registerIdentity(Jt808Message msg,
|
||||
Jt808Body.Register reg,
|
||||
IdentityResolution current) {
|
||||
VehicleIdentity currentIdentity = current.identity();
|
||||
boolean alreadyResolved = currentIdentity != null && currentIdentity.resolved();
|
||||
String vin = alreadyResolved ? currentIdentity.vin() : generatedInternalVin(msg.header().phone(), reg);
|
||||
identityRegistry.bind(new VehicleIdentityBinding(
|
||||
ProtocolId.JT808,
|
||||
vin,
|
||||
msg.header().phone(),
|
||||
reg.deviceId(),
|
||||
reg.plate(),
|
||||
Map.of(
|
||||
"province", Integer.toString(reg.province()),
|
||||
"city", Integer.toString(reg.city()),
|
||||
"maker", reg.maker(),
|
||||
"deviceType", reg.deviceType(),
|
||||
"plateColor", Integer.toString(reg.plateColor()))));
|
||||
VehicleIdentitySource source = alreadyResolved ? currentIdentity.source() : VehicleIdentitySource.REGISTERED;
|
||||
return new IdentityResolution(new VehicleIdentity(vin, true, source), current.errorMessage());
|
||||
}
|
||||
|
||||
private void dispatchFrameError(ChannelHandlerContext ctx, Jt808MalformedFrame malformed) {
|
||||
String peer = malformed.peer() == null || malformed.peer().isBlank() ? addr(ctx) : malformed.peer();
|
||||
dispatchMalformed(peer, malformed.rawBytes(), malformed.reason(), true);
|
||||
@@ -283,7 +272,7 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
new Jt808Header(0, frame == null ? 0 : frame.length, 0, false,
|
||||
Jt808Header.ProtocolVersion.V2013, "unknown", 0, 0, 0),
|
||||
new Jt808Body.Malformed(frame == null ? new byte[0] : frame, peer, message));
|
||||
Map<String, String> meta = new HashMap<>(4);
|
||||
Map<String, String> meta = new HashMap<>(8);
|
||||
meta.put("vin", "unknown");
|
||||
meta.put("peer", peer);
|
||||
meta.put("identityResolved", "false");
|
||||
@@ -295,6 +284,7 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
meta.put("parseError", "true");
|
||||
meta.put("parseErrorMessage", message);
|
||||
}
|
||||
putBodyMetadata(meta, malformed.body());
|
||||
dispatcher.dispatch(new RawFrame(
|
||||
ProtocolId.JT808,
|
||||
0,
|
||||
@@ -312,15 +302,19 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
msg.header().version(), msg.header().phone(), msg.header().serialNo(), 0, 0),
|
||||
new Jt808Body.Malformed(frame == null ? new byte[0] : frame, addr(ctx),
|
||||
PROCESSING_ERROR_PREFIX + message));
|
||||
Map<String, String> meta = new HashMap<>(8);
|
||||
Map<String, String> meta = new HashMap<>(16);
|
||||
meta.put("vin", "unknown");
|
||||
meta.put("phone", msg.header().phone());
|
||||
meta.put("peer", addr(ctx));
|
||||
meta.put("seq", Integer.toString(msg.header().serialNo()));
|
||||
meta.put("messageId", "0x" + Jt808MessageId.hex(msg.header().messageId()));
|
||||
meta.put("messageName", Jt808MessageId.nameOf(msg.header().messageId()));
|
||||
meta.put("reportType", Jt808MessageId.reportTypeOf(msg.header().messageId()));
|
||||
meta.put("identityResolved", "false");
|
||||
meta.put("identitySource", VehicleIdentitySource.UNKNOWN.name());
|
||||
meta.put("processingError", "true");
|
||||
meta.put("processingErrorMessage", message);
|
||||
putBodyMetadata(meta, failed.body());
|
||||
dispatcher.dispatch(new RawFrame(
|
||||
ProtocolId.JT808,
|
||||
0,
|
||||
@@ -343,25 +337,6 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
Instant.now(), Instant.now(),
|
||||
Map.of("protocolVersion", msg.header().version().name())));
|
||||
session = session.withVin(identity.vin());
|
||||
if (msg.body() instanceof Jt808Body.Register reg) {
|
||||
identityRegistry.register(new VehicleRegistrationBinding(
|
||||
ProtocolId.JT808,
|
||||
session.vin(),
|
||||
phone,
|
||||
reg.deviceId(),
|
||||
reg.plate(),
|
||||
reg.province(),
|
||||
reg.city(),
|
||||
reg.maker(),
|
||||
reg.deviceType(),
|
||||
reg.plateColor()));
|
||||
if (identity.resolved() && session.vin() != null
|
||||
&& !session.vin().isBlank()
|
||||
&& !"unknown".equalsIgnoreCase(session.vin())) {
|
||||
identityRegistry.bind(new VehicleIdentityBinding(
|
||||
ProtocolId.JT808, session.vin(), phone, reg.deviceId(), reg.plate()));
|
||||
}
|
||||
}
|
||||
sessions.put(session);
|
||||
return session;
|
||||
}
|
||||
@@ -380,37 +355,175 @@ public class Jt808ChannelHandler extends SimpleChannelInboundHandler<Object> {
|
||||
return body instanceof Jt808Body.Register reg ? reg.plate() : "";
|
||||
}
|
||||
|
||||
private static void addRegistrationMetadata(Map<String, String> meta, Jt808Body body) {
|
||||
if (!(body instanceof Jt808Body.Register reg)) {
|
||||
return;
|
||||
private static void putBodyMetadata(Map<String, String> meta, Jt808Body body) {
|
||||
switch (body) {
|
||||
case Jt808Body.Register reg -> {
|
||||
put(meta, "body.province", reg.province());
|
||||
put(meta, "body.city", reg.city());
|
||||
put(meta, "body.maker", reg.maker());
|
||||
put(meta, "body.deviceType", reg.deviceType());
|
||||
put(meta, "body.deviceId", reg.deviceId());
|
||||
put(meta, "body.plateColor", reg.plateColor());
|
||||
put(meta, "body.plate", reg.plate());
|
||||
}
|
||||
case Jt808Body.Auth auth -> {
|
||||
put(meta, "body.token", auth.token());
|
||||
put(meta, "body.imei", auth.imei());
|
||||
put(meta, "body.softwareVersion", auth.softwareVersion());
|
||||
}
|
||||
case Jt808Body.Location location -> putLocationMetadata(meta, "body.", location);
|
||||
case Jt808Body.LocationBatch batch -> {
|
||||
put(meta, "body.batchType", batch.batchType());
|
||||
put(meta, "body.locationCount", batch.locations() == null ? 0 : batch.locations().size());
|
||||
}
|
||||
case Jt808Body.ParamsReport report -> {
|
||||
put(meta, "body.responseSerialNo", report.responseSerialNo());
|
||||
put(meta, "body.parameterCount", report.parameters() == null ? 0 : report.parameters().size());
|
||||
if (report.parameters() != null) {
|
||||
report.parameters().forEach((id, bytes) ->
|
||||
put(meta, "body.param.0x" + String.format("%08X", id), toHex(bytes)));
|
||||
}
|
||||
}
|
||||
case Jt808Body.TerminalAttrs attrs -> {
|
||||
put(meta, "body.terminalType", attrs.terminalType());
|
||||
put(meta, "body.maker", attrs.maker());
|
||||
put(meta, "body.terminalModel", attrs.terminalModel());
|
||||
put(meta, "body.terminalId", attrs.terminalId());
|
||||
put(meta, "body.iccid", attrs.iccid());
|
||||
put(meta, "body.hardwareVersion", attrs.hardwareVersion());
|
||||
put(meta, "body.firmwareVersion", attrs.firmwareVersion());
|
||||
put(meta, "body.gnssProperty", attrs.gnssProperty());
|
||||
put(meta, "body.commProperty", attrs.commProperty());
|
||||
}
|
||||
case Jt808Body.MediaEvent event -> {
|
||||
put(meta, "body.mediaId", event.mediaId());
|
||||
put(meta, "body.mediaType", event.mediaType());
|
||||
put(meta, "body.mediaFormat", event.mediaFormat());
|
||||
put(meta, "body.eventCode", event.eventCode());
|
||||
put(meta, "body.channelId", event.channelId());
|
||||
}
|
||||
case Jt808Body.MediaUpload upload -> {
|
||||
put(meta, "body.mediaId", upload.mediaId());
|
||||
put(meta, "body.mediaType", upload.mediaType());
|
||||
put(meta, "body.mediaFormat", upload.mediaFormat());
|
||||
put(meta, "body.eventCode", upload.eventCode());
|
||||
put(meta, "body.channelId", upload.channelId());
|
||||
put(meta, "body.dataSizeBytes", upload.data() == null ? 0 : upload.data().length);
|
||||
if (upload.location() != null) {
|
||||
putLocationMetadata(meta, "body.location.", upload.location());
|
||||
}
|
||||
}
|
||||
case Jt808Body.MediaAttributes attrs -> {
|
||||
put(meta, "body.inputAudioEncoding", attrs.inputAudioEncoding());
|
||||
put(meta, "body.inputAudioChannels", attrs.inputAudioChannels());
|
||||
put(meta, "body.inputAudioSampleRate", attrs.inputAudioSampleRate());
|
||||
put(meta, "body.inputAudioBitDepth", attrs.inputAudioBitDepth());
|
||||
put(meta, "body.audioFrameLength", attrs.audioFrameLength());
|
||||
put(meta, "body.audioOutputSupported", attrs.audioOutputSupported());
|
||||
put(meta, "body.videoEncoding", attrs.videoEncoding());
|
||||
put(meta, "body.maxAudioChannels", attrs.maxAudioChannels());
|
||||
put(meta, "body.maxVideoChannels", attrs.maxVideoChannels());
|
||||
put(meta, "body.rawSizeBytes", attrs.raw() == null ? 0 : attrs.raw().length);
|
||||
}
|
||||
case Jt808Body.FileUploadComplete complete -> {
|
||||
put(meta, "body.responseSerialNo", complete.responseSerialNo());
|
||||
put(meta, "body.result", complete.result());
|
||||
put(meta, "body.rawSizeBytes", complete.raw() == null ? 0 : complete.raw().length);
|
||||
}
|
||||
case Jt808Body.FileList files -> {
|
||||
put(meta, "body.responseSerialNo", files.responseSerialNo());
|
||||
put(meta, "body.fileCount", files.fileCount());
|
||||
put(meta, "body.rawSizeBytes", files.raw() == null ? 0 : files.raw().length);
|
||||
}
|
||||
case Jt808Body.PassengerVolume volume -> {
|
||||
put(meta, "body.channelId", volume.channelId());
|
||||
put(meta, "body.startTime", volume.startTime());
|
||||
put(meta, "body.endTime", volume.endTime());
|
||||
put(meta, "body.passengerGetOn", volume.passengerGetOn());
|
||||
put(meta, "body.passengerGetOff", volume.passengerGetOff());
|
||||
put(meta, "body.rawSizeBytes", volume.raw() == null ? 0 : volume.raw().length);
|
||||
}
|
||||
case Jt808Body.Passthrough passthrough -> {
|
||||
put(meta, "body.passthroughType", "0x" + String.format("%02X", passthrough.passthroughType()));
|
||||
put(meta, "body.dataSizeBytes", passthrough.data() == null ? 0 : passthrough.data().length);
|
||||
put(meta, "body.dataHex", toHex(passthrough.data()));
|
||||
}
|
||||
case Jt808Body.Raw raw -> {
|
||||
put(meta, "body.rawMessageId", "0x" + Jt808MessageId.hex(raw.messageId()));
|
||||
put(meta, "body.rawSizeBytes", raw.bytes() == null ? 0 : raw.bytes().length);
|
||||
put(meta, "body.rawHex", toHex(raw.bytes()));
|
||||
}
|
||||
case Jt808Body.Malformed malformed -> {
|
||||
put(meta, "body.peer", malformed.peer());
|
||||
put(meta, "body.errorMessage", malformed.errorMessage());
|
||||
put(meta, "body.rawSizeBytes", malformed.bytes() == null ? 0 : malformed.bytes().length);
|
||||
}
|
||||
case Jt808Body.Heartbeat ignored -> {
|
||||
}
|
||||
case Jt808Body.Unregister ignored -> {
|
||||
}
|
||||
}
|
||||
meta.put("jt808.register.province", Integer.toString(reg.province()));
|
||||
meta.put("jt808.register.city", Integer.toString(reg.city()));
|
||||
meta.put("jt808.register.maker", nullToEmpty(reg.maker()));
|
||||
meta.put("jt808.register.deviceType", nullToEmpty(reg.deviceType()));
|
||||
meta.put("jt808.register.deviceId", nullToEmpty(reg.deviceId()));
|
||||
meta.put("jt808.register.plateColor", Integer.toString(reg.plateColor()));
|
||||
meta.put("jt808.register.plate", nullToEmpty(reg.plate()));
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
private static void putLocationMetadata(Map<String, String> meta, String prefix, Jt808Body.Location location) {
|
||||
put(meta, prefix + "alarmFlag", location.alarmFlag());
|
||||
put(meta, prefix + "statusFlag", location.statusFlag());
|
||||
meta.putAll(Jt808LocationFlagInfo.decodeStatus(location.statusFlag(), prefix + "status."));
|
||||
meta.putAll(Jt808LocationFlagInfo.decodeAlarm(location.alarmFlag(), prefix + "alarm."));
|
||||
put(meta, prefix + "longitude", location.longitude());
|
||||
put(meta, prefix + "latitude", location.latitude());
|
||||
put(meta, prefix + "altitudeM", location.altitudeM());
|
||||
put(meta, prefix + "speedKmh", location.speedKmh());
|
||||
put(meta, prefix + "directionDeg", location.directionDeg());
|
||||
put(meta, prefix + "deviceTime", location.deviceTime());
|
||||
if (location.extensionItems() != null) {
|
||||
location.extensionItems().forEach((id, bytes) ->
|
||||
put(meta, prefix + "extra.0x" + String.format("%02X", id), toHex(bytes)));
|
||||
meta.putAll(Jt808LocationAdditionalInfo.decode(location.extensionItems(), prefix + "extra."));
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] registerAckFrame(String phone, int ackSerial, String token) {
|
||||
var cmd = Jt808Commands.registerAck(ackSerial, 0, token);
|
||||
return Jt808FrameEncoder.encode(cmd.messageId(), phone, channelRegistry.nextSerial(phone), cmd.body());
|
||||
private static void put(Map<String, String> meta, String key, Object value) {
|
||||
if (value != null) {
|
||||
meta.put(key, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] terminalAckFrame(String phone, int ackSerial, int ackMsgId, int result) {
|
||||
private static String toHex(byte[] bytes) {
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder out = new StringBuilder(bytes.length * 2);
|
||||
for (byte b : bytes) {
|
||||
out.append(String.format("%02X", b & 0xFF));
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private void ackTerminal(ChannelHandlerContext ctx, String phone, int ackSerial, int ackMsgId, int result) {
|
||||
var cmd = Jt808Commands.platformAck(ackSerial, ackMsgId, result);
|
||||
return Jt808FrameEncoder.encode(cmd.messageId(), phone, channelRegistry.nextSerial(phone), cmd.body());
|
||||
byte[] frame = Jt808FrameEncoder.encode(cmd.messageId(), phone, channelRegistry.nextSerial(phone), cmd.body());
|
||||
ctx.writeAndFlush(Unpooled.wrappedBuffer(frame));
|
||||
}
|
||||
|
||||
private static String generateToken(String phone) {
|
||||
return UUID.nameUUIDFromBytes(("jt808:" + phone).getBytes()).toString().replace("-", "").substring(0, 16);
|
||||
}
|
||||
|
||||
private static String generatedInternalVin(String phone, Jt808Body.Register reg) {
|
||||
String material = String.join("|", phone, reg.deviceId(), reg.plate(), reg.maker(), reg.deviceType());
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(material.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder suffix = new StringBuilder(12);
|
||||
for (int i = 0; i < digest.length && suffix.length() < 12; i++) {
|
||||
suffix.append(String.format("%02X", digest[i] & 0xFF));
|
||||
}
|
||||
return "JT808" + suffix.substring(0, 12);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) {
|
||||
channelRegistry.unbind(ctx.channel())
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.lingniu.ingest.identity.VehicleIdentityResolver;
|
||||
import com.lingniu.ingest.identity.VehicleIdentitySource;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Header;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808LocationAdditionalInfo;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808LocationFlagInfo;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Message;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -276,12 +278,14 @@ public final class Jt808EventMapper implements EventMapper<Jt808Message> {
|
||||
}
|
||||
|
||||
private static Map<String, String> locationMeta(Map<String, String> meta, Jt808Body.Location loc) {
|
||||
if (loc.extensionItems().isEmpty()) {
|
||||
return meta;
|
||||
}
|
||||
java.util.HashMap<String, String> copy = new java.util.HashMap<>(meta);
|
||||
loc.extensionItems().forEach((id, bytes) ->
|
||||
copy.put("jt808.extra.0x" + String.format("%02X", id), hex(bytes)));
|
||||
copy.putAll(Jt808LocationFlagInfo.decodeStatus(loc.statusFlag(), "jt808.status."));
|
||||
copy.putAll(Jt808LocationFlagInfo.decodeAlarm(loc.alarmFlag(), "jt808.alarm."));
|
||||
if (!loc.extensionItems().isEmpty()) {
|
||||
loc.extensionItems().forEach((id, bytes) ->
|
||||
copy.put("jt808.extra.0x" + String.format("%02X", id), hex(bytes)));
|
||||
copy.putAll(Jt808LocationAdditionalInfo.decode(loc.extensionItems()));
|
||||
}
|
||||
return Map.copyOf(copy);
|
||||
}
|
||||
|
||||
@@ -304,15 +308,7 @@ public final class Jt808EventMapper implements EventMapper<Jt808Message> {
|
||||
}
|
||||
|
||||
private static Double totalMileageKm(Jt808Body.Location loc) {
|
||||
byte[] bytes = loc.extensionItems().get(0x01);
|
||||
if (bytes == null || bytes.length != 4) {
|
||||
return null;
|
||||
}
|
||||
long raw = ((long) bytes[0] & 0xFF) << 24
|
||||
| ((long) bytes[1] & 0xFF) << 16
|
||||
| ((long) bytes[2] & 0xFF) << 8
|
||||
| ((long) bytes[3] & 0xFF);
|
||||
return raw * 0.1;
|
||||
return Jt808LocationAdditionalInfo.mileageKm(loc.extensionItems());
|
||||
}
|
||||
|
||||
private static String hex(byte[] bytes) {
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package com.lingniu.ingest.protocol.jt808.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class Jt808LocationAdditionalInfo {
|
||||
|
||||
public static final String DEFAULT_PREFIX = "jt808.extra.";
|
||||
|
||||
private static final String[] VEHICLE_SIGNAL_BITS = {
|
||||
"low_beam",
|
||||
"high_beam",
|
||||
"right_turn",
|
||||
"left_turn",
|
||||
"brake",
|
||||
"reverse",
|
||||
"fog_lamp",
|
||||
"position_lamp",
|
||||
"horn",
|
||||
"air_conditioner",
|
||||
"neutral",
|
||||
"retarder",
|
||||
"abs",
|
||||
"heater",
|
||||
"clutch"
|
||||
};
|
||||
|
||||
private static final String[] IO_STATUS_BITS = {
|
||||
"deep_sleep",
|
||||
"sleep"
|
||||
};
|
||||
|
||||
private Jt808LocationAdditionalInfo() {
|
||||
}
|
||||
|
||||
public static Map<String, String> decode(Map<Integer, byte[]> extensionItems) {
|
||||
return decode(extensionItems, DEFAULT_PREFIX);
|
||||
}
|
||||
|
||||
public static Map<String, String> decode(Map<Integer, byte[]> extensionItems, String prefix) {
|
||||
if (extensionItems == null || extensionItems.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
String p = prefix == null ? "" : prefix;
|
||||
Map<String, String> out = new LinkedHashMap<>();
|
||||
extensionItems.forEach((id, bytes) -> decodeItem(out, p, id == null ? -1 : id, bytes));
|
||||
return Map.copyOf(out);
|
||||
}
|
||||
|
||||
public static Double mileageKm(Map<Integer, byte[]> extensionItems) {
|
||||
if (extensionItems == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] bytes = extensionItems.get(0x01);
|
||||
if (bytes == null || bytes.length != 4) {
|
||||
return null;
|
||||
}
|
||||
return u32(bytes, 0) * 0.1;
|
||||
}
|
||||
|
||||
private static void decodeItem(Map<String, String> out, String prefix, int id, byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
return;
|
||||
}
|
||||
switch (id) {
|
||||
case 0x01 -> put(out, prefix + "mileage_km", scaled(bytes, 4, 1));
|
||||
case 0x02 -> put(out, prefix + "fuel_l", scaled(bytes, 2, 1));
|
||||
case 0x03 -> put(out, prefix + "recorder_speed_kmh", scaled(bytes, 2, 1));
|
||||
case 0x04 -> put(out, prefix + "manual_alarm_event_id", unsigned(bytes, 2));
|
||||
case 0x05 -> decodeTirePressure(out, prefix, bytes);
|
||||
case 0x06 -> put(out, prefix + "compartment_temp_c", signedHighBitWord(bytes));
|
||||
case 0x11 -> decodeOverspeed(out, prefix, bytes);
|
||||
case 0x12 -> decodeAreaRouteAlarm(out, prefix, bytes);
|
||||
case 0x13 -> decodeRouteTime(out, prefix, bytes);
|
||||
case 0x25 -> decodeVehicleSignal(out, prefix, bytes);
|
||||
case 0x2A -> decodeIoStatus(out, prefix, bytes);
|
||||
case 0x2B -> decodeAnalog(out, prefix, bytes);
|
||||
case 0x30 -> put(out, prefix + "network_signal_strength", unsigned(bytes, 1));
|
||||
case 0x31 -> put(out, prefix + "gnss_satellite_count", unsigned(bytes, 1));
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void decodeTirePressure(Map<String, String> out, String prefix, byte[] bytes) {
|
||||
if (bytes.length != 30) {
|
||||
return;
|
||||
}
|
||||
List<String> values = new ArrayList<>();
|
||||
List<Integer> validIndexes = new ArrayList<>();
|
||||
List<Integer> invalidIndexes = new ArrayList<>();
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
int value = u8(bytes, i);
|
||||
if (value == 0xFF) {
|
||||
invalidIndexes.add(i);
|
||||
} else {
|
||||
validIndexes.add(i);
|
||||
values.add(Integer.toString(value));
|
||||
}
|
||||
}
|
||||
put(out, prefix + "tire_pressure_pa_values", String.join(",", values));
|
||||
put(out, prefix + "tire_pressure_valid_indexes", ranges(validIndexes));
|
||||
put(out, prefix + "tire_pressure_invalid_indexes", ranges(invalidIndexes));
|
||||
}
|
||||
|
||||
private static void decodeOverspeed(Map<String, String> out, String prefix, byte[] bytes) {
|
||||
if (bytes.length != 1 && bytes.length != 5) {
|
||||
return;
|
||||
}
|
||||
put(out, prefix + "overspeed.position_type", Integer.toString(u8(bytes, 0)));
|
||||
if (bytes.length == 5) {
|
||||
put(out, prefix + "overspeed.area_route_id", Long.toString(u32(bytes, 1)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void decodeAreaRouteAlarm(Map<String, String> out, String prefix, byte[] bytes) {
|
||||
if (bytes.length != 6) {
|
||||
return;
|
||||
}
|
||||
put(out, prefix + "area_route_alarm.position_type", Integer.toString(u8(bytes, 0)));
|
||||
put(out, prefix + "area_route_alarm.area_route_id", Long.toString(u32(bytes, 1)));
|
||||
put(out, prefix + "area_route_alarm.direction", Integer.toString(u8(bytes, 5)));
|
||||
}
|
||||
|
||||
private static void decodeRouteTime(Map<String, String> out, String prefix, byte[] bytes) {
|
||||
if (bytes.length != 7) {
|
||||
return;
|
||||
}
|
||||
put(out, prefix + "route_time.segment_id", Long.toString(u32(bytes, 0)));
|
||||
put(out, prefix + "route_time.duration_seconds", Integer.toString(u16(bytes, 4)));
|
||||
put(out, prefix + "route_time.result", Integer.toString(u8(bytes, 6)));
|
||||
}
|
||||
|
||||
private static void decodeVehicleSignal(Map<String, String> out, String prefix, byte[] bytes) {
|
||||
if (bytes.length != 4) {
|
||||
return;
|
||||
}
|
||||
long raw = u32(bytes, 0);
|
||||
put(out, prefix + "vehicle_signal_raw", Long.toString(raw));
|
||||
for (int i = 0; i < VEHICLE_SIGNAL_BITS.length; i++) {
|
||||
put(out, prefix + "vehicle_signal." + VEHICLE_SIGNAL_BITS[i], Boolean.toString(bit(raw, i)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void decodeIoStatus(Map<String, String> out, String prefix, byte[] bytes) {
|
||||
if (bytes.length != 2) {
|
||||
return;
|
||||
}
|
||||
int raw = u16(bytes, 0);
|
||||
put(out, prefix + "io_status_raw", Integer.toString(raw));
|
||||
for (int i = 0; i < IO_STATUS_BITS.length; i++) {
|
||||
put(out, prefix + "io_status." + IO_STATUS_BITS[i], Boolean.toString(bit(raw, i)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void decodeAnalog(Map<String, String> out, String prefix, byte[] bytes) {
|
||||
if (bytes.length != 4) {
|
||||
return;
|
||||
}
|
||||
long raw = u32(bytes, 0);
|
||||
put(out, prefix + "analog_raw", Long.toString(raw));
|
||||
put(out, prefix + "analog_ad0", Integer.toString((int) (raw & 0xFFFF)));
|
||||
put(out, prefix + "analog_ad1", Integer.toString((int) ((raw >>> 16) & 0xFFFF)));
|
||||
}
|
||||
|
||||
private static String scaled(byte[] bytes, int expectedLength, int scale) {
|
||||
if (bytes.length != expectedLength) {
|
||||
return null;
|
||||
}
|
||||
long raw = expectedLength == 4 ? u32(bytes, 0) : u16(bytes, 0);
|
||||
return BigDecimal.valueOf(raw, scale).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private static String unsigned(byte[] bytes, int expectedLength) {
|
||||
if (bytes.length != expectedLength) {
|
||||
return null;
|
||||
}
|
||||
return switch (expectedLength) {
|
||||
case 1 -> Integer.toString(u8(bytes, 0));
|
||||
case 2 -> Integer.toString(u16(bytes, 0));
|
||||
case 4 -> Long.toString(u32(bytes, 0));
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private static String signedHighBitWord(byte[] bytes) {
|
||||
if (bytes.length != 2) {
|
||||
return null;
|
||||
}
|
||||
int raw = u16(bytes, 0);
|
||||
int value = (raw & 0x8000) == 0 ? raw : -(raw & 0x7FFF);
|
||||
return Integer.toString(value);
|
||||
}
|
||||
|
||||
private static int u8(byte[] bytes, int offset) {
|
||||
return bytes[offset] & 0xFF;
|
||||
}
|
||||
|
||||
private static int u16(byte[] bytes, int offset) {
|
||||
return (u8(bytes, offset) << 8) | u8(bytes, offset + 1);
|
||||
}
|
||||
|
||||
private static long u32(byte[] bytes, int offset) {
|
||||
return ((long) u8(bytes, offset) << 24)
|
||||
| ((long) u8(bytes, offset + 1) << 16)
|
||||
| ((long) u8(bytes, offset + 2) << 8)
|
||||
| u8(bytes, offset + 3);
|
||||
}
|
||||
|
||||
private static boolean bit(long raw, int bit) {
|
||||
return ((raw >>> bit) & 1L) == 1L;
|
||||
}
|
||||
|
||||
private static void put(Map<String, String> out, String key, String value) {
|
||||
if (value != null) {
|
||||
out.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String ranges(List<Integer> indexes) {
|
||||
if (indexes.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
List<String> ranges = new ArrayList<>();
|
||||
int start = indexes.getFirst();
|
||||
int previous = start;
|
||||
for (int i = 1; i < indexes.size(); i++) {
|
||||
int current = indexes.get(i);
|
||||
if (current == previous + 1) {
|
||||
previous = current;
|
||||
continue;
|
||||
}
|
||||
ranges.add(formatRange(start, previous));
|
||||
start = current;
|
||||
previous = current;
|
||||
}
|
||||
ranges.add(formatRange(start, previous));
|
||||
return String.join(",", ranges);
|
||||
}
|
||||
|
||||
private static String formatRange(int start, int end) {
|
||||
return start == end ? Integer.toString(start) : start + "-" + end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.lingniu.ingest.protocol.jt808.model;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class Jt808LocationFlagInfo {
|
||||
|
||||
private static final String[] ALARM_BITS = {
|
||||
"emergency",
|
||||
"overspeed",
|
||||
"fatigue_driving",
|
||||
"warning",
|
||||
"gnss_module_fault",
|
||||
"gnss_antenna_disconnected",
|
||||
"gnss_antenna_short_circuit",
|
||||
"terminal_main_power_undervoltage",
|
||||
"terminal_main_power_lost",
|
||||
"display_fault",
|
||||
"tts_fault",
|
||||
"camera_fault",
|
||||
"transport_ic_card_fault",
|
||||
"overspeed_warning",
|
||||
"fatigue_driving_warning",
|
||||
"prohibited_driving",
|
||||
null,
|
||||
null,
|
||||
"daily_driving_timeout",
|
||||
"overtime_parking",
|
||||
"area_in_out",
|
||||
"route_in_out",
|
||||
"route_time_abnormal",
|
||||
"route_deviation",
|
||||
"vss_fault",
|
||||
"fuel_abnormal",
|
||||
"stolen",
|
||||
"illegal_ignition",
|
||||
"illegal_displacement",
|
||||
"collision_rollover",
|
||||
"rollover_warning"
|
||||
};
|
||||
|
||||
private Jt808LocationFlagInfo() {
|
||||
}
|
||||
|
||||
public static Map<String, String> decodeAlarm(long alarmFlag, String prefix) {
|
||||
Map<String, String> out = new LinkedHashMap<>();
|
||||
String p = prefix == null ? "" : prefix;
|
||||
for (int i = 0; i < ALARM_BITS.length; i++) {
|
||||
String name = ALARM_BITS[i];
|
||||
if (name != null) {
|
||||
out.put(p + name, Boolean.toString(bit(alarmFlag, i)));
|
||||
}
|
||||
}
|
||||
return Map.copyOf(out);
|
||||
}
|
||||
|
||||
public static Map<String, String> decodeStatus(long statusFlag, String prefix) {
|
||||
Map<String, String> out = new LinkedHashMap<>();
|
||||
String p = prefix == null ? "" : prefix;
|
||||
out.put(p + "acc_on", Boolean.toString(bit(statusFlag, 0)));
|
||||
out.put(p + "positioned", Boolean.toString(bit(statusFlag, 1)));
|
||||
out.put(p + "south_latitude", Boolean.toString(bit(statusFlag, 2)));
|
||||
out.put(p + "west_longitude", Boolean.toString(bit(statusFlag, 3)));
|
||||
out.put(p + "out_of_service", Boolean.toString(bit(statusFlag, 4)));
|
||||
out.put(p + "encrypted", Boolean.toString(bit(statusFlag, 5)));
|
||||
out.put(p + "forward_collision_warning", Boolean.toString(bit(statusFlag, 6)));
|
||||
out.put(p + "lane_departure_warning", Boolean.toString(bit(statusFlag, 7)));
|
||||
out.put(p + "load_status", loadStatus(statusFlag));
|
||||
out.put(p + "fuel_circuit_cut", Boolean.toString(bit(statusFlag, 10)));
|
||||
out.put(p + "electric_circuit_cut", Boolean.toString(bit(statusFlag, 11)));
|
||||
out.put(p + "door_locked", Boolean.toString(bit(statusFlag, 12)));
|
||||
out.put(p + "door1_open", Boolean.toString(bit(statusFlag, 13)));
|
||||
out.put(p + "door2_open", Boolean.toString(bit(statusFlag, 14)));
|
||||
out.put(p + "door3_open", Boolean.toString(bit(statusFlag, 15)));
|
||||
out.put(p + "door4_open", Boolean.toString(bit(statusFlag, 16)));
|
||||
out.put(p + "door5_open", Boolean.toString(bit(statusFlag, 17)));
|
||||
out.put(p + "gps_used", Boolean.toString(bit(statusFlag, 18)));
|
||||
out.put(p + "beidou_used", Boolean.toString(bit(statusFlag, 19)));
|
||||
out.put(p + "glonass_used", Boolean.toString(bit(statusFlag, 20)));
|
||||
out.put(p + "galileo_used", Boolean.toString(bit(statusFlag, 21)));
|
||||
out.put(p + "moving", Boolean.toString(bit(statusFlag, 22)));
|
||||
return Map.copyOf(out);
|
||||
}
|
||||
|
||||
private static String loadStatus(long statusFlag) {
|
||||
int code = (int) ((statusFlag >>> 8) & 0x03);
|
||||
return switch (code) {
|
||||
case 0 -> "EMPTY";
|
||||
case 1 -> "HALF_LOADED";
|
||||
case 2 -> "RESERVED";
|
||||
case 3 -> "FULL";
|
||||
default -> "UNKNOWN";
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean bit(long raw, int bit) {
|
||||
return ((raw >>> bit) & 1L) == 1L;
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,56 @@ public final class Jt808MessageId {
|
||||
public static final int PLATFORM_TRACK_CONTROL = 0x8202;
|
||||
public static final int PLATFORM_ALARM_ACK = 0x8203;
|
||||
public static final int PLATFORM_DELETE_CIRCLE_AREA = 0x8601;
|
||||
|
||||
public static String nameOf(int messageId) {
|
||||
return switch (messageId) {
|
||||
case TERMINAL_GENERAL_RESPONSE -> "TERMINAL_GENERAL_RESPONSE";
|
||||
case TERMINAL_HEARTBEAT -> "TERMINAL_HEARTBEAT";
|
||||
case TERMINAL_UNREGISTER -> "TERMINAL_UNREGISTER";
|
||||
case TERMINAL_REGISTER -> "TERMINAL_REGISTER";
|
||||
case TERMINAL_AUTH -> "TERMINAL_AUTH";
|
||||
case TERMINAL_PARAMS_REPORT -> "TERMINAL_PARAMS_REPORT";
|
||||
case TERMINAL_ATTRS_REPORT -> "TERMINAL_ATTRS_REPORT";
|
||||
case TERMINAL_LOCATION -> "TERMINAL_LOCATION";
|
||||
case TERMINAL_LOCATION_BATCH -> "TERMINAL_LOCATION_BATCH";
|
||||
case TERMINAL_EVENT_REPORT -> "TERMINAL_EVENT_REPORT";
|
||||
case TERMINAL_CAN_DATA_REPORT -> "TERMINAL_CAN_DATA_REPORT";
|
||||
case TERMINAL_MEDIA_EVENT -> "TERMINAL_MEDIA_EVENT";
|
||||
case TERMINAL_MEDIA_UPLOAD -> "TERMINAL_MEDIA_UPLOAD";
|
||||
case TERMINAL_PASSTHROUGH -> "TERMINAL_PASSTHROUGH";
|
||||
case PLATFORM_GENERAL_RESPONSE -> "PLATFORM_GENERAL_RESPONSE";
|
||||
case PLATFORM_REGISTER_ACK -> "PLATFORM_REGISTER_ACK";
|
||||
case PLATFORM_SET_PARAMS -> "PLATFORM_SET_PARAMS";
|
||||
case PLATFORM_QUERY_PARAMS -> "PLATFORM_QUERY_PARAMS";
|
||||
case PLATFORM_TERMINAL_CONTROL -> "PLATFORM_TERMINAL_CONTROL";
|
||||
case PLATFORM_QUERY_ATTRS -> "PLATFORM_QUERY_ATTRS";
|
||||
case PLATFORM_QUERY_LOCATION -> "PLATFORM_QUERY_LOCATION";
|
||||
case PLATFORM_TRACK_CONTROL -> "PLATFORM_TRACK_CONTROL";
|
||||
case PLATFORM_ALARM_ACK -> "PLATFORM_ALARM_ACK";
|
||||
case PLATFORM_DELETE_CIRCLE_AREA -> "PLATFORM_DELETE_CIRCLE_AREA";
|
||||
default -> "UNKNOWN_0x" + hex(messageId);
|
||||
};
|
||||
}
|
||||
|
||||
public static String reportTypeOf(int messageId) {
|
||||
return switch (messageId) {
|
||||
case TERMINAL_LOCATION -> "LOCATION";
|
||||
case TERMINAL_LOCATION_BATCH -> "LOCATION_BATCH";
|
||||
case TERMINAL_HEARTBEAT -> "HEARTBEAT";
|
||||
case TERMINAL_AUTH -> "AUTH";
|
||||
case TERMINAL_REGISTER -> "REGISTER";
|
||||
case TERMINAL_UNREGISTER -> "UNREGISTER";
|
||||
case TERMINAL_GENERAL_RESPONSE -> "TERMINAL_RESPONSE";
|
||||
case TERMINAL_PARAMS_REPORT -> "PARAMS_REPORT";
|
||||
case TERMINAL_ATTRS_REPORT -> "ATTRS_REPORT";
|
||||
case TERMINAL_MEDIA_EVENT -> "MEDIA_EVENT";
|
||||
case TERMINAL_MEDIA_UPLOAD -> "MEDIA_UPLOAD";
|
||||
case TERMINAL_PASSTHROUGH -> "PASSTHROUGH";
|
||||
default -> "RAW";
|
||||
};
|
||||
}
|
||||
|
||||
public static String hex(int messageId) {
|
||||
return String.format("%04X", messageId & 0xFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.within;
|
||||
|
||||
class Jt808ChannelHandlerTest {
|
||||
|
||||
@@ -86,7 +87,7 @@ class Jt808ChannelHandlerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unresolvedRegisterDoesNotBindUnknownVinToExternalIdentifiers() {
|
||||
void unresolvedRegisterBindsGeneratedInternalVinToExternalIdentifiers() {
|
||||
InMemorySessionStore sessions = new InMemorySessionStore();
|
||||
InMemoryVehicleIdentityService identity = new InMemoryVehicleIdentityService();
|
||||
DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(new ImmediateKafkaSink()));
|
||||
@@ -115,9 +116,9 @@ class Jt808ChannelHandlerTest {
|
||||
assertThat(identity.resolve(new VehicleIdentityLookup(
|
||||
ProtocolId.JT808, "", "123456789012", "", "")))
|
||||
.satisfies(resolved -> {
|
||||
assertThat(resolved.vin()).isEqualTo("123456789012");
|
||||
assertThat(resolved.resolved()).isFalse();
|
||||
assertThat(resolved.source()).isEqualTo(VehicleIdentitySource.FALLBACK_PHONE);
|
||||
assertThat(resolved.vin()).startsWith("JT808");
|
||||
assertThat(resolved.resolved()).isTrue();
|
||||
assertThat(resolved.source()).isEqualTo(VehicleIdentitySource.BOUND_PHONE);
|
||||
});
|
||||
|
||||
batchExecutor.close();
|
||||
@@ -269,14 +270,143 @@ class Jt808ChannelHandlerTest {
|
||||
assertThat(archive.metadata())
|
||||
.containsEntry("vin", "LNVIN000000000808")
|
||||
.containsEntry("phone", "123456789012")
|
||||
.containsEntry("messageId", "0x0200")
|
||||
.containsEntry("messageName", "TERMINAL_LOCATION")
|
||||
.containsEntry("reportType", "LOCATION")
|
||||
.containsEntry("body.alarmFlag", "0")
|
||||
.containsEntry("body.statusFlag", "196608")
|
||||
.containsEntry("body.status.acc_on", "false")
|
||||
.containsEntry("body.status.load_status", "EMPTY")
|
||||
.containsEntry("body.status.door4_open", "true")
|
||||
.containsEntry("body.status.door5_open", "true")
|
||||
.containsEntry("body.alarm.overspeed", "false")
|
||||
.containsEntry("body.longitude", "116.397128")
|
||||
.containsEntry("body.latitude", "39.916527")
|
||||
.containsEntry("body.altitudeM", "50")
|
||||
.containsEntry("body.directionDeg", "90")
|
||||
.containsEntry("body.deviceTime", "2024-01-01T19:04:05Z")
|
||||
.containsEntry("body.extra.0x01", "00003039")
|
||||
.containsEntry("body.extra.mileage_km", "1234.5")
|
||||
.containsEntry("body.extra.0x30", "58")
|
||||
.containsEntry("body.extra.network_signal_strength", "88")
|
||||
.containsEntry("identityResolved", "true")
|
||||
.containsEntry("identitySource", "BOUND_PHONE");
|
||||
assertThat(Double.parseDouble(archive.metadata().get("body.speedKmh"))).isCloseTo(52.3, within(0.001));
|
||||
});
|
||||
|
||||
batchExecutor.close();
|
||||
eventBus.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerArchiveIncludesParsedBodyFieldsForRawQuery() throws Exception {
|
||||
RecordingSink sink = new RecordingSink(1);
|
||||
DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
|
||||
AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
|
||||
Dispatcher dispatcher = new Dispatcher(
|
||||
new HandlerRegistry(),
|
||||
new InterceptorChain(List.of()),
|
||||
new HandlerInvoker(),
|
||||
eventBus,
|
||||
batchExecutor);
|
||||
InMemoryVehicleIdentityService identity = new InMemoryVehicleIdentityService();
|
||||
identity.bind(new VehicleIdentityBinding(
|
||||
ProtocolId.JT808, "LNVIN000000000808", "123456789012", "DEV808", "B80808"));
|
||||
Jt808ChannelHandler handler = new Jt808ChannelHandler(
|
||||
new Jt808MessageDecoder(new BodyParserRegistry(List.of(new RegisterBodyParser()))),
|
||||
dispatcher,
|
||||
new InMemorySessionStore(),
|
||||
identity,
|
||||
new Jt808ChannelRegistry(),
|
||||
new Jt808PendingRequests());
|
||||
EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
byte[] frame = buildFrame(
|
||||
Jt808MessageId.TERMINAL_REGISTER,
|
||||
"123456789012",
|
||||
3,
|
||||
buildRegisterBody("DEV808", "B80808"));
|
||||
|
||||
channel.writeInbound(frame);
|
||||
|
||||
assertThat(sink.await()).isTrue();
|
||||
assertThat(sink.events).singleElement().satisfies(event -> {
|
||||
assertThat(event).isInstanceOf(VehicleEvent.RawArchive.class);
|
||||
VehicleEvent.RawArchive archive = (VehicleEvent.RawArchive) event;
|
||||
assertThat(archive.metadata())
|
||||
.containsEntry("messageId", "0x0100")
|
||||
.containsEntry("messageName", "TERMINAL_REGISTER")
|
||||
.containsEntry("reportType", "REGISTER")
|
||||
.containsEntry("body.province", "44")
|
||||
.containsEntry("body.city", "4401")
|
||||
.containsEntry("body.maker", "MAKER")
|
||||
.containsEntry("body.deviceType", "TYPE-A")
|
||||
.containsEntry("body.deviceId", "DEV808")
|
||||
.containsEntry("body.plateColor", "1")
|
||||
.containsEntry("body.plate", "B80808");
|
||||
});
|
||||
|
||||
batchExecutor.close();
|
||||
eventBus.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unboundRegisterCreatesVinBindingForRegisterAndFollowingRawFrames() throws Exception {
|
||||
RecordingSink sink = new RecordingSink(2);
|
||||
DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
|
||||
AsyncBatchExecutor batchExecutor = new AsyncBatchExecutor(eventBus::publish);
|
||||
Dispatcher dispatcher = new Dispatcher(
|
||||
new HandlerRegistry(),
|
||||
new InterceptorChain(List.of()),
|
||||
new HandlerInvoker(),
|
||||
eventBus,
|
||||
batchExecutor);
|
||||
InMemoryVehicleIdentityService identity = new InMemoryVehicleIdentityService();
|
||||
Jt808ChannelHandler handler = new Jt808ChannelHandler(
|
||||
new Jt808MessageDecoder(new BodyParserRegistry(List.of(new RegisterBodyParser(), new LocationBodyParser()))),
|
||||
dispatcher,
|
||||
new InMemorySessionStore(),
|
||||
identity,
|
||||
new Jt808ChannelRegistry(),
|
||||
new Jt808PendingRequests());
|
||||
EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
|
||||
channel.writeInbound(buildFrame(
|
||||
Jt808MessageId.TERMINAL_REGISTER,
|
||||
"13079963289",
|
||||
3,
|
||||
buildRegisterBody("9963289", "HUA00113F")));
|
||||
channel.writeInbound(buildFrame(
|
||||
Jt808MessageId.TERMINAL_LOCATION,
|
||||
"13079963289",
|
||||
4,
|
||||
buildLocationBody()));
|
||||
|
||||
assertThat(sink.await()).isTrue();
|
||||
VehicleEvent.RawArchive register = (VehicleEvent.RawArchive) sink.events.get(0);
|
||||
VehicleEvent.RawArchive location = (VehicleEvent.RawArchive) sink.events.get(1);
|
||||
assertThat(register.metadata())
|
||||
.containsEntry("reportType", "REGISTER")
|
||||
.containsEntry("body.deviceId", "9963289")
|
||||
.containsEntry("body.plate", "HUA00113F")
|
||||
.containsEntry("identityResolved", "true")
|
||||
.containsEntry("identitySource", "REGISTERED");
|
||||
assertThat(register.vin())
|
||||
.startsWith("JT808")
|
||||
.hasSize(17);
|
||||
assertThat(location.metadata())
|
||||
.containsEntry("reportType", "LOCATION")
|
||||
.containsEntry("vin", register.vin())
|
||||
.containsEntry("identityResolved", "true")
|
||||
.containsEntry("identitySource", "BOUND_PHONE");
|
||||
assertThat(location.vin()).isEqualTo(register.vin());
|
||||
assertThat(identity.resolve(new com.lingniu.ingest.identity.VehicleIdentityLookup(
|
||||
ProtocolId.JT808, "", "13079963289", "", "")).vin())
|
||||
.isEqualTo(register.vin());
|
||||
|
||||
batchExecutor.close();
|
||||
eventBus.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unboundLocationArchiveUsesUnknownVinInsteadOfPhoneFallback() throws Exception {
|
||||
RecordingSink sink = new RecordingSink(1);
|
||||
@@ -309,6 +439,9 @@ class Jt808ChannelHandlerTest {
|
||||
assertThat(archive.metadata())
|
||||
.containsEntry("vin", "unknown")
|
||||
.containsEntry("phone", "123456789012")
|
||||
.containsEntry("messageId", "0x0200")
|
||||
.containsEntry("messageName", "TERMINAL_LOCATION")
|
||||
.containsEntry("reportType", "LOCATION")
|
||||
.containsEntry("identityResolved", "false")
|
||||
.containsEntry("identitySource", "FALLBACK_PHONE");
|
||||
});
|
||||
@@ -351,13 +484,13 @@ class Jt808ChannelHandlerTest {
|
||||
VehicleEvent.RawArchive archive = (VehicleEvent.RawArchive) event;
|
||||
assertThat(archive.command()).isEqualTo(Jt808MessageId.TERMINAL_REGISTER);
|
||||
assertThat(archive.metadata())
|
||||
.containsEntry("jt808.register.province", "44")
|
||||
.containsEntry("jt808.register.city", "4401")
|
||||
.containsEntry("jt808.register.maker", "MAKER")
|
||||
.containsEntry("jt808.register.deviceType", "TYPE-A")
|
||||
.containsEntry("jt808.register.deviceId", "DEV808")
|
||||
.containsEntry("jt808.register.plateColor", "1")
|
||||
.containsEntry("jt808.register.plate", "B80808");
|
||||
.containsEntry("body.province", "44")
|
||||
.containsEntry("body.city", "4401")
|
||||
.containsEntry("body.maker", "MAKER")
|
||||
.containsEntry("body.deviceType", "TYPE-A")
|
||||
.containsEntry("body.deviceId", "DEV808")
|
||||
.containsEntry("body.plateColor", "1")
|
||||
.containsEntry("body.plate", "B80808");
|
||||
});
|
||||
|
||||
batchExecutor.close();
|
||||
@@ -581,6 +714,12 @@ class Jt808ChannelHandlerTest {
|
||||
writeU16(os, 90);
|
||||
byte[] bcd = BcdCodec.encode("240102030405");
|
||||
os.write(bcd, 0, 6);
|
||||
os.write(0x01);
|
||||
os.write(4);
|
||||
writeU32(os, 12345);
|
||||
os.write(0x30);
|
||||
os.write(1);
|
||||
os.write(0x58);
|
||||
return os.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ class Jt808EventMapperTest {
|
||||
assertThat(events.get(0).metadata())
|
||||
.containsEntry("vin", "unknown")
|
||||
.containsEntry("phone", "123456789012")
|
||||
.containsEntry("jt808.status.acc_on", "false")
|
||||
.containsEntry("jt808.status.positioned", "false")
|
||||
.containsEntry("jt808.status.load_status", "EMPTY")
|
||||
.containsEntry("jt808.alarm.overspeed", "false")
|
||||
.containsEntry("identityResolved", "false")
|
||||
.containsEntry("identitySource", "FALLBACK_PHONE");
|
||||
}
|
||||
@@ -58,7 +62,66 @@ class Jt808EventMapperTest {
|
||||
.extracting(VehicleEvent::metadata)
|
||||
.satisfies(meta -> assertThat(meta)
|
||||
.containsEntry("jt808.extra.0x01", "00003039")
|
||||
.containsEntry("jt808.extra.0x30", "58"));
|
||||
.containsEntry("jt808.extra.0x30", "58")
|
||||
.containsEntry("jt808.extra.mileage_km", "1234.5")
|
||||
.containsEntry("jt808.extra.network_signal_strength", "88"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void locationAdditionalItemsAreDecodedAsTable27Fields() {
|
||||
var header = new Jt808Header(
|
||||
Jt808MessageId.TERMINAL_LOCATION, 64, 0, false,
|
||||
Jt808Header.ProtocolVersion.V2013, "123456789012", 1, 0, 0);
|
||||
var body = new Jt808Body.Location(
|
||||
0, 0, 116.397128, 39.916527, 50, 52.3, 90,
|
||||
Instant.parse("2024-01-02T03:04:05Z"),
|
||||
java.util.Map.ofEntries(
|
||||
java.util.Map.entry(0x01, new byte[]{0x00, 0x00, 0x30, 0x39}),
|
||||
java.util.Map.entry(0x02, new byte[]{0x00, 0x64}),
|
||||
java.util.Map.entry(0x03, new byte[]{0x01, (byte) 0xF4}),
|
||||
java.util.Map.entry(0x04, new byte[]{0x00, 0x02}),
|
||||
java.util.Map.entry(0x05, tirePressureBytes()),
|
||||
java.util.Map.entry(0x06, new byte[]{(byte) 0x80, 0x05}),
|
||||
java.util.Map.entry(0x11, new byte[]{0x01, 0x00, 0x00, 0x00, 0x2A}),
|
||||
java.util.Map.entry(0x12, new byte[]{0x04, 0x00, 0x00, 0x00, 0x2B, 0x01}),
|
||||
java.util.Map.entry(0x13, new byte[]{0x00, 0x00, 0x00, 0x2C, 0x00, 0x78, 0x01}),
|
||||
java.util.Map.entry(0x25, new byte[]{0x00, 0x00, 0x00, 0x05}),
|
||||
java.util.Map.entry(0x2A, new byte[]{0x00, 0x03}),
|
||||
java.util.Map.entry(0x2B, new byte[]{0x12, 0x34, 0x56, 0x78}),
|
||||
java.util.Map.entry(0x30, new byte[]{0x1F}),
|
||||
java.util.Map.entry(0x31, new byte[]{0x0C})));
|
||||
|
||||
VehicleEvent.Location event = (VehicleEvent.Location) mapper.toEvents(new Jt808Message(header, body)).getFirst();
|
||||
|
||||
assertThat(event.payload().totalMileageKm()).isEqualTo(1234.5);
|
||||
assertThat(event.metadata())
|
||||
.containsEntry("jt808.extra.mileage_km", "1234.5")
|
||||
.containsEntry("jt808.extra.fuel_l", "10")
|
||||
.containsEntry("jt808.extra.recorder_speed_kmh", "50")
|
||||
.containsEntry("jt808.extra.manual_alarm_event_id", "2")
|
||||
.containsEntry("jt808.extra.tire_pressure_pa_values", "32,33")
|
||||
.containsEntry("jt808.extra.tire_pressure_invalid_indexes", "2-29")
|
||||
.containsEntry("jt808.extra.compartment_temp_c", "-5")
|
||||
.containsEntry("jt808.extra.overspeed.position_type", "1")
|
||||
.containsEntry("jt808.extra.overspeed.area_route_id", "42")
|
||||
.containsEntry("jt808.extra.area_route_alarm.position_type", "4")
|
||||
.containsEntry("jt808.extra.area_route_alarm.area_route_id", "43")
|
||||
.containsEntry("jt808.extra.area_route_alarm.direction", "1")
|
||||
.containsEntry("jt808.extra.route_time.segment_id", "44")
|
||||
.containsEntry("jt808.extra.route_time.duration_seconds", "120")
|
||||
.containsEntry("jt808.extra.route_time.result", "1")
|
||||
.containsEntry("jt808.extra.vehicle_signal_raw", "5")
|
||||
.containsEntry("jt808.extra.vehicle_signal.low_beam", "true")
|
||||
.containsEntry("jt808.extra.vehicle_signal.high_beam", "false")
|
||||
.containsEntry("jt808.extra.vehicle_signal.right_turn", "true")
|
||||
.containsEntry("jt808.extra.io_status_raw", "3")
|
||||
.containsEntry("jt808.extra.io_status.deep_sleep", "true")
|
||||
.containsEntry("jt808.extra.io_status.sleep", "true")
|
||||
.containsEntry("jt808.extra.analog_raw", "305419896")
|
||||
.containsEntry("jt808.extra.analog_ad0", "22136")
|
||||
.containsEntry("jt808.extra.analog_ad1", "4660")
|
||||
.containsEntry("jt808.extra.network_signal_strength", "31")
|
||||
.containsEntry("jt808.extra.gnss_satellite_count", "12");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -249,4 +312,12 @@ class Jt808EventMapperTest {
|
||||
.containsEntry("parseError", "true");
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] tirePressureBytes() {
|
||||
byte[] bytes = new byte[30];
|
||||
java.util.Arrays.fill(bytes, (byte) 0xFF);
|
||||
bytes[0] = 32;
|
||||
bytes[1] = 33;
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user