package feichibridge import ( "encoding/binary" "errors" "fmt" "math" "strconv" "strings" "time" ) const ( CommandRealtime = byte(0x02) CommandReissue = byte(0x03) CommandLogin = byte(0x05) ) type Encoder struct{} func (Encoder) DataFrame(command byte, vin string, at time.Time, record Record) ([]byte, error) { if command != CommandRealtime && command != CommandReissue { return nil, fmt.Errorf("unsupported data command 0x%02X", command) } body := encodeTime(at) units := 0 if unit, ok := wholeVehicleUnit(record); ok { body = append(body, 0x01) body = append(body, unit...) units++ } if unit, ok := motorUnit(record); ok { body = append(body, 0x02) body = append(body, unit...) units++ } if unit, ok := fuelCellUnit(record); ok { body = append(body, 0x03) body = append(body, unit...) units++ } if unit, ok := positionUnit(record); ok { body = append(body, 0x05) body = append(body, unit...) units++ } if unit, ok := extremeUnit(record); ok { body = append(body, 0x06) body = append(body, unit...) units++ } if unit, ok := alarmUnit(record); ok { body = append(body, 0x07) body = append(body, unit...) units++ } for _, unit := range voltageUnits(record) { body = append(body, 0x08) body = append(body, unit...) units++ } for _, unit := range temperatureUnits(record) { body = append(body, 0x09) body = append(body, unit...) units++ } if units == 0 { return nil, errors.New("source record has no mappable GB/T 32960 units") } return buildFrame('#', command, 0xFE, vin, body) } func LoginFrame(platformID, username, password string, serial uint16, now time.Time) ([]byte, error) { if len(username) > 12 { return nil, errors.New("GB/T 32960 platform username exceeds 12 bytes") } if len(password) > 20 { return nil, errors.New("GB/T 32960 platform password exceeds 20 bytes") } body := encodeTime(now) body = binary.BigEndian.AppendUint16(body, serial) body = appendPaddedASCII(body, username, 12) body = appendPaddedASCII(body, password, 20) body = append(body, 0x01) return buildFrame('#', CommandLogin, 0xFE, platformID, body) } func buildFrame(start, command, response byte, vin string, body []byte) ([]byte, error) { if len(vin) != 17 { return nil, fmt.Errorf("GB/T 32960 identifier must be exactly 17 bytes, got %d", len(vin)) } if len(body) > math.MaxUint16 { return nil, errors.New("GB/T 32960 body exceeds 65535 bytes") } frame := make([]byte, 24, 25+len(body)) frame[0], frame[1] = start, start frame[2], frame[3] = command, response copy(frame[4:21], vin) frame[21] = 0x01 binary.BigEndian.PutUint16(frame[22:24], uint16(len(body))) frame = append(frame, body...) frame = append(frame, bcc(frame[2:])) return frame, nil } func wholeVehicleUnit(record Record) ([]byte, bool) { if !hasAny(record, "2201", "2202", "3201", "7615") { return nil, false } out := make([]byte, 20) out[0] = enum(recordValue(record, "3201"), map[string]byte{ "启动": 1, "启动状态": 1, "行驶": 1, "熄火": 2, "熄火状态": 2, "其他": 3, }, 0xFE) out[1] = enum(recordValue(record, "2301"), map[string]byte{ "停车充电": 1, "行驶充电": 2, "未充电": 3, "未充电状态": 3, "充电完成": 4, }, 0xFE) out[2] = enum(recordValue(record, "2213"), map[string]byte{ "纯电": 1, "纯电动": 1, "混动": 2, "混合动力": 2, "燃油": 3, }, 0xFE) putScaledU16(out[3:5], recordValue(record, "2201"), 10, 0) putScaledU32(out[5:9], recordValue(record, "2202"), 10) putScaledU16(out[9:11], recordValue(record, "2613"), 10, 0) putScaledU16(out[11:13], recordValue(record, "2614"), 10, 1000) out[13] = byteValue(recordValue(record, "7615")) out[14] = enum(recordValue(record, "2214"), map[string]byte{"工作": 1, "断开": 2}, 0xFE) out[15] = gearValue(firstField(recordValue(record, "2203"))) putU16(out[16:18], recordValue(record, "2617")) out[18] = byteValue(recordValue(record, "2208")) out[19] = byteValue(recordValue(record, "2209")) return out, true } func motorUnit(record Record) ([]byte, bool) { // The source carries every motor as a key:value composite separated by "|". // Fields are the GB/T 32960 sequence numbers exposed by the platform metadata. composite := recordValue(record, "2308") if composite == "" { return nil, false } groups := parseCompositeGroups(composite) if len(groups) == 0 || len(groups) > 253 { return nil, false } out := []byte{byte(len(groups))} for index, group := range groups { out = append(out, byteValue(firstNonempty(group["2302"], strconv.Itoa(index+1)))) out = append(out, enum(group["2303"], map[string]byte{ "耗电": 1, "发电": 2, "关闭": 3, "准备": 4, "准备状态": 4, }, 0xFE)) out = append(out, tempByte(group["2304"])) out = binary.BigEndian.AppendUint16(out, offsetU16(group["2305"], 1, 20000)) out = binary.BigEndian.AppendUint16(out, offsetU16(group["2306"], 10, 2000)) out = append(out, tempByte(group["2309"])) out = binary.BigEndian.AppendUint16(out, scaledU16(group["2311"], 10, 0)) out = binary.BigEndian.AppendUint16(out, scaledU16(group["2312"], 10, 1000)) } return out, true } func fuelCellUnit(record Record) ([]byte, bool) { if !hasAny(record, "2110", "2111", "2112", "2117", "2119") { return nil, false } temperatureGroups := parseSeriesGroups(recordValue(record, "2103")) var temperatures []string for _, group := range temperatureGroups { temperatures = append(temperatures, group.values...) } if len(temperatures) > math.MaxUint16 { temperatures = temperatures[:math.MaxUint16] } out := binary.BigEndian.AppendUint16(nil, scaledU16(recordValue(record, "2110"), 10, 0)) out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2111"), 10, 0)) out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2112"), 100, 0)) out = binary.BigEndian.AppendUint16(out, uint16(len(temperatures))) for _, value := range temperatures { out = append(out, tempByte(value)) } out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2115"), 10, 40)) out = append(out, byteValue(recordValue(record, "2116"))) out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2117"), 1, 0)) out = append(out, byteValue(recordValue(record, "2118"))) out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2119"), 10, 0)) out = append(out, byteValue(recordValue(record, "2120"))) out = append(out, enum(recordValue(record, "2121"), map[string]byte{"工作": 1, "断开": 2}, 0xFE)) return out, true } func positionUnit(record Record) ([]byte, bool) { longitude, lonOK := floatValue(recordValue(record, "2502")) latitude, latOK := floatValue(recordValue(record, "2503")) if !lonOK || !latOK || longitude < 0 || latitude < 0 { return nil, false } status := byte(0) text := strings.TrimSpace(recordValue(record, "2501")) if text != "" && (strings.Contains(text, "无效") || text == "1") { status = 1 } out := []byte{status} out = binary.BigEndian.AppendUint32(out, uint32(math.Round(longitude*1_000_000))) out = binary.BigEndian.AppendUint32(out, uint32(math.Round(latitude*1_000_000))) return out, true } func extremeUnit(record Record) ([]byte, bool) { if !hasAny(record, "2601", "2602", "2603", "2604", "2605", "2606", "2607", "2608", "2609", "2610", "2611", "2612") { return nil, false } out := []byte{ byteValue(recordValue(record, "2601")), byteValue(recordValue(record, "2602")), } out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2603"), 1000, 0)) out = append(out, byteValue(recordValue(record, "2604")), byteValue(recordValue(record, "2605"))) out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2606"), 1000, 0)) out = append(out, byteValue(recordValue(record, "2607")), byteValue(recordValue(record, "2608")), tempByte(recordValue(record, "2609")), byteValue(recordValue(record, "2610")), byteValue(recordValue(record, "2611")), tempByte(recordValue(record, "2612")), ) return out, true } func alarmUnit(record Record) ([]byte, bool) { levelRaw := recordValue(record, "2900", "2901") var general uint32 for bit := 0; bit < 32; bit++ { value := recordValue(record, strconv.Itoa(2901+bit)) if truthy(value) { general |= 1 << bit } } if strings.TrimSpace(levelRaw) == "" && general == 0 { return nil, false } out := []byte{byteValue(levelRaw)} out = binary.BigEndian.AppendUint32(out, general) out = append(out, 0, 0, 0, 0) return out, true } func voltageUnits(record Record) [][]byte { raw := recordValue(record, "2003", "batteryVoltages") groups := parseSeriesGroups(raw) var units [][]byte for _, group := range groups { for start := 0; start < len(group.values); start += 255 { end := start + 255 if end > len(group.values) { end = len(group.values) } out := []byte{1, byte(group.id)} out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2613"), 10, 0)) out = binary.BigEndian.AppendUint16(out, scaledU16(recordValue(record, "2614"), 10, 1000)) out = binary.BigEndian.AppendUint16(out, uint16(len(group.values))) out = binary.BigEndian.AppendUint16(out, uint16(start+1)) out = append(out, byte(end-start)) for _, value := range group.values[start:end] { out = binary.BigEndian.AppendUint16(out, scaledU16(value, 1000, 0)) } units = append(units, out) } } return units } func temperatureUnits(record Record) [][]byte { groups := parseSeriesGroups(recordValue(record, "2103", "batteryTemperatures")) var units [][]byte for _, group := range groups { for start := 0; start < len(group.values); start += math.MaxUint16 { end := start + math.MaxUint16 if end > len(group.values) { end = len(group.values) } out := []byte{1, byte(group.id)} out = binary.BigEndian.AppendUint16(out, uint16(end-start)) for _, value := range group.values[start:end] { out = append(out, tempByte(value)) } units = append(units, out) } } return units } func parseCompositeGroups(raw string) []map[string]string { var groups []map[string]string for _, encoded := range strings.Split(raw, "|") { group := map[string]string{} for _, field := range strings.Split(encoded, ",") { key, value, found := strings.Cut(field, ":") if found { group[strings.TrimSpace(key)] = strings.TrimSpace(value) } } if len(group) > 0 { groups = append(groups, group) } } return groups } type seriesGroup struct { id int values []string } func parseSeriesGroups(raw string) []seriesGroup { raw = strings.TrimSpace(raw) if raw == "" { return nil } var groups []seriesGroup for index, part := range strings.FieldsFunc(raw, func(r rune) bool { return r == ';' || r == '|' }) { group := seriesGroup{id: index + 1} if before, after, found := strings.Cut(part, ":"); found { if parsed, err := strconv.Atoi(strings.TrimSpace(before)); err == nil && parsed > 0 && parsed <= 255 { group.id = parsed } part = after } for _, value := range strings.FieldsFunc(part, func(r rune) bool { return r == '_' || r == ',' || r == ' ' || r == '[' || r == ']' }) { if strings.TrimSpace(value) != "" { group.values = append(group.values, strings.TrimSpace(value)) } } if len(group.values) > 0 { groups = append(groups, group) } } return groups } func recordValue(record Record, keys ...string) string { for _, key := range keys { if value := strings.TrimSpace(record[key]); value != "" && value != "--" && value != "null" { return value } } return "" } func firstField(value string) string { fields := strings.Fields(value) if len(fields) == 0 { return "" } return fields[0] } func firstNonempty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" } func hasAny(record Record, keys ...string) bool { return recordValue(record, keys...) != "" } func floatValue(raw string) (float64, bool) { raw = strings.TrimSpace(strings.TrimSuffix(raw, "%")) if raw == "" { return 0, false } value, err := strconv.ParseFloat(raw, 64) return value, err == nil && !math.IsNaN(value) && !math.IsInf(value, 0) } func byteValue(raw string) byte { value, ok := floatValue(raw) if !ok || value < 0 || value > 253 { return 0xFE } return byte(math.Round(value)) } func tempByte(raw string) byte { value, ok := floatValue(raw) if !ok || value < -40 || value > 210 { return 0xFE } return byte(math.Round(value + 40)) } func putU16(out []byte, raw string) { binary.BigEndian.PutUint16(out, scaledU16(raw, 1, 0)) } func putScaledU16(out []byte, raw string, scale, offset float64) { binary.BigEndian.PutUint16(out, scaledU16(raw, scale, offset)) } func putScaledU32(out []byte, raw string, scale float64) { value, ok := floatValue(raw) if !ok || value < 0 || value*scale > math.MaxUint32-2 { binary.BigEndian.PutUint32(out, 0xFFFFFFFE) return } binary.BigEndian.PutUint32(out, uint32(math.Round(value*scale))) } func scaledU16(raw string, scale, offset float64) uint16 { value, ok := floatValue(raw) encoded := (value + offset) * scale if !ok || encoded < 0 || encoded > math.MaxUint16-2 { return 0xFFFE } return uint16(math.Round(encoded)) } func offsetU16(raw string, scale, offset float64) uint16 { return scaledU16(raw, scale, offset) } func enum(raw string, values map[string]byte, missing byte) byte { raw = strings.TrimSpace(raw) if raw == "" { return missing } if numeric, err := strconv.ParseUint(raw, 10, 8); err == nil { return byte(numeric) } for label, value := range values { if raw == label || strings.Contains(raw, label) { return value } } return missing } func gearValue(raw string) byte { raw = strings.ToUpper(strings.TrimSpace(raw)) switch raw { case "P", "P档": return 15 case "R", "R档": return 13 case "N", "N档": return 0 case "D", "D档": return 14 } raw = strings.TrimSuffix(raw, "档") raw = strings.TrimPrefix(raw, "D") return byteValue(raw) } func truthy(raw string) bool { raw = strings.ToLower(strings.TrimSpace(raw)) return raw != "" && raw != "0" && raw != "false" && raw != "无" && raw != "正常" } func appendPaddedASCII(out []byte, value string, width int) []byte { start := len(out) out = append(out, make([]byte, width)...) copy(out[start:start+width], value) return out } func encodeTime(value time.Time) []byte { local := value.In(shanghai) return []byte{ byte(local.Year() - 2000), byte(local.Month()), byte(local.Day()), byte(local.Hour()), byte(local.Minute()), byte(local.Second()), } } func bcc(value []byte) byte { var result byte for _, current := range value { result ^= current } return result }