Skip to content

Commit 140ba74

Browse files
matthewrankinclaude
andcommitted
Reuse bufio.Reader, fix examples, and fix lint issues
Store a single *bufio.Reader on Controller instead of allocating one per Query/QueryController call, avoiding repeated allocations and potential buffered data loss. Fix examples to use updated query.Bool API and non-constant format strings. Fix misspelling and unchecked error return flagged by linter. Reformat long lines with golines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 15451ca commit 140ba74

File tree

8 files changed

+64
-17
lines changed

8 files changed

+64
-17
lines changed

.golangci.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
version: "2"
2+
3+
linters:
4+
default: standard
5+
enable:
6+
- staticcheck
7+
- govet
8+
- errcheck
9+
- ineffassign
10+
- unused
11+
- gosec
12+
- misspell
13+
- bodyclose
14+
- contextcheck
15+
16+
formatters:
17+
enable:
18+
- gofmt
19+
- goimports
20+
- golines
21+
22+
run:
23+
timeout: 5m
24+
tests: true
25+
26+
issues:
27+
max-issues-per-linter: 0
28+
max-same-issues: 0

CLAUDE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
Go package for communicating with Prologix GPIB-USB and GPIB-ETHERNET controllers, which bridge a computer to GPIB (IEEE 488) test instruments. Part of the [gotmc](https://github.com/gotmc) ecosystem and designed to work with the [ivi](https://github.com/gotmc/ivi) package for standardized instrument control.
7+
Go package for communicating with Prologix GPIB-USB and GPIB-ETHERNET controllers, which bridge a computer to GPIB (IEEE 488) test instruments. Part of the [gotmc](https://github.com/gotmc) ecosystem and designed to work with the [ivi](https://github.com/gotmc/ivi) package for standardized instrument control. Requires Go 1.25+.
88

99
## Build & Test Commands
1010

@@ -23,6 +23,8 @@ go test -run TestIsPrimaryAddressValid ./...
2323

2424
# HTML coverage report
2525
just cover
26+
27+
# Run example (requires hardware): just key3631 /dev/ttyUSB0 5
2628
```
2729

2830
`just` (Justfile) is the task runner; there is no Makefile.
@@ -34,13 +36,14 @@ The package uses a layered design separating transport from GPIB protocol logic:
3436
- **Transport layer** (`driver/`): Provides `io.ReadWriter` implementations for different connection types. Currently only `driver/vcp/` (Virtual COM Port via serial) is implemented. The `driver/usb/` and `driver/net/` directories are placeholders for future D2XX and Ethernet support.
3537
- **Controller** (`controller.go`): Core type wrapping any `io.ReadWriter`. Constructed via `NewController()` with functional options (`WithSecondaryAddress`, `WithDebug`, `WithAR488`). Handles GPIB address setup, controller initialization commands, and the `++` command prefix for Prologix-specific commands.
3638
- **Commands** (`commands.go`): Methods on `Controller` for Prologix `++` commands (e.g., `ClearDevice`, `SetReadTimeout`, `Version`, `InstrumentAddress`).
39+
- **Examples** (`examples/vcp/`): Working examples for real instruments (E3631A, DS345, Fluke 45, 33220A). All follow the same pattern: open VCP → create Controller → query version → run device commands → restore front panel → close.
3740

3841
Key design points:
3942
- `Controller` distinguishes between instrument communication (`Command`, `Query`, `Write`) and Prologix controller commands (`CommandController`, `QueryController`) which prepend `++`.
4043
- Context-aware I/O: `Controller` checks if the underlying `io.ReadWriter` implements optional `ContextReader`/`ContextWriter` interfaces. If so, it delegates directly; otherwise it falls back to goroutine-based context handling.
4144
- The `WithAR488()` option skips `verbose` and `savecfg` commands for Arduino AR488 compatibility.
4245
- `Query` automatically sends `++read eoi` when auto read-after-write is disabled.
43-
- Binary data methods (`Write`/`Read`) vs string methods (`WriteString`, `Command`, `Query`) handle GPIB character escaping differently.
46+
- Binary data methods (`Write`/`Read`) vs string methods (`WriteString`, `Command`, `Query`) handle GPIB character escaping differently. Prologix strips unescaped LF, CR, ESC, and `+` characters, so `Write` escapes them automatically.
4447
- VCP serial config is hardcoded to Prologix defaults: 115200 baud, 7 data bits, even parity, 1 stop bit.
4548

4649
## Dependencies

commands.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,14 @@ func (c *Controller) InstrumentAddress() (int, int, error) {
101101
var errs []error
102102

103103
if int(pri) != c.primaryAddr {
104-
errs = append(errs,
105-
fmt.Errorf("internal state mismatch, primary address was %d now %d", c.primaryAddr, pri))
104+
errs = append(
105+
errs,
106+
fmt.Errorf(
107+
"internal state mismatch, primary address was %d now %d",
108+
c.primaryAddr,
109+
pri,
110+
),
111+
)
106112
}
107113
c.primaryAddr = int(pri)
108114

@@ -117,8 +123,14 @@ func (c *Controller) InstrumentAddress() (int, int, error) {
117123
}
118124
}
119125
if c.secondaryAddr != int(sec) {
120-
errs = append(errs,
121-
fmt.Errorf("internal state mismatch, secondary address was %d now %d", c.secondaryAddr, sec))
126+
errs = append(
127+
errs,
128+
fmt.Errorf(
129+
"internal state mismatch, secondary address was %d now %d",
130+
c.secondaryAddr,
131+
sec,
132+
),
133+
)
122134
}
123135
c.secondaryAddr = int(sec)
124136

controller.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type ContextWriter interface {
3030
// Controller models a GPIB controller-in-charge.
3131
type Controller struct {
3232
rw io.ReadWriter
33+
reader *bufio.Reader
3334
primaryAddr int
3435
hasSecondaryAddr bool
3536
secondaryAddr int
@@ -57,6 +58,7 @@ func NewController(
5758
) (*Controller, error) {
5859
c := Controller{
5960
rw: rw,
61+
reader: bufio.NewReader(rw),
6062
primaryAddr: addr,
6163
hasSecondaryAddr: false,
6264
auto: false,
@@ -130,7 +132,7 @@ func WithSecondaryAddress(addr int) ControllerOption {
130132
// WithDebug causes commands and responses to be logged.
131133
func WithDebug() ControllerOption { return func(c *Controller) { c.debug = true } }
132134

133-
// WithAR488 slightly alters the init commands, for compatiblity with the
135+
// WithAR488 slightly alters the init commands, for compatibility with the
134136
// Arduino-based AR488. Specifically, we do not emit 'verbose 0', nor do
135137
// we toggle savecfg.
136138
func WithAR488() ControllerOption { return func(c *Controller) { c.ar488 = true } }
@@ -258,7 +260,7 @@ func (c *Controller) Query(cmd string) (string, error) {
258260
return "", fmt.Errorf("error sending %q command: %w", readCmd, err)
259261
}
260262
}
261-
s, err := bufio.NewReader(c.rw).ReadString(c.eotChar)
263+
s, err := c.reader.ReadString(c.eotChar)
262264
if errors.Is(err, io.EOF) {
263265
log.Printf("found EOF")
264266
return s, nil
@@ -276,7 +278,7 @@ func (c *Controller) QueryController(cmd string) (string, error) {
276278
if err != nil {
277279
return "", err
278280
}
279-
s, err := bufio.NewReader(c.rw).ReadString(c.eotChar)
281+
s, err := c.reader.ReadString(c.eotChar)
280282
if c.debug {
281283
log.Printf("read data: %q", s)
282284
}

driver/vcp/vcp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (vcp *VCP) ReadContext(ctx context.Context, p []byte) (n int, err error) {
6565
if err := vcp.port.SetReadTimeout(timeout); err != nil {
6666
return 0, err
6767
}
68-
defer vcp.port.SetReadTimeout(serial.NoTimeout)
68+
defer func() { _ = vcp.port.SetReadTimeout(serial.NoTimeout) }()
6969
}
7070
n, err = vcp.port.Read(p)
7171
if err != nil {

examples/vcp/ds345/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func main() {
116116
}
117117
for _, cmd := range cmds {
118118
log.Printf("Sending command: %s", cmd)
119-
err = gpib.Command(cmd)
119+
err = gpib.Command("%s", cmd)
120120
if err != nil {
121121
log.Fatal(err)
122122
}

examples/vcp/e3631a/main.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"flag"
1010
"io"
1111
"log"
12+
"strings"
1213

1314
"github.com/gotmc/prologix"
1415
"github.com/gotmc/prologix/driver/vcp"
15-
"github.com/gotmc/query"
1616
)
1717

1818
var (
@@ -112,17 +112,18 @@ func main() {
112112
}
113113

114114
for _, cmd := range cmds {
115-
err = gpib.Command(cmd)
115+
err = gpib.Command("%s", cmd)
116116
if err != nil {
117117
log.Fatal(err)
118118
}
119119
}
120120

121121
// Query the output state
122-
state, err := query.Bool(gpib, "OUTP:STAT?")
122+
stateStr, err := gpib.Query("OUTP:STAT?")
123123
if err != nil && err != io.EOF {
124124
log.Fatalf("error querying serial port: %s", err)
125125
}
126+
state := strings.TrimSpace(stateStr) == "1"
126127
if state {
127128
log.Println("output is enabled")
128129
} else {
@@ -135,7 +136,7 @@ func main() {
135136
}
136137

137138
for _, cmd := range cmds {
138-
err = gpib.Command(cmd)
139+
err = gpib.Command("%s", cmd)
139140
if err != nil {
140141
log.Fatal(err)
141142
}
@@ -156,10 +157,11 @@ func main() {
156157
log.Printf("voltage = %s", volt)
157158

158159
// Query the output state
159-
state, err = query.Bool(gpib, "OUTP:STAT?")
160+
stateStr, err = gpib.Query("OUTP:STAT?")
160161
if err != nil && err != io.EOF {
161162
log.Fatalf("error querying serial port: %s", err)
162163
}
164+
state = strings.TrimSpace(stateStr) == "1"
163165
if state {
164166
log.Println("output is enabled")
165167
} else {

examples/vcp/key33220a/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func main() {
125125
}
126126
for _, cmd := range cmds {
127127
log.Printf("Sending command: %s", cmd)
128-
err = gpib.Command(cmd)
128+
err = gpib.Command("%s", cmd)
129129
if err != nil {
130130
log.Fatal(err)
131131
}

0 commit comments

Comments
 (0)