diff --git a/dmr/burst.go b/dmr/burst.go new file mode 100644 index 0000000..b28ac02 --- /dev/null +++ b/dmr/burst.go @@ -0,0 +1,22 @@ +// Package dmr contains generic message structures for the Digital Mobile Radio standard +package dmr + +import ( + "fmt" + + "github.com/pd0mz/go-dmr/bit" +) + +// Burst contains data from a single burst, see 4.2.2 Burst and frame structure +type Burst struct { + bits bit.Bits +} + +func NewBurst(raw []byte) (*Burst, error) { + if len(raw)*8 != PayloadBits { + return nil, fmt.Errorf("dmr: expected %d bits, got %d", PayloadBits, len(raw)*8) + } + b := &Burst{} + b.bits = bit.NewBits(raw) + return b, nil +} diff --git a/dmr/dataheader.go b/dmr/dataheader.go new file mode 100644 index 0000000..a518f6c --- /dev/null +++ b/dmr/dataheader.go @@ -0,0 +1,362 @@ +package dmr + +import ( + "fmt" + + "github.com/pd0mz/go-dmr/bit" +) + +// 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 +) + +// Response Data Header Response Type +const ( + ResponseTypeACK uint8 = iota + ResponseTypeIllegalFormat + ResponseTypePacketCRCFailed + ResponseTypeMemoryFull + ResponseTypeRecvFSVNOutOfSeq + ResponseTypeUndeliverable + ResponseTypeRecvPktOutOfSeq + ResponseTypeDisallowed + ResponseTypeSelectiveACK +) + +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", +} + +type DataHeader interface { + CommonHeader() DataHeaderCommon +} + +type DataHeaderCommon struct { + PacketFormat uint8 + DstIsGroup bool + ResponseRequested bool + ServiceAccessPoint uint8 + DstID uint32 + SrcID uint32 + CRC uint16 +} + +func (h *DataHeaderCommon) Parse(header []byte) error { + h.PacketFormat = header[0] & 0xf + h.DstIsGroup = (header[0] & 0x80) > 0 + h.ResponseRequested = (header[0] & 0x40) > 0 + h.ServiceAccessPoint = (header[1] & 0xf0) >> 4 + h.DstID = uint32(header[2])<<16 | uint32(header[3])<<8 | uint32(header[4]) + h.SrcID = uint32(header[5])<<16 | uint32(header[6])<<8 | uint32(header[7]) + return nil +} + +type UDTDataHeader struct { + Common DataHeaderCommon + Format uint8 + PadNibble uint8 + AppendedBlocks uint8 + SupplementaryFlag bool + OPCode uint8 +} + +func (h UDTDataHeader) CommonHeader() DataHeaderCommon { return h.Common } + +type UnconfirmedDataHeader struct { + Common DataHeaderCommon + PadOctetCount uint8 + FullMessage bool + BlocksToFollow uint8 + FragmentSequenceNumber uint8 +} + +func (h UnconfirmedDataHeader) CommonHeader() DataHeaderCommon { return h.Common } + +type ConfirmedDataHeader struct { + Common DataHeaderCommon + PadOctetCount uint8 + FullMessage bool + BlocksToFollow uint8 + Resync bool + SendSequenceNumber uint8 + FragmentSequenceNumber uint8 +} + +func (h ConfirmedDataHeader) CommonHeader() DataHeaderCommon { return h.Common } + +type ResponseDataHeader struct { + Common DataHeaderCommon + BlocksToFollow uint8 + Class uint8 + Type uint8 + Status uint8 +} + +func (h ResponseDataHeader) CommonHeader() DataHeaderCommon { return h.Common } + +type ProprietaryDataHeader struct { + Common DataHeaderCommon + ManufacturerID uint8 +} + +func (h ProprietaryDataHeader) CommonHeader() DataHeaderCommon { return h.Common } + +type ShortDataRawDataHeader struct { + Common DataHeaderCommon + AppendedBlocks uint8 + SrcPort uint8 + DstPort uint8 + Resync bool + FullMessage bool + BitPadding uint8 +} + +func (h ShortDataRawDataHeader) CommonHeader() DataHeaderCommon { return h.Common } + +type ShortDataDefinedDataHeader struct { + Common DataHeaderCommon + AppendedBlocks uint8 + DDFormat uint8 + Resync bool + FullMessage bool + BitPadding uint8 +} + +func (h ShortDataDefinedDataHeader) CommonHeader() DataHeaderCommon { return h.Common } + +func ParseDataHeader(header []byte, proprietary bool) (DataHeader, error) { + if len(header) != 12 { + return nil, fmt.Errorf("header must be 12 bytes, got %d", len(header)) + } + var ( + ccrc = (uint16(header[10]) << 8) | uint16(header[11]) + hcrc = dataHeaderCRC(header) + ) + if ccrc != hcrc { + return nil, fmt.Errorf("header CRC mismatch, %#04x != %#04x", ccrc, hcrc) + } + + if proprietary { + return ProprietaryDataHeader{ + Common: DataHeaderCommon{ + ServiceAccessPoint: (header[0] & bit.B11110000) >> 4, + PacketFormat: (header[0] & bit.B00001111), + CRC: ccrc, + }, + ManufacturerID: header[1], + }, nil + } + + common := DataHeaderCommon{ + CRC: ccrc, + } + if err := common.Parse(header); err != nil { + return nil, err + } + + switch common.PacketFormat { + case PacketFormatUDT: + return UDTDataHeader{ + Common: common, + Format: (header[1] & bit.B00001111), + PadNibble: (header[8] & bit.B11111000) >> 3, + AppendedBlocks: (header[8] & bit.B00000011), + SupplementaryFlag: (header[9] & bit.B10000000) > 0, + OPCode: (header[9] & bit.B00111111), + }, nil + case PacketFormatResponse: + return ResponseDataHeader{ + Common: common, + BlocksToFollow: (header[8] & bit.B01111111), + Class: (header[9] & bit.B11000000) >> 6, + Type: (header[9] & bit.B00111000) >> 3, + Status: (header[9] & bit.B00000111), + }, nil + case PacketFormatUnconfirmedData: + return UnconfirmedDataHeader{ + Common: common, + PadOctetCount: (header[0] & bit.B00010000) | (header[1] & bit.B00001111), + FullMessage: (header[8] & bit.B10000000) > 0, + BlocksToFollow: (header[8] & bit.B01111111), + FragmentSequenceNumber: (header[9] & bit.B00001111), + }, nil + case PacketFormatConfirmedData: + return ConfirmedDataHeader{ + Common: common, + PadOctetCount: (header[0] & bit.B00010000) | (header[1] & bit.B00001111), + FullMessage: (header[8] & bit.B10000000) > 0, + BlocksToFollow: (header[8] & bit.B01111111), + Resync: (header[9] & bit.B10000000) > 0, + SendSequenceNumber: (header[9] & bit.B01110000) >> 4, + FragmentSequenceNumber: (header[9] & bit.B00001111), + }, nil + case PacketFormatShortDataRaw: + return ShortDataRawDataHeader{ + Common: common, + AppendedBlocks: (header[0] & bit.B00110000) | (header[1] & bit.B00001111), + SrcPort: (header[8] & bit.B11100000) >> 5, + DstPort: (header[8] & bit.B00011100) >> 2, + Resync: (header[8] & bit.B00000010) > 0, + FullMessage: (header[8] & bit.B00000001) > 0, + BitPadding: (header[9]), + }, nil + case PacketFormatShortDataDefined: + return ShortDataDefinedDataHeader{ + Common: common, + AppendedBlocks: (header[0] & bit.B00110000) | (header[1] & bit.B00001111), + DDFormat: (header[8] & bit.B11111100) >> 2, + Resync: (header[8] & bit.B00000010) > 0, + FullMessage: (header[8] & bit.B00000001) > 0, + BitPadding: (header[9]), + }, nil + default: + return nil, fmt.Errorf("dmr: unknown data header packet format %#02x (%d)", common.PacketFormat, common.PacketFormat) + } +} + +func dataHeaderCRC(header []byte) uint16 { + var crc uint16 + if len(header) < 10 { + return crc + } + + for i := 0; i < 10; i++ { + crc16(&crc, header[i]) + } + crc16end(&crc) + + return (^crc) ^ 0xcccc +} + +func crc16(crc *uint16, b byte) { + var v = uint8(0x80) + for i := 0; i < 8; i++ { + xor := ((*crc) & 0x8000) > 0 + (*crc) <<= 1 + if b&v > 0 { + (*crc)++ + } + if xor { + (*crc) ^= 0x1021 + } + v >>= 1 + } +} + +func crc16end(crc *uint16) { + for i := 0; i < 16; i++ { + xor := ((*crc) & 0x8000) > 0 + (*crc) <<= 1 + if xor { + (*crc) ^= 0x1021 + } + } +} diff --git a/dmr/dmr.go b/dmr/dmr.go new file mode 100644 index 0000000..64ceee8 --- /dev/null +++ b/dmr/dmr.go @@ -0,0 +1,17 @@ +package dmr + +const ( + PayloadBits = 98 + 10 + 48 + 10 + 98 + PayloadSize = 33 + InfoHalfBits = 98 + InfoBits = 2 * InfoHalfBits + SlotTypeHalfBits = 10 + SlotTypeBits = 2 * SlotTypeHalfBits + SignalBits = 48 + SyncBits = SignalBits + VoiceHalfBits = 108 + VoiceBits = 2 * VoiceHalfBits + EMBHalfBits = 8 + EMBBits = 2 * EMBHalfBits + EMBSignallingLCFragmentBits = 32 +) diff --git a/dmr/emb.go b/dmr/emb.go new file mode 100644 index 0000000..282858c --- /dev/null +++ b/dmr/emb.go @@ -0,0 +1,49 @@ +package dmr + +import ( + "errors" + "fmt" + + "github.com/pd0mz/go-dmr/bit" + "github.com/pd0mz/go-dmr/crc/quadres_16_7" +) + +// EMB LCSS fragments +const ( + SingleFragment uint8 = iota + FirstFragment + LastFragment + Continuation +) + +func ExtractEMBBitsFromSyncBits(sync bit.Bits) bit.Bits { + var b = make(bit.Bits, EMBBits) + var o = EMBHalfBits + EMBSignallingLCFragmentBits + copy(b[:EMBHalfBits], sync[:EMBHalfBits]) + copy(b[EMBHalfBits:], sync[o:o+EMBHalfBits]) + return b +} + +type EMB struct { + ColorCode uint8 + LCSS uint8 +} + +func ParseEMB(bits bit.Bits) (*EMB, error) { + if len(bits) != EMBBits { + return nil, fmt.Errorf("dmr/emb: expected %d bits, got %d", EMBBits, len(bits)) + } + + if !quadres_16_7.Check(bits) { + return nil, errors.New("dmr/emb: checksum error") + } + + if bits[4] != 0 { + return nil, errors.New("dmr/emb: pi is not 0") + } + + return &EMB{ + ColorCode: uint8(bits[0])<<3 | uint8(bits[1])<<2 | uint8(bits[2])<<1 | uint8(bits[3]), + LCSS: uint8(bits[5])<<1 | uint8(bits[6]), + }, nil +} diff --git a/dmr/info.go b/dmr/info.go new file mode 100644 index 0000000..a0d6470 --- /dev/null +++ b/dmr/info.go @@ -0,0 +1,10 @@ +package dmr + +import "github.com/pd0mz/go-dmr/bit" + +func ExtractInfoBits(payload bit.Bits) bit.Bits { + var b = make(bit.Bits, InfoBits) + copy(b[:InfoHalfBits], payload[:InfoHalfBits]) + copy(b[InfoHalfBits:], payload[InfoHalfBits+SignalBits+SlotTypeBits:]) + return b +} diff --git a/dmr/message.go b/dmr/message.go deleted file mode 100644 index cec98ae..0000000 --- a/dmr/message.go +++ /dev/null @@ -1,86 +0,0 @@ -// Package dmr contains generic message structures for the Digital Mobile Radio standard -package dmr - -import ( - "fmt" - - "github.com/pd0mz/go-dmr/bit" -) - -const ( - InfoPartBits = 98 - InfoBits = InfoPartBits * 2 - SlotPartBits = 10 - SlotBits = SlotPartBits * 2 - PayloadPartBits = InfoPartBits + SlotPartBits - PayloadBits = PayloadPartBits * 2 - SignalBits = 48 - BurstBits = PayloadBits + SignalBits -) - -// Table 9.2: SYNC Patterns -var SYNCPatterns = map[string]struct { - ControlMode string - PDU string -}{ - "\x07\x05\x05\x0f\x0d\x07\x0d\x0f\x07\x05\x0f\x07": {"BS sourced", "voice"}, - "\x0d\x0f\x0f\x05\x07\x0d\x07\x05\x0d\x0f\x05\x0d": {"BS sourced", "data"}, - "\x07\x0f\x07\x0d\x05\x0d\x0d\x05\x07\x0d\x0f\x0d": {"MS sourced", "voice"}, - "\x0d\x05\x0d\x07\x0f\x07\x07\x0f\x0d\x07\x05\x07": {"MS sourced", "data"}, - "\x07\x07\x0d\x05\x05\x0f\x07\x0d\x0f\x0d\x07\x07": {"MS sourced", "rc sync"}, - "\x05\x0d\x05\x07\x07\x0f\x07\x07\x05\x07\x0f\x0f": {"TDMA direct mode time slot 1", "voice"}, - "\x0f\x07\x0f\x0d\x0d\x05\x0d\x0d\x0f\x0d\x05\x05": {"TDMA direct mode time slot 1", "data"}, - "\x07\x0d\x0f\x0f\x0d\x05\x0f\x05\x05\x0d\x05\x0f": {"TDMA direct mode time slot 2", "voice"}, - "\x0d\x07\x05\x05\x07\x0f\x05\x0f\x0f\x07\x0f\x05": {"TDMA direct mode time slot 2", "data"}, - "\x0d\x0d\x07\x0f\x0f\x05\x0d\x07\x05\x07\x0d\x0d": {"Reserved SYNC pattern", "reserved"}, -} - -// Burst contains data from a single burst, see 4.2.2 Burst and frame structure -type Burst struct { - bits bit.Bits -} - -func NewBurst(raw []byte) (*Burst, error) { - if len(raw)*8 != BurstBits { - return nil, fmt.Errorf("dmr: expected %d bits, got %d", BurstBits, len(raw)*8) - } - b := &Burst{} - b.bits = bit.NewBits(raw) - return b, nil -} - -// Info returns the 196 bits of info in the burst. The data is usually BPTC(196, 96) encoded. -func (b *Burst) Info() bit.Bits { - // The info is contained in bits 0..98 and 166..216 for a total of 196 bits - var n = make(bit.Bits, InfoBits) - copy(n[0:InfoPartBits], b.bits[0:InfoPartBits]) - copy(n[InfoPartBits:InfoBits], b.bits[InfoPartBits+SignalBits+SlotBits:BurstBits]) - return n -} - -// Payload returns the 216 bits of payload in the burst. -func (b *Burst) Payload() bit.Bits { - // The payload is contained in bits 0..108 and 156..264 for a total of 216 bits - var p = make(bit.Bits, PayloadBits) - copy(p[0:PayloadPartBits], b.bits[0:PayloadPartBits]) - copy(p[PayloadPartBits:PayloadBits], b.bits[PayloadPartBits+SignalBits:BurstBits]) - return p -} - -// Signal returns the 48 bits of signal or SYNC information in the burst. -func (b *Burst) Signal() bit.Bits { - // The signal bits are contained in bits 108..156 for a total of 48 bits - var s = make(bit.Bits, SignalBits) - copy(s, b.bits[PayloadPartBits:PayloadPartBits+SignalBits]) - return s -} - -func (b *Burst) SlotType() uint32 { - /* The slottype is 20 bits, starting after the payload info */ - var s uint32 - for i := InfoPartBits; i < InfoPartBits+SlotBits; i++ { - var shift = uint32(20 - (i - InfoPartBits)) - s = s | uint32(b.bits[i]< %d\n", lc.SrcID, lc.DstID) + return nil +} + +func (r *Repeater) HandleVoiceLCHeader(p *ipsc.Packet) error { + r.DataCallEnd(p) + + var ( + err error + payload = make([]byte, 12) + ) + if err = bptc.Process(dmr.ExtractInfoBits(p.PayloadBits), payload); err != nil { + return err + } + + // CRC mask to the checksum. See DMR AI. spec. page 143 + for i := 9; i < 12; i++ { + payload[i] ^= 0x99 + } + + var lc *LC + if lc, err = r.lcDecodeFullLC(payload); err != nil { + return err + } + + fmt.Printf(" lc: %d -> %d\n", lc.SrcID, lc.DstID) + return nil +} + +func (r *Repeater) lcDecode(payload []byte) (*LC, error) { + if payload[0]&bit.B10000000 > 0 { + return nil, errors.New("dmr/lc: protect flag is not 0") + } + if payload[1] != 0 { + return nil, errors.New("dmr/lc: feature set ID is not 0") + } + + lc := &LC{} + switch payload[0] & bit.B00111111 { + case 3: + lc.CallType = ipsc.CallTypePrivate + case 0: + lc.CallType = ipsc.CallTypeGroup + default: + return nil, fmt.Errorf("dmr/lc: invalid FCLO; unknown call type %#02x", payload[0]&bit.B00111111) + } + + lc.DstID = uint32(payload[3])<<16 | uint32(payload[4])<<8 | uint32(payload[5]) + lc.SrcID = uint32(payload[6])<<16 | uint32(payload[7])<<8 | uint32(payload[8]) + return lc, nil +} + +func (r *Repeater) lcDecodeFullLC(payload []byte) (*LC, error) { + var ( + err error + syndrome = fec.RS_12_9_Poly{} + ) + if err = fec.RS_12_9_CalcSyndrome(payload, &syndrome); err != nil { + return nil, err + } + + if fec.RS_12_9_CheckSyndrome(&syndrome) { + if _, err = fec.RS_12_9_Correct(payload, &syndrome); err != nil { + return nil, err + } + } + + return r.lcDecode(payload) +} diff --git a/dmr/repeater/repeater.go b/dmr/repeater/repeater.go new file mode 100644 index 0000000..0991ea3 --- /dev/null +++ b/dmr/repeater/repeater.go @@ -0,0 +1,141 @@ +package repeater + +import ( + "fmt" + "log" + "time" + + "github.com/pd0mz/go-dmr/ipsc" +) + +const ( + Idle uint8 = iota + VoiceCallRunning + DataCallRunning +) + +type SlotData struct { + BlocksReceived uint8 + BlocksExpected uint8 + PacketHeaderValid bool +} + +type SlotVoice struct { + // Last frame number + Frame uint8 +} + +type Slot struct { + State uint8 + LastCallReceived time.Time + CallStart time.Time + CallEnd time.Time + CallType uint8 + SrcID, DstID uint32 + Data SlotData + Voice SlotVoice + LastSequence uint8 + LastSlotType uint16 +} + +type Repeater struct { + Slot []*Slot +} + +func New() *Repeater { + r := &Repeater{ + Slot: make([]*Slot, 2), + } + r.Slot[0] = &Slot{} + r.Slot[1] = &Slot{} + return r +} + +func (r *Repeater) DataCallEnd(p *ipsc.Packet) { + if p.Timeslot > 1 { + return + } + + slot := r.Slot[p.Timeslot] + if slot.State != DataCallRunning { + return + } + + log.Printf("dmr/repeater: data call ended on TS%d, %d -> %d\n", p.Timeslot+1, slot.SrcID, slot.DstID) + + slot.State = Idle + slot.CallEnd = time.Now() + slot.Data.PacketHeaderValid = false +} + +func (r *Repeater) VoiceCallStart(p *ipsc.Packet) { + if p.Timeslot > 1 { + return + } + + slot := r.Slot[p.Timeslot] + if slot.State == VoiceCallRunning { + r.VoiceCallEnd(p) + } + + log.Printf("dmr/repeater: voice call started on TS%d, %d -> %d\n", p.Timeslot+1, slot.SrcID, slot.DstID) + slot.CallStart = time.Now() + slot.CallType = p.CallType + slot.SrcID = p.SrcID + slot.DstID = p.DstID + slot.State = VoiceCallRunning +} + +func (r *Repeater) VoiceCallEnd(p *ipsc.Packet) { + if p.Timeslot > 1 { + return + } + + slot := r.Slot[p.Timeslot] + if slot.State != VoiceCallRunning { + return + } + + log.Printf("dmr/repeater: voice call ended on TS%d, %d -> %d\n", p.Timeslot+1, slot.SrcID, slot.DstID) + + slot.State = Idle + slot.CallEnd = time.Now() +} + +func (r *Repeater) Stream(p *ipsc.Packet) { + // Kill errneous timeslots here + if p.Timeslot > 1 { + log.Printf("killed packet with timeslot %d\n", p.Timeslot) + return + } + if p.Sequence == r.Slot[p.Timeslot].LastSequence { + return + } + r.Slot[p.Timeslot].LastSequence = p.Sequence + + var err error + + fmt.Printf("dmr[%d] [%d->%d]: %s: ", p.Sequence, p.SrcID, p.DstID, ipsc.SlotTypeName[p.SlotType]) + switch p.SlotType { + case ipsc.VoiceLCHeader: + err = r.HandleVoiceLCHeader(p) + case ipsc.TerminatorWithLC: + err = r.HandleTerminatorWithLC(p) + case ipsc.DataHeader: + err = r.HandleDataHeader(p) + case ipsc.VoiceDataA, ipsc.VoiceDataB, ipsc.VoiceDataC, ipsc.VoiceDataD, ipsc.VoiceDataE, ipsc.VoiceDataF: + // De-duplicate packets, since we could run in merged TS1/2 mode + if r.Slot[p.Timeslot].LastSlotType == p.SlotType { + fmt.Println("(ignored)") + } else { + err = r.HandleVoiceData(p) + } + r.Slot[p.Timeslot].LastSlotType = p.SlotType + default: + fmt.Println("unhandled") + } + + if err != nil { + fmt.Printf("error: %v\n", err) + } +} diff --git a/dmr/repeater/voice.go b/dmr/repeater/voice.go new file mode 100644 index 0000000..9baed10 --- /dev/null +++ b/dmr/repeater/voice.go @@ -0,0 +1,89 @@ +package repeater + +import ( + "errors" + "fmt" + "log" + + "github.com/pd0mz/go-dmr/bptc" + "github.com/pd0mz/go-dmr/dmr" + "github.com/pd0mz/go-dmr/ipsc" +) + +var voiceFrameMap = map[uint16]uint8{ + ipsc.VoiceDataA: 0, + ipsc.VoiceDataB: 1, + ipsc.VoiceDataC: 2, + ipsc.VoiceDataD: 3, + ipsc.VoiceDataE: 4, + ipsc.VoiceDataF: 5, +} + +func (r *Repeater) HandleVoiceData(p *ipsc.Packet) error { + r.DataCallEnd(p) + + var ( + err error + payload = make([]byte, 12) + ) + + if err = bptc.Process(dmr.ExtractInfoBits(p.PayloadBits), payload); err != nil { + return err + } + if r.Slot[p.Timeslot].State != VoiceCallRunning { + r.VoiceCallStart(p) + } + + return r.HandleVoiceFrame(p) +} + +func (r *Repeater) HandleVoiceFrame(p *ipsc.Packet) error { + // This may contain a sync frame + sync := dmr.ExtractSyncBits(p.PayloadBits) + patt := dmr.SyncPattern(sync) + if patt != dmr.SyncPatternUnknown && r.Slot[p.Timeslot].Voice.Frame != 0 { + fmt.Printf("sync pattern %s\n", dmr.SyncPatternName[patt]) + r.Slot[p.Timeslot].Voice.Frame = 0 + return nil + } else { + // This may be a duplicate frame + var ( + oldFrame = r.Slot[p.Timeslot].Voice.Frame + newFrame = voiceFrameMap[p.SlotType] + ) + switch { + case oldFrame > 5: + // Ignore, wait for next sync frame + return nil + case newFrame == oldFrame: + return errors.New("dmr/voice: ignored, duplicate frame") + case newFrame > oldFrame: + if newFrame-oldFrame > 1 { + log.Printf("dmr/voice: framedrop, went from %c -> %c", 'A'+oldFrame, 'A'+newFrame) + } + case newFrame < oldFrame: + if newFrame > 0 { + log.Printf("dmr/voice: framedrop, went from %c -> %c", 'A'+oldFrame, 'A'+newFrame) + } + } + r.Slot[p.Timeslot].Voice.Frame++ + } + + // If it's not a sync frame, then it should have an EMB inside the sync field. + var ( + emb *dmr.EMB + err error + ) + if emb, err = dmr.ParseEMB(dmr.ExtractEMBBitsFromSyncBits(sync)); err != nil { + fmt.Println("unknown sync pattern, no EMB") + return err + } + + fmt.Printf("EMB LCSS %d\n", emb.LCSS) + + // TODO(maze): implement VBPTC matrix handling + switch emb.LCSS { + } + + return nil +} diff --git a/dmr/slot.go b/dmr/slot.go new file mode 100644 index 0000000..060fc80 --- /dev/null +++ b/dmr/slot.go @@ -0,0 +1,35 @@ +package dmr + +import "github.com/pd0mz/go-dmr/bit" + +var SlotTypeName = [16]string{ + "PI Header", // 0000 + "VOICE Header:", // 0001 + "TLC:", // 0010 + "CSBK:", // 0011 + "MBC Header:", // 0100 + "MBC:", // 0101 + "DATA Header:", // 0110 + "RATE 1/2 DATA:", // 0111 + "RATE 3/4 DATA:", // 1000 + "Slot idle", // 1001 + "Rate 1 DATA", // 1010 + "Unknown/Bad (11)", // 1011 + "Unknown/Bad (12)", // 1100 + "Unknown/Bad (13)", // 1101 + "Unknown/Bad (14)", // 1110 + "Unknown/Bad (15)", // 1111 +} + +func ExtractSlotType(payload bit.Bits) []byte { + bits := ExtractSlotTypeBits(payload) + return bits.Bytes() +} + +func ExtractSlotTypeBits(payload bit.Bits) bit.Bits { + var b = make(bit.Bits, SlotTypeBits) + copy(b[:SlotTypeHalfBits], payload[InfoHalfBits:InfoHalfBits+SlotTypeHalfBits]) + var o = InfoHalfBits + SlotTypeHalfBits + SyncBits + copy(b[SlotTypeHalfBits:], payload[o:o+SlotTypeHalfBits]) + return b +} diff --git a/dmr/sync.go b/dmr/sync.go new file mode 100644 index 0000000..bd3399d --- /dev/null +++ b/dmr/sync.go @@ -0,0 +1,75 @@ +package dmr + +import ( + "bytes" + + "github.com/pd0mz/go-dmr/bit" +) + +// Table 9.2: SYNC Patterns +const ( + SyncPatternBSSourcedVoice uint16 = 1 << iota + SyncPatternBSSourcedData + SyncPatternMSSourcedVoice + SyncPatternMSSourcedData + SyncPatternMSSourcedRC + SyncPatternDirectVoiceTS1 + SyncPatternDirectDataTS1 + SyncPatternDirectVoiceTS2 + SyncPatternDirectDataTS2 + SyncPatternUnknown +) + +var ( + bsSourcedVoice = []byte{0x75, 0x5f, 0xd7, 0xdf, 0x75, 0xf7} + bsSourcedData = []byte{0xdf, 0xf5, 0x7d, 0x75, 0xdf, 0x5d} + msSourcedVoice = []byte{0x7f, 0x7d, 0x5d, 0xd5, 0x7d, 0xfd} + msSourcedData = []byte{0xd5, 0xd7, 0xf7, 0x7f, 0xd7, 0x57} + msSourcedRC = []byte{0x77, 0xd5, 0x5f, 0x7d, 0xfd, 0x77} + directVoiceTS1 = []byte{0x5d, 0x57, 0x7f, 0x77, 0x57, 0xff} + directDataTS1 = []byte{0xf7, 0xfd, 0xd5, 0xdd, 0xfd, 0x55} + directVoiceTS2 = []byte{0x7d, 0xff, 0xd5, 0xf5, 0x5d, 0x5f} + directDataTS2 = []byte{0xd7, 0x55, 0x7f, 0x5f, 0xf7, 0xf5} + SyncPatternName = map[uint16]string{ + SyncPatternBSSourcedVoice: "bs sourced voice", + SyncPatternBSSourcedData: "bs sourced data", + SyncPatternMSSourcedVoice: "ms sourced voice", + SyncPatternMSSourcedData: "ms sourced data", + SyncPatternMSSourcedRC: "ms sourced rc", + SyncPatternDirectVoiceTS1: "direct voice ts1", + SyncPatternDirectDataTS1: "direct data ts1", + SyncPatternDirectVoiceTS2: "direct voice ts2", + SyncPatternDirectDataTS2: "direct data ts2", + SyncPatternUnknown: "unknown", + } +) + +func ExtractSyncBits(payload bit.Bits) bit.Bits { + return payload[108:156] +} + +func SyncPattern(bits bit.Bits) uint16 { + var b = bits.Bytes() + switch { + case bytes.Equal(b, bsSourcedVoice): + return SyncPatternBSSourcedVoice + case bytes.Equal(b, bsSourcedData): + return SyncPatternBSSourcedData + case bytes.Equal(b, msSourcedVoice): + return SyncPatternMSSourcedVoice + case bytes.Equal(b, msSourcedData): + return SyncPatternMSSourcedData + case bytes.Equal(b, msSourcedRC): + return SyncPatternMSSourcedRC + case bytes.Equal(b, directVoiceTS1): + return SyncPatternDirectVoiceTS1 + case bytes.Equal(b, directDataTS1): + return SyncPatternDirectDataTS1 + case bytes.Equal(b, directVoiceTS2): + return SyncPatternDirectVoiceTS2 + case bytes.Equal(b, directDataTS2): + return SyncPatternDirectDataTS2 + default: + return SyncPatternUnknown + } +}