feat(gb32960): wire vendor parser profile selector into decoder pipeline
Final piece that lets the vendor extension framework introduced in0e6f120actually take effect on real frames. Decoder (Gb32960MessageDecoder): - Adds an overload decode(ByteBuffer, String platformAccount) alongside the existing decode(ByteBuffer). Realtime/resend frames build a Gb32960ParserContext(version, vin, platformAccount) and pass it to bodyParser.parse(ctx, body) so the selector can pick a vendor profile. - The legacy decode(ByteBuffer) delegates with a null account and is preserved for tests and any caller that doesn't care about routing. Channel handler (Gb32960ChannelHandler): - Defines AttributeKey<String> PLATFORM_ACCOUNT_ATTR. - On a successful 0x05 PLATFORM_LOGIN, stores the authenticated username on ctx.channel().attr(PLATFORM_ACCOUNT_ATTR). - On every channelRead0, reads the attribute (null for vehicle-direct connections) and passes it to decoder.decode(buffer, account). Autoconfig (Gb32960AutoConfiguration): - Replaces the old single-registry wiring with three new beans: * VendorExtensionCatalog with the built-in `guangdong-fc` entry (the 6 GdFc* parsers from957a86d) * Gb32960ProfileRegistry, built from the union of all standard InfoBlockParser beans plus, for each entry referenced in lingniu.ingest.gb32960.vendor-extensions, a per-extension registry that adds the catalog parsers on top of the defaults * VendorExtensionSelector — RuleBasedVendorExtensionSelector when vendor-extensions is non-empty, otherwise VendorExtensionSelector.NONE - gb32960BodyParser now takes (Gb32960ProfileRegistry, VendorExtensionSelector). Net effect: with the following yml fragment, lingniu account or any VIN starting with LNVFC routes to the guangdong-fc profile and the previously "unknown 0x30+" warns become real GdFc* InfoBlock entries: lingniu.ingest.gb32960.vendor-extensions: - name: guangdong-fc match: platform-accounts: [lingniu] vin-prefixes: [LNVFC, LZG] All existing 18 tests + 12 new tests (selector + vendor parsers) pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,15 @@ public final class Gb32960MessageDecoder implements FrameDecoder<Gb32960Message>
|
||||
|
||||
@Override
|
||||
public Gb32960Message decode(ByteBuffer buffer) {
|
||||
return decode(buffer, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带 platformAccount 提示的解码。Handler 在已经完成 0x05 PLATFORM_LOGIN 鉴权后通过
|
||||
* channel attribute 取到账号名再传进来;以便 BodyParser 内部的 vendor profile selector
|
||||
* 路由。其他命令传 {@code null} 即可。
|
||||
*/
|
||||
public Gb32960Message decode(ByteBuffer buffer, String platformAccount) {
|
||||
int frameLen = buffer.remaining();
|
||||
if (frameLen < MIN_FRAME) {
|
||||
throw new DecodeException("frame too short: " + frameLen);
|
||||
@@ -103,7 +112,8 @@ public final class Gb32960MessageDecoder implements FrameDecoder<Gb32960Message>
|
||||
if (dataLength < 6) throw new DecodeException("report body missing timestamp");
|
||||
Instant eventTime = parseTimestamp(frame, bodyStart);
|
||||
ByteBuffer body = ByteBuffer.wrap(frame, bodyStart + 6, dataLength - 6);
|
||||
BodyParseResult result = bodyParser.parse(version, body);
|
||||
Gb32960ParserContext ctx = new Gb32960ParserContext(version, vin, platformAccount);
|
||||
BodyParseResult result = bodyParser.parse(ctx, body);
|
||||
Gb32960Header header = new Gb32960Header(
|
||||
version, cmdType, rspFlag, vin, encType, dataLength, eventTime);
|
||||
return new Gb32960Message(header, result.blocks(), null, result.signature());
|
||||
|
||||
@@ -28,6 +28,16 @@ import com.lingniu.ingest.protocol.gb32960.codec.parser.v2025.PositionV2025Block
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.v2025.SuperCapacitorExtremeV2025BlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.v2025.SuperCapacitorV2025BlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.v2025.VehicleV2025BlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcAirConditionerBlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcAuxiliaryBlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcDcDcBlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcDemoExtensionBlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcStackBlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.parser.vendor.guangdong.GdFcVehicleInfoBlockParser;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.profile.Gb32960ProfileRegistry;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.profile.RuleBasedVendorExtensionSelector;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.profile.VendorExtensionCatalog;
|
||||
import com.lingniu.ingest.protocol.gb32960.codec.profile.VendorExtensionSelector;
|
||||
import com.lingniu.ingest.protocol.gb32960.handler.Gb32960RealtimeHandler;
|
||||
import com.lingniu.ingest.protocol.gb32960.inbound.Gb32960NettyServer;
|
||||
import com.lingniu.ingest.protocol.gb32960.mapper.Gb32960EventMapper;
|
||||
@@ -79,18 +89,63 @@ public class Gb32960AutoConfiguration {
|
||||
@Bean public InfoBlockParser gb32960V2025SuperCap() { return new SuperCapacitorV2025BlockParser(); }
|
||||
@Bean public InfoBlockParser gb32960V2025SuperCapExtreme() { return new SuperCapacitorExtremeV2025BlockParser(); }
|
||||
|
||||
// ================= Core components =================
|
||||
// ================= Vendor 扩展套件目录 =================
|
||||
|
||||
/**
|
||||
* 内置 vendor 扩展套件目录。当前注册:
|
||||
* <ul>
|
||||
* <li>{@code guangdong-fc} —— 广东燃料电池汽车示范规范 0x30~0x34、0x80
|
||||
* </ul>
|
||||
* 未来新增其他厂商扩展时直接在这里加 entry。
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public InfoBlockParserRegistry gb32960InfoBlockParserRegistry(List<InfoBlockParser> parsers) {
|
||||
return new InfoBlockParserRegistry(parsers);
|
||||
public VendorExtensionCatalog gb32960VendorExtensionCatalog() {
|
||||
return new VendorExtensionCatalog(java.util.Map.of(
|
||||
"guangdong-fc", List.of(
|
||||
new GdFcStackBlockParser(),
|
||||
new GdFcAuxiliaryBlockParser(),
|
||||
new GdFcDcDcBlockParser(),
|
||||
new GdFcAirConditionerBlockParser(),
|
||||
new GdFcVehicleInfoBlockParser(),
|
||||
new GdFcDemoExtensionBlockParser())));
|
||||
}
|
||||
|
||||
// ================= Core components =================
|
||||
|
||||
/**
|
||||
* Profile 注册表:把全部已注入的标准 parser 作为 default profile,
|
||||
* 同时按配置 {@code lingniu.ingest.gb32960.vendor-extensions} 里被引用的 vendor 套件名
|
||||
* 分别构建一份 "default + vendor" 的 registry。
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Gb32960ProfileRegistry gb32960ProfileRegistry(List<InfoBlockParser> parsers,
|
||||
VendorExtensionCatalog catalog,
|
||||
Gb32960Properties props) {
|
||||
java.util.Set<String> enabled = new java.util.LinkedHashSet<>();
|
||||
for (Gb32960Properties.VendorExtension e : props.getVendorExtensions()) {
|
||||
if (e.getName() != null && !e.getName().isBlank()) enabled.add(e.getName());
|
||||
}
|
||||
return new Gb32960ProfileRegistry(parsers, catalog, enabled);
|
||||
}
|
||||
|
||||
/** Vendor 扩展选择器:按配置 list 顺序首匹。 */
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public VendorExtensionSelector gb32960VendorExtensionSelector(Gb32960Properties props,
|
||||
VendorExtensionCatalog catalog) {
|
||||
if (props.getVendorExtensions() == null || props.getVendorExtensions().isEmpty()) {
|
||||
return VendorExtensionSelector.NONE;
|
||||
}
|
||||
return new RuleBasedVendorExtensionSelector(props.getVendorExtensions(), catalog.names());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Gb32960BodyParser gb32960BodyParser(InfoBlockParserRegistry registry) {
|
||||
return new Gb32960BodyParser(registry);
|
||||
public Gb32960BodyParser gb32960BodyParser(Gb32960ProfileRegistry profileRegistry,
|
||||
VendorExtensionSelector selector) {
|
||||
return new Gb32960BodyParser(profileRegistry, selector);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -19,6 +19,7 @@ import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -47,6 +48,14 @@ public class Gb32960ChannelHandler extends SimpleChannelInboundHandler<byte[]> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Gb32960ChannelHandler.class);
|
||||
|
||||
/**
|
||||
* 0x05 PLATFORM_LOGIN 鉴权通过后写入;后续 0x02/0x03 实时上报帧从 channel attribute 取出
|
||||
* 作为 {@code Gb32960ParserContext.platformAccount},供 vendor profile selector 路由。
|
||||
* 普通车端直连场景(0x01 VEHICLE_LOGIN)此 attribute 始终为 null。
|
||||
*/
|
||||
public static final AttributeKey<String> PLATFORM_ACCOUNT_ATTR =
|
||||
AttributeKey.valueOf("gb32960.platformAccount");
|
||||
|
||||
/** 仅用于调试:把解析后的 Gb32960Message 序列化成 JSON 打到日志。 */
|
||||
private static final ObjectMapper DEBUG_JSON = new ObjectMapper()
|
||||
.registerModule(new JavaTimeModule())
|
||||
@@ -94,7 +103,9 @@ public class Gb32960ChannelHandler extends SimpleChannelInboundHandler<byte[]> {
|
||||
protected void channelRead0(ChannelHandlerContext ctx, byte[] frame) {
|
||||
Gb32960Message msg;
|
||||
try {
|
||||
msg = decoder.decode(ByteBuffer.wrap(frame));
|
||||
String platformAccount = ctx.channel().hasAttr(PLATFORM_ACCOUNT_ATTR)
|
||||
? ctx.channel().attr(PLATFORM_ACCOUNT_ATTR).get() : null;
|
||||
msg = decoder.decode(ByteBuffer.wrap(frame), platformAccount);
|
||||
} catch (Exception e) {
|
||||
log.warn("[gb32960] decode failed peer={} len={}", addr(ctx), frame.length, e);
|
||||
return;
|
||||
@@ -164,6 +175,8 @@ public class Gb32960ChannelHandler extends SimpleChannelInboundHandler<byte[]> {
|
||||
}
|
||||
log.info("[gb32960] platform login peer={} username={} encryptRule={} serial={} time={} policy={}",
|
||||
addr(ctx), pl.username(), pl.encryptRule(), pl.serialNo(), pl.loginTime(), result);
|
||||
// 把 username 钉到 channel attribute,供后续 0x02/0x03 帧的 vendor profile 路由
|
||||
ctx.channel().attr(PLATFORM_ACCOUNT_ATTR).set(pl.username());
|
||||
byte[] ack = Gb32960FrameEncoder.buildResponse(
|
||||
msg.header().protocolVersion(),
|
||||
CommandType.PLATFORM_LOGIN,
|
||||
|
||||
Reference in New Issue
Block a user