diff --git a/cmd/bits/main.go b/cmd/bits/main.go new file mode 100644 index 0000000..3c5340b --- /dev/null +++ b/cmd/bits/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "crypto/rand" + "fmt" +) + +func main() { + var raw = make([]byte, 4) + rand.Read(raw) + fmt.Printf("raw = %02x%02x%02x%02x\n", raw[0], raw[1], raw[2], raw[3]) + + for i := 0; i < 4*8; i++ { + b := i / 8 + o := byte(7 - i%8) + fmt.Printf("%02x b=%d, o=%d, r=%d\n", raw[b], b, o, raw[b]>>o) + } + for i := 0; i < 4*8; i++ { + b := i / 8 + o := byte(7 - i%8) + if (raw[b]>>o)&0x01 == 0x01 { + fmt.Print("1") + } else { + fmt.Print("0") + } + } + fmt.Println("") +} diff --git a/cmd/dmrdatadump/main.go b/cmd/dmrdatadump/main.go index 8c83bc5..ee83a1d 100644 --- a/cmd/dmrdatadump/main.go +++ b/cmd/dmrdatadump/main.go @@ -10,270 +10,16 @@ import ( "gopkg.in/yaml.v2" + "github.com/google/gopacket" + "github.com/google/gopacket/pcap" + "github.com/gordonklaus/portaudio" "github.com/tehmaze/go-dmr/bit" - "github.com/tehmaze/go-dmr/dmr" "github.com/tehmaze/go-dmr/dmr/repeater" "github.com/tehmaze/go-dmr/homebrew" "github.com/tehmaze/go-dmr/ipsc" + "github.com/tehmaze/go-dsd" ) -var ( - bursttypes = map[uint32]string{ - 0: "pi header", - 1: "voice lc header", - 2: "terminator with lc", - 3: "csbk", - 4: "csbk", - 5: "appended mbc", - 6: "data header", - 7: "rate ½ data continuation", - 8: "rate ¾ data continuation", - 9: "idle", - } - headertypes = []string{ - "UDT ", "Resp", "UDat", "CDat", "Hdr4", "Hdr5", "Hdr6", "Hdr7", - "Hdr8", "Hdr9", "Hdr10", "Hdr11", "Hdr12", "DSDa", "RSDa", "Prop", - } - saptypes = []string{ - "UDT", "1", "TCP HC", "UDP HC", "IP Pkt", "ARP Pkt", "6", "7", - "8", "Prop Pkt", "Short Data", "11", "12", "13", "14", "15", - } - fidMap = [256]byte{ - 0x01, 0x00, 0x00, 0x00, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - } - fidName = map[uint8]string{ - 0: "Unknown", - 1: "Standard", - 2: "Flyde Micro", - 3: "PROD-EL SPA", - 4: "Motorola Connect+", - 5: "RADIODATA GmbH", - 6: "Hyteria (8)", - 7: "Motorola Capacity+", - 8: "EMC S.p.A (19)", - 9: "EMC S.p.A (28)", - 10: "Radio Activity Srl (51)", - 11: "Radio Activity Srl (60)", - 12: "Tait Electronics", - 13: "Hyteria (104)", - 14: "Vertex Standard", - } -) - -func dumpFCLO(payload []byte, fid uint8) { - fmt.Printf(" fid: %s (%d)\n", fidName[fid], fid) - if fid == 0 || fid == 16 { // MotoTRBO Capacity+ - fmt.Printf(" fclo: -\n") - } -} - -func dumpDataHeader(header []byte) { - dh, err := dmr.ParseDataHeader(header, false) - if err != nil { - fmt.Printf(" data header error: %v\n", err) - return - } - dc := dh.CommonHeader() - fmt.Printf(" service accesspt: %d\n", dc.ServiceAccessPoint) - fmt.Printf(" src id -> dst id: %d -> %d\n", dc.SrcID, dc.DstID) - switch d := dh.(type) { - case dmr.UDTDataHeader: - fmt.Printf(" format..........: %s (%#02x)\n", dmr.UDTFormatName[d.Format], d.Format) - fmt.Printf(" pad nibble......: %d\n", d.PadNibble) - fmt.Printf(" appended blocks.: %d\n", d.AppendedBlocks) - fmt.Printf(" supplementary f.: %b\n", d.SupplementaryFlag) - fmt.Printf(" opcode..........: %d\n", d.OPCode) - case dmr.UnconfirmedDataHeader: - fmt.Printf(" pad octet count.: %d\n", d.PadOctetCount) - fmt.Printf(" full message....: %b\n", d.FullMessage) - fmt.Printf(" blocks to follow: %d\n", d.BlocksToFollow) - fmt.Printf(" fragment seq.no.: %d\n", d.FragmentSequenceNumber) - } -} - -func dumpData(data bit.Bits) { - var out = make([][]byte, 7) - for y := 0; y < 7; y++ { - out[y] = make([]byte, 80) - for x := 0; x < 80; x++ { - if x == 0 || x == 18 || x == 21 || x == 30 || x == 33 || x == 51 { - out[y][x] = '|' - } else { - out[y][x] = ' ' - } - } - } - // Info first half - for i := 0; i < 98; i++ { - var x = 1 + (i % 16) - var y = (i / 16) - if x > 8 { - x++ - } - if data[i] > 0 { - out[y][x] = '1' - } else { - out[y][x] = '0' - } - } - // Slot type first half - for i := 98; i < 108; i++ { - var x = 19 + ((i - 98) % 2) - var y = (i - 98) / 2 - if data[i] > 0 { - out[y][x] = '1' - } else { - out[y][x] = '0' - } - } - // SYNC - for i := 108; i < 156; i++ { - var x = 22 + ((i - 108) % 8) - var y = (i - 108) / 8 - if data[i] > 0 { - out[y][x] = '1' - } else { - out[y][x] = '0' - } - } - // Slot type second half - for i := 156; i < 166; i++ { - var x = 31 + ((i - 156) % 2) - var y = (i - 156) / 2 - if data[i] > 0 { - out[y][x] = '1' - } else { - out[y][x] = '0' - } - } - // Info second half - for i := 166; i < 264; i++ { - var x = 34 + ((i - 166) % 16) - var y = ((i - 166) / 16) - if x > 41 { - x++ - } - if data[i] > 0 { - out[y][x] = '1' - } else { - out[y][x] = '0' - } - } - fmt.Println("|------INFO1------|ST|--SYNC--|ST|------INFO2------|") - for _, row := range out { - fmt.Println(string(row)) - } -} - -func dumpSync(p *ipsc.Packet, desc string) { - sync := dmr.ExtractSyncBits(p.PayloadBits) - patt := dmr.SyncPattern(sync) - fmt.Printf("dmr[%d->%d]: ts%d got %s:\n", p.SrcID, p.DstID, p.Timeslot+1, desc) - fmt.Printf(" sync pattern: %s\n", dmr.SyncPatternName[patt]) - if patt == dmr.SyncPatternUnknown { - fmt.Print(hex.Dump(sync.Bytes())) - for i, b := range sync { - sync[i] = b ^ 1 - } - fmt.Print(hex.Dump(sync.Bytes())) - } - - /* - slot := dmr.ExtractSlotType(p.PayloadBits) - var ( - codeword uint32 - bursttype uint32 - payload = make([]byte, 12) - ) - - codeword = (uint32(slot[0]) << 11) | (uint32(slot[1]) << 3) | uint32(slot[2])>>5 - bursttype = uint32(slot[0]) & 7 - fmt.Printf("codeword: %#08x, bursttype: %#04x\n", codeword, bursttype) - fec.Golay_23_12_Correct(&codeword) - fmt.Printf("codeword: %#08x (after correcting)\n", codeword) - codeword &= 0x0f - fmt.Printf("codeword: %#08x (after masking)\n", codeword) - bursttype ^= codeword - var errors int - if bursttype&1 > 0 { - errors++ - } - if bursttype&2 > 0 { - errors++ - } - if bursttype&4 > 0 { - errors++ - } - if bursttype&8 > 0 { - errors++ - } - bursttype = codeword - //fmt.Printf("%d errors detected, burstype: %08x (%d)\n", errors, bursttype, bursttype) - fmt.Printf(" burst type: %s (%d), %d errors\n", bursttypes[bursttype], bursttype, errors) - - if bursttype < 7 { - if err := bptc.Process(dmr.ExtractInfoBits(p.PayloadBits), payload); err != nil { - fmt.Printf(" payload error: %v\n", err) - return - } - fmt.Printf(" payload: (%d bytes)\n", len(payload)) - fmt.Print(" " + hex.Dump(payload)) - } - - fid := payload[1] - fidm := fidMap[fid] - - switch bursttype { - case 1, 2: - dumpFCLO(payload, fidm) - case 6: - dumpDataHeader(payload) - } - - /* - if err := fec.Golay_20_8_Check(slot); err != nil { - fmt.Printf("%v\n", err) - fmt.Print(hex.Dump(slot.Bytes())) - return - } - - cc := (slot[0] << 3) | (slot[1] << 2) | (slot[2] << 1) | slot[3] - dt := (slot[4] << 3) | (slot[5] << 2) | (slot[6] << 1) | slot[7] - fmt.Printf("cc: %d, data type: %d\n", cc, dt) - */ -} - func rc() *homebrew.RepeaterConfiguration { return &homebrew.RepeaterConfiguration{ Callsign: "PI1BOL", @@ -291,52 +37,70 @@ func rc() *homebrew.RepeaterConfiguration { } } -func dumpRaw(raw []byte) { - fmt.Printf("dump raw frame of %d bytes\n", len(raw)) - p, err := homebrew.ParseData(raw) - if err != nil { - fmt.Printf(" parse error: %v\n", err) - return - } - dumpPacket(p) -} - -func dumpPacket(p *ipsc.Packet) { - fmt.Print(p.Dump()) - dumpData(p.PayloadBits) - - switch p.SlotType { - case ipsc.VoiceLCHeader: - dumpSync(p, "voice LC header") - case ipsc.TerminatorWithLC: - dumpSync(p, "terminator with LC") - case ipsc.CSBK: - dumpSync(p, "CSBK") - case ipsc.VoiceDataA: - dumpSync(p, "voice A") - case ipsc.VoiceDataB: - dumpSync(p, "voice B") - case ipsc.VoiceDataC: - dumpSync(p, "voice C") - case ipsc.VoiceDataD: - dumpSync(p, "voice D") - case ipsc.VoiceDataE: - dumpSync(p, "voice E") - case ipsc.VoiceDataF: - dumpSync(p, "voice F") - } - - fmt.Print("\n---\n\n") -} - func main() { dumpFile := flag.String("dump", "", "dump file") + pcapFile := flag.String("pcap", "", "PCAP file") liveFile := flag.String("live", "", "live configuration file") showRaw := flag.Bool("raw", false, "show raw frames") + audioTS := flag.Int("audiots", 0, "play audio from time slot (1 or 2)") flag.Parse() + if *audioTS > 2 { + log.Fatalf("invalid time slot %d\n", *audioTS) + return + } + r := repeater.New() - if *liveFile != "" { + + if *audioTS > 0 { + ambeframe := make(chan float32) + + vs := dsd.NewAMBEVoiceStream(3) + r.VoiceFrameFunc = func(p *ipsc.Packet, bits bit.Bits) { + var in = make([]byte, len(bits)) + for i, b := range bits { + in[i] = byte(b) + } + + samples, err := vs.Decode(in) + if err != nil { + log.Printf("error decoding AMBE3000 frame: %v\n", err) + return + } + fmt.Printf("%v\n", samples) + for _, sample := range samples { + ambeframe <- sample + } + } + + portaudio.Initialize() + defer portaudio.Terminate() + h, err := portaudio.DefaultHostApi() + if err != nil { + panic(err) + } + + p := portaudio.LowLatencyParameters(nil, h.DefaultOutputDevice) + p.SampleRate = 8000 + p.Output.Channels = 1 + stream, err := portaudio.OpenStream(p, func(out []float32) { + for i := range out { + out[i] = <-ambeframe + } + }) + if err != nil { + log.Printf("error streaming: %v\n", err) + return + } + defer stream.Close() + if err := stream.Start(); err != nil { + log.Printf("error streaming: %v\n", err) + return + } + } + + switch { + case *liveFile != "": log.Printf("going in live mode using %q\n", *liveFile) f, err := os.Open(*liveFile) if err != nil { @@ -362,8 +126,7 @@ func main() { protocol.Dump = true panic(protocol.Run()) - } else { - + case *dumpFile != "": i, err := os.Open(*dumpFile) if err != nil { log.Fatal(err) @@ -394,5 +157,33 @@ func main() { panic(err) } } + + case *pcapFile != "": + var ( + handle *pcap.Handle + err error + ) + + if handle, err = pcap.OpenOffline(*pcapFile); err != nil { + panic(err) + } + defer handle.Close() + + dec := gopacket.DecodersByLayerName["Ethernet"] + source := gopacket.NewPacketSource(handle, dec) + for packet := range source.Packets() { + raw := packet.ApplicationLayer().Payload() + if *showRaw { + fmt.Println("raw packet:") + fmt.Print(hex.Dump(raw)) + } + + p, err := homebrew.ParseData(raw) + if err != nil { + fmt.Printf(" parse error: %v\n", err) + continue + } + r.Stream(p) + } } } diff --git a/dmr/repeater/data.go b/dmr/repeater/data.go new file mode 100644 index 0000000..15631c7 --- /dev/null +++ b/dmr/repeater/data.go @@ -0,0 +1,8 @@ +package repeater + +// DataFrame is a decoded frame with data +type DataFrame struct { + SrcID, DstID uint32 + Timeslot uint8 + Data []byte +} diff --git a/dmr/repeater/repeater.go b/dmr/repeater/repeater.go index e773ad8..e8770e5 100644 --- a/dmr/repeater/repeater.go +++ b/dmr/repeater/repeater.go @@ -5,6 +5,7 @@ import ( "log" "time" + "github.com/tehmaze/go-dmr/bit" "github.com/tehmaze/go-dmr/ipsc" ) @@ -39,7 +40,9 @@ type Slot struct { } type Repeater struct { - Slot []*Slot + Slot []*Slot + DataFrameFunc func(*ipsc.Packet, bit.Bits) + VoiceFrameFunc func(*ipsc.Packet, bit.Bits) } func New() *Repeater { diff --git a/dmr/repeater/voice.go b/dmr/repeater/voice.go index a30b2b7..e5bb059 100644 --- a/dmr/repeater/voice.go +++ b/dmr/repeater/voice.go @@ -76,13 +76,19 @@ func (r *Repeater) HandleVoiceFrame(p *ipsc.Packet) 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) + if emb != nil { + fmt.Printf("EMB LCSS %d\n", emb.LCSS) + + // TODO(maze): implement VBPTC matrix handling + switch emb.LCSS { + } + } - // TODO(maze): implement VBPTC matrix handling - switch emb.LCSS { + // Extract the three embedded AMBE frames if we have a callback function to process them. + if r.VoiceFrameFunc != nil { + r.VoiceFrameFunc(p, dmr.ExtractVoiceBits(p.PayloadBits)) } return nil diff --git a/dmr/voice.go b/dmr/voice.go new file mode 100644 index 0000000..15fce9f --- /dev/null +++ b/dmr/voice.go @@ -0,0 +1,10 @@ +package dmr + +import "github.com/tehmaze/go-dmr/bit" + +func ExtractVoiceBits(payload bit.Bits) bit.Bits { + var b = make(bit.Bits, VoiceBits) + copy(b[:VoiceHalfBits], payload[:VoiceHalfBits]) + copy(b[VoiceHalfBits:], payload[VoiceHalfBits+SyncBits:]) + return b +}