I'm sending and receiving data over an interface (serial in this case) with the following behaviour:
- The receiver sends back an Ack message if a message is delivered successfully.
- If an Ack is not received within a timeout, the command should be re-sent limited a number of times.
- Multiple clients may request sending at the same time, but no new commands should be sent until an Ack for the last command is received or it times out. Other sending commands shall wait until the channel is unreserved or they time out.
The algorithm to do this is fairly simple. (Feel free to say so if you think it can be improved):
The implementation in Go works as expected. I just feel there might be a smarter way to do it:
package main
import (
"fmt"
"math/rand"
"time"
)
/****** Simulating the receiver behaviour ******/
var receiveChannel = make(chan string, 1)
// Used to read messages from the interface
func Read() string {
return <-receiveChannel
}
// Used to write messages to the interface
func Write(data string) {
// Randomly drop 50% of packets
if n := rand.Intn(100); n < 50 {
receiveChannel <- "ACK"
}
}
/*******************************************/
var ackChannel = make(chan bool, 1)
var sendChannel = make(chan string, 1)
func run() {
for {
if Read() == "ACK" {
ackChannel <- true
}
}
}
func sendWrapper(data string) error {
// Reserve the sending channel
timeoutTimer := time.NewTimer(1 * time.Second)
select {
// This should block until sendChannel has a free spot or times out
case sendChannel <- data:
timeoutTimer.Stop()
fmt.Printf("Send chan reserved for command id=%v\n", data)
case <-timeoutTimer.C:
return fmt.Errorf("timed out while waiting for send channel to be free. id=%v", data)
}
attempts := 2
err := send(data, attempts)
// Free up the sending channel
select {
case x := <-sendChannel:
fmt.Printf("Send channel cleared %v\n", x)
default:
fmt.Printf("Send channel is empty. This should never happen!\n")
}
return err
}
func send(data string, attempts int) error {
// Send data
Write(data)
// Wait for an ACK to be received
ackTimeoutTimer := time.NewTimer(time.Millisecond * 100)
select {
case <-ackChannel:
ackTimeoutTimer.Stop()
case <-ackTimeoutTimer.C:
// Retry again
if attempts > 1 {
return send(data, attempts-1)
}
return fmt.Errorf("Timed out while waiting for ack. id=%v", data)
}
fmt.Printf("Frame sent and acked id=%v\n", data)
return nil
}
/****** Testing ******/
func main() {
go run()
// Multiple goroutines sending data
for i := 0; i < 7; i++ {
go func() {
x := i
if err := sendWrapper(fmt.Sprint(x)); err != nil {
fmt.Println(err.Error())
} else {
fmt.Printf("Sending successful. i=%v\n", x)
}
}()
time.Sleep(10 * time.Millisecond)
}
time.Sleep(4 * time.Second)
}