From 77fb281555ad9372b3a33c71f296aab19978e80a Mon Sep 17 00:00:00 2001 From: sm Date: Mon, 6 Feb 2023 19:05:25 +0100 Subject: [PATCH] go-client v0.5.0 --- .github/workflows/build.yml | 8 +- client/client.go | 309 ++++++++++++++++++++++------------ client/client_test.go | 71 +++++--- client/conn.go | 17 ++ client/example_client_test.go | 5 +- client/flash/flash.go | 70 ++++++++ client/parse.go | 57 ------- client/rbuf.go | 128 -------------- client/rbuf/rbuf.go | 118 +++++++++++++ client/serial.go | 36 +++- client/tcpclient.go | 33 +++- go.mod | 4 +- go.sum | 4 +- 13 files changed, 526 insertions(+), 334 deletions(-) create mode 100644 client/conn.go create mode 100644 client/flash/flash.go delete mode 100644 client/rbuf.go create mode 100644 client/rbuf/rbuf.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ec33e4..0e67957 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: - name: Setup go uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.20' - run: | go install golang.org/x/lint/golint@latest @@ -25,7 +25,7 @@ jobs: matrix: goos: [linux] goarch: [amd64, arm, arm64] - go: ['1.19'] + go: ['1.20'] fail-fast: false name: Go ${{ matrix.go }} ${{ matrix.goos }} ${{ matrix.goarch }} build @@ -65,7 +65,7 @@ jobs: runs-on: macos-latest strategy: matrix: - go: ['1.19'] + go: ['1.20'] fail-fast: false name: Go ${{ matrix.go }} macOS @@ -95,7 +95,7 @@ jobs: runs-on: windows-latest strategy: matrix: - go: ['1.19'] + go: ['1.20'] fail-fast: false name: Go ${{ matrix.go }} Windows diff --git a/client/client.go b/client/client.go index a79ff23..95f9af2 100644 --- a/client/client.go +++ b/client/client.go @@ -5,11 +5,13 @@ import ( "bufio" "errors" "fmt" - "io" "reflect" "strconv" "sync" "time" + + "github.com/pico-cs/go-client/client/flash" + "github.com/pico-cs/go-client/client/rbuf" ) const ( @@ -35,27 +37,34 @@ func formatBool(b bool) byte { } const ( - cmdHelp = "h" - cmdBoard = "b" - cmdTemp = "ct" - cmdMTE = "mte" - cmdMTCV = "mtcv" - cmdLocoDir = "ld" - cmdLocoSpeed128 = "ls" - cmdLocoFct = "lf" - cmdLocoCVByte = "lcvbyte" - cmdLocoCVBit = "lcvbit" - cmdLocoCV29Bit5 = "lcv29bit5" - cmdLocoLaddr = "lladdr" - cmdLocoCV1718 = "lcv1718" - cmdIOADC = "ioadc" - cmdIOVal = "ioval" - cmdIODir = "iodir" - cmdIOUp = "ioup" - cmdIODown = "iodown" - cmdRBuf = "r" - cmdRBufReset = "rr" - cmdRBufDel = "rd" + cmdHelp = "h" + cmdBoard = "b" + cmdStore = "s" + cmdTemp = "t" + cmdCV = "cv" + cmdMTE = "mte" + cmdLocoDir = "ld" + cmdLocoSpeed128 = "ls" + cmdLocoFct = "lf" + cmdLocoCVByte = "lcvbyte" + cmdLocoCVBit = "lcvbit" + cmdLocoCV29Bit5 = "lcv29bit5" + cmdLocoLaddr = "lladdr" + cmdLocoCV1718 = "lcv1718" + cmdAccFct = "af" + cmdAccTime = "at" + cmdAccStatus = "as" + cmdIOADC = "ioadc" + cmdIOVal = "ioval" + cmdIODir = "iodir" + cmdIOUp = "ioup" + cmdIODown = "iodown" + cmdRefreshBuffer = "r" + cmdRefreshBufferReset = "rr" + cmdRefreshBufferDelete = "rd" + cmdFlash = "f" + cmdFlashFormat = "ff" + cmdReboot = "reboot" ) // error texts. @@ -67,6 +76,8 @@ const ( etNoChange = "nochange" etInvGPIO = "invgpio" etNotImpl = "notimpl" + etNotExec = "notexec" + etIOErr = "ioerr" ) // Command station error definitions. @@ -78,6 +89,8 @@ var ( ErrNoChange = errors.New("no change") ErrInvGPIO = errors.New("invalid GPIO") ErrNotImpl = errors.New("not implemented") + ErrIO = errors.New("io error") + ErrNotExec = errors.New("not executed") ErrUnknown = errors.New("unknown error") ) @@ -89,17 +102,21 @@ var errorMap = map[string]error{ etNoChange: ErrNoChange, etInvGPIO: ErrInvGPIO, etNotImpl: ErrNotImpl, + etIOErr: ErrIO, } -// MTCVIdx represents a main track CV index. -type MTCVIdx byte +// CVIdx represents a command station CV index. +type CVIdx byte // Main track CV index constants. const ( - MTCVNumSyncBit MTCVIdx = iota // Number of DCC synchronization bits - MTCVNumRepeat // DCC command repetitions - MTCVNumRepeatCV // DCC command repetitions for main track CV programming - MTCVNumRepeatAcc // DCC command repetitions for accessory decoders + CVMT CVIdx = iota // Main track configuration flags + CVNumSyncBit // Number of DCC synchronization bits + CVNumRepeat // DCC command repetitions + CVNumRepeatCV // DCC command repetitions for main track CV programming + CVNumRepeatAcc // DCC command repetitions for accessory decoders + CVBidiTS // BiDi (microseconds until power off after end bit) + CVBidiTE // BiDi (microseconds to power on before start of 5th sync bit) ) const ( @@ -108,14 +125,10 @@ const ( timeout = 5 ) -// Conn is a stream oriented connection to the pico board. -type Conn interface { - io.ReadWriteCloser -} - // Client represents a command station client instance. type Client struct { conn Conn + handler func(msg Msg, err error) mu sync.Mutex // mutex for call w *bufio.Writer wg *sync.WaitGroup @@ -126,23 +139,40 @@ type Client struct { // New returns a new client instance. func New(conn Conn, handler func(msg Msg, err error)) *Client { c := &Client{ - conn: conn, - w: bufio.NewWriter(conn), - wg: new(sync.WaitGroup), + conn: conn, + handler: handler, + w: bufio.NewWriter(conn), } + c.startup() + return c +} + +func (c *Client) startup() { + c.wg = new(sync.WaitGroup) var pushCh <-chan string c.replyCh, pushCh = c.reader(c.wg) - c.pusher(c.wg, pushCh, handler) - return c + c.pusher(c.wg, pushCh, c.handler) } -// Close closes the client connection. -func (c *Client) Close() error { +func (c *Client) shutdown() error { err := c.conn.Close() c.wg.Wait() return err } +// Reconnect tries to reconnect the client. +func (c *Client) Reconnect() error { + c.shutdown() // ignore error + if err := c.conn.Reconnect(); err != nil { + return err + } + c.startup() + return nil +} + +// Close closes the client connection. +func (c *Client) Close() error { return c.shutdown() } + type replyKind int const ( @@ -280,7 +310,17 @@ func (c *Client) read() (any, error) { } } -func (c *Client) call(cmd string, args ...any) (any, error) { +func (c *Client) call(cmd string, args ...any) error { + // guarantee: + // - writing is not 'interleaved' and + // - reply order + c.mu.Lock() + defer c.mu.Unlock() + + return c.write(cmd, args) +} + +func (c *Client) callReply(cmd string, args ...any) (any, error) { // guarantee: // - writing is not 'interleaved' and // - reply order @@ -293,8 +333,8 @@ func (c *Client) call(cmd string, args ...any) (any, error) { return c.read() } -func (c *Client) callSingle(cmd string, args ...any) (string, error) { - res, err := c.call(cmd, args...) +func (c *Client) singleReply(cmd string, args ...any) (string, error) { + res, err := c.callReply(cmd, args...) if err != nil { return "", err } @@ -305,8 +345,8 @@ func (c *Client) callSingle(cmd string, args ...any) (string, error) { return string(v), nil } -func (c *Client) callMulti(cmd string, args ...any) ([]string, error) { - res, err := c.call(cmd, args...) +func (c *Client) multiReply(cmd string, args ...any) ([]string, error) { + res, err := c.callReply(cmd, args...) if err != nil { return nil, err } @@ -319,72 +359,81 @@ func (c *Client) callMulti(cmd string, args ...any) ([]string, error) { // Help returns the help texts of the command station. func (c *Client) Help() ([]string, error) { - v, err := c.callMulti(cmdHelp) + v, err := c.multiReply(cmdHelp) if err != nil { return nil, err } return v, nil } -// Board returns bord information like controller type and unique id. +// Board returns board information like controller type and unique id. func (c *Client) Board() (*Board, error) { - v, err := c.callSingle(cmdBoard) + v, err := c.singleReply(cmdBoard) if err != nil { return nil, err } return parseBoard(v) } +// Store stores the command station CVs on flash. +func (c *Client) Store() (bool, error) { + v, err := c.singleReply(cmdStore) + if err != nil { + return false, err + } + return strconv.ParseBool(v) +} + // Temp returns the temperature of the command station. func (c *Client) Temp() (float64, error) { - v, err := c.callSingle(cmdTemp) + v, err := c.singleReply(cmdTemp) if err != nil { return 0, err } return strconv.ParseFloat(v, 64) } -// MTE returns true if the main track DCC sigal generation is enabled, false otherwise. -func (c *Client) MTE() (bool, error) { - v, err := c.callSingle(cmdMTE) +// CV returns the value of a command station CV. +func (c *Client) CV(idx CVIdx) (byte, error) { + v, err := c.singleReply(cmdCV, idx) if err != nil { - return false, err + return 0, err } - return strconv.ParseBool(v) + return parseByte(v) } -// SetMTE sets main track DCC sigal generation whether to enabled or disabled. -func (c *Client) SetMTE(enabled bool) (bool, error) { - v, err := c.callSingle(cmdMTE, enabled) +// SetCV sets the value of a command station CV. +func (c *Client) SetCV(idx CVIdx, val byte) (byte, error) { + v, err := c.singleReply(cmdCV, idx, val) if err != nil { - return false, err + return 0, err } - return strconv.ParseBool(v) + return parseByte(v) } -// MTCV returns the value of a main track CV. -func (c *Client) MTCV(idx MTCVIdx) (byte, error) { - v, err := c.callSingle(cmdMTCV, idx) +// MTE returns true if the main track DCC sigal generation is enabled, false otherwise. +func (c *Client) MTE() (bool, error) { + v, err := c.singleReply(cmdMTE) if err != nil { - return 0, err + return false, err } - return parseByte(v) + return strconv.ParseBool(v) } -// SetMTCV sets the value of a main track CV. -func (c *Client) SetMTCV(idx MTCVIdx, val byte) (byte, error) { - v, err := c.callSingle(cmdMTCV, idx, val) +// SetMTE sets main track DCC sigal generation whether to enabled or disabled. +func (c *Client) SetMTE(enabled bool) (bool, error) { + v, err := c.singleReply(cmdMTE, enabled) if err != nil { - return 0, err + return false, err } - return parseByte(v) + return strconv.ParseBool(v) } // LocoDir returns the direction of a loco. // true : forward direction // false: backward direction func (c *Client) LocoDir(addr uint) (bool, error) { - v, err := c.callSingle(cmdLocoDir, addr) + v, err := c.singleReply(cmdLocoDir, addr) if err != nil { return false, err } @@ -395,7 +444,7 @@ func (c *Client) LocoDir(addr uint) (bool, error) { // true : forward direction // false: backward direction func (c *Client) SetLocoDir(addr uint, dir bool) (bool, error) { - v, err := c.callSingle(cmdLocoDir, addr, dir) + v, err := c.singleReply(cmdLocoDir, addr, dir) if err != nil { return false, err } @@ -404,7 +453,7 @@ func (c *Client) SetLocoDir(addr uint, dir bool) (bool, error) { // ToggleLocoDir toggles the direction of a loco. func (c *Client) ToggleLocoDir(addr uint) (bool, error) { - v, err := c.callSingle(cmdLocoDir, addr, charToggle) + v, err := c.singleReply(cmdLocoDir, addr, charToggle) if err != nil { return false, err } @@ -416,7 +465,7 @@ func (c *Client) ToggleLocoDir(addr uint) (bool, error) { // 1 : emergency stop // 2-127: 126 speed steps func (c *Client) LocoSpeed128(addr uint) (uint, error) { - v, err := c.callSingle(cmdLocoSpeed128, addr) + v, err := c.singleReply(cmdLocoSpeed128, addr) if err != nil { return 0, err } @@ -428,7 +477,7 @@ func (c *Client) LocoSpeed128(addr uint) (uint, error) { // 1 : emergency stop // 2-127: 126 speed steps func (c *Client) SetLocoSpeed128(addr, speed uint) (uint, error) { - v, err := c.callSingle(cmdLocoSpeed128, addr, speed) + v, err := c.singleReply(cmdLocoSpeed128, addr, speed) if err != nil { return 0, err } @@ -437,7 +486,7 @@ func (c *Client) SetLocoSpeed128(addr, speed uint) (uint, error) { // LocoFct returns a function value of a loco. func (c *Client) LocoFct(addr, no uint) (bool, error) { - v, err := c.callSingle(cmdLocoFct, addr, no) + v, err := c.singleReply(cmdLocoFct, addr, no) if err != nil { return false, err } @@ -446,7 +495,7 @@ func (c *Client) LocoFct(addr, no uint) (bool, error) { // SetLocoFct sets a function value of a loco. func (c *Client) SetLocoFct(addr, no uint, fct bool) (bool, error) { - v, err := c.callSingle(cmdLocoFct, addr, no, fct) + v, err := c.singleReply(cmdLocoFct, addr, no, fct) if err != nil { return false, err } @@ -455,7 +504,7 @@ func (c *Client) SetLocoFct(addr, no uint, fct bool) (bool, error) { // ToggleLocoFct toggles a function value of a loco. func (c *Client) ToggleLocoFct(addr, no uint) (bool, error) { - v, err := c.callSingle(cmdLocoFct, addr, no, charToggle) + v, err := c.singleReply(cmdLocoFct, addr, no, charToggle) if err != nil { return false, err } @@ -464,7 +513,7 @@ func (c *Client) ToggleLocoFct(addr, no uint) (bool, error) { // SetLocoCVByte sets the indexed CV byte value of a loco. func (c *Client) SetLocoCVByte(addr, idx uint, val byte) (byte, error) { - v, err := c.callSingle(cmdLocoCVByte, addr, idx, val) + v, err := c.singleReply(cmdLocoCVByte, addr, idx, val) if err != nil { return 0, err } @@ -473,7 +522,7 @@ func (c *Client) SetLocoCVByte(addr, idx uint, val byte) (byte, error) { // SetLocoCVBit sets the indexed CV bit value of a loco. func (c *Client) SetLocoCVBit(addr, idx uint, bit byte, val bool) (bool, error) { - v, err := c.callSingle(cmdLocoCVBit, addr, idx, bit, val) + v, err := c.singleReply(cmdLocoCVBit, addr, idx, bit, val) if err != nil { return false, err } @@ -482,7 +531,7 @@ func (c *Client) SetLocoCVBit(addr, idx uint, bit byte, val bool) (bool, error) // SetLocoCV29Bit5 sets the CV 29 bit 5 value of a loco. func (c *Client) SetLocoCV29Bit5(addr uint, bit bool) (bool, error) { - v, err := c.callSingle(cmdLocoCV29Bit5, addr, bit) + v, err := c.singleReply(cmdLocoCV29Bit5, addr, bit) if err != nil { return false, err } @@ -491,7 +540,7 @@ func (c *Client) SetLocoCV29Bit5(addr uint, bit bool) (bool, error) { // SetLocoLaddr sets the long address of a loco. func (c *Client) SetLocoLaddr(addr, laddr uint) (uint, error) { - v, err := c.callSingle(cmdLocoLaddr, addr, laddr) + v, err := c.singleReply(cmdLocoLaddr, addr, laddr) if err != nil { return 0, err } @@ -500,7 +549,7 @@ func (c *Client) SetLocoLaddr(addr, laddr uint) (uint, error) { // LocoCV1718 returns (calculates) the CV17 and CV18 values (long address) from a loco address. func (c *Client) LocoCV1718(addr uint) (byte, byte, error) { - v, err := c.callSingle(cmdLocoCV1718, addr) + v, err := c.singleReply(cmdLocoCV1718, addr) if err != nil { return 0, 0, err } @@ -509,16 +558,43 @@ func (c *Client) LocoCV1718(addr uint) (byte, byte, error) { // IOADC returns the 'raw' value of the ADC input. func (c *Client) IOADC(input uint) (float64, error) { - v, err := c.callSingle(cmdIOADC, input) + v, err := c.singleReply(cmdIOADC, input) if err != nil { return 0, err } return strconv.ParseFloat(v, 64) } +// SetAccFct sets the function value of an accessory decoder on output out. +func (c *Client) SetAccFct(addr uint, out byte, fct bool) (bool, error) { + v, err := c.singleReply(cmdAccFct, addr, out, fct) + if err != nil { + return false, err + } + return strconv.ParseBool(v) +} + +// SetAccTime sets the activation time of an accessory decoder on output out. +func (c *Client) SetAccTime(addr uint, out, time byte) (bool, error) { + v, err := c.singleReply(cmdAccTime, addr, out, time) + if err != nil { + return false, err + } + return strconv.ParseBool(v) +} + +// SetAccStatus sets the status byte of an extended accessory decoder. +func (c *Client) SetAccStatus(addr uint, status byte) (bool, error) { + v, err := c.singleReply(cmdAccStatus, addr, status) + if err != nil { + return false, err + } + return strconv.ParseBool(v) +} + // IOVal returns the boolean value of the GPIO. func (c *Client) IOVal(cmd, gpio uint) (bool, error) { - v, err := c.callSingle(cmdIOVal, cmd, gpio) + v, err := c.singleReply(cmdIOVal, cmd, gpio) if err != nil { return false, err } @@ -527,7 +603,7 @@ func (c *Client) IOVal(cmd, gpio uint) (bool, error) { // SetIOVal sets the boolean value of the GPIO. func (c *Client) SetIOVal(cmd, gpio uint, value bool) (bool, error) { - v, err := c.callSingle(cmdIOVal, cmd, gpio, value) + v, err := c.singleReply(cmdIOVal, cmd, gpio, value) if err != nil { return false, err } @@ -536,7 +612,7 @@ func (c *Client) SetIOVal(cmd, gpio uint, value bool) (bool, error) { // ToggleIOVal toggles the value of the GPIO. func (c *Client) ToggleIOVal(cmd, gpio uint) (bool, error) { - v, err := c.callSingle(cmdIOVal, cmd, gpio, charToggle) + v, err := c.singleReply(cmdIOVal, cmd, gpio, charToggle) if err != nil { return false, err } @@ -547,7 +623,7 @@ func (c *Client) ToggleIOVal(cmd, gpio uint) (bool, error) { // false: in // true: out func (c *Client) IODir(cmd, gpio uint) (bool, error) { - v, err := c.callSingle(cmdIODir, cmd, gpio) + v, err := c.singleReply(cmdIODir, cmd, gpio) if err != nil { return false, err } @@ -558,7 +634,7 @@ func (c *Client) IODir(cmd, gpio uint) (bool, error) { // false: in // true: out func (c *Client) SetIODir(cmd, gpio uint, value bool) (bool, error) { - v, err := c.callSingle(cmdIODir, cmd, gpio, value) + v, err := c.singleReply(cmdIODir, cmd, gpio, value) if err != nil { return false, err } @@ -567,7 +643,7 @@ func (c *Client) SetIODir(cmd, gpio uint, value bool) (bool, error) { // ToggleIODir toggles the direction of the GPIO. func (c *Client) ToggleIODir(cmd, gpio uint) (bool, error) { - v, err := c.callSingle(cmdIODir, cmd, gpio, charToggle) + v, err := c.singleReply(cmdIODir, cmd, gpio, charToggle) if err != nil { return false, err } @@ -576,7 +652,7 @@ func (c *Client) ToggleIODir(cmd, gpio uint) (bool, error) { // IOUp returns the pull-up status of the GPIO. func (c *Client) IOUp(cmd, gpio uint) (bool, error) { - v, err := c.callSingle(cmdIOUp, cmd, gpio) + v, err := c.singleReply(cmdIOUp, cmd, gpio) if err != nil { return false, err } @@ -585,7 +661,7 @@ func (c *Client) IOUp(cmd, gpio uint) (bool, error) { // SetIOUp sets the pull-up status of the GPIO. func (c *Client) SetIOUp(cmd, gpio uint, value bool) (bool, error) { - v, err := c.callSingle(cmdIOUp, cmd, gpio, value) + v, err := c.singleReply(cmdIOUp, cmd, gpio, value) if err != nil { return false, err } @@ -594,7 +670,7 @@ func (c *Client) SetIOUp(cmd, gpio uint, value bool) (bool, error) { // ToggleIOUp toggles the pull-up status of the GPIO. func (c *Client) ToggleIOUp(cmd, gpio uint) (bool, error) { - v, err := c.callSingle(cmdIOUp, cmd, gpio, charToggle) + v, err := c.singleReply(cmdIOUp, cmd, gpio, charToggle) if err != nil { return false, err } @@ -603,7 +679,7 @@ func (c *Client) ToggleIOUp(cmd, gpio uint) (bool, error) { // IODown returns the pull-down status of the GPIO. func (c *Client) IODown(cmd, gpio uint) (bool, error) { - v, err := c.callSingle(cmdIODown, cmd, gpio) + v, err := c.singleReply(cmdIODown, cmd, gpio) if err != nil { return false, err } @@ -612,7 +688,7 @@ func (c *Client) IODown(cmd, gpio uint) (bool, error) { // SetIODown sets the pull-down status of the GPIO. func (c *Client) SetIODown(cmd, gpio uint, value bool) (bool, error) { - v, err := c.callSingle(cmdIODown, cmd, gpio, value) + v, err := c.singleReply(cmdIODown, cmd, gpio, value) if err != nil { return false, err } @@ -621,36 +697,59 @@ func (c *Client) SetIODown(cmd, gpio uint, value bool) (bool, error) { // ToggleIODown toggles the pull-down status of the GPIO. func (c *Client) ToggleIODown(cmd, gpio uint) (bool, error) { - v, err := c.callSingle(cmdIODown, cmd, gpio, charToggle) + v, err := c.singleReply(cmdIODown, cmd, gpio, charToggle) if err != nil { return false, err } return strconv.ParseBool(v) } -// RBuf returns the command station refresh buffer (debugging). -func (c *Client) RBuf() (*RBuf, error) { - v, err := c.callMulti(cmdRBuf) +// RefreshBuffer returns the command station refresh buffer (debugging). +func (c *Client) RefreshBuffer() (*rbuf.Buffer, error) { + v, err := c.multiReply(cmdRefreshBuffer) if err != nil { return nil, err } - return parseRBuf(v) + return rbuf.Parse(v) } -// RBufReset resets the refresh buffer (debugging). -func (c *Client) RBufReset() (bool, error) { - v, err := c.callSingle(cmdRBufReset) +// RefreshBufferReset resets the refresh buffer (debugging). +func (c *Client) RefreshBufferReset() (bool, error) { + v, err := c.singleReply(cmdRefreshBufferReset) if err != nil { return false, err } return strconv.ParseBool(v) } -// RBufDel deletes address addr from refresh buffer (debugging). -func (c *Client) RBufDel(addr uint) (uint, error) { - v, err := c.callSingle(cmdRBufDel, addr) +// RefreshBufferDelete deletes address addr from refresh buffer (debugging). +func (c *Client) RefreshBufferDelete(addr uint) (uint, error) { + v, err := c.singleReply(cmdRefreshBufferDelete, addr) if err != nil { return 0, err } return parseUint(v) } + +// Flash returns the command station flash data (debugging). +func (c *Client) Flash() (*flash.Flash, error) { + v, err := c.multiReply(cmdFlash) + if err != nil { + return nil, err + } + return flash.Parse(v) +} + +// FlashFormat formats the command station flash (debugging). +func (c *Client) FlashFormat() (bool, error) { + v, err := c.singleReply(cmdFlashFormat) + if err != nil { + return false, err + } + return strconv.ParseBool(v) +} + +// Reboot reboots the command station (debugging). +func (c *Client) Reboot() error { + return c.call(cmdReboot) +} diff --git a/client/client_test.go b/client/client_test.go index 4211150..324e1c3 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,11 +1,11 @@ package client_test import ( - "log" "os" "testing" "github.com/pico-cs/go-client/client" + "github.com/pico-cs/go-client/client/rbuf" ) const ( @@ -65,21 +65,21 @@ const ( func testDCCSyncBits(c *client.Client, t *testing.T) { - defaultSyncBits, err := c.MTCV(client.MTCVNumSyncBit) + defaultSyncBits, err := c.CV(client.CVNumSyncBit) if err != nil { t.Fatal(err) } t.Logf("default DCC sync bits %d", defaultSyncBits) // test sync bits range minSyncBits <= sync bits <= maxSyncBits - syncBits, err := c.SetMTCV(client.MTCVNumSyncBit, minSyncBits-1) + syncBits, err := c.SetCV(client.CVNumSyncBit, minSyncBits-1) if err != nil { t.Fatal(err) } if syncBits != minSyncBits { t.Errorf("invalid number of sync bits %d - expected %d", syncBits, minSyncBits) } - syncBits, err = c.SetMTCV(client.MTCVNumSyncBit, maxSyncBits+1) + syncBits, err = c.SetCV(client.CVNumSyncBit, maxSyncBits+1) if err != nil { t.Fatal(err) } @@ -88,7 +88,7 @@ func testDCCSyncBits(c *client.Client, t *testing.T) { } // set back to default - syncBits, err = c.SetMTCV(client.MTCVNumSyncBit, defaultSyncBits) + syncBits, err = c.SetCV(client.CVNumSyncBit, defaultSyncBits) if err != nil { t.Fatal(err) } @@ -97,52 +97,71 @@ func testDCCSyncBits(c *client.Client, t *testing.T) { } } -func testRBuf(c *client.Client, t *testing.T) { - rbuf, err := c.RBuf() +func testRefreshBuffer(c *client.Client, t *testing.T) { + buf, err := c.RefreshBuffer() if err != nil { t.Fatal(err) } t.Log("refresh buffer") - t.Log(rbuf) - for _, entry := range rbuf.Entries { + t.Log(buf) + for _, entry := range buf.Entries { t.Log(entry) } } -func testRBufDel(c *client.Client, t *testing.T) { +func testRefreshBufferDelete(c *client.Client, t *testing.T) { // reset refresh buffer - c.RBufReset() + c.RefreshBufferReset() if _, err := c.SetLocoSpeed128(3, 12); err != nil { // add loco to buffer - log.Fatal(err) + t.Fatal(err) } t.Logf("added loco: %d", 3) if _, err := c.SetLocoSpeed128(10, 33); err != nil { // add loco to buffer - log.Fatal(err) + t.Fatal(err) } t.Logf("added loco: %d", 10) - addr, err := c.RBufDel(10) + addr, err := c.RefreshBufferDelete(10) if err != nil { - log.Fatal(err) + t.Fatal(err) } t.Logf("deleted loco: %d", addr) - rbuf, err := c.RBuf() + buf, err := c.RefreshBuffer() if err != nil { t.Fatal(err) } - if len(rbuf.Entries) != 1 { - log.Fatalf("invalid number of refresh buffer entries %d - expected 1", len(rbuf.Entries)) + if len(buf.Entries) != 1 { + t.Fatalf("invalid number of refresh buffer entries %d - expected 1", len(buf.Entries)) + } + entry := buf.Entries[0] + if buf.First != int(entry[rbuf.Idx]) || buf.Next != int(entry[rbuf.Idx]) { + t.Fatalf("invalid refresh buffer header first %d next %d - expected %d", buf.First, buf.Next, entry[rbuf.Idx]) + } + if entry[rbuf.Prev] != entry[rbuf.Idx] || entry[rbuf.Next] != entry[rbuf.Idx] { + t.Fatalf("invalid refresh buffer entry prev %d next %d - expected %d", entry[rbuf.Prev], entry[rbuf.Next], entry[rbuf.Idx]) + } +} + +func testFlash(c *client.Client, t *testing.T) { + flash, err := c.Flash() + if err != nil { + t.Fatal(err) } - entry := rbuf.Entries[0] - if rbuf.First != int(entry.Idx) || rbuf.Next != int(entry.Idx) { - log.Fatalf("invalid refresh buffer header first %d next %d - expected %d", rbuf.First, rbuf.Next, entry.Idx) + t.Log("flash") + t.Log(flash) +} + +func testReboot(c *client.Client, t *testing.T) { + if err := c.Reboot(); err != nil { + t.Fatal(err) } - if entry.Prev != entry.Idx || entry.Next != entry.Idx { - log.Fatalf("invalid refresh buffer entry prev %d next %d - expected %d", entry.Prev, entry.Next, entry.Idx) + if err := c.Reconnect(); err != nil { + t.Fatal(err) } + testBoard(c, t) } func testRun(conn client.Conn, t *testing.T) { @@ -155,8 +174,10 @@ func testRun(conn client.Conn, t *testing.T) { {"Temp", testTemp}, {"DCCSyncBits", testDCCSyncBits}, {"MTEnabled", testMTEnabled}, - {"RefreshBuffer", testRBuf}, - {"RefreshBufferDel", testRBufDel}, + {"RefreshBuffer", testRefreshBuffer}, + {"RefreshBufferDelete", testRefreshBufferDelete}, + {"Flash", testFlash}, + {"Reboot", testReboot}, } c := client.New(conn, nil) diff --git a/client/conn.go b/client/conn.go new file mode 100644 index 0000000..45946d4 --- /dev/null +++ b/client/conn.go @@ -0,0 +1,17 @@ +package client + +import ( + "io" + "time" +) + +const ( + reconnectRetry = 10 + reconnectWait = 5 * time.Second // wait some time to reconnect +) + +// Conn is a stream oriented connection to the pico board. +type Conn interface { + Reconnect() error + io.ReadWriteCloser +} diff --git a/client/example_client_test.go b/client/example_client_test.go index d447d76..3f98ae6 100644 --- a/client/example_client_test.go +++ b/client/example_client_test.go @@ -2,6 +2,7 @@ package client_test import ( "log" + "time" "github.com/pico-cs/go-client/client" ) @@ -9,6 +10,8 @@ import ( // ExampleClient shows how to establish a pico-cs command station client. func ExampleClient() { + time.Sleep(2 * time.Second) + defaultPortName, err := client.SerialDefaultPortName() if err != nil { log.Fatal(err) @@ -29,7 +32,7 @@ func ExampleClient() { }) defer client.Close() - // read borad information. + // read board information. board, err := client.Board() if err != nil { log.Fatal(err) diff --git a/client/flash/flash.go b/client/flash/flash.go new file mode 100644 index 0000000..8d5f25c --- /dev/null +++ b/client/flash/flash.go @@ -0,0 +1,70 @@ +// Package flash contains flash types, methods and functions. +package flash + +import ( + "fmt" + "strconv" + "strings" +) + +// Flash represents a command station flash memory. +type Flash struct { + ReadIdx, WriteIdx, PageNo uint + Content []byte +} + +func (f *Flash) String() string { + b := strings.Builder{} + b.WriteString(fmt.Sprintf("read idx %d write idx %d page no %d content:", f.ReadIdx, f.WriteIdx, f.PageNo)) + for i, v := range f.Content { + if i%32 == 0 { + b.WriteString("\n") + } + b.WriteString(fmt.Sprintf("%02x ", v)) + } + return b.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)) + } + + 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) + } + + readIdx, err := strconv.ParseUint(values[0], 10, 0) + if err != nil { + return nil, err + } + writeIdx, err := strconv.ParseUint(values[1], 10, 0) + if err != nil { + return nil, err + } + pageNo, err := strconv.ParseUint(values[2], 10, 0) + if err != nil { + return nil, err + } + flash := &Flash{ + ReadIdx: uint(readIdx), + WriteIdx: uint(writeIdx), + PageNo: uint(pageNo), + Content: []byte{}, + } + + // content + for i := 1; i < len(lines); i++ { + values := strings.Split(strings.TrimSpace(lines[i]), " ") + for _, value := range values { + u64, err := strconv.ParseUint(value, 16, 8) + if err != nil { + return nil, err + } + flash.Content = append(flash.Content, byte(u64)) + } + } + return flash, nil +} diff --git a/client/parse.go b/client/parse.go index 4f65244..b3ebb10 100644 --- a/client/parse.go +++ b/client/parse.go @@ -6,14 +6,6 @@ import ( "strings" ) -func parseInt(s string) (int, error) { - i64, err := strconv.ParseInt(s, 10, 0) - if err != nil { - return 0, err - } - return int(i64), nil -} - func parseUint(s string) (uint, error) { u64, err := strconv.ParseUint(s, 10, 0) if err != nil { @@ -45,52 +37,3 @@ func parseByteTuple(s string) (byte, byte, error) { } return b1, b2, nil } - -// parser parses string values. -type parser struct { - values []string - idx int - errIdx int - err error -} - -func (p *parser) reset(values []string) { - p.values = values - p.idx = 0 - p.errIdx = 0 - p.err = nil -} - -func (p *parser) Error() string { - return fmt.Errorf("parser error index %d %w", p.errIdx, p.err).Error() -} - -func (p *parser) parseInt() int { - i, err := parseInt(p.values[p.idx]) - if err != nil { - p.errIdx = p.idx - p.err = err - } - p.idx++ - return i -} - -func (p *parser) parseUInt() uint { - u, err := parseUint(p.values[p.idx]) - if err != nil { - p.errIdx = p.idx - p.err = err - } - p.idx++ - return u -} - -func (p *parser) parseByte() byte { - b, err := parseByte(p.values[p.idx]) - if err != nil { - p.errIdx = p.idx - p.err = err - } - p.idx++ - return b -} diff --git a/client/rbuf.go b/client/rbuf.go deleted file mode 100644 index 92340fa..0000000 --- a/client/rbuf.go +++ /dev/null @@ -1,128 +0,0 @@ -package client - -import ( - "fmt" - "strings" - - "golang.org/x/exp/slices" -) - -// RBuf represents a command station refresh buffer. -type RBuf struct { - First, Next int - Entries []*RBufEntry -} - -func (buf *RBuf) String() string { - return fmt.Sprintf("first %d next %d num entries %d", buf.First, buf.Next, len(buf.Entries)) -} - -// RBufEntry represents a command station refresh buffer entry. -type RBufEntry struct { - Idx byte - Addr uint - MaxRefreshCmd byte - RefreshCmd byte - DirSpeed byte - F0_4 byte - F5_8 byte - F9_12 byte //lint:ignore ST1003 complains about ALL_CAPS - F5_12 byte //lint:ignore ST1003 complains about ALL_CAPS - F13_20 byte //lint:ignore ST1003 complains about ALL_CAPS - F21_28 byte //lint:ignore ST1003 complains about ALL_CAPS - F29_36 byte //lint:ignore ST1003 complains about ALL_CAPS - F37_44 byte //lint:ignore ST1003 complains about ALL_CAPS - F45_52 byte //lint:ignore ST1003 complains about ALL_CAPS - F53_60 byte //lint:ignore ST1003 complains about ALL_CAPS - F61_68 byte //lint:ignore ST1003 complains about ALL_CAPS - Prev byte - Next byte -} - -func (e *RBufEntry) String() string { - ppDirSpeed := func(fct byte) string { return fmt.Sprintf("%1b-%03d", fct>>7, fct&0x7f) } - ppF0_4 := func(fct byte) string { return fmt.Sprintf("%1b-%04b", fct>>4, fct&0x0f) } - ppFct := func(fct byte) string { return fmt.Sprintf("%04b-%04b", fct>>4, fct&0x0f) } - - return fmt.Sprintf( - "idx %3d addr %5d maxRefreshCmd %3d RefreshCmd %3d dirSpeed %s f0_4 %s f5_8 %04b f9_12 %04b f5_12 %s f13_20 %s f21_28 %s f29_36 %s f37_44 %s f45_52 %s f53_60 %s f61_68 %s prev %3d next %3d", - e.Idx, - e.Addr, - e.MaxRefreshCmd, - e.RefreshCmd, - ppDirSpeed(e.DirSpeed), - ppF0_4(e.F0_4), - e.F5_8, - e.F9_12, - ppFct(e.F5_12), - ppFct(e.F13_20), - ppFct(e.F21_28), - ppFct(e.F29_36), - ppFct(e.F37_44), - ppFct(e.F45_52), - ppFct(e.F53_60), - ppFct(e.F61_68), - e.Prev, - e.Next, - ) -} - -const ( - numRBufValue = 2 - numRBufEntryValue = 18 -) - -func parseRBuf(lines []string) (*RBuf, error) { - if len(lines) < 1 { - return nil, fmt.Errorf("parse refresh buffer error - invalid number of lines %d", len(lines)) - } - - values := strings.Split(lines[0], " ") - if len(values) != numRBufValue { - return nil, fmt.Errorf("parse refresh buffer error - invalid number of values %d - expected %d", len(values), numRBufValue) - } - - p := &parser{values: values} - buf := &RBuf{ - First: p.parseInt(), - Next: p.parseInt(), - } - if p.err != nil { - return nil, p - } - - // entries - for i := 1; i < len(lines); i++ { - values := strings.Split(lines[i], " ") - if len(values) != numRBufEntryValue { - return nil, fmt.Errorf("parse refresh buffer error - invalid number of entry values %d - expected %d", len(values), numRBufValue) - } - - p.reset(values) - buf.Entries = append(buf.Entries, &RBufEntry{ - Idx: p.parseByte(), - Addr: p.parseUInt(), - MaxRefreshCmd: p.parseByte(), - RefreshCmd: p.parseByte(), - DirSpeed: p.parseByte(), - F0_4: p.parseByte(), - F5_8: p.parseByte(), - F9_12: p.parseByte(), - F5_12: p.parseByte(), - F13_20: p.parseByte(), - F21_28: p.parseByte(), - F29_36: p.parseByte(), - F37_44: p.parseByte(), - F45_52: p.parseByte(), - F53_60: p.parseByte(), - F61_68: p.parseByte(), - Prev: p.parseByte(), - Next: p.parseByte(), - }) - if p.err != nil { - return nil, p - } - slices.SortFunc(buf.Entries, func(a, b *RBufEntry) bool { return a.Idx < b.Idx }) - } - return buf, nil -} diff --git a/client/rbuf/rbuf.go b/client/rbuf/rbuf.go new file mode 100644 index 0000000..1415253 --- /dev/null +++ b/client/rbuf/rbuf.go @@ -0,0 +1,118 @@ +// Package rbuf contains refresh buffer types, methods and functions. +package rbuf + +import ( + "fmt" + "strconv" + "strings" + + "golang.org/x/exp/slices" +) + +// refresh buffer indices +const ( + Idx = iota + MSB + LSB + MaxRefreshCmd + RefreshCmd + DirSpeed + F0_4 + F5_8 + F9_12 //lint:ignore ST1003 complains about ALL_CAPS + F5_12 //lint:ignore ST1003 complains about ALL_CAPS + F13_20 //lint:ignore ST1003 complains about ALL_CAPS + F21_28 //lint:ignore ST1003 complains about ALL_CAPS + F29_36 //lint:ignore ST1003 complains about ALL_CAPS + F37_44 //lint:ignore ST1003 complains about ALL_CAPS + F45_52 //lint:ignore ST1003 complains about ALL_CAPS + F53_60 //lint:ignore ST1003 complains about ALL_CAPS + F61_68 //lint:ignore ST1003 complains about ALL_CAPS + Prev + Next + NumBytes +) + +// Entry represents a command station refresh buffer entry. +type Entry [NumBytes]byte + +func (e *Entry) String() string { + ppDirSpeed := func(fct byte) string { return fmt.Sprintf("%1b-%03d", fct>>7, fct&0x7f) } + ppF0_4 := func(fct byte) string { return fmt.Sprintf("%1b-%04b", fct>>4, fct&0x0f) } + ppFct := func(fct byte) string { return fmt.Sprintf("%04b-%04b", fct>>4, fct&0x0f) } + + return fmt.Sprintf( + "idx %3d addr %5d maxRefreshCmd %3d RefreshCmd %3d dirSpeed %s f0_4 %s f5_8 %04b f9_12 %04b f5_12 %s f13_20 %s f21_28 %s f29_36 %s f37_44 %s f45_52 %s f53_60 %s f61_68 %s prev %3d next %3d", + e[Idx], + (uint16(e[MSB])<<8)|uint16(e[LSB]), + e[MaxRefreshCmd], + e[RefreshCmd], + ppDirSpeed(e[DirSpeed]), + ppF0_4(e[F0_4]), + e[F5_8], + e[F9_12], + ppFct(e[F5_12]), + ppFct(e[F13_20]), + ppFct(e[F21_28]), + ppFct(e[F29_36]), + ppFct(e[F37_44]), + ppFct(e[F45_52]), + ppFct(e[F53_60]), + ppFct(e[F61_68]), + e[Prev], + e[Next], + ) +} + +// Buffer represents a command station refresh buffer. +type Buffer struct { + First, Next int + Entries []Entry +} + +func (buf *Buffer) String() string { + return fmt.Sprintf("first %d next %d num entries %d", buf.First, buf.Next, len(buf.Entries)) +} + +// Parse parses the refresh buffer send by a command station. +func Parse(lines []string) (*Buffer, error) { + if len(lines) < 1 { + return nil, fmt.Errorf("parse refresh buffer error - invalid number of lines %d", len(lines)) + } + + values := strings.Split(lines[0], " ") + if len(values) != 2 { + return nil, fmt.Errorf("parse refresh buffer error - invalid number of values %d - expected %d", len(values), 2) + } + + first, err := strconv.ParseInt(values[0], 10, 0) + if err != nil { + return nil, err + } + next, err := strconv.ParseInt(values[1], 10, 0) + if err != nil { + return nil, err + } + buf := &Buffer{ + First: int(first), + Next: int(next), + Entries: make([]Entry, len(lines)-1), + } + + // entries + for i := 1; i < len(lines); i++ { + values := strings.Split(lines[i], " ") + if len(values) != NumBytes { + return nil, fmt.Errorf("parse refresh buffer error - invalid number of entry values %d - expected %d", len(values), NumBytes) + } + for j := 0; j < NumBytes; j++ { + u64, err := strconv.ParseUint(values[j], 10, 8) + if err != nil { + return nil, err + } + buf.Entries[i-1][j] = byte(u64) + } + slices.SortFunc(buf.Entries, func(a, b Entry) bool { return a[Idx] < b[Idx] }) + } + return buf, nil +} diff --git a/client/serial.go b/client/serial.go index dd1c1de..3ef1cdf 100644 --- a/client/serial.go +++ b/client/serial.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strings" + "time" "go.bug.st/serial" ) @@ -29,6 +30,7 @@ func SerialDefaultPortName() (string, error) { type Serial struct { portName string port serial.Port + closed bool } // NewSerial returns a new serial connection instance. @@ -40,14 +42,38 @@ func NewSerial(portName string) (*Serial, error) { } } + s := &Serial{portName: portName} + if err := s.connect(); err != nil { + return nil, err + } + return s, nil +} + +func (s *Serial) connect() error { mode := &serial.Mode{ BaudRate: baudRate, } - port, err := serial.Open(portName, mode) + var err error + s.port, err = serial.Open(s.portName, mode) if err != nil { - return nil, fmt.Errorf("error opening serial device: %s - %w", portName, err) + return fmt.Errorf("error opening serial device: %s - %w", s.portName, err) + } + s.port.ResetInputBuffer() + s.port.ResetOutputBuffer() + 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 &Serial{portName: portName, port: port}, nil + return err } // Read implements the Conn interface. @@ -62,5 +88,9 @@ func (s *Serial) Write(p []byte) (n int, err error) { // Close implements the Conn interface. func (s *Serial) Close() error { + if s.closed { + return nil + } + s.closed = true return s.port.Close() } diff --git a/client/tcpclient.go b/client/tcpclient.go index 682fb4c..fc92dd5 100644 --- a/client/tcpclient.go +++ b/client/tcpclient.go @@ -1,13 +1,17 @@ package client -import "net" +import ( + "net" + "time" +) // DefaultTCPPort is the default TCP Port used by Pico W. const DefaultTCPPort = "4242" // TCPClient provides a TCP/IP connection to to the Raspberry Pi Pico W. type TCPClient struct { - conn net.Conn + host, port string + conn net.Conn } // NewTCPClient returns a new TCP/IP connection instance. @@ -16,13 +20,28 @@ func NewTCPClient(host, port string) (*TCPClient, error) { port = DefaultTCPPort } - addr := net.JoinHostPort(host, port) - - conn, err := net.Dial("tcp", addr) - if err != nil { + c := &TCPClient{host: host, port: port} + if err := c.connect(); err != nil { return nil, err } - return &TCPClient{conn: conn}, nil + return c, nil +} + +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. diff --git a/go.mod b/go.mod index 92a4b67..c6b753b 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/pico-cs/go-client -go 1.19 +go 1.20 require ( go.bug.st/serial v1.5.0 - golang.org/x/exp v0.0.0-20230116083435-1de6713980de + golang.org/x/exp v0.0.0-20230206171751-46f607a40771 ) require ( diff --git a/go.sum b/go.sum index e03aea1..9ce38f6 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb 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-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0= -golang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=