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 }