You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

396 lines
8.7 KiB
Go

// Package homebrew implements the Home Brew DMR IPSC protocol
package homebrew
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"log"
"net"
"runtime"
"strings"
"sync/atomic"
"time"
"github.com/pd0mz/go-dmr/ipsc"
)
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)
)
// 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
RepeaterID uint32
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.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
}
// 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
// CallType reflects the DMR data frame call type.
type CallType byte
const (
GroupCall CallType = iota
UnitCall
)
// StreamFunc is called by the DMR repeater when a DMR data frame is received.
type StreamFunc func(*ipsc.Packet)
type authStatus byte
const (
authNone authStatus = iota
authBegin
authDone
authFail
)
type Network struct {
AuthKey string
Local string
LocalID uint32
Master string
MasterID uint32
}
type packet struct {
addr *net.UDPAddr
data []byte
}
type Link struct {
Dump bool
config ConfigFunc
stream StreamFunc
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
}
}
}
// New starts a new DMR repeater using the Home Brew protocol.
func New(network *Network, cf ConfigFunc, sf StreamFunc) (*Link, error) {
if cf == nil {
return nil, errors.New("config func can't be nil")
}
link := &Link{
network: network,
config: cf,
stream: sf,
}
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
}
// Run starts the datagram receiver and logs the repeater in with the master.
func (l *Link) Run() error {
var err error
if l.conn, err = net.ListenUDP("udp", l.local.addr); err != nil {
return err
}
queue := make(chan packet)
go l.login()
go l.parse(queue)
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
}
queue <- packet{peer, data[:n]}
}
return nil
}
// Send data to an UDP address using the repeater datagram socket.
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()
if l.Dump {
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(queue <-chan packet) {
for {
select {
case p := <-queue:
size := len(p.data)
if size < 4 {
continue
}
switch l.master.status {
case authNone:
if bytes.Equal(p.data[:4], DMRData) {
return
}
if size < 14 {
return
}
packet := p.data[:6]
repeater, err := hex.DecodeString(string(p.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 = p.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(p.data[:4], DMRData) {
return
}
if size < 14 {
log.Println("dmr/homebrew: unexpected login reply from master")
l.master.status = authFail
break
}
packet := p.data[:6]
repeater, err := hex.DecodeString(string(p.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:
if l.Dump {
fmt.Printf("received from %s:\n", p.addr)
fmt.Print(hex.Dump(p.data))
}
switch {
case bytes.Equal(p.data[:4], DMRData):
l.parseDMR(p.data)
}
}
}
}
}
func (l *Link) parseDMR(d []byte) {
// If we have no stream callback, don't even bother to decode the DMR data frame.
if l.stream == nil {
return
}
var (
p *ipsc.Packet
err error
)
if p, err = ParseData(d); err != nil {
log.Printf("dmr/homebrew: error parsing DMRD frame: %v\n", err)
return
}
l.stream(p)
}