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>
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>
Adds a vendor-extensions entry under lingniu.ingest.gb32960 mapping the
existing lingniu platform account (already present in auth.platforms)
to the guangdong-fc parser bundle, so that incoming 0x02 realtime frames
from this peer get routed to the GuangdongFc.* parsers instead of
falling through to the default profile and producing the
"unknown info block typeCode=0x30" WARN.
vin-prefixes / vins are left empty pending real values from the peer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>