From 62c8585b0df5e0df0972ba9675edc0a026bf1498 Mon Sep 17 00:00:00 2001 From: Stefan Miller Date: Sun, 24 Mar 2024 23:31:14 +0100 Subject: [PATCH] go-client v0.5.1 --- .github/workflows/build.yml | 63 +++++++++++++++++----------------- .github/workflows/reuse.yml | 4 +-- .reuse/dep5 | 2 +- Makefile | 25 ++++++++------ README.md | 2 +- client/client.go | 56 ++++++++++++++++++++---------- client/client_test.go | 9 +++-- client/conn.go | 4 +-- client/flash/flash.go | 14 ++++---- client/rbuf/rbuf.go | 7 ++-- client/serial.go | 68 ++++++++++++++++++++----------------- client/tcpclient.go | 18 ++-------- go.mod | 9 ++--- go.sum | 14 ++++---- 14 files changed, 159 insertions(+), 136 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e67957..6ecfb41 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,20 +2,25 @@ name: build on: [push] jobs: - lint: + vetlint: runs-on: ubuntu-latest - name: Go lint + name: Go vet lint steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.22' - - run: | + - name: Vet + run: | + go vet ./... + + - name: Lint + run: | go install golang.org/x/lint/golint@latest golint ./... @@ -25,7 +30,7 @@ jobs: matrix: goos: [linux] goarch: [amd64, arm, arm64] - go: ['1.20'] + go: ['1.22.1'] fail-fast: false name: Go ${{ matrix.go }} ${{ matrix.goos }} ${{ matrix.goarch }} build @@ -34,6 +39,7 @@ jobs: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} GOARM: 6 + GOTOOLCHAIN: go${{ matrix.go }} steps: @@ -42,13 +48,13 @@ jobs: sudo apt-get update sudo apt-get -y install qemu-user - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go }} - + go-version: ${{ matrix.go }} + - name: Get dependencies run: | go get -v -t -d ./... @@ -57,25 +63,24 @@ jobs: run: | go build -v ./... - - name: Vet - run: | - go vet ./... - build-macos: runs-on: macos-latest strategy: matrix: - go: ['1.20'] + go: ['1.22.1'] fail-fast: false name: Go ${{ matrix.go }} macOS + + env: + GOTOOLCHAIN: go${{ matrix.go }} steps: - - - uses: actions/checkout@v3 + + - uses: actions/checkout@v4 - name: Setup go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} @@ -87,25 +92,24 @@ jobs: run: | go build -v ./... - - name: Vet - run: | - go vet ./... - build-windows: runs-on: windows-latest strategy: matrix: - go: ['1.20'] + go: ['1.22.1'] fail-fast: false name: Go ${{ matrix.go }} Windows + + env: + GOTOOLCHAIN: go${{ matrix.go }} steps: - - - uses: actions/checkout@v3 + + - uses: actions/checkout@v4 - name: Setup go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} @@ -116,8 +120,3 @@ jobs: - name: Build run: | go build -v ./... - - - name: Vet - run: | - go vet ./... - \ No newline at end of file diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index 9b90355..c9169a4 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -6,6 +6,6 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: REUSE Compliance Check - uses: fsfe/reuse-action@v1.1 \ No newline at end of file + uses: fsfe/reuse-action@v2 \ No newline at end of file diff --git a/.reuse/dep5 b/.reuse/dep5 index 46e883c..2ffcb31 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -4,5 +4,5 @@ Upstream-Contact: stefan.miller_at_sap.com Source: https://github.com/pico-cs/go-client Files: * -Copyright: 2021-2023 Stefan Miller +Copyright: 2021-2024 Stefan Miller License: GPL-3.0-or-later \ No newline at end of file diff --git a/Makefile b/Makefile index b133d12..01cdd91 100644 --- a/Makefile +++ b/Makefile @@ -8,25 +8,28 @@ all: go vet ./... golint -set_exit_status=true ./... staticcheck -checks all -fail none ./... + golangci-lint run ./... go test ./... +#see fsfe reuse tool (https://git.fsfe.org/reuse/tool) @echo "reuse (license) check" - reuse lint + pipx run reuse lint + +#go generate +generate: + @echo "generate" + go generate ./... #install additional tools tools: +#install stringer + @echo "install latest stringer version" + go install golang.org/x/tools/cmd/stringer@latest #install linter @echo "install latest go linter version" go install golang.org/x/lint/golint@latest #install staticcheck @echo "install latest staticcheck version" go install honnef.co/go/tools/cmd/staticcheck@latest - -#install fsfe reuse tool (https://git.fsfe.org/reuse/tool) -# pre-conditions: -# - Python 3.6+ -# - pip -# install pre-conditions in Debian like linux distros: -# - sudo apt install python3 -# - sudo apt install python3-pip -reuse: - pip3 install --user --upgrade reuse +#install golangci-lint + @echo "install latest golangci-lint version" + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest diff --git a/README.md b/README.md index d6de12b..fb6065c 100644 --- a/README.md +++ b/README.md @@ -6,5 +6,5 @@ ## Licensing -Copyright 2021-2023 Stefan Miller and pico-cs contributers. Please see our [LICENSE](LICENSE.md) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/pico-cs/go-client). +Copyright 2021-2024 Stefan Miller and pico-cs contributers. Please see our [LICENSE](LICENSE.md) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/pico-cs/go-client). diff --git a/client/client.go b/client/client.go index 95f9af2..4bff213 100644 --- a/client/client.go +++ b/client/client.go @@ -122,7 +122,7 @@ const ( const ( replyChSize = 1 pushChSize = 100 - timeout = 5 + timeout = 30 ) // Client represents a command station client instance. @@ -160,16 +160,33 @@ func (c *Client) shutdown() error { return err } -// Reconnect tries to reconnect the client. +func (c *Client) reconnect() error { + var err error + for i := 0; i < reconnectRetry; i++ { + time.Sleep(reconnectWait) + if err = c.conn.Connect(); err == nil { + return nil + } + } + return err +} + +// Reconnect reconnects the client. func (c *Client) Reconnect() error { - c.shutdown() // ignore error - if err := c.conn.Reconnect(); err != nil { + c.shutdown() //nolint: errcheck + if err := c.reconnect(); err != nil { return err } c.startup() return nil } +// IsSerialConn returns true if the connection is serial, false otherwise. +func (c *Client) IsSerialConn() bool { + _, ok := c.conn.(*Serial) + return ok +} + // Close closes the client connection. func (c *Client) Close() error { return c.shutdown() } @@ -212,12 +229,14 @@ func (c *Client) reader(wg *sync.WaitGroup) (<-chan any, <-chan string) { scanner := bufio.NewScanner(c.conn) - //TODO check scanner.Error() - multi := false var multiMsg []string for scanner.Scan() { + if err := scanner.Err(); err != nil { + replyCh <- err + } + //log.Printf("message: %s", scanner.Text()) rk, msg := c.parseReply(scanner.Bytes()) @@ -244,9 +263,6 @@ func (c *Client) reader(wg *sync.WaitGroup) (<-chan any, <-chan string) { multi = false } } - //if err := scanner.Err(); err != nil { - // fmt.Fprintln(os.Stderr, "reading standard input:", err) - //} close(replyCh) close(pushCh) @@ -270,24 +286,25 @@ func (c *Client) pusher(wg *sync.WaitGroup, pushCh <-chan string, handler func(M } func (c *Client) write(cmd string, args []any) error { - c.w.WriteByte(tagStart) - c.w.WriteString(cmd) + c.w.WriteByte(tagStart) //nolint: errcheck + c.w.WriteString(cmd) //nolint: errcheck for _, arg := range args { - c.w.WriteByte(' ') // argument separator + // argument separator + c.w.WriteByte(' ') //nolint: errcheck rv := reflect.ValueOf(arg) switch rv.Kind() { case reflect.Bool: - c.w.WriteByte(formatBool(rv.Bool())) + c.w.WriteByte(formatBool(rv.Bool())) //nolint: errcheck case reflect.Uint8, reflect.Uint: - c.w.WriteString(strconv.FormatUint(rv.Uint(), 10)) + c.w.WriteString(strconv.FormatUint(rv.Uint(), 10)) //nolint: errcheck case reflect.String: - c.w.WriteString(rv.String()) + c.w.WriteString(rv.String()) //nolint: errcheck default: panic(fmt.Sprintf("invalid argument %[1]v type %[1]T", arg)) // should never happen } } - c.w.WriteByte('\r') + c.w.WriteByte('\r') //nolint: errcheck if err := c.w.Flush(); err != nil { return err } @@ -749,7 +766,12 @@ func (c *Client) FlashFormat() (bool, error) { return strconv.ParseBool(v) } +var rebootWait = 5 * time.Second + // Reboot reboots the command station (debugging). func (c *Client) Reboot() error { - return c.call(cmdReboot) + err := c.call(cmdReboot) + // wait some time to be sure that device is re-booted. + time.Sleep(rebootWait) + return err } diff --git a/client/client_test.go b/client/client_test.go index 324e1c3..68b1859 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -111,8 +111,9 @@ func testRefreshBuffer(c *client.Client, t *testing.T) { func testRefreshBufferDelete(c *client.Client, t *testing.T) { // reset refresh buffer - c.RefreshBufferReset() - + if _, err := c.RefreshBufferReset(); err != nil { + t.Fatal(err) + } if _, err := c.SetLocoSpeed128(3, 12); err != nil { // add loco to buffer t.Fatal(err) } @@ -159,6 +160,10 @@ func testReboot(c *client.Client, t *testing.T) { t.Fatal(err) } if err := c.Reconnect(); err != nil { + if c.IsSerialConn() { // after reboot connection port might be different. + t.Log(err) + return + } t.Fatal(err) } testBoard(c, t) diff --git a/client/conn.go b/client/conn.go index 45946d4..16f6cc2 100644 --- a/client/conn.go +++ b/client/conn.go @@ -7,11 +7,11 @@ import ( const ( reconnectRetry = 10 - reconnectWait = 5 * time.Second // wait some time to reconnect + reconnectWait = 500 * time.Millisecond ) // Conn is a stream oriented connection to the pico board. type Conn interface { - Reconnect() error + Connect() error io.ReadWriteCloser } diff --git a/client/flash/flash.go b/client/flash/flash.go index 8d5f25c..48489c5 100644 --- a/client/flash/flash.go +++ b/client/flash/flash.go @@ -28,25 +28,25 @@ func (f *Flash) String() string { // Parse parses the flash memory send by a command station. func Parse(lines []string) (*Flash, error) { if len(lines) < 1 { - return nil, fmt.Errorf("parse flash error - invalid number of lines %d", len(lines)) + return nil, fmt.Errorf("flash parse error - invalid number of lines %d", len(lines)) } values := strings.Split(lines[0], " ") if len(values) != 3 { - return nil, fmt.Errorf("parse flash error - invalid number of values %d - expected %d", len(values), 3) + return nil, fmt.Errorf("flash parse error - invalid number of values %d - expected %d", len(values), 3) } readIdx, err := strconv.ParseUint(values[0], 10, 0) if err != nil { - return nil, err + return nil, fmt.Errorf("flash parse error - read index: %w", err) } writeIdx, err := strconv.ParseUint(values[1], 10, 0) if err != nil { - return nil, err + return nil, fmt.Errorf("flash parse error - write index: %w", err) } pageNo, err := strconv.ParseUint(values[2], 10, 0) if err != nil { - return nil, err + return nil, fmt.Errorf("flash parse error - page number: %w", err) } flash := &Flash{ ReadIdx: uint(readIdx), @@ -58,10 +58,10 @@ func Parse(lines []string) (*Flash, error) { // content for i := 1; i < len(lines); i++ { values := strings.Split(strings.TrimSpace(lines[i]), " ") - for _, value := range values { + for j, value := range values { u64, err := strconv.ParseUint(value, 16, 8) if err != nil { - return nil, err + return nil, fmt.Errorf("flash parse error - content line %d column %d: %w", i, j, err) } flash.Content = append(flash.Content, byte(u64)) } diff --git a/client/rbuf/rbuf.go b/client/rbuf/rbuf.go index 1415253..c87daaf 100644 --- a/client/rbuf/rbuf.go +++ b/client/rbuf/rbuf.go @@ -2,11 +2,11 @@ package rbuf import ( + "cmp" "fmt" + "slices" "strconv" "strings" - - "golang.org/x/exp/slices" ) // refresh buffer indices @@ -112,7 +112,8 @@ func Parse(lines []string) (*Buffer, error) { } buf.Entries[i-1][j] = byte(u64) } - slices.SortFunc(buf.Entries, func(a, b Entry) bool { return a[Idx] < b[Idx] }) + // slices.SortFunc(buf.Entries, func(a, b Entry) bool { return a[Idx] < b[Idx] }) } + slices.SortFunc(buf.Entries, func(a, b Entry) int { return cmp.Compare(a[Idx], b[Idx]) }) return buf, nil } diff --git a/client/serial.go b/client/serial.go index 3ef1cdf..c682d23 100644 --- a/client/serial.go +++ b/client/serial.go @@ -1,29 +1,53 @@ package client import ( - "errors" "fmt" + "runtime" "strings" - "time" "go.bug.st/serial" ) const baudRate = 115200 // default baud rate of the Raspberry Pi pico. -// SerialDefaultPortName returns the default serial port name if a detection is possible and an error otherwise. -func SerialDefaultPortName() (string, error) { +// Serial default port errors. +var ( + ErrSerialDefaultPortPathMissing = fmt.Errorf("missing default serial port path for %s", runtime.GOOS) + ErrSerialDefaultPortNotFound = fmt.Errorf("default port could not be detected for %s", defaultSerialPortPath) +) + +func defaultPortsList() ([]string, error) { + if defaultSerialPortPath == "" { + return nil, ErrSerialDefaultPortPathMissing + } portNames, err := serial.GetPortsList() if err != nil { - return "", err + return nil, err } - + var result []string for _, name := range portNames { if strings.HasPrefix(name, defaultSerialPortPath) { - return name, nil + result = append(result, name) } } - return "", errors.New("default port could not be detected") + return result, nil +} + +// SerialDefaultPortName returns the default serial port name if a detection is possible and an error otherwise. +func SerialDefaultPortName() (string, error) { + portNames, err := defaultPortsList() + if err != nil { + return "", err + } + switch len(portNames) { + case 0: + return "", ErrSerialDefaultPortNotFound + case 1: + return portNames[0], nil + default: // more than one. + return "", fmt.Errorf("default serial port not unique %v", portNames) + + } } // Serial provides a serial connection to to the Raspberry Pi Pico. @@ -35,21 +59,15 @@ type Serial struct { // NewSerial returns a new serial connection instance. func NewSerial(portName string) (*Serial, error) { - if portName == "" { - var err error - if portName, err = SerialDefaultPortName(); err != nil { - return nil, err - } - } - s := &Serial{portName: portName} - if err := s.connect(); err != nil { + if err := s.Connect(); err != nil { return nil, err } return s, nil } -func (s *Serial) connect() error { +// Connect connect the serial port.s +func (s *Serial) Connect() error { mode := &serial.Mode{ BaudRate: baudRate, } @@ -58,24 +76,12 @@ func (s *Serial) connect() error { if err != nil { return fmt.Errorf("error opening serial device: %s - %w", s.portName, err) } - s.port.ResetInputBuffer() - s.port.ResetOutputBuffer() + s.port.ResetInputBuffer() //nolint: errcheck + s.port.ResetOutputBuffer() //nolint: errcheck s.closed = false return nil } -// Reconnect tries to reconnect the serial connection. -func (s *Serial) Reconnect() (err error) { - err = nil - for i := 0; i < reconnectRetry; i++ { - time.Sleep(reconnectWait) - if err = s.connect(); err == nil { - return nil - } - } - return err -} - // Read implements the Conn interface. func (s *Serial) Read(p []byte) (n int, err error) { return s.port.Read(p) diff --git a/client/tcpclient.go b/client/tcpclient.go index fc92dd5..511df9b 100644 --- a/client/tcpclient.go +++ b/client/tcpclient.go @@ -2,7 +2,6 @@ package client import ( "net" - "time" ) // DefaultTCPPort is the default TCP Port used by Pico W. @@ -21,29 +20,18 @@ func NewTCPClient(host, port string) (*TCPClient, error) { } c := &TCPClient{host: host, port: port} - if err := c.connect(); err != nil { + if err := c.Connect(); err != nil { return nil, err } return c, nil } -func (c *TCPClient) connect() (err error) { +// Connect connects to the tcp address. +func (c *TCPClient) Connect() (err error) { c.conn, err = net.Dial("tcp", net.JoinHostPort(c.host, c.port)) return err } -// Reconnect tries to reconnect the TCP client. -func (c *TCPClient) Reconnect() (err error) { - err = nil - for i := 0; i < reconnectRetry; i++ { - time.Sleep(reconnectWait) - if err = c.connect(); err == nil { - return nil - } - } - return err -} - // Read implements the Conn interface. func (c *TCPClient) Read(p []byte) (n int, err error) { return c.conn.Read(p) diff --git a/go.mod b/go.mod index c6b753b..cd2abfd 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,10 @@ module github.com/pico-cs/go-client -go 1.20 +go 1.22.1 -require ( - go.bug.st/serial v1.5.0 - golang.org/x/exp v0.0.0-20230206171751-46f607a40771 -) +require go.bug.st/serial v1.6.2 require ( github.com/creack/goselect v0.1.2 // indirect - golang.org/x/sys v0.4.0 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 9ce38f6..2eab9f3 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,14 @@ github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -go.bug.st/serial v1.5.0 h1:ThuUkHpOEmCVXxGEfpoExjQCS2WBVV4ZcUKVYInM9T4= -go.bug.st/serial v1.5.0/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= -golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= -golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= +go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=