package scpi import ( "bufio" "fmt" "net" "strings" "sync" "time" ) // transport is the request/response channel to an instrument. The raw-socket // implementation below speaks line-oriented SCPI over TCP (the common // "SCPI raw" / port 5025 convention). The interface is kept narrow so a VXI-11 // (ONC-RPC) transport can be added later without touching the data source. type transport interface { // query sends cmd and returns the instrument's single-line response. query(cmd string) (string, error) // write sends cmd and does not wait for a response. write(cmd string) error close() } // rawSocket is a line-based SCPI transport over a single TCP connection. The // connection is dialled lazily and dropped on any I/O error so the next call // reconnects. Calls are serialised by mu because SCPI is request/response. type rawSocket struct { addr string timeout time.Duration terminator string mu sync.Mutex conn net.Conn br *bufio.Reader } func newRawSocket(addr string, timeout time.Duration, terminator string) *rawSocket { if timeout <= 0 { timeout = 3 * time.Second } if terminator == "" { terminator = "\n" } return &rawSocket{addr: addr, timeout: timeout, terminator: terminator} } func (s *rawSocket) dialLocked() error { if s.conn != nil { return nil } conn, err := net.DialTimeout("tcp", s.addr, s.timeout) if err != nil { return fmt.Errorf("scpi: dial %s: %w", s.addr, err) } s.conn = conn s.br = bufio.NewReader(conn) return nil } func (s *rawSocket) closeLocked() { if s.conn != nil { _ = s.conn.Close() s.conn = nil s.br = nil } } func (s *rawSocket) close() { s.mu.Lock() s.closeLocked() s.mu.Unlock() } func (s *rawSocket) sendLocked(cmd string) error { _ = s.conn.SetDeadline(time.Now().Add(s.timeout)) if _, err := s.conn.Write([]byte(cmd + s.terminator)); err != nil { s.closeLocked() return fmt.Errorf("scpi: write: %w", err) } return nil } func (s *rawSocket) write(cmd string) error { s.mu.Lock() defer s.mu.Unlock() if err := s.dialLocked(); err != nil { return err } return s.sendLocked(cmd) } func (s *rawSocket) query(cmd string) (string, error) { s.mu.Lock() defer s.mu.Unlock() if err := s.dialLocked(); err != nil { return "", err } if err := s.sendLocked(cmd); err != nil { return "", err } line, err := s.br.ReadString('\n') if err != nil { s.closeLocked() return "", fmt.Errorf("scpi: read: %w", err) } return strings.TrimRight(line, "\r\n"), nil }