Added homebrew protocol
parent
07cd122018
commit
f5e7f26850
@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"pd0mz/dmr/homebrew"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func rc() *homebrew.RepeaterConfiguration {
|
||||
return &homebrew.RepeaterConfiguration{
|
||||
Callsign: "PI1BOL",
|
||||
RepeaterID: 2043044,
|
||||
RXFreq: 433787500,
|
||||
TXFreq: 438787500,
|
||||
TXPower: 5,
|
||||
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/pd0mz",
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
callType = map[homebrew.CallType]string{
|
||||
homebrew.GroupCall: "group",
|
||||
homebrew.UnitCall: "unit",
|
||||
}
|
||||
frameType = map[homebrew.FrameType]string{
|
||||
homebrew.Voice: "voice",
|
||||
homebrew.VoiceSync: "voice sync",
|
||||
homebrew.DataSync: "data sync",
|
||||
homebrew.UnusedFrameType: "unused (should not happen)",
|
||||
}
|
||||
)
|
||||
|
||||
func dump(d *homebrew.Data) {
|
||||
fmt.Println("DMR data:")
|
||||
fmt.Printf("\tsequence: %d\n", d.Sequence)
|
||||
fmt.Printf("\ttarget..: %d -> %d\n", d.SrcID, d.DstID)
|
||||
fmt.Printf("\trepeater: %d\n", d.RepeaterID)
|
||||
fmt.Printf("\tslot....: %d\n", d.Slot())
|
||||
fmt.Printf("\tcall....: %s\n", callType[d.CallType()])
|
||||
fmt.Printf("\tframe...: %s\n", frameType[d.FrameType()])
|
||||
switch d.FrameType() {
|
||||
case homebrew.DataSync:
|
||||
fmt.Printf("\tdatatype: %d\n", d.DataType())
|
||||
case homebrew.Voice, homebrew.VoiceSync:
|
||||
fmt.Printf("\tsequence: %c (voice)\n", 'A'+d.DataType())
|
||||
}
|
||||
fmt.Printf("\tdump....:\n")
|
||||
fmt.Println(hex.Dump(d.DMR[:]))
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := flag.String("config", "dmr-homebrew.yaml", "configuration file")
|
||||
flag.Parse()
|
||||
|
||||
f, err := os.Open(*configFile)
|
||||
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)
|
||||
}
|
||||
|
||||
repeater, err := homebrew.New(network, rc, dump)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
repeater.Dump = true
|
||||
panic(repeater.Run())
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
master: brandmeister.pd0zry.ampr.org:62030
|
||||
authkey: secret
|
||||
local: 0.0.0.0:62030
|
||||
localid: 123456
|
@ -0,0 +1,405 @@
|
||||
// Package homebrew implements the Home Brew DMR IPSC protocol
|
||||
package homebrew
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "20151208"
|
||||
SoftwareID = fmt.Sprintf("%s:go-dmr:%s", runtime.GOOS, Version)
|
||||
PackageID = fmt.Sprintf("%s:go-dmr:%s-%s", runtime.GOOS, Version, runtime.GOARCH)
|
||||
)
|
||||
|
||||
type RepeaterConfiguration struct {
|
||||
Callsign string
|
||||
RepeaterID uint32
|
||||
RXFreq uint32
|
||||
TXFreq uint32
|
||||
TXPower uint8
|
||||
ColorCode uint8
|
||||
Latitude float32
|
||||
Longitude float32
|
||||
Height uint16
|
||||
Location string
|
||||
Description string
|
||||
URL string
|
||||
}
|
||||
|
||||
func (r *RepeaterConfiguration) Bytes() []byte {
|
||||
return []byte(r.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.RepeaterID)
|
||||
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", SoftwareID)
|
||||
b += fmt.Sprintf("%-40s", PackageID)
|
||||
return b
|
||||
}
|
||||
|
||||
type configFunc func() *RepeaterConfiguration
|
||||
|
||||
type CallType byte
|
||||
|
||||
const (
|
||||
GroupCall CallType = iota
|
||||
UnitCall
|
||||
)
|
||||
|
||||
type FrameType byte
|
||||
|
||||
const (
|
||||
Voice FrameType = iota
|
||||
VoiceSync
|
||||
DataSync
|
||||
UnusedFrameType
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
Signature [4]byte
|
||||
Sequence byte
|
||||
SrcID uint32
|
||||
DstID uint32
|
||||
RepeaterID uint32
|
||||
Flags byte
|
||||
StreamID uint32
|
||||
DMR [33]byte
|
||||
}
|
||||
|
||||
func (d *Data) CallType() CallType {
|
||||
return CallType((d.Flags >> 1) & 0x01)
|
||||
}
|
||||
|
||||
func (d *Data) DataType() byte {
|
||||
return d.Flags >> 4
|
||||
}
|
||||
|
||||
func (d *Data) FrameType() FrameType {
|
||||
return FrameType((d.Flags >> 2) & 0x03)
|
||||
}
|
||||
|
||||
func (d *Data) Slot() int {
|
||||
return int(d.Flags&0x01) + 1
|
||||
}
|
||||
|
||||
func ParseData(data []byte) (*Data, error) {
|
||||
if len(data) != 53 {
|
||||
return nil, errors.New("invalid packet length")
|
||||
}
|
||||
|
||||
d := &Data{}
|
||||
copy(d.Signature[:], data[:4])
|
||||
d.Sequence = data[4]
|
||||
d.SrcID = binary.BigEndian.Uint32(append([]byte{0x00}, data[5:7]...))
|
||||
d.DstID = binary.BigEndian.Uint32(append([]byte{0x00}, data[8:10]...))
|
||||
d.RepeaterID = binary.BigEndian.Uint32(data[11:15])
|
||||
d.Flags = data[15]
|
||||
d.StreamID = binary.BigEndian.Uint32(data[16:20])
|
||||
copy(d.DMR[:], data[20:])
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
type dataFunc func(*Data)
|
||||
|
||||
type authStatus byte
|
||||
|
||||
const (
|
||||
authNone authStatus = iota
|
||||
authBegin
|
||||
authDone
|
||||
authFail
|
||||
)
|
||||
|
||||
type Network struct {
|
||||
AuthKey string
|
||||
Local string
|
||||
LocalID uint32
|
||||
Master string
|
||||
MasterID uint32
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Dump bool
|
||||
config configFunc
|
||||
stream dataFunc
|
||||
network *Network
|
||||
conn *net.UDPConn
|
||||
authKey []byte
|
||||
local struct {
|
||||
addr *net.UDPAddr
|
||||
id []byte
|
||||
}
|
||||
master struct {
|
||||
addr *net.UDPAddr
|
||||
id []byte
|
||||
status authStatus
|
||||
secret []byte
|
||||
keepalive struct {
|
||||
outstanding uint32
|
||||
sent uint64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func New(network *Network, cf configFunc, df dataFunc) (*Link, error) {
|
||||
if cf == nil {
|
||||
return nil, errors.New("config func can't be nil")
|
||||
}
|
||||
|
||||
link := &Link{
|
||||
network: network,
|
||||
config: cf,
|
||||
stream: df,
|
||||
}
|
||||
|
||||
var err error
|
||||
if strings.HasPrefix(network.AuthKey, "0x") {
|
||||
if link.authKey, err = hex.DecodeString(network.AuthKey[2:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
link.authKey = []byte(network.AuthKey)
|
||||
}
|
||||
if network.Local == "" {
|
||||
network.Local = "0.0.0.0:62030"
|
||||
}
|
||||
if network.LocalID == 0 {
|
||||
return nil, errors.New("missing localid")
|
||||
}
|
||||
link.local.id = []byte(fmt.Sprintf("%08x", network.LocalID))
|
||||
if link.local.addr, err = net.ResolveUDPAddr("udp", network.Local); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if network.Master == "" {
|
||||
return nil, errors.New("no master address configured")
|
||||
}
|
||||
if link.master.addr, err = net.ResolveUDPAddr("udp", network.Master); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (l *Link) Run() error {
|
||||
var err error
|
||||
|
||||
if l.conn, err = net.ListenUDP("udp", l.local.addr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go l.login()
|
||||
|
||||
for {
|
||||
var (
|
||||
n int
|
||||
peer *net.UDPAddr
|
||||
data = make([]byte, 512)
|
||||
)
|
||||
if n, peer, err = l.conn.ReadFromUDP(data); err != nil {
|
||||
log.Printf("dmr/homebrew: error reading from %s: %v\n", peer, err)
|
||||
continue
|
||||
}
|
||||
|
||||
go l.parse(peer, data[:n])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Link) Send(addr *net.UDPAddr, data []byte) error {
|
||||
for len(data) > 0 {
|
||||
n, err := l.conn.WriteToUDP(data, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = data[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Link) login() {
|
||||
var previous = authDone
|
||||
for l.master.status != authFail {
|
||||
var p []byte
|
||||
|
||||
if l.master.status != previous {
|
||||
switch l.master.status {
|
||||
case authNone:
|
||||
log.Printf("dmr/homebrew: logging in as %d\n", l.network.LocalID)
|
||||
p = append(RepeaterLogin, l.local.id...)
|
||||
|
||||
case authBegin:
|
||||
log.Printf("dmr/homebrew: authenticating as %d\n", l.network.LocalID)
|
||||
p = append(RepeaterKey, l.local.id...)
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write(l.master.secret)
|
||||
hash.Write(l.authKey)
|
||||
|
||||
p = append(p, []byte(hex.EncodeToString(hash.Sum(nil)))...)
|
||||
|
||||
case authDone:
|
||||
config := l.config().Bytes()
|
||||
fmt.Printf(hex.Dump(config))
|
||||
log.Printf("dmr/homebrew: logged in, sending %d bytes of repeater configuration\n", len(config))
|
||||
|
||||
if err := l.Send(l.master.addr, config); err != nil {
|
||||
log.Printf("dmr/homebrew: send(%s) failed: %v\n", l.master.addr, err)
|
||||
return
|
||||
}
|
||||
l.keepAlive()
|
||||
return
|
||||
|
||||
case authFail:
|
||||
log.Println("dmr/homebrew: login failed")
|
||||
return
|
||||
}
|
||||
if p != nil {
|
||||
l.Send(l.master.addr, p)
|
||||
}
|
||||
previous = l.master.status
|
||||
} else {
|
||||
log.Println("dmr/homebrew: waiting for master to respond in login sequence...")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) keepAlive() {
|
||||
for {
|
||||
atomic.AddUint32(&l.master.keepalive.outstanding, 1)
|
||||
atomic.AddUint64(&l.master.keepalive.sent, 1)
|
||||
var p = append(MasterPing, l.local.id...)
|
||||
if err := l.Send(l.master.addr, p); err != nil {
|
||||
log.Printf("dmr/homebrew: send(%s) failed: %v\n", l.master.addr, err)
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Link) parse(addr *net.UDPAddr, data []byte) {
|
||||
size := len(data)
|
||||
|
||||
switch l.master.status {
|
||||
case authNone:
|
||||
if bytes.Equal(data, DMRData) {
|
||||
return
|
||||
}
|
||||
if size < 14 {
|
||||
return
|
||||
}
|
||||
packet := data[:6]
|
||||
repeater, err := hex.DecodeString(string(data[6:14]))
|
||||
if err != nil {
|
||||
log.Println("dmr/homebrew: unexpected login reply from master")
|
||||
l.master.status = authFail
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.Equal(packet, MasterNAK):
|
||||
log.Printf("dmr/homebrew: login refused by master %d\n", repeater)
|
||||
l.master.status = authFail
|
||||
break
|
||||
case bytes.Equal(packet, MasterACK):
|
||||
log.Printf("dmr/homebrew: login accepted by master %d\n", repeater)
|
||||
l.master.secret = data[14:]
|
||||
l.master.status = authBegin
|
||||
break
|
||||
default:
|
||||
log.Printf("dmr/homebrew: unexpected login reply from master %d\n", repeater)
|
||||
l.master.status = authFail
|
||||
break
|
||||
}
|
||||
|
||||
case authBegin:
|
||||
if bytes.Equal(data, DMRData) {
|
||||
return
|
||||
}
|
||||
if size < 14 {
|
||||
log.Println("dmr/homebrew: unexpected login reply from master")
|
||||
l.master.status = authFail
|
||||
break
|
||||
}
|
||||
packet := data[:6]
|
||||
repeater, err := hex.DecodeString(string(data[6:14]))
|
||||
if err != nil {
|
||||
log.Println("dmr/homebrew: unexpected login reply from master")
|
||||
l.master.status = authFail
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.Equal(packet, MasterNAK):
|
||||
log.Printf("dmr/homebrew: authentication refused by master %d\n", repeater)
|
||||
l.master.status = authFail
|
||||
break
|
||||
case bytes.Equal(packet, MasterACK):
|
||||
log.Printf("dmr/homebrew: authentication accepted by master %d\n", repeater)
|
||||
l.master.status = authDone
|
||||
break
|
||||
default:
|
||||
log.Printf("dmr/homebrew: unexpected authentication reply from master %d\n", repeater)
|
||||
l.master.status = authFail
|
||||
break
|
||||
}
|
||||
|
||||
case authDone:
|
||||
switch {
|
||||
case bytes.Equal(data, DMRData):
|
||||
if l.stream == nil {
|
||||
return
|
||||
}
|
||||
frame, err := ParseData(data)
|
||||
if err != nil {
|
||||
log.Printf("error parsing DMR data: %v\n", err)
|
||||
return
|
||||
}
|
||||
l.stream(frame)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package homebrew
|
||||
|
||||
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")
|
||||
)
|
Loading…
Reference in New Issue