From 8d6b67438264e10628e9857bf4f6d8a84379e626 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Wed, 29 Sep 2021 17:05:40 +0200 Subject: [PATCH] add commands to control the radio terminal --- com/com.go | 4 +++ ctrl/command.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ ctrl/ctrl.go | 40 +++++++++++++++++++++++++++++ sds/command.go | 12 +-------- tetra/tetra.go | 14 ++++++++++ 5 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 ctrl/command.go create mode 100644 ctrl/ctrl.go diff --git a/com/com.go b/com/com.go index 44dc4d4..bb68d10 100644 --- a/com/com.go +++ b/com/com.go @@ -211,6 +211,10 @@ func (c *COM) ClearSyntaxErrors(ctx context.Context) error { return nil } +func (c *COM) Request(ctx context.Context, request string) ([]string, error) { + return c.AT(ctx, request) +} + func (c *COM) AT(ctx context.Context, request string) ([]string, error) { cmd := command{ request: request, diff --git a/ctrl/command.go b/ctrl/command.go new file mode 100644 index 0000000..ffa8f1e --- /dev/null +++ b/ctrl/command.go @@ -0,0 +1,68 @@ +package ctrl + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/ftl/tetra-pei/tetra" +) + +// SetOperatingMode according to [PEI] 6.14.7.2 +func SetOperatingMode(mode AIMode) string { + return fmt.Sprintf("AT+CTOM=%d", mode) +} + +var requestOperatingModeResponse = regexp.MustCompile(`^\+CTOM: (\d+)$`) + +// RequestOperatingMode reads the current operating mode according to [PEI] 6.14.7.4 +func RequestOperatingMode(ctx context.Context, requester tetra.Requester) (AIMode, error) { + responses, err := requester.Request(ctx, "AT+CTOM?") + if err != nil { + return 0, err + } + if len(responses) < 1 { + return 0, fmt.Errorf("no response received") + } + response := strings.ToUpper(strings.TrimSpace(responses[0])) + parts := requestOperatingModeResponse.FindStringSubmatch(response) + + if len(parts) != 2 { + return 0, fmt.Errorf("unexpected response: %s", responses[0]) + } + + result, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, err + } + + return AIMode(result), nil +} + +// SetTalkgroup according to [PEI] 6.15.6.2 +func SetTalkgroup(gtsi string) string { + return fmt.Sprintf("AT+CTGS=1,%s", gtsi) +} + +var requestTalkgroupResponse = regexp.MustCompile(`^\+CTGS: .*,(\d+)$`) + +// RequestTalkgroup reads the current talkgroup according to [PEI] 6.15.6.4 +func RequestTalkgroup(ctx context.Context, requester tetra.Requester) (string, error) { + responses, err := requester.Request(ctx, "AT+CTGS?") + if err != nil { + return "", err + } + if len(responses) < 1 { + return "", fmt.Errorf("no response received") + } + response := strings.ToUpper(strings.TrimSpace(responses[0])) + parts := requestTalkgroupResponse.FindStringSubmatch(response) + + if len(parts) != 2 { + return "", fmt.Errorf("unexpected response: %s", responses[0]) + } + + return parts[1], nil +} diff --git a/ctrl/ctrl.go b/ctrl/ctrl.go new file mode 100644 index 0000000..bd185b1 --- /dev/null +++ b/ctrl/ctrl.go @@ -0,0 +1,40 @@ +package ctrl + +import ( + "fmt" + "strings" +) + +// AIModeByName returns the AIMode with the given name +func AIModeByName(name string) (AIMode, error) { + sanitized := strings.ToUpper(strings.TrimSpace(name)) + result, ok := AIModesByName[sanitized] + if !ok { + return 0, fmt.Errorf("invalid operating mode %s", name) + } + return result, nil +} + +// AIMode represents an operating mode according to [PEI] 6.17.4 +type AIMode byte + +func (m AIMode) String() string { + for k, v := range AIModesByName { + if v == m { + return k + } + } + return "UNKNOWN" +} + +// All supported operating modes +const ( + TMO AIMode = iota + DMO +) + +// AIModesByName maps all supported operating modes by their string representation +var AIModesByName = map[string]AIMode{ + "TMO": TMO, + "DMO": DMO, +} diff --git a/sds/command.go b/sds/command.go index a94550a..599a24e 100644 --- a/sds/command.go +++ b/sds/command.go @@ -20,16 +20,6 @@ func (f EncoderFunc) Encode() ([]byte, int) { return f() } -type Requester interface { - Request(context.Context, string) ([]string, error) -} - -type RequesterFunc func(context.Context, string) ([]string, error) - -func (f RequesterFunc) Request(ctx context.Context, request string) ([]string, error) { - return f(ctx, request) -} - const ( // CRLF line ending for AT commands CRLF = "\x0d\x0a" @@ -53,7 +43,7 @@ func SendMessage(destination tetra.Identity, message Encoder) string { var sendMessageDescription = regexp.MustCompile(`^\+CMGS: .+\(\d*-(\d*)\)$`) // RequestMaxMessagePDUBits uses the given RequesterFunc to find out how many bits a message PDU may have (see [PEI] 6.13.2). -func RequestMaxMessagePDUBits(ctx context.Context, requester Requester) (int, error) { +func RequestMaxMessagePDUBits(ctx context.Context, requester tetra.Requester) (int, error) { responses, err := requester.Request(ctx, "AT+CMGS=?") if err != nil { return 0, err diff --git a/tetra/tetra.go b/tetra/tetra.go index 8efcab1..c8476cb 100644 --- a/tetra/tetra.go +++ b/tetra/tetra.go @@ -1,11 +1,25 @@ package tetra import ( + "context" "encoding/hex" "regexp" "strings" ) +// Requester is used for commands that return more than an error code. +type Requester interface { + Request(context.Context, string) ([]string, error) +} + +// RequesterFunc wraps a match function into the Requester interface. +type RequesterFunc func(context.Context, string) ([]string, error) + +// Request calls the wrapped RequesterFunc. +func (f RequesterFunc) Request(ctx context.Context, request string) ([]string, error) { + return f(ctx, request) +} + // Identity represents an identity of a party in a TETRA communication type Identity string