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,119 @@
|
||||
package com.lingniu.ingest.sink.mq;
|
||||
|
||||
import com.lingniu.ingest.api.event.AlarmPayload;
|
||||
import com.lingniu.ingest.api.event.LocationPayload;
|
||||
import com.lingniu.ingest.api.event.RealtimePayload;
|
||||
import com.lingniu.ingest.api.event.VehicleEvent;
|
||||
import com.lingniu.ingest.sink.mq.proto.VehicleEnvelope;
|
||||
|
||||
/**
|
||||
* 领域事件 → Protobuf Envelope 的纯函数映射。通过 switch 模式匹配穷尽处理,
|
||||
* 新增 {@link VehicleEvent} 子类型时编译期就会提示补齐分支。
|
||||
*/
|
||||
public final class EnvelopeMapper {
|
||||
|
||||
private static final String SCHEMA_VERSION = "1.0";
|
||||
private final String nodeId;
|
||||
|
||||
public EnvelopeMapper(String nodeId) {
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
|
||||
public VehicleEnvelope toEnvelope(VehicleEvent event) {
|
||||
VehicleEnvelope.Builder b = VehicleEnvelope.newBuilder()
|
||||
.setSchemaVersion(SCHEMA_VERSION)
|
||||
.setEventId(event.eventId())
|
||||
.setTraceId(event.traceId() == null ? "" : event.traceId())
|
||||
.setVin(event.vin())
|
||||
.setSource(event.source().name())
|
||||
.setEventTimeMs(event.eventTime().toEpochMilli())
|
||||
.setIngestTimeMs(event.ingestTime().toEpochMilli())
|
||||
.setIngestNodeId(nodeId);
|
||||
if (event.metadata() != null) b.putAllMetadata(event.metadata());
|
||||
|
||||
switch (event) {
|
||||
case VehicleEvent.Realtime r -> b.setRealtime(buildRealtime(r.payload()));
|
||||
case VehicleEvent.Location l -> b.setLocation(buildLocation(l.payload()));
|
||||
case VehicleEvent.Alarm a -> b.setAlarm(buildAlarm(a.payload()));
|
||||
case VehicleEvent.Login lg -> b.setLogin(
|
||||
com.lingniu.ingest.sink.mq.proto.LoginPayload.newBuilder()
|
||||
.setIccid(nullToEmpty(lg.iccid()))
|
||||
.setProtocolVersion(nullToEmpty(lg.protocolVersion()))
|
||||
.build());
|
||||
case VehicleEvent.Logout ignored -> b.setLogout(
|
||||
com.lingniu.ingest.sink.mq.proto.LogoutPayload.getDefaultInstance());
|
||||
case VehicleEvent.Heartbeat ignored -> b.setHeartbeat(
|
||||
com.lingniu.ingest.sink.mq.proto.HeartbeatPayload.getDefaultInstance());
|
||||
case VehicleEvent.MediaMeta m -> b.setMediaMeta(
|
||||
com.lingniu.ingest.sink.mq.proto.MediaMetaPayload.newBuilder()
|
||||
.setMediaId(nullToEmpty(m.mediaId()))
|
||||
.setMediaType(nullToEmpty(m.mediaType()))
|
||||
.setSizeBytes(m.sizeBytes())
|
||||
.setArchiveRef(nullToEmpty(m.archiveRef()))
|
||||
.build());
|
||||
case VehicleEvent.Passthrough p -> b.setPassthrough(
|
||||
com.lingniu.ingest.sink.mq.proto.PassthroughPayload.newBuilder()
|
||||
.setPassthroughType(p.passthroughType())
|
||||
.setData(com.google.protobuf.ByteString.copyFrom(
|
||||
p.data() == null ? new byte[0] : p.data()))
|
||||
.build());
|
||||
}
|
||||
return b.build();
|
||||
}
|
||||
|
||||
private static com.lingniu.ingest.sink.mq.proto.RealtimePayload buildRealtime(RealtimePayload p) {
|
||||
var b = com.lingniu.ingest.sink.mq.proto.RealtimePayload.newBuilder();
|
||||
if (p.speedKmh() != null) b.setSpeedKmh(p.speedKmh());
|
||||
if (p.totalMileageKm() != null) b.setTotalMileageKm(p.totalMileageKm());
|
||||
if (p.batterySoc() != null) b.setBatterySoc(p.batterySoc());
|
||||
if (p.batteryVoltageV() != null) b.setBatteryVoltageV(p.batteryVoltageV());
|
||||
if (p.batteryCurrentA() != null) b.setBatteryCurrentA(p.batteryCurrentA());
|
||||
if (p.fcVoltageV() != null) b.setFcVoltageV(p.fcVoltageV());
|
||||
if (p.fcCurrentA() != null) b.setFcCurrentA(p.fcCurrentA());
|
||||
if (p.fcTempC() != null) b.setFcTempC(p.fcTempC());
|
||||
if (p.hydrogenRemainingKg() != null) b.setHydrogenRemainingKg(p.hydrogenRemainingKg());
|
||||
if (p.hydrogenHighPressureMpa() != null) b.setHydrogenHighPressureMpa(p.hydrogenHighPressureMpa());
|
||||
if (p.hydrogenLowPressureMpa() != null) b.setHydrogenLowPressureMpa(p.hydrogenLowPressureMpa());
|
||||
if (p.vehicleState() != null) b.setVehicleState(p.vehicleState().name());
|
||||
if (p.chargingState() != null) b.setChargingState(p.chargingState().name());
|
||||
if (p.runningMode() != null) b.setRunningMode(p.runningMode().name());
|
||||
if (p.gearLevel() != null) b.setGearLevel(p.gearLevel());
|
||||
if (p.acceleratorPedal() != null) b.setAcceleratorPedal(p.acceleratorPedal());
|
||||
if (p.brakePedal() != null) b.setBrakePedal(p.brakePedal());
|
||||
if (p.longitude() != null) b.setLongitude(p.longitude());
|
||||
if (p.latitude() != null) b.setLatitude(p.latitude());
|
||||
if (p.altitudeM() != null) b.setAltitudeM(p.altitudeM());
|
||||
if (p.directionDeg() != null) b.setDirectionDeg(p.directionDeg());
|
||||
if (p.ambientTempC() != null) b.setAmbientTempC(p.ambientTempC());
|
||||
if (p.coolantTempC() != null) b.setCoolantTempC(p.coolantTempC());
|
||||
return b.build();
|
||||
}
|
||||
|
||||
private static com.lingniu.ingest.sink.mq.proto.LocationPayload buildLocation(LocationPayload p) {
|
||||
return com.lingniu.ingest.sink.mq.proto.LocationPayload.newBuilder()
|
||||
.setLongitude(p.longitude())
|
||||
.setLatitude(p.latitude())
|
||||
.setAltitudeM(p.altitudeM())
|
||||
.setSpeedKmh(p.speedKmh())
|
||||
.setDirectionDeg(p.directionDeg())
|
||||
.setAlarmFlag(p.alarmFlag())
|
||||
.setStatusFlag(p.statusFlag())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static com.lingniu.ingest.sink.mq.proto.AlarmPayload buildAlarm(AlarmPayload p) {
|
||||
var b = com.lingniu.ingest.sink.mq.proto.AlarmPayload.newBuilder()
|
||||
.setLevel(p.level().name())
|
||||
.setAlarmTypeCode(p.alarmTypeCode())
|
||||
.setAlarmTypeName(nullToEmpty(p.alarmTypeName()));
|
||||
if (p.faultCodes() != null) b.addAllFaultCodes(p.faultCodes());
|
||||
if (p.activeBits() != null) b.addAllActiveBits(p.activeBits());
|
||||
if (p.longitude() != null) b.setLongitude(p.longitude());
|
||||
if (p.latitude() != null) b.setLatitude(p.latitude());
|
||||
return b.build();
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.lingniu.ingest.sink.mq;
|
||||
|
||||
import com.lingniu.ingest.api.event.VehicleEvent;
|
||||
import com.lingniu.ingest.api.sink.EventSink;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
import org.apache.kafka.clients.producer.KafkaProducer;
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Kafka Sink:序列化为 Protobuf Envelope,按 vin 分区,异步发送。
|
||||
*
|
||||
* <p>失败处理:Resilience4j 熔断;熔断开启时事件转投 DLQ topic。
|
||||
*/
|
||||
public final class KafkaEventSink implements EventSink, AutoCloseable {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(KafkaEventSink.class);
|
||||
|
||||
private final KafkaProducer<String, byte[]> producer;
|
||||
private final EnvelopeMapper mapper;
|
||||
private final TopicRouter router;
|
||||
private final String dlqTopic;
|
||||
private final CircuitBreaker breaker;
|
||||
|
||||
public KafkaEventSink(KafkaProducer<String, byte[]> producer,
|
||||
EnvelopeMapper mapper,
|
||||
TopicRouter router,
|
||||
String dlqTopic,
|
||||
CircuitBreaker breaker) {
|
||||
this.producer = producer;
|
||||
this.mapper = mapper;
|
||||
this.router = router;
|
||||
this.dlqTopic = dlqTopic;
|
||||
this.breaker = breaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "kafka";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> publish(VehicleEvent event) {
|
||||
CompletableFuture<Void> cf = new CompletableFuture<>();
|
||||
byte[] payload;
|
||||
try {
|
||||
payload = mapper.toEnvelope(event).toByteArray();
|
||||
} catch (Exception e) {
|
||||
log.error("envelope build failed eventId={} vin={}", event.eventId(), event.vin(), e);
|
||||
cf.completeExceptionally(e);
|
||||
return cf;
|
||||
}
|
||||
|
||||
String topic = breaker.tryAcquirePermission() ? router.route(event) : dlqTopic;
|
||||
ProducerRecord<String, byte[]> record = new ProducerRecord<>(topic, event.vin(), payload);
|
||||
record.headers().add("event-id", event.eventId().getBytes());
|
||||
record.headers().add("trace-id", event.traceId() == null ? new byte[0] : event.traceId().getBytes());
|
||||
record.headers().add("source", event.source().name().getBytes());
|
||||
|
||||
producer.send(record, (metadata, ex) -> {
|
||||
if (ex != null) {
|
||||
breaker.onError(0, java.util.concurrent.TimeUnit.MILLISECONDS, ex);
|
||||
cf.completeExceptionally(ex);
|
||||
} else {
|
||||
breaker.onSuccess(0, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
cf.complete(null);
|
||||
}
|
||||
});
|
||||
return cf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
producer.flush();
|
||||
producer.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.lingniu.ingest.sink.mq;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
|
||||
import org.apache.kafka.clients.producer.KafkaProducer;
|
||||
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||
import org.apache.kafka.common.serialization.ByteArraySerializer;
|
||||
import org.apache.kafka.common.serialization.StringSerializer;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* MQ Sink 自动装配。
|
||||
*
|
||||
* <p>装配前提(两个条件都要满足):
|
||||
* <ol>
|
||||
* <li>{@code lingniu.ingest.sink.mq.enabled=true}(默认 true)—— <b>总开关</b>,
|
||||
* 设为 false 时本模块完全不装配,ingest-core 的 DisruptorEventBus 仍然运行但
|
||||
* 没有 Kafka sink。
|
||||
* <li>{@code lingniu.ingest.sink.mq.type=kafka}(默认 kafka)—— 后端选择,预留
|
||||
* rocketmq/pulsar 等扩展。
|
||||
* </ol>
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(SinkMqProperties.class)
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.sink.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class SinkMqAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public EnvelopeMapper envelopeMapper(SinkMqProperties props) {
|
||||
return new EnvelopeMapper(props.getNodeId());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TopicRouter topicRouter(SinkMqProperties props) {
|
||||
return new TopicRouter(props.getTopics());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.sink.mq", name = "type", havingValue = "kafka", matchIfMissing = true)
|
||||
public CircuitBreaker kafkaSinkCircuitBreaker() {
|
||||
return CircuitBreaker.of("kafka-sink", CircuitBreakerConfig.custom()
|
||||
.slidingWindowSize(100)
|
||||
.failureRateThreshold(50)
|
||||
.waitDurationInOpenState(Duration.ofSeconds(10))
|
||||
.permittedNumberOfCallsInHalfOpenState(10)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.sink.mq", name = "type", havingValue = "kafka", matchIfMissing = true)
|
||||
public KafkaProducer<String, byte[]> kafkaProducer(SinkMqProperties props) {
|
||||
Properties p = new Properties();
|
||||
p.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, props.getBootstrapServers());
|
||||
p.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
|
||||
p.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getName());
|
||||
p.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, props.getCompressionType());
|
||||
p.put(ProducerConfig.LINGER_MS_CONFIG, props.getLingerMs());
|
||||
p.put(ProducerConfig.BATCH_SIZE_CONFIG, props.getBatchSize());
|
||||
p.put(ProducerConfig.ACKS_CONFIG, props.getAcks());
|
||||
p.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, props.isEnableIdempotence());
|
||||
return new KafkaProducer<>(p);
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.sink.mq", name = "type", havingValue = "kafka", matchIfMissing = true)
|
||||
public KafkaEventSink kafkaEventSink(KafkaProducer<String, byte[]> producer,
|
||||
EnvelopeMapper mapper,
|
||||
TopicRouter router,
|
||||
SinkMqProperties props,
|
||||
CircuitBreaker breaker) {
|
||||
return new KafkaEventSink(producer, mapper, router, props.getTopics().getDlq(), breaker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.lingniu.ingest.sink.mq;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "lingniu.ingest.sink.mq")
|
||||
public class SinkMqProperties {
|
||||
|
||||
/**
|
||||
* MQ Sink 总开关。默认 {@code true}。
|
||||
* 设为 {@code false} 时 {@link SinkMqAutoConfiguration} 完全不装配任何 Bean(Kafka Producer、
|
||||
* EnvelopeMapper、TopicRouter、KafkaEventSink 都不会创建),ingest-core 的 DisruptorEventBus
|
||||
* 仍然正常运行但没有外部 sink(事件落地到 sink-archive 或 Noop 吞掉)。
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/** MQ 后端类型。目前仅支持 kafka,预留 rocketmq/pulsar 等。 */
|
||||
private String type = "kafka";
|
||||
private String bootstrapServers = "localhost:9092";
|
||||
private String compressionType = "zstd";
|
||||
private int lingerMs = 20;
|
||||
private int batchSize = 65536;
|
||||
private String acks = "all";
|
||||
private boolean enableIdempotence = true;
|
||||
private String nodeId = "ingest-local";
|
||||
private Topics topics = new Topics();
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
public String getBootstrapServers() { return bootstrapServers; }
|
||||
public void setBootstrapServers(String bootstrapServers) { this.bootstrapServers = bootstrapServers; }
|
||||
public String getCompressionType() { return compressionType; }
|
||||
public void setCompressionType(String compressionType) { this.compressionType = compressionType; }
|
||||
public int getLingerMs() { return lingerMs; }
|
||||
public void setLingerMs(int lingerMs) { this.lingerMs = lingerMs; }
|
||||
public int getBatchSize() { return batchSize; }
|
||||
public void setBatchSize(int batchSize) { this.batchSize = batchSize; }
|
||||
public String getAcks() { return acks; }
|
||||
public void setAcks(String acks) { this.acks = acks; }
|
||||
public boolean isEnableIdempotence() { return enableIdempotence; }
|
||||
public void setEnableIdempotence(boolean enableIdempotence) { this.enableIdempotence = enableIdempotence; }
|
||||
public String getNodeId() { return nodeId; }
|
||||
public void setNodeId(String nodeId) { this.nodeId = nodeId; }
|
||||
public Topics getTopics() { return topics; }
|
||||
public void setTopics(Topics topics) { this.topics = topics; }
|
||||
|
||||
public static class Topics {
|
||||
private String realtime = "vehicle.realtime";
|
||||
private String location = "vehicle.location";
|
||||
private String alarm = "vehicle.alarm";
|
||||
private String session = "vehicle.session";
|
||||
private String mediaMeta = "vehicle.media.meta";
|
||||
private String rawArchive = "vehicle.raw.archive";
|
||||
private String dlq = "vehicle.dlq";
|
||||
|
||||
public String getRealtime() { return realtime; }
|
||||
public void setRealtime(String realtime) { this.realtime = realtime; }
|
||||
public String getLocation() { return location; }
|
||||
public void setLocation(String location) { this.location = location; }
|
||||
public String getAlarm() { return alarm; }
|
||||
public void setAlarm(String alarm) { this.alarm = alarm; }
|
||||
public String getSession() { return session; }
|
||||
public void setSession(String session) { this.session = session; }
|
||||
public String getMediaMeta() { return mediaMeta; }
|
||||
public void setMediaMeta(String mediaMeta) { this.mediaMeta = mediaMeta; }
|
||||
public String getRawArchive() { return rawArchive; }
|
||||
public void setRawArchive(String rawArchive) { this.rawArchive = rawArchive; }
|
||||
public String getDlq() { return dlq; }
|
||||
public void setDlq(String dlq) { this.dlq = dlq; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.lingniu.ingest.sink.mq;
|
||||
|
||||
import com.lingniu.ingest.api.event.VehicleEvent;
|
||||
|
||||
/**
|
||||
* 事件 → Kafka topic 路由。集中管理避免散落。
|
||||
*/
|
||||
public final class TopicRouter {
|
||||
|
||||
private final SinkMqProperties.Topics topics;
|
||||
|
||||
public TopicRouter(SinkMqProperties.Topics topics) {
|
||||
this.topics = topics;
|
||||
}
|
||||
|
||||
public String route(VehicleEvent event) {
|
||||
return switch (event) {
|
||||
case VehicleEvent.Realtime _ -> topics.getRealtime();
|
||||
case VehicleEvent.Location _ -> topics.getLocation();
|
||||
case VehicleEvent.Alarm _ -> topics.getAlarm();
|
||||
case VehicleEvent.Login _,
|
||||
VehicleEvent.Logout _,
|
||||
VehicleEvent.Heartbeat _ -> topics.getSession();
|
||||
case VehicleEvent.MediaMeta _ -> topics.getMediaMeta();
|
||||
case VehicleEvent.Passthrough _ -> topics.getAlarm();
|
||||
};
|
||||
}
|
||||
}
|
||||
110
sink-mq/src/main/proto/vehicle_envelope.proto
Normal file
110
sink-mq/src/main/proto/vehicle_envelope.proto
Normal file
@@ -0,0 +1,110 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package com.lingniu.ingest.sink.mq.proto;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "com.lingniu.ingest.sink.mq.proto";
|
||||
option java_outer_classname = "VehicleEnvelopeProto";
|
||||
|
||||
// 统一消息外壳:所有 Topic 共用此 Envelope,payload 通过 oneof 区分具体事件类型。
|
||||
message VehicleEnvelope {
|
||||
string schema_version = 1;
|
||||
string event_id = 2;
|
||||
string trace_id = 3;
|
||||
string vin = 4;
|
||||
string source = 5; // ProtocolId 字符串形式
|
||||
string protocol_version = 6;
|
||||
int64 event_time_ms = 7;
|
||||
int64 ingest_time_ms = 8;
|
||||
string ingest_node_id = 9;
|
||||
map<string, string> metadata = 10;
|
||||
|
||||
oneof payload {
|
||||
RealtimePayload realtime = 20;
|
||||
LocationPayload location = 21;
|
||||
AlarmPayload alarm = 22;
|
||||
LoginPayload login = 23;
|
||||
LogoutPayload logout = 24;
|
||||
HeartbeatPayload heartbeat = 25;
|
||||
MediaMetaPayload media_meta = 26;
|
||||
PassthroughPayload passthrough = 27;
|
||||
}
|
||||
|
||||
// 可选:原始字节指针
|
||||
RawArchiveRef raw_archive = 40;
|
||||
}
|
||||
|
||||
message RawArchiveRef {
|
||||
string uri = 1;
|
||||
string checksum = 2;
|
||||
int64 size_bytes = 3;
|
||||
}
|
||||
|
||||
message RealtimePayload {
|
||||
optional double speed_kmh = 1;
|
||||
optional double total_mileage_km = 2;
|
||||
optional double battery_soc = 3;
|
||||
optional double battery_voltage_v = 4;
|
||||
optional double battery_current_a = 5;
|
||||
optional double fc_voltage_v = 6;
|
||||
optional double fc_current_a = 7;
|
||||
optional double fc_temp_c = 8;
|
||||
optional double hydrogen_remaining_kg = 9;
|
||||
optional double hydrogen_high_pressure_mpa = 10;
|
||||
optional double hydrogen_low_pressure_mpa = 11;
|
||||
optional string vehicle_state = 12;
|
||||
optional string charging_state = 13;
|
||||
optional string running_mode = 14;
|
||||
optional int32 gear_level = 15;
|
||||
optional double accelerator_pedal = 16;
|
||||
optional double brake_pedal = 17;
|
||||
optional double longitude = 18;
|
||||
optional double latitude = 19;
|
||||
optional double altitude_m = 20;
|
||||
optional double direction_deg = 21;
|
||||
optional double ambient_temp_c = 22;
|
||||
optional double coolant_temp_c = 23;
|
||||
}
|
||||
|
||||
message LocationPayload {
|
||||
double longitude = 1;
|
||||
double latitude = 2;
|
||||
double altitude_m = 3;
|
||||
double speed_kmh = 4;
|
||||
double direction_deg = 5;
|
||||
int64 alarm_flag = 6;
|
||||
int64 status_flag = 7;
|
||||
}
|
||||
|
||||
message AlarmPayload {
|
||||
string level = 1;
|
||||
int32 alarm_type_code = 2;
|
||||
string alarm_type_name = 3;
|
||||
repeated string fault_codes = 4;
|
||||
optional double longitude = 5;
|
||||
optional double latitude = 6;
|
||||
// 通用报警标志位(2016 版 0~15 / 2025 版 0~27),对照 GB/T 32960.3 表 24。
|
||||
// 例:SOC_LOW / BATTERY_HIGH_TEMP / HYDROGEN_LEAK
|
||||
repeated string active_bits = 7;
|
||||
}
|
||||
|
||||
message LoginPayload {
|
||||
string iccid = 1;
|
||||
string protocol_version = 2;
|
||||
}
|
||||
|
||||
message LogoutPayload {}
|
||||
|
||||
message HeartbeatPayload {}
|
||||
|
||||
message MediaMetaPayload {
|
||||
string media_id = 1;
|
||||
string media_type = 2;
|
||||
int64 size_bytes = 3;
|
||||
string archive_ref = 4;
|
||||
}
|
||||
|
||||
message PassthroughPayload {
|
||||
int32 passthrough_type = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.lingniu.ingest.sink.mq.SinkMqAutoConfiguration
|
||||
Reference in New Issue
Block a user