Commit Graph

10 Commits

Author SHA1 Message Date
kkfluous
9cbc19578b test(gb32960): red — isolation test suite for body parser block failures
4 个测试覆盖:
- fixedLengthBlockFailure_isIsolated_subsequentBlocksStillParsed
- truncatedFixedLengthBlock_isWrappedAsRaw_loopTerminates
- variableLengthBlockFailure_swallowsRemainderAsRaw_loopBreaks
- strictMode_throwsOnAnyBlockFailure(当前行为已经抛异常,即通过)

外加在 Gb32960BodyParser 上加了 lenientBlockFailure 字段 + setter 的 stub(默认 true,
尚未接入主循环逻辑;行为变化由下一个 commit 实现),确保测试文件能编译。

当前运行:Tests run: 4, Errors: 3(3 个隔离场景未实现),strictMode PASS。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:13:53 +08:00
kkfluous
fa552f4b20 config(gb32960): add parse.lenientBlockFailure toggle
新增 lingniu.ingest.gb32960.parse.lenientBlockFailure 配置开关(默认 true),
为 Gb32960BodyParser 的单块异常隔离行为做回退开关。实现在后续 commit。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:12:16 +08:00
lingniu-dev
d8279ac3b7 chore(gb32960): demote high-frequency report/heartbeat ack logs to DEBUG
Every 0x02/0x03/0x07 ack produced an INFO line that, at one report per
vehicle every few seconds across many vehicles, drowns out meaningful
events in the log stream.

writeAck now checks the tag string: tags starting with "report-ack-" or
"heartbeat-ack" log their success at DEBUG (guarded by isDebugEnabled to
avoid building the hex dump string when DEBUG is off). Sparse ack tags
-- vehicle-login-ack / vehicle-logout-ack / platform-login-ack /
platform-logout-ack / time-calibration-ack -- keep INFO for audit.
Flush *failure* is still WARN for every tag so problems are never
silenced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:43:52 +08:00
lingniu-dev
e8150114df feat(gb32960/vendor): add TLV catch-all parser for Guangdong peer 0x83
Same peer that uses the Guangdong fuel-cell extension also emits a
proprietary 0x83 block that is NOT in the published v1.0 spec table 8.
Empirically it follows a TLV layout: type(1) + length(2) + value(N).

Adds:
- InfoBlock.GuangdongFc.VendorTlv(typeCode, declaredLength, payload)
  record + permits clause + InfoBlockType.GD_FC_VENDOR_TLV enum entry.
- GdFcVendorTlvBlockParser: typeCode injected via constructor, reads
  2B big-endian length then exactly that many payload bytes. Defends
  against declaredLength > remaining by truncating to remaining.
- AutoConfig: registers `new GdFcVendorTlvBlockParser(0x83)` under the
  guangdong-fc catalog entry. Future unknown vendor typeCodes can be
  added by inserting more instances; no new class required.

Critical correctness property — the parser is strictly bounded by the
length field, so BodyParser's main loop continues processing blocks
that come AFTER the TLV instead of having them swallowed by the
old generic Raw fallback. Three unit tests cover this:
- parsesPayloadOfDeclaredLengthExactly: round-trip a 5-byte payload
- doesNotSwallowSubsequentBlocksWhenChainedInBodyParser: feeds a body
  containing 0x83 TLV(4B) followed by a 0x01 standard Vehicle block;
  asserts both blocks are parsed
- truncatesPayloadIfDeclaredLengthExceedsRemaining: defensive case

Test count: 32 -> 35.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:13:46 +08:00
lingniu-dev
184963e1c2 test(gb32960): end-to-end test for Guangdong fuel-cell vendor profile
Adds GuangdongFcEndToEndTest covering the full vendor extension pipeline:
build a synthetic V2016 realtime frame containing standard blocks
(0x01/0x05/0x07/0x08/0x09) plus the Guangdong-specific 0x30 stack (with
4 sample cell voltages), 0x32 DC/DC, 0x33 air conditioner, 0x34 vehicle
info, and 0x80 demo extension. Decodes it through Gb32960MessageDecoder
with a hand-wired BodyParser whose Gb32960ProfileRegistry knows about
the guangdong-fc catalog entry and a RuleBasedVendorExtensionSelector
matching platformAccount=lingniu.

Two cases:
- parsesRealtimeFrameWithGuangdongFcExtensions: passes "lingniu" as the
  platform account, asserts each GuangdongFc.* record is present and
  field values match the synthesized bytes, and that no Raw fallback
  is produced.
