Files
2026-06-24 06:12:52 +02:00

108 lines
2.4 KiB
Go

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
}