482 lines
14 KiB
Go
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
|
|
}
|