feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
193
go/vehicle-gateway/internal/feichibridge/target.go
Normal file
193
go/vehicle-gateway/internal/feichibridge/target.go
Normal file
@@ -0,0 +1,193 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user