diff --git a/ctrl/command.go b/ctrl/command.go index 8cd5c50..6cf0b14 100644 --- a/ctrl/command.go +++ b/ctrl/command.go @@ -54,6 +54,96 @@ func RequestTalkgroup(ctx context.Context, requester tetra.Requester) (string, e return parts[1], nil } +const ( + talkgroupRangeRequest = "AT+CNUM%s=?" + talkgroupsPrepareRequest = "AT+CNUM%s=0,%d,%d" + talkgroupsReadRequest = "AT+CNUM%s?" +) + +type TalkgroupKind string + +const ( + TalkgroupFixed TalkgroupKind = "F" + TalkgroupStatic TalkgroupKind = "S" + TalkgroupDynamic TalkgroupKind = "D" +) + +type TalkgroupRange struct { + Min int + Max int +} + +type TalkgroupInfo struct { + GTSI string + Name string +} + +// RequestTalkgroups reads all available static talkgroups from the device, see [PEI] 6.11.5.2 +func RequestTalkgroups(ctx context.Context, requester tetra.Requester, kind TalkgroupKind, result []TalkgroupInfo) ([]TalkgroupInfo, error) { + rng, err := RequestTalkgroupRange(ctx, requester, kind) + if err != nil { + return nil, err + } + + prepareRequest := fmt.Sprintf(talkgroupsPrepareRequest, kind, rng.Min, rng.Max) + _, err = requester.Request(ctx, prepareRequest) + if err != nil { + return nil, err + } + + readRequest := fmt.Sprintf(talkgroupsReadRequest, kind) + responses, err := requester.Request(ctx, readRequest) + if err != nil { + return nil, err + } + if len(responses) < 1 { + return nil, fmt.Errorf("no response received") + } + + for _, line := range responses { + info, err := parseTalkgroupInfo(line) + if err != nil { + return nil, err + } + result = append(result, info) + } + return result, nil +} + +var talkgroupInfoLine = regexp.MustCompile(`^(\+CNUM(S|D): )?(\d+),(\d+),(.+)`) + +func parseTalkgroupInfo(line string) (TalkgroupInfo, error) { + parts := talkgroupInfoLine.FindStringSubmatch(line) + if len(parts) != 6 { + return TalkgroupInfo{}, fmt.Errorf("invalid talkgroup info: %s", line) + } + return TalkgroupInfo{ + GTSI: parts[4], + Name: parts[5], + }, nil +} + +var talkgroupRangeResponse = regexp.MustCompile(`^\+CNUM(S|D): \(.*\),\((\d+)-(\d+)\),\((\d+)-(\d+)\)`) + +func RequestTalkgroupRange(ctx context.Context, requester tetra.Requester, kind TalkgroupKind) (TalkgroupRange, error) { + cmd := fmt.Sprintf("AT+CNUM%s=?", kind) + parts, err := requestWithSingleLineResponse(ctx, requester, cmd, talkgroupRangeResponse, 6) + if err != nil { + return TalkgroupRange{}, err + } + + min, err := strconv.Atoi(parts[2]) + if err != nil { + return TalkgroupRange{}, fmt.Errorf("cannot parse range minimum: %v", err) + } + max, err := strconv.Atoi(parts[5]) + if err != nil { + return TalkgroupRange{}, fmt.Errorf("cannot parse range maximum: %v", err) + } + + return TalkgroupRange{Min: min, Max: max}, nil +} + const batteryChargeRequest = "AT+CBC?" var batteryChargeResponse = regexp.MustCompile(`^\+CBC: .*,(\d+)$`) diff --git a/ctrl/command_test.go b/ctrl/command_test.go index 2ce2005..66a9f42 100644 --- a/ctrl/command_test.go +++ b/ctrl/command_test.go @@ -34,3 +34,66 @@ func TestDegreesMinutesToDecimalDegrees(t *testing.T) { }) } } + +func TestTalkgroupRangeResponse(t *testing.T) { + tt := []struct { + response string + kind string + min string + max string + }{ + { + response: "+CNUMS: (0),(1-2000),(1-2000)", + kind: "S", + min: "1", + max: "2000", + }, + { + response: "+CNUMD: (0,1,3),(1-10000),(1-10000)", + kind: "D", + min: "1", + max: "10000", + }, + } + for _, tc := range tt { + t.Run(tc.response, func(t *testing.T) { + parts := talkgroupRangeResponse.FindStringSubmatch(tc.response) + assert.Equal(t, 6, len(parts)) + assert.Equal(t, tc.kind, parts[1]) + assert.Equal(t, tc.min, parts[2]) + assert.Equal(t, tc.max, parts[5]) + }) + } +} + +func TestParseTalkgroupInfo(t *testing.T) { + tt := []struct { + line string + gtsi string + name string + }{ + { + line: "+CNUMD: 1,123456712341234,Test Group", + gtsi: "123456712341234", + name: "Test Group", + }, + { + line: "+CNUMS: 1,123456712341234,Test Group", + gtsi: "123456712341234", + name: "Test Group", + }, + { + line: "1,123456712341234,Test Group", + gtsi: "123456712341234", + name: "Test Group", + }, + } + for _, tc := range tt { + t.Run(tc.line, func(t *testing.T) { + info, err := parseTalkgroupInfo(tc.line) + assert.NoError(t, err) + assert.Equal(t, tc.gtsi, info.GTSI) + assert.Equal(t, tc.name, info.Name) + }) + } +}