781 lines
25 KiB
Go
781 lines
25 KiB
Go
package gb32960
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
|
)
|
|
|
|
var (
|
|
ErrFrameTooShort = errors.New("gb32960 frame too short")
|
|
ErrBadStartSymbol = errors.New("gb32960 bad start symbol")
|
|
ErrBodyLength = errors.New("gb32960 body length mismatch")
|
|
ErrBCC = errors.New("gb32960 bcc mismatch")
|
|
)
|
|
|
|
const (
|
|
minFrameLen = 25
|
|
headerLen = 24
|
|
)
|
|
|
|
// ExtractFrames splits GB/T 32960 TCP streams using the protocol length field.
|
|
// Returned frames include the start symbols and trailing BCC byte.
|
|
func ExtractFrames(stream []byte) (frames [][]byte, remainder []byte, err error) {
|
|
offset := 0
|
|
for {
|
|
start := indexStart(stream[offset:])
|
|
if start < 0 {
|
|
return frames, nil, nil
|
|
}
|
|
offset += start
|
|
remaining := stream[offset:]
|
|
if len(remaining) < headerLen {
|
|
return frames, append([]byte(nil), remaining...), nil
|
|
}
|
|
bodyLen := int(binary.BigEndian.Uint16(remaining[22:24]))
|
|
frameLen := headerLen + bodyLen + 1
|
|
if len(remaining) < frameLen {
|
|
return frames, append([]byte(nil), remaining...), nil
|
|
}
|
|
frames = append(frames, append([]byte(nil), remaining[:frameLen]...))
|
|
offset += frameLen
|
|
if offset >= len(stream) {
|
|
return frames, nil, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func ParseFrame(raw []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error) {
|
|
if len(raw) < minFrameLen {
|
|
return envelope.FrameEnvelope{}, fmt.Errorf("%w: len=%d", ErrFrameTooShort, len(raw))
|
|
}
|
|
version, ok := version(raw[0], raw[1])
|
|
if !ok {
|
|
return envelope.FrameEnvelope{}, fmt.Errorf("%w: %s", ErrBadStartSymbol, hex.EncodeToString(raw[:2]))
|
|
}
|
|
bodyLen := int(binary.BigEndian.Uint16(raw[22:24]))
|
|
if len(raw) != headerLen+bodyLen+1 {
|
|
return envelope.FrameEnvelope{}, fmt.Errorf("%w: declared=%d actual=%d", ErrBodyLength, bodyLen, len(raw)-headerLen-1)
|
|
}
|
|
if got, want := bcc(raw[2:len(raw)-1]), raw[len(raw)-1]; got != want {
|
|
return envelope.FrameEnvelope{}, fmt.Errorf("%w: got=0x%02x want=0x%02x", ErrBCC, got, want)
|
|
}
|
|
|
|
command := raw[2]
|
|
responseFlag := raw[3]
|
|
vin := strings.TrimRight(string(raw[4:21]), "\x00 ")
|
|
body := raw[headerLen : headerLen+bodyLen]
|
|
parsed := map[string]any{
|
|
"header": map[string]any{
|
|
"version": version,
|
|
"command": fmt.Sprintf("0x%02X", command),
|
|
"response_flag": fmt.Sprintf("0x%02X", responseFlag),
|
|
"vin": vin,
|
|
"encrypt": raw[21],
|
|
"body_length": bodyLen,
|
|
},
|
|
}
|
|
fields := map[string]any{}
|
|
eventTimeMS := receivedAtMS
|
|
|
|
if command == 0x02 || command == 0x03 {
|
|
eventTime, units := parseDataBody(version, body, fields)
|
|
if !eventTime.IsZero() {
|
|
eventTimeMS = eventTime.UnixMilli()
|
|
fields["device_time"] = eventTime.Format(time.RFC3339)
|
|
parsed["device_time"] = eventTime.Format(time.RFC3339)
|
|
}
|
|
parsed["data_units"] = units
|
|
}
|
|
|
|
env := envelope.FrameEnvelope{
|
|
Protocol: envelope.ProtocolGB32960,
|
|
MessageID: fmt.Sprintf("0x%02X", command),
|
|
VIN: vin,
|
|
SourceEndpoint: sourceEndpoint,
|
|
EventTimeMS: eventTimeMS,
|
|
ReceivedAtMS: receivedAtMS,
|
|
RawHex: strings.ToUpper(hex.EncodeToString(raw)),
|
|
Parsed: parsed,
|
|
Fields: fields,
|
|
ParseStatus: envelope.ParseOK,
|
|
}
|
|
env.EventID = env.StableEventID()
|
|
return env, nil
|
|
}
|
|
|
|
func parseDataBody(version string, body []byte, fields map[string]any) (time.Time, []map[string]any) {
|
|
if len(body) < 6 {
|
|
return time.Time{}, nil
|
|
}
|
|
eventTime := parseBCDTime(body[:6])
|
|
cursor := 6
|
|
var units []map[string]any
|
|
for cursor < len(body) {
|
|
unitType := body[cursor]
|
|
cursor++
|
|
switch unitType {
|
|
case 0x01:
|
|
size := 20
|
|
if version == "V2025" {
|
|
size = 18
|
|
}
|
|
if len(body[cursor:]) < size {
|
|
units = append(units, map[string]any{"type": "0x01", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseVehicleData(body[cursor : cursor+size])
|
|
units = append(units, map[string]any{"type": "0x01", "name": "vehicle", "value": unit})
|
|
fields["vehicle_status"] = unit["vehicle_status"]
|
|
fields["charge_status"] = unit["charge_status"]
|
|
fields["running_mode"] = unit["running_mode"]
|
|
fields[envelope.FieldSpeedKMH] = unit["speed_kmh"]
|
|
fields[envelope.FieldTotalMileageKM] = unit["total_mileage_km"]
|
|
fields[envelope.FieldSOCPercent] = unit["soc_percent"]
|
|
cursor += size
|
|
case 0x05:
|
|
if len(body[cursor:]) < 9 {
|
|
units = append(units, map[string]any{"type": "0x05", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parsePositionData(body[cursor : cursor+9])
|
|
units = append(units, map[string]any{"type": "0x05", "name": "position", "value": unit})
|
|
fields["position_status"] = unit["position_status"]
|
|
fields[envelope.FieldLongitude] = unit["longitude"]
|
|
fields[envelope.FieldLatitude] = unit["latitude"]
|
|
cursor += 9
|
|
case 0x02:
|
|
if len(body[cursor:]) < 1 {
|
|
units = append(units, map[string]any{"type": "0x02", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
size := 1 + int(body[cursor])*12
|
|
if len(body[cursor:]) < size {
|
|
units = append(units, map[string]any{"type": "0x02", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseDriveMotorData(body[cursor : cursor+size])
|
|
units = append(units, map[string]any{"type": "0x02", "name": "drive_motor", "value": unit})
|
|
cursor += size
|
|
case 0x03:
|
|
if len(body[cursor:]) < 8 {
|
|
units = append(units, map[string]any{"type": "0x03", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
probeCount := int(binary.BigEndian.Uint16(body[cursor+6 : cursor+8]))
|
|
size := 8 + probeCount + 10
|
|
if len(body[cursor:]) < size {
|
|
units = append(units, map[string]any{"type": "0x03", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseFuelCellData(body[cursor : cursor+size])
|
|
units = append(units, map[string]any{"type": "0x03", "name": "fuel_cell", "value": unit})
|
|
fields["fuel_cell_hydrogen_consumption_kg_per_100km"] = unit["hydrogen_consumption_kg_per_100km"]
|
|
fields["fuel_cell_voltage_v"] = unit["fuel_cell_voltage_v"]
|
|
fields["fuel_cell_current_a"] = unit["fuel_cell_current_a"]
|
|
cursor += size
|
|
case 0x04:
|
|
if len(body[cursor:]) < 5 {
|
|
units = append(units, map[string]any{"type": "0x04", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseEngineData(body[cursor : cursor+5])
|
|
units = append(units, map[string]any{"type": "0x04", "name": "engine", "value": unit})
|
|
cursor += 5
|
|
case 0x06:
|
|
if len(body[cursor:]) < 14 {
|
|
units = append(units, map[string]any{"type": "0x06", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseExtremeData(body[cursor : cursor+14])
|
|
units = append(units, map[string]any{"type": "0x06", "name": "extreme", "value": unit})
|
|
cursor += 14
|
|
case 0x07:
|
|
size, ok := alarmDataSize(body[cursor:])
|
|
if !ok {
|
|
units = append(units, map[string]any{"type": "0x07", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseAlarmData(body[cursor : cursor+size])
|
|
units = append(units, map[string]any{"type": "0x07", "name": "alarm", "value": unit})
|
|
cursor += size
|
|
case 0x08:
|
|
size, ok := voltageDataSize(body[cursor:])
|
|
if !ok {
|
|
units = append(units, map[string]any{"type": "0x08", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseVoltageData(body[cursor : cursor+size])
|
|
units = append(units, map[string]any{"type": "0x08", "name": "voltage", "value": unit})
|
|
cursor += size
|
|
case 0x09:
|
|
size, ok := temperatureDataSize(body[cursor:])
|
|
if !ok {
|
|
units = append(units, map[string]any{"type": "0x09", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseTemperatureData(body[cursor : cursor+size])
|
|
units = append(units, map[string]any{"type": "0x09", "name": "temperature", "value": unit})
|
|
cursor += size
|
|
case 0x30:
|
|
size, ok := gdFCStackDataSize(body[cursor:])
|
|
if !ok {
|
|
units = append(units, map[string]any{"type": "0x30", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseGdFCStackData(body[cursor : cursor+size])
|
|
units = append(units, map[string]any{"type": "0x30", "name": "gd_fc_stack", "value": unit})
|
|
if summaries, ok := unit["summaries"].([]map[string]any); ok && len(summaries) > 0 {
|
|
fields["gd_fc_stack_hydrogen_inlet_pressure_kpa"] = summaries[0]["hydrogen_inlet_pressure_kpa"]
|
|
fields["gd_fc_stack_water_outlet_temp_c"] = summaries[0]["stack_water_outlet_temp_c"]
|
|
fields["gd_fc_stack_cell_count"] = summaries[0]["cell_count"]
|
|
fields["gd_fc_stack_frame_cell_count"] = summaries[0]["frame_cell_count"]
|
|
}
|
|
cursor += size
|
|
case 0x31:
|
|
size, ok := gdFCAuxiliaryDataSize(body[cursor:])
|
|
if !ok {
|
|
units = append(units, map[string]any{"type": "0x31", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseGdFCAuxiliaryData(body[cursor : cursor+size])
|
|
units = append(units, map[string]any{"type": "0x31", "name": "gd_fc_auxiliary", "value": unit})
|
|
cursor += size
|
|
case 0x32:
|
|
if len(body[cursor:]) < 9 {
|
|
units = append(units, map[string]any{"type": "0x32", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseGdFCDcDcData(body[cursor : cursor+9])
|
|
units = append(units, map[string]any{"type": "0x32", "name": "gd_fc_dcdc", "value": unit})
|
|
fields["gd_fc_dcdc_controller_temp_c"] = unit["controller_temp_c"]
|
|
cursor += 9
|
|
case 0x33:
|
|
if len(body[cursor:]) < 5 {
|
|
units = append(units, map[string]any{"type": "0x33", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseGdFCAirConditionerData(body[cursor : cursor+5])
|
|
units = append(units, map[string]any{"type": "0x33", "name": "gd_fc_air_conditioner", "value": unit})
|
|
cursor += 5
|
|
case 0x34:
|
|
if len(body[cursor:]) < 7 {
|
|
units = append(units, map[string]any{"type": "0x34", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseGdFCVehicleInfoData(body[cursor : cursor+7])
|
|
units = append(units, map[string]any{"type": "0x34", "name": "gd_fc_vehicle_info", "value": unit})
|
|
fields["gd_fc_vehicle_hydrogen_mass_kg"] = unit["hydrogen_mass_kg"]
|
|
cursor += 7
|
|
case 0x80:
|
|
if len(body[cursor:]) < 11 {
|
|
units = append(units, map[string]any{"type": "0x80", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseGdFCDemoExtensionData(body[cursor : cursor+11])
|
|
units = append(units, map[string]any{"type": "0x80", "name": "gd_fc_demo_extension", "value": unit})
|
|
fields["gd_fc_demo_stack_temp_c"] = unit["stack_temp_c"]
|
|
cursor += 11
|
|
case 0x83:
|
|
size, ok := gdFCVendorTLVDataSize(body[cursor:])
|
|
if !ok {
|
|
units = append(units, map[string]any{"type": "0x83", "error": "truncated"})
|
|
return eventTime, units
|
|
}
|
|
unit := parseGdFCVendorTLVData(0x83, body[cursor:cursor+size])
|
|
units = append(units, map[string]any{"type": "0x83", "name": "gd_fc_vendor_tlv", "value": unit})
|
|
cursor += size
|
|
default:
|
|
units = append(units, map[string]any{
|
|
"type": fmt.Sprintf("0x%02X", unitType),
|
|
"raw_tail": strings.ToUpper(hex.EncodeToString(body[cursor:])),
|
|
"parse": "unknown_unit",
|
|
"byte_size": len(body) - cursor,
|
|
})
|
|
return eventTime, units
|
|
}
|
|
}
|
|
return eventTime, units
|
|
}
|
|
|
|
func parseVehicleData(data []byte) map[string]any {
|
|
return map[string]any{
|
|
"vehicle_status": int(data[0]),
|
|
"charge_status": int(data[1]),
|
|
"running_mode": int(data[2]),
|
|
"speed_kmh": float64(binary.BigEndian.Uint16(data[3:5])) / 10,
|
|
"total_mileage_km": float64(binary.BigEndian.Uint32(data[5:9])) / 10,
|
|
"total_voltage_v": float64(binary.BigEndian.Uint16(data[9:11])) / 10,
|
|
"total_current_a": float64(binary.BigEndian.Uint16(data[11:13]))/10 - 1000,
|
|
"soc_percent": int(data[13]),
|
|
"dc_dc_status": int(data[14]),
|
|
"gear": int(data[15]),
|
|
"insulation_kohm": binary.BigEndian.Uint16(data[16:18]),
|
|
"accelerator_pct": int(data[18]),
|
|
"brake_pct": int(data[19]),
|
|
}
|
|
}
|
|
|
|
func parseDriveMotorData(data []byte) map[string]any {
|
|
count := int(data[0])
|
|
motors := make([]map[string]any, 0, count)
|
|
cursor := 1
|
|
for i := 0; i < count; i++ {
|
|
motors = append(motors, map[string]any{
|
|
"serial_no": int(data[cursor]),
|
|
"state": int(data[cursor+1]),
|
|
"controller_temperature_c": int(data[cursor+2]) - 40,
|
|
"speed_rpm": int(binary.BigEndian.Uint16(data[cursor+3:cursor+5])) - 20000,
|
|
"torque_nm": float64(int(binary.BigEndian.Uint16(data[cursor+5:cursor+7]))-20000) / 10,
|
|
"motor_temperature_c": int(data[cursor+7]) - 40,
|
|
"controller_voltage_v": float64(binary.BigEndian.Uint16(data[cursor+8:cursor+10])) / 10,
|
|
"controller_current_a": float64(binary.BigEndian.Uint16(data[cursor+10:cursor+12]))/10 - 1000,
|
|
})
|
|
cursor += 12
|
|
}
|
|
return map[string]any{
|
|
"count": count,
|
|
"motors": motors,
|
|
}
|
|
}
|
|
|
|
func parseFuelCellData(data []byte) map[string]any {
|
|
probeCount := int(binary.BigEndian.Uint16(data[6:8]))
|
|
probes := make([]int, 0, probeCount)
|
|
cursor := 8
|
|
for i := 0; i < probeCount; i++ {
|
|
probes = append(probes, int(data[cursor])-40)
|
|
cursor++
|
|
}
|
|
out := map[string]any{
|
|
"fuel_cell_voltage_v": float64(binary.BigEndian.Uint16(data[0:2])) / 10,
|
|
"fuel_cell_current_a": float64(binary.BigEndian.Uint16(data[2:4])) / 10,
|
|
"hydrogen_consumption_kg_per_100km": float64(binary.BigEndian.Uint16(data[4:6])) / 100,
|
|
"temperature_probe_count": probeCount,
|
|
"temperature_probe_values_c": probes,
|
|
"max_hydrogen_temperature_c": float64(binary.BigEndian.Uint16(data[cursor:cursor+2]))/10 - 40,
|
|
"max_hydrogen_temperature_probe_id": int(data[cursor+2]),
|
|
"max_hydrogen_concentration_fraction": float64(binary.BigEndian.Uint16(data[cursor+3:cursor+5])) * 0.000001,
|
|
"max_hydrogen_concentration_probe_id": int(data[cursor+5]),
|
|
"max_hydrogen_pressure_mpa": float64(binary.BigEndian.Uint16(data[cursor+6:cursor+8])) / 10,
|
|
"max_hydrogen_pressure_probe_id": int(data[cursor+8]),
|
|
"dc_dc_status": int(data[cursor+9]),
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseEngineData(data []byte) map[string]any {
|
|
return map[string]any{
|
|
"engine_status": int(data[0]),
|
|
"crank_speed_rpm": binary.BigEndian.Uint16(data[1:3]),
|
|
"fuel_rate": binary.BigEndian.Uint16(data[3:5]),
|
|
}
|
|
}
|
|
|
|
func parseExtremeData(data []byte) map[string]any {
|
|
return map[string]any{
|
|
"max_voltage_subsystem_no": int(data[0]),
|
|
"max_voltage_cell_no": int(data[1]),
|
|
"max_voltage_v": float64(binary.BigEndian.Uint16(data[2:4])) / 1000,
|
|
"min_voltage_subsystem_no": int(data[4]),
|
|
"min_voltage_cell_no": int(data[5]),
|
|
"min_voltage_v": float64(binary.BigEndian.Uint16(data[6:8])) / 1000,
|
|
"max_temp_subsystem_no": int(data[8]),
|
|
"max_temp_probe_no": int(data[9]),
|
|
"max_temp_c": int(data[10]) - 40,
|
|
"min_temp_subsystem_no": int(data[11]),
|
|
"min_temp_probe_no": int(data[12]),
|
|
"min_temp_c": int(data[13]) - 40,
|
|
}
|
|
}
|
|
|
|
func alarmDataSize(data []byte) (int, bool) {
|
|
if len(data) < 5 {
|
|
return 0, false
|
|
}
|
|
cursor := 5
|
|
for i := 0; i < 4; i++ {
|
|
if len(data[cursor:]) < 1 {
|
|
return 0, false
|
|
}
|
|
count := int(data[cursor])
|
|
cursor++
|
|
cursor += count * 4
|
|
if cursor > len(data) {
|
|
return 0, false
|
|
}
|
|
}
|
|
return cursor, true
|
|
}
|
|
|
|
func parseAlarmData(data []byte) map[string]any {
|
|
cursor := 5
|
|
readList := func() []string {
|
|
count := int(data[cursor])
|
|
cursor++
|
|
values := make([]string, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
values = append(values, fmt.Sprintf("0x%08X", binary.BigEndian.Uint32(data[cursor:cursor+4])))
|
|
cursor += 4
|
|
}
|
|
return values
|
|
}
|
|
return map[string]any{
|
|
"max_alarm_level": int(data[0]),
|
|
"general_alarm_flag": fmt.Sprintf("0x%08X", binary.BigEndian.Uint32(data[1:5])),
|
|
"battery_faults": readList(),
|
|
"motor_faults": readList(),
|
|
"engine_faults": readList(),
|
|
"other_faults": readList(),
|
|
}
|
|
}
|
|
|
|
func voltageDataSize(data []byte) (int, bool) {
|
|
if len(data) < 1 {
|
|
return 0, false
|
|
}
|
|
cursor := 1
|
|
for i := 0; i < int(data[0]); i++ {
|
|
if len(data[cursor:]) < 10 {
|
|
return 0, false
|
|
}
|
|
frameCellCount := int(data[cursor+9])
|
|
cursor += 10 + frameCellCount*2
|
|
if cursor > len(data) {
|
|
return 0, false
|
|
}
|
|
}
|
|
return cursor, true
|
|
}
|
|
|
|
func parseVoltageData(data []byte) map[string]any {
|
|
subCount := int(data[0])
|
|
cursor := 1
|
|
totalVoltage := 0.0
|
|
maxCell := 0.0
|
|
minCell := 0.0
|
|
cellSeen := false
|
|
for i := 0; i < subCount; i++ {
|
|
totalVoltage += float64(binary.BigEndian.Uint16(data[cursor+1:cursor+3])) / 10
|
|
cellCount := int(binary.BigEndian.Uint16(data[cursor+5 : cursor+7]))
|
|
frameCellCount := int(data[cursor+9])
|
|
cursor += 10
|
|
for c := 0; c < frameCellCount; c++ {
|
|
voltage := float64(binary.BigEndian.Uint16(data[cursor:cursor+2])) / 1000
|
|
if !cellSeen || voltage > maxCell {
|
|
maxCell = voltage
|
|
}
|
|
if !cellSeen || voltage < minCell {
|
|
minCell = voltage
|
|
}
|
|
cellSeen = true
|
|
cursor += 2
|
|
}
|
|
_ = cellCount
|
|
}
|
|
return map[string]any{
|
|
"subsystem_count": subCount,
|
|
"total_voltage_v": totalVoltage,
|
|
"max_cell_v": maxCell,
|
|
"min_cell_v": minCell,
|
|
}
|
|
}
|
|
|
|
func temperatureDataSize(data []byte) (int, bool) {
|
|
if len(data) < 1 {
|
|
return 0, false
|
|
}
|
|
cursor := 1
|
|
for i := 0; i < int(data[0]); i++ {
|
|
if len(data[cursor:]) < 3 {
|
|
return 0, false
|
|
}
|
|
probeCount := int(binary.BigEndian.Uint16(data[cursor+1 : cursor+3]))
|
|
cursor += 3 + probeCount
|
|
if cursor > len(data) {
|
|
return 0, false
|
|
}
|
|
}
|
|
return cursor, true
|
|
}
|
|
|
|
func parseTemperatureData(data []byte) map[string]any {
|
|
subCount := int(data[0])
|
|
cursor := 1
|
|
maxTemp := 0
|
|
minTemp := 0
|
|
seen := false
|
|
for i := 0; i < subCount; i++ {
|
|
probeCount := int(binary.BigEndian.Uint16(data[cursor+1 : cursor+3]))
|
|
cursor += 3
|
|
for p := 0; p < probeCount; p++ {
|
|
temp := int(data[cursor]) - 40
|
|
if !seen || temp > maxTemp {
|
|
maxTemp = temp
|
|
}
|
|
if !seen || temp < minTemp {
|
|
minTemp = temp
|
|
}
|
|
seen = true
|
|
cursor++
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"subsystem_count": subCount,
|
|
"max_temp_c": maxTemp,
|
|
"min_temp_c": minTemp,
|
|
}
|
|
}
|
|
|
|
func gdFCStackDataSize(data []byte) (int, bool) {
|
|
if len(data) < 1 {
|
|
return 0, false
|
|
}
|
|
cursor := 1
|
|
for i := 0; i < int(data[0]); i++ {
|
|
if len(data[cursor:]) < 22 {
|
|
return 0, false
|
|
}
|
|
frameCellCount := int(data[cursor+21])
|
|
cursor += 22 + frameCellCount*2
|
|
if cursor > len(data) {
|
|
return 0, false
|
|
}
|
|
}
|
|
return cursor, true
|
|
}
|
|
|
|
func parseGdFCStackData(data []byte) map[string]any {
|
|
count := int(data[0])
|
|
cursor := 1
|
|
summaries := make([]map[string]any, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
frameCellCount := int(data[cursor+21])
|
|
summary := map[string]any{
|
|
"engine_work_state": int(data[cursor]),
|
|
"stack_water_outlet_temp_c": nullableTempOffset40(data[cursor+1]),
|
|
"hydrogen_inlet_pressure_kpa": nullableScaledU16WithOffset(data[cursor+2:cursor+4], 0.1, -100),
|
|
"air_inlet_pressure_kpa": nullableScaledU16WithOffset(data[cursor+4:cursor+6], 0.1, -100),
|
|
"air_inlet_temp_c": nullableTempOffset40(data[cursor+6]),
|
|
"max_cell_voltage_id": nullableU16(data[cursor+7 : cursor+9]),
|
|
"min_cell_voltage_id": nullableU16(data[cursor+9 : cursor+11]),
|
|
"max_cell_voltage_v": nullableScaledU16(data[cursor+11:cursor+13], 0.001),
|
|
"min_cell_voltage_v": nullableScaledU16(data[cursor+13:cursor+15], 0.001),
|
|
"avg_cell_voltage_v": nullableScaledU16(data[cursor+15:cursor+17], 0.001),
|
|
"cell_count": nullableU16(data[cursor+17 : cursor+19]),
|
|
"frame_cell_start": nullableU16(data[cursor+19 : cursor+21]),
|
|
"frame_cell_count": frameCellCount,
|
|
}
|
|
cursor += 22
|
|
maxCell := 0.0
|
|
minCell := 0.0
|
|
seen := false
|
|
for c := 0; c < frameCellCount; c++ {
|
|
voltage := float64(binary.BigEndian.Uint16(data[cursor:cursor+2])) / 1000
|
|
if !seen || voltage > maxCell {
|
|
maxCell = voltage
|
|
}
|
|
if !seen || voltage < minCell {
|
|
minCell = voltage
|
|
}
|
|
seen = true
|
|
cursor += 2
|
|
}
|
|
if seen {
|
|
summary["frame_max_cell_voltage_v"] = maxCell
|
|
summary["frame_min_cell_voltage_v"] = minCell
|
|
}
|
|
summaries = append(summaries, summary)
|
|
}
|
|
return map[string]any{
|
|
"stack_count": count,
|
|
"summaries": summaries,
|
|
}
|
|
}
|
|
|
|
func gdFCAuxiliaryDataSize(data []byte) (int, bool) {
|
|
if len(data) < 1 {
|
|
return 0, false
|
|
}
|
|
size := 1 + int(data[0])*16
|
|
return size, len(data) >= size
|
|
}
|
|
|
|
func parseGdFCAuxiliaryData(data []byte) map[string]any {
|
|
count := int(data[0])
|
|
cursor := 1
|
|
subsystems := make([]map[string]any, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
subsystems = append(subsystems, map[string]any{
|
|
"air_compressor_motor_voltage_v": nullableScaledU16(data[cursor:cursor+2], 0.1),
|
|
"air_compressor_power_kw": nullableU16WithOffset(data[cursor+2:cursor+4], -5),
|
|
"hydrogen_pump_motor_voltage_v": nullableScaledU16(data[cursor+4:cursor+6], 0.2),
|
|
"hydrogen_pump_power_kw": nullableU16WithOffset(data[cursor+6:cursor+8], -5),
|
|
"water_pump_voltage_v": nullableScaledU16(data[cursor+8:cursor+10], 0.1),
|
|
"ptc_voltage_v": nullableScaledU16(data[cursor+10:cursor+12], 0.1),
|
|
"ptc_power_kw": nullableU16WithOffset(data[cursor+12:cursor+14], -5),
|
|
"low_voltage_battery_voltage_v": nullableScaledU16(data[cursor+14:cursor+16], 0.1),
|
|
})
|
|
cursor += 16
|
|
}
|
|
return map[string]any{
|
|
"subsystem_count": count,
|
|
"subsystems": subsystems,
|
|
}
|
|
}
|
|
|
|
func parseGdFCDcDcData(data []byte) map[string]any {
|
|
return map[string]any{
|
|
"input_voltage_v": nullableScaledU16(data[0:2], 0.1),
|
|
"input_current_a": nullableScaledU16(data[2:4], 0.1),
|
|
"output_voltage_v": nullableScaledU16(data[4:6], 0.1),
|
|
"output_current_a": nullableScaledU16(data[6:8], 0.1),
|
|
"controller_temp_c": nullableTempOffset40(data[8]),
|
|
}
|
|
}
|
|
|
|
func parseGdFCAirConditionerData(data []byte) map[string]any {
|
|
return map[string]any{
|
|
"status": int(data[0]),
|
|
"power_kw": nullableU16WithOffset(data[1:3], -5),
|
|
"compressor_input_voltage_v": nullableScaledU16(data[3:5], 0.1),
|
|
}
|
|
}
|
|
|
|
func parseGdFCVehicleInfoData(data []byte) map[string]any {
|
|
return map[string]any{
|
|
"collision_alarm": int(data[0]),
|
|
"ambient_temp_c": nullableU16WithOffset(data[1:3], -40),
|
|
"ambient_pressure_kpa": nullableScaledU16WithOffset(data[3:5], 0.1, -100),
|
|
"hydrogen_mass_kg": nullableScaledU16(data[5:7], 0.1),
|
|
}
|
|
}
|
|
|
|
func parseGdFCDemoExtensionData(data []byte) map[string]any {
|
|
return map[string]any{
|
|
"declared_length": int(binary.BigEndian.Uint16(data[0:2])),
|
|
"stack_temp_c": nullableTempOffset40(data[2]),
|
|
"air_compressor_voltage_v": nullableScaledU16(data[3:5], 0.1),
|
|
"air_compressor_current_a": nullableScaledU16WithOffset(data[5:7], 0.1, -1000),
|
|
"hydrogen_pump_voltage_v": nullableScaledU16(data[7:9], 0.1),
|
|
"hydrogen_pump_current_a": nullableScaledU16WithOffset(data[9:11], 0.1, -1000),
|
|
}
|
|
}
|
|
|
|
func gdFCVendorTLVDataSize(data []byte) (int, bool) {
|
|
if len(data) < 2 {
|
|
return 0, false
|
|
}
|
|
size := 2 + int(binary.BigEndian.Uint16(data[0:2]))
|
|
return size, len(data) >= size
|
|
}
|
|
|
|
func parseGdFCVendorTLVData(typeCode byte, data []byte) map[string]any {
|
|
length := int(binary.BigEndian.Uint16(data[0:2]))
|
|
return map[string]any{
|
|
"type": fmt.Sprintf("0x%02X", typeCode),
|
|
"declared_length": length,
|
|
"payload_hex": strings.ToUpper(hex.EncodeToString(data[2:])),
|
|
}
|
|
}
|
|
|
|
func nullableU16(data []byte) any {
|
|
value := binary.BigEndian.Uint16(data)
|
|
if value == 0xfffe || value == 0xffff {
|
|
return nil
|
|
}
|
|
return int(value)
|
|
}
|
|
|
|
func nullableU16WithOffset(data []byte, offset int) any {
|
|
value := binary.BigEndian.Uint16(data)
|
|
if value == 0xfffe || value == 0xffff {
|
|
return nil
|
|
}
|
|
return int(value) + offset
|
|
}
|
|
|
|
func nullableScaledU16(data []byte, scale float64) any {
|
|
value := binary.BigEndian.Uint16(data)
|
|
if value == 0xfffe || value == 0xffff {
|
|
return nil
|
|
}
|
|
return float64(value) * scale
|
|
}
|
|
|
|
func nullableScaledU16WithOffset(data []byte, scale float64, offset float64) any {
|
|
value := binary.BigEndian.Uint16(data)
|
|
if value == 0xfffe || value == 0xffff {
|
|
return nil
|
|
}
|
|
return float64(value)*scale + offset
|
|
}
|
|
|
|
func nullableTempOffset40(value byte) any {
|
|
if value == 0xfe || value == 0xff {
|
|
return nil
|
|
}
|
|
return int(value) - 40
|
|
}
|
|
|
|
func parsePositionData(data []byte) map[string]any {
|
|
return map[string]any{
|
|
"position_status": int(data[0]),
|
|
"longitude": float64(binary.BigEndian.Uint32(data[1:5])) / 1_000_000,
|
|
"latitude": float64(binary.BigEndian.Uint32(data[5:9])) / 1_000_000,
|
|
}
|
|
}
|
|
|
|
func indexStart(data []byte) int {
|
|
for i := 0; i+1 < len(data); i++ {
|
|
if _, ok := version(data[i], data[i+1]); ok {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func version(a byte, b byte) (string, bool) {
|
|
switch {
|
|
case a == '#' && b == '#':
|
|
return "V2016", true
|
|
case a == '$' && b == '$':
|
|
return "V2025", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func parseBCDTime(data []byte) time.Time {
|
|
if len(data) != 6 {
|
|
return time.Time{}
|
|
}
|
|
year := 2000 + bcdByte(data[0])
|
|
month := time.Month(bcdByte(data[1]))
|
|
day := bcdByte(data[2])
|
|
hour := bcdByte(data[3])
|
|
minute := bcdByte(data[4])
|
|
second := bcdByte(data[5])
|
|
if month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59 {
|
|
return time.Time{}
|
|
}
|
|
return time.Date(year, month, day, hour, minute, second, 0, time.FixedZone("Asia/Shanghai", 8*3600))
|
|
}
|
|
|
|
func bcdByte(value byte) int {
|
|
return int(value>>4)*10 + int(value&0x0f)
|
|
}
|
|
|
|
func bcc(data []byte) byte {
|
|
var out byte
|
|
for _, value := range data {
|
|
out ^= value
|
|
}
|
|
return out
|
|
}
|