1
0
Fork 0

Initial working serial port writer

main
Ambrose Chua 2020-06-16 15:35:55 +08:00
commit 3c9ba66295
8 changed files with 365 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
apply

93
conf.go Normal file
View File

@ -0,0 +1,93 @@
package main
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"time"
)
type Device interface {
ReadTimeout(time.Duration) ([]byte, error)
WriteLine([]byte) error
Close() error
}
var ErrorDeviceOperationNotSupported = errors.New("device operation not supported")
type ConfigurationLine interface {
Apply(Device) error
}
type ConfigurationDirective struct {
Name string
Argument string
}
type ConfigurationSimple []byte
func (d ConfigurationDirective) Apply(dev Device) error {
if d.Name == "sleep" {
seconds := 0
_, err := fmt.Sscanf(string(d.Argument), "%d", &seconds)
if err != nil {
return fmt.Errorf("bad sleep arguments: %w", err)
}
time.Sleep(time.Duration(seconds) * time.Second)
}
return nil
}
func (s ConfigurationSimple) Apply(dev Device) error {
err := dev.WriteLine([]byte(s))
if err != nil {
return err
}
response, err := dev.ReadTimeout(200 * time.Millisecond)
if errors.Is(err, ErrorDeviceOperationNotSupported) {
} else if err != nil {
return err
}
fmt.Printf("%s", response)
return nil
}
type ConfigurationReader bufio.Reader
func NewConfigurationReader(r io.Reader) *ConfigurationReader {
return (*ConfigurationReader)(bufio.NewReader(r))
}
func (c *ConfigurationReader) Read() (ConfigurationLine, error) {
line, err := (*bufio.Reader)(c).ReadBytes('\n')
if err != nil {
return nil, err
}
line = bytes.TrimSuffix(line, []byte{'\n'})
if len(line) > 1 && line[0] == '!' && line[1] == ' ' {
// Comment
return ConfigurationSimple(string(line)), nil
} else if len(line) > 0 && line[0] == '!' {
// Directive
line = line[1:]
firstSep := bytes.IndexByte(line, ' ')
if firstSep < 0 {
return ConfigurationDirective{
Name: string(line),
}, nil
}
directive := line[:firstSep]
rest := line[firstSep+1:]
return ConfigurationDirective{
Name: string(directive),
Argument: string(rest),
}, nil
} else {
return ConfigurationSimple(line), nil
}
}

35
file.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"fmt"
"os"
"time"
)
type DeviceFile struct {
file *os.File
}
func NewDeviceFile(name string) (DeviceFile, error) {
file, err := os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return DeviceFile{}, err
}
return DeviceFile{
file: file,
}, nil
}
func (f DeviceFile) ReadTimeout(timeout time.Duration) ([]byte, error) {
return nil, fmt.Errorf("%w: Read", ErrorDeviceOperationNotSupported)
}
func (f DeviceFile) WriteLine(buf []byte) error {
buf = append(buf, '\n')
f.file.Write(buf)
return nil
}
func (f DeviceFile) Close() error {
return f.file.Close()
}

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module git.makerforce.io/dump/sit/cs2203/confs/apply
go 1.14
require (
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
go.bug.st/serial v1.1.0
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
)

15
go.sum Normal file
View File

@ -0,0 +1,15 @@
github.com/creack/goselect v0.1.1 h1:tiSSgKE1eJtxs1h/VgGQWuXUP0YS4CDIFMp6vaI1ls0=
github.com/creack/goselect v0.1.1/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
go.bug.st/serial v1.1.0 h1:O0EHZw8ZdhmTAikak5ZY/8vyKCpFxZYgqZw1bGegxU8=
go.bug.st/serial v1.1.0/go.mod h1:rpXPISGjuNjPTRTcMlxi9lN6LoIPxd1ixVjBd8aSk/Q=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

109
main.go Normal file
View File

