Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 056c22e

Browse files
umbynosper1234silvanocerza
committedAug 18, 2021
Add serial binary communication (#653)
* 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 <[email protected]> * 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 <[email protected]> Co-authored-by: Silvano Cerza <[email protected]>
1 parent d759c46 commit 056c22e

File tree

14 files changed

+644
-482
lines changed

14 files changed

+644
-482
lines changed
 

‎bufferflow.go

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,7 @@
11
package main
22

3-
import (
4-
//"log"
5-
//"time"
6-
)
7-
8-
var availableBufferAlgorithms = []string{"default", "timed", "timedraw"}
9-
10-
type BufferMsg struct {
11-
Cmd string
12-
Port string
13-
TriggeringResponse string
14-
//Desc string
15-
//Desc string
16-
}
17-
183
type Bufferflow interface {
194
Init()
20-
BlockUntilReady(cmd string, id string) (bool, bool) // implement this method
21-
//JustQueue(cmd string, id string) bool // implement this method
22-
OnIncomingData(data string) // implement this method
23-
ClearOutSemaphore() // implement this method
24-
BreakApartCommands(cmd string) []string // implement this method
25-
Pause() // implement this method
26-
Unpause() // implement this method
27-
SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool // implement this method
28-
SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool // implement this method
29-
SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool // implement this method
30-
SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool // implement this method
31-
SeeIfSpecificCommandsReturnNoResponse(cmd string) bool // implement this method
32-
ReleaseLock() // implement this method
33-
IsBufferGloballySendingBackIncomingData() bool // implement this method
34-
Close() // implement this method
35-
}
36-
37-
/*data packets returned to client*/
38-
type DataCmdComplete struct {
39-
Cmd string
40-
Id string
41-
P string
42-
BufSize int `json:"-"`
43-
D string `json:"-"`
44-
}
45-
46-
type DataPerLine struct {
47-
P string
48-
D string
5+
OnIncomingData(data string) // implement this method
6+
Close() // implement this method
497
}

‎bufferflow_default.go

Lines changed: 31 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,52 @@
11
package main
22

33
import (
4+
"encoding/json"
5+
46
log "github.com/sirupsen/logrus"
57
)
68

79
type BufferflowDefault struct {
8-
Name string
9-
Port string
10+
port string
11+
output chan<- []byte
12+
input chan string
13+
done chan bool
1014
}
1115

12-
var ()
16+
func NewBufferflowDefault(port string, output chan<- []byte) *BufferflowDefault {
17+
return &BufferflowDefault{
18+
port: port,
19+
output: output,
20+
input: make(chan string),
21+
done: make(chan bool),
22+
}
23+
}
1324

1425
func (b *BufferflowDefault) Init() {
1526
log.Println("Initting default buffer flow (which means no buffering)")
27+
go b.consumeInput()
1628
}
1729

18-
func (b *BufferflowDefault) BlockUntilReady(cmd string, id string) (bool, bool) {
19-
//log.Printf("BlockUntilReady() start\n")
20-
return true, false
30+
func (b *BufferflowDefault) consumeInput() {
31+
Loop:
32+
for {
33+
select {
34+
case data := <-b.input:
35+
m := SpPortMessage{b.port, data}
36+
message, _ := json.Marshal(m)
37+
b.output <- message
38+
case <-b.done:
39+
break Loop //this is required, a simple break statement would only exit the innermost switch statement
40+
}
41+
}
42+
close(b.input) // close the input channel at the end of the computation
2143
}
2244

2345
func (b *BufferflowDefault) OnIncomingData(data string) {
24-
//log.Printf("OnIncomingData() start. data:%v\n", data)
25-
}
26-
27-
// Clean out b.sem so it can truly block
28-
func (b *BufferflowDefault) ClearOutSemaphore() {
29-
}
30-
31-
func (b *BufferflowDefault) BreakApartCommands(cmd string) []string {
32-
return []string{cmd}
33-
}
34-
35-
func (b *BufferflowDefault) Pause() {
36-
return
37-
}
38-
39-
func (b *BufferflowDefault) Unpause() {
40-
return
41-
}
42-
43-
func (b *BufferflowDefault) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool {
44-
return false
45-
}
46-
47-
func (b *BufferflowDefault) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool {
48-
return false
49-
}
50-
51-
func (b *BufferflowDefault) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool {
52-
return false
53-
}
54-
55-
func (b *BufferflowDefault) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool {
56-
return false
57-
}
58-
59-
func (b *BufferflowDefault) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool {
60-
return false
61-
}
62-
63-
func (b *BufferflowDefault) ReleaseLock() {
64-
}
65-
66-
func (b *BufferflowDefault) IsBufferGloballySendingBackIncomingData() bool {
67-
return false
46+
b.input <- data
6847
}
6948

7049
func (b *BufferflowDefault) Close() {
50+
b.done <- true
51+
close(b.done)
7152
}

‎bufferflow_timed.go

Lines changed: 41 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -8,104 +8,57 @@ import (
88
)
99

1010
type BufferflowTimed struct {
11-
Name string
12-
Port string
13-
Output chan []byte
14-
Input chan string
15-
done chan bool
16-
ticker *time.Ticker
11+
port string
12+
output chan<- []byte
13+
input chan string
14+
done chan bool
15+
ticker *time.Ticker
16+
sPort string
17+
bufferedOutput string
1718
}
1819

19-
var (
20-
bufferedOutput string
21-
sPort string
22-
)
20+
func NewBufferflowTimed(port string, output chan<- []byte) *BufferflowTimed {
21+
return &BufferflowTimed{
22+
port: port,
23+
output: output,
24+
input: make(chan string),
25+
done: make(chan bool),
26+
ticker: time.NewTicker(16 * time.Millisecond),
27+
sPort: "",
28+
bufferedOutput: "",
29+
}
30+
}
2331

2432
func (b *BufferflowTimed) Init() {
2533
log.Println("Initting timed buffer flow (output once every 16ms)")
26-
bufferedOutput = ""
27-
sPort = ""
28-
29-
go func() {
30-
b.ticker = time.NewTicker(16 * time.Millisecond)
31-
b.done = make(chan bool)
32-
Loop:
33-
for {
34-
select {
35-
case data := <-b.Input:
36-
bufferedOutput = bufferedOutput + data
37-
sPort = b.Port
38-
case <-b.ticker.C:
39-
if bufferedOutput != "" {
40-
m := SpPortMessage{sPort, bufferedOutput}
41-
buf, _ := json.Marshal(m)
42-
// data is now encoded in base64 format
43-
// need a decoder on the other side
44-
b.Output <- []byte(buf)
45-
bufferedOutput = ""
46-
sPort = ""
47-
}
48-
case <-b.done:
49-
break Loop
34+
go b.consumeInput()
35+
}
36+
37+
func (b *BufferflowTimed) consumeInput() {
38+
Loop:
39+
for {
40+
select {
41+
case data := <-b.input: // use the buffer and append data to it
42+
b.bufferedOutput = b.bufferedOutput + data
43+
b.sPort = b.port
44+
case <-b.ticker.C: // after 16ms send the buffered output message
45+
if b.bufferedOutput != "" {
46+
m := SpPortMessage{b.sPort, b.bufferedOutput}
47+
buf, _ := json.Marshal(m)
48+
b.output <- buf
49+
// reset the buffer and the port
50+
b.bufferedOutput = ""
51+
b.sPort = ""
5052
}
53+
case <-b.done:
54+
break Loop //this is required, a simple break statement would only exit the innermost switch statement
5155
}
52-
53-
close(b.Input)
54-
55-
}()
56-
57-
}
58-
59-
func (b *BufferflowTimed) BlockUntilReady(cmd string, id string) (bool, bool) {
60-
//log.Printf("BlockUntilReady() start\n")
61-
return true, false
56+
}
57+
close(b.input)
6258
}
6359

6460
func (b *BufferflowTimed) OnIncomingData(data string) {
65-
b.Input <- data
66-
}
67-
68-
// Clean out b.sem so it can truly block
69-
func (b *BufferflowTimed) ClearOutSemaphore() {
70-
}
71-
72-
func (b *BufferflowTimed) BreakApartCommands(cmd string) []string {
73-
return []string{cmd}
74-
}
75-
76-
func (b *BufferflowTimed) Pause() {
77-
return
78-
}
79-
80-
func (b *BufferflowTimed) Unpause() {
81-
return
82-
}
83-
84-
func (b *BufferflowTimed) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool {
85-
return false
86-
}
87-
88-
func (b *BufferflowTimed) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool {
89-
return false
90-
}
91-
92-
func (b *BufferflowTimed) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool {
93-
return false
94-
}
95-
96-
func (b *BufferflowTimed) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool {
97-
return false
98-
}
99-
100-
func (b *BufferflowTimed) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool {
101-
return false
102-
}
103-
104-
func (b *BufferflowTimed) ReleaseLock() {
105-
}
106-
107-
func (b *BufferflowTimed) IsBufferGloballySendingBackIncomingData() bool {
108-
return true
61+
b.input <- data
10962
}
11063

11164
func (b *BufferflowTimed) Close() {

‎bufferflow_timedraw.go

Lines changed: 44 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,95 +8,62 @@ import (
88
)
99

1010
type BufferflowTimedRaw struct {
11-
Name string
12-
Port string
13-
Output chan []byte
14-
Input chan string
15-
ticker *time.Ticker
11+
port string
12+
output chan<- []byte
13+
input chan string
14+
done chan bool
15+
ticker *time.Ticker
16+
bufferedOutputRaw []byte
17+
sPortRaw string
1618
}
1719

18-
var (
19-
bufferedOutputRaw []byte
20-
)
20+
func NewBufferflowTimedRaw(port string, output chan<- []byte) *BufferflowTimedRaw {
21+
return &BufferflowTimedRaw{
22+
port: port,
23+
output: output,
24+
input: make(chan string),
25+
done: make(chan bool),
26+
ticker: time.NewTicker(16 * time.Millisecond),
27+
bufferedOutputRaw: nil,
28+
sPortRaw: "",
29+
}
30+
}
2131

2232
func (b *BufferflowTimedRaw) Init() {
23-
log.Println("Initting timed buffer flow (output once every 16ms)")
24-
25-
go func() {
26-
for data := range b.Input {
27-
bufferedOutputRaw = append(bufferedOutputRaw, []byte(data)...)
28-
}
29-
}()
30-
31-
go func() {
32-
b.ticker = time.NewTicker(16 * time.Millisecond)
33-
for _ = range b.ticker.C {
34-
if len(bufferedOutputRaw) != 0 {
35-
m := SpPortMessageRaw{b.Port, bufferedOutputRaw}
33+
log.Println("Initting timed buffer raw flow (output once every 16ms)")
34+
go b.consumeInput()
35+
}
36+
37+
func (b *BufferflowTimedRaw) consumeInput() {
38+
Loop:
39+
for {
40+
select {
41+
case data := <-b.input: // use the buffer and append data to it
42+
b.bufferedOutputRaw = append(b.bufferedOutputRaw, []byte(data)...)
43+
b.sPortRaw = b.port
44+
case <-b.ticker.C: // after 16ms send the buffered output message
45+
if b.bufferedOutputRaw != nil {
46+
m := SpPortMessageRaw{b.sPortRaw, b.bufferedOutputRaw}
3647
buf, _ := json.Marshal(m)
37-
// data is now encoded in base64 format
38-
// need a decoder on the other side
39-
b.Output <- []byte(buf)
40-
bufferedOutputRaw = nil
48+
// since bufferedOutputRaw is a []byte is base64-encoded by json.Marshal() function automatically
49+
b.output <- buf
50+
// reset the buffer and the port
51+
b.bufferedOutputRaw = nil
52+
b.sPortRaw = ""
4153
}
54+
case <-b.done:
55+
break Loop //this is required, a simple break statement would only exit the innermost switch statement
4256
}
43-
}()
44-
45-
}
46-
47-
func (b *BufferflowTimedRaw) BlockUntilReady(cmd string, id string) (bool, bool) {
48-
//log.Printf("BlockUntilReady() start\n")
49-
return true, false
57+
}
58+
close(b.input)
5059
}
5160

5261
func (b *BufferflowTimedRaw) OnIncomingData(data string) {
53-
b.Input <- data
54-
}
55-
56-
// Clean out b.sem so it can truly block
57-
func (b *BufferflowTimedRaw) ClearOutSemaphore() {
58-
}
59-
60-
func (b *BufferflowTimedRaw) BreakApartCommands(cmd string) []string {
61-
return []string{cmd}
62-
}
63-
64-
func (b *BufferflowTimedRaw) Pause() {
65-
return
66-
}
67-
68-
func (b *BufferflowTimedRaw) Unpause() {
69-
return
70-
}
71-
72-
func (b *BufferflowTimedRaw) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool {
73-
return false
74-
}
75-
76-
func (b *BufferflowTimedRaw) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool {
77-
return false
78-
}
79-
80-
func (b *BufferflowTimedRaw) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool {
81-
return false
82-
}
83-
84-
func (b *BufferflowTimedRaw) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool {
85-
return false
86-
}
87-
88-
func (b *BufferflowTimedRaw) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool {
89-
return false
90-
}
91-
92-
func (b *BufferflowTimedRaw) ReleaseLock() {
93-
}
94-
95-
func (b *BufferflowTimedRaw) IsBufferGloballySendingBackIncomingData() bool {
96-
return true
62+
b.input <- data
9763
}
9864

9965
func (b *BufferflowTimedRaw) Close() {
10066
b.ticker.Stop()
101-
close(b.Input)
67+
b.done <- true
68+
close(b.done)
10269
}

‎go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
391391
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
392392
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
393393
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
394+
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
394395
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
395396
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
396397
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=

‎hub.go

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package main
22

33
import (
44
"encoding/json"
5+
"fmt"
6+
"html"
57
"io"
68
"os"
79
"runtime"
@@ -38,60 +40,64 @@ var h = hub{
3840
connections: make(map[*connection]bool),
3941
}
4042

43+
const commands = `{
44+
"Commands": [
45+
"list",
46+
"open <portName> <baud> [bufferAlgorithm: ({default}, timed, timedraw)]",
47+
"(send, sendnobuf, sendraw) <portName> <cmd>",
48+
"close <portName>",
49+
"restart",
50+
"exit",
51+
"killupload",
52+
"downloadtool <tool> <toolVersion: {latest}> <pack: {arduino}> <behaviour: {keep}>",
53+
"log",
54+
"memorystats",
55+
"gc",
56+
"hostname",
57+
"version"
58+
]
59+
}`
60+
61+
func (h *hub) unregisterConnection(c *connection) {
62+
if _, contains := h.connections[c]; !contains {
63+
return
64+
}
65+
delete(h.connections, c)
66+
close(c.send)
67+
}
68+
69+
func (h *hub) sendToRegisteredConnections(data []byte) {
70+
for c := range h.connections {
71+
select {
72+
case c.send <- data:
73+
//log.Print("did broadcast to ")
74+
//log.Print(c.ws.RemoteAddr())
75+
//c.send <- []byte("hello world")
76+
default:
77+
h.unregisterConnection(c)
78+
}
79+
}
80+
}
81+
4182
func (h *hub) run() {
4283
for {
4384
select {
4485
case c := <-h.register:
4586
h.connections[c] = true
4687
// send supported commands
47-
c.send <- []byte("{\"Version\" : \"" + version + "\"} ")
48-
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]\"]} ")
49-
c.send <- []byte("{\"Hostname\" : \"" + *hostname + "\"} ")
50-
c.send <- []byte("{\"OS\" : \"" + runtime.GOOS + "\"} ")
88+
c.send <- []byte(fmt.Sprintf(`{"Version" : "%s"} `, version))
89+
c.send <- []byte(html.EscapeString(commands))
90+
c.send <- []byte(fmt.Sprintf(`{"Hostname" : "%s"} `, *hostname))
91+
c.send <- []byte(fmt.Sprintf(`{"OS" : "%s"} `, runtime.GOOS))
5192
case c := <-h.unregister:
52-
delete(h.connections, c)
53-
// put close in func cuz it was creating panics and want
54-
// to isolate
55-
func() {
56-
// this method can panic if websocket gets disconnected
57-
// from users browser and we see we need to unregister a couple
58-
// of times, i.e. perhaps from incoming data from serial triggering
59-
// an unregister. (NOT 100% sure why seeing c.send be closed twice here)
60-
defer func() {
61-
if e := recover(); e != nil {
62-
log.Println("Got panic: ", e)
63-
}
64-
}()
65-
close(c.send)
66-
}()
93+
h.unregisterConnection(c)
6794
case m := <-h.broadcast:
6895
if len(m) > 0 {
6996
checkCmd(m)
70-
71-
for c := range h.connections {
72-
select {
73-
case c.send <- m:
74-
//log.Print("did broadcast to ")
75-
//log.Print(c.ws.RemoteAddr())
76-
//c.send <- []byte("hello world")
77-
default:
78-
delete(h.connections, c)
79-
close(c.send)
80-
}
81-
}
97+
h.sendToRegisteredConnections(m)
8298
}
8399
case m := <-h.broadcastSys:
84-
for c := range h.connections {
85-
select {
86-
case c.send <- m:
87-
//log.Print("did broadcast to ")
88-
//log.Print(c.ws.RemoteAddr())
89-
//c.send <- []byte("hello world")
90-
default:
91-
delete(h.connections, c)
92-
close(c.send)
93-
}
94-
}
100+
h.sendToRegisteredConnections(m)
95101
}
96102
}
97103
}
@@ -127,7 +133,7 @@ func checkCmd(m []byte) {
127133
}
128134
// pass in buffer type now as string. if user does not
129135
// ask for a buffer type pass in empty string
130-
bufferAlgorithm := ""
136+
bufferAlgorithm := "default" // use the default buffer if none is specified
131137
if len(args) > 3 {
132138
// cool. we got a buffer type request
133139
buftype := strings.Replace(args[3], "\n", "", -1)
@@ -153,7 +159,7 @@ func checkCmd(m []byte) {
153159
}()
154160

155161
} else if strings.HasPrefix(sl, "send") {
156-
// will catch send and sendnobuf
162+
// will catch send and sendnobuf and sendraw
157163
go spWrite(s)
158164
} else if strings.HasPrefix(sl, "list") {
159165
go spList(false)

‎poetry.lock

Lines changed: 106 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ license = "GPLv2"
88
[tool.poetry.dependencies]
99
python = "^3.9"
1010
psutil = "^5.8.0"
11-
12-
[tool.poetry.dev-dependencies]
13-
pytest = "^6.2.1"
11+
pytest = "^6.2.2"
1412
requests = "^2.25.1"
1513
invoke = "^1.5.0"
14+
pathlib = "^1.0.1"
15+
asyncio = "^3.4.3"
16+
python-socketio = "^4"
17+
18+
[tool.poetry.dev-dependencies]
1619

1720
[build-system]
1821
requires = ["poetry-core>=1.0.0"]

‎serial.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import (
1414
type writeRequest struct {
1515
p *serport
1616
d string
17-
buffer bool
18-
id string
17+
buffer string
1918
}
2019

2120
type serialhub struct {
@@ -84,19 +83,21 @@ func (sh *serialhub) run() {
8483
close(p.sendNoBuf)
8584
case wr := <-sh.write:
8685
// if user sent in the commands as one text mode line
87-
write(wr, "")
86+
write(wr)
8887
}
8988
}
9089
}
9190

92-
func write(wr writeRequest, id string) {
93-
if wr.buffer {
94-
//log.Println("Send was normal send, so sending to wr.p.sendBuffered")
91+
func write(wr writeRequest) {
92+
switch wr.buffer {
93+
case "send":
9594
wr.p.sendBuffered <- wr.d
96-
} else {
97-
//log.Println("Send was sendnobuf, so sending to wr.p.sendNoBuf")
98-
wr.p.sendNoBuf <- wr.d
95+
case "sendnobuf":
96+
wr.p.sendNoBuf <- []byte(wr.d)
97+
case "sendraw":
98+
wr.p.sendRaw <- wr.d
9999
}
100+
// no default since we alredy verified in spWrite()
100101
}
101102

102103
// spList broadcasts a Json representation of the ports found
@@ -275,13 +276,13 @@ func spWrite(arg string) {
275276
var wr writeRequest
276277
wr.p = myport
277278

278-
// see if args[0] is send or sendnobuf
279-
if args[0] != "sendnobuf" {
280-
// we were just given a "send" so buffer it
281-
wr.buffer = true
282-
} else {
283-
//log.Println("sendnobuf specified so wr.buffer is false")
284-
wr.buffer = false
279+
// see if args[0] is send or sendnobuf or sendraw
280+
switch args[0] {
281+
case "send", "sendnobuf", "sendraw":
282+
wr.buffer = args[0]
283+
default:
284+
spErr("Unsupported send command:" + args[0] + ". Please specify a valid one")
285+
return
285286
}
286287

287288
// include newline or not in the write? that is the question.

‎serialport.go

Lines changed: 94 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package main
22

33
import (
44
"bytes"
5-
"encoding/json"
5+
"encoding/base64"
66
"io"
77
"strconv"
88
"time"
@@ -13,19 +13,8 @@ import (
1313
)
1414

1515
type SerialConfig struct {
16-
Name string
17-
Baud int
18-
19-
// Size int // 0 get translated to 8
20-
// Parity SomeNewTypeToGetCorrectDefaultOf_None
21-
// StopBits SomeNewTypeToGetCorrectDefaultOf_1
22-
23-
// RTSFlowControl bool
24-
// DTRFlowControl bool
25-
// XONFlowControl bool
26-
27-
// CRLFTranslate bool
28-
// TimeoutStuff int
16+
Name string
17+
Baud int
2918
RtsOn bool
3019
DtrOn bool
3120
}
@@ -35,8 +24,6 @@ type serport struct {
3524
portConf *SerialConfig
3625
portIo io.ReadWriteCloser
3726

38-
done chan bool // signals the end of this request
39-
4027
// Keep track of whether we're being actively closed
4128
// just so we don't show scary error messages
4229
isClosing bool
@@ -50,38 +37,17 @@ type serport struct {
5037
sendBuffered chan string
5138

5239
// unbuffered channel of outbound messages that bypass internal serial port buffer
53-
sendNoBuf chan string
40+
sendNoBuf chan []byte
41+
42+
// channel containing raw base64 encoded binary data (outbound messages)
43+
sendRaw chan string
5444

5545
// Do we have an extra channel/thread to watch our buffer?
5646
BufferType string
5747
//bufferwatcher *BufferflowDummypause
5848
bufferwatcher Bufferflow
5949
}
6050

61-
type Cmd struct {
62-
data string
63-
id string
64-
skippedBuffer bool
65-
willHandleCompleteResponse bool
66-
}
67-
68-
type CmdComplete struct {
69-
Cmd string
70-
Id string
71-
P string
72-
BufSize int `json:"-"`
73-
D string `json:"-"`
74-
}
75-
76-
type qwReport struct {
77-
Cmd string
78-
QCnt int
79-
Id string
80-
D string `json:"-"`
81-
Buf string `json:"-"`
82-
P string
83-
}
84-
8551
type SpPortMessage struct {
8652
P string // the port, i.e. com22
8753
D string // the data, i.e. G0 X0 Y0
@@ -92,86 +58,59 @@ type SpPortMessageRaw struct {
9258
D []byte // the data, i.e. G0 X0 Y0
9359
}
9460

95-
func (p *serport) reader() {
61+
func (p *serport) reader(buftype string) {
9662

97-
//var buf bytes.Buffer
98-
ch := make([]byte, 1024)
9963
timeCheckOpen := time.Now()
10064
var buffered_ch bytes.Buffer
10165

66+
serialBuffer := make([]byte, 1024)
10267
for {
68+
n, err := p.portIo.Read(serialBuffer)
69+
bufferPart := serialBuffer[:n]
10370

104-
n, err := p.portIo.Read(ch)
105-
106-
//if we detect that port is closing, break out o this for{} loop.
71+
//if we detect that port is closing, break out of this for{} loop.
10772
if p.isClosing {
10873
strmsg := "Shutting down reader on " + p.portConf.Name
10974
log.Println(strmsg)
11075
h.broadcastSys <- []byte(strmsg)
11176
break
11277
}
11378

114-
if err == nil {
115-
ch = append(buffered_ch.Bytes(), ch[:n]...)
116-
n += len(buffered_ch.Bytes())
117-
buffered_ch.Reset()
118-
}
119-
12079
// read can return legitimate bytes as well as an error
121-
// so process the bytes if n > 0
122-
if n > 0 {
123-
//log.Print("Read " + strconv.Itoa(n) + " bytes ch: " + string(ch))
124-
125-
data := ""
80+
// so process the n bytes red, if n > 0
81+
if n > 0 && err == nil {
12682

127-
for i, w := 0, 0; i < n; i += w {
128-
runeValue, width := utf8.DecodeRune(ch[i:n])
129-
if runeValue == utf8.RuneError {
130-
buffered_ch.Write(append(ch[i:n]))
131-
break
132-
}
133-
if i == n {
134-
buffered_ch.Reset()
135-
}
136-
data += string(runeValue)
137-
w = width
138-
}
83+
log.Print("Read " + strconv.Itoa(n) + " bytes ch: " + string(bufferPart[:n]))
13984

140-
//log.Print("The data i will convert to json is:")
141-
//log.Print(data)
142-
143-
// give the data to our bufferflow so it can do it's work
144-
// to read/translate the data to see if it wants to block
145-
// writes to the serialport. each bufferflow type will decide
146-
// this on its own based on its logic, i.e. tinyg vs grbl vs others
147-
//p.b.bufferwatcher..OnIncomingData(data)
148-
p.bufferwatcher.OnIncomingData(data)
149-
150-
// see if the OnIncomingData handled the broadcast back
151-
// to the user. this option was added in case the OnIncomingData wanted
152-
// to do something fancier or implementation specific, i.e. TinyG Buffer
153-
// actually sends back data on a perline basis rather than our method
154-
// where we just send the moment we get it. the reason for this is that
155-
// the browser was sometimes getting back packets out of order which
156-
// of course would screw things up when parsing
157-
158-
if p.bufferwatcher.IsBufferGloballySendingBackIncomingData() == false {
159-
//m := SpPortMessage{"Alice", "Hello"}
160-
m := SpPortMessage{p.portConf.Name, data}
161-
//log.Print("The m obj struct is:")
162-
//log.Print(m)
163-
164-
//b, err := json.MarshalIndent(m, "", "\t")
165-
b, err := json.Marshal(m)
166-
if err != nil {
167-
log.Println(err)
168-
h.broadcastSys <- []byte("Error creating json on " + p.portConf.Name + " " +
169-
err.Error() + " The data we were trying to convert is: " + string(ch[:n]))
170-
break
85+
data := ""
86+
switch buftype {
87+
case "timedraw", "timed":
88+
data = string(bufferPart[:n])
89+
// give the data to our bufferflow so it can do it's work
90+
// to read/translate the data to see if it wants to block
91+
// writes to the serialport. each bufferflow type will decide
92+
// this on its own based on its logic
93+
p.bufferwatcher.OnIncomingData(data)
94+
case "default": // the bufferbuftype is actually called default 🤷‍♂️
95+
// save the left out bytes for the next iteration due to UTF-8 encoding
96+
bufferPart = append(buffered_ch.Bytes(), bufferPart[:n]...)
97+
n += len(buffered_ch.Bytes())
98+
buffered_ch.Reset()
99+
for i, w := 0, 0; i < n; i += w {
100+
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)
101+
if runeValue == utf8.RuneError {
102+
buffered_ch.Write(bufferPart[i:n])
103+
break
104+
}
105+
if i == n {
106+
buffered_ch.Reset()
107+
}
108+
data += string(runeValue)
109+
w = width
171110
}
172-
//log.Print("Printing out json byte data...")
173-
//log.Print(string(b))
174-
h.broadcastSys <- b
111+
p.bufferwatcher.OnIncomingData(data)
112+
default:
113+
log.Panicf("unknown buffer type %s", buftype)
175114
}
176115
}
177116

@@ -235,18 +174,10 @@ func (p *serport) writerBuffered() {
235174
// sees something come in
236175
for data := range p.sendBuffered {
237176

238-
// we want to block here if we are being asked to pause.
239-
goodToGo, _ := p.bufferwatcher.BlockUntilReady(string(data), "")
240-
241-
if goodToGo == false {
242-
log.Println("We got back from BlockUntilReady() but apparently we must cancel this cmd")
243-
// since we won't get a buffer decrement in p.sendNoBuf, we must do it here
244-
p.itemsInBuffer--
245-
} else {
246-
// send to the non-buffered serial port writer
247-
//log.Println("About to send to p.sendNoBuf channel")
248-
p.sendNoBuf <- data
249-
}
177+
// send to the non-buffered serial port writer
178+
//log.Println("About to send to p.sendNoBuf channel")
179+
p.sendNoBuf <- []byte(data)
180+
250181
}
251182
msgstr := "writerBuffered just got closed. make sure you make a new one. port:" + p.portConf.Name
252183
log.Println(msgstr)
@@ -269,7 +200,7 @@ func (p *serport) writerNoBuf() {
269200

270201
// FINALLY, OF ALL THE CODE IN THIS PROJECT
271202
// WE TRULY/FINALLY GET TO WRITE TO THE SERIAL PORT!
272-
n2, err := p.portIo.Write([]byte(data))
203+
n2, err := p.portIo.Write(data)
273204

274205
log.Print("Just wrote ", n2, " bytes to serial: ", string(data))
275206
if err != nil {
@@ -287,6 +218,38 @@ func (p *serport) writerNoBuf() {
287218
spList(false)
288219
}
289220

221+
// this method runs as its own thread because it's instantiated
222+
// as a "go" method. so if it blocks inside, it is ok
223+
func (p *serport) writerRaw() {
224+
// this method can panic if user closes serial port and something is
225+
// in BlockUntilReady() and then a send occurs on p.sendNoBuf
226+
227+
defer func() {
228+
if e := recover(); e != nil {
229+
log.Println("Got panic: ", e)
230+
}
231+
}()
232+
233+
// this for loop blocks on p.sendRaw until that channel
234+
// sees something come in
235+
for data := range p.sendRaw {
236+
237+
// Decode stuff
238+
sDec, err := base64.StdEncoding.DecodeString(data)
239+
if err != nil {
240+
log.Println("Decoding error:", err)
241+
}
242+
log.Println(string(sDec))
243+
244+
// send to the non-buffered serial port writer
245+
p.sendNoBuf <- sDec
246+
247+
}
248+
msgstr := "writerRaw just got closed. make sure you make a new one. port:" + p.portConf.Name
249+
log.Println(msgstr)
250+
h.broadcastSys <- []byte(msgstr)
251+
}
252+
290253
func spHandlerOpen(portname string, baud int, buftype string) {
291254

292255
log.Print("Inside spHandler")
@@ -319,16 +282,19 @@ func spHandlerOpen(portname string, baud int, buftype string) {
319282
log.Print("Opened port successfully")
320283
//p := &serport{send: make(chan []byte, 256), portConf: conf, portIo: sp}
321284
// we can go up to 256,000 lines of gcode in the buffer
322-
p := &serport{sendBuffered: make(chan string, 256000), sendNoBuf: make(chan string), portConf: conf, portIo: sp, BufferType: buftype}
285+
p := &serport{sendBuffered: make(chan string, 256000), sendNoBuf: make(chan []byte), sendRaw: make(chan string), portConf: conf, portIo: sp, BufferType: buftype}
323286

324287
var bw Bufferflow
325288

326-
if buftype == "timed" {
327-
bw = &BufferflowTimed{Name: "timed", Port: portname, Output: h.broadcastSys, Input: make(chan string)}
328-
} else if buftype == "timedraw" {
329-
bw = &BufferflowTimedRaw{Name: "timedraw", Port: portname, Output: h.broadcastSys, Input: make(chan string)}
330-
} else {
331-
bw = &BufferflowDefault{Port: portname}
289+
switch buftype {
290+
case "timed":
291+
bw = NewBufferflowTimed(portname, h.broadcastSys)
292+
case "timedraw":
293+
bw = NewBufferflowTimedRaw(portname, h.broadcastSys)
294+
case "default":
295+
bw = NewBufferflowDefault(portname, h.broadcastSys)
296+
default:
297+
log.Panicf("unknown buffer type: %s", buftype)
332298
}
333299

334300
bw.Init()
@@ -344,14 +310,13 @@ func spHandlerOpen(portname string, baud int, buftype string) {
344310
go p.writerBuffered()
345311
// this is thread to send to serial port regardless of block
346312
go p.writerNoBuf()
347-
p.reader()
313+
// this is thread to send to serial port but with base64 decoding
314+
go p.writerRaw()
315+
316+
p.reader(buftype)
348317

349318
spListDual(false)
350319
spList(false)
351-
352-
//go p.reader()
353-
//p.done = make(chan bool)
354-
//<-p.done
355320
}
356321

357322
func spHandlerClose(p *serport) {

‎test/common.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import os
2+
3+
def running_on_ci():
4+
"""
5+
Returns whether the program is running on a CI environment
6+
"""
7+
return 'GITHUB_WORKFLOW' in os.environ

‎test/conftest.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pytest
88
from invoke import Local
99
from invoke.context import Context
10-
10+
import socketio as io
1111

1212
@pytest.fixture(scope="function")
1313
def agent(pytestconfig):
@@ -43,3 +43,42 @@ def agent(pytestconfig):
4343
@pytest.fixture(scope="session")
4444
def base_url():
4545
return "http://127.0.0.1:8991"
46+
47+
@pytest.fixture(scope="function")
48+
def socketio(base_url, agent):
49+
sio = io.Client()
50+
sio.connect(base_url)
51+
yield sio
52+
sio.disconnect()
53+
54+
@pytest.fixture(scope="session")
55+
def serial_port():
56+
return "/dev/ttyACM0" # maybe this could be enhanced by calling arduino-cli
57+
58+
@pytest.fixture(scope="session")
59+
def baudrate():
60+
return "9600"
61+
62+
# open_port cannot be coced as a fixture because of the buffertype parameter
63+
64+
# at the end of the test closes the serial port
65+
@pytest.fixture(scope="function")
66+
def close_port(socketio, serial_port):
67+
yield socketio
68+
socketio.emit('command', 'close ' + serial_port)
69+
time.sleep(.5)
70+
71+
72+
@pytest.fixture(scope="function")
73+
def message(socketio):
74+
global message
75+
message = []
76+
#in message var we will find the "response"
77+
socketio.on('message', message_handler)
78+
return message
79+
80+
# callback called by socketio when a message is received
81+
def message_handler(msg):
82+
# print('Received message: ', msg)
83+
global message
84+
message.append(msg)

‎test/test_ws.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import time
2+
import json
3+
import base64
4+
import pytest
5+
6+
from common import running_on_ci
7+
message = []
8+
9+
10+
def test_ws_connection(socketio):
11+
print('my sid is', socketio.sid)
12+
assert socketio.sid is not None
13+
14+
15+
def test_list(socketio, message):
16+
socketio.emit('command', 'list')
17+
time.sleep(.2)
18+
print (message)
19+
assert any("list" in i for i in message)
20+
assert any("Ports" in i for i in message)
21+
assert any("Network" in i for i in message)
22+
23+
24+
# NOTE run the following tests with a board connected to the PC
25+
@pytest.mark.skipif(
26+
running_on_ci(),
27+
reason="VMs have no serial ports",
28+
)
29+
def test_open_serial_default(socketio, serial_port, baudrate, message):
30+
general_open_serial(socketio, serial_port, baudrate, message, "default")
31+
32+
33+
@pytest.mark.skipif(
34+
running_on_ci(),
35+
reason="VMs have no serial ports",
36+
)
37+
def test_open_serial_timed(socketio, serial_port, baudrate, message):
38+
general_open_serial(socketio, serial_port, baudrate, message, "timed")
39+
40+
41+
@pytest.mark.skipif(
42+
running_on_ci(),
43+
reason="VMs have no serial ports",
44+
)
45+
def test_open_serial_timedraw(socketio, serial_port, baudrate, message):
46+
general_open_serial(socketio, serial_port, baudrate, message, "timedraw")
47+
48+
49+
# 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
50+
@pytest.mark.skipif(
51+
running_on_ci(),
52+
reason="VMs have no serial ports",
53+
)
54+
def test_send_serial_default(socketio, close_port, serial_port, baudrate, message):
55+
general_send_serial(socketio, close_port, serial_port, baudrate, message, "default")
56+
57+
58+
@pytest.mark.skipif(
59+
running_on_ci(),
60+
reason="VMs have no serial ports",
61+
)
62+
def test_send_serial_timed(socketio, close_port, serial_port, baudrate, message):
63+
general_send_serial(socketio, close_port, serial_port, baudrate, message, "timed")
64+
65+
66+
@pytest.mark.skipif(
67+
running_on_ci(),
68+
reason="VMs have no serial ports",
69+
)
70+
def test_send_serial_timedraw(socketio, close_port, serial_port, baudrate, message):
71+
general_send_serial(socketio, close_port, serial_port, baudrate, message, "timedraw")
72+
73+
74+
@pytest.mark.skipif(
75+
running_on_ci(),
76+
reason="VMs have no serial ports",
77+
)
78+
def test_send_emoji_serial_default(socketio, close_port, serial_port, baudrate, message):
79+
general_send_emoji_serial(socketio, close_port, serial_port, baudrate, message, "default")
80+
81+
82+
@pytest.mark.skipif(
83+
running_on_ci(),
84+
reason="VMs have no serial ports",
85+
)
86+
def test_send_emoji_serial_timed(socketio, close_port, serial_port, baudrate, message):
87+
general_send_emoji_serial(socketio, close_port, serial_port, baudrate, message, "timed")
88+
89+
90+
@pytest.mark.skipif(
91+
running_on_ci(),
92+
reason="VMs have no serial ports",
93+
)
94+
def test_send_emoji_serial_timedraw(socketio, close_port, serial_port, baudrate, message):
95+
general_send_emoji_serial(socketio, close_port, serial_port, baudrate, message, "timedraw")
96+
97+
98+
def general_open_serial(socketio, serial_port, baudrate, message, buffertype):
99+
open_serial_port(socketio, serial_port, baudrate, message, buffertype)
100+
# test the closing of the serial port, we are gonna use close_port for the other tests
101+
socketio.emit('command', 'close ' + serial_port)
102+
time.sleep(.2)
103+
print (message)
104+
#check if port has been closed
105+
assert any("\"IsOpen\": false," in i for i in message)
106+
107+
108+
def general_send_serial(socketio, close_port, serial_port, baudrate, message, buffertype):
109+
open_serial_port(socketio, serial_port, baudrate, message, buffertype)
110+
# send the string "ciao" using the serial connection
111+
socketio.emit('command', 'send ' + serial_port + ' ciao')
112+
time.sleep(1)
113+
print(message)
114+
# check if the send command has been registered
115+
assert any("send " + serial_port + " ciao" in i for i in message)
116+
#check if message has been sent back by the connected board
117+
if buffertype == "timedraw":
118+
output = decode_output(extract_serial_data(message))
119+
elif buffertype in ("default", "timed"):
120+
output = extract_serial_data(message)
121+
assert "ciao" in output
122+
# the serial connection is closed by close_port() fixture: even if in case of test failure
123+
124+
125+
def general_send_emoji_serial(socketio, close_port, serial_port, baudrate, message, buffertype):
126+
open_serial_port(socketio, serial_port, baudrate, message, buffertype)
127+
# send a lot of emoji: they can be messed up
128+
socketio.emit('command', 'send ' + serial_port + ' /"🧀🧀🧀🧀🧀🧀🧀🧀🧀🧀/"')
129+
time.sleep(1)
130+
print(message)
131+
# check if the send command has been registered
132+
assert any("send " + serial_port + " /\"🧀🧀🧀🧀🧀🧀🧀🧀🧀🧀/\"" in i for i in message)
133+
if buffertype == "timedraw":
134+
output = decode_output(extract_serial_data(message))
135+
elif buffertype in ("default", "timed"):
136+
output = extract_serial_data(message)
137+
assert "/\"🧀🧀🧀🧀🧀🧀🧀🧀🧀🧀/\"" in output
138+
# the serial connection is closed by close_port() fixture: even if in case of test failure
139+
140+
141+
def open_serial_port(socketio, serial_port, baudrate, message, buffertype):
142+
#open a new serial connection with the specified buffertype
143+
socketio.emit('command', 'open ' + serial_port + ' ' + baudrate + ' ' + buffertype)
144+
# give time to the message var to be filled
145+
time.sleep(.5)
146+
print(message)
147+
# the serial connection should be open now
148+
assert any("\"IsOpen\": true" in i for i in message)
149+
150+
151+
@pytest.mark.skipif(
152+
running_on_ci(),
153+
reason="VMs have no serial ports",
154+
)
155+
def test_sendraw_serial(socketio, close_port, serial_port, baudrate, message):
156+
open_serial_port(socketio, serial_port, baudrate, message, "timedraw")
157+
#test with bytes
158+
integers = [1, 2, 3, 4, 5]
159+
bytes_array=bytearray(integers)
160+
encoded_integers = base64.b64encode(bytes_array).decode('ascii')
161+
socketio.emit('command', 'sendraw ' + serial_port + ' ' + encoded_integers)
162+
time.sleep(1)
163+
print(message)
164+
# check if the send command has been registered
165+
assert any(("sendraw " + serial_port + ' ' + encoded_integers) in i for i in message)
166+
#check if message has been sent back by the connected board
167+
output = extract_serial_data(message) # TODO use decode_output()
168+
print (output)
169+
assert encoded_integers in output
170+
171+
172+
# helper function used to extract serial data from its JSON representation
173+
# NOTE make sure to pass a clean message (maybe reinitialize the message global var before populating it)
174+
def extract_serial_data(msg):
175+
serial_data = ""
176+
for i in msg:
177+
if "{\"P\"" in i:
178+
print (json.loads(i)["D"])
179+
serial_data+=json.loads(i)["D"]
180+
print("serialdata:"+serial_data)
181+
return serial_data
182+
183+
def decode_output(raw_output):
184+
# print(raw_output)
185+
base64_bytes = raw_output.encode('ascii') #encode rawoutput message into a bytes-like object
186+
output_bytes = base64.b64decode(base64_bytes)
187+
return output_bytes.decode('utf-8')
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
int incomingByte = 0; // for incoming serial data
2+
3+
void setup() {
4+
Serial.begin(9600); // opens serial port, sets data rate to 9600 bps
5+
}
6+
7+
void loop() {
8+
// send data only when you receive data:
9+
if (Serial.available() > 0) {
10+
11+
// read the incoming byte:
12+
incomingByte = Serial.read();
13+
14+
// say what you got:
15+
Serial.print((char)incomingByte);
16+
}
17+
18+
}

0 commit comments

Comments
 (0)
Please sign in to comment.