Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/feichibridge/encoder.go
2026-07-27 16:46:15 +08:00

482 lines
14 KiB
Go

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 (e 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
}