Prepare for public release
parent
493e72583d
commit
cba4cf198d
@ -0,0 +1 @@
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
@ -1,27 +0,0 @@
|
||||
all: deps build
|
||||
|
||||
build: build-dmrstream
|
||||
|
||||
build-dmrstream:
|
||||
go build ./cmd/dmrstream/
|
||||
@ls -alh dmrstream
|
||||
|
||||
deps: godeps deps-platform oggfwd
|
||||
|
||||
deps-platform:
|
||||
@echo "For OS X: make deps-brew"
|
||||
@echo "For Debian: make deps-debian"
|
||||
|
||||
deps-brew:
|
||||
brew install --HEAD mbelib
|
||||
brew install lame
|
||||
brew install sox
|
||||
|
||||
deps-debian:
|
||||
sudo apt-get install lame sox
|
||||
|
||||
godeps:
|
||||
go get -v $(shell go list -f '{{ join .Deps "\n" }}' ./... | sort -u | egrep '(gopkg|github)' | grep -v '/tehmaze/go-dmr')
|
||||
|
||||
oggfwd:
|
||||
$(CC) -O2 -pipe -Wall -ffast-math -fsigned-char -lshout -pthread -o $@ $@.c
|
@ -1,2 +1,32 @@
|
||||
# dmr
|
||||
Golang Digital Mobile Radio protocols
|
||||
# go-dmr
|
||||
|
||||
Golang Digital Mobile Radio protocols.
|
||||
|
||||
## References
|
||||
|
||||
The DMR Air Interface protocol is specified in *Electromagnetic compatibility
|
||||
and Radio spectrum Matters (ERM); Digital Mobile Radio (DMR) Systems; Part 1:
|
||||
DMR Air Interface (AI) protocol*, [ETSI TS 102 361-1][ETSI TS 102 361-1].
|
||||
|
||||
The Brandmeister Homebrew protocol is specified in
|
||||
[IPSC Protocol Specs for homebrew DMR repeater][homebrew specs]
|
||||
by [Hans DL5DI](mailto:dl5di@gmx.de),
|
||||
[Jonathan Naylor (G4KLXG)](https://twitter.com/g4klx) and Torsten Schultze
|
||||
(DG1HT).
|
||||
|
||||
[ETSI TS 102 361-1]: docs/ts_10236101v010405p.pdf
|
||||
[homebrew specs]: docs/DMRplus%20IPSC%20Protocol%20for%20HB%20repeater%20(20150726).pdf
|
||||
|
||||
## Warning
|
||||
|
||||
This implementation is not suitable for commercial use and is for educational
|
||||
purposes only.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
The implementation is possible because of the invaluable help from the
|
||||
following persons. Thanks for your patience and providing me with sample data
|
||||
and links to test the protocols.
|
||||
|
||||
* Rudy Hardeman (PD0ZRY)
|
||||
* Artem Prilutskiy (R3ABM)
|
||||
|
@ -1,97 +0,0 @@
|
||||
package bit
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Bit byte
|
||||
|
||||
func (b *Bit) Flip() {
|
||||
(*b) ^= 0x01
|
||||
}
|
||||
|
||||
type Bits []Bit
|
||||
|
||||
func toBits(b byte) Bits {
|
||||
var o = make(Bits, 8)
|
||||
for bit, mask := 0, byte(128); bit < 8; bit, mask = bit+1, mask>>1 {
|
||||
if b&mask != 0 {
|
||||
o[bit] = 1
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func NewBits(bytes []byte) Bits {
|
||||
var l = len(bytes)
|
||||
var o = make(Bits, 0)
|
||||
for i := 0; i < l; i++ {
|
||||
o = append(o, toBits(bytes[i])...)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (bits *Bits) Bytes() []byte {
|
||||
var l = len(*bits)
|
||||
var o = make([]byte, (l+7)/8)
|
||||
for i, b := range *bits {
|
||||
if b == 0x01 {
|
||||
o[i/8] |= (1 << byte(7-(i%8)))
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (bits Bits) Debits() Debits {
|
||||
var debits = make(Debits, (len(bits)+1)/2)
|
||||
for i := 0; i < len(bits); i += 2 {
|
||||
debits[i/2] = Debit((bits[i] << 1) | (bits[i+1]))
|
||||
}
|
||||
return debits
|
||||
}
|
||||
|
||||
func (bits Bits) Dump() string {
|
||||
var (
|
||||
s string
|
||||
bytes = bits.Bytes()
|
||||
)
|
||||
|
||||
for i, b := range bytes {
|
||||
if i%7 == 0 {
|
||||
if i != 0 {
|
||||
s += "\n"
|
||||
}
|
||||
s += fmt.Sprintf("%08x ", i)
|
||||
}
|
||||
s += fmt.Sprintf("%08b ", b)
|
||||
}
|
||||
s += "\n"
|
||||
return s
|
||||
}
|
||||
|
||||
func (bits Bits) Equal(other Bits) bool {
|
||||
var l = bits.Len()
|
||||
if l != other.Len() {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < l; i++ {
|
||||
if bits[i] != other[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (bits Bits) Len() int {
|
||||
return len(bits)
|
||||
}
|
||||
|
||||
func (bits Bits) String() string {
|
||||
var s = ""
|
||||
for _, b := range bits {
|
||||
if b == 0x01 {
|
||||
s += "1"
|
||||
} else {
|
||||
s += "0"
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package bit
|
||||
|
||||
type Debit uint8
|
||||
|
||||
type Debits []Debit
|
||||
|
||||
func toDebits(b byte) Debits {
|
||||
var o = make(Debits, 4)
|
||||
for bit, mask := 0, byte(128); bit < 8; bit, mask = bit+2, mask>>2 {
|
||||
o[bit/2] = Debit((b >> mask) & 3)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func NewDebits(bytes []byte) Debits {
|
||||
var l = len(bytes)
|
||||
var o = make(Debits, 0)
|
||||
for i := 0; i < l; i++ {
|
||||
o = append(o, toDebits(bytes[i])...)
|
||||
}
|
||||
return o
|
||||
}
|
@ -1,31 +1,39 @@
|
||||
package bit
|
||||
package dmr
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBit(t *testing.T) {
|
||||
var tests = []struct {
|
||||
Test []byte
|
||||
Want Bits
|
||||
Want []byte
|
||||
}{
|
||||
{
|
||||
[]byte{0x2a},
|
||||
Bits{0, 0, 1, 0, 1, 0, 1, 0},
|
||||
[]byte{0, 0, 1, 0, 1, 0, 1, 0},
|
||||
},
|
||||
{
|
||||
[]byte{0xbe, 0xef},
|
||||
Bits{1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1},
|
||||
[]byte{1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := NewBits(test.Test)
|
||||
got := BytesToBits(test.Test)
|
||||
if len(got) != len(test.Want) {
|
||||
t.Fatalf("expected length %d, got %d [%s]", len(test.Want), len(got), got.String())
|
||||
t.Fatalf("expected length %d, got %d [%s]", len(test.Want), len(got), string(got))
|
||||
}
|
||||
for i, b := range got {
|
||||
if b != test.Want[i] {
|
||||
t.Fatalf("bit %d is off: %v != %v", i, got, test.Want)
|
||||
}
|
||||
}
|
||||
|
||||
rev := BitsToBytes(got)
|
||||
if !bytes.Equal(rev, test.Test) {
|
||||
t.Fatalf("reverse bits to bytes failed, %v != %v", rev, test.Test)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
package bptc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tehmaze/go-dmr/bit"
|
||||
)
|
||||
|
||||
func Decode(bits bit.Bits, deinterleave bool) (bit.Bits, error) {
|
||||
var debits bit.Bits
|
||||
if deinterleave {
|
||||
debits = Deinterleave(bits)
|
||||
} else {
|
||||
debits = bits
|
||||
}
|
||||
if err := Check(bits); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Extract(debits), nil
|
||||
}
|
||||
|
||||
// Deinterleave raw bits
|
||||
func Deinterleave(r bit.Bits) bit.Bits {
|
||||
// The first bit is R(3) which is not used so can be ignored
|
||||
var d = make(bit.Bits, 196)
|
||||
var i int
|
||||
for a := 0; a < 196; a++ {
|
||||
i = (a * 181) % 196
|
||||
d[a] = r[i]
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Hamming(13, 9, 3) check
|
||||
func Hamming1393(debits bit.Bits) (bool, bit.Bits) {
|
||||
var err = make(bit.Bits, 4)
|
||||
err[0] = debits[0] ^ debits[1] ^ debits[3] ^ debits[5] ^ debits[6]
|
||||
err[1] = debits[0] ^ debits[1] ^ debits[2] ^ debits[4] ^ debits[6] ^ debits[7]
|
||||
err[2] = debits[0] ^ debits[1] ^ debits[2] ^ debits[3] ^ debits[5] ^ debits[7] ^ debits[8]
|
||||
err[3] = debits[0] ^ debits[2] ^ debits[4] ^ debits[5] ^ debits[8]
|
||||
return (err[0] == debits[9]) && (err[1] == debits[10]) && (err[2] == debits[11]) && (err[3] == debits[12]), err
|
||||
}
|
||||
|
||||
// Hamming(15, 11, 3) check
|
||||
func Hamming15113(debits bit.Bits) (bool, bit.Bits) {
|
||||
var err = make(bit.Bits, 4)
|
||||
err[0] = debits[0] ^ debits[1] ^ debits[2] ^ debits[3] ^ debits[5] ^ debits[7] ^ debits[8]
|
||||
err[1] = debits[1] ^ debits[2] ^ debits[3] ^ debits[4] ^ debits[6] ^ debits[8] ^ debits[9]
|
||||
err[2] = debits[2] ^ debits[3] ^ debits[4] ^ debits[5] ^ debits[7] ^ debits[9] ^ debits[10]
|
||||
err[3] = debits[0] ^ debits[1] ^ debits[2] ^ debits[4] ^ debits[6] ^ debits[7] ^ debits[10]
|
||||
return (err[0] == debits[11]) && (err[1] == debits[12]) && (err[2] == debits[13]) && (err[3] == debits[14]), err
|
||||
}
|
||||
|
||||
// Check each row with a Hamming (15,11,3) code
|
||||
func Check(debits bit.Bits) error {
|
||||
var (
|
||||
row = make(bit.Bits, 15)
|
||||
col = make(bit.Bits, 13)
|
||||
)
|
||||
|
||||
// Run through each of the 9 rows containing data
|
||||
for r := 0; r < 9; r++ {
|
||||
p := (r * 15) + 1
|
||||
for a := 0; a < 15; a++ {
|
||||
row[a] = debits[p]
|
||||
}
|
||||
if ok, _ := Hamming15113(row); !ok {
|
||||
return fmt.Errorf("hamming(15, 11, 3) check failed on row #%d", r)
|
||||
}
|
||||
}
|
||||
|
||||
// Run through each of the 15 columns
|
||||
for c := 0; c < 15; c++ {
|
||||
p := c + 1
|
||||
for a := 0; a < 13; a++ {
|
||||
col[a] = debits[p]
|
||||
p += 15
|
||||
}
|
||||
if ok, _ := Hamming1393(col); !ok {
|
||||
return fmt.Errorf("hamming(13, 9, 3) check failed on col #%d", c)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract the 96 bits of data
|
||||
func Extract(debits bit.Bits) bit.Bits {
|
||||
var (
|
||||
out = make(bit.Bits, 96)
|
||||
a, pos int
|
||||
)
|
||||
|
||||
for a = 4; a <= 11; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
for a = 16; a <= 26; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
for a = 31; a <= 41; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
for a = 46; a <= 56; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
for a = 61; a <= 71; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
for a = 76; a <= 86; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
for a = 91; a <= 101; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
for a = 106; a <= 116; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
for a = 121; a <= 131; a++ {
|
||||
out[pos] = debits[a]
|
||||
pos++
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
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("")
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"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/repeater"
|
||||
"github.com/tehmaze/go-dmr/homebrew"
|
||||
"github.com/tehmaze/go-dmr/ipsc"
|
||||
"github.com/tehmaze/go-dsd"
|
||||
)
|
||||
|
||||
func rc() *homebrew.RepeaterConfiguration {
|
||||
return &homebrew.RepeaterConfiguration{
|
||||
Callsign: "PI1BOL",
|
||||
RepeaterID: 2043044,
|
||||
RXFreq: 0,
|
||||
TXFreq: 0,
|
||||
TXPower: 0,
|
||||
ColorCode: 1,
|
||||
Latitude: 52.296786,
|
||||
Longitude: 4.595454,
|
||||
Height: 12,
|
||||
Location: "Hillegom, ZH, NL",
|
||||
Description: fmt.Sprintf("%s go-dmr", homebrew.Version),
|
||||
URL: "https://github.com/tehmaze",
|
||||
}
|
||||
}
|
||||
|
||||
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 *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
|
||||
}
|
||||
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 {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
d, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
network := &homebrew.Network{}
|
||||
if err := yaml.Unmarshal(d, network); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
protocol, err := homebrew.New(network, rc, r.Stream)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
protocol.Dump = true
|
||||
panic(protocol.Run())
|
||||
|
||||
case *dumpFile != "":
|
||||
i, err := os.Open(*dumpFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer i.Close()
|
||||
|
||||
var raw = make([]byte, 53)
|
||||
for {
|
||||
if _, err := i.Read(raw); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if *showRaw {
|
||||
fmt.Println("raw packet:")
|
||||
fmt.Print(hex.Dump(raw))
|
||||
}
|
||||
|
||||
//dumpRaw(raw)
|
||||
|
||||
p, err := homebrew.ParseData(raw)
|
||||
if err != nil {
|
||||
fmt.Printf(" parse error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
r.Stream(p)
|
||||
|
||||
// Skip newline in recording
|
||||
if _, err := i.Seek(1, 1); err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,697 +0,0 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lshout
|
||||
#include "shout/shout.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/pcap"
|
||||
"github.com/tehmaze/go-dmr/bit"
|
||||
"github.com/tehmaze/go-dmr/dmr/repeater"
|
||||
"github.com/tehmaze/go-dmr/homebrew"
|
||||
"github.com/tehmaze/go-dmr/ipsc"
|
||||
"github.com/tehmaze/go-dsd"
|
||||
)
|
||||
|
||||
const (
|
||||
Timeslot1 uint8 = iota
|
||||
Timeslot2
|
||||
)
|
||||
|
||||
var (
|
||||
VoiceFrameDuration = time.Millisecond * 60
|
||||
VoiceSyncDuration = time.Millisecond * 360
|
||||
UserMap = map[uint32]string{}
|
||||
)
|
||||
|
||||
type Protocol interface {
|
||||
Close() error
|
||||
Run() error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Repeater *homebrew.RepeaterConfiguration
|
||||
Link map[string]*Link
|
||||
User string
|
||||
}
|
||||
|
||||
type Shout struct {
|
||||
Host string
|
||||
Port uint
|
||||
User string
|
||||
Password string
|
||||
Mount string
|
||||
Format int
|
||||
Protocol int
|
||||
Description string
|
||||
Genre string
|
||||
|
||||
// wrap the native C struct
|
||||
shout *C.struct_shout
|
||||
metadata *C.struct_shout_metadata_t
|
||||
|
||||
stream chan []byte
|
||||
}
|
||||
|
||||
func (s *Shout) getError() error {
|
||||
errstr := C.GoString(C.shout_get_error(s.shout))
|
||||
return errors.New("shout: " + errstr)
|
||||
}
|
||||
|
||||
func (s *Shout) init() error {
|
||||
if s.shout != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.shout = C.shout_new()
|
||||
s.stream = make(chan []byte)
|
||||
return s.update()
|
||||
}
|
||||
|
||||
func (s *Shout) update() error {
|
||||
// set hostname
|
||||
p := C.CString(s.Host)
|
||||
C.shout_set_host(s.shout, p)
|
||||
C.free(unsafe.Pointer(p))
|
||||
|
||||
// set port
|
||||
C.shout_set_port(s.shout, C.ushort(s.Port))
|
||||
|
||||
// set username
|
||||
p = C.CString(s.User)
|
||||
C.shout_set_user(s.shout, p)
|
||||
C.free(unsafe.Pointer(p))
|
||||
|
||||
// set password
|
||||
p = C.CString(s.Password)
|
||||
C.shout_set_password(s.shout, p)
|
||||
C.free(unsafe.Pointer(p))
|
||||
|
||||
// set mount point
|
||||
p = C.CString(s.Mount)
|
||||
C.shout_set_mount(s.shout, p)
|
||||
C.free(unsafe.Pointer(p))
|
||||
|
||||
// set description
|
||||
p = C.CString(s.Description)
|
||||
C.shout_set_description(s.shout, p)
|
||||
C.free(unsafe.Pointer(p))
|
||||
|
||||
// set genre
|
||||
p = C.CString(s.Genre)
|
||||
C.shout_set_genre(s.shout, p)
|
||||
C.free(unsafe.Pointer(p))
|
||||
|
||||
// set format
|
||||
C.shout_set_format(s.shout, C.uint(s.Format))
|
||||
|
||||
// set protocol
|
||||
C.shout_set_protocol(s.shout, C.uint(s.Protocol))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Shout) Close() error {
|
||||
if s.shout != nil {
|
||||
C.shout_free(s.shout)
|
||||
s.shout = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Shout) Open() error {
|
||||
if err := s.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errno := int(C.shout_open(s.shout))
|
||||
if errno != 0 {
|
||||
return s.getError()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Shout) Stream(data []byte) error {
|
||||
if s.shout == nil {
|
||||
return errors.New("shout: stream not open")
|
||||
}
|
||||
|
||||
ptr := (*C.uchar)(&data[0])
|
||||
C.shout_send(s.shout, ptr, C.size_t(len(data)))
|
||||
errno := int(C.shout_get_errno(s.shout))
|
||||
if errno != 0 {
|
||||
return s.getError()
|
||||
}
|
||||
|
||||
C.shout_sync(s.shout)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Shout) UpdateDescription(desc string) {
|
||||
ptr := C.CString(desc)
|
||||
C.shout_set_description(s.shout, ptr)
|
||||
C.free(unsafe.Pointer(ptr))
|
||||
}
|
||||
|
||||
func (s *Shout) UpdateGenre(genre string) {
|
||||
ptr := C.CString(genre)
|
||||
C.shout_set_genre(s.shout, ptr)
|
||||
C.free(unsafe.Pointer(ptr))
|
||||
}
|
||||
|
||||
func (s *Shout) UpdateMetadata(mname string, mvalue string) {
|
||||
md := C.shout_metadata_new()
|
||||
ptr1 := C.CString(mname)
|
||||
ptr2 := C.CString(mvalue)
|
||||
C.shout_metadata_add(md, ptr1, ptr2)
|
||||
C.free(unsafe.Pointer(ptr1))
|
||||
C.free(unsafe.Pointer(ptr2))
|
||||
C.shout_set_metadata(s.shout, md)
|
||||
C.shout_metadata_free(md)
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Disable bool
|
||||
|
||||
// Supported protocols
|
||||
Homebrew *homebrew.Network
|
||||
PCAP *PCAPProtocol
|
||||
|
||||
// Shout streams
|
||||
Transcode string
|
||||
TS1Stream *Shout
|
||||
TS2Stream *Shout
|
||||
}
|
||||
|
||||
type Stream struct {
|
||||
Timeslot uint8
|
||||
Repeater string
|
||||
Transcode string // Transcoder binary/script
|
||||
Shout *Shout // Shout server details
|
||||
Buffer chan float32
|
||||
Samples []float32
|
||||
|
||||
pipe io.WriteCloser // Our transcoder input
|
||||
running bool
|
||||
connected bool
|
||||
retry int
|
||||
}
|
||||
|
||||
func NewStream(ts uint8, repeater string, sh *Shout, transcode string) *Stream {
|
||||
if sh.Description == "" {
|
||||
sh.Description = fmt.Sprintf("DMR Repeater %s (TS%d)", strings.ToUpper(repeater), ts+1)
|
||||
}
|
||||
if sh.Genre == "" {
|
||||
sh.Genre = "ham"
|
||||
}
|
||||
return &Stream{
|
||||
Timeslot: ts,
|
||||
Repeater: repeater,
|
||||
Transcode: transcode,
|
||||
Shout: sh,
|
||||
Buffer: make(chan float32),
|
||||
Samples: make([]float32, 8000),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) Close() error {
|
||||
if s.running {
|
||||
s.running = false
|
||||
if s.pipe != nil {
|
||||
return s.pipe.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stream) Run() error {
|
||||
var err error
|
||||
|
||||
log.Printf("dmr/stream: connecting to icecast server %s:%d%s as %s\n",
|
||||
s.Shout.Host, s.Shout.Port, s.Shout.Mount, s.Shout.User)
|
||||
if err = s.Shout.Open(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.connected = true
|
||||
s.Shout.UpdateDescription(fmt.Sprintf("DMR repeater link to %s (TS%d)", s.Repeater, s.Timeslot+1))
|
||||
|
||||
log.Println("dmr/stream: setting up transcoder pipe")
|
||||
cmnd := strings.Split(s.Transcode, " ")
|
||||
pipe := exec.Command(cmnd[0], cmnd[1:]...)
|
||||
enc, err := pipe.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer enc.Close()
|
||||
s.pipe = enc
|
||||
|
||||
out, err := pipe.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("dmr/stream: error connecting to stream output: %v\n", err)
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Connect stderr
|
||||
pipe.Stderr = os.Stderr
|
||||
|
||||
if err := pipe.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Spawn goroutine that deals with new audio from the transcode process
|
||||
go func(out io.Reader) {
|
||||
var buf = make([]byte, 1024)
|
||||
for {
|
||||
if _, err := out.Read(buf); err != nil {
|
||||
log.Printf("dmr/stream: error reading from stream: %v\n", err)
|
||||
s.Close()
|
||||
return
|
||||
}
|
||||
var err = s.Shout.Stream(buf)
|
||||
for err != nil {
|
||||
log.Printf("dmr/stream: error streaming: %v\n", err)
|
||||
s.Close()
|
||||
|
||||
s.retry++
|
||||
if s.retry > 15 {
|
||||
log.Printf("dmr/stream: retry limit exceeded\n")
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * time.Duration(3*s.retry))
|
||||
log.Printf("dmr/stream: connecting to icecast server %s:%d%s as %s\n",
|
||||
s.Shout.Host, s.Shout.Port, s.Shout.Mount, s.Shout.User)
|
||||
err = s.Shout.Open()
|
||||
}
|
||||
|
||||
s.retry = 0
|
||||
}
|
||||
}(out)
|
||||
|
||||
var i uint32
|
||||
s.running = true
|
||||
for s.running {
|
||||
// Ensure that we *always* have new data (even be it silence) within the duration of a voice frame
|
||||
select {
|
||||
case sample := <-s.Buffer:
|
||||
s.Samples[i] = sample
|
||||
i++
|
||||
case <-time.After(VoiceFrameDuration):
|
||||
log.Printf("dmr/stream: filling silence, timeout after %s\n", VoiceFrameDuration)
|
||||
for ; i < 8000; i++ {
|
||||
s.Samples[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
if i >= 8000 {
|
||||
log.Printf("dmr/stream: writing %d samples to encoder\n", i)
|
||||
var buffer = make([]byte, 4)
|
||||
for _, sample := range s.Samples {
|
||||
binary.BigEndian.PutUint32(buffer, math.Float32bits(sample))
|
||||
if _, err := enc.Write(buffer); err != nil {
|
||||
log.Printf("dmr/stream: error writing to encoder: %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stream) UpdateMetadata(repeater string, src, dst uint32) {
|
||||
if s.connected {
|
||||
log.Printf("dmr/stream: updating meta data to %s and %d -> %d\n", repeater, src, dst)
|
||||
s.Shout.UpdateMetadata("description", fmt.Sprintf("Repeater %s", strings.ToUpper(repeater)))
|
||||
s.Shout.UpdateMetadata("artist", fmt.Sprintf("Repeater %s", strings.ToUpper(repeater)))
|
||||
|
||||
var (
|
||||
dstName = strconv.Itoa(int(dst))
|
||||
srcName = strconv.Itoa(int(src))
|
||||
)
|
||||
if name, ok := UserMap[dst]; ok {
|
||||
dstName = name
|
||||
}
|
||||
if name, ok := UserMap[src]; ok {
|
||||
srcName = name
|
||||
}
|
||||
|
||||
s.Shout.UpdateMetadata("song", fmt.Sprintf("TS%d [%s -> %s]", s.Timeslot+1, srcName, dstName))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) Write(sample float32) {
|
||||
s.Buffer <- sample
|
||||
}
|
||||
|
||||
type Samples struct {
|
||||
data []float32
|
||||
size, rptr, wptr int
|
||||
}
|
||||
|
||||
func NewSamples(size int) *Samples {
|
||||
return &Samples{
|
||||
data: make([]float32, size),
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Samples) Read() float32 {
|
||||
d := s.data[s.rptr]
|
||||
s.data[s.rptr] = 0
|
||||
s.rptr++
|
||||
if s.rptr == s.size {
|
||||
s.rptr = 0
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (s *Samples) Write(sample float32) {
|
||||
s.data[s.wptr] = sample
|
||||
s.wptr++
|
||||
if s.wptr == s.size {
|
||||
s.wptr = 0
|
||||
}
|
||||
}
|
||||
|
||||
type PCAPProtocol struct {
|
||||
Filename string
|
||||
DumpRaw bool
|
||||
Stream homebrew.StreamFunc
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (pp *PCAPProtocol) Close() error {
|
||||
pp.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pp *PCAPProtocol) Run() error {
|
||||
var (
|
||||
handle *pcap.Handle
|
||||
err error
|
||||
)
|
||||
|
||||
if handle, err = pcap.OpenOffline(pp.Filename); err != nil {
|
||||
return err
|
||||
}
|
||||
defer handle.Close()
|
||||
|
||||
dec := gopacket.DecodersByLayerName["Ethernet"]
|
||||
source := gopacket.NewPacketSource(handle, dec)
|
||||
for packet := range source.Packets() {
|
||||
raw := packet.ApplicationLayer().Payload()
|
||||
if pp.DumpRaw {
|
||||
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
|
||||
}
|
||||
if pp.Stream != nil {
|
||||
pp.Stream(p)
|
||||
}
|
||||
if pp.closed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
C.shout_init()
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := flag.String("config", "", "configuration file")
|
||||
amplify := flag.Float64("amplify", 25.0, "audio amplify rate")
|
||||
verbose := flag.Bool("verbose", false, "be verbose")
|
||||
enabled := flag.String("enable", "", "comma separated list of enabled links (overrides config)")
|
||||
disabled := flag.String("disable", "", "comma separated list of disabled links (overrides config)")
|
||||
flag.Parse()
|
||||
|
||||
overrides := map[string]bool{}
|
||||
for _, call := range strings.Split(*enabled, ",") {
|
||||
overrides[call] = true
|
||||
}
|
||||
for _, call := range strings.Split(*disabled, ",") {
|
||||
overrides[call] = false
|
||||
}
|
||||
|
||||
log.Printf("using configuration file %q\n", *configFile)
|
||||
f, err := os.Open(*configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open %q: %v\n", *configFile, err)
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
d, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read %q: %v\n", *configFile, err)
|
||||
return
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
if err := yaml.Unmarshal(d, config); err != nil {
|
||||
log.Fatalf("failed to parse %q: %v\n", *configFile, err)
|
||||
return
|
||||
}
|
||||
|
||||
if config.User != "" {
|
||||
uf, err := os.Open(config.User)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open %q: %v\n", config.User, err)
|
||||
return
|
||||
}
|
||||
defer uf.Close()
|
||||
|
||||
scanner := bufio.NewScanner(uf)
|
||||
var lines int
|
||||
for scanner.Scan() {
|
||||
part := strings.Split(string(scanner.Text()), ";")
|
||||
if lines > 1 {
|
||||
if dmrID, err := strconv.ParseUint(part[2], 10, 32); err == nil {
|
||||
UserMap[uint32(dmrID)] = fmt.Sprintf("%s (%s)", part[3], part[1])
|
||||
}
|
||||
}
|
||||
lines++
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatalf("failed to parse %q: %v\n", config.User, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.Link) == 0 {
|
||||
log.Fatalln("no links configured")
|
||||
return
|
||||
}
|
||||
|
||||
sm := map[string]map[uint8]*Stream{}
|
||||
ps := map[string]Protocol{}
|
||||
|
||||
for call, link := range config.Link {
|
||||
status, ok := overrides[call]
|
||||
switch {
|
||||
case ok:
|
||||
if !status {
|
||||
log.Printf("link/%s: link disabled, skipping (override)\n", call)
|
||||
continue
|
||||
}
|
||||
log.Printf("link/%s: link enabled (override)\n", call)
|
||||
case link.Disable:
|
||||
log.Printf("link/%s: link disabled, skipping\n", call)
|
||||
continue
|
||||
}
|
||||
log.Printf("link/%s: configuring link\n", call)
|
||||
|
||||
// Repeater
|
||||
r := repeater.New()
|
||||
|
||||
// Protocol
|
||||
switch {
|
||||
case link.Homebrew != nil:
|
||||
log.Printf("link/%s: homebrew protocol, %s\n", call, link.Homebrew.Master)
|
||||
|
||||
rc := func() *homebrew.RepeaterConfiguration {
|
||||
return config.Repeater
|
||||
}
|
||||
proto, err := homebrew.New(link.Homebrew, rc, r.Stream)
|
||||
if err != nil {
|
||||
log.Fatalf("link/%s: failed to setup protocol: %v\n", call, err)
|
||||
return
|
||||
}
|
||||
|
||||
ps[call] = proto
|
||||
|
||||
case link.PCAP != nil:
|
||||
log.Printf("link/%s: PCAP file %q\n", call, link.PCAP.Filename)
|
||||
link.PCAP.Stream = r.Stream
|
||||
|
||||
ps[call] = link.PCAP
|
||||
|
||||
default:
|
||||
log.Fatalf("[%s]: unknown or no protocol configured\n", call)
|
||||
return
|
||||
}
|
||||
|
||||
// Streams
|
||||
sm[call] = map[uint8]*Stream{}
|
||||
if link.TS1Stream != nil {
|
||||
if link.Transcode == "" {
|
||||
log.Fatalf("link/%s: TS1 stream defined, but no transcoder\n", call)
|
||||
}
|
||||
sm[call][Timeslot1] = NewStream(Timeslot1, call, link.TS1Stream, link.Transcode)
|
||||
}
|
||||
if link.TS2Stream != nil {
|
||||
if link.Transcode == "" {
|
||||
log.Fatalf("link/%s: TS2 stream defined, but no transcoder\n", call)
|
||||
}
|
||||
sm[call][Timeslot2] = NewStream(Timeslot1, call, link.TS2Stream, link.Transcode)
|
||||
}
|
||||
|
||||
// Setup AMBE voice stream decoder
|
||||
var (
|
||||
lastsrc, lastdst uint32
|
||||
last = time.Now()
|
||||
vs = dsd.NewAMBEVoiceStream(3)
|
||||
)
|
||||
|
||||
// Function that receives decoded AMBE frames as float32 PCM (8kHz mono)
|
||||
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 decode AMBE3000 frames: %v\n", err)
|
||||
return
|
||||
}
|
||||
for _, sample := range samples {
|
||||
if stream, ok := sm[call][p.Timeslot]; ok {
|
||||
stream.Write(sample * float32(*amplify))
|
||||
|
||||
if lastsrc != p.SrcID || lastdst != p.DstID {
|
||||
stream.UpdateMetadata(call, p.SrcID, p.DstID)
|
||||
lastsrc = p.SrcID
|
||||
lastdst = p.DstID
|
||||
}
|
||||
}
|
||||
}
|
||||
if false {
|
||||
diff := time.Now().Sub(last)
|
||||
if *verbose {
|
||||
log.Printf("%s since last voice sync\n", diff)
|
||||
}
|
||||
if diff < VoiceFrameDuration {
|
||||
t := VoiceFrameDuration - diff
|
||||
if *verbose {
|
||||
log.Printf("delaying %s, last tick was %s ago\n", t, diff)
|
||||
}
|
||||
time.Sleep(t)
|
||||
}
|
||||
last = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Signal handler
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func(signals chan os.Signal) {
|
||||
for _ = range signals {
|
||||
// Terminate protocols
|
||||
for call, p := range ps {
|
||||
log.Printf("link/%s: closing\n", call)
|
||||
if err := p.Close(); err != nil {
|
||||
log.Printf("link/%s: close error: %v\n", call, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Terminate streams
|
||||
for call, streams := range sm {
|
||||
for ts, stream := range streams {
|
||||
log.Printf("link/%s: closing stream for TS%d\n", call, ts+1)
|
||||
if err := stream.Close(); err != nil {
|
||||
log.Printf("link/%s: error closing stream for TS%d: %v\n", call, ts+1, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}(c)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
// Spawn a goroutine for all the protocol runners
|
||||
for call, p := range ps {
|
||||
wg.Add(1)
|
||||
go func(call string, p Protocol, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
if err := p.Run(); err != nil {
|
||||
log.Printf("link/%s: error running: %v\n", call, err)
|
||||
}
|
||||
log.Printf("link/%s: done\n", call)
|
||||
delete(ps, call)
|
||||
}(call, p, wg)
|
||||
}
|
||||
|
||||
// Spawn a goroutine for all the streamers
|
||||
for call, streams := range sm {
|
||||
if len(streams) == 0 {
|
||||
continue
|
||||
}
|
||||
for ts, stream := range streams {
|
||||
log.Printf("link/%s: starting stream for TS%d\n", call, ts+1)
|
||||
wg.Add(1)
|
||||
go func(call string, stream *Stream, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
if err := stream.Run(); err != nil {
|
||||
log.Printf("link/%s: error streaming: %v\n", call, err)
|
||||
}
|
||||
log.Printf("link/%s: stream done\n", call)
|
||||
}(call, stream, wg)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for protocols to finish
|
||||
log.Println("all routines started, waiting for protocols to finish")
|
||||
wg.Wait()
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
---
|
||||
repeater:
|
||||
callsign: PD0MZ
|
||||
id: 2043044
|
||||
rxfreq: 0
|
||||
txfreq: 0
|
||||
txpower: 0
|
||||
colorcode: 1
|
||||
latitude: 52.296786
|
||||
longitude: 4.595454
|
||||
height: 12
|
||||
location: Hillegom
|
||||
description: \o/ go-dmr
|
||||
url: https://maze.io/
|
||||
|
||||
user: user_by_call.csv
|
||||
|
||||
link:
|
||||
pd0zry:
|
||||
homebrew:
|
||||
master: brandmeister.pd0zry.ampr.org:62030
|
||||
authkey: passw0rd
|
||||
local: 0.0.0.0:62030
|
||||
localid: 2042214
|
||||
ts1stream:
|
||||
host: 127.0.0.1
|
||||
port: 8000
|
||||
user: source
|
||||
password: source
|
||||
mount: /ts1.mp3
|
||||
format: 1
|
||||
transcode: ./transcode-mp3.sh
|
||||
|
||||
debug:
|
||||
disable: true
|
||||
pcap:
|
||||
filename: brandmeister.pcap
|
||||
ts1stream:
|
||||
host: 127.0.0.1
|
||||
port: 8000
|
||||
user: source
|
||||
password: source
|
||||
mount: /debug-ts1.mp3
|
||||
format: 1
|
||||
transcode: ./transcode-mp3.sh
|
@ -1,4 +0,0 @@
|
||||
master: brandmeister.pd0zry.ampr.org:62030
|
||||
authkey: secret
|
||||
local: 0.0.0.0:62030
|
||||
localid: 123456
|
@ -0,0 +1,2 @@
|
||||
// Package dmr implements various protocols for interfacing Digital Mobile Radio repeaters and base stations.
|
||||
package dmr
|
@ -1,4 +0,0 @@
|
||||
radio_id: 2042214
|
||||
auth_key: 7061737377307264
|
||||
master: brandmeister.pd0zry.ampr.org:55000
|
||||
listen: 0.0.0.0:55000
|
@ -1,22 +0,0 @@
|
||||
// Package dmr contains generic message structures for the Digital Mobile Radio standard
|
||||
package dmr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tehmaze/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
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,10 +0,0 @@
|
||||
package dmr
|
||||
|
||||
import "github.com/tehmaze/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
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package repeater
|
||||
|
||||
// DataFrame is a decoded frame with data
|
||||
type DataFrame struct {
|
||||
SrcID, DstID uint32
|
||||
Timeslot uint8
|
||||
Data []byte
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package repeater
|
||||
|
||||
import (
|
||||
"github.com/tehmaze/go-dmr/bptc"
|
||||
"github.com/tehmaze/go-dmr/dmr"
|
||||
"github.com/tehmaze/go-dmr/ipsc"
|
||||
)
|
||||
|
||||
func (r *Repeater) HandleDataHeader(p *ipsc.Packet) error {
|
||||
var (
|
||||
h dmr.DataHeader
|
||||
err error
|
||||
payload = make([]byte, 12)
|
||||
)
|
||||
|
||||
if err = bptc.Process(dmr.ExtractInfoBits(p.PayloadBits), payload); err != nil {
|
||||
return err
|
||||
}
|
||||
if h, err = dmr.ParseDataHeader(payload, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(maze): handle receiving of data blocks
|
||||
switch h.(type) {
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
package repeater
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tehmaze/go-dmr/bit"
|
||||
"github.com/tehmaze/go-dmr/bptc"
|
||||
"github.com/tehmaze/go-dmr/dmr"
|
||||
"github.com/tehmaze/go-dmr/fec"
|
||||
"github.com/tehmaze/go-dmr/ipsc"
|
||||
)
|
||||
|
||||
type LC struct {
|
||||
CallType uint8
|
||||
DstID, SrcID uint32
|
||||
}
|
||||
|
||||
func (r *Repeater) HandleTerminatorWithLC(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.
|
||||
payload[9] ^= 0x99
|
||||
payload[10] ^= 0x99
|
||||
payload[11] ^= 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) 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)
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
package repeater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/tehmaze/go-dmr/bit"
|
||||
"github.com/tehmaze/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
|
||||
DataFrameFunc func(*ipsc.Packet, bit.Bits)
|
||||
VoiceFrameFunc func(*ipsc.Packet, bit.Bits)
|
||||
}
|
||||
|
||||
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("ts%d/dmr[%03d] [%d->%d]: %s: ", p.Timeslot+1, 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)
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package repeater
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/tehmaze/go-dmr/bptc"
|
||||
"github.com/tehmaze/go-dmr/dmr"
|
||||
"github.com/tehmaze/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")
|
||||
}
|
||||
|
||||
if emb != nil {
|
||||
fmt.Printf("EMB LCSS %d\n", 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
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package dmr
|
||||
|
||||
import "github.com/tehmaze/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
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5f51c3e75c21287d3038c1dc84d99838509e46ac65fcfda35a8f24ea0dc50fa
|
||||
size 123620
|
@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:560f886910ee68c9d17bebb17ea4f96b297ef77ff036a0bcd149da0b2316ec8c
|
||||
size 1259856
|
@ -0,0 +1,12 @@
|
||||
package homebrew
|
||||
|
||||
type Config struct {
|
||||
// ID is the local DMR ID.
|
||||
ID uint32
|
||||
|
||||
// PeerID is the remote DMR ID.
|
||||
PeerID uint32
|
||||
|
||||
// AuthKey is the shared secret.
|
||||
AuthKey string
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package homebrew
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tehmaze/go-dmr/bit"
|
||||
"github.com/tehmaze/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
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package homebrew
|
||||
|
||||
// Messages as documented by DL5DI, G4KLX and DG1HT, see
|
||||
// http://download.prgm.org/dl5di-soft/dmrplus/documentation/Homebrew-Repeater/DMRplus%20IPSC%20Protocol%20for%20HB%20repeater%20(20150726).pdf
|
||||
var (
|
||||
DMRData = []byte("DMRD")
|
||||
MasterNAK = []byte("MSTNAK")
|
||||
MasterACK = []byte("MSTACK")
|
||||
RepeaterLogin = []byte("RPTL")
|
||||
RepeaterKey = []byte("RPTK")
|
||||
MasterPing = []byte("MSTPING")
|
||||
RepeaterPong = []byte("RPTPONG")
|
||||
MasterClosing = []byte("MSTCL")
|
||||
RepeaterClosing = []byte("RPTCL")
|
||||
)
|
@ -0,0 +1,74 @@
|
||||
package homebrew
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tehmaze/go-dmr"
|
||||
)
|
||||
|
||||
// RepeaterConfiguration holds information about the current repeater. It
|
||||
// should be returned by a callback in the implementation, returning actual
|
||||
// information about the current repeater status.
|
||||
type RepeaterConfiguration struct {
|
||||
Callsign string
|
||||
ID uint32 // Our RepeaterID
|
||||
RXFreq uint32
|
||||
TXFreq uint32
|
||||
TXPower uint8
|
||||
ColorCode uint8
|
||||
Latitude float32
|
||||
Longitude float32
|
||||
Height uint16
|
||||
Location string
|
||||
Description string
|
||||
URL string
|
||||
}
|
||||
|
||||
// Bytes returns the configuration as bytes.
|
||||
func (r *RepeaterConfiguration) Bytes() []byte {
|
||||
return []byte(r.String())
|
||||
}
|
||||
|
||||
// String returns the configuration as string.
|
||||
func (r *RepeaterConfiguration) String() string {
|
||||
if r.ColorCode < 1 {
|
||||
r.ColorCode = 1
|
||||
}
|
||||
if r.ColorCode > 15 {
|
||||
r.ColorCode = 15
|
||||
}
|
||||
if r.TXPower > 99 {
|
||||
r.TXPower = 99
|
||||
}
|
||||
|
||||
var lat = fmt.Sprintf("%-08f", r.Latitude)
|
||||
if len(lat) > 8 {
|
||||
lat = lat[:8]
|
||||
}
|
||||
var lon = fmt.Sprintf("%-09f", r.Longitude)
|
||||
if len(lon) > 9 {
|
||||
lon = lon[:9]
|
||||
}
|
||||
|
||||
var b = "RPTC"
|
||||
b += fmt.Sprintf("%-8s", r.Callsign)
|
||||
b += fmt.Sprintf("%08x", r.ID)
|
||||
b += fmt.Sprintf("%09d", r.RXFreq)
|
||||
b += fmt.Sprintf("%09d", r.TXFreq)
|
||||
b += fmt.Sprintf("%02d", r.TXPower)
|
||||
b += fmt.Sprintf("%02d", r.ColorCode)
|
||||
b += lat
|
||||
b += lon
|
||||
b += fmt.Sprintf("%03d", r.Height)
|
||||
b += fmt.Sprintf("%-20s", r.Location)
|
||||
b += fmt.Sprintf("%-20s", r.Description)
|
||||
b += fmt.Sprintf("%-124s", r.URL)
|
||||
b += fmt.Sprintf("%-40s", dmr.SoftwareID)
|
||||
b += fmt.Sprintf("%-40s", dmr.PackageID)
|
||||
return b
|
||||
}
|
||||
|
||||
// ConfigFunc returns an actual RepeaterConfiguration instance when called.
|
||||
// This is used by the DMR repeater to poll for current configuration,
|
||||
// statistics and metrics.
|
||||
type ConfigFunc func() *RepeaterConfiguration
|
@ -1,400 +0,0 @@
|
||||
/* vi:si:noexpandtab:sw=4:sts=4:ts=4
|
||||
*/
|
||||
/*
|
||||
* oggfwd -- Forward an Ogg stream from stdin to an Icecast server
|
||||
* A useful demonstration of the libshout API
|
||||
*
|
||||
* This program is distributed under the GNU General Public License, version 2.
|
||||
* A copy of this license is included with this source.
|
||||
*
|
||||
* This program is provided "as-is", with no explicit or implied warranties of
|
||||
* any kind.
|
||||
*
|
||||
* Copyright (C) 2003-2006, J <j@v2v.cc>,
|
||||
* rafael2k <rafael(at)riseup(dot)net>,
|
||||
* Moritz Grimm <gtgbr@gmx.net>
|
||||
* Copyright (C) 2015, Philipp Schafft <lion@lion.leolix.org>
|
||||
*/
|
||||
/* thanx to rhatto <rhatto (AT) riseup (DOT) net> and others at submidialogia :-P */
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/param.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#ifndef NO_UNISTD_H
|
||||
# include <unistd.h>
|
||||
#endif /* no-NO_UNISTD_H */
|
||||
|
||||
#include <shout/shout.h>
|
||||
|
||||
extern char *__progname;
|
||||
extern char *optarg;
|
||||
extern int optind;
|
||||
extern int errno;
|
||||
|
||||
volatile sig_atomic_t print_total = 0;
|
||||
volatile sig_atomic_t quit = 0;
|
||||
|
||||
#define METABUFSIZE 4096
|
||||
char *metafilename;
|
||||
shout_t *shout;
|
||||
|
||||
#define BUFFERSIZE 4096
|
||||
|
||||
#if defined(__dead)
|
||||
__dead void
|
||||
#else
|
||||
void
|
||||
#endif /* __dead */
|
||||
usage(void)
|
||||
{
|
||||
printf("usage: %s "
|
||||
"[-hp] "
|
||||
#ifdef SHOUT_TLS
|
||||
"[-T {disabled|auto|auto_no_plain|rfc2818|rfc2817}] "
|
||||
#endif
|
||||
"[-m metadata file] "
|
||||
"[-d description] "
|
||||
"[-g genre] "
|
||||
"[-n name] "
|
||||
"[-u URL]\n"
|
||||
" address port password mountpoint\n",
|
||||
__progname);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
load_metadata()
|
||||
{
|
||||
int i, fh, r;
|
||||
char buf[METABUFSIZE], *key, *val;
|
||||
enum {state_comment, state_key, state_value, state_unknown} state;
|
||||
|
||||
bzero(buf, METABUFSIZE);
|
||||
|
||||
if (!metafilename) {
|
||||
fprintf(stderr, "Please use the -m argument to set the meta file name!\n");
|
||||
return;
|
||||
}
|
||||
|
||||
fh = open(metafilename, O_RDONLY);
|
||||
|
||||
if (-1==fh) {
|
||||
fprintf(stderr, "Error while opening meta file \"%s\": %s\n", metafilename, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
r = read(fh, &buf, METABUFSIZE);
|
||||
if (-1==r) {
|
||||
fprintf(stderr, "Error while reading meta file \"%s\": %s\n", metafilename, strerror(errno));
|
||||
close(fh);
|
||||
return;
|
||||
}
|
||||
|
||||
state = state_unknown;
|
||||
key = val = NULL;
|
||||
i = 0;
|
||||
|
||||
while (i<METABUFSIZE) {
|
||||
switch (buf[i]) {
|
||||
case 0:
|
||||
/* we're done */
|
||||
i = METABUFSIZE;
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
case '\n':
|
||||
if (state_value==state) {
|
||||
buf[i] = 0;
|
||||
|
||||
if (key && val) {
|
||||
if (0==strcmp("name", key)) {
|
||||
shout_set_name(shout, val);
|
||||
} else if (0==strcmp("genre", key)) {
|
||||
shout_set_genre(shout, val);
|
||||
} else if (0==strcmp("description", key)) {
|
||||
shout_set_description(shout, val);
|
||||
} else if (0==strcmp("url", key)) {
|
||||
shout_set_url(shout, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state = state_unknown;
|
||||
key = NULL;
|
||||
val = NULL;
|
||||
break;
|
||||
|
||||
case '=':
|
||||
if (state_key==state) {
|
||||
buf[i] = 0;
|
||||
state = state_value;
|
||||
val = &buf[i+1];
|
||||
}
|
||||
break;
|
||||
|
||||
case '#':
|
||||
if (state_unknown==state) {
|
||||
state = state_comment;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (state_unknown==state) {
|
||||
state = state_key;
|
||||
key = &buf[i];
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
close(fh);
|
||||
}
|
||||
|
||||
void
|
||||
sig_handler(int sig)
|
||||
{
|
||||
switch (sig) {
|
||||
case SIGHUP:
|
||||
print_total = 1;
|
||||
break;
|
||||
case SIGTERM:
|
||||
case SIGINT:
|
||||
quit = 1;
|
||||
break;
|
||||
case SIGUSR1:
|
||||
load_metadata();
|
||||
break;
|
||||
default:
|
||||
/* ignore */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
set_argument_string(char **param, char *opt, char optname)
|
||||
{
|
||||
size_t siz;
|
||||
|
||||
if (*param) {
|
||||
fprintf(stderr, "%s: Parameter -%c given multiple times\n",
|
||||
__progname, optname);
|
||||
usage();
|
||||
}
|
||||
|
||||
siz = strlen(opt) + 1;
|
||||
if (siz >= MAXPATHLEN) {
|
||||
fprintf(stderr, "%s: Argument for parameter -%c too long\n",
|
||||
__progname, optname);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ((*param = malloc(siz)) == NULL) {
|
||||
fprintf(stderr, "%s: %s\n", __progname, strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
snprintf(*param, siz, "%s", opt);
|
||||
}
|
||||
|
||||
#ifdef SHOUT_TLS
|
||||
void
|
||||
set_tls_mode(int *tls_mode, char *opt, char optname)
|
||||
{
|
||||
if (0==strcasecmp("DISABLED", opt)) {
|
||||
*tls_mode = SHOUT_TLS_DISABLED;
|
||||
} else if (0==strcasecmp("AUTO", opt)) {
|
||||
*tls_mode = SHOUT_TLS_AUTO;
|
||||
} else if (0==strcasecmp("AUTO_NO_PLAIN", opt)) {
|
||||
*tls_mode = SHOUT_TLS_AUTO_NO_PLAIN;
|
||||
} else if (0==strcasecmp("RFC2818", opt)) {
|
||||
*tls_mode = SHOUT_TLS_RFC2818;
|
||||
} else if (0==strcasecmp("RFC2817", opt)) {
|
||||
*tls_mode = SHOUT_TLS_RFC2817;
|
||||
} else {
|
||||
fprintf(stderr, "%s: Invalid value for -%c.\n",
|
||||
__progname, optname);
|
||||
usage();
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
unsigned char buff[BUFFERSIZE];
|
||||
int ret, ch;
|
||||
unsigned int pFlag;
|
||||
char *description, *genre, *name, *url;
|
||||
size_t bytes_read = 0;
|
||||
unsigned short port;
|
||||
unsigned long long total;
|
||||
#ifdef SHOUT_TLS
|
||||
int tls_mode = SHOUT_TLS_AUTO;
|
||||
#endif
|
||||
|
||||
pFlag = 0;
|
||||
description = genre = name = url = metafilename = NULL;
|
||||
while ((ch = getopt(argc, argv, "d:g:hn:m:pu:T:")) != -1) {
|
||||
switch (ch) {
|
||||
case 'd':
|
||||
set_argument_string(&description, optarg, 'D');
|
||||
break;
|
||||
case 'g':
|
||||
set_argument_string(&genre, optarg, 'g');
|
||||
break;
|
||||
case 'n':
|
||||
set_argument_string(&name, optarg, 'n');
|
||||
break;
|
||||
case 'm':
|
||||
set_argument_string(&metafilename, optarg, 'm');
|
||||
break;
|
||||
case 'p':
|
||||
pFlag = 1;
|
||||
break;
|
||||
case 'u':
|
||||
set_argument_string(&url, optarg, 'u');
|
||||
break;
|
||||
case 'T':
|
||||
#ifdef SHOUT_TLS
|
||||
set_tls_mode(&tls_mode, optarg, 'T');
|
||||
break;
|
||||
#endif
|
||||
case 'h':
|
||||
default:
|
||||
usage();
|
||||
}
|
||||
}
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
|
||||
if (argc != 4) {
|
||||
fprintf(stderr, "%s: Wrong number of arguments\n", __progname);
|
||||
usage();
|
||||
}
|
||||
|
||||
if ((shout = shout_new()) == NULL) {
|
||||
fprintf(stderr, "%s: Could not allocate shout_t\n",
|
||||
__progname);
|
||||
return (1);
|
||||
}
|
||||
|
||||
shout_set_format(shout, SHOUT_FORMAT_OGG);
|
||||
|
||||
#ifdef SHOUT_TLS
|
||||
if (shout_set_tls(shout, tls_mode) != SHOUTERR_SUCCESS) {
|
||||
fprintf(stderr, "%s: Error setting TLS mode: %s\n", __progname,
|
||||
shout_get_error(shout));
|
||||
return (1);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (shout_set_host(shout, argv[0]) != SHOUTERR_SUCCESS) {
|
||||
fprintf(stderr, "%s: Error setting hostname: %s\n", __progname,
|
||||
shout_get_error(shout));
|
||||
return (1);
|
||||
}
|
||||
|
||||
if (sscanf(argv[1], "%hu", &port) != 1) {
|
||||
fprintf(stderr, "Invalid port `%s'\n", argv[1]);
|
||||
usage();
|
||||
}
|
||||
if (shout_set_port(shout, port) != SHOUTERR_SUCCESS) {
|
||||
fprintf(stderr, "%s: Error setting port: %s\n", __progname,
|
||||
shout_get_error(shout));
|
||||
return (1);
|
||||
}
|
||||
|
||||
if (shout_set_password(shout, argv[2]) != SHOUTERR_SUCCESS) {
|
||||
fprintf(stderr, "%s: Error setting password: %s\n", __progname,
|
||||
shout_get_error(shout));
|
||||
return (1);
|
||||
}
|
||||
|
||||
if (shout_set_mount(shout, argv[3]) != SHOUTERR_SUCCESS) {
|
||||
fprintf(stderr, "%s: Error setting mount: %s\n", __progname,
|
||||
shout_get_error(shout));
|
||||
return (1);
|
||||
}
|
||||
|
||||
shout_set_public(shout, pFlag);
|
||||
|
||||
if (metafilename)
|
||||
load_metadata();
|
||||
|
||||
if (description)
|
||||
shout_set_description(shout, description);
|
||||
|
||||
if (genre)
|
||||
shout_set_genre(shout, genre);
|
||||
|
||||
if (name)
|
||||
shout_set_name(shout, name);
|
||||
|
||||
if (url)
|
||||
shout_set_url(shout, url);
|
||||
|
||||
signal(SIGUSR1, sig_handler);
|
||||
|
||||
//wait for data before opening connection to server
|
||||
bytes_read = fread(buff, 1, sizeof(buff), stdin);
|
||||
|
||||
if (shout_open(shout) == SHOUTERR_SUCCESS) {
|
||||
printf("%s: Connected to server\n", __progname);
|
||||
total = 0;
|
||||
|
||||
signal(SIGHUP, sig_handler);
|
||||
signal(SIGTERM, sig_handler);
|
||||
signal(SIGINT, sig_handler);
|
||||
|
||||
while (quit == 0) {
|
||||
total += bytes_read;
|
||||
|
||||
if (bytes_read > 0) {
|
||||
ret = shout_send(shout, buff, bytes_read);
|
||||
if (ret != SHOUTERR_SUCCESS) {
|
||||
printf("%s: Send error: %s\n",
|
||||
__progname,
|
||||
shout_get_error(shout));
|
||||
quit = 1;
|
||||
}
|
||||
} else
|
||||
quit = 1;
|
||||
|
||||
if (quit) {
|
||||
printf("%s: Quitting ...\n", __progname);
|
||||
print_total = 1;
|
||||
}
|
||||
|
||||
if (print_total) {
|
||||
printf("%s: Total bytes read: %llu\n",
|
||||
__progname, total);
|
||||
print_total = 0;
|
||||
}
|
||||
|
||||
shout_sync(shout);
|
||||
|
||||
bytes_read = fread(buff, 1, sizeof(buff), stdin);
|
||||
if (bytes_read != sizeof(buff) && feof(stdin)) {
|
||||
quit = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "%s: Error connecting: %s\n", __progname,
|
||||
shout_get_error(shout));
|
||||
return (1);
|
||||
}
|
||||
|
||||
shout_close(shout);
|
||||
|
||||
return (0);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package dmr
|
||||
|
||||
// Repeater implements a repeater station.
|
||||
type Repeater interface {
|
||||
Active() bool
|
||||
Close() error
|
||||
ListenAndServe() error
|
||||
}
|
@ -1 +0,0 @@
|
||||
cmd/dmrstream/stream.yaml
|
@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# sox converts big endian float32 to signed pcm wave
|
||||
|
||||
sox --endian big -t f32 - -t wav - | lame -b 96 - -
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# sox converts big endian float32 to signed pcm wave
|
||||
|
||||
sox --endian big -t f32 - -t wav - \
|
||||
| oggenc --quiet --skeleton -q 1 -
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,12 @@
|
||||
package dmr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "0.2.1" // Version number
|
||||
SoftwareID = fmt.Sprintf("%s go-dmr %s", Version, runtime.GOOS) // Software identifier
|
||||
PackageID = fmt.Sprintf("%s/%s", SoftwareID, runtime.GOARCH) // Package identifier
|
||||
)
|
Loading…
Reference in New Issue