194 lines
4.5 KiB
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
|
|
}
|