package com import ( "context" "fmt" "io" "strings" "time" ) const ( readBufferSize = 1024 atSendingQueueTimeout = 500 * time.Millisecond ) // NewWithTrace creates a new COM instance that traces all communications to a second writer. func NewWithTrace(device io.ReadWriter, tracer io.Writer) *COM { result := New(device) result.tracer = tracer return result } // New creates a new COM instance using the given io.ReadWriter to communicate with the radio's PEI. func New(device io.ReadWriter) *COM { lines := readLoop(device) commands := make(chan command) result := &COM{ commands: commands, closed: make(chan struct{}), indications: make(map[string]indicationConfig), } go func() { result.trace("****\n* SESSION START\n****\n") defer result.trace("****\n* SESSION END\n****\n") defer close(result.closed) var commandCancelled <-chan struct{} var activeCommand *command var activeIndication *indication tick := time.NewTicker(100 * time.Millisecond) defer tick.Stop() for { select { case line, valid := <-lines: if !valid { return } result.tracef("rx: %s\nhex: %X\n--\n", line, line) switch { case activeIndication != nil: activeIndication.AddLine(line) if activeIndication.Complete() { activeIndication = nil } case activeCommand != nil: activeIndication = result.newIndication(line) if activeIndication != nil { break } activeCommand.AddLine(line) if activeCommand.Complete() { commandCancelled = nil activeCommand = nil } default: activeIndication = result.newIndication(line) } case <-commandCancelled: commandCancelled = nil activeCommand = nil case <-tick.C: } if activeCommand == nil { select { case cmd := <-commands: if len(cmd.request) == 0 { break } txbytes := make([]byte, 0, len(cmd.request)+2) txbytes = append(txbytes, []byte(cmd.request)...) lastbyte := txbytes[len(txbytes)-1] if (lastbyte != 0x1a) && (lastbyte != 0x1b) { txbytes = append(txbytes, 0x0d, 0x0a) } result.tracef("tx: %s\nhex: %X\n--\n", txbytes, txbytes) device.Write(txbytes) commandCancelled = cmd.cancelled activeCommand = &cmd default: } } } }() return result } // COM allows to communicate with a radio's PEI using AT commands. type COM struct { commands chan<- command closed chan struct{} tracer io.Writer indications map[string]indicationConfig } func readLoop(r io.Reader) <-chan string { lines := make(chan string, 1) go func() { buf := make([]byte, readBufferSize) currentLine := make([]byte, 0, readBufferSize) for { n, err := r.Read(buf) if err == io.EOF { if len(currentLine) > 0 { lines <- string(currentLine) } close(lines) return } else if err != nil { if len(currentLine) > 0 { lines <- string(currentLine) } close(lines) return } for _, b := range buf[0:n] { switch { case b == '\n': if len(currentLine) == 0 { continue } lines <- string(currentLine) currentLine = currentLine[:0] case b < ' ': continue default: currentLine = append(currentLine, b) } } } }() return lines } func (c *COM) Closed() bool { select { case <-c.closed: return true default: return false } } func (c *COM) AddIndication(prefix string, trailingLines int, handler func(lines []string)) error { config := indicationConfig{ prefix: strings.ToUpper(prefix), trailingLines: trailingLines, handler: handler, } c.indications[config.prefix] = config return nil } func (c *COM) newIndication(line string) *indication { for _, config := range c.indications { result := config.NewIfMatches(line) if result != nil { return result } } return nil } func (c *COM) ClearSyntaxErrors(ctx context.Context) error { for true { _, err := c.AT(ctx, "AT") if err == nil { return nil } if err.Error() == "+CME ERROR: 35" { time.Sleep(200) } else { return err } } return nil } func (c *COM) AT(ctx context.Context, request string) ([]string, error) { cmd := command{ request: request, response: make(chan []string, 1), err: make(chan error, 1), cancelled: ctx.Done(), completed: make(chan struct{}), } select { case c.commands <- cmd: case <-ctx.Done(): return nil, ctx.Err() case <-time.After(atSendingQueueTimeout): return nil, fmt.Errorf("AT sending queue timeout") } select { case response := <-cmd.response: return response, nil case err := <-cmd.err: return nil, err case <-ctx.Done(): return nil, ctx.Err() } } func (c *COM) ATs(ctx context.Context, requests ...string) error { for _, request := range requests { _, err := c.AT(ctx, request) if err != nil { return fmt.Errorf("%s failed: %w", request, err) } } return nil } func (c *COM) trace(args ...interface{}) { if c.tracer == nil { return } fmt.Fprint(c.tracer, args...) } func (c *COM) tracef(format string, args ...interface{}) { if c.tracer == nil { return } fmt.Fprintf(c.tracer, format, args...) } type indicationConfig struct { prefix string trailingLines int handler func(lines []string) } func (c *indicationConfig) NewIfMatches(line string) *indication { if !strings.HasPrefix(strings.ToUpper(line), c.prefix) { return nil } result := &indication{ config: *c, lines: []string{line}, } if result.Complete() { c.handler([]string{line}) return nil } return result } type indication struct { config indicationConfig lines []string } func (ind *indication) AddLine(line string) { if ind.Complete() { return } ind.lines = append(ind.lines, line) if ind.Complete() { go func() { ind.config.handler(ind.lines) }() } } func (ind *indication) Complete() bool { return len(ind.lines) >= ind.config.trailingLines+1 } type command struct { lines []string request string response chan []string err chan error cancelled <-chan struct{} completed chan struct{} } func (c *command) AddLine(line string) { select { case <-c.cancelled: return case <-c.completed: return default: } saniLine := strings.TrimSpace(strings.ToUpper(line)) switch { case saniLine == "OK": c.response <- c.lines close(c.completed) case strings.HasPrefix(saniLine, "ERROR"): c.err <- fmt.Errorf("%s", line) close(c.completed) case strings.HasPrefix(saniLine, "+CME ERROR:"): c.err <- fmt.Errorf("%s", line) close(c.completed) case strings.HasPrefix(saniLine, "+CMS ERROR"): c.err <- fmt.Errorf("%s", line) close(c.completed) default: c.lines = append(c.lines, line) } } func (c *command) Complete() bool { select { case <-c.cancelled: return true case <-c.completed: return true default: return false } }