From 056c22e4ae6988d6be36732ce4c7490fd70c9197 Mon Sep 17 00:00:00 2001 From: Umberto Baldi <34278123+umbynos@users.noreply.github.com> Date: Wed, 18 Aug 2021 10:32:49 +0200 Subject: [PATCH] Add serial binary communication (#653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update bufferflow_timedraw as bufferflow_timed * remove old commands * remove utf8 decoding with timedraw buffer type * binary support (WIP) * use switch case * fixed test deps * socketio test connection is working ๐ŸŽ‰ (with the correct python-socketio version) * add callback to capture returned message, add new test for serial * fix tests: "socketio.exceptions.ConnectionError: Connection refused by the server" * minor optimizations: data and buf are already an array of bytes * enhanced a bit how the logic of the serial works * enhance a lot test on serial communication (with different buffer types) The tests should be skipped on the CI (no board connected) * update and enhance commands output (the space in front of `<` and `>` is required) ๐Ÿคทโ€โ™‚๏ธ * increased sleeptime, remove harcoded message[i]: should work on different systems * generalize the tests * Apply suggestions from code review Co-authored-by: per1234 * add sketch used for testing * Fix panic closing closed channel * apply suggestions * Partially revert #e80400b7ddbbc2e8f34f1e6701b55102c3a99289 * ๐Ÿงน(cleanup) and ๐Ÿ› ๏ธ(refactoring) of bufferflow stuff * extract code in helper function and uniform the code reintroduce the closing of input channel (it's required) * optimize the handling of data coming from the serial port * uniform default bufferflow and ๐Ÿงน * forgot to fix this in #621 * apply suggestions from code review โœจ * remove timedbinary: it's the same as timedraw except for the casting * Escape html commands string * forgot to remove timed_binary * remove useless id field (was unused) * remove useless channel done & other stuff * make sendNoBuf more general: will be used later ๐Ÿ˜ * add `sendraw` command to send base64 encoded bytes, add tests (for send raw and for open/close port) * forgot to skip test_sendraw_serial on CI * update comments * refactor tests * remove BlockUntilReady because it was unused Co-authored-by: per1234 Co-authored-by: Silvano Cerza --- bufferflow.go | 46 +---- bufferflow_default.go | 81 ++++----- bufferflow_timed.go | 129 +++++--------- bufferflow_timedraw.go | 121 +++++-------- go.sum | 1 + hub.go | 94 +++++----- poetry.lock | 136 +++++++++++---- pyproject.toml | 9 +- serial.go | 33 ++-- serialport.go | 223 ++++++++++-------------- test/common.py | 7 + test/conftest.py | 41 ++++- test/test_ws.py | 187 ++++++++++++++++++++ test/testdata/SerialEcho/SerialEcho.ino | 18 ++ 14 files changed, 644 insertions(+), 482 deletions(-) create mode 100644 test/common.py create mode 100644 test/test_ws.py create mode 100644 test/testdata/SerialEcho/SerialEcho.ino diff --git a/bufferflow.go b/bufferflow.go index 42ee9e9b4..07d34698b 100644 --- a/bufferflow.go +++ b/bufferflow.go @@ -1,49 +1,7 @@ package main -import ( -//"log" -//"time" -) - -var availableBufferAlgorithms = []string{"default", "timed", "timedraw"} - -type BufferMsg struct { - Cmd string - Port string - TriggeringResponse string - //Desc string - //Desc string -} - type Bufferflow interface { Init() - BlockUntilReady(cmd string, id string) (bool, bool) // implement this method - //JustQueue(cmd string, id string) bool // implement this method - OnIncomingData(data string) // implement this method - ClearOutSemaphore() // implement this method - BreakApartCommands(cmd string) []string // implement this method - Pause() // implement this method - Unpause() // implement this method - SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool // implement this method - SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool // implement this method - SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool // implement this method - SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool // implement this method - SeeIfSpecificCommandsReturnNoResponse(cmd string) bool // implement this method - ReleaseLock() // implement this method - IsBufferGloballySendingBackIncomingData() bool // implement this method - Close() // implement this method -} - -/*data packets returned to client*/ -type DataCmdComplete struct { - Cmd string - Id string - P string - BufSize int `json:"-"` - D string `json:"-"` -} - -type DataPerLine struct { - P string - D string + OnIncomingData(data string) // implement this method + Close() // implement this method } diff --git a/bufferflow_default.go b/bufferflow_default.go index 7f37ac3d2..2d6d6d281 100644 --- a/bufferflow_default.go +++ b/bufferflow_default.go @@ -1,71 +1,52 @@ package main import ( + "encoding/json" + log "github.com/sirupsen/logrus" ) type BufferflowDefault struct { - Name string - Port string + port string + output chan<- []byte + input chan string + done chan bool } -var () +func NewBufferflowDefault(port string, output chan<- []byte) *BufferflowDefault { + return &BufferflowDefault{ + port: port, + output: output, + input: make(chan string), + done: make(chan bool), + } +} func (b *BufferflowDefault) Init() { log.Println("Initting default buffer flow (which means no buffering)") + go b.consumeInput() } -func (b *BufferflowDefault) BlockUntilReady(cmd string, id string) (bool, bool) { - //log.Printf("BlockUntilReady() start\n") - return true, false +func (b *BufferflowDefault) consumeInput() { +Loop: + for { + select { + case data := <-b.input: + m := SpPortMessage{b.port, data} + message, _ := json.Marshal(m) + b.output <- message + case <-b.done: + break Loop //this is required, a simple break statement would only exit the innermost switch statement + } + } + close(b.input) // close the input channel at the end of the computation } func (b *BufferflowDefault) OnIncomingData(data string) { - //log.Printf("OnIncomingData() start. data:%v\n", data) -} - -// Clean out b.sem so it can truly block -func (b *BufferflowDefault) ClearOutSemaphore() { -} - -func (b *BufferflowDefault) BreakApartCommands(cmd string) []string { - return []string{cmd} -} - -func (b *BufferflowDefault) Pause() { - return -} - -func (b *BufferflowDefault) Unpause() { - return -} - -func (b *BufferflowDefault) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { - return false -} - -func (b *BufferflowDefault) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { - return false -} - -func (b *BufferflowDefault) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { - return false -} - -func (b *BufferflowDefault) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { - return false -} - -func (b *BufferflowDefault) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { - return false -} - -func (b *BufferflowDefault) ReleaseLock() { -} - -func (b *BufferflowDefault) IsBufferGloballySendingBackIncomingData() bool { - return false + b.input <- data } func (b *BufferflowDefault) Close() { + b.done <- true + close(b.done) } diff --git a/bufferflow_timed.go b/bufferflow_timed.go index d0be90227..b89427f37 100644 --- a/bufferflow_timed.go +++ b/bufferflow_timed.go @@ -8,104 +8,57 @@ import ( ) type BufferflowTimed struct { - Name string - Port string - Output chan []byte - Input chan string - done chan bool - ticker *time.Ticker + port string + output chan<- []byte + input chan string + done chan bool + ticker *time.Ticker + sPort string + bufferedOutput string } -var ( - bufferedOutput string - sPort string -) +func NewBufferflowTimed(port string, output chan<- []byte) *BufferflowTimed { + return &BufferflowTimed{ + port: port, + output: output, + input: make(chan string), + done: make(chan bool), + ticker: time.NewTicker(16 * time.Millisecond), + sPort: "", + bufferedOutput: "", + } +} func (b *BufferflowTimed) Init() { log.Println("Initting timed buffer flow (output once every 16ms)") - bufferedOutput = "" - sPort = "" - - go func() { - b.ticker = time.NewTicker(16 * time.Millisecond) - b.done = make(chan bool) - Loop: - for { - select { - case data := <-b.Input: - bufferedOutput = bufferedOutput + data - sPort = b.Port - case <-b.ticker.C: - if bufferedOutput != "" { - m := SpPortMessage{sPort, bufferedOutput} - buf, _ := json.Marshal(m) - // data is now encoded in base64 format - // need a decoder on the other side - b.Output <- []byte(buf) - bufferedOutput = "" - sPort = "" - } - case <-b.done: - break Loop + go b.consumeInput() +} + +func (b *BufferflowTimed) consumeInput() { +Loop: + for { + select { + case data := <-b.input: // use the buffer and append data to it + b.bufferedOutput = b.bufferedOutput + data + b.sPort = b.port + case <-b.ticker.C: // after 16ms send the buffered output message + if b.bufferedOutput != "" { + m := SpPortMessage{b.sPort, b.bufferedOutput} + buf, _ := json.Marshal(m) + b.output <- buf + // reset the buffer and the port + b.bufferedOutput = "" + b.sPort = "" } + case <-b.done: + break Loop //this is required, a simple break statement would only exit the innermost switch statement } - - close(b.Input) - - }() - -} - -func (b *BufferflowTimed) BlockUntilReady(cmd string, id string) (bool, bool) { - //log.Printf("BlockUntilReady() start\n") - return true, false + } + close(b.input) } func (b *BufferflowTimed) OnIncomingData(data string) { - b.Input <- data -} - -// Clean out b.sem so it can truly block -func (b *BufferflowTimed) ClearOutSemaphore() { -} - -func (b *BufferflowTimed) BreakApartCommands(cmd string) []string { - return []string{cmd} -} - -func (b *BufferflowTimed) Pause() { - return -} - -func (b *BufferflowTimed) Unpause() { - return -} - -func (b *BufferflowTimed) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { - return false -} - -func (b *BufferflowTimed) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { - return false -} - -func (b *BufferflowTimed) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { - return false -} - -func (b *BufferflowTimed) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { - return false -} - -func (b *BufferflowTimed) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { - return false -} - -func (b *BufferflowTimed) ReleaseLock() { -} - -func (b *BufferflowTimed) IsBufferGloballySendingBackIncomingData() bool { - return true + b.input <- data } func (b *BufferflowTimed) Close() { diff --git a/bufferflow_timedraw.go b/bufferflow_timedraw.go index b20f27f48..ab238bfe8 100644 --- a/bufferflow_timedraw.go +++ b/bufferflow_timedraw.go @@ -8,95 +8,62 @@ import ( ) type BufferflowTimedRaw struct { - Name string - Port string - Output chan []byte - Input chan string - ticker *time.Ticker + port string + output chan<- []byte + input chan string + done chan bool + ticker *time.Ticker + bufferedOutputRaw []byte + sPortRaw string } -var ( - bufferedOutputRaw []byte -) +func NewBufferflowTimedRaw(port string, output chan<- []byte) *BufferflowTimedRaw { + return &BufferflowTimedRaw{ + port: port, + output: output, + input: make(chan string), + done: make(chan bool), + ticker: time.NewTicker(16 * time.Millisecond), + bufferedOutputRaw: nil, + sPortRaw: "", + } +} func (b *BufferflowTimedRaw) Init() { - log.Println("Initting timed buffer flow (output once every 16ms)") - - go func() { - for data := range b.Input { - bufferedOutputRaw = append(bufferedOutputRaw, []byte(data)...) - } - }() - - go func() { - b.ticker = time.NewTicker(16 * time.Millisecond) - for _ = range b.ticker.C { - if len(bufferedOutputRaw) != 0 { - m := SpPortMessageRaw{b.Port, bufferedOutputRaw} + log.Println("Initting timed buffer raw flow (output once every 16ms)") + go b.consumeInput() +} + +func (b *BufferflowTimedRaw) consumeInput() { +Loop: + for { + select { + case data := <-b.input: // use the buffer and append data to it + b.bufferedOutputRaw = append(b.bufferedOutputRaw, []byte(data)...) + b.sPortRaw = b.port + case <-b.ticker.C: // after 16ms send the buffered output message + if b.bufferedOutputRaw != nil { + m := SpPortMessageRaw{b.sPortRaw, b.bufferedOutputRaw} buf, _ := json.Marshal(m) - // data is now encoded in base64 format - // need a decoder on the other side - b.Output <- []byte(buf) - bufferedOutputRaw = nil + // since bufferedOutputRaw is a []byte is base64-encoded by json.Marshal() function automatically + b.output <- buf + // reset the buffer and the port + b.bufferedOutputRaw = nil + b.sPortRaw = "" } + case <-b.done: + break Loop //this is required, a simple break statement would only exit the innermost switch statement } - }() - -} - -func (b *BufferflowTimedRaw) BlockUntilReady(cmd string, id string) (bool, bool) { - //log.Printf("BlockUntilReady() start\n") - return true, false + } + close(b.input) } func (b *BufferflowTimedRaw) OnIncomingData(data string) { - b.Input <- data -} - -// Clean out b.sem so it can truly block -func (b *BufferflowTimedRaw) ClearOutSemaphore() { -} - -func (b *BufferflowTimedRaw) BreakApartCommands(cmd string) []string { - return []string{cmd} -} - -func (b *BufferflowTimedRaw) Pause() { - return -} - -func (b *BufferflowTimedRaw) Unpause() { - return -} - -func (b *BufferflowTimedRaw) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { - return false -} - -func (b *BufferflowTimedRaw) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { - return false -} - -func (b *BufferflowTimedRaw) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { - return false -} - -func (b *BufferflowTimedRaw) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { - return false -} - -func (b *BufferflowTimedRaw) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { - return false -} - -func (b *BufferflowTimedRaw) ReleaseLock() { -} - -func (b *BufferflowTimedRaw) IsBufferGloballySendingBackIncomingData() bool { - return true + b.input <- data } func (b *BufferflowTimedRaw) Close() { b.ticker.Stop() - close(b.Input) + b.done <- true + close(b.done) } diff --git a/go.sum b/go.sum index 43f085b20..786092344 100644 --- a/go.sum +++ b/go.sum @@ -391,6 +391,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/hub.go b/hub.go index 46aa9f679..c141a6cd8 100755 --- a/hub.go +++ b/hub.go @@ -2,6 +2,8 @@ package main import ( "encoding/json" + "fmt" + "html" "io" "os" "runtime" @@ -38,60 +40,64 @@ var h = hub{ connections: make(map[*connection]bool), } +const commands = `{ + "Commands": [ + "list", + "open [bufferAlgorithm: ({default}, timed, timedraw)]", + "(send, sendnobuf, sendraw) ", + "close ", + "restart", + "exit", + "killupload", + "downloadtool ", + "log", + "memorystats", + "gc", + "hostname", + "version" + ] +}` + +func (h *hub) unregisterConnection(c *connection) { + if _, contains := h.connections[c]; !contains { + return + } + delete(h.connections, c) + close(c.send) +} + +func (h *hub) sendToRegisteredConnections(data []byte) { + for c := range h.connections { + select { + case c.send <- data: + //log.Print("did broadcast to ") + //log.Print(c.ws.RemoteAddr()) + //c.send <- []byte("hello world") + default: + h.unregisterConnection(c) + } + } +} + func (h *hub) run() { for { select { case c := <-h.register: h.connections[c] = true // send supported commands - c.send <- []byte("{\"Version\" : \"" + version + "\"} ") - c.send <- []byte("{\"Commands\" : [\"list\", \"open [portName] [baud] [bufferAlgorithm (optional)]\", \"send [portName] [cmd]\", \"sendnobuf [portName] [cmd]\", \"close [portName]\", \"bufferalgorithms\", \"baudrates\", \"restart\", \"exit\", \"program [portName] [board:name] [$path/to/filename/without/extension]\", \"programfromurl [portName] [board:name] [urlToHexFile]\"]} ") - c.send <- []byte("{\"Hostname\" : \"" + *hostname + "\"} ") - c.send <- []byte("{\"OS\" : \"" + runtime.GOOS + "\"} ") + c.send <- []byte(fmt.Sprintf(`{"Version" : "%s"} `, version)) + c.send <- []byte(html.EscapeString(commands)) + c.send <- []byte(fmt.Sprintf(`{"Hostname" : "%s"} `, *hostname)) + c.send <- []byte(fmt.Sprintf(`{"OS" : "%s"} `, runtime.GOOS)) case c := <-h.unregister: - delete(h.connections, c) - // put close in func cuz it was creating panics and want - // to isolate - func() { - // this method can panic if websocket gets disconnected - // from users browser and we see we need to unregister a couple - // of times, i.e. perhaps from incoming data from serial triggering - // an unregister. (NOT 100% sure why seeing c.send be closed twice here) - defer func() { - if e := recover(); e != nil { - log.Println("Got panic: ", e) - } - }() - close(c.send) - }() + h.unregisterConnection(c) case m := <-h.broadcast: if len(m) > 0 { checkCmd(m) - - for c := range h.connections { - select { - case c.send <- m: - //log.Print("did broadcast to ") - //log.Print(c.ws.RemoteAddr()) - //c.send <- []byte("hello world") - default: - delete(h.connections, c) - close(c.send) - } - } + h.sendToRegisteredConnections(m) } case m := <-h.broadcastSys: - for c := range h.connections { - select { - case c.send <- m: - //log.Print("did broadcast to ") - //log.Print(c.ws.RemoteAddr()) - //c.send <- []byte("hello world") - default: - delete(h.connections, c) - close(c.send) - } - } + h.sendToRegisteredConnections(m) } } } @@ -127,7 +133,7 @@ func checkCmd(m []byte) { } // pass in buffer type now as string. if user does not // ask for a buffer type pass in empty string - bufferAlgorithm := "" + bufferAlgorithm := "default" // use the default buffer if none is specified if len(args) > 3 { // cool. we got a buffer type request buftype := strings.Replace(args[3], "\n", "", -1) @@ -153,7 +159,7 @@ func checkCmd(m []byte) { }() } else if strings.HasPrefix(sl, "send") { - // will catch send and sendnobuf + // will catch send and sendnobuf and sendraw go spWrite(s) } else if strings.HasPrefix(sl, "list") { go spList(false) diff --git a/poetry.lock b/poetry.lock index 0d51c0ae4..1261fb68e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,8 +1,16 @@ +[[package]] +name = "asyncio" +version = "3.4.3" +description = "reference implementation of PEP 3156" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -10,7 +18,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "20.3.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -24,23 +32,23 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> name = "certifi" version = "2020.12.5" description = "Python package for providing Mozilla's CA Bundle." -category = "dev" +category = "main" optional = false python-versions = "*" [[package]] name = "chardet" -version = "4.0.0" +version = "3.0.4" description = "Universal encoding detector for Python 2 and 3" -category = "dev" +category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "*" [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -48,7 +56,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "idna" version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -56,7 +64,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -64,26 +72,34 @@ python-versions = "*" name = "invoke" version = "1.5.0" description = "Pythonic task execution" -category = "dev" +category = "main" optional = false python-versions = "*" [[package]] name = "packaging" -version = "20.8" +version = "20.9" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "pathlib" +version = "1.0.1" +description = "Object-oriented filesystem paths" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -105,7 +121,7 @@ test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -113,15 +129,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "pyparsing" version = "2.4.7" description = "Python parsing module" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "6.2.1" +version = "6.2.2" description = "pytest: simple powerful testing with Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -138,11 +154,42 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "python-engineio" +version = "3.14.2" +description = "Engine.IO server" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +asyncio_client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] + +[[package]] +name = "python-socketio" +version = "4.6.1" +description = "Socket.IO server" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +python-engineio = ">=3.13.0,<4" +six = ">=1.9.0" + +[package.extras] +asyncio_client = ["aiohttp (>=3.4)", "websockets (>=7.0)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] + [[package]] name = "requests" version = "2.25.1" description = "Python HTTP for Humans." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -156,19 +203,27 @@ urllib3 = ">=1.21.1,<1.27" security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "urllib3" -version = "1.26.2" +version = "1.26.3" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" @@ -180,9 +235,15 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "d4024a50235f771811ce457845b6cfef87950b8eb07a8a6e3ad10005482c8a05" +content-hash = "168545733833f6a77146c9976e6ead33d1d85eefa70685180b7c7843671b8db0" [metadata.files] +asyncio = [ + {file = "asyncio-3.4.3-cp33-none-win32.whl", hash = "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de"}, + {file = "asyncio-3.4.3-cp33-none-win_amd64.whl", hash = "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c"}, + {file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"}, + {file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -196,8 +257,8 @@ certifi = [ {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -217,8 +278,11 @@ invoke = [ {file = "invoke-1.5.0.tar.gz", hash = "sha256:f0c560075b5fb29ba14dad44a7185514e94970d1b9d57dcd3723bec5fed92650"}, ] packaging = [ - {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, - {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, +] +pathlib = [ + {file = "pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, @@ -263,18 +327,30 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, - {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, + {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"}, + {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"}, +] +python-engineio = [ + {file = "python-engineio-3.14.2.tar.gz", hash = "sha256:eab4553f2804c1ce97054c8b22cf0d5a9ab23128075248b97e1a5b2f29553085"}, + {file = "python_engineio-3.14.2-py2.py3-none-any.whl", hash = "sha256:5a9e6086d192463b04a1428ff1f85b6ba631bbb19d453b144ffc04f530542b84"}, +] +python-socketio = [ + {file = "python-socketio-4.6.1.tar.gz", hash = "sha256:cd1f5aa492c1eb2be77838e837a495f117e17f686029ebc03d62c09e33f4fa10"}, + {file = "python_socketio-4.6.1-py2.py3-none-any.whl", hash = "sha256:5a21da53fdbdc6bb6c8071f40e13d100e0b279ad997681c2492478e06f370523"}, ] requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] urllib3 = [ - {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, - {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, ] diff --git a/pyproject.toml b/pyproject.toml index c3d576b7c..ca7672636 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,14 @@ license = "GPLv2" [tool.poetry.dependencies] python = "^3.9" psutil = "^5.8.0" - -[tool.poetry.dev-dependencies] -pytest = "^6.2.1" +pytest = "^6.2.2" requests = "^2.25.1" invoke = "^1.5.0" +pathlib = "^1.0.1" +asyncio = "^3.4.3" +python-socketio = "^4" + +[tool.poetry.dev-dependencies] [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/serial.go b/serial.go index 38057ff6c..0bfd77873 100755 --- a/serial.go +++ b/serial.go @@ -14,8 +14,7 @@ import ( type writeRequest struct { p *serport d string - buffer bool - id string + buffer string } type serialhub struct { @@ -84,19 +83,21 @@ func (sh *serialhub) run() { close(p.sendNoBuf) case wr := <-sh.write: // if user sent in the commands as one text mode line - write(wr, "") + write(wr) } } } -func write(wr writeRequest, id string) { - if wr.buffer { - //log.Println("Send was normal send, so sending to wr.p.sendBuffered") +func write(wr writeRequest) { + switch wr.buffer { + case "send": wr.p.sendBuffered <- wr.d - } else { - //log.Println("Send was sendnobuf, so sending to wr.p.sendNoBuf") - wr.p.sendNoBuf <- wr.d + case "sendnobuf": + wr.p.sendNoBuf <- []byte(wr.d) + case "sendraw": + wr.p.sendRaw <- wr.d } + // no default since we alredy verified in spWrite() } // spList broadcasts a Json representation of the ports found @@ -275,13 +276,13 @@ func spWrite(arg string) { var wr writeRequest wr.p = myport - // see if args[0] is send or sendnobuf - if args[0] != "sendnobuf" { - // we were just given a "send" so buffer it - wr.buffer = true - } else { - //log.Println("sendnobuf specified so wr.buffer is false") - wr.buffer = false + // see if args[0] is send or sendnobuf or sendraw + switch args[0] { + case "send", "sendnobuf", "sendraw": + wr.buffer = args[0] + default: + spErr("Unsupported send command:" + args[0] + ". Please specify a valid one") + return } // include newline or not in the write? that is the question. diff --git a/serialport.go b/serialport.go index c7e574a7a..b427e1d2e 100755 --- a/serialport.go +++ b/serialport.go @@ -2,7 +2,7 @@ package main import ( "bytes" - "encoding/json" + "encoding/base64" "io" "strconv" "time" @@ -13,19 +13,8 @@ import ( ) type SerialConfig struct { - Name string - Baud int - - // Size int // 0 get translated to 8 - // Parity SomeNewTypeToGetCorrectDefaultOf_None - // StopBits SomeNewTypeToGetCorrectDefaultOf_1 - - // RTSFlowControl bool - // DTRFlowControl bool - // XONFlowControl bool - - // CRLFTranslate bool - // TimeoutStuff int + Name string + Baud int RtsOn bool DtrOn bool } @@ -35,8 +24,6 @@ type serport struct { portConf *SerialConfig portIo io.ReadWriteCloser - done chan bool // signals the end of this request - // Keep track of whether we're being actively closed // just so we don't show scary error messages isClosing bool @@ -50,7 +37,10 @@ type serport struct { sendBuffered chan string // unbuffered channel of outbound messages that bypass internal serial port buffer - sendNoBuf chan string + sendNoBuf chan []byte + + // channel containing raw base64 encoded binary data (outbound messages) + sendRaw chan string // Do we have an extra channel/thread to watch our buffer? BufferType string @@ -58,30 +48,6 @@ type serport struct { bufferwatcher Bufferflow } -type Cmd struct { - data string - id string - skippedBuffer bool - willHandleCompleteResponse bool -} - -type CmdComplete struct { - Cmd string - Id string - P string - BufSize int `json:"-"` - D string `json:"-"` -} - -type qwReport struct { - Cmd string - QCnt int - Id string - D string `json:"-"` - Buf string `json:"-"` - P string -} - type SpPortMessage struct { P string // the port, i.e. com22 D string // the data, i.e. G0 X0 Y0 @@ -92,18 +58,17 @@ type SpPortMessageRaw struct { D []byte // the data, i.e. G0 X0 Y0 } -func (p *serport) reader() { +func (p *serport) reader(buftype string) { - //var buf bytes.Buffer - ch := make([]byte, 1024) timeCheckOpen := time.Now() var buffered_ch bytes.Buffer + serialBuffer := make([]byte, 1024) for { + n, err := p.portIo.Read(serialBuffer) + bufferPart := serialBuffer[:n] - n, err := p.portIo.Read(ch) - - //if we detect that port is closing, break out o this for{} loop. + //if we detect that port is closing, break out of this for{} loop. if p.isClosing { strmsg := "Shutting down reader on " + p.portConf.Name log.Println(strmsg) @@ -111,67 +76,41 @@ func (p *serport) reader() { break } - if err == nil { - ch = append(buffered_ch.Bytes(), ch[:n]...) - n += len(buffered_ch.Bytes()) - buffered_ch.Reset() - } - // read can return legitimate bytes as well as an error - // so process the bytes if n > 0 - if n > 0 { - //log.Print("Read " + strconv.Itoa(n) + " bytes ch: " + string(ch)) - - data := "" + // so process the n bytes red, if n > 0 + if n > 0 && err == nil { - for i, w := 0, 0; i < n; i += w { - runeValue, width := utf8.DecodeRune(ch[i:n]) - if runeValue == utf8.RuneError { - buffered_ch.Write(append(ch[i:n])) - break - } - if i == n { - buffered_ch.Reset() - } - data += string(runeValue) - w = width - } + log.Print("Read " + strconv.Itoa(n) + " bytes ch: " + string(bufferPart[:n])) - //log.Print("The data i will convert to json is:") - //log.Print(data) - - // give the data to our bufferflow so it can do it's work - // to read/translate the data to see if it wants to block - // writes to the serialport. each bufferflow type will decide - // this on its own based on its logic, i.e. tinyg vs grbl vs others - //p.b.bufferwatcher..OnIncomingData(data) - p.bufferwatcher.OnIncomingData(data) - - // see if the OnIncomingData handled the broadcast back - // to the user. this option was added in case the OnIncomingData wanted - // to do something fancier or implementation specific, i.e. TinyG Buffer - // actually sends back data on a perline basis rather than our method - // where we just send the moment we get it. the reason for this is that - // the browser was sometimes getting back packets out of order which - // of course would screw things up when parsing - - if p.bufferwatcher.IsBufferGloballySendingBackIncomingData() == false { - //m := SpPortMessage{"Alice", "Hello"} - m := SpPortMessage{p.portConf.Name, data} - //log.Print("The m obj struct is:") - //log.Print(m) - - //b, err := json.MarshalIndent(m, "", "\t") - b, err := json.Marshal(m) - if err != nil { - log.Println(err) - h.broadcastSys <- []byte("Error creating json on " + p.portConf.Name + " " + - err.Error() + " The data we were trying to convert is: " + string(ch[:n])) - break + data := "" + switch buftype { + case "timedraw", "timed": + data = string(bufferPart[:n]) + // give the data to our bufferflow so it can do it's work + // to read/translate the data to see if it wants to block + // writes to the serialport. each bufferflow type will decide + // this on its own based on its logic + p.bufferwatcher.OnIncomingData(data) + case "default": // the bufferbuftype is actually called default ๐Ÿคทโ€โ™‚๏ธ + // save the left out bytes for the next iteration due to UTF-8 encoding + bufferPart = append(buffered_ch.Bytes(), bufferPart[:n]...) + n += len(buffered_ch.Bytes()) + buffered_ch.Reset() + for i, w := 0, 0; i < n; i += w { + runeValue, width := utf8.DecodeRune(bufferPart[i:n]) // try to decode the first i bytes in the buffer (UTF8 runes do not have a fixed length) + if runeValue == utf8.RuneError { + buffered_ch.Write(bufferPart[i:n]) + break + } + if i == n { + buffered_ch.Reset() + } + data += string(runeValue) + w = width } - //log.Print("Printing out json byte data...") - //log.Print(string(b)) - h.broadcastSys <- b + p.bufferwatcher.OnIncomingData(data) + default: + log.Panicf("unknown buffer type %s", buftype) } } @@ -235,18 +174,10 @@ func (p *serport) writerBuffered() { // sees something come in for data := range p.sendBuffered { - // we want to block here if we are being asked to pause. - goodToGo, _ := p.bufferwatcher.BlockUntilReady(string(data), "") - - if goodToGo == false { - log.Println("We got back from BlockUntilReady() but apparently we must cancel this cmd") - // since we won't get a buffer decrement in p.sendNoBuf, we must do it here - p.itemsInBuffer-- - } else { - // send to the non-buffered serial port writer - //log.Println("About to send to p.sendNoBuf channel") - p.sendNoBuf <- data - } + // send to the non-buffered serial port writer + //log.Println("About to send to p.sendNoBuf channel") + p.sendNoBuf <- []byte(data) + } msgstr := "writerBuffered just got closed. make sure you make a new one. port:" + p.portConf.Name log.Println(msgstr) @@ -269,7 +200,7 @@ func (p *serport) writerNoBuf() { // FINALLY, OF ALL THE CODE IN THIS PROJECT // WE TRULY/FINALLY GET TO WRITE TO THE SERIAL PORT! - n2, err := p.portIo.Write([]byte(data)) + n2, err := p.portIo.Write(data) log.Print("Just wrote ", n2, " bytes to serial: ", string(data)) if err != nil { @@ -287,6 +218,38 @@ func (p *serport) writerNoBuf() { spList(false) } +// this method runs as its own thread because it's instantiated +// as a "go" method. so if it blocks inside, it is ok +func (p *serport) writerRaw() { + // this method can panic if user closes serial port and something is + // in BlockUntilReady() and then a send occurs on p.sendNoBuf + + defer func() { + if e := recover(); e != nil { + log.Println("Got panic: ", e) + } + }() + + // this for loop blocks on p.sendRaw until that channel + // sees something come in + for data := range p.sendRaw { + + // Decode stuff + sDec, err := base64.StdEncoding.DecodeString(data) + if err != nil { + log.Println("Decoding error:", err) + } + log.Println(string(sDec)) + + // send to the non-buffered serial port writer + p.sendNoBuf <- sDec + + } + msgstr := "writerRaw just got closed. make sure you make a new one. port:" + p.portConf.Name + log.Println(msgstr) + h.broadcastSys <- []byte(msgstr) +} + func spHandlerOpen(portname string, baud int, buftype string) { log.Print("Inside spHandler") @@ -319,16 +282,19 @@ func spHandlerOpen(portname string, baud int, buftype string) { log.Print("Opened port successfully") //p := &serport{send: make(chan []byte, 256), portConf: conf, portIo: sp} // we can go up to 256,000 lines of gcode in the buffer - p := &serport{sendBuffered: make(chan string, 256000), sendNoBuf: make(chan string), portConf: conf, portIo: sp, BufferType: buftype} + p := &serport{sendBuffered: make(chan string, 256000), sendNoBuf: make(chan []byte), sendRaw: make(chan string), portConf: conf, portIo: sp, BufferType: buftype} var bw Bufferflow - if buftype == "timed" { - bw = &BufferflowTimed{Name: "timed", Port: portname, Output: h.broadcastSys, Input: make(chan string)} - } else if buftype == "timedraw" { - bw = &BufferflowTimedRaw{Name: "timedraw", Port: portname, Output: h.broadcastSys, Input: make(chan string)} - } else { - bw = &BufferflowDefault{Port: portname} + switch buftype { + case "timed": + bw = NewBufferflowTimed(portname, h.broadcastSys) + case "timedraw": + bw = NewBufferflowTimedRaw(portname, h.broadcastSys) + case "default": + bw = NewBufferflowDefault(portname, h.broadcastSys) + default: + log.Panicf("unknown buffer type: %s", buftype) } bw.Init() @@ -344,14 +310,13 @@ func spHandlerOpen(portname string, baud int, buftype string) { go p.writerBuffered() // this is thread to send to serial port regardless of block go p.writerNoBuf() - p.reader() + // this is thread to send to serial port but with base64 decoding + go p.writerRaw() + + p.reader(buftype) spListDual(false) spList(false) - - //go p.reader() - //p.done = make(chan bool) - //<-p.done } func spHandlerClose(p *serport) { diff --git a/test/common.py b/test/common.py new file mode 100644 index 000000000..34e16b523 --- /dev/null +++ b/test/common.py @@ -0,0 +1,7 @@ +import os + +def running_on_ci(): + """ + Returns whether the program is running on a CI environment + """ + return 'GITHUB_WORKFLOW' in os.environ diff --git a/test/conftest.py b/test/conftest.py index fd50856ec..3f1b185f2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -7,7 +7,7 @@ import pytest from invoke import Local from invoke.context import Context - +import socketio as io @pytest.fixture(scope="function") def agent(pytestconfig): @@ -43,3 +43,42 @@ def agent(pytestconfig): @pytest.fixture(scope="session") def base_url(): return "http://127.0.0.1:8991" + +@pytest.fixture(scope="function") +def socketio(base_url, agent): + sio = io.Client() + sio.connect(base_url) + yield sio + sio.disconnect() + +@pytest.fixture(scope="session") +def serial_port(): + return "/dev/ttyACM0" # maybe this could be enhanced by calling arduino-cli + +@pytest.fixture(scope="session") +def baudrate(): + return "9600" + +# open_port cannot be coced as a fixture because of the buffertype parameter + +# at the end of the test closes the serial port +@pytest.fixture(scope="function") +def close_port(socketio, serial_port): + yield socketio + socketio.emit('command', 'close ' + serial_port) + time.sleep(.5) + + +@pytest.fixture(scope="function") +def message(socketio): + global message + message = [] + #in message var we will find the "response" + socketio.on('message', message_handler) + return message + +# callback called by socketio when a message is received +def message_handler(msg): + # print('Received message: ', msg) + global message + message.append(msg) \ No newline at end of file diff --git a/test/test_ws.py b/test/test_ws.py new file mode 100644 index 000000000..8a7143e74 --- /dev/null +++ b/test/test_ws.py @@ -0,0 +1,187 @@ +import time +import json +import base64 +import pytest + +from common import running_on_ci +message = [] + + +def test_ws_connection(socketio): + print('my sid is', socketio.sid) + assert socketio.sid is not None + + +def test_list(socketio, message): + socketio.emit('command', 'list') + time.sleep(.2) + print (message) + assert any("list" in i for i in message) + assert any("Ports" in i for i in message) + assert any("Network" in i for i in message) + + +# NOTE run the following tests with a board connected to the PC +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_open_serial_default(socketio, serial_port, baudrate, message): + general_open_serial(socketio, serial_port, baudrate, message, "default") + + +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_open_serial_timed(socketio, serial_port, baudrate, message): + general_open_serial(socketio, serial_port, baudrate, message, "timed") + + +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_open_serial_timedraw(socketio, serial_port, baudrate, message): + general_open_serial(socketio, serial_port, baudrate, message, "timedraw") + + +# NOTE run the following tests with a board connected to the PC and with the sketch found in test/testdata/SerialEcho.ino on it be sure to change serial_address in conftest.py +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_send_serial_default(socketio, close_port, serial_port, baudrate, message): + general_send_serial(socketio, close_port, serial_port, baudrate, message, "default") + + +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_send_serial_timed(socketio, close_port, serial_port, baudrate, message): + general_send_serial(socketio, close_port, serial_port, baudrate, message, "timed") + + +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_send_serial_timedraw(socketio, close_port, serial_port, baudrate, message): + general_send_serial(socketio, close_port, serial_port, baudrate, message, "timedraw") + + +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_send_emoji_serial_default(socketio, close_port, serial_port, baudrate, message): + general_send_emoji_serial(socketio, close_port, serial_port, baudrate, message, "default") + + +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_send_emoji_serial_timed(socketio, close_port, serial_port, baudrate, message): + general_send_emoji_serial(socketio, close_port, serial_port, baudrate, message, "timed") + + +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_send_emoji_serial_timedraw(socketio, close_port, serial_port, baudrate, message): + general_send_emoji_serial(socketio, close_port, serial_port, baudrate, message, "timedraw") + + +def general_open_serial(socketio, serial_port, baudrate, message, buffertype): + open_serial_port(socketio, serial_port, baudrate, message, buffertype) + # test the closing of the serial port, we are gonna use close_port for the other tests + socketio.emit('command', 'close ' + serial_port) + time.sleep(.2) + print (message) + #check if port has been closed + assert any("\"IsOpen\": false," in i for i in message) + + +def general_send_serial(socketio, close_port, serial_port, baudrate, message, buffertype): + open_serial_port(socketio, serial_port, baudrate, message, buffertype) + # send the string "ciao" using the serial connection + socketio.emit('command', 'send ' + serial_port + ' ciao') + time.sleep(1) + print(message) + # check if the send command has been registered + assert any("send " + serial_port + " ciao" in i for i in message) + #check if message has been sent back by the connected board + if buffertype == "timedraw": + output = decode_output(extract_serial_data(message)) + elif buffertype in ("default", "timed"): + output = extract_serial_data(message) + assert "ciao" in output + # the serial connection is closed by close_port() fixture: even if in case of test failure + + +def general_send_emoji_serial(socketio, close_port, serial_port, baudrate, message, buffertype): + open_serial_port(socketio, serial_port, baudrate, message, buffertype) + # send a lot of emoji: they can be messed up + socketio.emit('command', 'send ' + serial_port + ' /"๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€/"') + time.sleep(1) + print(message) + # check if the send command has been registered + assert any("send " + serial_port + " /\"๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€/\"" in i for i in message) + if buffertype == "timedraw": + output = decode_output(extract_serial_data(message)) + elif buffertype in ("default", "timed"): + output = extract_serial_data(message) + assert "/\"๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€๐Ÿง€/\"" in output + # the serial connection is closed by close_port() fixture: even if in case of test failure + + +def open_serial_port(socketio, serial_port, baudrate, message, buffertype): + #open a new serial connection with the specified buffertype + socketio.emit('command', 'open ' + serial_port + ' ' + baudrate + ' ' + buffertype) + # give time to the message var to be filled + time.sleep(.5) + print(message) + # the serial connection should be open now + assert any("\"IsOpen\": true" in i for i in message) + + +@pytest.mark.skipif( + running_on_ci(), + reason="VMs have no serial ports", +) +def test_sendraw_serial(socketio, close_port, serial_port, baudrate, message): + open_serial_port(socketio, serial_port, baudrate, message, "timedraw") + #test with bytes + integers = [1, 2, 3, 4, 5] + bytes_array=bytearray(integers) + encoded_integers = base64.b64encode(bytes_array).decode('ascii') + socketio.emit('command', 'sendraw ' + serial_port + ' ' + encoded_integers) + time.sleep(1) + print(message) + # check if the send command has been registered + assert any(("sendraw " + serial_port + ' ' + encoded_integers) in i for i in message) + #check if message has been sent back by the connected board + output = extract_serial_data(message) # TODO use decode_output() + print (output) + assert encoded_integers in output + + +# helper function used to extract serial data from its JSON representation +# NOTE make sure to pass a clean message (maybe reinitialize the message global var before populating it) +def extract_serial_data(msg): + serial_data = "" + for i in msg: + if "{\"P\"" in i: + print (json.loads(i)["D"]) + serial_data+=json.loads(i)["D"] + print("serialdata:"+serial_data) + return serial_data + +def decode_output(raw_output): + # print(raw_output) + base64_bytes = raw_output.encode('ascii') #encode rawoutput message into a bytes-like object + output_bytes = base64.b64decode(base64_bytes) + return output_bytes.decode('utf-8') diff --git a/test/testdata/SerialEcho/SerialEcho.ino b/test/testdata/SerialEcho/SerialEcho.ino new file mode 100644 index 000000000..efb7c59b7 --- /dev/null +++ b/test/testdata/SerialEcho/SerialEcho.ino @@ -0,0 +1,18 @@ +int incomingByte = 0; // for incoming serial data + +void setup() { + Serial.begin(9600); // opens serial port, sets data rate to 9600 bps +} + +void loop() { + // send data only when you receive data: + if (Serial.available() > 0) { + + // read the incoming byte: + incomingByte = Serial.read(); + + // say what you got: + Serial.print((char)incomingByte); + } + +}