- unmatchedAccountFallsBackToDefaultAndProducesRawForVendorBlocks: passes
  null as the account, asserts standard blocks still parse but the
  vendor 0x30 segment now lands in InfoBlock.Raw via the default-profile
  unknown-typeCode path. This pins down the selector's gate behavior.

Test count: 30 -> 32. All green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:56:04 +08:00
lingniu-dev
dd8f75e0da refactor(gb32960/model): nest InfoBlock by <protocol+version>.<dataType>
Restructure InfoBlock from a flat sealed interface into three nested
sealed interfaces grouped by protocol and version, so every record's
fully-qualified name follows the rule InfoBlock.<protocolVersion>.<dataType>
and the type system enforces strict separation between version variants.

New shape:

  InfoBlock (sealed)
  ├── Gb32960V2016 (sealed)
  │   ├── Vehicle           (20 B, has accel/brake pedal)
  │   ├── DriveMotor        (rpm offset 20000, torque 2 B)
  │   ├── FuelCell          (with N x 1B probe temps)
  │   ├── Engine            (5 B fixed)
  │   ├── Position          (9 B, no coord system)
  │   ├── Extreme           (14 B, 2016-only)
  │   ├── Alarm             (typeCode 0x07, no general alarm levels)
  │   ├── Voltage           (0x08 storage voltage)
  │   └── Temperature       (0x09 storage temperature)
  ├── Gb32960V2025 (sealed)
  │   ├── Vehicle           (18 B, no pedal fields)
  │   ├── DriveMotor        (rpm offset 32000, torque 4 B)
  │   ├── FuelCell          (no current/voltage, no probe list)
  │   ├── Engine            (5 B fixed, identical layout to V2016 but
  │                          duplicated to keep version separation strict)
  │   ├── Position          (10 B, with coordSystem byte)
  │   ├── Alarm             (typeCode 0x06, with generalAlarmLevels)
  │   ├── MinParallelVoltage (0x07 in V2025)
  │   ├── BatteryTemperature (0x08 in V2025)
  │   ├── FuelCellStack     (0x30)
  │   ├── SuperCapacitor    (0x31)
  │   └── SuperCapacitorExtreme (0x32)
  ├── GuangdongFc (sealed)
  │   ├── Stack             (0x30, vendor variant — different field order
  │                          and adds max/min/avg single-cell voltage stats)
  │   ├── Auxiliary         (0x31, air compressor / hydrogen pump / PTC)
  │   ├── DcDc              (0x32 fixed 9 B)
  │   ├── AirConditioner    (0x33 fixed 5 B)
  │   ├── VehicleInfo       (0x34 fixed 7 B)
  │   └── DemoExtension     (0x80 demo extension)
  └── Raw

Even types that share an identical field list (Engine, the two
DriveMotor.Motor inner records, Vehicle when ignoring pedals) are
deliberately duplicated rather than shared, per the rule "数据类型严格按
版本拆分". This lets consumers branch on instanceof / pattern-match
without losing track of which protocol version produced the data.

Knock-on changes (mechanical for the most part):

- All 16 standard parsers (v2016/, v2025/) updated to instantiate the
  matching nested record; e.g.
    new InfoBlock.Vehicle(...)
  becomes
    new InfoBlock.Gb32960V2016.Vehicle(...).
- The 6 Guangdong vendor parsers now return InfoBlock.GuangdongFc.* records
  (previously InfoBlock.GdFcXxx flat records).
- Position/Alarm record pairs split: Position no longer carries a nullable
  coordSystem field (V2025 has its own record with coordSystem; V2016
  has neither), and Alarm no longer carries the protocolVersion tag —
  the nested type already encodes the version.
- Gb32960EventMapper grew three small private "view" records
  (VehicleView, PositionView, AlarmView) plus extract* helpers that try
  V2016 first then fall back to V2025, so downstream payload assembly
  still sees a single uniform view.
- Test files updated to reference the new fully-qualified names.

InfoBlockType enum is left untouched: it is the logical type tag, and
both DriveMotor variants still report InfoBlockType.DRIVE_MOTOR even
though they live under different sealed parents — the *structural*
separation is what matters.

All 30 tests pass (18 existing + 8 selector + 4 vendor parser).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:52:57 +08:00
lingniu-dev
982272460a feat(gb32960): wire vendor parser profile selector into decoder pipeline
Final piece that lets the vendor extension framework introduced in
0e6f120 actually 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 from 957a86d)
    * 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>