@ -0,0 +1,109 @@
/*
apply interacts with a serial port to configure a Cisco product using files.
This tool does not attempt to understand the proprietary donkey format that is
the Cisco CLI. Instead, it provides some directives around it to make life
easier when crafting configuration files.
Basic usage is very simple: Pass a file containing Cisco commands and it will
write it out to serial port. It also prints the output back to stdout for
visual inspection of success.
apply also contains some additional features to help you in handling files. The
main feature is directives that perform certain actions. These are expressed
within a line beginning with "!".
Directives
!sleep <seconds>
Sleep a fixed number of seconds.
!assert <text>
Ensure that in the next 1 second, output from the serial port contains <text>.
*/
package main
import (
"errors"
"flag"
"fmt"
"io"
"os"
"time"
)
var Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [-port PORT] [-writefile] FILE...\n", os.Args[0])
flag.PrintDefaults()
}
func main() {
port := flag.String("port", "/dev/ttyUSB0", "Serial port to write to")
writefile := flag.Bool("writefile", false, "Treat PORT as file, and write to file")
flag.Parse()
confs := flag.Args()
if len(confs) < 1 {
Usage()
return
}
var device Device
var err error
if *writefile {
device, err = NewDeviceFile(*port)
} else {
device, err = NewDeviceSerial(*port)
}
if err != nil {
panic(err)
}
defer device.Close()
for _, conf := range confs {
file, err := os.Open(conf)
if err != nil {
panic(fmt.Errorf("file %s: %w", conf, err))
}
reader := NewConfigurationReader(file)
lineNum := 0
var line ConfigurationLine
for {
lineNum++
line, err = reader.Read()
if err != nil {
break
}
if _, ok := line.(ConfigurationSimple); !ok {
fmt.Printf("directive: %v\n", line)
}
err = line.Apply(device)
if err != nil {
break
}
}
if err != nil && err != io.EOF {
panic(fmt.Errorf("file %s line %d: %w", conf, lineNum, err))
}
// Attempt to read rest of output
response, err := device.ReadTimeout(1000 * time.Millisecond)
if errors.Is(err, ErrorDeviceOperationNotSupported) {
} else if err != nil {
panic(fmt.Errorf("device: %w", err))
}
fmt.Printf("%s", response)
if err := file.Close(); err != nil {
panic(fmt.Errorf("file %s: %w", conf, err))
}
}
}

64
serial.go Normal file
View File

@ -0,0 +1,64 @@
package main
import (
"time"
"go.bug.st/serial"
)
type DeviceSerial struct {
port serial.Port
lines chan []byte
lineErr chan error
}
func NewDeviceSerial(name string) (DeviceSerial, error) {
serialMode := &serial.Mode{
BaudRate: 9600,
DataBits: 8,
Parity: serial.NoParity,
StopBits: serial.OneStopBit,
}
port, err := serial.Open(name, serialMode)
if err != nil {
return DeviceSerial{}, err
}
lines, lineErr := lineChannel(port)
return DeviceSerial{
port: port,
lines: lines,
lineErr: lineErr,
}, nil
}
func (s DeviceSerial) ReadTimeout(timeout time.Duration) ([]byte, error) {
var err error
buf := make([]byte, 0)
t := timerChannel(timeout)
for {
// Read lines until timeout
select {
case line := <-s.lines:
buf = append(buf, line...)
case err = <-s.lineErr:
return buf, err
case <-t:
return buf, err
}
}
return nil, nil
}
func (s DeviceSerial) WriteLine(buf []byte) error {
// TODO: handle n
buf = append(buf, '\r')
_, err := s.port.Write(buf)
if err != nil {
return err
}
return nil
}
func (s DeviceSerial) Close() error {
return s.port.Close()
}

39
util.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"bufio"
"io"
"time"
)
func lineChannel(r io.Reader) (chan []byte, chan error) {
lineChan := make(chan []byte, 100)
errChan := make(chan error)
reader := bufio.NewReader(r)
go func() {
for {
line, err := reader.ReadBytes('\n')
lineChan <- line
if err != nil {
errChan <- err
break
}
}
close(lineChan)
}()
return lineChan, errChan
}
func timerChannel(t time.Duration) chan bool {
timerChan := make(chan bool)
go func() {
time.Sleep(t)
timerChan <- true
}()
return timerChan
}