refactor link control and add talker alias support
parent
9942e7c892
commit
954edcd147
@ -0,0 +1,70 @@
|
|||||||
|
package lc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
dmr "github.com/pd0mz/go-dmr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Data Format
|
||||||
|
// ref: ETSI TS 102 361-2 7.2.18
|
||||||
|
const (
|
||||||
|
ErrorLT2m uint8 = iota
|
||||||
|
ErrorLT20m
|
||||||
|
ErrorLT200m
|
||||||
|
ErrorLT2km
|
||||||
|
ErrorLT20km
|
||||||
|
ErrorLE200km
|
||||||
|
ErrorGT200km
|
||||||
|
ErrorUnknown
|
||||||
|
)
|
||||||
|
|
||||||
|
// PositionErrorName is a map of position error to string.
|
||||||
|
var PositionErrorName = map[uint8]string{
|
||||||
|
ErrorLT2m: "< 2m",
|
||||||
|
ErrorLT20m: "< 20m",
|
||||||
|
ErrorLT200m: "< 200m",
|
||||||
|
ErrorLT2km: "< 2km",
|
||||||
|
ErrorLT20km: "< 20km",
|
||||||
|
ErrorLE200km: "<= 200km",
|
||||||
|
ErrorGT200km: "> 200km",
|
||||||
|
ErrorUnknown: "unknown",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GpsInfoPDU Conforms to ETSI TS 102 361-2 7.1.1.3
|
||||||
|
type GpsInfoPDU struct {
|
||||||
|
PositionError uint8
|
||||||
|
Longitude uint32
|
||||||
|
Latitude uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseGpsInfoPDU parse gps info pdu
|
||||||
|
func ParseGpsInfoPDU(data []byte) (*GpsInfoPDU, error) {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return nil, fmt.Errorf("dmr/lc/talkeralias: expected 7 bytes, got %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GpsInfoPDU{
|
||||||
|
PositionError: (data[0] & dmr.B00001110) >> 1,
|
||||||
|
Longitude: uint32(data[0]&dmr.B00000001)<<24 | uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3]),
|
||||||
|
Latitude: uint32(data[4])<<16 | uint32(data[5])<<8 | uint32(data[6]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns GpsInfoPDU as bytes
|
||||||
|
func (g *GpsInfoPDU) Bytes() []byte {
|
||||||
|
return []byte{
|
||||||
|
uint8((g.PositionError&dmr.B00000111)<<1) | uint8((g.Longitude>>24)&dmr.B00000001),
|
||||||
|
uint8(g.Longitude >> 16),
|
||||||
|
uint8(g.Longitude >> 8),
|
||||||
|
uint8(g.Longitude),
|
||||||
|
uint8(g.Latitude >> 16),
|
||||||
|
uint8(g.Latitude >> 8),
|
||||||
|
uint8(g.Latitude),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GpsInfoPDU) String() string {
|
||||||
|
return fmt.Sprintf("GpsInfo: [ error: %s lon: %d lat: %d ]",
|
||||||
|
PositionErrorName[g.PositionError], g.Longitude, g.Latitude)
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
package lc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pd0mz/go-dmr"
|
||||||
|
"github.com/pd0mz/go-dmr/fec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Full Link Control Opcode
|
||||||
|
const (
|
||||||
|
GroupVoiceChannelUser uint8 = 0x00 // B000000
|
||||||
|
UnitToUnitVoiceChannelUser uint8 = 0x03 // B000011
|
||||||
|
TalkerAliasHeader uint8 = 0x04 // B000100
|
||||||
|
TalkerAliasBlk1 uint8 = 0x05 // B000101
|
||||||
|
TalkerAliasBlk2 uint8 = 0x06 // B000110
|
||||||
|
TalkerAliasBlk3 uint8 = 0x07 // B000111
|
||||||
|
GpsInfo uint8 = 0x08 // B001000
|
||||||
|
)
|
||||||
|
|
||||||
|
// LC is a Link Control message.
|
||||||
|
type LC struct {
|
||||||
|
CallType uint8
|
||||||
|
Opcode uint8
|
||||||
|
FeatureSetID uint8
|
||||||
|
VoiceChannelUser *VoiceChannelUserPDU
|
||||||
|
GpsInfo *GpsInfoPDU
|
||||||
|
TalkerAliasHeader *TalkerAliasHeaderPDU
|
||||||
|
TalkerAliasBlocks [3]*TalkerAliasBlockPDU
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes packs the Link Control message to bytes.
|
||||||
|
func (lc *LC) Bytes() []byte {
|
||||||
|
var (
|
||||||
|
lcHeader = []byte{
|
||||||
|
lc.Opcode,
|
||||||
|
lc.FeatureSetID,
|
||||||
|
}
|
||||||
|
innerPdu []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
switch lc.Opcode {
|
||||||
|
case GroupVoiceChannelUser:
|
||||||
|
fallthrough
|
||||||
|
case UnitToUnitVoiceChannelUser:
|
||||||
|
innerPdu = lc.VoiceChannelUser.Bytes()
|
||||||
|
case TalkerAliasHeader:
|
||||||
|
innerPdu = lc.TalkerAliasHeader.Bytes()
|
||||||
|
case TalkerAliasBlk1:
|
||||||
|
innerPdu = lc.TalkerAliasBlocks[0].Bytes()
|
||||||
|
case TalkerAliasBlk2:
|
||||||
|
innerPdu = lc.TalkerAliasBlocks[1].Bytes()
|
||||||
|
case TalkerAliasBlk3:
|
||||||
|
innerPdu = lc.TalkerAliasBlocks[2].Bytes()
|
||||||
|
case GpsInfo:
|
||||||
|
innerPdu = lc.GpsInfo.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(lcHeader, innerPdu...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LC) String() string {
|
||||||
|
var (
|
||||||
|
header = fmt.Sprintf("opcode %d, call type %s, feature set id %d",
|
||||||
|
lc.Opcode, dmr.CallTypeName[lc.CallType], lc.FeatureSetID)
|
||||||
|
r string
|
||||||
|
)
|
||||||
|
|
||||||
|
switch lc.Opcode {
|
||||||
|
case GroupVoiceChannelUser:
|
||||||
|
fallthrough
|
||||||
|
case UnitToUnitVoiceChannelUser:
|
||||||
|
r = fmt.Sprintf("%s %v", header, lc.VoiceChannelUser)
|
||||||
|
case GpsInfo:
|
||||||
|
r = fmt.Sprintf("%s %v", header, lc.GpsInfo)
|
||||||
|
case TalkerAliasHeader:
|
||||||
|
r = fmt.Sprintf("%s %v", header, lc.TalkerAliasHeader)
|
||||||
|
case TalkerAliasBlk1:
|
||||||
|
r = fmt.Sprintf("%s %v", header, lc.TalkerAliasBlocks[0])
|
||||||
|
case TalkerAliasBlk2:
|
||||||
|
r = fmt.Sprintf("%s %v", header, lc.TalkerAliasBlocks[1])
|
||||||
|
case TalkerAliasBlk3:
|
||||||
|
r = fmt.Sprintf("%s %v", header, lc.TalkerAliasBlocks[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLC parses a packed Link Control message.
|
||||||
|
func ParseLC(data []byte) (*LC, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, errors.New("dmr/lc: data can't be nil")
|
||||||
|
}
|
||||||
|
if len(data) != 9 {
|
||||||
|
return nil, fmt.Errorf("dmr/lc: expected 9 LC bytes, got %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[0]&dmr.B10000000 > 0 {
|
||||||
|
return nil, errors.New("dmr/lc: protect flag is not 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
fclo = data[0] & dmr.B00111111
|
||||||
|
lc = &LC{
|
||||||
|
Opcode: fclo,
|
||||||
|
FeatureSetID: data[1],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
switch fclo {
|
||||||
|
case GroupVoiceChannelUser:
|
||||||
|
var pdu *VoiceChannelUserPDU
|
||||||
|
lc.CallType = dmr.CallTypeGroup
|
||||||
|
pdu, err = ParseVoiceChannelUserPDU(data[2:9])
|
||||||
|
lc.VoiceChannelUser = pdu
|
||||||
|
case UnitToUnitVoiceChannelUser:
|
||||||
|
var pdu *VoiceChannelUserPDU
|
||||||
|
lc.CallType = dmr.CallTypePrivate
|
||||||
|
pdu, err = ParseVoiceChannelUserPDU(data[2:9])
|
||||||
|
lc.VoiceChannelUser = pdu
|
||||||
|
case TalkerAliasHeader:
|
||||||
|
var pdu *TalkerAliasHeaderPDU
|
||||||
|
pdu, err = ParseTalkerAliasHeaderPDU(data[2:9])
|
||||||
|
lc.TalkerAliasHeader = pdu
|
||||||
|
case TalkerAliasBlk1:
|
||||||
|
var pdu *TalkerAliasBlockPDU
|
||||||
|
pdu, err = ParseTalkerAliasBlockPDU(data[2:9])
|
||||||
|
lc.TalkerAliasBlocks[0] = pdu
|
||||||
|
case TalkerAliasBlk2:
|
||||||
|
var pdu *TalkerAliasBlockPDU
|
||||||
|
pdu, err = ParseTalkerAliasBlockPDU(data[2:9])
|
||||||
|
lc.TalkerAliasBlocks[1] = pdu
|
||||||
|
case TalkerAliasBlk3:
|
||||||
|
var pdu *TalkerAliasBlockPDU
|
||||||
|
pdu, err = ParseTalkerAliasBlockPDU(data[2:9])
|
||||||
|
lc.TalkerAliasBlocks[2] = pdu
|
||||||
|
case GpsInfo:
|
||||||
|
var pdu *GpsInfoPDU
|
||||||
|
pdu, err = ParseGpsInfoPDU(data[2:9])
|
||||||
|
lc.GpsInfo = pdu
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("dmr/lc: unknown FCLO %06b (%d)", fclo, fclo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing link control header pdu: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFullLC parses a packed Link Control message and checks/corrects the Reed-Solomon check data.
|
||||||
|
func ParseFullLC(data []byte) (*LC, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, errors.New("dmr/full lc: data can't be nil")
|
||||||
|
}
|
||||||
|
if len(data) != 12 {
|
||||||
|
return nil, fmt.Errorf("dmr/full lc: expected 12 bytes, got %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
syndrome := &fec.RS_12_9_Poly{}
|
||||||
|
if err := fec.RS_12_9_CalcSyndrome(data, syndrome); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !fec.RS_12_9_CheckSyndrome(syndrome) {
|
||||||
|
if _, err := fec.RS_12_9_Correct(data, syndrome); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseLC(data[:9])
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package serviceoptions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dmr "github.com/pd0mz/go-dmr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Priority Levels
|
||||||
|
const (
|
||||||
|
NoPriority uint8 = iota
|
||||||
|
Priority1
|
||||||
|
Priority2
|
||||||
|
Priority3
|
||||||
|
)
|
||||||
|
|
||||||
|
// PriorityName is a map of priority level to string.
|
||||||
|
var PriorityName = map[uint8]string{
|
||||||
|
NoPriority: "no priority",
|
||||||
|
Priority1: "priority 1",
|
||||||
|
Priority2: "priority 2",
|
||||||
|
Priority3: "priority 3",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceOptions Conforms to ETSI TS 102-361-2 7.2.1
|
||||||
|
type ServiceOptions struct {
|
||||||
|
// Emergency service
|
||||||
|
Emergency bool
|
||||||
|
// Not defined in document
|
||||||
|
Privacy bool
|
||||||
|
// Broadcast service (only defined in group calls)
|
||||||
|
Broadcast bool
|
||||||
|
// Open Voice Call Mode
|
||||||
|
OpenVoiceCallMode bool
|
||||||
|
// Priority 3 (0b11) is the highest priority
|
||||||
|
Priority uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte packs the service options to a single byte.
|
||||||
|
func (so *ServiceOptions) Byte() byte {
|
||||||
|
var b byte
|
||||||
|
if so.Emergency {
|
||||||
|
b |= dmr.B00000001
|
||||||
|
}
|
||||||
|
if so.Privacy {
|
||||||
|
b |= dmr.B00000010
|
||||||
|
}
|
||||||
|
if so.Broadcast {
|
||||||
|
b |= dmr.B00010000
|
||||||
|
}
|
||||||
|
if so.OpenVoiceCallMode {
|
||||||
|
b |= dmr.B00100000
|
||||||
|
}
|
||||||
|
b |= (so.Priority << 6)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// String representatation of the service options.
|
||||||
|
func (so *ServiceOptions) String() string {
|
||||||
|
var part = []string{}
|
||||||
|
if so.Emergency {
|
||||||
|
part = append(part, "emergency")
|
||||||
|
}
|
||||||
|
if so.Privacy {
|
||||||
|
part = append(part, "privacy")
|
||||||
|
}
|
||||||
|
if so.Broadcast {
|
||||||
|
part = append(part, "broadcast")
|
||||||
|
}
|
||||||
|
if so.OpenVoiceCallMode {
|
||||||
|
part = append(part, "Open Voice Call Mode")
|
||||||
|
}
|
||||||
|
part = append(part, fmt.Sprintf("%s (%d)", PriorityName[so.Priority], so.Priority))
|
||||||
|
return strings.Join(part, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseServiceOptions parses the service options byte.
|
||||||
|
func ParseServiceOptions(data byte) ServiceOptions {
|
||||||
|
return ServiceOptions{
|
||||||
|
Emergency: (data & dmr.B00000001) > 0,
|
||||||
|
Privacy: (data & dmr.B00000010) > 0,
|
||||||
|
Broadcast: (data & dmr.B00010000) > 0,
|
||||||
|
OpenVoiceCallMode: (data & dmr.B00100000) > 0,
|
||||||
|
Priority: (data & dmr.B11000000) >> 6,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package lc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
dmr "github.com/pd0mz/go-dmr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Data Format
|
||||||
|
// ref: ETSI TS 102 361-2 7.2.18
|
||||||
|
const (
|
||||||
|
Format7Bit uint8 = iota
|
||||||
|
FormatISO8Bit
|
||||||
|
FormatUTF8
|
||||||
|
FormatUTF16BE
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataFormatName is a map of data format to string.
|
||||||
|
var DataFormatName = map[uint8]string{
|
||||||
|
Format7Bit: "7 bit",
|
||||||
|
FormatISO8Bit: "ISO 8 bit",
|
||||||
|
FormatUTF8: "unicode utf-8",
|
||||||
|
FormatUTF16BE: "unicode utf-16be",
|
||||||
|
}
|
||||||
|
|
||||||
|
// TalkerAliasHeaderPDU Conforms to ETSI TS 102 361-2 7.1.1.4
|
||||||
|
type TalkerAliasHeaderPDU struct {
|
||||||
|
DataFormat uint8
|
||||||
|
Length uint8
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// TalkerAliasBlockPDU Conforms to ETSI TS 102 361-2 7.1.1.5
|
||||||
|
type TalkerAliasBlockPDU struct {
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTalkerAliasHeaderPDU parses TalkerAliasHeader PDU from bytes
|
||||||
|
func ParseTalkerAliasHeaderPDU(data []byte) (*TalkerAliasHeaderPDU, error) {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return nil, fmt.Errorf("dmr/lc/talkeralias: expected 7 bytes, got %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO parse bit 49
|
||||||
|
|
||||||
|
return &TalkerAliasHeaderPDU{
|
||||||
|
DataFormat: (data[0] & dmr.B11000000) >> 6,
|
||||||
|
Length: (data[0] & dmr.B00111110) >> 1,
|
||||||
|
Data: data[1:6],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns object as bytes
|
||||||
|
func (t *TalkerAliasHeaderPDU) Bytes() []byte {
|
||||||
|
return []byte{
|
||||||
|
((t.DataFormat << 6) & dmr.B11000000) | ((t.Length << 1) & dmr.B00111110), // TODO bit 49
|
||||||
|
t.Data[0],
|
||||||
|
t.Data[1],
|
||||||
|
t.Data[2],
|
||||||
|
t.Data[3],
|
||||||
|
t.Data[4],
|
||||||
|
t.Data[5],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TalkerAliasHeaderPDU) String() string {
|
||||||
|
return fmt.Sprintf("TalkerAliasHeader: [ format: %s, length: %d, data: \"%s\" ]",
|
||||||
|
DataFormatName[t.DataFormat], t.Length, string(t.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTalkerAliasBlockPDU parse talker alias block pdu
|
||||||
|
func ParseTalkerAliasBlockPDU(data []byte) (*TalkerAliasBlockPDU, error) {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return nil, fmt.Errorf("dmr/lc/talkeralias: expected 7 bytes, got %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TalkerAliasBlockPDU{
|
||||||
|
Data: data[0:6],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns object as bytes
|
||||||
|
func (t *TalkerAliasBlockPDU) Bytes() []byte {
|
||||||
|
return t.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TalkerAliasBlockPDU) String() string {
|
||||||
|
return fmt.Sprintf("TalkerAliasBlock: [ data: \"%s\" ]", string(t.Data))
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package lc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pd0mz/go-dmr/lc/serviceoptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VoiceChannelUserPDU Conforms to ETSI TS 102-361-2 7.1.1.(1 and 2)
|
||||||
|
type VoiceChannelUserPDU struct {
|
||||||
|
ServiceOptions serviceoptions.ServiceOptions
|
||||||
|
DstID uint32
|
||||||
|
SrcID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseVoiceChannelUserPDU Parses either Group Voice Channel User or
|
||||||
|
// Unit to Unit Channel User PDUs
|
||||||
|
func ParseVoiceChannelUserPDU(data []byte) (*VoiceChannelUserPDU, error) {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return nil, fmt.Errorf("dmr/lc/voicechanneluser: expected 7 bytes, got %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &VoiceChannelUserPDU{
|
||||||
|
ServiceOptions: serviceoptions.ParseServiceOptions(data[0]),
|
||||||
|
DstID: uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3]),
|
||||||
|
SrcID: uint32(data[4])<<16 | uint32(data[5])<<8 | uint32(data[6]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes packs the Voice Channel User PDU message to bytes.
|
||||||
|
func (v *VoiceChannelUserPDU) Bytes() []byte {
|
||||||
|
return []byte{
|
||||||
|
v.ServiceOptions.Byte(),
|
||||||
|
uint8(v.DstID >> 16),
|
||||||
|
uint8(v.DstID >> 8),
|
||||||
|
uint8(v.DstID),
|
||||||
|
uint8(v.SrcID >> 16),
|
||||||
|
uint8(v.SrcID >> 8),
|
||||||
|
uint8(v.SrcID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoiceChannelUserPDU) String() string {
|
||||||
|
return fmt.Sprintf("VoiceChannelUser: [ %d->%d, service options %s ]",
|
||||||
|
v.SrcID, v.DstID, v.ServiceOptions.String())
|
||||||
|
}
|
Loading…
Reference in New Issue