2026-04-15 16:38:31 +08:00
lingniu-dev
957a86d581 feat(gb32960/vendor): implement Guangdong fuel-cell extension parsers
Adds 6 vendor parsers under codec/parser/vendor/guangdong/ implementing the
Guangdong "燃料电池汽车数据接入技术规范 v1.0" extension typeCodes that the
city's data ingestion platform sends inside V2016 frames:

  0x30  GdFcStackBlockParser           §7.2.3.4 表 13/14  fuel-cell stack
  0x31  GdFcAuxiliaryBlockParser       §7.2.3.5 表 15/16  auxiliary system
  0x32  GdFcDcDcBlockParser            §7.2.3.6 表 17     DC/DC
  0x33  GdFcAirConditionerBlockParser  §7.2.3.7 表 18     air conditioner
  0x34  GdFcVehicleInfoBlockParser     §7.2.3.8 表 19     vehicle info
  0x80  GdFcDemoExtensionBlockParser   §7.2.3.14 表 27    demo data extension

Each parser uses ProtocolVersion.V2016 + its respective typeCode and is
strictly Guangdong-spec layout, not the GB/T 32960.3-2025 fuel-cell stack
layout (the two share typeCode 0x30 but have different field orders and
extra cell-voltage statistics in the Guangdong variant).

Field model in InfoBlock.java gains:
- GdFcStack(stackCount, stacks: Stack(workState, waterTempC,
  h2InletPressureKpa, airInletPressureKpa, airInletTempC, max/min/avg cell
  voltage ids and values, cellCount, frameCellStart, frameCellCount,
  frameCellVoltagesV))
- GdFcAuxiliary(subSystemCount, subsystems: Subsystem with air compressor,
  hydrogen pump, water pump, PTC, low-voltage battery)
- GdFcDcDc, GdFcAirConditioner, GdFcVehicleInfo, GdFcDemoExtension
InfoBlockType enum gains the matching GD_FC_* constants.

Tests:
- GdFcParserTest covers the 4 fixed-length parsers (0x32/0x33/0x34/0x80)
  with hand-crafted bytes asserting field-by-field decoded values.
- 0x30/0x31 are variable-length and will be exercised end-to-end via the
  body parser integration test in a follow-up.

Spec doc: adds reference/广东燃料电池汽车示范应用城市群综合监管平台-...pdf
(v1.0.220822) used as the source of truth for these parsers.

These parsers exist as classes only; no Spring beans are wired yet.
Wiring + decoder context plumbing land in the next commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:37:53 +08:00
lingniu-dev
0e6f120ca9 feat(gb32960): introduce parser profile registry and rule-based vendor selector
Adds a per-context routing layer for InfoBlock parsers, so different
peers can be routed to different parser sets (e.g. standard GB/T 32960
vs. a vendor extension that adds blocks in the 0x30~0x7F reserved range).

New types in protocol-gb32960:
- Gb32960ParserContext (record): version + vin + platformAccount tuple
- codec/profile/VendorExtensionCatalog: name -> list of vendor parsers
- codec/profile/Gb32960ProfileRegistry: name -> InfoBlockParserRegistry,
  with a default registry built from the standard parsers and one extra
  registry per enabled vendor extension (default + vendor parsers)
- codec/profile/VendorExtensionSelector: interface returning the matching
  extension name (or null) for a given context
- codec/profile/RuleBasedVendorExtensionSelector: top-down first-match
  scan over Gb32960Properties.vendorExtensions with a (account,vin)
  cache keyed on case-insensitive normalized form

Gb32960Properties gains a `vendorExtensions` list with per-entry
`name` + `match` (platformAccounts / vins / vinPrefixes). All matching
fields are OR'd within an entry; entries are scanned top-down with
first-match-wins as documented.

Gb32960BodyParser keeps its legacy single-registry constructor for
back-compat (used by existing tests) and adds a new constructor that
takes the profile registry + selector. The parse loop now lives under
parse(Gb32960ParserContext, ByteBuffer); the legacy parse(version,body)
delegates to it with a minimal context (selector returns null -> default).

This commit only introduces the framework. Vendor parsers, autoconfig
wiring, and decoder/handler context plumbing land in follow-ups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:37:15 +08:00
lingniu-dev
064ecc479c 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>
2026-04-15 16:08:57 +08:00