You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
544 lines
16 KiB
Go
544 lines
16 KiB
Go
package dmr
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Data Header Packet Format
|
|
const (
|
|
PacketFormatUDT uint8 = iota // 0b0000
|
|
PacketFormatResponse // 0b0001
|
|
PacketFormatUnconfirmedData // 0b0010
|
|
PacketFormatConfirmedData // 0b0011
|
|
_ // 0b0100
|
|
_ // 0b0101
|
|
_ // 0b0110
|
|
_ // 0b0111
|
|
_ // 0b1000
|
|
_ // 0b1001
|
|
_ // 0b1010
|
|
_ // 0b1011
|
|
_ // 0b1100
|
|
PacketFormatShortDataDefined // 0b1101
|
|
PacketFormatShortDataRaw // 0b1110
|
|
PacketFormatProprietaryData // 0b1111
|
|
)
|
|
|
|
// Service Access Point
|
|
const (
|
|
ServiceAccessPointUDT uint8 = iota // 0b0000
|
|
_ // 0b0001
|
|
ServiceAccessPointTCPIPHeaderCompression // 0b0010
|
|
ServiceAccessPointUDPIPHeaderCompression // 0b0011
|
|
ServiceAccessPointIPBasedPacketData // 0b0100
|
|
ServiceAccessPointARP // 0b0101
|
|
_ // 0b0110
|
|
_ // 0b0111
|
|
_ // 0b1000
|
|
ServiceAccessPointProprietaryData // 0b1001
|
|
ServiceAccessPointShortData // 0b1010
|
|
)
|
|
|
|
var ServiceAccessPointName = map[uint8]string{
|
|
ServiceAccessPointUDT: "UDT",
|
|
ServiceAccessPointTCPIPHeaderCompression: "TCP/IP header compression",
|
|
ServiceAccessPointUDPIPHeaderCompression: "UDP/IP header compression",
|
|
ServiceAccessPointIPBasedPacketData: "IP based packet data",
|
|
ServiceAccessPointARP: "ARP",
|
|
ServiceAccessPointProprietaryData: "proprietary data",
|
|
ServiceAccessPointShortData: "short data",
|
|
}
|
|
|
|
// Response Data Header Response Type, encodes class and type
|
|
const (
|
|
_ uint8 = iota // Class 0b00, Type 0b000
|
|
ResponseTypeACK // Class 0b00, Type 0b001
|
|
_ // Class 0b00, Type 0b010
|
|
_ // Class 0b00, Type 0b011
|
|
_ // Class 0b00, Type 0b100
|
|
_ // Class 0b00, Type 0b101
|
|
_ // Class 0b00, Type 0b110
|
|
_ // Class 0b00, Type 0b111
|
|
ResponseTypeIllegalFormat // Class 0b01, Type 0b000
|
|
ResponseTypePacketCRCFailed // Class 0b01, Type 0b001
|
|
ResponseTypeMemoryFull // Class 0b01, Type 0b010
|
|
ResponseTypeRecvFSVNOutOfSeq // Class 0b01, Type 0b011
|
|
ResponseTypeUndeliverable // Class 0b01, Type 0b100
|
|
ResponseTypeRecvPktOutOfSeq // Class 0b01, Type 0b101
|
|
ResponseTypeDisallowed // Class 0b01, Type 0b110
|
|
_ // Class 0b01, Type 0b111
|
|
ResponseTypeSelectiveACK // Class 0b10, Type 0b000
|
|
)
|
|
|
|
var ResponseTypeName = map[uint8]string{
|
|
ResponseTypeACK: "ACK",
|
|
ResponseTypeIllegalFormat: "illegal format",
|
|
ResponseTypePacketCRCFailed: "packet CRC failed",
|
|
ResponseTypeMemoryFull: "memory full",
|
|
ResponseTypeRecvFSVNOutOfSeq: "recv FSN out of sequence",
|
|
ResponseTypeUndeliverable: "undeliverable",
|
|
ResponseTypeRecvPktOutOfSeq: "recv PKT our of sequence",
|
|
ResponseTypeDisallowed: "disallowed",
|
|
ResponseTypeSelectiveACK: "selective ACK",
|
|
}
|
|
|
|
// UDP Response Header UDT Format
|
|
const (
|
|
UDTFormatBinary uint8 = iota
|
|
UDTFormatMSAddress
|
|
UDTFormat4BitBCD
|
|
UDTFormatISO_7BitChars
|
|
UDTFormatISO_8BitChars
|
|
UDTFormatNMEALocation
|
|
UDTFormatIPAddress
|
|
UDTFormat16BitUnicodeChars
|
|
UDTFormatCustomCodeD1
|
|
UDTFormatCustomCodeD2
|
|
)
|
|
|
|
var UDTFormatName = map[uint8]string{
|
|
UDTFormatBinary: "binary",
|
|
UDTFormatMSAddress: "MS address",
|
|
UDTFormat4BitBCD: "4-bit BCD",
|
|
UDTFormatISO_7BitChars: "ISO 7-bit characters",
|
|
UDTFormatISO_8BitChars: "ISO 8-bit characters",
|
|
UDTFormatNMEALocation: "NMEA location",
|
|
UDTFormatIPAddress: "IP address",
|
|
UDTFormat16BitUnicodeChars: "16-bit Unicode characters",
|
|
UDTFormatCustomCodeD1: "custom code D1",
|
|
UDTFormatCustomCodeD2: "custom code D2",
|
|
}
|
|
|
|
// UDT Response Header DD Format
|
|
const (
|
|
DDFormatBinary uint8 = iota
|
|
DDFormatBCD
|
|
DDFormat7BitChar
|
|
DDFormat8BitISO8859_1
|
|
DDFormat8BitISO8859_2
|
|
DDFormat8BitISO8859_3
|
|
DDFormat8BitISO8859_4
|
|
DDFormat8BitISO8859_5
|
|
DDFormat8BitISO8859_6
|
|
DDFormat8BitISO8859_7
|
|
DDFormat8BitISO8859_8
|
|
DDFormat8BitISO8859_9
|
|
DDFormat8BitISO8859_10
|
|
DDFormat8BitISO8859_11
|
|
DDFormat8BitISO8859_13
|
|
DDFormat8BitISO8859_14
|
|
DDFormat8BitISO8859_15
|
|
DDFormat8BitISO8859_16
|
|
DDFormatUTF8
|
|
DDFormatUTF16
|
|
DDFormatUTF16BE
|
|
DDFormatUTF16LE
|
|
DDFormatUTF32
|
|
DDFormatUTF32BE
|
|
DDFormatUTF32LE
|
|
)
|
|
|
|
var DDFormatName = map[uint8]string{
|
|
DDFormatBinary: "binary",
|
|
DDFormatBCD: "BCD",
|
|
DDFormat7BitChar: "7-bit characters",
|
|
DDFormat8BitISO8859_1: "8-bit ISO 8859-1",
|
|
DDFormat8BitISO8859_2: "8-bit ISO 8859-2",
|
|
DDFormat8BitISO8859_3: "8-bit ISO 8859-3",
|
|
DDFormat8BitISO8859_4: "8-bit ISO 8859-4",
|
|
DDFormat8BitISO8859_5: "8-bit ISO 8859-5",
|
|
DDFormat8BitISO8859_6: "8-bit ISO 8859-6",
|
|
DDFormat8BitISO8859_7: "8-bit ISO 8859-7",
|
|
DDFormat8BitISO8859_8: "8-bit ISO 8859-8",
|
|
DDFormat8BitISO8859_9: "8-bit ISO 8859-9",
|
|
DDFormat8BitISO8859_10: "8-bit ISO 8859-10",
|
|
DDFormat8BitISO8859_11: "8-bit ISO 8859-11",
|
|
DDFormat8BitISO8859_13: "8-bit ISO 8859-13",
|
|
DDFormat8BitISO8859_14: "8-bit ISO 8859-14",
|
|
DDFormat8BitISO8859_15: "8-bit ISO 8859-15",
|
|
DDFormat8BitISO8859_16: "8-bit ISO 8859-16",
|
|
DDFormatUTF8: "UTF-8",
|
|
DDFormatUTF16: "UTF-16",
|
|
DDFormatUTF16BE: "UTF-16 big endian",
|
|
DDFormatUTF16LE: "UTF-16 little endian",
|
|
DDFormatUTF32: "UTF-32",
|
|
DDFormatUTF32BE: "UTF-32 big endian",
|
|
DDFormatUTF32LE: "UTF-32 little endian",
|
|
}
|
|
|
|
// http://www.etsi.org/images/files/DMRcodes/dmrs-mfid.xls
|
|
var ManufacturerName = map[uint8]string{
|
|
0x00: "Reserved",
|
|
0x01: "Reserved",
|
|
0x02: "Reserved",
|
|
0x03: "Reserved",
|
|
0x04: "Flyde Micro Ltd.",
|
|
0x05: "PROD-EL SPA",
|
|
0x06: "Trident Datacom DBA Trident Micro Systems",
|
|
0x07: "RADIODATA GmbH",
|
|
0x08: "HYT science tech",
|
|
0x09: "ASELSAN Elektronik Sanayi ve Ticaret A.S.",
|
|
0x0a: "Kirisun Communications Co. Ltd",
|
|
0x0b: "DMR Association Ltd.",
|
|
0x10: "Motorola Ltd.",
|
|
0x13: "EMC S.p.A. (Electronic Marketing Company)",
|
|
0x1c: "EMC S.p.A. (Electronic Marketing Company)",
|
|
0x20: "JVCKENWOOD Corporation",
|
|
0x33: "Radio Activity Srl",
|
|
0x3c: "Radio Activity Srl",
|
|
0x58: "Tait Electronics Ltd",
|
|
0x68: "HYT science tech",
|
|
0x77: "Vertex Standard",
|
|
}
|
|
|
|
type DataHeaderData interface {
|
|
String() string
|
|
Write([]byte) error
|
|
}
|
|
|
|
type DataHeader struct {
|
|
PacketFormat uint8
|
|
DstIsGroup bool
|
|
ResponseRequested bool
|
|
HeaderCompression bool
|
|
ServiceAccessPoint uint8
|
|
DstID uint32
|
|
SrcID uint32
|
|
CRC uint16
|
|
Data DataHeaderData
|
|
}
|
|
|
|
func (h *DataHeader) Bytes() ([]byte, error) {
|
|
var data = make([]byte, 12)
|
|
|
|
data[0] = (h.PacketFormat & B00001111)
|
|
if h.DstIsGroup {
|
|
data[0] |= B10000000
|
|
}
|
|
if h.ResponseRequested {
|
|
data[0] |= B01000000
|
|
}
|
|
if h.HeaderCompression {
|
|
data[0] |= B00100000
|
|
}
|
|
data[1] = (h.ServiceAccessPoint & B00001111)
|
|
data[2] = uint8(h.DstID >> 16)
|
|
data[3] = uint8(h.DstID >> 8)
|
|
data[4] = uint8(h.DstID)
|
|
data[5] = uint8(h.SrcID >> 16)
|
|
data[6] = uint8(h.SrcID >> 8)
|
|
data[7] = uint8(h.SrcID)
|
|
|
|
if h.Data != nil {
|
|
if err := h.Data.Write(data); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
h.CRC = 0
|
|
for i := 0; i < 10; i++ {
|
|
crc16(&h.CRC, data[i])
|
|
}
|
|
crc16end(&h.CRC)
|
|
|
|
// Inverting according to the inversion polynomial.
|
|
h.CRC = ^h.CRC
|
|
// Applying CRC mask, see DMR AI spec. page 143.
|
|
h.CRC ^= 0xcccc
|
|
|
|
data[10] = uint8(h.CRC >> 8)
|
|
data[11] = uint8(h.CRC)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (h DataHeader) String() string {
|
|
var part = []string{"data header"}
|
|
if h.DstIsGroup {
|
|
part = append(part, "group")
|
|
} else {
|
|
part = append(part, "unit")
|
|
}
|
|
part = append(part, fmt.Sprintf("response %t, sap %s (%d), %d->%d",
|
|
h.ResponseRequested, ServiceAccessPointName[h.ServiceAccessPoint], h.ServiceAccessPoint,
|
|
h.SrcID, h.DstID))
|
|
if h.Data != nil {
|
|
part = append(part, h.Data.String())
|
|
}
|
|
return strings.Join(part, ", ")
|
|
}
|
|
|
|
type UDTData struct {
|
|
Format uint8
|
|
PadNibble uint8
|
|
AppendedBlocks uint8
|
|
SupplementaryFlag bool
|
|
Opcode uint8
|
|
}
|
|
|
|
func (d UDTData) String() string {
|
|
return fmt.Sprintf("UDT, format %s (%d), pad nibble %d, appended blocks %d, supplementary %t, opcode %d",
|
|
UDTFormatName[d.Format], d.Format, d.PadNibble, d.AppendedBlocks, d.SupplementaryFlag, d.Opcode)
|
|
}
|
|
|
|
func (d UDTData) Write(data []byte) error {
|
|
data[1] |= (d.Format & B00001111)
|
|
data[8] = (d.AppendedBlocks & B00000011) | (d.PadNibble << 3)
|
|
data[9] = (d.Opcode & B00111111)
|
|
if d.SupplementaryFlag {
|
|
data[9] |= B10000000
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type UnconfirmedData struct {
|
|
PadOctetCount uint8
|
|
FullMessage bool
|
|
BlocksToFollow uint8
|
|
FragmentSequenceNumber uint8
|
|
}
|
|
|
|
func (d UnconfirmedData) String() string {
|
|
return fmt.Sprintf("unconfirmed, pad octet %d, full %t, blocks %d, sequence %d",
|
|
d.PadOctetCount, d.FullMessage, d.BlocksToFollow, d.FragmentSequenceNumber)
|
|
}
|
|
|
|
func (d UnconfirmedData) Write(data []byte) error {
|
|
data[0] |= d.PadOctetCount & B00010000
|
|
data[1] |= d.PadOctetCount & B00001111
|
|
data[8] = d.BlocksToFollow & B01111111
|
|
if d.FullMessage {
|
|
data[8] |= B10000000
|
|
}
|
|
data[9] = d.FragmentSequenceNumber & B00001111
|
|
return nil
|
|
}
|
|
|
|
type ConfirmedData struct {
|
|
PadOctetCount uint8
|
|
FullMessage bool
|
|
BlocksToFollow uint8
|
|
Resync bool
|
|
SendSequenceNumber uint8
|
|
FragmentSequenceNumber uint8
|
|
}
|
|
|
|
func (d ConfirmedData) String() string {
|
|
return fmt.Sprintf("confirmed, pad octet %d, full %t, blocks %d, resync %t, send sequence %d, sequence %d",
|
|
d.PadOctetCount, d.FullMessage, d.BlocksToFollow, d.Resync, d.SendSequenceNumber, d.FragmentSequenceNumber)
|
|
}
|
|
|
|
func (d ConfirmedData) Write(data []byte) error {
|
|
data[0] |= d.PadOctetCount & B00010000
|
|
data[1] |= d.PadOctetCount & B00001111
|
|
data[8] = d.BlocksToFollow & B01111111
|
|
if d.FullMessage {
|
|
data[8] |= B10000000
|
|
}
|
|
data[9] = (d.FragmentSequenceNumber&B00000111)<<0 | (d.SendSequenceNumber&B00000111)<<4
|
|
if d.Resync {
|
|
data[9] |= B10000000
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ResponseData struct {
|
|
BlocksToFollow uint8
|
|
ClassType uint8 // See ResponseType map above
|
|
Status uint8
|
|
}
|
|
|
|
func (d ResponseData) String() string {
|
|
return fmt.Sprintf("response, blocks %d, type %s (%#02b %#03b), status %d",
|
|
d.BlocksToFollow, ResponseTypeName[d.ClassType], (d.ClassType >> 3), (d.ClassType & 0x07), d.Status)
|
|
}
|
|
|
|
func (d ResponseData) Write(data []byte) error {
|
|
data[8] = d.BlocksToFollow & B01111111
|
|
data[9] = d.Status | d.ClassType<<3
|
|
return nil
|
|
}
|
|
|
|
type ProprietaryData struct {
|
|
ManufacturerID uint8
|
|
}
|
|
|
|
func (d ProprietaryData) String() string {
|
|
return fmt.Sprintf("proprietary, manufacturer %s (%d)",
|
|
ManufacturerName[d.ManufacturerID], d.ManufacturerID)
|
|
}
|
|
|
|
func (d ProprietaryData) Write(data []byte) error {
|
|
data[1] = (d.ManufacturerID & B01111111)
|
|
return nil
|
|
}
|
|
|
|
type ShortDataRawData struct {
|
|
AppendedBlocks uint8
|
|
SrcPort uint8
|
|
DstPort uint8
|
|
Resync bool
|
|
FullMessage bool
|
|
BitPadding uint8
|
|
}
|
|
|
|
func (d ShortDataRawData) String() string {
|
|
return fmt.Sprintf("short data raw, blocks %d, src port %d, dst port %d, rsync %t, full %t, padding %d",
|
|
d.AppendedBlocks, d.SrcPort, d.DstPort, d.Resync, d.FullMessage, d.BitPadding)
|
|
}
|
|
|
|
func (d ShortDataRawData) Write(data []byte) error {
|
|
data[0] |= d.AppendedBlocks & B00110000
|
|
data[1] |= d.AppendedBlocks & B00001111
|
|
data[8] = (d.SrcPort&B00000111)<<5 | (d.DstPort&B00000111)<<2
|
|
if d.Resync {
|
|
data[8] |= B00000010
|
|
}
|
|
if d.FullMessage {
|
|
data[8] |= B00000001
|
|
}
|
|
data[9] = d.BitPadding
|
|
return nil
|
|
}
|
|
|
|
type ShortDataDefinedData struct {
|
|
AppendedBlocks uint8
|
|
DDFormat uint8
|
|
Resync bool
|
|
FullMessage bool
|
|
BitPadding uint8
|
|
}
|
|
|
|
func (d ShortDataDefinedData) String() string {
|
|
return fmt.Sprintf("short data defined, blocks %d, dd format %s (%d), resync %t, full %t, padding %d",
|
|
d.AppendedBlocks, DDFormatName[d.DDFormat], d.DDFormat, d.Resync, d.FullMessage, d.BitPadding)
|
|
}
|
|
|
|
func (d ShortDataDefinedData) Write(data []byte) error {
|
|
data[0] |= d.AppendedBlocks & B00110000
|
|
data[1] |= d.AppendedBlocks & B00001111
|
|
data[8] = (d.DDFormat & B00111111) << 2
|
|
if d.Resync {
|
|
data[8] |= B00000010
|
|
}
|
|
if d.FullMessage {
|
|
data[8] |= B00000001
|
|
}
|
|
data[9] = d.BitPadding
|
|
return nil
|
|
}
|
|
|
|
var _ (DataHeaderData) = (*ShortDataDefinedData)(nil)
|
|
|
|
func ParseDataHeader(data []byte, proprietary bool) (*DataHeader, error) {
|
|
if len(data) != 12 {
|
|
return nil, fmt.Errorf("data must be 12 bytes, got %d", len(data))
|
|
}
|
|
var (
|
|
ccrc = (uint16(data[10]) << 8) | uint16(data[11])
|
|
hcrc = dataHeaderCRC(data)
|
|
)
|
|
if ccrc != hcrc {
|
|
return nil, fmt.Errorf("data CRC mismatch, %#04x != %#04x", ccrc, hcrc)
|
|
}
|
|
|
|
h := &DataHeader{
|
|
DstIsGroup: (data[0] & B10000000) > 0,
|
|
ResponseRequested: (data[0] & B01000000) > 0,
|
|
HeaderCompression: (data[0] & B00100000) > 0,
|
|
PacketFormat: (data[0] & B00001111),
|
|
ServiceAccessPoint: (data[1] & B11110000) >> 4,
|
|
DstID: uint32(data[2])<<16 | uint32(data[3])<<8 | uint32(data[4]),
|
|
SrcID: uint32(data[5])<<16 | uint32(data[6])<<8 | uint32(data[7]),
|
|
CRC: ccrc,
|
|
}
|
|
|
|
if proprietary {
|
|
h.Data = ProprietaryData{
|
|
ManufacturerID: data[1] & B01111111,
|
|
}
|
|
|
|
} else {
|
|
switch h.PacketFormat {
|
|
case PacketFormatUDT:
|
|
h.Data = &UDTData{
|
|
Format: (data[1] & B00001111),
|
|
PadNibble: (data[8] & B11111000) >> 3,
|
|
AppendedBlocks: (data[8] & B00000011),
|
|
SupplementaryFlag: (data[9] & B10000000) > 0,
|
|
Opcode: (data[9] & B00111111),
|
|
}
|
|
break
|
|
|
|
case PacketFormatResponse:
|
|
h.Data = &ResponseData{
|
|
BlocksToFollow: (data[8] & B01111111),
|
|
ClassType: (data[9] & B11111000) >> 3,
|
|
Status: (data[9] & B00000111),
|
|
}
|
|
break
|
|
|
|
case PacketFormatUnconfirmedData:
|
|
h.Data = &UnconfirmedData{
|
|
PadOctetCount: (data[0] & B00010000) | (data[1] & B00001111),
|
|
FullMessage: (data[8] & B10000000) > 0,
|
|
BlocksToFollow: (data[8] & B01111111),
|
|
FragmentSequenceNumber: (data[9] & B00001111),
|
|
}
|
|
break
|
|
|
|
case PacketFormatConfirmedData:
|
|
h.Data = &ConfirmedData{
|
|
PadOctetCount: (data[0] & B00010000) | (data[1] & B00001111),
|
|
FullMessage: (data[8] & B10000000) > 0,
|
|
BlocksToFollow: (data[8] & B01111111),
|
|
Resync: (data[9] & B10000000) > 0,
|
|
SendSequenceNumber: (data[9] & B01110000) >> 4,
|
|
FragmentSequenceNumber: (data[9] & B00001111),
|
|
}
|
|
break
|
|
|
|
case PacketFormatShortDataRaw:
|
|
h.Data = &ShortDataRawData{
|
|
AppendedBlocks: (data[0] & B00110000) | (data[1] & B00001111),
|
|
SrcPort: (data[8] & B11100000) >> 5,
|
|
DstPort: (data[8] & B00011100) >> 2,
|
|
Resync: (data[8] & B00000010) > 0,
|
|
FullMessage: (data[8] & B00000001) > 0,
|
|
BitPadding: (data[9]),
|
|
}
|
|
break
|
|
|
|
case PacketFormatShortDataDefined:
|
|
h.Data = &ShortDataDefinedData{
|
|
AppendedBlocks: (data[0] & B00110000) | (data[1] & B00001111),
|
|
DDFormat: (data[8] & B11111100) >> 2,
|
|
Resync: (data[8] & B00000010) > 0,
|
|
FullMessage: (data[8] & B00000001) > 0,
|
|
BitPadding: (data[9]),
|
|
}
|
|
break
|
|
|
|
default:
|
|
return nil, fmt.Errorf("dmr: unknown data data packet format %#02x (%d)", h.PacketFormat, h.PacketFormat)
|
|
}
|
|
}
|
|
|
|
return h, nil
|
|
}
|
|
|
|
func dataHeaderCRC(data []byte) uint16 {
|
|
var crc uint16
|
|
if len(data) < 10 {
|
|
return crc
|
|
}
|
|
|
|
for i := 0; i < 10; i++ {
|
|
crc16(&crc, data[i])
|
|
}
|
|
crc16end(&crc)
|
|
|
|
return (^crc) ^ 0xcccc
|
|
}
|