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
|
# go-dmr
|
||||||
Golang Digital Mobile Radio protocols
|
|
||||||
|
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) {
|
func TestBit(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
Test []byte
|
Test []byte
|
||||||
Want Bits
|
Want []byte
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
[]byte{0x2a},
|
[]byte{0x2a},
|
||||||
Bits{0, 0, 1, 0, 1, 0, 1, 0},
|
[]byte{0, 0, 1, 0, 1, 0, 1, 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[]byte{0xbe, 0xef},
|
[]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 {
|
for _, test := range tests {
|
||||||
got := NewBits(test.Test)
|
got := BytesToBits(test.Test)
|
||||||
if len(got) != len(test.Want) {
|
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 {
|
for i, b := range got {
|
||||||
if b != test.Want[i] {
|
if b != test.Want[i] {
|
||||||
t.Fatalf("bit %d is off: %v != %v", i, got, test.Want)
|
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