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 }