Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/feichibridge/target.go

194 lines
4.5 KiB
Go

package feichibridge
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"strings"
"sync"
"time"
)
type TargetConfig struct {
Address string
PlatformID string
Username string
Password string
Timeout time.Duration
}
type Target struct {
mu sync.Mutex
config TargetConfig
state *StateStore
dialer net.Dialer
conn net.Conn
lastACK time.Time
}
func NewTarget(config TargetConfig, state *StateStore) (*Target, error) {
if strings.TrimSpace(config.Address) == "" {
return nil, errors.New("GB/T 32960 target address is required")
}
if len(config.PlatformID) != 17 {
return nil, fmt.Errorf("target platform ID must be exactly 17 bytes")
}
if state == nil {
return nil, errors.New("state store is required")
}
if config.Timeout <= 0 {
config.Timeout = 10 * time.Second
}
return &Target{
config: config,
state: state,
dialer: net.Dialer{Timeout: config.Timeout, KeepAlive: 30 * time.Second},
}, nil
}
func (t *Target) Send(ctx context.Context, frame []byte) error {
t.mu.Lock()
defer t.mu.Unlock()
var lastErr error
for attempt := 0; attempt < 2; attempt++ {
if err := t.ensureConnected(ctx); err != nil {
lastErr = err
t.closeLocked()
continue
}
if err := t.sendAndACK(ctx, frame); err != nil {
lastErr = err
t.closeLocked()
continue
}
t.lastACK = time.Now()
return nil
}
return fmt.Errorf("send GB/T 32960 frame after reconnect: %w", lastErr)
}
func (t *Target) Connect(ctx context.Context) error {
t.mu.Lock()
defer t.mu.Unlock()
if err := t.ensureConnected(ctx); err != nil {
t.closeLocked()
return err
}
return nil
}
func (t *Target) Close() error {
t.mu.Lock()
defer t.mu.Unlock()
if t.conn == nil {
return nil
}
err := t.conn.Close()
t.conn = nil
return err
}
func (t *Target) LastACK() time.Time {
t.mu.Lock()
defer t.mu.Unlock()
return t.lastACK
}
func (t *Target) ensureConnected(ctx context.Context) error {
if t.conn != nil {
return nil
}
conn, err := t.dialer.DialContext(ctx, "tcp", t.config.Address)
if err != nil {
return fmt.Errorf("dial target %s: %w", t.config.Address, err)
}
t.conn = conn
serial, err := t.state.NextPlatformSerial()
if err != nil {
return fmt.Errorf("allocate platform login serial: %w", err)
}
login, err := LoginFrame(t.config.PlatformID, t.config.Username, t.config.Password, serial, time.Now())
if err != nil {
return err
}
if err := t.sendAndACK(ctx, login); err != nil {
return fmt.Errorf("GB/T 32960 platform login: %w", err)
}
t.lastACK = time.Now()
return nil
}
func (t *Target) sendAndACK(ctx context.Context, frame []byte) error {
if t.conn == nil {
return errors.New("target connection is closed")
}
deadline := time.Now().Add(t.config.Timeout)
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
deadline = contextDeadline
}
if err := t.conn.SetDeadline(deadline); err != nil {
return err
}
if err := writeFull(t.conn, frame); err != nil {
return fmt.Errorf("write target frame: %w", err)
}
response, err := readFrame(t.conn)
if err != nil {
return fmt.Errorf("read target ACK: %w", err)
}
if response[2] != frame[2] {
return fmt.Errorf("target ACK command mismatch: got 0x%02X want 0x%02X", response[2], frame[2])
}
if response[3] != 0x01 {
return fmt.Errorf("target rejected command 0x%02X with response 0x%02X", response[2], response[3])
}
if string(response[4:21]) != string(frame[4:21]) {
return errors.New("target ACK identifier mismatch")
}
return nil
}
func (t *Target) closeLocked() {
if t.conn != nil {
_ = t.conn.Close()
t.conn = nil
}
}
func readFrame(reader io.Reader) ([]byte, error) {
header := make([]byte, 24)
if _, err := io.ReadFull(reader, header); err != nil {
return nil, err
}
if (header[0] != '#' || header[1] != '#') && (header[0] != '$' || header[1] != '$') {
return nil, fmt.Errorf("bad GB/T 32960 ACK start %q", header[:2])
}
bodyLength := int(binary.BigEndian.Uint16(header[22:24]))
tail := make([]byte, bodyLength+1)
if _, err := io.ReadFull(reader, tail); err != nil {
return nil, err
}
frame := append(header, tail...)
if got, want := bcc(frame[2:len(frame)-1]), frame[len(frame)-1]; got != want {
return nil, fmt.Errorf("bad GB/T 32960 ACK BCC: got 0x%02X want 0x%02X", got, want)
}
return frame, nil
}
func writeFull(writer io.Writer, value []byte) error {
for len(value) > 0 {
written, err := writer.Write(value)
if err != nil {
return err
}
if written == 0 {
return io.ErrShortWrite
}
value = value[written:]
}
return nil
}