/
device.go
133 lines (108 loc) · 2.48 KB
/
device.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package protocol3000
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/byuoitav/connpool"
"go.uber.org/zap"
)
const (
asciiLineFeed = 0x0d
)
type Device struct {
pool *connpool.Pool
Log *zap.Logger
}
func New(addr string, opts ...Option) *Device {
options := &options{
ttl: _defaultTTL,
delay: _defaultDelay,
}
for _, o := range opts {
o.apply(options)
}
dev := &Device{
pool: &connpool.Pool{
TTL: options.ttl,
Delay: options.delay,
NewConnection: func(ctx context.Context) (net.Conn, error) {
dial := net.Dialer{}
conn, err := dial.DialContext(ctx, "tcp", addr+":5000")
if err != nil {
return nil, err
}
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(5 * time.Second)
}
conn.SetDeadline(deadline)
// read the first 'welcome' line from the connection
buf := make([]byte, 64)
for !bytes.Contains(buf, []byte{asciiLineFeed}) {
_, err := conn.Read(buf)
if err != nil {
conn.Close()
return nil, fmt.Errorf("unable to read welcome line: %w", err)
}
}
return conn, nil
},
},
Log: options.logger,
}
if options.logger != nil {
dev.pool.Logger = options.logger.Sugar()
}
return dev
}
func (d *Device) sendCommand(ctx context.Context, cmd []byte) (string, error) {
var str string
err := d.pool.Do(ctx, func(conn connpool.Conn) error {
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(10 * time.Second)
}
conn.SetDeadline(deadline)
n, err := conn.Write(cmd)
switch {
case err != nil:
return fmt.Errorf("unable to write command: %w", err)
case n != len(cmd):
return fmt.Errorf("unable to write command: wrote %v/%v bytes", n, len(cmd))
}
r, err := conn.ReadUntil(asciiLineFeed, deadline)
if err != nil {
return fmt.Errorf("unable to read response: %w", err)
}
r = bytes.TrimSpace(r)
if len(r) == 0 {
// read the next line, where the error is
r, err = conn.ReadUntil(asciiLineFeed, deadline)
if err != nil {
return fmt.Errorf("unable to read error: %w", err)
}
// parse the error
r = bytes.TrimSpace(r)
resp := string(r)
if !strings.HasPrefix(resp, "ERR ") {
return errors.New(string(r))
}
code, err := strconv.Atoi(strings.TrimPrefix(resp, "ERR "))
if err != nil {
return errors.New(string(r))
}
return errors.New(Error(code))
}
str = string(r)
return nil
})
if err != nil {
return "", err
}
return str, nil
}