diff --git a/homebrew/data.go b/homebrew/data.go new file mode 100644 index 0000000..9fc6e6d --- /dev/null +++ b/homebrew/data.go @@ -0,0 +1,65 @@ +package homebrew + +import ( + "fmt" + + "github.com/pd0mz/go-dmr/bit" + "github.com/pd0mz/go-dmr/ipsc" +) + +// ParseData reads a raw DMR data frame from the homebrew protocol and parses it as it were an IPSC packet. +func ParseData(data []byte) (*ipsc.Packet, error) { + if len(data) != 53 { + return nil, fmt.Errorf("invalid packet length %d, expected 53 bytes", len(data)) + } + + var ( + slotType uint16 + dataType = (data[15] & bit.B11110000) >> 4 + frameType = (data[15] & bit.B00001100) >> 2 + ) + switch frameType { + case bit.B00000000, bit.B00000001: // voice/voice sync + switch dataType { + case bit.B00000000: + slotType = ipsc.VoiceDataA + case bit.B00000001: + slotType = ipsc.VoiceDataB + case bit.B00000010: + slotType = ipsc.VoiceDataC + case bit.B00000011: + slotType = ipsc.VoiceDataD + case bit.B00000100: + slotType = ipsc.VoiceDataE + case bit.B00000101: + slotType = ipsc.VoiceDataF + } + case bit.B00000010: // data sync + switch dataType { + case bit.B00000001: + slotType = ipsc.VoiceLCHeader + case bit.B00000010: + slotType = ipsc.TerminatorWithLC + case bit.B00000011: + slotType = ipsc.CSBK + case bit.B00000110: + slotType = ipsc.DataHeader + case bit.B00000111: + slotType = ipsc.Rate12Data + case bit.B00001000: + slotType = ipsc.Rate34Data + } + } + + return &ipsc.Packet{ + Timeslot: (data[15] & bit.B00000001), + CallType: (data[15] & bit.B00000010) >> 1, + FrameType: (data[15] & bit.B00001100) >> 2, + SlotType: slotType, + SrcID: uint32(data[5])<<16 | uint32(data[6])<<8 | uint32(data[7]), + DstID: uint32(data[8])<<16 | uint32(data[9])<<8 | uint32(data[10]), + Payload: data[20:], + PayloadBits: bit.NewBits(data[20:]), + Sequence: data[4], + }, nil +} diff --git a/homebrew/homebrew.go b/homebrew/homebrew.go index 988b210..1b85ae3 100644 --- a/homebrew/homebrew.go +++ b/homebrew/homebrew.go @@ -4,7 +4,6 @@ package homebrew import ( "bytes" "crypto/sha256" - "encoding/binary" "encoding/hex" "errors" "fmt" @@ -14,6 +13,8 @@ import ( "strings" "sync/atomic" "time" + + "github.com/pd0mz/go-dmr/ipsc" ) var ( @@ -97,89 +98,8 @@ const ( UnitCall ) -// FrameType reflects the DMR data frame type. -type FrameType byte - -const ( - Voice FrameType = iota - VoiceSync - DataSync - UnusedFrameType -) - -// Frame is a frame of DMR data. -type Frame struct { - Signature [4]byte - Sequence byte - SrcID uint32 - DstID uint32 - RepeaterID uint32 - Flags byte - StreamID uint32 - DMR [33]byte -} - -// Bytes packs the frame into bytes ready to send over the network. -func (f *Frame) Bytes() []byte { - var b = make([]byte, 53) - copy(b[:4], f.Signature[:]) - b[4] = f.Sequence - b[5] = byte(f.SrcID >> 16) - b[6] = byte(f.SrcID >> 8) - b[7] = byte(f.SrcID) - b[8] = byte(f.DstID >> 16) - b[9] = byte(f.DstID >> 8) - b[10] = byte(f.DstID) - binary.BigEndian.PutUint32(b[11:15], f.RepeaterID) - b[15] = f.Flags - binary.BigEndian.PutUint32(b[16:20], f.StreamID) - copy(b[21:], f.DMR[:]) - - return b -} - -// CallType returns if the current frame is a group or unit call. -func (f *Frame) CallType() CallType { - return CallType((f.Flags >> 1) & 0x01) -} - -// DataType is the slot type when the FrameType is a data sync frame. For voice -// frames it's the voice sequence number in DMR numbering, where 0=A, 1=B, etc. -func (f *Frame) DataType() byte { - return f.Flags >> 4 -} - -// FrameType returns if the current frame is a voice, voice sync or data sync frame. -func (f *Frame) FrameType() FrameType { - return FrameType((f.Flags >> 2) & 0x03) -} - -// Slot is the time slot number. -func (f *Frame) Slot() int { - return int(f.Flags&0x01) + 1 -} - -// ParseFrame parses a raw DMR data frame. -func ParseFrame(data []byte) (*Frame, error) { - if len(data) != 53 { - return nil, errors.New("invalid packet length") - } - - f := &Frame{} - copy(f.Signature[:], data[:4]) - f.Sequence = data[4] - f.SrcID = (uint32(data[5]) << 16) | (uint32(data[6]) << 8) | uint32(data[7]) - f.DstID = (uint32(data[8]) << 16) | (uint32(data[9]) << 8) | uint32(data[10]) - f.RepeaterID = binary.BigEndian.Uint32(data[11:15]) - f.Flags = data[15] - f.StreamID = binary.BigEndian.Uint32(data[16:20]) - copy(f.DMR[:], data[20:]) - - return f, nil -} - // StreamFunc is called by the DMR repeater when a DMR data frame is received. -type StreamFunc func(*Frame) +type StreamFunc func(*ipsc.Packet) type authStatus byte @@ -444,19 +364,32 @@ func (l *Link) parse(queue <-chan packet) { } case authDone: + if l.Dump { + fmt.Printf("received from %s:\n", p.addr) + fmt.Print(hex.Dump(p.data)) + } switch { case bytes.Equal(p.data[:4], DMRData): - if l.stream == nil { - return - } - frame, err := ParseFrame(p.data) - if err != nil { - log.Printf("error parsing DMR data: %v\n", err) - return - } - l.stream(frame) + l.parseDMR(p.data) } } } } } + +func (l *Link) parseDMR(d []byte) { + // If we have no stream callback, don't even bother to decode the DMR data frame. + if l.stream == nil { + return + } + + var ( + p *ipsc.Packet + err error + ) + if p, err = ParseData(d); err != nil { + log.Printf("dmr/homebrew: error parsing DMRD frame: %v\n", err) + return + } + l.stream(p) +} diff --git a/ipsc/packet.go b/ipsc/packet.go new file mode 100644 index 0000000..41df52e --- /dev/null +++ b/ipsc/packet.go @@ -0,0 +1,105 @@ +package ipsc + +import ( + "encoding/hex" + "fmt" + + "github.com/pd0mz/go-dmr/bit" + "github.com/pd0mz/go-dmr/dmr" +) + +const ( + VoiceLCHeader uint16 = 0x1111 + TerminatorWithLC uint16 = 0x2222 + CSBK uint16 = 0x3333 + DataHeader uint16 = 0x4444 + Rate12Data uint16 = 0x5555 + Rate34Data uint16 = 0x6666 + VoiceDataA uint16 = 0xbbbb + VoiceDataB uint16 = 0xcccc + VoiceDataC uint16 = 0x7777 + VoiceDataD uint16 = 0x8888 + VoiceDataE uint16 = 0x9999 + VoiceDataF uint16 = 0xaaaa + IPSCSync uint16 = 0xeeee + UnknownSlotType uint16 = 0x0000 +) + +const ( + CallTypePrivate uint8 = iota + CallTypeGroup +) + +var ( + TimeslotName = map[uint8]string{ + 0x00: "TS1", + 0x01: "TS2", + } + FrameTypeName = map[uint8]string{ + 0x00: "voice", + 0x01: "voice sync", + 0x02: "data sync", + 0x03: "unused", + } + SlotTypeName = map[uint16]string{ + 0x0000: "unknown", + 0x1111: "voice LC header", + 0x2222: "terminator with LC", + 0x3333: "CBSK", + 0x4444: "data header", + 0x5555: "rate 1/2 data", + 0x6666: "rate 3/4 data", + 0x7777: "voice data C", + 0x8888: "voice data D", + 0x9999: "voice data E", + 0xaaaa: "voice data F", + 0xbbbb: "voice data A", + 0xcccc: "voice data B", + 0xeeee: "IPSC sync", + } + CallTypeName = map[uint8]string{ + 0x00: "private", + 0x01: "group", + } +) + +type Packet struct { + Timeslot uint8 // 0=ts1, 1=ts2 + FrameType uint8 + SlotType uint16 + CallType uint8 // 0=private, 1=group + SrcID uint32 + DstID uint32 + Payload []byte // 34 bytes + PayloadBits bit.Bits // 264 bits + Sequence uint8 +} + +func (p *Packet) Dump() string { + var s string + s += fmt.Sprintf("timeslot..: 0b%02b (%s)\n", p.Timeslot, TimeslotName[p.Timeslot]) + s += fmt.Sprintf("frame type: 0b%02b (%s)\n", p.FrameType, FrameTypeName[p.FrameType]) + s += fmt.Sprintf("slot type.: 0x%04x (%s)\n", p.SlotType, SlotTypeName[p.SlotType]) + s += fmt.Sprintf("call type.: 0x%02x (%s)\n", p.CallType, CallTypeName[p.CallType]) + s += fmt.Sprintf("source....: %d\n", p.SrcID) + s += fmt.Sprintf("target....: %d\n", p.DstID) + s += fmt.Sprintf("payload...: %d bytes (swapped):\n", len(p.Payload)) + s += hex.Dump(p.Payload) + s += fmt.Sprintf("payload...: %d bits:\n", len(p.PayloadBits)) + s += p.PayloadBits.Dump() + return s +} + +func (p *Packet) InfoBits() bit.Bits { + var b = make(bit.Bits, dmr.InfoBits) + copy(b[0:dmr.InfoHalfBits], p.PayloadBits[0:dmr.InfoHalfBits]) + copy(b[dmr.InfoHalfBits:], p.PayloadBits[dmr.InfoHalfBits+dmr.SlotTypeBits+dmr.SignalBits:]) + return b +} + +func (p *Packet) VoiceBits() bit.Bits { + var b = make(bit.Bits, dmr.VoiceBits) + copy(b[:dmr.VoiceHalfBits], p.PayloadBits[:dmr.VoiceHalfBits]) + copy(b[dmr.VoiceHalfBits:], p.PayloadBits[dmr.VoiceHalfBits+dmr.SignalBits:]) + return b +} diff --git a/ipsc/payload.go b/ipsc/payload.go new file mode 100644 index 0000000..8dc0e04 --- /dev/null +++ b/ipsc/payload.go @@ -0,0 +1,7 @@ +package ipsc + +func SwapPayloadBytes(payload []byte) { + for i := 0; i < len(payload)-1; i += 2 { + payload[i], payload[i+1] = payload[i+1], payload[i] + } +}